关于闭包的见解

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

原文地址:https://www.cnblogs.com/NKnife/p/6179667.html