7.2 闭包
闭包是指有权访问另一个函数作用域中的变量的函数。
7.2.1 闭包与变量
作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数(外部函数)中任何变量的最后一个值
function createFunctions () {
var result = new Array(),i;
for (i = 0; i < 10; i++) { // 这里的i只有一个值
result[i] = function () {
return i;
}
}
return result;
}
/**
var i = 0;
function demo1() {
return i;
}
i = 2;
function demo2() {
return i;
}
console.log(demo1()); // 2
console.log(demo2()); // 2
**/
var arr = createFunctions();
for (var j = 0; j < arr.length; j++) {
console.log(arr[j]()); // 全部是10
}
官方解释:因为每个函数的作用域链中都保存这createFuncitons()函数的活动对象,所以它们引用的都是同一个变量i。当createFuncitons()返回后,变量i的值是10,此时每个函数都引用这保存变量i的同一个变量对象,所以在每个函数内部i的值都是10.
个人见解:①数组保存的时候实际发生的的是创建10个函数,创建外部函数和内部的作用域链,当数组被返回后,它的作用域链被初始化为外部
createFunctions()函数的活动对象(result,this,arguments,i)和全局变量对象(window),这样,数组中的函数就可以访问在createFunctions()函数中定义的所有变量。注意的是,createFunctions()在执行完毕后,活动对象不会被立即被销毁,因为数组中函数的作用域链仍然在引用这个活动对象(这里指i),仍然会留在内存中,所以当函数返回后,变量I的值是10;
②而在函数实际调用的时候 ,会为函数创建一个执行环境,构建起执行环境的作用域链,然后创建局部环境的变量对象I,this,arguments,由于每个函数都引用这变量i的同一个变量对象,从作用域链中搜索具有相应名字的变量,在外部函数的活动对象中找到了i,所以每个函数内部i的值都是10
通过创建另一个匿名函数强制让闭包的行为符合预期:
function createFunctions() {
var result = new Array();
for (var i = 0; i < 10; i++) {
/* 这段代码也可以
(function(num){
result[num] = function() {
return num;
};
})(i)
*/
result[i] = function (num) {
return function () {
return num;
};
}(i);
}
return result;
}
var arr = createFunctions();
for (var j = 0; j < arr.length; j++) {
console.log(arr[j]());
}
在调用每个匿名函数时,传入了变量i。由于函数传参是按值传递的,这里传递的是i,属于基本类型的值,被传递的值会被复制给一个局部变量num,就算在匿名函数内部改变num的值,也不会影响外部函数的i变量。而在这个匿名函数内部,又创建并返回了一个访问num的闭包,这样一来,result数组中的每个函数都有自己num变量的一个副本,因此就可以返回各自不同的数值了。
7.2.2 关于this对象
this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过匿名函数的执行环境具有全局性,因此其this对象通常指向window.
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function(){
return function() {
return this.name;
}
}
}
console.log(object.getNameFunc()()); // "The Window"
每个函数在被调用时,其活动对象都会自动取得两个特殊变量:this和arguments.内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因为永远不可能直接访问外部函数中的这两个变量(本身已经有了)。不过,把外部作用域中this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function(){
var that = this;
return function() {
return that.name;
}
}
}
console.log(object.getNameFunc()()); // "My Object"
7.3 模仿块级作用域
function outputNumbers(count){
(function(){
// 这里是块级作用域
for(var i = 0; i < count; i++){
console.log(i);
}
console.log(i);
})();
console.log(i); // 导致一个错误!
}
在匿名函数中定义的任何变量,都会在执行结束时被销毁。因此,变量i只能在匿名函数内访问,使用后即被销毁,而且私有作用域中能够访问变量count,是因为这个匿名函数是一个闭包,它能够访问包含作用域中的所有变量
(function(){
var now = new Date();
if(now.getMonth() == 0 && now.getDate() == 1){
console.log("Hello World");
}
})();
// 这段代码放在全局作用域中,变量now现在是匿名函数的局部变量,不必在全局作用域中创建它;
:::tips 这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。 :::
7.4.2 模块模式
var singleton = function(){
// 私有变量和私有函数
var privateVariable = 10;
function privateFunction() {
return false;
}
// 特权/公有方法和属性
return {
publicProperty: true,
publicMethod: function(){
privateVariable++;
return privateFunction();
}
};
}();
小结:
- 函数表达式不同于函数声明。函数声明要求有名字,但函数表达式不需要。没有名字的函数表达式也叫做匿名函数
- 递归函数应该始终使用arguments.callee来递归调用自身,不要使用函数名—函数名可能会发生变化
当在函数内部定义了其他函数时,并访问了外部函数的变量,就创建了闭包。原理如下: - 在后台执行环境中,闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域。
- 通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。
- 但是,当使用闭包时,这个函数的作用域将会一直在内内中保存到闭包不存在为止。
使用闭包可以在JS中模仿块级作用域 - 创建并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用。
- 结果就是函数内部的所有变量都会被立即销毁—除非将某些变量赋值给了外部作用域中的变量(也就是引用了外部的变量啦)。