【译】JavaScript进阶 从实现理解闭包

来源于 现代JavaScript教程
闭包章节
中文翻译计划
本文很清晰地解释了闭包是什么,以及闭包如何产生,相信你看完也会有所收获javascript

关键字 Closure 闭包 Lexical Environment 词法环境 Environment Record 环境记录前端

闭包(Closure)

JavaScript 是一个 function-oriented 的语言。这带来了很大的操做自由。函数只需建立一次,能够拷贝到另外一个变量,或者做为一个参数传入另外一个函数而后在一个全新的环境调用。java

咱们知道函数能够访问它外部的变量,这个 feature 十分经常使用。git

可是当外部变量改变时会发生什么?函数时获取最新的值,仍是函数建立当时的值?github

还有一个问题,当函数被送到其余地方再调用……他能访问那个地方的外部变量吗?面试

不一样语言的表现有所不一样,下面咱们研究一下 JavaScript 中的表现。express

两个问题

咱们先思考下面两种状况,看完这篇文章你就能够回答这两个问题,更复杂的问题也不在话下。编程

  1. sayHi 函数使用了外部变量 name。函数运行时,会使用两个值中的哪一个?浏览器

    let name = "John";
    
    function sayHi() {
      alert("Hi, " + name);
    }
    
    name = "Pete";
    
    sayHi(); // "John" 仍是 "Pete"?
    复制代码

    这个状况不管是浏览器端仍是服务器端都很常见。函数极可能在它建立一段时间后才执行,例如等待用户操做或者网络请求。服务器

    问题是:函数是否会选择变量最新的值呢?

  2. makeWorker 函数创造并返回了另外一个函数。这个新函数能够在任何地方调用。他会访问建立时的变量仍是调用时的变量呢?

    function makeWorker() {
      let name = "Pete";
    
      return function() {
        alert(name);
      };
    }
    
    let name = "John";
    
    // 建立函数
    let work = makeWorker();
    
    // 调用函数
    work(); // "Pete" (建立时) 仍是 "John" (调用时)?
    复制代码

Lexical Environment (词法环境)

要理解里面发生了什么,必须先明白“变量”究竟是什么。

在 JavaScript 里,任何运行的函数、代码块、整个 script 都会关联一个被叫作 Lexical Environment (词法环境) 的对象。

Lexical Environment 对象包含两个部分:(译者:这里是重点)

  1. Environment Record (环境记录)是一个拥有所有局部变量做为属性的对象(以及其余如 this 值的信息)。
  2. *outer lexical environment (外部词法环境)*的引用,一般词法关联外面一层代码(花括号外一层)。

因此,“变量”就是内部对象 Environment Record 的一个属性。要改变一个对象,意味着改变 Lexical Environment 的属性。

例如在这段简单的代码中,只有一个 Lexical Environment:

lexical environment

这就是所谓 global Lexical Environment (全局语法环境),对应整个 script。对于浏览端,整个 <script> 标签共享一个全局环境。

(译者:这里是重点) 上图中,正方形表明 Environment Record (变量储存),箭头表明 outer reference (外部引用)。global Lexical Environment 没有外部引用,因此指向 null

下图展现 let 变量的工做机制:

lexical environment

右边的正方形描述 global Lexical Environment 在执行中如何改变:

  1. 脚本开始运行,Lexical Environment 空。
  2. let phrase 定义出现了。由于没有赋值因此储存为 undefined
  3. phrase 被赋值。
  4. phrase 被赋新值。

看起来很简单对不对?

总结:

  • 变量是一个特殊内部对象的属性,关联于执行时的块、函数、 script 。
  • 对变量的操做其实是对这个对象属性的操做。

Function Declaration (函数声明)

Function Declaration 并不是处理于被执行的时候,而是 Lexical Environment 建立的时候。对于 global Lexical Environment ,这意味着 script 开始运行的时候。

这就是函数能够在定义前调用的缘由。

