前言

​ 既然执行上下文环境记录作用域作用域链词法环境变量环境闭包等是JS的重要概念,那么它们对于我们来说,是不陌生的。

​ 然而大多数人在学习前端的过程中,很少也很难从语言规范入手,所以这些概念是属于早接触,晚理解的那一类。

​ 然而,整个学习过程中,本人虽然也尝试深入理解了这些概念,但并没有达到理想的效果(总有人在概念后面打括号,将一些概念划等号,不告诉我们为什么没问题,问题是不同的人划的等号还不同?!(╯‵□′)╯︵┻━┻),为此,我决定根据ECMAScript2021,来进一步地理解它们。

规范类型(Specification Types)

在讲以上的重要概念之前,我们需要先了解这么一个概念——规范类型。

​ 引用规范中的话:

A specification type corresponds to meta-values that are used within algorithms to describe the semantics of ECMAScript language constructs and ECMAScript language types.

规范类型,对应于算法中用于描述ECMAScript语言结构和语言类型的语义的元值。

​ 简单来说,就是一些不可拆分的数据结构(个人理解,有误望指出),而这些数据结构,被用来描述ECMAScript这一门语言。其中包括我们熟知的列表List),指针Reference),集合Set)等等,其中这里需要强调的也是规范中常用的一种数据类型,叫作记录Record)。

记录(Record)

​ 同样,引用规范中的介绍:

The Record type is used to describe data aggregations within the algorithms of this specification.

​ 在本规范中,记录,是用来描述算法中数据的集合(集合体)的。

记录的值在形式上类似于我们的键值对,由字段与字段的组成。其中字段总是形如[[Field]]

关于规范类型记录,就先扯这么多,大致理解前者是描述语言的元数据类型,后者是前者中的一种就先够了。我们后面会提的环境记录,就属于记录的一种。

执行上下文(Execution Contexts)——也就是作用域(Scope)这一概念在JS中的体现

首先我们先来解释一下这个标题。为什么说作用域这一概念在JS中的体现是执行上下文呢?

作用域这一概念,本就不是,也不需要由ECMAScript来定义。它的意义百度一下就能知道:

作用域(scope),程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。

MDN中也明确地写到,Scope:

The current context of execution.

只是作用域这个概念相比于执行上下文来说,前者更偏向于表示代码中名称(标识符)的可达性,而后者更偏向于表示规范中跟踪代码运行的一个程式(device)。后者的意义显得更加完整一些,当然,实际生活中我们使用的时候不一定用的那么得严谨。

接下来我们先来搞清楚执行上下文,再来说作用域链

那么,执行上下文到底是个什么东西?

扯一个题外话

为什么我们要理解执行上下文这个概念而不是其他?一些小伙伴对其他概念可能也有这样的疑问,道理是一样的

​ 我们都知道,由于流控制语句与函数调用的关系,程序并不是单纯的从上往下执行的。所以要想正确的执行代码,就需要将代码转化为正确的机器码。而这,有两种办法,编译与解释,分别对应了两种语言,编译型语言和解释型语言。

​ 随便一查“JavaScript”,就会看到众多“脚本语言”的字眼。而脚本语言的特点之一,就是解释执行。然而为了解决解释语言解释器低效的问题,浏览器,也就是ECMAScript Implementation,引入Just-in-time编译器(JIT)。从而实现了变量提升等解释型语言难以实现的特性。

​ 上面说了这么多,和执行上下文有什么关系,为啥要理解执行上下文???好吧,一个点是因为JIT是基于执行上下文来实现的。当然还有其他的原因,比如说,为了理解闭包分析复杂结构代码,或者检查 Bug 等等。不仅能通过执行上下文栈来解决工作中的Bug,还能进一步了解闭包的产生条件闭包的工作原理等等,所以,执行上下文他。。不香嘛???๑乛◡乛๑。(那。。。为什么要理解闭包?因为经常要用并且还老是出Bug啊,此处禁止套娃哈哈,另,下面有闭包的一些适用场景)

回归正题

执行上下文是什么?老样子,引用:

An execution context is a specification device that is used to track the runtime evaluation of code by an ECMAScript implementation.

​ 有道直译是这样的:

