关于闭包的见解
近日看到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 是因为每次循环都覆盖了原本的值。

浙公网安备 33010602011771号