前言
既然执行上下文,环境记录,作用域,作用域链,词法环境,变量环境,闭包等是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的值),等等。
此处我们给出规范中的介绍,其他的不再过多的解释。
简而言之,
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.
在?感兴趣的还可以看看规范对闭包的描述:
抽象闭包规范类型用于引用算法步骤和值的集合。
闭包的概念,实际上我认为并不难,难的另有所在。
其中之一,就是...
闭包的产生条件
这一点各有各的说法,在这里我推荐一个我认为讲的很清楚的文章(波神的公众号是宝藏哦~)
引用文章中的一句话。
对于有一点 JavaScript 使用经验但从未真正理解闭包概念的人来说,理解闭包可以看作是某种意义上的重生,突破闭包的瓶颈可以使你功力大增。
另外的难点就是......
闭包的出现场景
(闭包这块对于我来说还是太难啦,所以这部分就当分享与交流咯~)
-
在需要清除setInterval的地方。
我们知道,每个setInterval一旦启动,就需要手动清除,并且只能在其回调函数内清除(不然呢不然呢不然呢)。这里的回调函数因为捕获了外部的定时器标识,而生成了闭包。
-
每一个中间模块(即接受别的模块,导出用到了这个模块的新的模块)。
这里引用一段大神的话。
本质上,JavaScript中并没有自己的模块概念,我们只能使用函数/自执行函数来模拟模块。
现在的前端工程中(ES6的模块语法规范),使用的模块,本质上都是函数或者自执行函数。
(webpack等打包工具会帮助我们将其打包成为函数)
推荐看大神的这篇文章,相信会有意想不到的收货。
-
......(想不到了!!!我好菜。。)
闭包到这里就先告一段落。
其他概念
变量对象(?)与活动(函数)对象
不知道从什么时候起,有了变量对象和函数对象的概念,应该是一些教材里面的,由于我没有看过,,所以这些概念我也只能从规范中寻找。。。
结果只找到了活动(函数)对象的概念。就一句话:
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中的所有数据都是引用类型的,只是通过创建新的实例的方式,来保证数字、字符串等数据类型的不可变性。(例如,新建一个基本数据类型,你会发现它也能调用方法)
**对于引用类型的数据,按值传递可以说是自然地选择。**函数对形参的改变,会实际反映到实参上(搞清楚形参与实参哟),这一点与值类型的按引用传递相同;但函数内无法更换实参的引用,对形参重新赋值,仅仅是更换了形参的引用,对实参没有影响,这一点却和值类型的按值传递相同。
静态作用域与动态作用域
。。。桥豆麻袋,待我翻下书再补。
全局执行上下文
刚刚了解这个概念,大体上是:
执行上下文栈为空时,创建的第一个执行上下文,根据全局环境记录(Global Environment Record)创建,在一般的执行上下文基础上,增添了基础的全局变量等。(有待考证,先记录一下)
差点忘了this
由于JavaScript自己的执行上下文,闭包等,并不能做到一个需求——在对象内部的方法中,人为地指定使用对象内部的某个属性。所以,有了this机制,并且this指向无法更改。
这样想来,我们大可不必将this与其他奇怪的概念扯到一起,联系一下执行上下文就行了。
所以,一般情况下,this 的作用就是在对象的方法中,指向对象本身,方便开发者使用对象的属性。
然鹅,为什么要说一般呢(可达鸭眉头一皱,发现事情并不简单(。•ˇ‸ˇ•。))。
由于设计缺陷(斗胆),在非严格模式下,this在普通函数中仍然存在,并指向window对象,违背了其出现的意义。
而且,由于this的绑定是在创建执行上下文时进行的,因此导致了另一个问题——在对象方法中嵌套使用函数,其内部this的指向会违背使用直觉。
(对于this具体的绑定过程,只是我的推断,但并未验证,望大佬指点。)每当创建函数时,就会创建新的执行上下文,进行this的绑定,当其顺着作用域链查找临近的一个外层执行上下文时,发现自己是在对象当中创建的,则将this绑定为这个对象;若临近的外层执行上下文中没发现对象,则将this绑定为window或undefined
因此在对象的方法中,若继续嵌套函数,由于创建新的执行上下文时,临近的执行上下文内并没有对象(是对象内的方法),所以其内部的this将不再指向对象,发生了重新绑定,变为了window(非严格模式)或undefined。
当然,若在同样的场景嵌套箭头函数,则不会产生这样的问题,因为箭头函数是不存在自身的this的,换言之,箭头函数不会对内部的this进行绑定。
阿这,好累啊。。。感谢观看
该文章首发于个人博客,若文中存在理解错误的概念,欢迎大佬指正。
author: Jankin
authorLink: https://www.laic.club
tags:
- ECMAScript2021
- JavaScript
date: 2020-06-30 00:08:31