JS执行环境

首先让我们来看看《JavaScript高级程序设计》的原话

变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。

每个上下文都有一个关联的变量对象(variable object),而这个上下文中定义的所有变量和函数都存在于这个对象上

假设把每个人都看做一段代码。

正如我们的执行环境在宇宙。

对我们来说宇宙就是一个全局执行上下文,所以宇宙中的变量比如太阳,地球的都挂在宇宙这个对象上虽然可能层级是(宇宙.xxx.xxx.地球)但是不影响我们用这个来理解。

那么在浏览器中全局上下文就是我们常说的 window 对象,nodejs中就是global对象。

知道了这个概念,我们就很容易去理解这句话

上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数

(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)

但是所有行为都是有生命周期的,不可能每个人做一件事都必须等到宇宙毁灭才会真正结束。

所以宇宙为了更好的利用资源,从而可以产生更多的行为来充实这一次短暂而又漫长的生命。

就出现了内部(包含)上下文。正如我们每个人会有自己学习环境,工作环境,家庭环境等等一样。

JS的每个函数都会有自己的执行上下文。

当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。

在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文

那么这段话如何理解?上下文在哪?

变量对象&活动对象

函数执行上下文的创建,发生在函数调用时但在执行函数体内的具体代码之前在创建阶段JS引擎会使用当前函数的参数列表(arguments)初始化一个上面最开始提到的变量对象。

并将当前执行上下文与之关联,函数代码块中声明的 变量 和 函数 将作为属性添加到这个变量对象上。

注意:只有函数声明(function declaration)会被加入到变量对象中,而函数表达式(function expression)会被忽略

// 这里是函数声明 会被加入变量对象
function haha(){}

// 这里是函数表达式 只有hehe会被已变量的形式加入变量对象  而_hehe不会被加入
var hehe = function _hehe(){}

在这一阶段,会进行变量和函数的初始化声明,然后再接着往下开始执行(父级上下文)。

这里又有一个概念,具体执行之前。非全局上下文的变量对象(VO)此时还是处于不能使用的状态。是无法直接访问的。在进入执行阶段之时,变量对象(VO)被激活为活动对象(AO)时,我们才能访问到其中的属性和方法。

其实变量对象和活动对象是一个东西,只不过处于不同的状态和阶段而已。

如果碰到新的需要创建上下文的函数调用,又会开始重复创建上下文的工作(子级上下文)。但是此时会将控制权从手中交出,给到子级上下文执行。直到执行完毕退出,子级将控制权又返还给到父级上下文。如此这般周而复始,直到全局上下文销毁。

看下面的代码

function a() {
    console.log('a')
}

function b() {
    a();
}

function c() {
    b();
}

c();

首先创建全局上下文,然后定义a,b,c三个函数,所以此时全局上下文作为最外层的上下文环境被压入上下文栈。

然后碰到c函数执行,于是开始创建c函数上下文。之后全局上下文将控制权交给c函数上下文,并将c函数上下文压入上下文栈。

c函数上下文开始执行,碰到b函数执行,于是开始创建b函数上下文。之后c将控制权交出给到b。并将b压入上下文栈。

b函数上下文开始执行,碰到a函数执行,于是开始创建a函数上下文。之后b将控制权交出给到a。并将a压入上下文栈。

此时上下文栈内按照先进顺序展示是 全局->c->b->a

那么a执行中执行完了console.log('a')之后。a函数告诉上下文栈我执行完毕了,于是a被推出上下文栈,并将控制权交给b,告诉b可以继续执行剩下的东西了。

而b拿到控制权之后开始继续往下执行,找不到需要执行的。就执行完毕了,于是第二个被推出了栈。同样的将控制权交给了c。c又重复了上面那个过程。直到c执行完毕被推出了栈。此时控制权回到全局上下文。而全局上下文上面说了会等到整个环境被销毁,比如关闭页面,或者nodejs执行完毕了所有东西 才会被销毁。

至此对于执行上下文的变量对象我们已经有了一定的了解。

作用域

那么是什么让我们可以在子级上下文中使用父级,爷级...乃至全局上下文的各种资源呢?

上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定

了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域

链的最前端。如果上下文是函数,则其活动对象(activation object)用作变量对象。活动对象最初只有

一个定义变量:arguments。(全局上下文中没有这个变量。)作用域链中的下一个变量对象来自包含上

下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文;全局上下文的变量对象始终

是作用域链的最后一个变量对象。

没错,链如其名。用人话讲,这就是在执行阶段会创建一根绑了很多变量对象的链子。

嗯~大概长下面这样

(图片来自网络,侵删)

链子的两头是当前变量对象和全局变量对象。而这中间可以经历无数个上级变量对象

所以当前执行代码会优先从当前变量对象中找需要的变量。(毕竟是自家人)。