如下代码 Lexical Environment 开始时非空。由于有 say 函数声明,以后又有了 let 声明的 phrase

lexical environment

Inner and outer Lexical Environment (内部词法环境和外部词法环境)

调用 say() 的过程当中,它使用了外部变量,一块儿看看这里面发生了什么。

(译者:这里是重点) 函数运行时会自动建立一个新的函数 Lexical Environment 。这是全部函数的通用规则。这个新的 Lexical Environment 用于当前运行函数的存放局部变量和形参。

箭头标记的是执行 say("John") 时的 Lexical Environment :

lexical environment

函数调用过程当中,能够看到两个 Lexical Environment :里面的是函数调用产生的,外面的是全局的:

  • 内层 Lexical Environment 对应当前执行的 say 。它只有一个变量: 函数实参 name 。咱们调用 say("John") ,因此 name 的值是 "John"
  • 外层 Lexical Environment 是 global Lexical Environment 。

内层 Lexical Environment 有一个 outer 属性,指向外层 Lexical Environment。

代码要访问一个变量,首先搜索内层 Lexical Environment ,接着是外层,再外层,直到链的结束。

若是走完整条链变量都找不到,在 strict mode 就会报错了。不使用 use strict 的状况下,对未定义变量的赋值,会创造一个新的全局变量。

下面一块儿看看变量搜索如何处理:

  • say 里的 alert 想要访问 name ,当即就能在当前函数的 Lexical Environment 找到。
  • 对于 phrase ,局部变量不存在 phrase ,因此要循着 outer 在全局变量里找到。

lexical environment lookup

如今咱们能够回答本章开头的第一个问题了。

函数获取外部变量当前值

旧变量值不储存在任何地方,函数须要他们的时候,它取得来源于自身或外部 Lexical Environment 的当前值。

因此第一个问题的答案是 Pete

let name = "John";

function sayHi() {
 alert("Hi, " + name);
}

name = "Pete"; // (*)

sayHi(); // Pete
复制代码

上述代码的执行流:

  1. global Lexical Environment 存在 name: "John"
  2. (*) 行中,全局变量修改了,如今成了这样 name: "Pete"
  3. say() 执行的时候, 取外部 name 。此时在 global Lexical Environment 中已是 "Pete"

一次调用,一个 Lexical Environment
请注意,每当一个函数运行,就会建立一个新的 function Lexical Environment。
若是一个函数被屡次调用,那么每次调用都会生成一个属于当前调用的全新 Lexical Environment ,里面装载着当前调用的变量和实参。

Lexical Environment 是一个标准对象 (specification object)
"Lexical Environment" 是一个标准对象 (specification object)。咱们不能直接获取或设置它,JavaScript 引擎也可能优化它,抛弃未使用的变量来节省内存或者做其余优化,可是可见行为应该如上面所述。

嵌套函数

在一个函数中建立另外一个函数,称为“嵌套”。这在 JavaScript 很容易作到:

function sayHiBye(firstName, lastName) {

 // helper nested function to use below
 function getFullName() {
   return firstName + " " + lastName;
 }

 alert( "Hello, " + getFullName() );
 alert( "Bye, " + getFullName() );

}
复制代码

嵌套函数 getFullName() 能够访问外部变量,帮助咱们很方便地返回 FullName 。

更有趣的是,嵌套函数能够被 return ,做为一个新对象的属性或者做为本身的结果。这样它们就能在其余地方使用,不管在哪里,它都能访问一样的外部变量。

一个构造函数(详见 info:constructor-new)的例子:

// 构造函数返回一个新对象
function User(name) {

 // 嵌套函数创造对象方法
 this.sayHi = function() {
   alert(name);
 };
}

let user = new User("John");
user.sayHi(); // 方法返回外部 "name"
复制代码

一个 return 函数的例子:

function makeCounter() {
 let count = 0;

 return function() {
   return count++; // has access to the outer counter
 };
}

