JavaScript执行环境 + 变量对象 + 作用域链 + 闭包

闭包真的是一个谈烂掉的内容。说到闭包,自然就涉及到执行环境、变量对象以及作用域链。汤姆大叔翻译的《深入理解JavaScript系列》很好,帮我解决了一直以来似懂非懂的很多问题,包括闭包。下面就给自己总结一下。包括参考大叔的译文以及《JavaScript高级程序设计(第3版)》,一些例子引用自它们。

附上大叔的链接:《深入理解JavaScript系列》

一、执行环境(或“执行上下文”,意义一样)

首先说下ECMAScript可执行代码的类型包括:全局代码、函数代码、eval()代码。

每当执行流转到可执行代码时,即会进入一个执行环境。活动的执行环境构成一个栈:栈的底部始终是全局环境,顶部是当前活动的执行环境。

❶全局执行环境是最外围的一个执行环境。在浏览器中,全局环境就是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。

❷每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境被推入栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。某个执行环境中的代码执行完后,该环境销毁,保存在其中的所有变量和函数定义也随之销毁。而全局执行环境直到应用程序退出才会被销毁。

❸eval的执行环境与调用环境的执行环境相同。

二、变量对象

我们知道变量和执行环境有着密切的关系:

var a = 10; // 全局上下文中的变量
 
(function () {var b = 20; // function上下文中的局部变量
})();alert(a); // 10
alert(b); // 全局变量 "b" 没有声明

而且我们也知道在JS里没有块级作用域这一说法,ES规范指出独立作用域只能通过函数(function)代码类型的执行环境创建。也就是说,像for循环并不能创建一个局部环境:

for (var k in {a: 1, b: 2}) {alert(k);
}alert(k); // 尽管循环已经结束但变量k依然在当前作用域

既然变量与执行环境相关,那变量自己应该知道它的数据存放在哪里,并知道如何访问。这就引出了“变量对象”这个概念。

每个执行环境都有一个与之关联的变量对象,这个对象存储着在环境中定义的以下内容:

1. 函数的形参
2. var声明的变量
3. 函数声明(不包括函数表达式)

举例来说,用一个普通对象来表示变量对象,它是执行环境的一个属性:

执行环境 = {变量对象:{//环境中的数据
    }
};        

例如:

var a = 10;function test(x) {var b = 20;
};test(30);

对应的变量对象为:

// 全局执行环境的变量对象
全局环境的变量对象= {a: 10,test: 指向test()函数
};// test函数执行环境的变量对象
test函数环境的变量对象 = {x: 30,b: 20
};

 那么,不同执行环境中的变量对象的初始化是怎样的呢?下面详细看一下:

❶全局环境中的变量对象

先看下全局对象的明确定义:

全局对象 是在进入任何执行环境之前就已经创建了的对象。
这个对象只存在一份,它的属性在程序中的任何地方都可以访问,全局对象的生命周期终止于程序退出那一刻。

全局对象初始创建阶段,将Math、String等作为自身属性,初始化如下:

globla = {Math:String:......window:globla   //引用自身
};

在这里,变量对象就是全局对象自己。

❷函数环境中的变量对象

在函数执行环境中,“活动对象” 扮演着变量对象这个角色。活动对象是在进入函数执行环境时创建的,它通过函数的arguments属性初始化:

活动对象 = {arguments:  //是个对象,包括callee、length等属性 
}; 

理解了变量对象的初始化之后,下面就是关于变量对象的核心了。

环境中的代码,被分为两个阶段来处理:进入执行环境 、执行代码。变量对象的修改变化与这两个阶段紧密相关。

这2个阶段的处理是一般行为,和环境的类型无关(即,在全局环境和函数环境中的表现是一样的)。

①进入环境

当进入执行环境时(代码执行之前),变量对象已包含下列属性(上面有提到):

①函数的所有形参(如果是在函数执行环境中。因为全局环境没有形参。)
————由 形参名称 和 对应值 组成,作为变量对象的属性。如果没有传递对应的参数,将undefined作为对应值。

