(本文假设大家对闭包有一定的理解真人手机网页版:),原文出处

2019-11-26 作者:首页   |   浏览(113)

作用域

开始介绍作用域链之前,先看看JavaScript中的作用域(scope)。在很多语言中(C++,C#,Java),作用域都是通过代码块(由{}包起来的代码)来决定的,但是,在JavaScript作用域是跟函数相关的,也可以说成是function-based。

例如,当for循环这个代码块结束后,依然可以访问变量”i”。

JavaScript

for(var i = 0; i < 3; i++){ console.log(i); } console.log(i); //3

1
2
3
4
5
for(var i = 0; i < 3; i++){
    console.log(i);
}
 
console.log(i); //3

对于作用域,又可以分为全局作用域(Global scope)和局部作用域(Local scpoe)。

全局作用域中的对象可以在代码的任何地方访问,一般来说,下面情况的对象会在全局作用域中:

  • 最外层函数和在最外层函数外面定义的变量
  • 没有通过关键字”var”声明的变量
  • 浏览器中,window对象的属性

局部作用域又被称为函数作用域(Function scope),所有的变量和函数只能在作用域内部使用。

JavaScript

var foo = 1; window.bar = 2; function baz(){ a = 3; var b = 4; } // Global scope: foo, bar, baz, a // Local scope: b

1
2
3
4
5
6
7
8
9
var foo = 1;
window.bar = 2;
 
function baz(){
    a = 3;
    var b = 4;
}
// Global scope: foo, bar, baz, a
// Local scope: b

对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》

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

上面这段代码中,如果我们传入不同的参数,j的打印结果是不一样的。比如:

Lexical 俘获是reference不是value

这是我的上篇博客想要强调的地方。如果上面的myObj执行时,如果俘获的是x的值,那么这三个函数func1,get1,get2就不会有任何联系了。

* *

因为俘获的是x的reference,  所以上面三个函数所看到的x是同一个变量。

 

这一点很重要,因为JavaScript中的local 变量并不是都是heap中的。起码 GOOGLE  V2 就不是。但是上面line 3 的x必须在heap中“出生和生活”,否则func1,get1,get2就会在已经毁灭了的stack 变量x上工作,使得上面的程序变得毫无意义了。

 

也就是说,闭包的runtime代价是将所被闭包的变量从stack中转到heap中。

作用域链

通过前面一篇文章了解到,每一个Execution Context中都有一个VO,用来存放变量,函数和参数等信息。

在JavaScript代码运行中,所有用到的变量都需要去当前AO/VO中查找,当找不到的时候,就会继续查找上层Execution Context中的AO/VO。这样一级级向上查找的过程,就是所有Execution Context中的AO/VO组成了一个作用域链。

所以说,作用域链与一个执行上下文相关,是内部上下文所有变量对象(包括父变量对象)的列表,用于变量查询。

JavaScript

Scope = VO/AO + All Parent VO/AOs

1
Scope = VO/AO + All Parent VO/AOs

看一个例子:

JavaScript

var x = 10; function foo() { var y = 20; function bar() { var z = 30; console.log(x + y + z); }; bar() }; foo();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var x = 10;
 
function foo() {
    var y = 20;
 
    function bar() {
        var z = 30;
 
        console.log(x + y + z);
    };
 
    bar()
};
 
foo();

上面代码的输出结果为”60″,函数bar可以直接访问”z”,然后通过作用域链访问上层的”x”和”y”。

真人手机网页版 1

  • 绿色箭头指向VO/AO
  • 蓝色箭头指向scope chain(VO/AO + All Parent VO/AOs)

再看一个比较典型的例子:

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

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

第一感觉(错觉)这段代码会输出”0,1,2″。但是根据前面的介绍,变量”i”是存放在”Global VO”中的变量,循环结束后”i”的值就被设置为3,所以代码最后的三次函数调用访问的是相同的”Global VO”中已经被更新的”i”。

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》

function foo() {

Lexical Scope and Scope Chain

Lexical Scope的概念并不是Javacript发明,但是它作为JavaScript函数的组成部分,是一个在“传统”函数概念上的附加值。

 

传统函数(C, C++, Java, C#等)的lexical scope 和runtime scope 是一样的。JavaScript 的lexical scope指的是函数定义时的“环境”,而不是函数运行时的环境。 

对于一个特定函数来说,其”自由变量”是这个函数闭包中需要俘获的主要内容。自由变量(本函数没有定义的变量)的lexical capture(俘获)顺序是 (也就是scope chaining 的顺序):

 

            A, 母函数的local 变量

            B. 母函数的input argument

            C.在母函数的母函数中重复A,B,直到最顶层(GLOBAL scope)

 

在下面 的 myObj  的定义中:

var x = 1000;                               // line 0

function *myObj(x, y) {                // Line1*

* *

*            this.func1 = function() { // Line2*

*                        x++;*

*                        y --;*

*            }*

*            this.get1 = function ()*

*            {*

*                        return x;*

*            }*

*            this.get2 = function ()*

*            {*

*                        return y;*

*            }*

*            var x = 0;                // Line 3*

}

*myObj**.prototype.AddTwo = function(z)*

{

*            return this.get1() + this.get2() + z;*

}

var m1 = new myObj(10, 20);      // Line 4

var m2 = new myObj(30, 70);      // Line 5

console.log('m1.x: ' + m1.get1());  // Line 6

console.log('m1.y: ' + m1.get2());  // Line 7

console.log('m2.x: ' + m2.get1());  // Line 8

console.log('m2.y: ' + m2.get2());  // Line 9

 

对于上面的例子,如果不是lexical scope, line 6 ~ line 9 打印的应该是10, 20, 30, 70。

但是因为lexical  scope俘获顺序,x 是0(见line 3), 所以打印的是:0, 20, 0, 70。

注释掉line3,根据俘获顺序,打印的就成了10, 20, 30, 70。

*myObj**(x, y)改成myObj(z, y),打印的就成了1000, 20, 1000, 70。其中 1000是从global里(**Line*** 0)俘获的。

结合作用域链看闭包

在JavaScript中,闭包跟作用域链有紧密的关系。相信大家对下面的闭包例子一定非常熟悉,代码中通过闭包实现了一个简单的计数器。

JavaScript

function counter() { var x = 0; return { increase: function increase() { return ++x; }, decrease: function decrease() { return --x; } }; } var ctor = counter(); console.log(ctor.increase()); console.log(ctor.decrease());

1
2
3
4
5
6
7
8
9
10
11
12
13
function counter() {
    var x = 0;
 
    return {
        increase: function increase() { return ++x; },
        decrease: function decrease() { return --x; }
    };
}
 
var ctor = counter();
 
console.log(ctor.increase());
console.log(ctor.decrease());

下面我们就通过Execution Context和scope chain来看看在上面闭包代码执行中到底做了哪些事情。

  1. 当代码进入Global Context后,会创建Global VO

真人手机网页版 2.

  • 绿色箭头指向VO/AO
  • 蓝色箭头指向scope chain(VO/AO + All Parent VO/AOs)

 

  1. 当代码执行到”var cter = counter();”语句的时候,进入counter Execution Context;根据上一篇文章的介绍,这里会创建counter AO,并设置counter Execution Context的scope chain

真人手机网页版 3

  1. 当counter函数执行的最后,并退出的时候,Global VO中的ctor就会被设置;这里需要注意的是,虽然counter Execution Context退出了执行上下文栈,但是因为ctor中的成员仍然引用counter AO(因为counter AO是increase和decrease函数的parent scope),所以counter AO依然在Scope中。

真人手机网页版 4

  1. 当执行”ctor.increase()”代码的时候,代码将进入ctor.increase Execution Context,并为该执行上下文创建VO/AO,scope chain和设置this;这时,ctor.increase AO将指向counter AO。

真人手机网页版 5

  • 绿色箭头指向VO/AO
  • 蓝色箭头指向scope chain(VO/AO + All Parent VO/AOs)
  • 红色箭头指向this
  • 黑色箭头指向parent VO/AO

 

相信看到这些,一定会对JavaScript闭包有了比较清晰的认识,也了解为什么counter Execution Context退出了执行上下文栈,但是counter AO没有销毁,可以继续访问。

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)——简介【作用域】

console.log(y);    // 结果为2

总结

我认为,只有弄懂了闭包的上诉三个概念,才可以在闭包的应用上立于不败之地。

 

2014-8-26 于西雅图

 

 

 

 

 

 

 

 

 

 

 

二维作用域链查找

通过上面了解到,作用域链(scope chain)的主要作用就是用来进行变量查找。但是,在JavaScript中还有原型链(prototype chain)的概念。

由于作用域链和原型链的相互作用,这样就形成了一个二维的查找。

对于这个二维查找可以总结为:当代码需要查找一个属性(property)或者描述符(identifier)的时候,首先会通过作用域链(scope chain)来查找相关的对象;一旦对象被找到,就会根据对象的原型链(prototype chain)来查找属性(property)

下面通过一个例子来看看这个二维查找:

JavaScript

var foo = {} function baz() { Object.prototype.a = 'Set foo.a from prototype'; return function inner() { console.log(foo.a); } } baz()(); // Set bar.a from prototype

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var foo = {}
 
function baz() {
 
    Object.prototype.a = 'Set foo.a from prototype';
 
    return function inner() {
        console.log(foo.a);
    }
 
}
 
baz()();
// Set bar.a from prototype

对于这个例子,可以通过下图进行解释,代码首先通过作用域链(scope chain)查找”foo”,最终在Global context中找到;然后因为”foo”中没有找到属性”a”,将继续沿着原型链(prototype chain)查找属性”a”。

真人手机网页版 6

  • 蓝色箭头表示作用域链查找
  • 橘色箭头表示原型链查找

参考资料

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

阮一峰博客《学习JavaScript闭包(closure)》

我的上篇博客标题不对,造成一些误解。我认为博客的宗旨不是背教科书,而是分享研发心得。...

理解JavaScript的作用域链

2015/10/31 · JavaScript · 作用域链

原文出处: 田小计划   

上一篇文章中介绍了Execution Context中的三个重要部分:VO/AO,scope chain和this,并详细的介绍了VO/AO在JavaScript代码执行中的表现。

本文就看看Execution Context中的scope chain。

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

};

Lexical 俘获是在parsing stage进行的

上面的俘获顺序必须在函数的parsing阶段进行。函数的数据结构中在parsing后已经包含了所有“俘获变量的reference”,运行阶段不会改变了。这就是为什么上面的line 3定义的可以优先于input 参数x的原因。若是执行时capture, line 3 是在函数的定义之后,该capture的因该说是input 参数x了。

C,C++等编译语言是直接翻译成native 函数的,所有的函数运行信息都靠stack frame来动态获取。唯一和闭包有所接近的概念是“全程变量(global variable)”. 这些global变量在编译时也都转换成内存地址,运行时可以“就地解决”,无需一个独立的闭包。这些函数不是object,不需动态生成,所以无需一个“静态`”的闭包。

JavaScript之所以需要一个独立的闭包,本人认为是因为所有的JavaScript都是object,可以“动态生成”,但是定义(第一道parsing)却是静态的,这个“静态”的部分需要闭包,动态的部分和传统函数一样,靠runtime context 支撑。

这种“实现上的复杂性”,是为了闭包所带来的,处理异步事件时的方便付出的代价。

总结

本文介绍了JavaScript中的作用域以及作用域链,通过作用域链分析了闭包的执行过程,进一步认识了JavaScript的闭包。

同时,结合原型链,演示了JavaScript中的描述符和属性的查找。

下一篇我们就看看Execution Context中的this属性。

1 赞 5 收藏 评论

真人手机网页版 7

foo();

JavaScript的“闭包”到底是什么(2),javascript

我的上篇博客标题不对,造成一些误解。我认为博客的宗旨不是背教科书,而是分享研发心得。我的上篇标题因该改成“JavaScript 闭包的一个议题:它对outer scope 的影响”,因为我没有严格地去分析闭包的定义,而是分析了实现闭包的其中一个语义问题。

讲清楚闭包是件麻烦事,我也没有看到什么关于JavaScript的权威性著作(比如像C++语言有 *Bjarne Stroustrup**C++ programming language)。所以除了苦读JavaScript语言国际标准《**Standard ECMA-262 specification**》*,我无法推荐一个论述“闭包”的最好的教材。

网友“穆己”的“scope chaining”的确是比较接近实质,但也不全面。我只好抛砖引玉,再做一次企图。

闭包的含义包含了下列三个主要概念:

return function(){

return function(){

console.log(k);    // 输出数字0-9

getNameFunc : function(){

function foo() {

};

var a = 3;

test(o);

var o = {name: "qin", old: 23};

}

其实上面的代码就是典型的闭包,闭包函数为foo。

var x = 0;

console.log(i);

}

name : "My Object",

第一是上面所说的可以读取到函数内部的变量。

var name = "The Window";

简单理解就是:子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

console.log(add5(2));     // 7

console.log(add10(2));   // 12

作用域链是一个对象列表或者链表,当执行JavaScript代码需要查找变量y的值的时候(这个过程叫做“变量解析”),它会从链表中的第一个对象开始查找,如果这个对象中有一个名为y的属性,则会直接使用这个值,如果没有,就会继续查找下一个对象,以此类推。当整个链表的对象中都没有y这个属性的话,就会抛出一个引用异常(ReferenceError)的错误。

console.log(x);    // 打印结果为qin

}

关于变量作用域及函数作用域可参考这篇文章:什么是变量作用域和函数作用域?(坑未填)

function test() {

}

首先在上面包含嵌套函数的例子中,我们如何在外层函数test中访问到嵌套函数foo中的变量a的值呢?

test();

本文由美高梅赌堵59599发布于首页,转载请注明出处:(本文假设大家对闭包有一定的理解真人手机网页版:),原文出处

关键词: