百度IFE之Task3学习笔记(JavaScript深度学习)

08 May 2015

一、JavaScript 作用域

Remember:

JS中一切皆是对象。(这句话出现频率好高)
JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里。

关于作用域具体需要了解:

1 . 什么是作用域 ?
作用域是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。在JavaScript中,变量的作用域有全局作用域和局部作用域两种。
(1)全局作用域
在代码中任何地方都能访问到的对象拥有全局作用域。
包括:

  • 最外层函数和在最外层函数外面定义的变量拥有全局作用域;
  • 所有未定义直接赋值的变量自动声明为拥有全局作用域;
  • 所有window对象的属性拥有全局作用域。

(2)局部作用域
局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部,所有在一些地方也会看到有人把这种作用域称为函数作用域。

🔥定义局部变量的时候记得加上 var

2 . 作用域链
函数对象和其它对象一样,拥有可以通过代码访问的属性和一系列仅供JavaScript引擎访问的内部属性。其中一个内部属性是[[Scope]],由ECMA-262标准第三版定义,该内部属性包含了函数被创建的作用域中对象的集合,这个集合被称为函数的作用域链,它决定了哪些数据能被函数访问。

【关于作用域链 看下面两个小栗子比较🌰】

// 小栗子A
var a = "hi";
function test() {
    console.log(a);//👀这里的运行结果是hi,毫无疑问。
}
test();

// 小栗子B
var a = "hi";
function test() {
    console.log(a);//👀注意这里的结果是undefined而不是hi 
    var a = "hello";
    console.log(a);//hello
    console.log(b);//脚本错误
}
test();

【为什么小栗子🌰B中a的输出结果不是 "hi" 呢?】
因为作用域链的问题,a的作用域链可以理解为window--test--a,虽然在window定义了全局变量a,但是在test函数中定义了局部变量a,在作用域链上优先于全局变量,所以会先找到test函数中的a,而小栗子🌰A中由于test函数中找不到a,所以会沿着作用域链往上找,访问到window里的全局变量a。

【那么为什么小栗子🌰B中a的输出结果也不是 "hello" 呢?】
因为在console的时候,a只是定义了,还未被赋值,赋值在下一句。
🔥 var a = "hello",这句声明包括定义和赋值两个步骤,实际上等价于 var a ; a = "hello";
另外,在浏览器的眼中,它会先看到所有的定义。比如下面的代码:

//我们的视角
function fn() {
    var a = 0;
    var b = 1;
    var c = 2;
}
//浏览器的视角
function fn() {
    var a;
    var b;
    var c;
    a = 0;
    b = 1;
    c = 2;
}

【参考资料】

鸟哥:Javascript作用域原理

JavaScript 开发进阶:理解 JavaScript 作用域和作用域链

二、JavaScript 原型

什么是原型?

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个对象,它的用途是包含可以由特定类型的所有实例共享的属性和方法。

通过原型的方法创建对象(对比构造函数)

🔥【创建对象——构造函数VS原型】

//🌲构造函数
//构造函数第一个字母大写(为了区分,但非强制)
//构造函数和普通函数的唯一区别,就是他们调用的方式不同。  
//构造函数也是函数,必须用new运算符来调用,否则就是普通函数。
function Box(name,age) {   //创建对象
    this.name = name; //添加属性(实例属性)
    this.age = age;
    this.run = function() {
        return this.name + this.age + '运行中...';//添加方法(实例方法)
    };  
}
var box1 = new Box('Lee',100);//实例化
var box2 = new Box('Jack',200);//实例化
//console.log(box1.run());//Lee100运行中
//console.log(box2.run());//Jack200运行中
var o = new Object();
Box.call(o,'Allen','30');//通过【对象冒充】也可以使用Box里的属性和方法
//console.log(o.run());//Allen30运行中

//🌲原型
function Box() {};
Box.prototype.name = "Lee";//原型属性
Box.prototype.age = 100;
Box.prototype.run = function() { //原型方法
    return this.name + this.age + '运行中...';
};

var box1 = new Box();
//console.log(box1.run());//Lee100运行中

【区别】
如果是构造函数(实例方法),不同的实例化,方法地址是不一样的,是唯一的; 如果是原型方法,那么方法地址是共享的。可以验证一下:

//针对上面的构造函数
var box1 = new Box('David',10);
var box2 = new Box('David',10);
console.log(box1.run() == box2.run());//true 显然二者的值相同
console.log(box1.run == box2.run);//false 但是可以验证二者的方法地址其实不同

//针对上面的原型方法
var box1 = new Box();
var box2 = new Box();
console.log(box1.run() == box2.run());//true 二者的值相同
console.log(box1.run == box2.run);//true 可以验证二者的方法地址也相同