let counter = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2
复制代码

咱们接着研究 makeCounter 。counter 函数每调用一次就会返回下一个数。尽管这很简单,但只要轻微修改,它便具备必定的实用性,例如伪随机数生成器

counter 内部如何工做?

内部函数运行, count++ 中的变量由内到外搜索:

image

  1. 嵌套函数局部变量……
  2. 外层函数……
  3. 直到全局变量。

第二步咱们找到了 count 。当外部变量被修改,它所在的地方就被修改。因此 count++ 检索外部变量并对其加一是操做于该变量本身的 Lexical Environment 。就像操做了 let count = 1 同样。

这里须要思考两个问题:

  1. 咱们能经过 makeCounter 之外的方法重置 counter 吗?
  2. 若是咱们能够屡次调用 makeCounter() ,返回了不少 counter 函数,他们的 count 是独立的仍是共享的?

继续阅读前能够先尝试思考一下。

...

ok ?

那咱们开始揭晓谜底:

  1. 没门。 counter 是局部变量,不可能在外部直接访问。
  2. 每次调用 makeCounter() 都会新建 Lexical Environment,每个环境都有本身的 counter 。因此不一样 counter 里的 count 是独立的。

一个 demo :

function makeCounter() {
 let count = 0;
 return function() {
   return count++;
 };
}

let counter1 = makeCounter();
let counter2 = makeCounter();

alert( counter1() ); // 0
alert( counter1() ); // 1

alert( counter2() ); // 0 (独立)
复制代码

如今你能清楚外部变量的使用,可是你仍然须要更深刻地理解以面对更复杂的状况,如今咱们进入下一步。

Environment 细节

对 closure (闭包)有了初步了解以后,能够开始深刻细节了。

下面是 makeCounter 例子的动做分解,跟着看你就能理解一切了。注意, [[Environment]] 属性咱们以前还未介绍。

  1. 脚本开始运行,此时只存在 global Lexical Environment :

    image

    这时候只有 makeCounter 一个函数,这是函数声明,还未被调用

    全部函数都带着一个隐藏属性 [[Environment]] “诞生”。 [[Environment]] 指向它们建立的 Lexical Environment 。是[[Environment]] 让函数知道它“诞生”于什么环境。

    makeCounter 建立于 global Lexical Environment ,因此 [[Environment]] 指向它。

    换句话说,Lexical Environment 在函数诞生时就“铭刻”在这个函数中。[[Environment]] 是指向 Lexical Environment 的隐藏函数属性。

  2. 代码继续走, makeCounter() 登上舞台。这是代码运行到 makeCounter() 瞬间的快照:

    image

    makeCounter() 调用时,保存当前变量和实参的 Lexical Environment 已经被建立。

    Lexical Environment 储存 2 个东西:

    1. 带有局部变量的 Environment Record 。例子中 count 是惟一的局部变量( let count 被执行的时候记录)。
    2. 被绑定到函数 [[Environment]] 的外部词法引用。例子里 makeCounter[[Environment]] 引用了 global Lexical Environment 。

    因此这里有两个 Lexical Environments :全局,和 makeCounter (outer 引用全局)。

  3. makeCounter() 执行的过程当中,建立了一个嵌套函数。

    这无关于函数建立使用的是 Function Declaration (函数声明)仍是 Function Expression (函数表达式)。全部函数都会获得引用他们被建立时 Lexical Environment 的 [[Environment]] 属性。

    这个嵌套函数的 [[Environment]]makeCounter() (它的诞生地)的 Lexical Environment:

    image

    一样注意,这一步是函数声明而非调用。

  4. 代码继续执行,makeCounter() 调用结束,内嵌函数被赋值到全局变量 counter

    image

    这个函数只有一行: return count++

  5. counter() 被调用,自动建立一个 “空” Lexical Environment 。 此函数无局部变量,可是 [[Environment]] 引用了外面一层,因此它能够访问 makeCounter() 的变量。

    image

    要访问变量,先检索本身的 Lexical Environment (empty),而后是 makeCounter() 的,最后是全局的。例子中在最近的外层 Lexical Environment makeCounter 中发现了 count

    重点来了,内存在这里是怎么管理的?尽管 makeCounter() 调用结束了,它的 Lexical Environment 依然保存在内存中,这是由于嵌套函数的 [[Environment]] 引用了它。

    一般, Lexical Environment 对象随着使用它的函数的存在而存在。没有函数引用它的时候,它才会被清除。

  6. counter() 函数不仅是返回 count ,还会对其 +1 操做。这个修改已经在“适当的位置”完成了。count 的值在它被找到的环境中被修改。

    image

    这一步出了返回了新的 count ,其余彻底相同。

    (译者:总结一下,声明时记录环境 [[Environment]](函数所在环境),执行时建立词法环境(局部+outer 就是引用 [[Environment]] ),而闭包就是函数 + 它的词法环境,因此定义上来讲全部函数都是闭包,可是以后被返回出来可使用的闭包才是“实用意义”上的闭包)

  7. 下一个 counter() 调用操做同上。