如果没有找到,则会去上级找,再没有再上一级,直到找到全局中为止。 我们假设一条链上两个变量对象之间的距离需要十分钟才能走完。那么全局是距离当前最远的。所以你知道就近原则怎么来的嘛。

所以作用域链的核心就是这句话: 作用域链决定了各级上下文中的代码在访问变量和函数时的顺序

闭包

那么闭包呢?闭包的执行已经结束,而闭包内的变量都是挂载闭包那个上下文对象的。为什么出来后还可以继续使用?

那么这里先看看闭包的定义

有权访问另一个函数内部变量的函数。简单说来,如果一个函数被作为另一个函数的返回值,并在外部被引用,那么这个函数就被称为闭包。

// 我是闭包啊
function f(){
  var a = 1;
  return function(){
    console.log(a)
  }
}
var demo = f()
demo() // -> 1

这段代码里可以看到在f的变量对象中会有一个a,这个a被return 出去的匿名函数引用了。所以即使f的执行上下文执行完毕之后已经销毁,但辣鸡回收机制还是无法回收a变量,也就是f的变量对象无法销毁,一直常驻内存中。但f的作用域链已经随着上下文的销毁而寿终正寝。而f的变量对象只有等到外部引用的demo执行上下文也销毁完毕才会被释放。 所以这就是闭包为什么能在执行结束之后还可以在外部继续使用内部变量。其核心就是变量对象没用被销毁。

作用域链增强

什么是作用域链增强?

我们上面介绍了作用域链的核心:作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。

那么增强就可以理解为人为的去改变作用域链。怎么能不刺激?

来看书里的原话:

虽然执行上下文主要有全局上下文和函数上下文两种(eval()调用内部存在第三种上下文),但有

其他方式来增强作用域链。某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执

行后会被删除。通常在两种情况下会出现这个现象,即代码执行到下面任意一种情况时:

 try/catch 语句的 catch 块

 with 语句

这两种情况下,都会在作用域链前端添加一个变量对象。对 with 语句来说,会向作用域链前端添

加指定的对象;对 catch 语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误

对象的声明

所以我们可以了解到 catch会创建一个新的变量对象来加入到作用域链中

with会将一个变量对象添到作用域链的最前端来

虽然不推荐但我们可以看看效果

with

with把本属于全局变量对象的location 放到内部块的作用域链最前端。

所以会优先先去location对象中找href变量。

catch

carth则是在捕获到异常的情况,创建了一个新的error变量对象。并放置在内部的作用域的前面。

变量提升

上了点年纪的前端都受到过这个问题的困扰,没上年纪的也在面试中被经常作为基础题问到。

那么什么是变量提升?

上面我们知道了执行上下文内的所有变量(var声明的)和函数都会挂在生成的变量对象上。

然鹅 知其然也要知其所以然。

所以我们需要先了解什么是变量声明以及这个挂载的过程。

var a = 1;

上面的代码在JS生成变量对象的时候。会先检测内部的变量和函数。 然后将其提升到执行上下文的顶部最先初始化。

所以var a = 1 就会先变成 var a; 然后在变成 a = 1; 分开两步走。

那么我们通过代码来验证一下

console.log(a); // undefined
var a = 1;

可以看到在变量声明之前的打印已经可以打印出undefined。

var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。这个现象叫作“提升”

那么在JS中还存在一种这样的写法

function demo(){
    a = 2;
}
demo();
console.log(a); // 2

此时我们可以看到 变量a并没有使用var语句以及其他语句来声明,而是直接赋值。

这是因为如果省略var语句声明变量,该变量就会被提升到最顶级也就是全局上下文的变量对象中区初始化。

ES6.let&const

let

ES6 新增的 let 关键字跟 var 很相似,但它的作用域是块级的,这也是 JavaScript 中的新概念。

什么是块级作用域?非常容易理解,你只需要记住,最近的一对花括号{}就是一个块级作用域。

那么这个块级作用域有什么用呢?

顾名思义就是由let声明的变量只能在这个块内起作用。所以我们可以来干下面这样的事情

for (var i = 0; i < 10; ++i) {} 
console.log(i); // 10 
for (let j = 0; j < 10; ++j) {} 
console.log(j); // ReferenceError: j 没有定义

和var的区别在于:

  1. 使用 var 声明的迭代变量会泄漏到循环外部,let不会
  2. var可以重复声明,let不可以
  3. 都会变量提升但是var的变量可以在赋值之前访问到,而let的变量会在提升之后,赋值之前形成一个封闭区域(暂时性死区),此时无法访问。

const

使用 const 声明的变量必须同时初始化为某个值。一经声明,在其生命周期的任何时候都不能再重新赋予新值。

核心就是两点:

  1. 声明的同时必须有值
  2. 常量,任何时候不可以更改

