关于闭包
书中对闭包的定义:闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。在内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。
前面说到,局部变量当函数运行完以后就会销毁这个变量,假如有多次调用这个函数它下一次调用的时候又会重新创建那个变量。例如:
function fn(){
var num = 5;
num+=1;
console.log(num);
}
fn(); //6
fn(); //6
两次调用fn(),每次都输出6,说明每次执行都是重新创建变量num并初始化的。当函数执行完毕的时候num就会被销毁。
那么,有没有方法可以让num不被销毁,并一直递增下去呢?闭包这个时候就派上用场了。
function a(){
var num = 5;
function b(){
num+=1;
console.log(num);
}
return b;
}
var c = a();
c(); //6
c(); //7
这里在函数a()的内部定义了一个函数b(),并return了回去,同时在函数a的外部定义了一个变量c并引用了函数a。这时候发现第二次执行c()的时候输出为7,这说明在执行过一次的时候,变量num没有被销毁。JavaScript中有回收机制,函数在被执行完以后这个函数的作用域就会被销毁,如果一个函数被其他变量引用,这个函数的作用域将不会被销毁。因为num被外部变量c所引用,所以没有被回收。
在Chrome浏览器中添加断点调试,一步一步往下执行,可以看到call stack(函数调用栈)
与 scope(作用域链)
的变化,以及函数执行位置的变化:
将函数a()赋给了全局变量c,而此时还没有调用a(),可以看到此时的作用域链只有一个Global。
通过执行c()语句开始调用a(),接着a()内部开始执行b(),可以看到,当前的作用域链:b()的活动对象(this:window)、a()的活动对象num以及全局活动对象(Global)。由于b()中引用了a()中定义的变量num,a()因此成为闭包。
继续往下执行,此时num增加了1
第二次执行c()过程中发现num的值为6而不是5,这说明第一次num加1之后并没有被销毁,因为它一直在被b()引用。
关于原型(prototype)
每创建一个函数,就会为该函数创建一个prototype
属性,这个属性指向函数的原型对象,而原型对象中会自动获得一个constructor(构造函数)
属性,这个属性包含一个指向prototype
属性所在函数的指针。当调用构造函数创建一个新实例后,该实例内部将包含一个指针[[prototype]](内部属性)
,指向构造函数的原型对象
。这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。简单讲就是:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
function Person(){ }
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
console.log(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true
下面的图片中展示了上述代码中各个对象间的关系:
原型具有动态性:可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来。比如,给上面的例子Person
类型的原型添加一个方法sayHi()
:
Person.prototype.sayHi = function(){
console.log("hi!");
}
person1.sayHi(); //"hi!"
可以看到,person1
实例是在之前定义的,后面又给Person
类型的原型添加一个方法sayHi()
,这时候person1
同样能够访问到该方法。这就是JavaScript中的原型搜索机制
:当以读取模式访问一个实例属性时,首先会在实例中搜索该属性。如果没有找到该属性,则会继续搜索实例的原型。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。
注意
:虽然可以为原型动态添加属性和方法,但如果重写了整个原型,在之前原型基础上创建的实例对象再调用原型中的方法时就会产生错误,因为重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。
原型链
原型链就是让原型对象等于另一个类型的实例,该实例也会有一个原型对象,而该实例的原型对象则可以又是另外一个类型的实例,如此层层递进就会形成原型链。例如:
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
//继承了 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true
在上面的例子中定义了两个类型,分别是SuperType和SubType。两个类型都包含一个属性和一个方法;同时,SubType类型的原型又是SuperType类型的一个实例,这样SubType类型的原型就会拥有SuperType类型中所有的属性和方法。也就是说,SubType类型的原型中包含了property、getSuperValue()和getSubValue()一个属性两个方法。因此通过原型链可以实现继承。
下图中上述代码例子中的实例以及构造函数和原型之间的关系: