近日看到JavaScript高级程序设计第三版 7.2,终于解决了对闭包的疑惑。
function func() { var i = 0; return function() { console.log(i++); } } var test = func();//第一次调用 test();//第二次调用 0 test();//第三次调用 1 test();//第四次调用 2 func()();//直接一次调用 0 func()();//直接一次调用 0
上面这个函数可以说是标准的闭包,之前一直疑惑为什么要在定义闭包后调用两次函数。直到今天在chrome调试后才发现:
第一次调用函数时,var test = func() ,只执行了 var i = 0 这句,碰到return function(){}时,将这个return 的函数地址存储赋予test。而return function(){}由于是返回值而不是IIFE,不调用的时,里面的内容都只会声明而不会执行。第二次调用函数时才会执行里面的代码。
而func()() ,这样的直接一次调用会在每次调用的时候重新执行外部函数的代码,也就是 var i = 0,导致每次调用函数,变量都会被重置。
所以 var test = func() ,这步相当于存储了执行后的外部函数,也就是执行一次 var i = 0 。而之后每次调用 test() ,都不会再次执行 var i = 0 ,同时 test() 每次执行时都会调用外部函数 func() 的活动对象 i ,保证了作用域链的链接,所以让 i++ 的结果得到存储。(也就是说,不会重复声明变量的情况下,可以只调用一次)
此外闭包还有一些细节,例1:
function func() { var closure = []; for (var i = 0; i < 10; i++) { closure[i] = function() { console.log(i); } } return closure; } var test = func(); test[5](); //10 test[9](); //10
上面这个闭包会永远返回10。
因为这个闭包中的 i 属于 func 函数的作用域里的变量,而一个作用域中的变量,同时只能拥有一个值。同时 for 循环中的 closure 只是声明,而没有调用。最后 return closure的时候, i 经过循环变为了10。因为作用域的变量只能拥有一个值,这时候就算是 test[0] 返回的也只是 func 作用域里的最后一个 i, 也就是 10。
如果要存储这个for循环里面的所有i,可以使用下面这个方法:
function func() { var arr = []; for (var i = 0; i < 10; i++) { arr[i] = function(num) { return function(){ return console.log(num); } }(i); } return arr; } var test = func(); test[1](); //1 test[5](); //5
因为变量 i 传参给了 function(num){}(i) 这个函数,函数变量是按值传递的,每个i都会复制一次给num,由不同的地址存储起来。而num的作用域在匿名函数里面,所以不存在调用外层函数的活动对象,也就是保存着每一次的值。同时这是个匿名函数,会立即执行。执行时,而函数里面则又是一个闭包,函数内部的代码 return num 也就是对应的 i 给了 对应的arr[i]。
如果将例1改成下面的样子
function func() { var closure; for (var i = 0; i < 10; i++) { (function(j){ closure = function() { console.log(j); } })(i) } return closure; } var test = func(); test(); //9 test(); //9
结果怎么调用都是9,也就是最后 i = 9; 而 i++ 没有执行。都是 9 是因为每次循环都重新给 closure 赋值,所以保留了最后的值。
for里面是个IIFE,接受参数 j ,由外部作用域的变量 i 传递。 j 属于匿名函数的变量(匿名函数作用域包裹 j ),匿名函数模拟了块级作用域,每次循环重新生成一个作用域,里面的变量 j 随着作用域的不同,存储的地址也随之改变(当然最后的值都是9)。同时也不需要调用外层的活动对象,函数的参数是按值传递,每次传递给 j 都是值而不是地址,也不需要调用外层的活动对象。
把例1的var i = 0;改成 let i = 0; (使用块级作用域)也能达到同样的效果,避免了引用外层函数的活动对象,循环时,每个成员 i 存储在一个独立的作用域。
如果将例1的函数表达式:var closure = function(){} 替换成 return function(){} 。即下面这个形式:
function func() { var i; for (i = 0; i < 10; i++) { return function() { console.log(i); } } } var test = func(); test();
这个例子无论多少次调用,结果都是 i = 0 。虽然是闭包,但是函数在执行 i++ 这一步的时候就已经return function(){} 了。所以调用多少次都永远是 i = 0 。所以这个函数等价于下面这个函数:
function func() { var i = 0; return function() { console.log(i); } } var test = func(); test();
而这个下面这个函数虽然也是匿名函数闭包返回num。但由于不是数组,第一次调用时 closure 这个函数表达式for循环给 closure 赋值,而这个值是一个函数(实际上是一个指针,在第二次调用时才会执行里面的代码),最后一次赋值时 num = i = 9 ,之后由于 i++ === 10 所以跳出了循环 , 所以无论调用多少次,都不会再次执行for语句(num一直是9)。
function func() { for (var i = 0; i < 10; i++) { var closure = function(num) { return function(){ return num; } }(i); } return closure; } var test = func(); test(); //9
下面这个例子用变量a存储起num的值,可以直观看出每次调用 test() 时,num 都是9。
function func() { var a = 0; for (var i = 0; i < 10; i++) { var closure = function(num) { return function(){ a += num; return console.log(a); } }(i); } return closure; } var test = func(); test(); //9 test(); //18 test(); //27
可以发现每次调用 test() 时,a的值都是9的倍数,证明每次调用 closure 里面的匿名函数时,返回的值都是9。
而下面这个例子可以看出for循环只执行了一次(也就是 i 只进行了一轮赋值。调用函数 test() 的时候,闭包调用了变量对象,也就是 i 的最后一次赋值 9 )
function func() { var i; for (i = 0; i < 10; i++) { var closure = function(num) { return function(){ return console.log(num++); } }(i); } return closure; } var test = func(); test(); //9 test(); //10 test(); //11
证明了for循环只进行了一次,所以在不使用运算符进行操作的时候, i 永远都是9。
而下面这种方式则是错误的
function func() { var i; var arr = []; for (i = 0; i < 10; i++) { arr = function(num) { return console.log(num); }(i); } return arr; } var test = func();//一次调用 等价于不用 var test 直接使用 func();
上面这个函数由于只返回并调用了一次,看似是是闭包,实际上没闭包的效果,等价于下面的函数:
function func() { var i; for (i = 0; i < 10; i++) { console.log(i); } } func();//一次调用
实际上只是直接调用 func() 函数里面的代码而已,由于没有作用域链链接外部函数作用域,所以无论调用多少次 func() 都只会循环打印出 1-9。
关于闭包中的this指向,首先一点,this永远指向函数被调时的对象,而不是定义的对象。
var name = 'the window'; var object = { name: "my object", method: function(){ return function(){ return this.name; }; } } console.log(object.method()()); //the window
解释1:object.method()() 实际上等于 (object.method())() 。也就是说object.method()虽然是对象方法(用hasOwnPrototypeProperty检测method可以知道)同时object.method()里的this指向object,但是 (object.method())() 则是一个普通函数。所以this指向的是全局对象window。
解释2:由于闭包只能获取外部函数里面的活动对象,也就是外部函数里的 this, arguments 以及声明的变量。而调用object对象不会生成活动对象,所以闭包在查找外部活动对象时,跳过了object对象,导致this没有查找到object.name 而是window.name。
像下面例子可以正常调用object对象里的this值。
var name = 'the window'; var object = { name: "my object", method: function(){ var that = this; return function(){ return that.name; }; } } console.log(object.method()()); //my object
那样先将method方法执行时的this地址赋值给变量 that ,由于闭包会查找外部变量的活动变量,所以that指向了object里的this地址,所以输出my object。
调用都是 9 是因为每次循环都覆盖了原本的值。