本章开头第二个问题的答案如今显而易见了。

如下代码的 work() 函数经过外层 lexical environment 引用了它原地点的 name

image

因此这里的答案是 "Pete"

可是若是 makeWorker() 没了 let name ,如咱们所见,做用域搜索会到达外层,获取全局变量。这个状况下答案会是 "John"

闭包 (Closure)
开发者们都应该知道编程领域的通用名词闭包 (closure)。
Closure 是一个记录并可访问外层变量的函数。在一些编程语言中,这是不可能的,或者要以一种特殊的方式书写以实现这个功能。可是如上面解释的, JavaScript 的全部函数都(很天然地)是个闭包。(有一个例外,详见info:new-function
这就是闭包:它们使用 [[Environment]] 属性自动记录各自的建立地点,而后由此访问外部变量。
在前端面试中,若是面试官问你什么是闭包,正确答案应该包括闭包的定义,以及解释为什么 JavaScript 的全部函数都是闭包,最好能够再简单说说里面的技术细节: [[Environment]] 属性和 Lexical Environments 的原理。

代码块、循环、 IIFE

上面的例子都着重于函数,可是 Lexical Environment 也存在于代码块 {...}

它们在代码块运行时建立,包含块局部变量。这里有一些例子。

If

下例中,当执行到 if 块,会为这个块建立新的 "if-only" Lexical Environment :

image

与函数一样原理,块内能够找到 phrase ,可是块外不能使用块内的变量和函数。若是执意在 if 外面用 user ,那只能获得一个报错了。

For, while

对于循环,每一个 iteration 都会有本身的 Lexical Environment ,在 for 里定义的变量,也是块的局部变量,也属于块的 Lexical Environment :

for (let i = 0; i < 10; i++) {
 // Each loop has its own Lexical Environment
 // {i: value}
}

alert(i); // Error, no such variable
复制代码

let i 只在块内可用,每次循环都有它本身的 Lexical Environment ,每次循环都会带着当前的 i ,最后循环结束, i 不可用。

代码块

咱们也能够直接用 {…} 把变量隔离到一个“局部做用域”(local scope)。

在浏览器中全部 script 共享全局变量,这就很容易形成变量的重名、覆盖。

为了不这种状况咱们可使用代码块隔离本身的代码:

{
 // do some job with local variables that should not be seen outside

 let message = "Hello";

 alert(message); // Hello
}

alert(message); // Error: message is not defined
复制代码

代码块有本身的 Lexical Environment ,块外没法访问块内变量。

IIFE

之前没有代码块,要实现上述效果要依靠所谓的“当即执行函数表达式”(immediately-invoked function expressions ,缩写 IIFE):

(function() {

 let message = "Hello";

 alert(message); // Hello

})();
复制代码

这个函数表达式建立后当即执行,这段代码当即执行并有本身的私有变量。

函数表达式须要被括号包裹。 JavaScript 执行时遇到 "function" 会理解为一个函数声明,函数声明必须有名称,没有就会报错:

// Error: Unexpected token (
function() { // <-- JavaScript cannot find function name, meets ( and gives error

 let message = "Hello";

 alert(message); // Hello

}();
复制代码

你可能会说:“那我给他加个名字咯”,但这依然行不通,JavaScript 不容许函数声明马上被执行:

// syntax error because of brackets below
function go() {

}(); // <-- can't call Function Declaration immediately
复制代码

圆括号告诉 JavaScript 这个函数建立于其余表达式的上下文,所以这是个函数表达式。不须要名称,也能够当即执行。

也有其余方法告诉 JavaScript 咱们须要的是函数表达式:

// 建立 IIFE 的方法

(function() {
 alert("Brackets around the function");
})();

(function() {
 alert("Brackets around the whole thing");
}());

!function() {
 alert("Bitwise NOT operator starts the expression");
}();

+function() {
 alert("Unary plus starts the expression");
}();
复制代码

垃圾回收

Lexical Environment 对象与普通的值的内存管理规则是同样的。

  • 一般 Lexical Environment 在函数运行完毕就会被清理:

    function f() {
      let value1 = 123;
      let value2 = 456;
    }
    
    f();
    复制代码

    这两个值是 Lexical Environment 的属性,可是 f() 执行完后,这个 Lexical Environment 无任何变量引用(unreachable),因此它会从内存删除。

  • ...可是若是有内嵌函数,它的 [[Environment]] 会引用 f 的 Lexical Environment(reachable):

    function f() {
      let value = 123;
    
      function g() { alert(value); }
    
      return g;
    }
    
    let g = f(); // g is reachable, and keeps the outer lexical environment in memory
    复制代码
  • 注意, f() 若是被屡次调用,返回的函数都被保存,相应的 Lexical Environment 会分别保存在内存:

    function f() {
      let value = Math.random();
    
      return function() { alert(value); };
    }
    
    // 3 functions in array, every one of them links to Lexical Environment
    // from the corresponding f() run
    // LE LE LE
    let arr = [f(), f(), f()];
    复制代码
  • Lexical Environment 对象在不被引用 (unreachable) 后被清除: 无嵌套函数引用它。下例中, g 自身不被引用后, value 也会被清除:

    function f() {
      let value = 123;
    
      function g() { alert(value); }
    
      return g;
    }
    
    let g = f(); // while g is alive
    // there corresponding Lexical Environment lives
    
    g = null; // ...and now the memory is cleaned up
    复制代码

现实中的优化

理论上,函数还在,它的全部外部变量都会被保留。

但在实践中,JavaScript 引擎可能会对此做出优化,引擎在分析变量的使用状况后,把没有使用的外部变量删除。

在 V8 (Chrome, Opera) 有个问题,这些被删除的变量不能在 debugger 观察了。

尝试在 Chrome Developer Tools 运行如下代码:

function f() {
 let value = Math.random();

 function g() {
   debugger; // 在 console 输入 alert( value ); 发现无此变量!
 }

 return g;
}

let g = f();
g();
复制代码

你能够看到,这里没有保存 value 变量!理论上它应该是可访问的,可是引擎优化移除了这个变量。

还有一个有趣的 debug 问题。下面的代码 alert 出外面的同名变量而不是里面的:

let value = "Surprise!";

function f() {
 let value = "the closest value";

 function g() {
   debugger; // in console: type alert( value ); Surprise!
 }

 return g;
}

let g = f();
g();
复制代码

再会! 若是你用 Chrome/Opera 来debug ,很快就能发现这个 V8 feature。 这不是 bug 而是 V8 feature,或许未来会被修改。至于改没改,运行一下上面的例子就能判断啦。

相关文章
相关标签/搜索