深度剖析Javascript执行环境、作用域链
一、执行环境
执行环境(也叫做执行上下文,Execution Context)是Javascript中最为重要的一个概念。执行环境定义了变量或函数有权访问其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,执行环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理时会在后台使用它。
全局执行环境是最外围的一个执行环境。根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样。在web浏览器中,全局执行环境被认为是window对象,因此所有全局对象和函数都是作为window对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数随之销毁(全局执行环境直到应用程序退出——例如关闭网页或浏览器时才被销毁)。
每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入到一个环境栈(也叫做函数调用栈)中,同样遵循先进后出、后进先出的存取方式。而在函数执行之后,栈将起环境弹出,把控制权返回给之前的执行环境。ECMAScript程序中的执行流正是由这个方便的机制控制着。
var color = 'blue'; function changeColor() {
var anotherColor = 'red'; function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
} swapColors();
} changeColor();
以上代码共涉及三个执行环境:全局环境、changeColor的局部环境、swapColors的局部环境。
我们很容易知道:
第一步,首先是全局环境入栈。
全局环境入栈之后,执行流将其中的可执行代码开始执行,直到遇到了changeColor()
,这一句激活函数changeColor
创建它自己的执行环境,因此第二步就是执行流将changeColor的执行环境入栈。
changeColor的执行环境入栈之后,执行流开始执行其中的可执行代码,遇到swapColors()
之后又激活了一个执行环境。因此第三步是执行流将swapColors的执行环境入栈。
在swapColors的可执行代码中,再没有遇到其他能生成执行环境的情况,因此这段代码顺利执行完毕,swapColors的执行环境中弹出。
swapColors的执行环境弹出之后,继续执行changeColor的可执行代码,也没有再遇到其他执行环境,顺利执行完毕之后弹出。这样,ECStack中就只剩下全局环境了。
全局环境在浏览器窗口关闭后出栈。
ps:这个所谓的执行流其实就是指的线程。
二、作用域链
当代码在执行环境中执行时,会创建变量对象的一个作用域链。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果当前执行环境是函数,则将其活动对象作为变量对象。活动对象在最开始的时候只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。
ps:this对象是在运行时基于函数的执行环境绑定的。也就是说在执行环境中一旦使用this,那么就会给这个this指向一个明确的对象。
在上述代码中我们可以描述成以下:
标识符解析是沿着作用链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后组件向后回溯,直到找到标识符为止(如果找不到标识符,通常会导致错误发生)。
在上图中内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数,这些环境之间的联系是线性、有次序的。
作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。来看下面这一条栗子:
function compare(value1, value2){
if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
}
var result = compare(5, 10);
以上代码先定义了 compare()函数,然后又在全局作用域中调用了它。当调用 compare()时,会 创建一个包含 arguments、value1 和 value2 的活动对象。全局执行环境的变量对象(包含 result 和 compare)在 compare()执行环境的作用域链中则处于第二位。通过下图可以很明显上述栗子的作用域链。
无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。 但是,闭包的情况又有所不同。
三、闭包
闭包是指有权访问另一个函数执行环境中的变量的函数。
如何创建作用域链以及作用域链有什么作用的细节,对彻底理解闭包至关重要。
function createComparisonFunction(propertyName) { return function(object1, object2){
var value1 = object1[propertyName];
var value2 = object2[propertyName]; if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
};
}
var compare = createComparisonFunction("name");
var result = compare({ name: "Nicholas" }, { name: "Greg" });
在上述代码中可以看出有两条作用域链。一条是全局环境到createComparisonFunction局部环境的作用域链,另一条则是全局环境到匿名函数局部环境的作用域链。然而另一条作用域链需要依赖createComparisonFunction()函数的活动对象,因此,在 createComparisonFunction()函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数 createComparisonFunction()的活动对象。
在匿名函数从 createComparisonFunction()中被返回后,它的作用域链被初始化为包含 createComparisonFunction()函数的活动对象和全局变量对象。这样,匿名函数就可以访问在 createComparisonFunction()中定义的所有变量。更为重要的是,createComparisonFunction() 函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当 createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁。因此闭包会比其他函数占用更多的内存。
四、闭包的作用
1、封装变量
闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”。假设我们现在计算所传参数变量的乘积:
const mult = function () {
let a = 1
for (const item of arguments) {
a = a * item
}
return a
}
对于这样一份代码我们会觉得对于那些相同的参数来说,每次都进行计算是一种浪费,我们可以加入缓存机制来提高这个函数的性能:
let cache = {}
const mult = function () {
const args = Array.prototype.join.call(arguments)
if (cache[args]) {
return cache[args]
}
let a = 1
for (const item of arguments) {
a = a * item
}
return cache[args] = a
}
虽然性能有所改进,但是我们看到cache这个变量仅仅在mult函数中被使用,完完全全的暴露在全局作用域下。因此不如把它放在mult函数内部,这样可以减少页面中的全局变量,避免在其他地方不小心被修改而引发错误,代码如下:
const mult = (function () {
let cache = {}
return function () {
const args = Array.prototype.join.call(arguments)
if (cache[args]) {
return cache[args]
}
let a = 1
for (const item of arguments) {
a = a * item
}
return cache[args] = a
}
})()
以上代码我们就是通过闭包的形式有效解决了我们遇到的一些问题。但是在实际项目中,我们会为了业务白那些很大一块代码块,因此如果我们在一个大的函数中有一些代码能够独立出来,通常会封装在独立的小函数里面,这样有利于代码复用。再加上一个良好的命名,那样也起到了注释的作用。因此提炼函数是代码重构中的一种常见技巧。
如果这些小函数不需要程序在程序的其他地方使用,那么最好的方式就是用闭包封闭起来。比如上面求乘积的代码我们现在可以这样来写:
const mult = (function () {
let cache = {}
// 抽离计算乘积的函数calcu
const calcu = function () {
let a = 1
for (const item of arguments) {
a = a * item
}
return a
} return function () {
const args = Array.prototype.join.call(arguments)
if (cache[args]) {
return cache[args]
}
return cache[args] = calcu.apply(null, arguments)
}
})()
2、延续局部变量的生命周期
Image对象经常用于数据上报,就是只需要将数据通知上报给服务端,而客户端不需要关心服务端的状态和返回值,如下所示:
const report = function (src) {
const image = new Image()
image.src = src
} report('http://xxx.ooo.com/reportData')
但是在一些特殊情况下比如低版本浏览器下上报数据会存在丢失一部分数据,也就是说report函数并不是每一次都成功发起了HTTP请求。
丢失数据的原因是因为变量image在report函数中是一个局部变量,当report函数调用结束后,image局部变量会立即被销毁,因此就会有还没得及发出HTTP请求就丢失的情况。为了避免这种情况我们可以把image变量用闭包封闭起来,延长这个局部变量的生命的周期就可以解决请求丢失的问题。如:
const report = (function () {
let image = null
return function (src) {
image = new Image()
image.src = src
}
})()
深度剖析Javascript执行环境、作用域链的更多相关文章
- javascript 执行环境,变量对象,作用域链
前言 这几天在看<javascript高级程序设计>,看到执行环境和作用域链的时候,就有些模糊了.书中还是讲的不够具体. 通过上网查资料,特来总结,以备回顾和修正. 要讲的依次为: EC( ...
- JavaScript 执行环境以及作用域链
执行环境(execution context,为简单起见,有时也称为"环境")是 JavaScript 中最为重要的一个概念.执行环境定义了变量或函数有权访问的其他数据,决定了它们 ...
- JavaScript 执行环境、作用域、内存管理及垃圾回收机制
前言 JavaScript具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存. [原理]找出那些不再继续使用的变量,然后释放其占用的内存.为此,垃圾收集器会按照固定的时间间隔( ...
- 图解JavaScript执行环境结构
JavaScript引擎在开始编译代码的时候,会对JavaScript代码进行一次预编译,生成一个执行环境,比如如下代码: window.onload=function(){ function sub ...
- javascript的关键所在---作用域链
javascript的关键所在---作用域链 javascript里的作用域是理解javascript语言的关键所在,正确使用作用域原理才能写出高效的javascript代码,很多javascript ...
- javascript执行环境(执行期上下文)详解
javascript执行环境(执行期上下文) 当js控制器(control)进入可执行代码时,控制器会进入一个执行环境,活动的多个执行环境构成执行环境栈,最上面的是正在运行的执行环境,当控制器进入一个 ...
- JavaScript 执行环境(执行上下文) 变量对象 作用域链 上下文 块级作用域 私有变量和特权方法
总结自<高程三>第四章 理解Javascript_12_执行模型浅析 JS的执行环境与作用域 javascript高级程序第三版学习笔记[执行环境.作用域] 在javascript ...
- JavaScript——执行环境、变量对象、作用域链
前言 这几天在看<javascript高级程序设计>,看到执行环境和作用域链的时候,就有些模糊了.书中还是讲的不够具体.通过上网查资料,特来总结,以备回顾和修正. 目录: EC(执行环境或 ...
- javascript 执行环境,作用域、作用域链、闭包
1.执行环境 执行环境是JavaScript中国最为重要的一个概念.执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为.每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数 ...
随机推荐
- .Net Core 3.0 IdentityServer4 快速入门
.Net Core 3.0 IdentityServer4 快速入门 一.简介 IdentityServer4是用于ASP.NET Core的OpenID Connect和OAuth 2.0框架. 将 ...
- opencv实践::直线检测
问题描述 寻找英语试卷填空题的下划线,这个对后期的切图与自动 识别都比较重要. 解决思路 方法: 通过图像形态学操作来寻找直线,霍夫获取位置信息与显示. #include <opencv2/op ...
- python编程系列---白痴女朋友(我没有女朋友!)看了都能懂的TCP/IP协议介绍
前言 早期的计算机网络,都是由各厂商自己规定一套协议,IBM.Apple和Microsoft都有各自的网络协议,互不兼容:为了把全世界的所有不同类型的计算机都连接起来,就必须规定一套全球通用的协议,为 ...
- CentOS6-Linux内核编译 详细步骤
CentOS6-Linux内核编译 详细步骤 背景 Win10用VMwareWorkstation搭的虚拟机 CentOS6.5,内核版本2.6.32-431.el6.x86_64 在该环境下升级至4 ...
- StopWatch任务计时器
介 绍: StopWatch 是用来计算程序块的执行时间工具, 目前有好多框架都有实现提供此工具(实现结果都区别不大), 本文介绍org.springframework.util.StopWatc ...
- 计算机网络(1)- TCP
TCP的全称是传输控制协议(Transmission Control Protocol)[RFC 793] TCP提供面向连接的服务.在传送数据之前必须先建立连接,数据传送结束后要释放连接.TCP不提 ...
- Redis5源码解析-Sentinel
简单的概念就不解释.基于Redis5.0.5 从Sentinel主函数触发 在sentinel.c文件的最后面可以发现sentinelTimer函数,这个就是Sentinel的主函数,sentinel ...
- 深入理解.NET Core的基元(二) - 共享框架
原文:Deep-dive into .NET Core primitives, part 2: the shared framework 作者:Nate McMaster 译文:深入理解.NET Co ...
- MongoDB自建和阿里云RDS备份还原
MongoDB是一个基于分布式文件存储的数据库.由C++语言编写.旨在为WEB应用提供可扩展的高性能数据存储解决方案. MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功 ...
- python基础-数字类型及内置方法
--数字类型及内置方法 整型-int 用途:多用于年龄.电话.QQ号等变量 定义方法 age = 18 # age = int(18) 常用方式:多用于数学计算 # int(x)将x转换成整数,是向下 ...