从上下文,到作用域(彩蛋:理解闭包)彩民之

2019-10-11 12:54 来源:未知

彩民之家高手论坛 1前言

近几天在编程群中的聊天,让我发现了很多人并不清楚什么是上下文(context)、什么是作用域(scope),而且纠结在其中。我当初对这两个概念也只有粗浅的理解,不过我从一开始就不怎么困惑,因为我清楚自己对这一问题的认识边界。现在,我对它们的认识也只加深了一点点。不过,群聊中小伙伴的热情鼓舞了我——很多最最初学的小伙伴,想到和思考的是很多我从没考虑过的问题,小伙伴们真是达到了“进一寸有一寸的欢喜”这一境界。见贤思齐,我决定把这一点点进步记录下来。


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

JavaScript的闭包

我们都会遇到在一个外部函数套着一个内部函数的情况,比如说:

function foo(x) {
    var tmp = 3;
    function b(y) {
        alert(x   y   (  tmp));
    }
    b(2);
    b(3);
}
foo(0);

在foo函数结束的时候,tmp就会被销毁。一般来说,当内部函数被return的时候,外部就可以引用内部的函数,闭包就会通过return而产生。如:

function foo(x) {
    var tmp = 3;
    return function (y) {
        alert(x   y   (  tmp));
    }
}
var bar = foo(2); // bar 现在是一个闭包
bar(10);

按照我们原本的理解,在没有闭包的情况下,foo函数执行完,它内部的tmp变量就会被销毁,但是因为外部函数引用了内部的变量产生了闭包,内部函数的词法上下文没有被销毁,tmp变量也没有被销毁。

当然,也有不用闭包的return的例子,比如利用setInterval或者绑定一个事件等等方法:

function a(){
  var temp = 0;// let也可以
  function b(){
    console.log(temp  );
  }
  // setInterval可以产生闭包
  setInterval(b,1000);
  // 绑定可以产生闭包
  window.addEventListener('click',b);
  // ajax传入callback可以产生闭包
  ajax(b);
  // 或者直接把这个函数传给window或者其它函数外部的元素
  window.closure = b;
}
a();

可以看到,只要内部函数有机会在函数外部被调用,或者说内部函数被外部的某个变量引用,就会产生闭包。就像《深入浅出Node.js》中提到的那样:

闭包是JavaScript中的高级特性,利用它可以产生很多巧妙的效果。它的问题在于,一旦有变量引用了这个中间函数,这个中间函数不会释放,同时也使得原始作用域不会得到释放。作用域中产生的内存占用也不会被释放。除非不再有引用,才会逐步释放。

参考自 《深入浅出Node.js》

从上下文,到作用域(彩蛋:理解闭包)

2017/10/18 · JavaScript · 1 评论 · 作用域

原文出处: 天方夜   

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);

对Closure的一些定义

各种专业文献上的"闭包"(closure)定义非常抽象,很难看懂。我的理解是,闭包就是能够读取其他函数内部变量的函数。

参考自阮一峰 学习Javascript闭包(Closure)

A closure is the combination of a function and the lexical environment within which that function was declared.

参考自MDN Closure

MDN的定义指出了闭包需要的东西:闭包 = 函数 函数定义的词法上下文环境。阮一峰老师的定义指出了闭包产生的现象:一个函数能够读取其他函数内部变量

In programming languages, closures (also lexical closures or function closures) are techniques for implementing lexically scoped name binding in languages with first-class functions.

参考自wiki百科 Closure(computer programming))

wiki百科上的定义指出了闭包需要的语言条件: first-class functions。关于这个知识点可以参考“函数是一等公民”背后的含义。另外,定义中提到的implementing lexically scoped name binding ,即基于词法作用域的name绑定与scope中的binding概念相互照应。本质上就是说的就是词法作用域与变量有效性的关系。

在JavaScript中,实现外部作用域访问内部作用域中变量的方法叫做闭包。

参考自《深入浅出Node.js》

以上对闭包的定义都略有差别,有的将闭包定义为函数,有的将闭包定义为方法,也有将闭包定义为组合。我觉得将闭包理解为一个方法,或者某个东西都对。两种定义的方法都对我们理解闭包有帮助。

上下文与作用域的关系

很多人弄不清除,原因当然是既不了解上下文,也不了解作用域——我是说,几乎没有人明白上下文是什么而不明白作用域是什么,反之亦然。上下文(context)和作用域(scope)都是编译原理的知识,具体编程语言有具体的实现规则,本文关注 JavaScript 语言的实现。首先需要关注的是,这两个概念的关系非常密切,所以先了解它们的关系,有助于理解它们到底是什么。

上下文(context)和作用域(scope)的关系:

上下文是一段程序运行所需要的最小数据集合;作用域是当前上下文中,按照具体规则能够访问到的标识符(变量)的范围。