②所有函数声明(注意是声明,函数表达式不算。)
————由 函数名 和 对应值(函数对象)组成,作为变量对象的属性。如果变量对象已经存在同名的属性,则覆盖这个属性。

③所有变量声明(由var声明的变量)
————由 变量名 和 对应值(undefined) 组成,作为变量对象的属性。如果变量名与已经声明的形参或函数相同,则变量声明不会干扰已经存在的这类属性。
————注意:此时的对应值是undefined。

让我们来看一个例子:

function test(a, b) {alert(c);    //undefinedalert(d);   //function d() {}alert(e);   //undefinedalert(x);   //出错var c = 10;function d() {}var e = function _e() {};(function x() {});
}test(10); //

 我们考虑当进入带有参数10的test函数环境时(代码执行之前),活动对象表现如下:

活动对象(test) = {a: 10,b: undefined,  c: undefined,d:指向函数d,e: undefined
};

注意,活动对象里不包含函数x。这是因为x是一个函数表达式而不是函数声明,函数表达式不会影响变量对象(在这里是活动对象)。函数_e同样是函数表达式,但是我们注意到它分配给了变量e,所以可以通过名称e来访问。

在这之后,将进入处理代码的第二个阶段:执行代码。

②执行代码

这个阶段内,变量/活动对象已经拥有了属性(不过,并不是所有属性都有值,就像上面那个例子,大部分属性的值还是系统默认的undefined)。

继续上面那个例子,活动对象在“执行代码”这个阶段被修改如下():

AO(test) = {a: 10,b: undefined,  //没有相应该参数传入,undefinedc: 10,    //之前是undefined
  d: 指向函数d,e: 指向函数表达式_e   //之前是undefined
};

注意此时,函数表达式_e保存到了已声明的变量e上,但函数表达式"x"本身不存在于活动对象中,也就是说,如果尝试调用函数"x",无论在函数定义之前或之后,都会出现        “x is not defined”的错误。

理解了以上内容之后,再来看一个例子:

alert(x); // functionvar x = 10;
alert(x); // 10
 
x = 20;function x() {};alert(x); // 20

为什么第一个alert(x)的值是function,而且它还是在x声明之前访问的x?为什么不是10或20呢?

现在我们知道,函数声明是在进入环境时填入活动对象的,同一时间,还有一个变量声明'x',但是正如前面所说,变量声明在顺序上跟在函数声明和形参声明之后。即,在进入环境阶段,变量声明不会干扰变量对象中已经存在的同名函数或形参声明。所以,就这个例子来说,在进入环境时,变量对象的结构如下:

变量对象 = {x:指向函数x//如果function x没有已经声明的话,这时的x应该是undefined
};

紧接着,在代码执行阶段,变量对象作如下修改:

变量对象['x'] = 10;
变量对象['x'] = 20;
//可以在第二、三个alert看到这个结果

再看一个例子:

if (true) {var a = 1;   
} else {var b = 2;
}
//变量是在进入环境阶段放入变量对象的,虽然else部分永远不会执行,
//但是不管怎样,变量b仍然存在于变量对象中。
alert(a); //1
alert(b); //undefined,不是b未声明,而是b的值是undefined

另外,关于var声明变量和不用var声明:

大叔的译文中指出:任何时候,变量只能通过var关键字才能声明。

像a = 10;这仅仅是给全局对象创建了一个新属性(但它不是变量)。它之所以能成为全局对象的属性,完全是因为全局对象===全局变量对象。看例子:

alert(a); // undefined
alert(b); // "b" 没有声明,出错
 
b = 10;
var a = 20;

进入环境阶段:

变量对象 = {a: undefined
};

可以看到,因为b不是一个变量,所以在这个阶段根本就没有b,b将只在代码执行阶段才会出现,但在这里,还未执行到那就出错了。

还有一个要注意的:var声明的变量,相对于属性(如a = 10;或window.a = 10;),变量的[[Configurable]]特性值为false,即不能通过delete删除,而属性则可以。