执行上下文是一种规范设备,用于跟踪ECMAScript实现对代码的运行时评估。

​ 那么,个人理解一下。执行上下文是一种特殊的手段,被用来跟踪不同的ES实现(例如V8引擎、Node.JS)中,代码的运行。

​ 首先,为了跟踪(track),或者说管理执行上下文。有了执行上下文栈的概念。

The execution context stack is used to track execution contexts. The running execution context is always the top element of this stack.

​ 相信大家对这个栈的疑问并不是很大,只需要明白这里又多了个当前执行上下文(或者就叫正在运行的执行上下文,字太多了,后文就用当前执行上下文称呼)的概念,在浏览器中是由一个记录当前执行状态的指针ESP所指向的,通过控制该指针,来销毁栈中的执行上下文(没指向就能用下一个执行上下文直接覆盖,并由垃圾回收器回收该执行上下文中使用的内存,关于垃圾回收,在找到官方解释之后我也会总结)。

​ 好家伙,一个执行上下文整出这么多事儿,看样子我还需要其他信息才能知道他是啥!!

An execution context contains whatever implementation specific state is necessary to track the execution progress of its associated code.

​ 。。。这里直译就可以了:

在实现中,执行上下文包含跟踪相关代码的执行进度所必需的,任何特定状态。

​ 说了等于没说。。。那不然说这是规范呢!但人还是有要求的:

其中构成上,至少有六个部分(我们知道,当我们尝试理解一个新事物的时候,新事物的构成并不一定是最重要的,因为我们可能并没有达到需要了解他的地步,例如,牛奶。):

code evaluation state,Function,Realm,ScriptOrModule,LexicalEnvironment,VariableEnvironment

简单的说下我的理解。

通过一些component来构成一个执行上下文,描述了其关联的代码所能使用的作用域(执行上下文)的(广义上的)变量(LexicalEnvironment和VariableEnvironment);这个执行上下文本身的跟踪(评估)对象(Function Component的值),等等。

此处我们给出规范中的介绍,其他的不再过多的解释。

components for execution context

additional state components

简而言之,

An execution context contains whatever implementation specific state is necessary to track the execution progress of its associated code.

Whatever is necessary. \( ̄︶ ̄)/

类比于牛奶,对于执行上下文的构成我们不需要了解的很深,我们将剩下的精力集中在它的运行机制上,然而也很简单。

前面说了,有一个执行上下文栈来维护执行上下文。栈顶的执行上下文就是当前执行上下文

比如说浏览器中,代码刚开始运行,负责运行JS的线程就通过某些算法(用来Enqueue Jobs)取得任务,创建第一个执行上下文,然后开始执行,此时也是当前执行上下文,每当遇到新的Function、Modul/Script时,就会创建新的执行上下文,并挂起当前的执行上下文(若没执行完的话),即新执行上下文成为栈顶,当新的执行上下文关联的代码执行完毕,即出栈后,则重新恢复刚刚挂起的执行上下文。

值得我们注意的只是,每个当前执行上下文的作用域,都包含了栈内的其他执行上下文中的作用域。

好,执行上下文没了。就这?就这。

作用域链&变量环境、词法环境

前面说,JS中通过执行上下文通过来描述作用域。而作用域链这个概念呢,在规范中是不存在的。所以我斗胆自己总结了一下(逃)。

作用域链,就是当前执行上下文的两个环境记录(Environment Record,一种记录类型),即词法环境(LexicalEnvironment),和变量环境(VariableEnvironment)以及它们所指向的其他环境记录

这些环境记录记录了所有的变量、函数、类、模块,对象,内置全局对象,内置全局对象的属性,以及顶级声明与名称(标识符)的绑定关系。简单的来说,即静态作用域(静态作用域与动态作用域的概念。。。下面也会补)中所有的名称(标识符)绑定。

这两种环境记录呢,就是前面所提的Record类型,因为其内部的[[OuterEnv]]字段,指向了其他(外部)执行上下文的环境记录(没有外部环境记录的,该字段值就为null),很容易抽象成链条的形式,所以被称为作用域链。

