玩命加载中 . . .

什么是闭包


闭包

指有权访问外层函数作用域中的变量的函数。

什么意思呢?举个栗子:

var a = 1;
function foo() {
  console.log(a);
}
foo();

a 对于 foo 函数来说,属于外层函数作用域中的变量,由于作用域链,使得 foo 函数可以访问到变量 a ,因此就构成了一个闭包。

创建闭包的方式

那么该如何创建一个闭包呢?
常见方式:就是在一个函数内部创建另一个函数。

执行上下文的角度看闭包

下面从执行上下文的角度看下闭包是怎么一回事:

var scope = "global scope";
function checkscope() {
    var scope = "local scope";
    function f() {
        return scope;
    }
    return f;
}

var foo = checkscope();
foo();

首先要分析一下这段代码中执行上下文栈和执行上下文的变化情况。

  1. 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
  2. 全局执行上下文初始化
  3. 初始化的同时,checkscope 函数被创建,保存作用域链到函数的内部属性[[scope]]
  4. 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈
  5. checkscope 执行上下文初始化,创建变量对象、作用域链、this等
  6. checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
  7. 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈
  8. f 执行上下文初始化,创建变量对象、作用域链、this等
  9. f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

思考一个问题:当 f 函数执行的时候,checkscope 函数上下文已经被销毁了(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?

  • 这是因为 f 执行上下文维护了一个作用域链:
    fContext = {
      Scope:[AO, checkscopeContext.AO, globalContext.VO]
    }
  • 就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。

一道刷题必刷,面试必考的闭包题

var data = [];
for(var i = 0; i < 3; i++) {
  data[i] = function() {
    console.log(i);
  };
}

data[0](); // 3
data[1](); // 3
data[2](); // 3

我们来分析一下为什么答案全为 3 ?

  • 当执行到 data[0] 函数之前,此时全局上下文的 VO 为:
    globalContext = {
      VO:{
        data:[...],
        i:3
      }
    }
  • 当执行 data[0] 函数的时候,data[0] 函数的作用域链为:
    data[0]Context = {
      Scope:[AO, globalContext.VO]
    }
  • data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3。
  • data[1] 和 data[2] 是一样的道理。

将上面的题改成闭包看看:

var data = [];
for(var i = 0; i < 3; i++) {
  data[i] = (function(i) {
    return function() {
      console.log(i);
    }
  })(i);
}

data[0]();
data[1]();
data[2]();
  • 当执行到 data[0] 函数之前,此时全局上下文的 VO 为:(跟没改之前一模一样)
    globalContext = {
        VO:{
            data:[...],
            i:3
        }
    }
  • 当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:
    data[0]Context = {
        Scope:[AO, 匿名函数Context.AO, globalContext.VO]
    }
  • 匿名函数执行上下文的AO为:
    匿名函数Context = {
        AO:{
            arguments:{
                0:0,
                length:1
            },
            i:0
        }
    }
  • data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数 Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了(即使 globalContext.VO 也有 i 的值–为3),所以打印的结果就是0。
  • data[1] 和 data[2] 是一样的道理。

以下代码块中,c 会报错,并不会读取到bar 执行上下文中变量对象c

var fn = null;
function foo() {
    var a = 2;
    function innerFoo() {
        console.log(c);
        console.log(a);
    }
    fn = innerFoo;
}
function bar() {
    var c = 100;
    fn();
}

foo();
bar();
  • 虽然 fn() ,即 innerFoo() 是在 bar 里面执行的,但是 innerFoo 函数执行的时候它的作用域 scope 里面分别是 [AO,fooContext.AO, globalContext.AO] ,并没有包括 barContext.AO 在里面,所以根本就没有声明 c 这个变量,所以报错

IIFE

推荐阅读文章:[译] JavaScript:立即执行函数表达式(IIFE)

当函数变成立即执行的函数表达式时,表达式中的变量不能从外部访问

(function() {
  var name = "Barry";
})();

// 无法从外部访问变量 name
console.log(name); // 抛出错误:"Uncaught ReferenceError:name is not defined"

将 IIFE 分配给一个变量,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果。

var result = (function() {
    var name = "Barry";
    return name;
})();

// IIFE 执行后返回的结果:
console.log(result); // "Barry"

文章作者: hcyety
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hcyety !
评论
  目录