从执行上下文深入理解闭包

1.概念

关于闭包的定义我看到过好多个版本,这里简单的列举一下:
MDN:包是函数和声明该函数的词法环境的组合。(PS:个人理解词法环境就是变量对象)
Tyler McGinnis:子函数在其父级函数的变量环境上“关闭”(译者注:原文为a child function “closing” over the variable environment of its parent function)的概念,就叫做闭包。
w3school:闭包,指的是词法表示包括不被计算的变量的函数,也就是说,函数可以使用函数之外定义的变量。
阮一峰:他的理解是,闭包就是能够读取其他函数内部变量的函数。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。
一次性搞懂 JavaScript 闭包 —— 简书:闭包简单来说就是一个函数访问了它的外部变量。

还有《JavaScript高级编程语言》,《JavaScript权威指南》亲有没有发现每个人感觉都给闭包有一个定义,如果你是一个小白你一定和我一样的郁闷( ˇˍˇ )。
最近了解了一下JavaScript执行上下文之后才突然发现这么多概念原来说的其实都是一件事。如果你看了之后和我当初一样,希望我下面的内容可以帮助你,进入正题。

2.执行上下文(或者叫作用域)

执行上下文是用来帮助Javascript引擎管理整个解析和运行代码的复杂过程。那么现在我们了解了执行上下文的存在目的,下一个问题就是执行上下文是怎么创建的?它们由什么组成?

概念:当且仅当Javascript引擎首次开始解析代码(对应全局执行上下文)或当一个函数被调用时,才会创建执行上下文。

全局执行上下文:当Javascript引擎运行代码,第一个被创建的执行上下文叫做“全局执行上下文”。最初,这个全局上下文由这二位组成:一个全局对象和一个this变量。this引用的是全局对象,如果在浏览器中运行Javascript,那么这个全局对象就是window对象,如果在Node环境中运行,这个全局对象就是global对象。 在全局执行上下文的创建阶段,Javascript引擎会:

  1. 创建一个全局对象;
  2. 创建this对象,指向window;
  3. 给函数分配内存;
  4. 给变量分配内存;
  5. 给变量赋默认值undefined,把所有函数声明放进内存。

函数执行上下文:当函数被调用,它就被创建出来了。函数执行上下文中应该创建的应该是arguments对象,所以当创建函数执行上下文时,Javascript引擎会:

  1. 1.创建一个全局对象
  2. 创建一个arguments对象;
  3. 创建this对象,指向函数调用对象;
  4. 给函数分配内存;
  5. 给变量(包括内部定义的变量和参数变量)分配内存;
  6. 给变量赋默认值undefined,把所有函数声明放进内存。

关于变量对象的创建有什么疑问可以看看JavaScript深入之变量对象

举个栗子:我们来说明一下:
实际操作

这里有几处重要细节需要注意。首先,传入函数的所有参数都作为局部变量存在于该函数的执行上下文中。在例子中,handle同时存在与全局执行上下文和getURL执行上下文中,因为我们把它传入了getURL函数做为参数。其次,在函数中声明的变量存在于函数的执行上下文中。

作用域链:Javascript中一切皆对象,这些对象有一个[[Scope]]属性,该属性包含了函数被创建时的作用域中对象的集合,这个集合被称为函数的作用域链(Scope Chain),它决定了哪些数据能被函数访问。当函数创建的时候,它的[[scope]]属性自动添加好全局作用域。之所以要强调创建是因为JavaScript采用词法作用域(lexical scoping),也就是静态作用域.

举个栗子:我们来通过简单的代码说明一下作用域链:
实际操作

function a () {
  console.log('In fn a')
  function b () {
    console.log('In fn b')
    function c () {
      console.log('In fn c')
    }
    c()
  }
  b()
}

a()
复制代码

从图中可以清楚的发现在函数的执行过程中,最开始创建了一个全局执行上下文,然后没执行一个函数就会创建一个函数执行上下文,当开始执行函数 C() 的时候 C 函数有一个[[scope]]属性,里面的值会是:

//c的作用域链
[
 0:{
     arguments:{length:0},
     this:window
 },
 1:{
     arguments:{length:0},
     this:window,
     c:fn()
 }
 2:{
     arguments:{length:0},
     this:window,
     b:fn()
 },
 3:{
     this:window,
     a:fn()
 }
]
复制代码

细心观察你会发现每个函数执行完之后,每个函数的执行上下文会消失,事实上,Javascript引擎创建了一个叫“执行栈”(也叫调用栈)的东西。每当函数被调用,就创建一个新的执行上下文并把它加入到调用栈;每当一个函数运行完毕,就被从调用栈中弹出来。所以“通常情况下”函数执行完毕后函数的执行上下文就会消失,闭包就是不是“通常情况下”。

问题来了什么叫做词法作用域(也可以说静态作用域)?