这两种环境记录还可以稍加区别,如,”var声明的变量存在变量环境中,let、const声明的变量存在词法环境“——该结论来源于李兵大佬关于浏览器的一门课程,关于浏览器,推荐有条件的同学看这系列文章,之后我也会做相关的学习记录。

对以上概念如有疑问,详见规范的8.1节

闭包

在?先看我的理解。(洗脑洗脑洗脑

闭包,是一种,能够捕获标识符(广义上的变量),携带参数,并且以类似函数的调用方式(closure(arg1, arg2))调用,的规范类型。

个人认为,重要的就只有两部分。

一,咳咳,它是一种规范类型(Specification Type),这是毋庸置疑的,毕竟规范里写的明明白白。(所以我们就像对待Record那样对待它就行啦,如果特别感兴趣,再去思考内存中的闭包到底是怎样的罢——函数¿)

二,它能够捕获变量,一旦闭包捕获了变量,那这些变量就不会无缘无故的消失(指垃圾回收),这也是闭包在众多场景出现的主要原因。规范中的例子也表明了这一点:

Abstract Closures are created inline as part of other algorithms, shown in the following example.

1.Let addend be 41.

2.Let closure be a new Abstract Closure with parameters (x) that captures addend and performs the following steps when called:

​ Return x + addend.

3.Let val be closure(1).

4.Assert: val is 42.

在?感兴趣的还可以看看规范对闭包的描述:

The Abstract Closure specification type is used to refer to algorithm steps together with a collection of values.

抽象闭包规范类型用于引用算法步骤和值的集合。

闭包的概念,实际上我认为并不难,难的另有所在。

其中之一,就是…

闭包的产生条件

这一点各有各的说法,在这里我推荐一个我认为讲的很清楚的文章波神的公众号是宝藏哦~

引用文章中的一句话。

对于有一点 JavaScript 使用经验但从未真正理解闭包概念的人来说,理解闭包可以看作是某种意义上的重生,突破闭包的瓶颈可以使你功力大增。

另外的难点就是……

闭包的出现场景

(闭包这块对于我来说还是太难啦,所以这部分就当分享与交流咯~)

  1. 在需要清除setInterval的地方。

    我们知道,每个setInterval一旦启动,就需要手动清除,并且只能在其回调函数内清除(不然呢不然呢不然呢)。这里的回调函数因为捕获了外部的定时器标识,而生成了闭包。

  2. 每一个中间模块(即接受别的模块,导出用到了这个模块的新的模块)。

    这里引用一段大神的话。

    本质上,JavaScript中并没有自己的模块概念,我们只能使用函数/自执行函数来模拟模块。

    现在的前端工程中(ES6的模块语法规范),使用的模块,本质上都是函数或者自执行函数。

    (webpack等打包工具会帮助我们将其打包成为函数)

    推荐看大神的这篇文章,相信会有意想不到的收货。

  3. ……(想不到了!!!我好菜。。)

闭包到这里就先告一段落。

其他概念

变量对象(?)与活动(函数)对象

不知道从什么时候起,有了变量对象和函数对象的概念,应该是一些教材里面的,由于我没有看过,,所以这些概念我也只能从规范中寻找。。。

结果只找到了活动(函数)对象的概念。就一句话:

The value of the Function component of the running execution context is also called the active function object.

即,当前执行上下文Function Component的值。这个值,在前面的图片上有说,就是当前执行上下文评估的那个函数对象。原话是:

If this execution context is evaluating the code of a function object, then the value of this component is that function object. If the context is evaluating the code of a Script or Module, the value is null.

然而我没有找到变量对象的概念(暂时),所以我就结合起来理解,将他理解为非当前执行上下文的Function Component的值,即非当前执行上下文所评估的函数对象。

名称绑定&绑定关系——值模型、引用模型

这个概念是对所有编程语言来说的~

所谓的名称绑定(Name binding),是指将名字和他所要代表的实体联系在一起。通常在编程语言中,名称被称为标识符。一般来说,名称绑定的实体是可以被更换的,这样的名称就是大家熟知的变量,而被绑定的实体则是变量值,通过赋值来更换变量值。

其中,变量,根据其与绑定的值的关系,可以分为值模型引用模型

这两种模型的差异导致的主要影响是:一个变量的值被赋予另一个变量,应用值模型时,值会被复制,副本保存在被复制的变量中,两者就此互不相关;而应用引用模型师,只是指针被复制,赋予第二个变量,数据仍然只有一份,若是其中一方修改了指针指向的数据,另一方也能看到同样的变化。

因此这两个模型,前者更安全,后者对体量巨大的数据则节省了复制的时间和空间。采用引用模型的数据,被称为引用类型,而采用值模型的数据,则被称为值类型。

JavaScript的参数传递方式

JavaScript属于动态类型的语言,因为这类语言可以被赋予任何类型的值,所以基本都采用引用模型,JavaScript亦是如此。

但是值得一提的是,数据的值类型、引用类型和参数传递的两种方式——按值传递、按引用传递,是不同的概念。

按值传递将实际参数的值复制到函数的形参中,两者互不干扰;而按引用传递是将实际参数的引用传递给形参,相当于在函数内部进行了一次新的名称绑定。

对于JavaScript的参数传递方式,普遍存在两种声音。但实际上,JavaScript采用的是按值传递的方式

在JavaScript中的所有数据都是引用类型的,只是通过创建新的实例的方式,来保证数字、字符串等数据类型的不可变性。(例如,新建一个基本数据类型,你会发现它也能调用方法)

对于引用类型的数据,按值传递可以说是自然地选择。函数对形参的改变,会实际反映到实参上(搞清楚形参与实参哟),这一点与值类型的按引用传递相同;但函数内无法更换实参的引用,对形参重新赋值,仅仅是更换了形参的引用,对实参没有影响,这一点却和值类型的按值传递相同。

静态作用域与动态作用域

。。。静态作用域与动态作用域的区别:它们开头第一个字不一样。

开个玩笑,其实实质上也很简单。

静态作用域,指的是任何一个标识符,在访问其值时,访问的都是其被定义时所在作用域的值。

举个简单的例子:

let a = 0;

function foo(){
    console.log(a);
}

function bar(){
    let a = 1;
    foo();
}

bar();    //0

这就是静态作用域,与之相反,动态作用域,在访问任何一个标识符时,都是其在访问/调用的位置的值。

全局执行上下文

刚刚了解这个概念,大体上是:

执行上下文栈为空时,创建的第一个执行上下文,根据全局环境记录(Global Environment Record)创建,在一般的执行上下文基础上,增添了基础的全局变量等。(有待考证,先记录一下)

差点忘了this

由于JavaScript自己的执行上下文,闭包等,并不能做到一个需求——在对象内部的方法中,人为地指定使用对象内部的某个属性。所以,有了this机制,并且this指向无法更改。

这样想来,我们大可不必将this与其他奇怪的概念扯到一起,联系一下执行上下文就行了。

所以,一般情况下,this 的作用就是在对象的方法中,指向对象本身,方便开发者使用对象的属性

然鹅,为什么要说一般呢(可达鸭眉头一皱,发现事情并不简单(。•ˇ‸ˇ•。))。

由于设计缺陷(斗胆),在非严格模式下,this在普通函数中仍然存在,并指向window对象,违背了其出现的意义

而且,由于this的绑定是在创建执行上下文时进行的,因此导致了另一个问题——在对象方法中嵌套使用函数,其内部this的指向会违背使用直觉

(对于this具体的绑定过程,只是我的推断,但并未验证,望大佬指点。)每当创建函数时,就会创建新的执行上下文,进行this的绑定,当其顺着作用域链查找临近的一个外层执行上下文时,发现自己是在对象当中创建的,则将this绑定为这个对象;若临近的外层执行上下文中没发现对象,则将this绑定为window或undefined

因此在对象的方法中,若继续嵌套函数,由于创建新的执行上下文时,临近的执行上下文内并没有对象(是对象内的方法),所以其内部的this将不再指向对象,发生了重新绑定,变为了window(非严格模式)或undefined。

当然,若在同样的场景嵌套箭头函数,则不会产生这样的问题,因为箭头函数是不存在自身的this的,换言之,箭头函数不会对内部的this进行绑定。

阿这,好累啊。。。感谢观看


你喜欢独处,却又担心寂寞,于是你爱上一阵又一阵迎面吹来的风