三、作用域链

现在我们已经知道,一个执行环境的数据(变量、函数声明和函数形参)作为属性存储在变量对象中。

同时也知道,变量对象在每次进入环境时创建,并填入初始值,值的更新出现在代码执行阶段。

下面的内容讨论作用域链。

如果要简要地描述并展示其重点,那么作用域链大多数与内部函数相关。

我们可以创建内部函数,甚至能从父函数中返回这些函数。

var x = 10;function foo() { var y = 20; function bar() {alert(x + y);} return bar; 
}foo()(); // 30

很明显每个环境拥有自己的变量对象:对于全局环境,它是全局对象自身;对于函数,它是活动对象。

作用域链正是内部环境所有变量对象(包括父变量对象)的列表。此链用来在标识符解析中变量查找。

作用域链本质上,是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

对于上面这个例子,bar执行环境中的作用域链包括:bar变量对象、foo变量对象、全局变量对象。

函数执行环境中的作用域链在函数调用时创建,包含这个函数的活动对象和函数的[[scope]]属性。示例如下:

活动的执行环境 = {变量对象: {...}, // or 活动对象this: thisValue,Scope: [ // 作用域链// 它是所有变量对象的列表。
    ]
};

其中的Scope定义为:Scope = 被调用函数的活动对象 + [[scope]]。

这种标识符的解析过程,与函数的生命周期相关,下面详细讨论。

(1)函数的生命周期

函数的生命周期分为创建和激活(调用时)两个阶段。

❶函数创建

让我们先看看在全局环境中的变量和函数声明(这里的变量对象就是全局对象自身,我们懂的。)

var x = 10;function foo() {var y = 20;alert(x + y);
}foo(); // 30

函数激活时,得到了正确的也是预期中的结果。但我们注意到,变量y在函数foo中定义(意味着它在foo的活动对象中),但是x并未在foo环境中定义,相应地,它不会添加到foo的活动对象中。那么,foo是如何访问到变量x的?其实我们大都知道函数能访问更高一层的环境中的变量对象,事实也是如此,而这种机制正是通过函数内部的[[scope]]属性实现的。

[[scope]]是所有父变量对象的层级链,处于当前函数环境,在函数创建时存在于其中。

注意重要的一点:[[scope]]属性在函数创建时被存储,永远不变,直到函数销毁。函数可以不被调用,但这个属性一直存在。
且,与作用域链相比,作用域链是执行环境的一个属性,而[[scope]]是函数的属性。

上面的例子,函数foo的[[scope]]如下:

foo.[[Scope]] = [全局执行环境.变量对象 // === Global
];

继续,我们知道在函数调用时进入执行环境,这时活动对象被创建,this、作用域链被确定。下面详细考虑这个时刻。

❷函数激活

正如上面提到的,进入环境创建变量/活动对象之后,环境的Scope属性(即作用域链)定义为:Scope = 变量/活动对象 + [[scope]]。

这个定义意思是:活动对象是被添加到[[scope]]前端,在作用域链中处理第一位。这很重要,对于标识符的查找,是从自身变量对象开始的,逐渐往父变量对象查找。

(2)通过构造函数创建的函数的[[scope]]

在上面的例子中,我们看到,在函数创建时,函数获得[[scope]]属性,该属性存储着所有父环境的变量/活动对象。但有一个例外,那就是通过构造函数创建的函数。

var x = 10;function foo() {var y = 20;function barFD() { // 函数声明
    alert(x);alert(y);}var barFE = function () { // 函数表达式
    alert(x);alert(y);};var barFn = Function('alert(x); alert(y);');barFD(); // 10, 20barFE(); // 10, 20barFn(); // 10, "y" is not defined
 
}foo();

从以上例子中,我们看出问题所在:通过构造函数创建的函数,它的[[scope]]仅包含全局对象。

另外关于eval,实践中很少用到eval,但有一点提示,eval代码的环境与当前的调用环境拥有相同的作用域链。

(3)延长作用域链

