深度剖析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中国最为重要的一个概念.执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为.每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数 ...
随机推荐
- 事业单位招聘网搭建思路和seo方法
期望目标 自动采集事业单位招聘信息+自动发布到网站+自动提交网址到百度+自动发外链. 技术框架 前端用layUI,后端用flask,数据库用的elasticsearch,编程语言用Python 3.7 ...
- HDU 6112 今夕何夕 (预处理 枚举)
中文题意都看的懂啦~ 思路很简单,就是通过前一天推出当天是星期几,直接枚举所有2017-9999年的每一天就好了.ㄟ( ▔, ▔ )ㄏ 代码: #include <cstdio> #def ...
- POJ 3020 Antenna Placement(二分图 匈牙利算法)
题目网址: http://poj.org/problem?id=3020 题意: 用椭圆形去覆盖给出所有环(即图上的小圆点),有两种类型的椭圆形,左右朝向和上下朝向的,一个椭圆形最多可以覆盖相邻的两 ...
- Python斐波那契数列
今天偶然看到这个题目,闲着没事练一下手 if __name__ == '__main__': """ 斐波那契数列(Fibonacci sequence), 又称黄金分割 ...
- Linux与Git学习笔记
Linux基础概念篇: 终端.控制器 命令行界面 (CLI).终端 (Terminal).Shell.TTY的区别 Linux下的yum与apt-get Linux中su.su -和sudo的区别 L ...
- spark cdh5编译安装[spark-1.0.2 hadoop2.3.0 cdh5.1.0]
前提你得安装有Hadoop 我的版本hadoop2.3-cdh5.1.0 1.下载maven包 2.配置M2_HOME环境变量,配置maven 的bin目录到path路径 3.export MAVEN ...
- Java8系列 (二) Stream流
概述 Stream流是Java8新引入的一个特性, 它允许你以声明性方式处理数据集合, 而不是像以前的指令式编程那样需要编写具体怎么实现. 比如炒菜, 用指令式编程需要编写具体的实现 配菜(); 热锅 ...
- java实现,使用opencv合成全景图,前端使用krpano展示
这周花三天做了一demo,算上之前的,怎么也有五天,上一篇是opencv介绍,以及定义native方法,通过本地图片路径传参,底层调用Opencv图像库合成,有兴趣的可以看看,这篇重点在于krpano ...
- 《TypeScript入门教程》笔记
基础 原始数据类型 布尔值 let isDone: boolean = false; 数值 let decLiteral: number = 6; 字符串 let myName: string = ' ...
- SQL查询选修了所有课程的学生姓名
select sname from student where not exists (select * from course where not exists (select * from s ...