后文是对上下文和作用域更详细的解释,知道了上面指出的关系,往下阅读时就可以加深对这一关系的理解了。


闭包

闭包是前端开发中的一个重要概念,也是前端面试的必问问题之一。对于JavaScript初学者而言,闭包学习JavaScript的一大障碍。网上有很多闭包的教程,形象地告诉了我闭包长什么样。但是大部分教程没有对闭包的定义给出精准的表达,也没有对闭包背后的一些原理和逻辑进行解释。本文通过整合网上各路资料,对闭包前前后后的知识点进行梳理,希望可以帮助大家准确并且深刻理解闭包的概念。(本文假设大家对闭包有一定的理解)

彩蛋:理解闭包

上一节中的代码中,之所以输出 2,是因为 foo 是一个闭包函数。如果从本文中理解了上下文和作用域的概念,对于闭包是什么这一问题是不是感到豁然开朗?

前面说过,词法作用域也叫静态作用域,变量在词法阶段确定,也就是定义时确定。虽然在 bar 内调用,但由于 foo 是闭包函数,即使它在自己定义的词法作用域以外的地方执行,它也一直保持着自己的作用域。所谓闭包函数,即这个函数封闭了它自己的定义时的环境,形成了一个闭包,所以 foo 并不会从 bar 中寻找变量,这就是静态作用域的特点。

一个更加典型的例子是:``

JavaScript

function fn() { let a = 0; function func() { console.log(a); } return func; } let a = 1; let sub = fn(); sub(); // 0;

1
2
3
4
5
6
7
8
9
10
11
12
function fn() {
  let a = 0;
  function func() {
    console.log(a);
  }
  return func;
}
 
let a = 1;
let sub = fn();
 
sub(); // 0;

sub 就是 func 这一返回值,func 定义在 fn 内部并且被传递出来了,所以 fn 执行之后垃圾回收器依然没有回收它的内部作用域,因为 func/sub 在使用。sub 依然持有 func 定义时的作用域的引用,而这个引用就叫作闭包。调用 sub 时,它可以访问 func 定义时的词法作用域,因此找到的 a 是 fn 内部的变量 a,它的值是 0。


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

参考资料

动态作用域和词法域的区别是什么?
“函数是一等公民”背后的含义
js闭包的概念作用域内存模型
阮一峰 学习Javascript闭包(Closure)
javascript基础拾遗——词法作用域
深入理解javascript原型和闭包(12)——简介【作用域】

参考资料

You Don’t Know JS: Scope & Closures

Context (computing)

Scope (computer science)

Function.prototype.bind()

Function _.bind()

1 赞 3 收藏 1 评论

彩民之家高手论坛 2

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

Scope

要理解闭包,先要理解一个重要概念—作用域。

In computer programming, the scope of a name binding – an association of a name to an entity, such as a variable – is the region of a computer program where the binding is valid: where the name can be used to refer to the entity.

Such a region referred to as is a scope block.

参考自wiki百科 Scope (computer science)#Lexical_scoping)

scope又可以分为词法作用域(Lexical scope)和动态作用域(Dynamic scope)。两者区别与对区域这个概念的解读。Wiki百科对两者的解释如下:

In languages with lexical scope (also called static scope), name resolution depends on the location in the source code and the lexical context, which is defined by where the named variable or function is defined. In contrast, in languages with dynamic scope the name resolution depends upon the program state when the name is encountered which is determined by the execution context or calling context.

参考自wiki百科 Scope (computer science)#Lexical_scoping)

在词法作用域中,一个name是否有效取决于它在源代码中的位置,也就是词法上下文。而动态作用域要相对复杂一点,在动态作用域中,一个name是否有效取决于这个程序的运行时状态,也就是运行时上下文。

对词法作用域在JavaScript中的表现在本文不作阐述,具体参考这篇博文:深入理解javascript原型和闭包(12)——简介【作用域】

作用域

作用域(scope)是标识符(变量)在程序中的可见性范围。作用域规则是按照具体规则维护标识符的可见性,以确定当前执行的代码对这些标识符的访问权限。作用域(scope)是在具体的作用域规则之下确定的。

前面说过,有时候上下文、环境、作用域是同义词;不过,上下文(context)指代的是整体环境,作用域关注的是标识符(变量)的可访问性(可见性)。上下文确定了,根据具体编程语言的作用域规则,作用域也就确定了。这就是上下文与作用域的关系

写 JavaScript 代码时,如果 Function 作为参数,可以指定它在具体对象上调用时,这个对象常常叫做 context:

JavaScript

function callWithContext(fn, context) { return fn.call(context); } const apple = { name: "Apple" }; const orange = { name: "Orange" }; function echo() { console.log(this.name); } callWithContext(echo, apple); // Apple callWithContext(echo, orange); // Orange

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function callWithContext(fn, context) {
  return fn.call(context);
}
 