🔥二者的区别可以通过图来更好的比较:

【构造函数方式】
在内存中会分配两个空间分别给两个实例box1和box2,二者彼此独立,因此地址是不共享的。

【原型模式方式】 在原型模式声明中,多了两个属性,这两个属性都是创建对象时自动生成的。__proto__ 属性是实例指向原型对象的一个指针,它的作用就是指向构造函数的原型属性constructor。 通过这两个属性,就可以访问到原型里的属性和方法了。

具体看下代码:

//还是刚才的原型方法栗子
function Box() {};
Box.prototype.name = "Lee";//原型属性
Box.prototype.age = 100;
Box.prototype.run = function() { //原型方法
    return this.name + this.age + '运行中...';
};

var box1 = new Box();
//看下面五个写法的运行结果
(1)console.log(box1.prototype);//undefined 
(使用对象实例无法直接访问prototype,需要用__proto__指针)

(2)console.log(Box.prototype);//{ name: 'Lee', age: 100, run: [Function] }
(使用构造函数名(对象名)可以访问prototype,运行结果和(3)一样)

(3)console.log(box1.__proto__);//{ name: 'Lee', age: 100, run: [Function] }
(这个属性是一个指针,指向prototype原型对象,但是IE浏览器不支持)

(4)console.log(box1.constructor);//[Function: Box]
(构造属性,可以获取构造函数本身)

(5)console.log(Box.prototype.constructor);//[Function: Box]
(获取构造函数,运行结果和上面的(4)一样)

进一步了解原型

🌲判断一个对象是否指向了该构造函数的原型对象,可以使用isPrototypeOf()方法来测试。

//继续刚才的栗子
console.log(Box.prototype.isPrototypeOf(box1)); 
//只要实例化对象,即都会指向

🌲【原型模式的执行流程】
1.先查找构造函数实例里的属性或方法,如果有,立刻返回;
2.如果构造函数实例里没有,则去它的原型对象里找,如果有,就返回;
虽然我们可以通过对象实例访问保存在原型中的值,但却不能访问通过对象实例重写原 型中的值。
看下面的小栗子🌰

varbox1=newBox();
console.log(box1.name); //Lee,原型里的值
box1.name='Jack';
console.log(box.1name); //Jack,就近原则
varbox2=newBox();
alert(box2.name); //Lee,原型里的值,没有被box1修改

//如果想要box1也能在后面继续访问到原型里的值,可以把构造函数里的属性删除即可
deletebox1.name; //删除属性
console.log(box1.name);//Lee 因为实例中没有,则找到原型对象中的值

🌲hasOwnProperty() 函数可用来【判断属性是否在实例中】。

console.log(box1.hasOwnProperty('name')); 
//实例里有返回true,否则返回false

in操作符 可用来【判断属性是否存在实例或原型中】。

alert('name'in box1); //true,实例中或原型中只要一方存在,即返回true;

🔥将上面两种方法结合起来可以【判断只有原型中有属性】,做法如下:

function isProperty(object,property) {
    return !object.hasOwnProperty(property)&&(properety in object);
}
//运用一下
console.log(isProperty(box1,'name');//true 只有原型中有

🌲原型对象不仅仅可以在自定义对象的情况下使用,而ECMAScript内置的引用类型都可以使用这种方式,并且内置的引用类型本身也使用了原型。

console.log(Array.prototype.sort); //sort可用于数组排序
//sort就是Array类型的原型方法

console.log(String.prototype.substring); 
//substring就是String类型的原型方法

String.prototype.addstring=function(){ //给String类型添加一个方法
    returnthis+',被添加了!'; //this代表调用的字符串
};
console.log('Lee'.addstring()); //使用这个方法

PS:尽管给原生的内置引用类型添加方法使用起来特别方便,但不推荐使用这种 方法。因为它可能会导致命名冲突,不利于代码维护。

字面量方式

为了让属性和方法更好的体现封装的效果,并且减少不必要的输入,原型的创建可以使用字面量的方式:

function Box(){};
Box.prototype={ //使用字面量的方式
    name:'Lee',
    age:100,
    run:function(){
        returnthis.name+this.age+'运行中...';
    }
};
var box = new Box();
console.log(box.constructor);//[Function: Object]
为什么这里的结果不是Box呢?

🔥注意:使用构造函数创建原型对象和使用字面量创建对象在使用上基本相同,但还是有一些区别,字面量创建的方式使用constructor属性不会指向实例,而会指向Object,构造函数创建的方式则相反。

如果想让字面量方式的constructor指向实例对象,那么可以这么做:

Box.prototype={
    constructor:Box, //直接强制指向即可
   (在上一个栗子中,只添加上面一句,其他的保持不变,此处省略)
};

PS:字面量方式为什么constructor会指向Object?因为Box.prototype={};这种写法其实就是创建了一个新对象。而每创建一个函数,就会同时创建它prototype,这个对象也会自动获取constructor属性。所以,新对象的constructor重写了Box原来的constructor,因此会指向新对象,那个新对象没有指定构造函数,那么就默认为Object。
🔥原型的声明是有先后顺序的,所以,重写的原型会切断之前的原型。

//比如继续上面的栗子,重写原型
Box.prototype = {
    age:200 //这里不会保留之前的原型的任何信息
    把原来的原型对象和构造函数实例之间的关系切断了
}
console.log(box.run())//运行出错,因为run已经被覆盖掉了

原型的缺点

原型的最大缺点其实就是它的最大优点——共享。 具体而言,就是无法传参,引用类型在第一个实例修改后,保持了共享。

function Box(){};
Box.prototype={ //使用字面量的方式
    constructor:Box,
    name:'Lee',
    age:100,
    number:[1,2,3],
    run:function(){
        returnthis.name+this.age+this.number;
    }   
};
var box1 = new Box();
var box2 = new Box();
//第一种情况
box1.number=[1,2];
console.log(box1.number);//[1,2]
console.log(box2.number);//[1,2,3]
//第二种情况(先把第一种情况注释掉)
box1.number.push(4);
console.log(box1.number);//[1,2,3,4]
console.log(box2.number);//[1,2,3,4]
共享了box1添加后的引用类型的原型

PS:数据共享的缘故,导致很多开发者放弃使用原型,因为每次实例化出的数据需要 保留自己的特性,而不能共享。

组合构造函数+原型模式

为了解决构造传参和共享问题,可以组合构造函数+原型模式:

functionBox(name,age){ //不共享的使用构造函数
    this.name=name;
    this.age=age;
    this.number=[1,2,3];
};
Box.prototype={  //共享的使用原型模式
    constructor:Box,
    run:function(){
        returnthis.name+this.age+this.number;
    }
};
var box1 = new Box('Lee',100);
var box2 = new Box('Jack',200);

这种混合模式很好的解决了传参和引用共享的大难题。是创建对象比较好的方法。

动态原型模式

原型模式,不管你是否调用了原型中的共享方法,它都会初始化原型中的方法,并且在声明一个对象时,构造函数+原型部分让人感觉又很怪异,最好就是把构造函数和原型封装到一起。为了解决这个问题,我们可以使用动态原型模式。

functionBox(name,age){ //将所有信息封装到函数体内
    this.name=name;
    this.age=age;
    if(typeof this.run!='function') { //仅在第一次调用的初始化
        Box.prototype.run=function(){
            returnthis.name+this.age+'运行中...';
        };
    }
}
var box=new Box('Lee',100);
console.log(box.run());
//当第一次调用构造函数时,run()方法发现不存在,然后初始化原型。
//当第二次调用,就不会初始化,并且第二次创建新对象,原型也不会再初始化了。
//这样既得到了封装,又实现了原型方法共享,并且属性都保持独立。

原型链和继承

ECMAScript实现继承的方式依靠原型链完成。

function Box(){ // 被继承的函数叫做超类型(父类、基类)
    this.name='Lee';
}
function Desk(){ // 继承的函数叫做子类型(子类、派生类)
    this.age=100;
}
//通过原型链继承 (超类型实例化后的对象实例赋值给子类型的原型属性)
//new Box()会将Box构造里的信息和原型的信息都交给Desk
即:Desk的原型得到Box的构造+原型里的信息

Desk.prototype=new Box(); //Desc继承了Box,通过原型,形成链条

var desk=new Desk();
console.log(desk.age);
console.log(desk.name); //得到被继承的属性

function Table(){
    this.level='AAAAA';
}
Table.prototype=new Desk(); //继续原型链继承

var table=new Table();
console.log(table.name); //继承了Box和Desk

🔥原型链继承中的就近原则问题

function Box() { 
    this.name='Lee';
}
Box.prototype.name = 'Jack';
function Desk() {
    this.age = 100;
}
Desk.prototype = new Box();
var desk = new Desk();
console.log(desk.name);//Lee  就近原则 获取实例中的值

参考资料

原型这块的内容,主要学习了【瓢城Web俱乐部】上JS部分第十五章的视频,面向对象与原型一共七小节,讲解的还是很详细。
其他资料: 强大的原型和原型链理解JavaScript原型js原型链原理看图说明