有两个能延长作用域链的方法:with声明和catch语句。它们添加到作用域链的最前端(比被调用函数的活动对象还要靠前)。

如果发生其中一个,作用域链作如下修改:

Scope = withObject|catchObject +活动/变量对象 + [[Scope]]

看个例子:

var x = 10, y = 10;with ({x: 20}) {var x = 30, y = 30;alert(x); // 30alert(y); // 30
}alert(x); // 10
alert(y); // 30
//1. x = 10,y = 10;
//2. 进入环境,对象{x:20}添加到作用域链的前端。
//3. 执行代码,x为20,变为30,y为10,变为30。
//4.with声明完成后,对象被移除,那个因with对象而改变的x=30也被移除。
//最后两个alert,x保持最初不变,y在with里已发生改变。

四、闭包

到了这里,其实如果对前面的[[scope]]和作用域链完全理解的话,闭包也就懂了。

大叔的译文对闭包给出的2个定义是:

❶从理论角度:所有函数都是闭包。因为它们在创建的时候就将所有父环境的数据保存起来了。哪怕是简单的全局变量也是如此,因为在函数中访问全局变量就相当于在访问自由变量(指不在参数声明,也不在局部声明的变量),这个时候使用最外层的作用域。

❷从实践角度:以下函数才算是闭包:

   ①即使创建它的环境销毁,它仍然存在(比如,内部函数从父函数返回);②在代码中引用了自由变量。

闭包的性能问题总被提及,现在我们知道原因了:创建闭包的父环境即使被销毁了,但闭包仍然引用着父环境的变量对象,也就是说需要继续维护着这个变量对象的内存。

下面我们再来具体看一下。

var x = 10;function foo() {alert(x);
}(function (funArg) {var x = 20;// 变量"x"在foo中静态保存的,在该函数创建的时候就保存了funArg(); // 10, 而不是20

})(foo);

我们已经知道,创建foo函数的父级环境(在这里是全局环境)的数据是保存在foo函数的内部属性[[scope]]中的。

这里还要注意的是:同一个父环境创建的闭包是共用一个[[scope]]属性的。也就是说,某个闭包对其中[[scope]]的变量的修改会影响到其他闭包对其变量的读取。

var firstClosure;
var secondClosure;function foo() {var x = 1;firstClosure = function () { return ++x; };secondClosure = function () { return --x; };x = 2; // 影响"x", 在2个闭包公有的[[Scope]]中
alert(firstClosure()); // 3, 通过第一个闭包的[[Scope]]
}foo();alert(firstClosure()); // 4
alert(secondClosure()); // 3

关于这个问题,大叔的译文和《JS高级》里都有一个例子:

var data = [];for (var k = 0; k < 3; k++) {data[k] = function () {alert(k);};
}data[0](); // 3, 而不是0
data[1](); // 3, 而不是1
data[2](); // 3, 而不是2

这就是闭包共用一个[[scope]]的问题。可以按下面的方法解决:

var data = [];for (var k = 0; k < 3; k++) {data[k] = (function _helper(x) {return function () {alert(x);};})(k); // 传入"k"值
}// 现在结果是正确的了
data[0](); // 0
data[1](); // 1
data[2](); // 2

在上例中,每次_helper都会创建一个新的变量对象,其中含有参数x,其值就是传递进来的k值。此时,返回的函数的[[scope]]如下:

data[0].[[Scope]] === [... // 其它变量对象父级环境中的活动对象: {data: [...], k: 3},_helper环境中的活动对象: {x: 0}
];data[1].[[Scope]] === [... // 其它变量对象父级环境中的活动对象: {data: [...], k: 3},_helper环境中的活动对象: {x: 1}
];data[2].[[Scope]] === [... // 其它变量对象父级环境中的活动对象: {data: [...], k: 3},_helper环境中的活动对象: {x: 2}
];

要注意的是,如果在返回的函数中,要获取k值,那么该值还会是3。

五、小结

总结得好长啊好长。因为我觉得一口气将这几个点连在一起梳理一下比较好。

嗯,就这样吧。

转载于:https://www.cnblogs.com/no-particular/archive/2013/01/31/2887293.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/295937.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

58年前,这3个人在“撕逼”中拿下诺贝尔奖!一起研究DNA的女科学家却痛苦去世.........

全世界只有3.14 % 的人关注了爆炸吧知识人类的本质是双螺旋结构1953年4月25日&#xff0c;《自然》杂志发表了一篇不到千字的论文&#xff1a;消息一出&#xff0c;便掀起了科学界的轩然大波。因为这篇短小精悍的论文史无前例地揭示了正确的DNA立体结构。图片仅供参考&#xff…

Azure App Service 上的根证书

点击上方蓝字关注“汪宇杰博客”原文&#xff1a;Amol Mehrotra翻译&#xff1a;Edi Wang导语App Service 有一个受信任的根证书列表&#xff0c;您不能在 App Service 的多租户版本中修改这些证书&#xff0c;但您可以在应用服务环境 (ASE) 的受信任根存储中加载自己的 CA 证书…

String.Format和StringBuilder的效率

到底String.Format还是StringBuilder效率更高一点&#xff1f;至于这个问题&#xff0c;直接叫他用强大的武器Reflector了解一下String.Format的方法.结果他很快就找出相关的代码: public static string Format(IFormatProvider provider, string format, params object[] arg…

jettytomcat对待表单过长问题

为什么80%的码农都做不了架构师&#xff1f;>>> 结论两句话&#xff1a; tomcat知道自己处理不了了&#xff0c;什么也不干过去了 jett知道自己处理不了了&#xff0c;抛个IllegalStateException出来通知一下 jetty默认允许的content-length2001000 org.eclips…

Android之Adapter用法总结

Android之Adapter用法总结 Adapter是连接后端数据和前端显示的适配器接口,是数据和UI(View)之间一个重要的纽带。在常见的View(ListView,GridView)等地方都需要用到Adapter。如下图直观的表达了Data、Adapter、View三者的关系: Android中所有的Adapter一览: …

那些年,画家发明的黑科技

全世界只有3.14 % 的人关注了爆炸吧知识在现代摄影技术发明之前&#xff0c;获取图像的最佳方式只有绘画。在绘画的时代&#xff0c;如何把事物画的逼真&#xff0c;几乎成为了困扰画家一生的问题。为了准确展现客观世界&#xff0c;让画面看起来更加逼真&#xff0c;古代的艺术…

shared_ptrT make_shared( Args ... args );

shared_ptr很好地消除了显式的delete调用&#xff0c;如果读者掌握了它的用法&#xff0c;可以肯定delete将会在你的编程字典中彻底消失。但这还不够&#xff0c;因为shared_ptr的构造还需要new调用&#xff0c;这导致了代码中的某种不对称性。虽然shared_ptr很好地包装了new表…

Android jdwp 自动断开,开启debug调试模式,进入不了,等一会就闪退了!!!!

原因&#xff1a;debug标记加多了art/runtime/jdwp/jdwp_event.cc:661] Check failed: Thread::Current() ! GetDebugThread() (Thread::Current()0xe1908400, GetDebugThread()0xe1908400) Expected event thread11-16 15:29:18.457 10331-10339/包名 A/art: art/runtime/runt…

Visual Studio 2022发布了,我最爱的5大特性

VS2022今天Visual Studio 2022 终于发布了&#xff0c;你只需要按下⾯地址就可以快速安装你所需要的Visual Studio 2022版本&#xff0c;包括了企业版本&#xff0c;专业版本&#xff0c;以及社区版本。请输入以下⽹址安装最新的Visual Studio 2022 https://aka.ms/InstallVS或…

ASP.NET3.5 企业级项目开发 -- 第二章 数据访问层(DAL)的开发

为什么80%的码农都做不了架构师&#xff1f;>>> ASP.NET3.5 企业级项目开发 &#xff0d;&#xff0d; 第二章 数据访问层(DAL)的开发 前言&#xff1a;本篇主要讲述数据访问层的开发&#xff0c;而且为了大家交流&#xff0c;已经创建企业项目开发团队&…