其他都和上面的let一致

所以你以为到此就结束了嘛,龙宫法术,博大精深。远不止于此。

词法环境

没错,出现了一个新的东西(至少在JS高程4里我没看到)。

出现于各种介绍JS执行上下文的文章之中,但我最终确实没有看懂什么是词法环境,以及他出现的原因和解决的问题。

那么什么是词法环境?先看JS说明书的原文

A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code. A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment. Usually a Lexical Environment is associated with some specific syntactic structure of ECMAScript code such as a FunctionDeclaration, a BlockStatement, or a Catch clause of a TryStatement and a new Lexical Environment is created each time such code is evaluated.

An Environment Record records the identifier bindings that are created within the scope of its associated Lexical Environment. It is referred to as the Lexical Environment's EnvironmentRecord.

The outer environment reference is used to model the logical nesting of Lexical Environment values. The outer reference of a (inner) Lexical Environment is a reference to the Lexical Environment that logically surrounds the inner Lexical Environment. An outer Lexical Environment may, of course, have its own outer Lexical Environment. A Lexical Environment may serve as the outer environment for multiple inner Lexical Environments. For example, if a FunctionDeclaration contains two nested FunctionDeclarations then the Lexical Environments of each of the nested functions will have as their outer Lexical Environment the Lexical Environment of the current evaluation of the surrounding function.

A global environment is a Lexical Environment which does not have an outer environment. The global environment's outer environment reference is null. A global environment's EnvironmentRecord may be prepopulated with identifier bindings and includes an associated global object whose properties provide some of the global environment's identifier bindings. As ECMAScript code is executed, additional properties may be added to the global object and the initial properties may be modified.

A module environment is a Lexical Environment that contains the bindings for the top level declarations of a Module. It also contains the bindings that are explicitly imported by the Module. The outer environment of a module environment is a global environment.

A function environment is a Lexical Environment that corresponds to the invocation of an ECMAScript function object. A function environment may establish a new this binding. A function environment also captures the state necessary to support super method invocations.

Lexical Environments and Environment Record values are purely specification mechanisms and need not correspond to any specific artefact of an ECMAScript implementation. It is impossible for an ECMAScript program to directly access or manipulate such values.

嗯~通过强大的谷歌翻译再加我自己的大脑翻译

大致就是这么个意思(个人理解):

首先词法环境你可以说是一个类似于变量环境的东西。但说明书里只说这是一种定义的规范机制,不应该把他和任何实现联系。但是有趣的是通过查阅其他文章和资料我们发现,和一些具体实现还是有联系的。

Lexical Environments and Environment Record values are purely specification mechanisms and need not correspond to any specific artefact of an ECMAScript implementation. It is impossible for an ECMAScript program to directly access or manipulate such values.

首先与变量环境只挂载var变量不同,你用let和const声明的变量是挂在词法环境的,而说明书里也说了词法环境实质上不可访问,所以我们可以把暂时性死区放到这边关联起来就说的通。而这个阶段实际上有个专业名称叫词法分析。

而整个往下过程也和上面的执行环境介绍惊人相似。

通过说明书可以看到,词法环境也是分为全局词法环境(实际上说明书并不承认这是全局词法环境,但我的理解全局词法环境其实就是和全局上下文一样。最外层的那个词法环境),模块环境(不要被这些高大上的名字欺骗,我的理解就是块级作用域),函数环境(递归上面的概念,但是会创建一个新的this绑定,没错)所以看完这些 我的理解词法环境的核心就是:一个比执行环境原有概念和功能更丰富的,但机制和流程上没有区别的新的环境对象,内部也会有类似变量对象(let&const 函数 挂载地),作用域链(词法环境嵌套).---有错误请大佬及时指正

环境记录

详细说明说可以看这里 https://262.ecma-international.org/10.0/#sec-declarative-environment-records

大致就是将整个环境记录分为了三个过程, 声明记录,对象记录,以及全局记录

其中声明记录又分为 模块记录 和 函数记录两个过程

所以我这边理解一下就是

对于变量的let&const 函数 import/export 以及class 都是在声明记录的过程中去进行解析和挂载。

而对象记录, 就是对于对象内部的一个环境记录,也就是基于对象型的值内部做解析和挂载。有提到with的其实我也把他理解为一个对象,因为上面说了with就是类似复制了一个变量对象添加到作用域链前端

函数记录就相对比较好理解, 只要不是箭头函数和super,在此都会提供一个this绑定进去.

全局环境记录,就理解成全局环境就好了,具体里面做的事情也很多 绑定全局对象,设定内置变量等等

模块环境记录,这个从名字上就可以知道。是对于import/export的对象进行一个记录和解析。

以上仅是个人学习之后的理解,如有错误,请各位大佬指正。

javascript
49 views
Comments
登录后评论
Sign In