const apple = {
  name: "Apple"
};
 
const orange = {
  name: "Orange"
};
 
function echo() {
  console.log(this.name);
}
 
callWithContext(echo, apple);  // Apple
callWithContext(echo, orange); // Orange

为什么将这个参数叫做 context?因为它关系到调用环境,指定了它,就指定了函数的调用上下文。再加上具体的作用域规则,作用域也确定了。

在 JavaScript 中,这个具体的作用域规则就是词法作用域(lexical scope),也就是 JavaScript 中的作用域链的规则。词法作用域是的变量在编译时(词法阶段)就是确定的,所以词法作用域又叫静态作用域(static scope),与之相对的是动态作用域(dynamic scope)

You Don’t Know JS: Scope & Closures 用简单例子解释过动态作用域,下面用一个类似的例子说明一下:``

JavaScript

function foo() { console.log(a); } function bar() { let a = 3; foo(); } let a = 2; bar(); // 2

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
  console.log(a);
}
 
function bar() {
  let a = 3;
  foo();
}
 
let a = 2;
 
bar(); // 2

有一定 JavaScript 编程经验的人都能看出,这段程序会输出 2,但如果在动态作用域的规则下,应该输出 3,即 a 的引用不再是编译时确定,而是调用时确定的。这有点像 JavaScript 中的 this,所以 MDN 中,function.bind 的方法签名中第一个形参名称用的是 thisArg 这一更科学的名字:

fun.bind(thisArg[, arg1[, arg2[, …]]])

同样情况的还可见于 Lodash 的文档:

_.bind(func, thisArg, [partials])


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

上下文

上下文(context)是一段程序运行所需要的最小数据集合。我们可以从上下文交换(context switch)来理解上下文,在多进程或多线程环境中,任务切换时首先要中断当前的任务,将计算资源交给下一个任务。因为稍后还要恢复之前的任务,所以中断的时候要保存现场,即当前任务的上下文,也可以叫做环境。即上下文就是恢复现场所需的最小数据集合。容易把人弄晕的一点是,我们这里说的上下文环境有时候也称作作用域(scope),即这两个概念有时候是混用的。不过,它们有不同的侧重点,下一节将会说明。

另外,JavaScript 中常见的情形是一个方法/函数的执行。从一段程序的角度看,这段程序运行所需的所有变量,就是它的上下文。


执行上下文

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 上查看 rawnesting.js

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

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

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
*/
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

JavaScript

在 GitHub 上查看 rawiCantThinkOfAName.js

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 */

Example 4:

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

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

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

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

关于作者:刘唱

彩民之家高手论坛 3

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

在 GitHub 上查看 rawnestingEnv.js

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

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

 

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

结语

在 GitHub 上查看 rawforloopwrong.js

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

JavaScript

JavaScript

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

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

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

在 GitHub 上查看 rawmysteriousCalculatorEnv.js

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

彩民之家高手论坛 4

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
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
};

彩民之家高手论坛 5

 

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 上查看 rawforloopwrongenv.js

让我们来看一些例子:

Example 3:

JavaScript

彩民之家高手论坛 6

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

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

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

Example 2:

(感叹!)

彩民之家高手论坛 7

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

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

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

在 GitHub 上查看 rawlexicalEnv.js

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

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

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

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

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

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,
};

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

JavaScript

Source: 

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
LexicalEnvironment = {
  EnvironmentRecord: {
  // Identifier bindings go here
  },
  // Reference to the outer environment
  outer: < >
};

彩民之家高手论坛 8

深入理解闭包

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

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;
}

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

Example 1:

什么是闭包?

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

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

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

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

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 上查看 rawstaticvsdynamic2.js

在 GitHub 上查看 rawforloopcorrect.js

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

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

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

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

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

耶!这样就改好了:)

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

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

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 上查看 rawmysteriousCalculator.js

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

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

在 GitHub 上查看 rawforlooplet.js

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;
}

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

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

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

JavaScript

相关阅读

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

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

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

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

在 GitHub 上查看 rawstaticvsdynamic1.js

JavaScript

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

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

JavaScript

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

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);

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

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

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

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);

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

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

Execution Context   执行上下文

Example 2:

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

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

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

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

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

JavaScript

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 };

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

彩民之家高手论坛 9

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

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

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);

Example 1:

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 上查看 rawnesting2.js

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

打赏译者

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

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

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

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

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

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

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

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

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

让我们一起学习JavaScript闭包吧

JavaScript

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

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

彩民之家高手论坛 10

1 赞 20 收藏 2 评论

词法环境

Example 2:

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

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

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

Example 1:

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

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

JavaScript

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

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

JavaScript

版权声明:本文由彩民之家高手论坛发布于前端知识,转载请注明出处:从上下文,到作用域(彩蛋:理解闭包)彩民之