深入学习js之——闭包#8

747 阅读6分钟

深入学习js系列是自己阶段性成长的见证,希望通过文章的形式更加严谨、客观地梳理js的相关知识,也希望能够帮助更多的前端开发的朋友解决问题,期待我们的共同进步。

如果觉得本系列不错,欢迎点赞、评论、转发,您的支持就是我坚持的最大动力。


开篇

闭包(closure)是 Javascript 语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。

要理解闭包,首先必须理解 Javascript 特殊的变量作用域。

变量的作用域无非就是两种:全局变量和局部变量。

定义

MDN 对于闭包的定义为:

闭包是指那些能够访问自由变量的函数。

那么什么是自由变量呢?

自由变量是指在函数中使用的,但既不是函数的参数也不是函数的局部变量的变量。

由此,我们可以看出来闭包有两部分组成:

闭包 = 函数+函数能够访问的自由变量。

举一个例子:

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

foo 函数可以访问变量 a 但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数, 所以 a 就是自由变量。

那么,函数 foo + foo 函数访问的自由变量 a 就已经构成了一个闭包……

还真的是这样!

所以在《JavaScript 权威指南》中就讲到:从技术的角度讲,所有的 JavaScript 函数都是闭包。

上面所说的是理论角度的闭包,其实还有一种实践角度的闭包,让我们看看汤姆大叔翻译的关于闭包的文章中的定义:

ECMAScript 中闭包指的是:

1、从理论角度:所有的函数,因为它们都在创建的时候就将上层上下文的数据保存下来了,哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。 2、从实践角度:以下函数才算是闭包:

  • 1、即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)。
  • 2、在代码中引用自由变量。

接下来就来聊聊实践上闭包。

分析

让我们来先看一个例子:

var scope = "global scope";

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

var foo = checkscope();
foo();

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

这里给出简要的执行过程:

1、进入全局代码,创建全局执行上下文,全局上下文压入执行上下文栈。
2、全局执行上下文初始化。
3、执行 checkscope 函数,创建 checkscope函数执行上下文,checkscope 执行上下文被压入执行上下文栈。
4、checkscope 执行上下文初始化,创建变量对象、作用域链、this 等。
5、checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈弹出。
6、执行 f 函数,创建 f 函数的执行上下文,f 执行上下文被压入执行上下文栈
7、f 执行上下文初始化,创建变量对象、作用域链、this 等
8、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 做到了这一点,从而实现了闭包这个概念。

所以,让我们再看一遍实践角度上闭包的定义:

1、即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回) 2、在代码中引用了自由变量

面试必刷题

接下来,看这道刷题必刷。面试必考的闭包题目:

var data = [];

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

答案都是 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 的值,所以会从 globalConetxt.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:{
    dataL[...],
    i:3
  }
}

跟没改变之前一样,

当执行 data[0]函数的时候,data[0]函数的作用域链发生了改变:

data[0]Context = {
  Scope:[AO,匿名函数Context.VO 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] 是一样的道理。

参考:

《JavaScript深入之闭包》

《学习JavaScript闭包》

深入学习JavaScript系列目录

欢迎添加我的个人微信讨论技术和个体成长。

欢迎关注我的个人微信公众号——指尖的宇宙,更多优质思考干货