一、执行环境

执行环境(也叫做执行上下文,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执行环境、作用域链的更多相关文章

  1. javascript 执行环境,变量对象,作用域链

    前言 这几天在看<javascript高级程序设计>,看到执行环境和作用域链的时候,就有些模糊了.书中还是讲的不够具体. 通过上网查资料,特来总结,以备回顾和修正. 要讲的依次为: EC( ...

  2. JavaScript 执行环境以及作用域链

    执行环境(execution context,为简单起见,有时也称为"环境")是 JavaScript 中最为重要的一个概念.执行环境定义了变量或函数有权访问的其他数据,决定了它们 ...

  3. JavaScript 执行环境、作用域、内存管理及垃圾回收机制

    前言 JavaScript具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存. [原理]找出那些不再继续使用的变量,然后释放其占用的内存.为此,垃圾收集器会按照固定的时间间隔( ...

  4. 图解JavaScript执行环境结构

    JavaScript引擎在开始编译代码的时候,会对JavaScript代码进行一次预编译,生成一个执行环境,比如如下代码: window.onload=function(){ function sub ...

  5. javascript的关键所在---作用域链

    javascript的关键所在---作用域链 javascript里的作用域是理解javascript语言的关键所在,正确使用作用域原理才能写出高效的javascript代码,很多javascript ...

  6. javascript执行环境(执行期上下文)详解

    javascript执行环境(执行期上下文) 当js控制器(control)进入可执行代码时,控制器会进入一个执行环境,活动的多个执行环境构成执行环境栈,最上面的是正在运行的执行环境,当控制器进入一个 ...

  7. JavaScript 执行环境(执行上下文) 变量对象 作用域链 上下文 块级作用域 私有变量和特权方法

    总结自<高程三>第四章  理解Javascript_12_执行模型浅析   JS的执行环境与作用域  javascript高级程序第三版学习笔记[执行环境.作用域] 在javascript ...

  8. JavaScript——执行环境、变量对象、作用域链

    前言 这几天在看<javascript高级程序设计>,看到执行环境和作用域链的时候,就有些模糊了.书中还是讲的不够具体.通过上网查资料,特来总结,以备回顾和修正. 目录: EC(执行环境或 ...

  9. javascript 执行环境,作用域、作用域链、闭包

    1.执行环境 执行环境是JavaScript中国最为重要的一个概念.执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为.每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数 ...

随机推荐

  1. 存储物理页属性的PFN数据库

    Windows内核分析索引目录:https://www.cnblogs.com/onetrainee/p/11675224.html 存储物理页属性的PFN数据库 一.PFN的基础概念 页帧:即CPU ...

  2. 问题:LinkedList 是原始类型。应该将对通用类型 LinkedList<E> 的引用参数化

    jdk1.5之后,引入了泛型,类似下面这种写法会出现类似警告,可以忽略,  LinkedList llist = new LinkedList();也可以修改一下,指定类型  LinkedList&l ...

  3. 本月16日SpringBoot2.2发布,有哪些变化先知晓

    本月(2019年10月16日)Spring Boot 2.2已经正式发布了!在此篇文章中,将给大家介绍一下2.2版为大家带来了哪些重要的新变化.笔者用心书写,希望阅读完成之后转发关注,你的支持是我不竭 ...

  4. Unity - HasExitTime用法

    本文详细分析了AnimatorController中动画切换过渡问题,即Translation过渡及hasExitTime的问题.方法为对实际项目中的所有情况进行分类,规划逻辑图,可视化分析解决这些问 ...

  5. java架构之路-(Redis专题)聊聊大厂那些redis

    上几次说了redis的主从,哨兵,集群配置,但是内部的选举一直没说,先来简单说一下选举吧. 集群选举 redis cluster节点间采取gossip协议进行通信,也就是说,在每一个节点间,无论主节点 ...

  6. vue入门笔记(新手入门必看)

    一.什么是Vue? 1.    vue为我们提供了构建用户界面的渐进式框架,让我们不再去操作dom元素,直接对数据进行操作,让程序员不再浪费时间和精力在操作dom元素上,解放了双手,程序员只需要关心业 ...

  7. LIGHTX-CMS —— 基于 Node.js,Express.js 以及 SQLite 3 搭建的个人博客系统

    概述 LIGHTX-CMS 是我基于 Node.js,Express.js 以及 SQLite 3 搭建的个人博客发布系统. 项目本身可以拿来部署个人博客网站,同时我认为其也适合用以新手学习 Node ...

  8. Activity 的 36 大难点,你会几个?「建议收藏」

    前言 学 Android 有一段时间了,一直都只顾着学新的东西,最近发现很多平常用的少的东西竟让都忘了,趁着这两天,打算把有关 Activity 的内容以问题的形式梳理出来,也供大家查缺补漏. 本文中 ...

  9. logistic回归 python代码实现

    本代码参考自:https://github.com/lawlite19/MachineLearning_Python/blob/master/LogisticRegression/LogisticRe ...

  10. JVM - 复习

    内存模型图 程序计数器(PC) 程序计数器的特点 PC是一小块内存空间,用于记录当前线程执行的字节码指令的地址.如果执行的是本地方法(native),PC里此时显示Undefined 优点: 控制程序 ...