我们在举个栗子:

var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar();

// 结果是 ???
复制代码

答案是 1 (小伙伴你答对了么?)
解释:因也很简单,因为JavaScript采用的是词法作用域(如果不明白可以看看《JavaScript深入之词法作用域和动态作用域》),函数的作用域基于函数创建的位置。函数foo() 定义在全局作用域下,当打印value时沿着作用于链查找就找到了全局执行上下文,而不是bar函数执行上下文。所以结果是1。

而引用《JavaScript权威指南》的回答就是: JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数 f() 定义在这个作用域链里,其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执行 f() 时依然有效。

3.闭包

闭包就是不是“通常情况下”,如果你在一个函数中嵌入了另一个函数,并且让外部一个指针引用内部的函数,例外情况就产生了。这种函数套函数的情况下,即使父级函数的执行上下文从调用栈弹出了,子级函数仍然能够访问父级函数的作用域。
实际操作

makeAdder执行上下文从调用栈弹出后,Javascript Visualizer创建了一个Closure Scope(闭包作用域)。Closure Scope中的变量环境和makeAdder执行上下文中的变量环境相同。这是因为我们在函数中嵌入了另一个函数。在本例中,inner函数嵌在makeAdder中,所以inner在makeAdder变量环境的基础上创建了一个闭包。因为闭包作用域的存在,即使makeAdder已经从调用栈弹出了,inner仍然能够访问到x变量(通过作用域链)。

现在是不是感觉自己明白了一点什么是闭包呢?反正闭包的定义我还下不了,但是我还是要粗略的表达一下我自己的想法就是:闭包就是使一个函数作为另一个函数的返回,从而达到内部函数可以读取外部函数内部的变量和让外部函数中的变量的值始终保持在内存中的作用的一个写法。(PS.不知道大家可不可以接受,不喜勿喷!!!)

4.小检验

function func() {
    var arr = [];
    for(var i = 0;i<3;i++){
        arr.push(()=> {
            console.log(i);
        })
    }
    return arr
}
var result = func();
result.forEach((item)=> {
    item();
})
复制代码

输出结果是:3 3 3 有没有答对呢 ?
解释:首先在func函数执行上下文中创建了 i 变量(这里涉及到变量提升的知识,不了解自己可以看一下),当执行匿名的函数要console.log(i)的时候发现在改匿名函数的执行上下文没有这个变量,则沿着作用域链向上查找,发现在func的作用域中有i,这个i的值是3(for循环最后结束后i记录为3)。

我们使用闭包进行如下修改,亲,你再猜猜?

function func() {
    var arr = [];
    for(var i = 0;i<3;i++){
        (function(){
            arr.push(()=> {
                console.log(i);
            })
        })()
    }
    return arr
}
var result = func();
result.forEach((item)=> {
    item();
})
复制代码

答案是 3 3 3,解释和上面的一样。如果你想输出 0 ,1 ,2 有两种方案:闭包和使用let

//方案一
function func() {
    var arr = [];
    for(var i = 0;i<3;i++){
        (function(i){
            arr.push(()=> {
                console.log(i);
            })
        })(i)
    }
    return arr
}
var result = func();
result.forEach((item)=> {
    item();
})
//输出 0 1 2

//方案二
function func() {
    var arr = [];
    for(let i = 0;i<3;i++){
        arr.push(()=> {
            console.log(i);
        })
    }
    return arr
}
var result = func();
result.forEach((item)=> {
    item();
})
//输出 0 1 2
复制代码

5.使用闭包的注意点

  1. 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

  2. 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

6.闭包运行机制

思考题:
代码一

var name = "The Window";
  var object = {
    name : "My Object",

    getNameFunc : function(){
      return function(){
        return this.name;
      };

    }
  };
  alert(object.getNameFunc()());//The Window
复制代码

代码二

var name = "The Window";
  var object = {
    name : "My Object",

    getNameFunc : function(){
      var that=this;
      return function(){
        return that.name;
      };

    }
  };
  alert(object.getNameFunc()());//My Object
复制代码

javascript 中this的定义:就是上下文对象,即被调用函数所处的环境,也就是说,this 在函数内部指向了调用函数的对象。如果没有搞懂就去研究一下javascript的this吧

7.引用

  1. 【译】终极指南:变量提升、作用域和闭包
  2. 闭包的错误使用
  3. 学习Javascript闭包(Closure)--阮一峰
  4. 一次性搞懂JavaScript闭包--简书
  5. 高效使用 JavaScript 闭包
  6. JavaScript深入之词法作用域和动态作用域
  7. JavaScript深入之变量对象

结束语

后面发现好的闭包的内容我还会加进来,如果有什么不对的地方欢迎指正。

猜你喜欢

转载自juejin.im/post/5c257b61e51d451b1c6de48c