金沙棋牌官方平台

当前位置:金沙棋牌 > 金沙棋牌官方平台 > 让我们一起学习JavaScript闭包吧,变量的生命周期

让我们一起学习JavaScript闭包吧,变量的生命周期

来源:http://www.logblo.com 作者:金沙棋牌 时间:2019-11-21 10:18

ES6 变量作用域与提升:变量的生命周期详解

2017/08/16 · JavaScript · 1 评论 · es6, 作用域

原文出处: 王下邀月熊   

 

ES6 变量作用域与提升:变量的生命周期详解从属于笔者的现代 JavaScript 开发:语法基础与实践技巧系列文章。本文详细讨论了 JavaScript 中作用域、执行上下文、不同作用域下变量提升与函数提升的表现、顶层对象以及如何避免创建全局对象等内容;建议阅读前文ES6 变量声明与赋值。

本文由 伯乐在线 - 刘唱 翻译,年迈的程序猿 校稿。未经许可,禁止转载!
英文出处:Preethi Kasireddy。欢迎加入翻译组。

变量作用域与提升

在 ES6 之前,JavaScript 中只存在着函数作用域;而在 ES6 中,JavaScript 引入了 let、const 等变量声明关键字与块级作用域,在不同作用域下变量与函数的提升表现也是不一致的。在 JavaScript 中,所有绑定的声明会在控制流到达它们出现的作用域时被初始化;这里的作用域其实就是所谓的执行上下文(Execution Context),每个执行上下文分为内存分配(Memory Creation Phase)与执行(Execution)这两个阶段。在执行上下文的内存分配阶段会进行变量创建,即开始进入了变量的生命周期;变量的生命周期包含了声明(Declaration phase)、初始化(Initialization phase)与赋值(Assignment phase)过程这三个过程。

传统的 var 关键字声明的变量允许在声明之前使用,此时该变量被赋值为 undefined;而函数作用域中声明的函数同样可以在声明前使用,其函数体也被提升到了头部。这种特性表现也就是所谓的提升(Hoisting);虽然在 ES6 中以 let 与 const 关键字声明的变量同样会在作用域头部被初始化,不过这些变量仅允许在实际声明之后使用。在作用域头部与变量实际声明处之间的区域就称为所谓的暂时死域(Temporal Dead Zone),TDZ 能够避免传统的提升引发的潜在问题。另一方面,由于 ES6 引入了块级作用域,在块级作用域中声明的函数会被提升到该作用域头部,即允许在实际声明前使用;而在部分实现中该函数同时被提升到了所处函数作用域的头部,不过此时被赋值为 undefined。

 

作用域

作用域(Scope)即代码执行过程中的变量、函数或者对象的可访问区域,作用域决定了变量或者其他资源的可见性;计算机安全中一条基本原则即是用户只应该访问他们需要的资源,而作用域就是在编程中遵循该原则来保证代码的安全性。除此之外,作用域还能够帮助我们提升代码性能、追踪错误并且修复它们。JavaScript 中的作用域主要分为全局作用域(Global Scope)与局部作用域(Local Scope)两大类,在 ES5 中定义在函数内的变量即是属于某个局部作用域,而定义在函数外的变量即是属于全局作用域。

让我们一起学习JavaScript闭包吧

全局作用域

当我们在浏览器控制台或者 Node.js 交互终端中开始编写 JavaScript 时,即进入了所谓的全局作用域:

// the scope is by default global var name = 'Hammad';

1
2
// the scope is by default global
var name = 'Hammad';

定义在全局作用域中的变量能够被任意的其他作用域中访问:

var name = 'Hammad'; console.log(name); // logs 'Hammad' function logName() { console.log(name); // 'name' is accessible here and everywhere else } logName(); // logs 'Hammad'

1
2
3
4
5
6
7
8
9
var name = 'Hammad';
 
console.log(name); // logs 'Hammad'
 
function logName() {
    console.log(name); // 'name' is accessible here and everywhere else
}
 
logName(); // logs 'Hammad'

闭包是JavaScript中的一个基本概念,每一个认真的程序员都应该对它了如指掌。

函数作用域

定义在某个函数内的变量即从属于当前函数作用域,在每次函数调用中都会创建出新的上下文;换言之,我们可以在不同的函数中定义同名变量,这些变量会被绑定到各自的函数作用域中:

// Global Scope function someFunction() { // Local Scope #1 function someOtherFunction() { // Local Scope #2 } } // Global Scope function anotherFunction() { // Local Scope #3 } // Global Scope

1
2
3
4
5
6
7
8
9
10
11
12
13
// Global Scope
function someFunction() {
    // Local Scope #1
function someOtherFunction() {
        // Local Scope #2
    }
}
 
// Global Scope
function anotherFunction() {
    // Local Scope #3
}
// Global Scope

函数作用域的缺陷在于粒度过大,在使用闭包或者其他特性时导致异常的变量传递:

var callbacks = []; // 这里的 i 被提升到了当前函数作用域头部 for (var i = 0; i <= 2; i++) { callbacks[i] = function () { return i * 2; }; } console.log(callbacks[0]()); //6 console.log(callbacks[1]()); //6 console.log(callbacks[2]()); //6

1
2
3
4
5
6
7
8
9
10
11
12
var callbacks = [];
 
// 这里的 i 被提升到了当前函数作用域头部
for (var i = 0; i <= 2; i++) {
    callbacks[i] = function () {
return i * 2;
        };
}
 
console.log(callbacks[0]()); //6
console.log(callbacks[1]()); //6
console.log(callbacks[2]()); //6

互联网上充斥着大量关于“什么是闭包”的解释,却很少有人深入探究它“为什么”的一面。

块级作用域

类似于 if、switch 条件选择或者 for、while 这样的循环体即是所谓的块级作用域;在 ES5 中,要实现块级作用域,即需要在原来的函数作用域上包裹一层,即在需要限制变量提升的地方手动设置一个变量来替代原来的全局变量,譬如:

var callbacks = []; for (var i = 0; i <= 2; i++) { (function (i) { // 这里的 i 仅归属于该函数作用域 callbacks[i] = function () { return i * 2; }; })(i); } callbacks[0]() === 0; callbacks[1]() === 2; callbacks[2]() === 4;

1
2
3
4
5
6
7
8
9
10
11
12
var callbacks = [];
for (var i = 0; i <= 2; i++) {
    (function (i) {
        // 这里的 i 仅归属于该函数作用域
        callbacks[i] = function () {
return i * 2;
        };
    })(i);
}
callbacks[0]() === 0;
callbacks[1]() === 2;
callbacks[2]() === 4;

而在 ES6 中,可以直接利用 let 关键字达成这一点:

let callbacks = [] for (let i = 0; i <= 2; i++) { // 这里的 i 属于当前块作用域 callbacks[i] = function () { return i * 2 } } callbacks[0]() === 0 callbacks[1]() === 2 callbacks[2]() === 4

1
2
3
4
5
6
7
8
9
10
let callbacks = []
for (let i = 0; i <= 2; i++) {
    // 这里的 i 属于当前块作用域
    callbacks[i] = function () {
        return i * 2
    }
}
callbacks[0]() === 0
callbacks[1]() === 2
callbacks[2]() === 4

我发现理解闭包的内在原理会使开发者们在使用开发工具时有更大的把握。所以,本文将致力于讲解闭包是如何工作的以及其工作原理的具体细节。

词法作用域

词法作用域是 JavaScript 闭包特性的重要保证,笔者在基于 JSX 的动态数据绑定一文中也介绍了如何利用词法作用域的特性来实现动态数据绑定。一般来说,在编程语言里我们常见的变量作用域就是词法作用域与动态作用域(Dynamic Scope),绝大部分的编程语言都是使用的词法作用域。词法作用域注重的是所谓的 Write-Time,即编程时的上下文,而动态作用域以及常见的 this 的用法,都是 Run-Time,即运行时上下文。词法作用域关注的是函数在何处被定义,而动态作用域关注的是函数在何处被调用。JavaScript 是典型的词法作用域的语言,即一个符号参照到语境中符号名字出现的地方,局部变量缺省有着词法作用域。此二者的对比可以参考如下这个例子:

function foo() { console.log( a ); // 2 in Lexical Scope ,But 3 in Dynamic Scope } function bar() { var a = 3; foo(); } var a = 2; bar();

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
    console.log( a ); // 2 in Lexical Scope ,But 3 in Dynamic Scope
}
 
function bar() {
var a = 3;
    foo();
}
 
var a = 2;
 
bar();

希望在你能从中获得更好的知识储备,以便在日常工作中更好地利用闭包。让我们开始吧!

执行上下文与提升

作用域(Scope)与上下文(Context)常常被用来描述相同的概念,不过上下文更多的关注于代码中 this 的使用,而作用域则与变量的可见性相关;而 JavaScript 规范中的执行上下文(Execution Context)其实描述的是变量的作用域。众所周知,JavaScript 是单线程语言,同时刻仅有单任务在执行,而其他任务则会被压入执行上下文队列中(更多知识可以阅读 Event Loop 机制详解与实践应用);每次函数调用时都会创建出新的上下文,并将其添加到执行上下文队列中。

什么是闭包?

执行上下文

每个执行上下文又会分为内存创建(Creation Phase)与代码执行(Code Execution Phase)两个步骤,在创建步骤中会进行变量对象的创建(Variable Object)、作用域链的创建以及设置当前上下文中的 this 对象。所谓的 Variable Object ,又称为 Activation Object,包含了当前执行上下文中的所有变量、函数以及具体分支中的定义。当某个函数被执行时,解释器会先扫描所有的函数参数、变量以及其他声明:

'variableObject': { // contains function arguments, inner variable and function declarations }

1
2
3
'variableObject': {
    // contains function arguments, inner variable and function declarations
}

在 Variable Object 创建之后,解释器会继续创建作用域链(Scope Chain);作用域链往往指向其副作用域,往往被用于解析变量。当需要解析某个具体的变量时,JavaScript 解释器会在作用域链上递归查找,直到找到合适的变量或者任何其他需要的资源。作用域链可以被认为是包含了其自身 Variable Object 引用以及所有的父 Variable Object 引用的对象:

'scopeChain': { // contains its own variable object and other variable objects of the parent execution contexts }

1
2
3
'scopeChain': {
    // contains its own variable object and other variable objects of the parent execution contexts
}

而执行上下文则可以表述为如下抽象对象:

executionContextObject = { 'scopeChain': {}, // contains its own variableObject and other variableObject of the parent execution contexts 'variableObject': {}, // contains function arguments, inner variable and function declarations 'this': valueOfThis }

1
2
3
4
5
executionContextObject = {
    'scopeChain': {}, // contains its own variableObject and other variableObject of the parent execution contexts
    'variableObject': {}, // contains function arguments, inner variable and function declarations
    'this': valueOfThis
}

闭包是 JavaScript (以及其他大多数编程语言) 的一个极其强大的属性。正如在MDN(Mozilla Developer Network) 中定义的那样:

变量的生命周期与提升

变量的生命周期包含着变量声明(Declaration Phase)、变量初始化(Initialization Phase)以及变量赋值(Assignment Phase)三个步骤;其中声明步骤会在作用域中注册变量,初始化步骤负责为变量分配内存并且创建作用域绑定,此时变量会被初始化为 undefined,最后的分配步骤则会将开发者指定的值分配给该变量。传统的使用 var 关键字声明的变量的生命周期如下:

而 let 关键字声明的变量生命周期如下:

如上文所说,我们可以在某个变量或者函数定义之前访问这些变量,这即是所谓的变量提升(Hoisting)。传统的 var 关键字声明的变量会被提升到作用域头部,并被赋值为 undefined:

// var hoisting num; // => undefined var num; num = 10; num; // => 10 // function hoisting getPi; // => function getPi() {...} getPi(); // => 3.14 function getPi() { return 3.14; }

1
2
3
4
5
6
7
8
9
10
11
// var hoisting
num;     // => undefined  
var num;  
num = 10;  
num;     // => 10  
// function hoisting
getPi;   // => function getPi() {...}  
getPi(); // => 3.14  
function getPi() {  
return 3.14;
}

变量提升只对 var 命令声明的变量有效,如果一个变量不是用 var 命令声明的,就不会发生变量提升。

console.log(b); b = 1;

1
2
console.log(b);
b = 1;

上面的语句将会报错,提示 ReferenceError: b is not defined,即变量 b 未声明,这是因为 b 不是用 var 命令声明的,JavaScript 引擎不会将其提升,而只是视为对顶层对象的 b 属性的赋值。ES6 引入了块级作用域,块级作用域中使用 let 声明的变量同样会被提升,只不过不允许在实际声明语句前使用:

> let x = x; ReferenceError: x is not defined at repl:1:9 at ContextifyScript.Script.runInThisContext (vm.js:44:33) at REPLServer.defaultEval (repl.js:239:29) at bound (domain.js:301:14) at REPLServer.runBound [as eval] (domain.js:314:12) at REPLServer.onLine (repl.js:433:10) at emitOne (events.js:120:20) at REPLServer.emit (events.js:210:7) at REPLServer.Interface._onLine (readline.js:278:10) at REPLServer.Interface._line (readline.js:625:8) > let x = 1; SyntaxError: Identifier 'x' has already been declared

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> let x = x;
ReferenceError: x is not defined
    at repl:1:9
    at ContextifyScript.Script.runInThisContext (vm.js:44:33)
    at REPLServer.defaultEval (repl.js:239:29)
    at bound (domain.js:301:14)
    at REPLServer.runBound [as eval] (domain.js:314:12)
    at REPLServer.onLine (repl.js:433:10)
    at emitOne (events.js:120:20)
    at REPLServer.emit (events.js:210:7)
    at REPLServer.Interface._onLine (readline.js:278:10)
    at REPLServer.Interface._line (readline.js:625:8)
> let x = 1;
SyntaxError: Identifier 'x' has already been declared

 

函数的生命周期与提升

基础的函数提升同样会将声明提升至作用域头部,不过不同于变量提升,函数同样会将其函数体定义提升至头部;譬如:

function b() { a = 10; return; function a() {} }

1
2
3
4
5
function b() {  
   a = 10;  
return;  
function a() {}
}

会被编译器修改为如下模式:

function b() { function a() {} a = 10; return; }

1
2
3
4
5
function b() {
function a() {}
  a = 10;
return;
}

在内存创建步骤中,JavaScript 解释器会通过 function 关键字识别出函数声明并且将其提升至头部;函数的生命周期则比较简单,声明、初始化与赋值三个步骤都被提升到了作用域头部:

如果我们在作用域中重复地声明同名函数,则会由后者覆盖前者:

sayHello(); function sayHello () { function hello () { console.log('Hello!'); } hello(); function hello () { console.log('Hey!'); } } // Hey!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sayHello();
 
function sayHello () {
function hello () {
        console.log('Hello!');
    }
 
    hello();
 
function hello () {
        console.log('Hey!');
    }
}
 
// Hey!

而 JavaScript 中提供了两种函数的创建方式,函数声明(Function Declaration)与函数表达式(Function Expression);函数声明即是以 function 关键字开始,跟随者函数名与函数体。而函数表达式则是先声明函数名,然后赋值匿名函数给它;典型的函数表达式如下所示:

var sayHello = function() { console.log('Hello!'); }; sayHello(); // Hello!

1
2
3
4
5
6
7
var sayHello = function() {
  console.log('Hello!');
};
 
sayHello();
 
// Hello!

函数表达式遵循变量提升的规则,函数体并不会被提升至作用域头部:

sayHello(); function sayHello () { function hello () { console.log('Hello!'); } hello(); var hello = function () { console.log('Hey!'); } } // Hello!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sayHello();
 
function sayHello () {
function hello () {
        console.log('Hello!');
    }
 
    hello();
 
var hello = function () {
        console.log('Hey!');
    }
}
 
// Hello!

在 ES5 中,是不允许在块级作用域中创建函数的;而 ES6 中允许在块级作用域中创建函数,块级作用域中创建的函数同样会被提升至当前块级作用域头部与函数作用域头部。不同的是函数体并不会再被提升至函数作用域头部,而仅会被提升到块级作用域头部:

f; // Uncaught ReferenceError: f is not defined (function () { f; // undefined x; // Uncaught ReferenceError: x is not defined if (true) { f(); let x; function f() { console.log('I am function!'); } } }());

1
2
3
4
5
6
7
8
9
10
11
f; // Uncaught ReferenceError: f is not defined
(function () {
  f; // undefined
  x; // Uncaught ReferenceError: x is not defined
if (true) {
    f();
    let x;
function f() { console.log('I am function!'); }
  }
 
}());

闭包是指能够访问自由变量的函数。换句话说,在闭包中定义的函数可以“记忆”它被创建的环境。

避免全局变量

在计算机编程中,全局变量指的是在所有作用域中都能访问的变量。全局变量是一种不好的实践,因为它会导致一些问题,比如一个已经存在的方法和全局变量的覆盖,当我们不知道变量在哪里被定义的时候,代码就变得很难理解和维护了。在 ES6 中可以利用 let关键字来声明本地变量,好的 JavaScript 代码就是没有定义全局变量的。在 JavaScript 中,我们有时候会无意间创建出全局变量,即如果我们在使用某个变量之前忘了进行声明操作,那么该变量会被自动认为是全局变量,譬如:

function sayHello(){ hello = "Hello World"; return hello; } sayHello(); console.log(hello);

1
2
3
4
5
6
function sayHello(){
  hello = "Hello World";
return hello;
}
sayHello();
console.log(hello);

在上述代码中因为我们在使用 sayHello 函数的时候并没有声明 hello 变量,因此其会创建作为某个全局变量。如果我们想要避免这种偶然创建全局变量的错误,可以通过强制使用 strict mode 来禁止创建全局变量。

注:自由变量是既不是在本地声明又不作为参数传递的一类变量。(译者注:如果一个作用域中使用的变量并不是在该作用域中声明的,那么这个变量对于该作用域来说就是自由变量)

函数包裹

为了避免全局变量,第一件事情就是要确保所有的代码都被包在函数中。最简单的办法就是把所有的代码都直接放到一个函数中去:

(function(win) { "use strict"; // 进一步避免创建全局变量 var doc = window.document; // 在这里声明你的变量 // 一些其他的代码 }(window));

1
2
3
4
5
6
(function(win) {
    "use strict"; // 进一步避免创建全局变量
var doc = window.document;
    // 在这里声明你的变量
    // 一些其他的代码
}(window));

 

声明命名空间

var MyApp = { namespace: function(ns) { var parts = ns.split("."), object = this, i, len; for(i = 0, len = parts.lenght; i < len; i ++) { if(!object[parts[i]]) { object[parts[i]] = {}; } object = object[parts[i]]; } return object; } }; // 定义命名空间 MyApp.namespace("Helpers.Parsing"); // 你现在可以使用该命名空间了 MyApp.Helpers.Parsing.DateParser = function() { //做一些事情 };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var MyApp = {
    namespace: function(ns) {
var parts = ns.split("."),
            object = this, i, len;
for(i = 0, len = parts.lenght; i < len; i ++) {
if(!object[parts[i]]) {
                object[parts[i]] = {};
            }
            object = object[parts[i]];
        }
return object;
    }
};
 
// 定义命名空间
MyApp.namespace("Helpers.Parsing");
 
// 你现在可以使用该命名空间了
MyApp.Helpers.Parsing.DateParser = function() {
    //做一些事情
};

让我们来看一些例子:

模块化

另一项开发者用来避免全局变量的技术就是封装到模块 Module 中。一个模块就是不需要创建新的全局变量或者命名空间的通用的功能。不要将所有的代码都放一个负责执行任务或者发布接口的函数中。这里以异步模块定义 Asynchronous Module Definition (AMD) 为例,更详细的 JavaScript 模块化相关知识参考 JavaScript 模块演化简史

//定义 define( "parsing", //模块名字 [ "dependency1", "dependency2" ], // 模块依赖 function( dependency1, dependency2) { //工厂方法 // Instead of creating a namespace AMD modules // are expected to return their public interface var Parsing = {}; Parsing.DateParser = function() { //do something }; return Parsing; } ); // 通过 Require.js 加载模块 require(["parsing"], function(Parsing) { Parsing.DateParser(); // 使用模块 });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//定义
define( "parsing", //模块名字
        [ "dependency1", "dependency2" ], // 模块依赖
        function( dependency1, dependency2) { //工厂方法
 
            // Instead of creating a namespace AMD modules
            // are expected to return their public interface
            var Parsing = {};
            Parsing.DateParser = function() {
              //do something
            };
return Parsing;
        }
);
 
// 通过 Require.js 加载模块
require(["parsing"], function(Parsing) {
    Parsing.DateParser(); // 使用模块
});

1 赞 2 收藏 1 评论

图片 1

Example 1:

JavaScript

Function numberGenerator() { // Local “free” variable that ends up within the closure var num = 1; function checkNumber() { console.log(num); } num++; return checkNumber; } var number = numberGenerator(); number(); // 2

1
2
3
4
5
6
7
8
9
10
11
Function numberGenerator() {
  // Local “free” variable that ends up within the closure
  var num = 1;
  function checkNumber() {
    console.log(num);
  }
  num++;
  return checkNumber;
}
var number = numberGenerator();
number(); // 2

在 GitHub 上查看** rawnumberGenerator.js **

在以上例子中,numberGenerator 函数创建了一个局部的自由变量 num (一个数字) 和 checkNumber 函数 (一个在控制台打印 num 的函数)。checkNumber 函数没有自己的局部变量,但是,由于使用了闭包,它可以通过 numberGenerator 这个外部函数来访问(外部声明的)变量。因此即使在 numberGenerator 函数被返回以后,checkNumber 函数也可以使用 numberGenerator 中声明的变量 num 从而成功地在控制台记录日志。

Example 2:

JavaScript

function sayHello() { var say = function() { console.log(hello); } // Local variable that ends up within the closure var hello = 'Hello, world!'; return say; } var sayHelloClosure = sayHello(); sayHelloClosure(); // ‘Hello, world!’

1
2
3
4
5
6
7
8
function sayHello() {
  var say = function() { console.log(hello); }
  // Local variable that ends up within the closure
  var hello = 'Hello, world!';
  return say;
}
var sayHelloClosure = sayHello();
sayHelloClosure(); // ‘Hello, world!’

在 GitHub 上查看 raw[sayHello.js]()

在这个例子中我们演示了一个闭包包含了外围函数中声明的全部局部变量。

请注意,变量 hello 是在匿名函数之后定义的,但是该匿名函数仍然可以访问到 hello 这个变量。这是因为变量hello在创建这个函数的“作用域”时就已经被定义了,这使得它在匿名函数最终执行的时候是可用的。(不必担心,我会在本文的后面解释“作用域”是什么,现在暂时跳过它!)

深入理解闭包

这些例子从更深层次阐述了什么是闭包。总体来说情况是这样的:即使声明这些变量的外围函数已经返回以后,我们仍然可以访问在外围函数中声明的变量。显然,在这背后有一些事情发生了,使得这些变量在外围函数返回值以后仍然可以被访问到。

为了理解这是如何发生的,我们需要接触到几个相关的概念——从3000英尺的高空(抽象的概念)逐步地返回到闭包的“陆地”上来。让我们从函数运行中最重要的内容——“执行上下文”开始吧!

Execution Context   执行上下文

执行上下文是一个抽象的概念,ECMAScript 规范使用它来追踪代码的执行。它可能是你的代码第一次执行或执行的流程进入函数主体时所在的全局上下文。

图片 2

执行上下文

在任意一个时间点,只能有唯一一个执行上下文在运行之中。这就是为什么 JavaScript 是“单线程”的原因,意思就是一次只能处理一个请求。一般来说,浏览器会用“栈”来保存这个执行上下文。栈是一种“后进先出” (Last In First Out) 的数据结构,即最后插入该栈的元素会最先从栈中被弹出(这是因为我们只能从栈的顶部插入或删除元素)。当前的执行上下文,或者说正在运行中的执行上下文永远在栈顶。当运行中的上下文被完全执行以后,它会由栈顶弹出,使得下一个栈顶的项接替它成为正在运行的执行上下文。

除此之外,一个执行上下文正在运行并不代表另一个执行上下文需要等待它完成运行之后才可以开始运行。有时会出现这样的情况,一个正在运行中的上下文暂停或中止,另外一个上下文开始执行。暂停的上下文可能在稍后某一时间点从它中止的位置继续执行。一个新的执行上下文被创建并推入栈顶,成为当前的执行上下文,这就是执行上下文替代的机制。

图片 3

以下是这个概念在浏览器中的行为实例:

JavaScript

var x = 10; function foo(a) { var b = 20; function bar(c) { var d = 30; return boop(x + a + b + c + d); } function boop(e) { return e * -1; } return bar; } var moar = foo(5); // Closure /* The function below executes the function bar which was returned when we executed the function foo in the line above. The function bar invokes boop, at which point bar gets suspended and boop gets push onto the top of the call stack (see the screenshot below) */ moar(15);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var x = 10;
function foo(a) {
  var b = 20;
  function bar(c) {
    var d = 30;
    return boop(x + a + b + c + d);
  }
  function boop(e) {
    return e * -1;
  }
  return bar;
}
var moar = foo(5); // Closure
/*
  The function below executes the function bar which was returned
  when we executed the function foo in the line above. The function bar
  invokes boop, at which point bar gets suspended and boop gets push
  onto the top of the call stack (see the screenshot below)
*/
moar(15);

在 GitHub 上查看 raw[executionContext.js]()

图片 4

当 boop 返回时,它会从栈中弹出,bar 函数会恢复运行:

图片 5

当我们有很多执行上下文一个接一个地运行时——通常情况下会在中间暂停然后再恢复运行——为了能很好地管理这些上下文的顺序和执行情况,我们需要用一些方法来对其状态进行追踪。而实际上也是如此,根据ECMAScript的规范,每个执行上下文都有用于跟踪代码执行进程的各种状态的组件。包括:

  • 代码执行状态:任何需要开始运行,暂停和恢复执行上下文相关代码执行的状态
  • 函数:上下文中正在执行的函数对象(正在执行的上下文是脚本或模块的情况下可能是null)
  • Realm:一系列内部对象,一个ECMAScript全局环境,所有在全局环境的作用域内加载的ECMAScript代码,和其他相关的状态及资源。
  • 词法环境:用于解决此执行上下文内代码所做的标识符引用。
  • 变量环境:一种词法环境,该词法环境的环境记录保留了变量声明时在执行上下文中创建的绑定关系。

如果以上这些让你读起来很困惑,不必担心。在所有变量之中,词法环境变量是我们最感兴趣的一个,因为它明确声明它解决了这个执行上下文内代码中的“标识符引用”。你可以把“标识符”想成是变量。由于我们最初的目的就是弄清楚它是如何做到在一个函数(或“上下文”)返回以后还能神奇地访问变量,因此词法环境看起来就是我们需要深入挖掘的东西!

注意:从技术上来说,变量环境和词法环境都是用来实现闭包的,但为了简单起见,我们将这二者归纳为“环境”。想了解关于词法环境和变量环境的区别的更详尽的解释,可以参看 Alex Rauschmayer 博士这篇非常棒的文章。

词法环境

定义:词法环境是一个基于 ECMAScript 代码的词法嵌套结构来定义特定变量和函数标识符的关联的规范类型。词法环境由一个环境记录及一个可能为空的对外部词法环境的引用构成。通常,一个词法环境会与ECMAScript代码的一些特定语法结构相关联,例如:FunctionDeclaration(函数声明), BlockStatement(块语句), TryStatement(Try语句)的Catch clause(Catch子句)。每当此类代码执行时,都会创建一个新的词法环境。— ECMAScript-262/6.0

让我们来把这个概念分解一下。

  • “用于定义标识符的关联”:词法环境目的就是在代码中管理数据(即标识符)。换句话说,它给标识符赋予了含义。比如当我们写出这样一行代码 “log(x /10)”如果我们没有给变量x赋予一些含义(声明变量 x),那么这个变量(或者说标识符)x 就是毫无意义的。词法环境就通过它的环境记录(参见下文)提供了这个含义(或“关联”)。
  • “词法环境包含一个环境记录”:环境记录保留了所有存在于该词法环境中的标识符及其绑定的记录。每一个词法环境都有它自己的环境记录。
  • “词法嵌套结构”:这是最有趣的部分,它大致说明了一个内部环境引用了包围它的外部环境,同时,这个外部环境还可以有它自己的外部环境。结果就是,一个环境可以作为外部环境服务于多个内部环境。全局环境是唯一一个没有外部环境的词法环境。这里会有一点难理解,让我们来用一个比喻:把词法环境想成是洋葱的层,全局环境是洋葱的最外层,随后的每一层都依次被嵌套在内部。

图片 6

Source: 

抽象地来说,(嵌套的)环境就像下面的伪代码中表现的这样:

LexicalEnvironment = { EnvironmentRecord: { // Identifier bindings go here }, // Reference to the outer environment outer: < > };

1
2
3
4
5
6
7
LexicalEnvironment = {
  EnvironmentRecord: {
  // Identifier bindings go here
  },
  // Reference to the outer environment
  outer: < >
};

在 GitHub 上查看 rawlexicalEnv.js

  • “每当此类代码执行时,就会创建一个新的词法环境”:每次一个外围函数被调用时,就会创建一个新的词法环境。这很重要——我们会在文末再回到这一点。(边注:函数并不是创建词法环境的唯一途径。其他途径包括:块语句或 catch 子句。为简单起见,我会在本文中将重点放在通过函数创建环境)

总之,每个执行上下文都有一个词法环境。这个词法环境保留了变量和与其相关联的值,以及对其外部环境的引用。词法环境可以是全局环境,模块的环境(包含一个模块的顶级声明的绑定),或是函数的环境(该环境随着函数的调用而创建)。

作用域链

基于以上概念,我们知道了一个环境可以访问它的父环境,并且该父环境还可以继续访问它的父环境,以此类推。每个环境能够访问的一系列标识符,我们称其为“作用域”。我们可以将多个作用域嵌套到一个环境的分级链式结构中,即“作用域链”。

让我们来看这种嵌套结构的一个例子:

JavaScript

var x = 10; function foo() { var y = 20; // free variable function bar() { var z = 15; // free variable return x + y + z; } return bar; }

1
2
3
4
5
6
7
8
9
var x = 10;
function foo() {
  var y = 20; // free variable
  function bar() {
    var z = 15; // free variable
    return x + y + z;
  }
  return bar;
}

在 GitHub 上查看 rawnesting.js

可以看到,bar 嵌套在 foo 之中。为了帮助你更清晰地看到嵌套结构,请看下方图解:

图片 7

我们会在本文的后面重温这个例子。

这个作用域链,或者说与函数相关联的环境链,在函数被创建时就被保存在函数对象当中。换句话说,它按照位置被静态地定义在源代码内部。(这也被称为“词法作用域”。)

让我们来快速地绕个路,来理解一下“动态作用域”和“静态作用域”的区别。它讲帮助我们阐明为什么想实现闭包,静态作用域(或词法作用域)是必不可少的。

动态作用域 vs. 静态作用域

动态作用域的语言“基于栈来实现”,意思就是函数的局部变量和参数都储存在栈中。因此,程序堆栈的运行状态决定你引用的是什么变量。

另一方面,静态作用域是指当创建上下文时,被引用的变量就被记录在其中。也就是说,这个程序的源代码结构决定你指向的是什么变量。

此刻你可能会想动态作用域和静态作用域究竟有何不同。在此我们借助两个例子来说明:

Example 1:

JavaScript

var x = 10; function foo() { var y = x + 5; return y; } function bar() { var x = 2; return foo(); } function main() { foo(); // Static scope: 15; Dynamic scope: 15 bar(); // Static scope: 15; Dynamic scope: 7 return 0; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var x = 10;
function foo() {
  var y = x + 5;
  return y;
}
function bar() {
  var x = 2;
  return foo();
}
function main() {
  foo(); // Static scope: 15; Dynamic scope: 15
  bar(); // Static scope: 15; Dynamic scope: 7
  return 0;
}

在 GitHub 上查看 rawstaticvsdynamic1.js

从上述代码我们看到,当调用函数 bar 的时候,静态作用域和动态作用域返回了不同的值。

在静态作用域中,bar 的返回值是基于函数 foo 创建时 x 的值。这是因为源代码的静态和词法的结构导致 x 是 10 而最终结果是 15.

而另一方面,动态作用域给了我们一个在运行时追踪变量定义的栈——因此,由于我们使用的 x 在运行时被动态地定义,所以它的值取决于 x 在当前作用域中的实际的定义。函数 bar 在运行时将 x=2 推入栈顶,从而使得 foo 返回 7.

Example 2:

JavaScript

var myVar = 100; function foo() { console.log(myVar); } foo(); // Static scope: 100; Dynamic scope: 100 (function () { var myVar = 50; foo(); // Static scope: 100; Dynamic scope: 50 })(); // Higher-order function (function (arg) { var myVar = 1500; arg(); // Static scope: 100; Dynamic scope: 1500 })(foo);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var myVar = 100;
function foo() {
  console.log(myVar);
}
foo(); // Static scope: 100; Dynamic scope: 100
(function () {
  var myVar = 50;
  foo(); // Static scope: 100; Dynamic scope: 50
})();
// Higher-order function
(function (arg) {
  var myVar = 1500;
  arg();  // Static scope: 100; Dynamic scope: 1500
})(foo);

在 GitHub 上查看 rawstaticvsdynamic2.js

类似地,在以上动态作用域的例子中,变量 myVar 是通过被调用的函数中(动态定义)的 myVar 来解析的 ,而相对静态作用域来说,myVar 解析为在创建时即储存于两个立即调用函数(IIFE, Immediately Invoked Function Expression)的作用域中的变量。

可以看到,动态作用域通常会导致一些歧义。它没有明确自由变量会从哪个作用域被解析。

闭包

你可能会认为以上讨论是题外话,但事实上,我们已经覆盖了需要用来理解闭包的所有(知识):

每个函数都有一个执行上下文,它包括一个在函数中能够赋予变量含义的环境和一个对其父环境的引用。对父环境的引用使得它父环境中的所有变量可以用于内部函数,无论内部函数是在创建它们(这些变量)的作用域以外还是以内调用的。

因此,这看起来就像是函数会“记得”这个环境(或者说作用域),因为字面上来看函数能够引用环境(和环境中定义的变量)!

让我们回到这个嵌套结构的例子

JavaScript

var x = 10; function foo() { var y = 20; // free variable function bar() { var z = 15; // free variable return x + y + z; } return bar; } var test = foo(); test(); // 45

1
2
3
4
5
6
7
8
9
10
11
var x = 10;
function foo() {
   var y = 20; // free variable
   function bar() {
    var z = 15; // free variable
    return x + y + z;
  }
  return bar;
}
var test = foo();
test(); // 45

在 GitHub 上查看 rawnesting2.js

基于我们对环境如何运作的理解,我们可以说,在上述例子中环境的定义看起来就像是以下代码中这样的(注意,这只是伪代码而已):

GlobalEnvironment = {   EnvironmentRecord: {     // built-in identifiers     Array: '<func>',     Object: '<func>',     // etc..     // custom identifiers      x: 10    },    outer: null  }; fooEnvironment = {   EnvironmentRecord: {     y: 20,     bar: '<func>'    }   outer: GlobalEnvironment }; barEnvironment = {   EnvironmentRecord: {     z: 15   }   outer: fooEnvironment };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
GlobalEnvironment = {
  EnvironmentRecord: {
    // built-in identifiers
    Array: '<func>',
    Object: '<func>',
    // etc..
    // custom identifiers 
    x: 10 
  }, 
  outer: null 
};
fooEnvironment = {
  EnvironmentRecord: {
    y: 20,
    bar: '<func>' 
  }
  outer: GlobalEnvironment
};
barEnvironment = {
  EnvironmentRecord: {
    z: 15
  }
  outer: fooEnvironment
};

在 GitHub 上查看 rawnestingEnv.js

当我们调用函数test,我们得到的值是 45,它也是调用函数 bar 的返回值(因为 foo 返回函数 bar)。即使 foo 已经返回了值,但是 bar 仍然可以访问自由变量 y,因为 bar 通过外部环境引用 y,这个外部环境即 foo 的环境!bar 还可以访问全局变量 x,因为 foo 的环境通向全局环境。这叫做“作用域链查找”。

回到我们关于动态作用域和静态作用域的讨论:为了实现闭包,我们不能经由一个动态的栈来储存变量(不能使用动态作用域)。原因是,这(使用动态作用域)意味着当一个函数返回时,变量将会从栈中弹出并且不再可用——这与我们最初定义的闭包相互矛盾。真正的情况应该正相反,闭包中父上下文的数据储存于“堆”(heap,一种数据结构)中,它允许数据在调用的函数返回(也就是在执行上下文在执行调用的栈中弹出)以后仍然能够保留。

明白了吗?好的!既然我们已经从抽象的层面理解了内在含义,让我们来多看几个例子:

Example 1:

我们在 for-loop 中试图将其中的计数变量和其它函数关联在一起时的一个典型的例子/错误:

JavaScript

var result = []; for (var i = 0; i < 5; i++) { result[i] = function () { console.log(i); }; } result[0](); // 5, expected 0 result[1](); // 5, expected 1 result[2](); // 5, expected 2 result[3](); // 5, expected 3 result[4](); // 5, expected 4

1
2
3
4
5
6
7
8
9
10
11
var result = [];
for (var i = 0; i < 5; i++) {
  result[i] = function () {
    console.log(i);
  };
}
result[0](); // 5, expected 0
result[1](); // 5, expected 1
result[2](); // 5, expected 2
result[3](); // 5, expected 3
result[4](); // 5, expected 4

在 GitHub 上查看 rawforloopwrong.js

回顾我们刚刚学习的知识,就会超级容易看出这里的错误!用伪代码来分析,当 for-loop 存在时,它的环境看起来是这样的:

environment: { EnvironmentRecord: { result: [...], i: 5 }, outer: null, }

1
2
3
4
5
6
7
environment: {
  EnvironmentRecord: {
    result: [...],
    i: 5
  },
  outer: null,
}

在 GitHub 上查看 rawforloopwrongenv.js

这里错误的假设就是,在结果(result)数列中,五个函数的作用域是不同的。事实上正相反,实际上五个函数的环境(上下文/作用域)全部相同。因此,每次变量i增加时,作用域都会更新——这个作用域被所有函数共享。这就是为什么这五个函数中的任意一个在访问i时都返回 5(i 在 for-loop 存在时等于 5)。

一个解决办法就是为每个函数创建一个额外的封闭环境,使得它们各自都有自己的执行上下文/作用域。

JavaScript

var result = []; for (var i = 0; i < 5; i++) { result[i] = (function inner(x) { // additional enclosing context return function() { console.log(x); } })(i); } result[0](); // 0, expected 0 result[1](); // 1, expected 1 result[2](); // 2, expected 2 result[3](); // 3, expected 3 result[4](); // 4, expected 4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var result = [];
for (var i = 0; i < 5; i++) {
  result[i] = (function inner(x) {
    // additional enclosing context
    return function() {
      console.log(x);
    }
  })(i);
}
result[0](); // 0, expected 0
result[1](); // 1, expected 1
result[2](); // 2, expected 2
result[3](); // 3, expected 3
result[4](); // 4, expected 4

在 GitHub 上查看 rawforloopcorrect.js

耶!这样就改好了:)

另外,一个非常聪明的途径就是使用 let 来代替 var,因为 let 声明的是块级作用域,因此每次 for-loop 的迭代都会创建一个新的标识符绑定。

JavaScript

var result = []; for (let i = 0; i < 5; i++) { result[i] = function () { console.log(i); }; } result[0](); // 0, expected 0 result[1](); // 1, expected 1 result[2](); // 2, expected 2 result[3](); // 3, expected 3 result[4](); // 4, expected 4

1
2
3
4
5
6
7
8
9
10
11
var result = [];
for (let i = 0; i < 5; i++) {
  result[i] = function () {
    console.log(i);
  };
}
result[0](); // 0, expected 0
result[1](); // 1, expected 1
result[2](); // 2, expected 2
result[3](); // 3, expected 3
result[4](); // 4, expected 4

在 GitHub 上查看 rawforlooplet.js

(感叹!)

Example 2:

这个例子展示了每调用一次函数就会创建一个新的单独的闭包:

JavaScript

function iCantThinkOfAName(num, obj) { // This array variable, along with the 2 parameters passed in, // are 'captured' by the nested function 'doSomething' var array = [1, 2, 3]; function doSomething(i) { num += i; array.push(num); console.log('num: ' + num); console.log('array: ' + array); console.log('obj.value: ' + obj.value); } return doSomething; } var referenceObject = { value: 10 }; var foo = iCantThinkOfAName(2, referenceObject); // closure #1 var bar = iCantThinkOfAName(6, referenceObject); // closure #2 foo(2); /* num: 4 array: 1,2,3,4 obj.value: 10 */ bar(2); /* num: 8 array: 1,2,3,8 obj.value: 10 */ referenceObject.value++; foo(4); /* num: 8 array: 1,2,3,4,8 obj.value: 11 */ bar(4); /* num: 12 array: 1,2,3,8,12 obj.value: 11 */

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function iCantThinkOfAName(num, obj) {
  // This array variable, along with the 2 parameters passed in,
  // are 'captured' by the nested function 'doSomething'
  var array = [1, 2, 3];
  function doSomething(i) {
    num += i;
    array.push(num);
    console.log('num: ' + num);
    console.log('array: ' + array);
    console.log('obj.value: ' + obj.value);
  }
  return doSomething;
}
var referenceObject = { value: 10 };
var foo = iCantThinkOfAName(2, referenceObject); // closure #1
var bar = iCantThinkOfAName(6, referenceObject); // closure #2
foo(2);
/*
  num: 4
  array: 1,2,3,4
  obj.value: 10
*/
bar(2);
/*
  num: 8
  array: 1,2,3,8
  obj.value: 10
*/
referenceObject.value++;
foo(4);
/*
  num: 8
  array: 1,2,3,4,8
  obj.value: 11
*/
bar(4);
/*
  num: 12
  array: 1,2,3,8,12
  obj.value: 11
*/

在 GitHub 上查看 rawiCantThinkOfAName.js

在这个例子中,可以看到每次调用函数 iCantThinkOfAName 都会创建一个新的闭包,叫做foo和bar。随后对每个闭包函数的调用更新了其中的变量,表明在 iCantThinkOfAName 返回以后的很长一段时间,每个闭包中的变量仍能够继续在iCantThinkOfAName 的 doSomething 函数中继续使用。

Example 3:

JavaScript

function mysteriousCalculator(a, b) { var mysteriousVariable = 3; return { add: function() { var result = a + b + mysteriousVariable; return toFixedTwoPlaces(result); }, subtract: function() { var result = a - b - mysteriousVariable; return toFixedTwoPlaces(result); } } } function toFixedTwoPlaces(value) { return value.toFixed(2); } var myCalculator = mysteriousCalculator(10.01, 2.01); myCalculator.add() // 15.02 myCalculator.subtract() // 5.00

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function mysteriousCalculator(a, b) {
      var mysteriousVariable = 3;
      return {
           add: function() {
                 var result = a + b + mysteriousVariable;
                 return toFixedTwoPlaces(result);
           },
           subtract: function() {
                 var result = a - b - mysteriousVariable;
                 return toFixedTwoPlaces(result);
           }
      }
}
function toFixedTwoPlaces(value) {
      return value.toFixed(2);
}
var myCalculator = mysteriousCalculator(10.01, 2.01);
myCalculator.add() // 15.02
myCalculator.subtract() // 5.00

在 GitHub 上查看 rawmysteriousCalculator.js

可以观察到 mysteriousCalculator 在全局作用域中,并且它返回两个函数。用伪代码分析,以上例子的环境看起来是这个样子的:

GlobalEnvironment = { EnvironmentRecord: { // built-in identifiers Array: '<func>', Object: '<func>', // etc... // custom identifiers mysteriousCalculator: '<func>', toFixedTwoPlaces: '<func>', }, outer: null, }; mysteriousCalculatorEnvironment = { EnvironmentRecord: { a: 10.01, b: 2.01, mysteriousVariable: 3, } outer: GlobalEnvironment, }; addEnvironment = { EnvironmentRecord: { result: 15.02 } outer: mysteriousCalculatorEnvironment, }; subtractEnvironment = { EnvironmentRecord: { result: 5.00 } outer: mysteriousCalculatorEnvironment, };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
GlobalEnvironment = {
  EnvironmentRecord: {
    // built-in identifiers
 
    Array: '<func>',
    Object: '<func>',
    // etc...
    // custom identifiers
    mysteriousCalculator: '<func>',
    toFixedTwoPlaces: '<func>',
  },
  outer: null,
};
mysteriousCalculatorEnvironment = {
  EnvironmentRecord: {
    a: 10.01,
    b: 2.01,
    mysteriousVariable: 3,
  }
  outer: GlobalEnvironment,
};
addEnvironment = {
  EnvironmentRecord: {
    result: 15.02
  }
  outer: mysteriousCalculatorEnvironment,
};
subtractEnvironment = {
  EnvironmentRecord: {
    result: 5.00
  }
  outer: mysteriousCalculatorEnvironment,
};

在 GitHub 上查看 rawmysteriousCalculatorEnv.js

因为我们的 add 和 subtract 函数引用了 mysteriousCalculator 函数的环境,这两个函数能够使用该环境中的变量来计算结果。

Example 4:

最后一个例子表明了闭包的一个非常重要的用途:保留外部作用域对一个变量的私有引用(仅通过唯一途径例如某一个特定函数来访问一个变量)。

JavaScript

function secretPassword() { var password = 'xh38sk'; return { guessPassword: function(guess) { if (guess === password) { return true; } else { return false; } } } } var passwordGame = secretPassword(); passwordGame.guessPassword('heyisthisit?'); // false passwordGame.guessPassword('xh38sk'); // true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function secretPassword() {
  var password = 'xh38sk';
  return {
    guessPassword: function(guess) {
      if (guess === password) {
        return true;
      } else {
        return false;
      }
    }
  }
}
var passwordGame = secretPassword();
passwordGame.guessPassword('heyisthisit?'); // false
passwordGame.guessPassword('xh38sk'); // true

在 GitHub 上查看 raw[secretPassword.js]() 

这是一个非常强大的技术——它使闭包函数 guessPassword 能独家访问 password 变量,也保证了不能从外部(其他途径)访问 password。

太长不想看?以下是本文摘要

  • 执行上下文是由 ECMAScript 规范所使用的一个抽象的概念,它用于追踪代码的执行状态。在任意时间点,只能有唯一一个执行上下文对应正在执行的代码。
  • 每个执行上下文都有一个词法环境。这个词法环境保持着标识符的绑定(即变量和与其相关联的变量),还可以引用它的外部环境。
  • 每个环境能够访问的标识符集叫做“作用域”。我们可以将这些作用域嵌套成为一个分级的环境链——就是我们所知的“作用域链”。
  • 每个函数都有一个执行上下文,它包括一个在函数中赋予变量含义的词法环境和对其父环境的引用。因为函数对环境的引用,使它看起来就像是函数“记住了”这个环境(作用域)一样。这就是一个闭包
  • 每当一个封闭的外部函数被调用时都会创建一个闭包。换句话说,内部函数不需要为了创建闭包而返回。
  • 在 JavaScript 中,闭包是词法相关的,意思是它在源代码中由它的位置而被静态地定义。
  • 闭包有很多实际应用案例。一个非常重要的用途就是保留外部作用域对一个变量的私有引用(仅通过唯一途径例如某一个特定函数来访问一个变量)。

结语

希望这篇文章对你有一定帮助,并且能让你在头脑中形成一个关于 JavaScript 中闭包是如何实现的模型。可以看到,理解它工作原理的细节能让人更容易看懂闭包——更不用说这会让我们在debug的时候不那么头痛。

另外:人无完人,我也会犯一些错误——所以如果你发现其中的错误,请告知!

相关阅读

为简短期间,我省略了一些读者可能会感兴趣的话题。以下是我希望和大家分享的几个链接:

  • 什么是执行上下文的变量环境?Axel Rauschmayer博士做了一些非凡的工作来解释它。该链接是它的博文: 
  • 不同类型的环境记录都有什么?请在这里阅读: 
  • MDN有关闭包的一篇非常好的文章:
  • 还有其他有趣的文章?请提出建议,我会添加进来!

打赏支持我翻译更多好文章,谢谢!

打赏译者

打赏支持我翻译更多好文章,谢谢!

图片 8

1 赞 20 收藏 2 评论

关于作者:刘唱

图片 9

数据挖掘研究生 个人主页 · 我的文章 · 37 ·    

本文由金沙棋牌发布于金沙棋牌官方平台,转载请注明出处:让我们一起学习JavaScript闭包吧,变量的生命周期

关键词:

上一篇:用webgl打造自己的3D迷宫游戏

下一篇:没有了