Android之URI简介

就Android平台而言&#xff0c;URI主要分三个部分&#xff1a;scheme, authority and path。其中authority又分为host和port。格式如下&#xff1a; scheme://host:port/path 举个实际的例子&#xff1a; content://com.example.project:200/folder/subfolder/etc \---------/ …

虚拟机安装和使用

软件更新快&#xff0c;所以我也不知道什么时候这些密钥啊就失效了&#xff0c;最新应用的安装时间&#xff1a;15年 虚拟机软件不少&#xff0c;其中应用最广发的一个就是VMware Workstation&#xff0c;以后用了别的虚拟机&#xff0c;安装教程再加~ VMware Workstation http…

十个jQuery的幻灯片图片轮播切换插件[转]

1、Simple Controls Gallery 是基于jQuery的一个幻灯插件&#xff0c;非常不错&#xff0c;详细演示及下载请点击下面的链接http://www.dynamicdrive.com/dynamicindex4/simplegallery.htm 2、jQuery Cycle Plugin 同样是jQuery的插件&#xff0c;支持非常多的样式&#xff0c;…

22张图片倒叙霍金:我爱宇宙也爱这苦乐人生

全世界只有3.14 % 的人关注了爆炸吧知识▲斯蒂芬威廉霍金&#xff08;Stephen William Hawking&#xff09;&#xff0c;1942年1月8日出生于英国牛津&#xff0c;21岁时患上肌肉萎缩性侧索硬化症&#xff08;卢伽雷氏症&#xff09;&#xff0c;全身瘫痪&#xff0c;不能言语&a…

鸿蒙思维和小央美,北市场附近艺术培训

最佳答案&#xff1a;北市场附近有叮当少儿美术空间,弈趣围棋培训中心,舞王回一街舞俱乐部,童心童画美术馆,小央美儿童美术,纸飞机少儿美术,星海艺术培训,鸿蒙教育思维绘画,小画虫少儿美术培训中心,红点美术培训中心,风之舞打击乐培训中心,红英艺术中心,墨涛书法教育,格林童画,…

简述LINQ的发展历程

LINQ&#xff1a;最终统治了所有的语言&#xff01;让我们看看LINQ如何彻底改变了.NET中访问数据的方式.NET与其他技术栈的不同之处之一绝对是LINQ&#xff0c;它是Language Integrated Query的首字母缩写。实际上&#xff0c;它是随.NET Framework 3.5和Visual Studio 2008引入…

关于异或的一些东西和应用

异或是一种基于二进制的位运算&#xff0c;用符号XOR或者 ^ 表示&#xff0c;其运算法则是对运算符两侧数的每一个二进制位&#xff0c;同值取0&#xff0c;异值取1。它与布尔运算的区别在于&#xff0c;当运算符两侧均为1时&#xff0c;布尔运算的结果为1&#xff0c;异或运算…

Android之自定义ContentProvider详解

第一个版本 对android中MIME类型的理解 初始MIME类型&#xff0c;是在学习ContentProvider的时候。 当在创建自己的ContentProvider的时&#xff0c;需要从抽象类ContentProvider中派生出自己的子类&#xff0c;并实现其中5个抽象方法&#xff1a; query(Uri, String[], Strin…

dwr 写的小程序,配置

第一、在web.xml里面有如下配置&#xff1a; <?xml version"1.0" encoding"UTF-8"?><web-app xmlns"http://java.sun.com/xml/ns/j2ee" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation"h…

PHP开发学习-Apache+PHP+MySQL环境搭建

我现在开始php的学习之旅啦&#xff01; 入门&#xff1a;开发环境搭建 组件版本&#xff1a; apache2.2.22 下载地址&#xff1a;http://httpd.apache.org/ php5.4.11 下载地址&#xff1a;http://windows.php.net/download/ mysql5.5.29 下载地址&#xff1a;http://www.mys…