前言
最近打算深入学习下js的一些基本而又重要的概念,因为即便技术在不断更新,但基本的思想是不变的,只要能够掌握这些基础,再去学习的新的东西就能事半功倍。以此篇文章做个记录。
执行上下文
定义:代码被解析和执行时所在的环境。
类型:执行上下文分为三种类型
- 全局执行上下文:只有一个,在浏览器中的全局对象就是window对象。
- 函数执行上下文:有无数个,在函数被调用时被创建,每次调用函数都会创建一个新的执行上下文。
- Eval函数执行上下文:指运行在eval函数中的代码,少用也不建议使用。
创建:分为两个阶段:(1)创建阶段(2)执行阶段
创建阶段
- 确定this的值,也被称为 This Binding。
- LexicalEnvironment(词法环境)组件创建。
- VariableEnvironment(变量环境)组件创建。
This Binding
在全局执行上下文中,this的值指向全局对象,在浏览器中也就是window对象,在nodejs中为这个文件的module对象。在函数执行上下文中,this的值取决于函数的调用方式,有默认绑定、隐式绑定、显示绑定、箭头函数等。
词法环境
词法环境分为两个部分组成:
- 环境记录:存储变量和函数声明的位置
- 对外环境的引用:可以访问其外部词法环境
词法环境分为两种类型:
- 全局环境: 是一个没有外部环境的词法环境,其外部环境引用为null,拥有一个全局对象及其关联的方法和属性以及自定义的全局变量,this的值指向这个全局对象
- 函数环境:用户在函数中定义的变量被存储在环境记录中,包含了arguments对象,对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数
变量环境
变量环境也是一个词法环境,因此它具有词法环境中所有的属性。
在ES6中,词法环境和变量环境的区别在于存储函数声明和变量(let和const)绑定,而后者仅用于存储变量(var)绑定。
在创建阶段,函数声明存储在环境中,而变量会被设置为undefined(在var的情况下)或保持未初始化(在let和const的情况下)。这就是为什么可以在声明之前访问var定义的变量(尽管是undefined),但是在声明之前访问let和const定义的变量就会提示引用错误的原因,这就是所谓的变量提升。
执行阶段
在此阶段,完成对所有便利店分配,最后执行代码。
注:如果javascript引擎在源代码中声明的实际位置找不到let变量的值,那么将为其分配undefined值
执行栈
执行栈也叫调用栈,具有先进后出的结构,用于存储代码执行中创建的执行上下文。
当代码首次运行时,会创建一个全局执行上下文并push到执行栈中,然后每调用函数,js引擎都会函数创建一个新的函数执行上下文并push到当前的执行栈中。当栈顶的函数执行完后,其对应的函数执行上下文会从执行栈中pop出,当前上下文的控制权就会移交到下一个执行上下文。只有当整个应用程序结束的时候,执行栈才会被清空,所以程序结束之前, 执行栈最底部永远有个全局执行上下文。
函数执行上下文
在函数执行上下文中,用活动对象(activation object, AO)来表示变量对象
活动对象和变量对象的区别在于:
- 变量对象(VO)是规范上或者js引擎上实现的,并不能在js环境中直接访问。
- 当进入一个执行上下文后,这个变量对象才会被激活,所以叫活动对象(AO),这各时候活动对象上的各种属性才能被访问。
所以可以认为活动对象是被激活的变量对象。
函数的执行上下文维护了一个作用域链,会指向上级作用域,作用域链其实是一个数组,结构如下:
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
复制代码
当前指向关系为当前作用域 --> 上级作用域 --> 全局作用域。所以即使checkscopeContext被销毁了,但是javaScript仍然会让其活动对象存活在内存中,当前函数通过作用域链找到它,这就是闭包实现的关键。
作用域
含义:负责收集并维护所有的声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
为了帮助我们理解JavaScript的工作原理及作用域的作用,我们介绍两个角色:
-
引擎
从头到尾负责整个JavaScript程序的编译及执行过程。
-
编译器
引擎的好朋友之一,负责语法分析及代码生成等脏活累活。
接着我们思考下,当javaScript引擎遇到var a = 3;
它是怎么分析处理的呢?你可能会觉得很简单,这不就是一句声明语句嘛,声明一个变量a,然后将值2赋值给a。其实这并不完成正确。
事实上编译器会进行如下处理:
-
遇到var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域集合中声明一个新的变量,并命名为a。
-
紧接着编译器会为引擎生成运行时所需的代码,这些代码被用来处理a = 3这个赋值操作,引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作a的变量,如果是,引擎就会使用这个变量,否则,引擎会继续查找该变量,如果最终找到了这个a变量,就会将3赋值给它。
所以变量的赋值操作会分两个步骤执行,首先编译器会在当前作用域中声明一个变量(如果之前没声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它进行赋值。
在上面的介绍中,我们提到了引擎在执行编译器生成的代码过程中,会通过查找变量a来判断它是否已经声明过,这个查找过程就要作用域来进行协助,那具体是如果查找的呢?
在这个例子中,引擎会为变量a进行LHS查询,还有另一个查询类型叫RHS。 我猜你看到“L”和“R”就能猜到它的含义,没错,它们分别代表左侧和右侧。
那么是什么东西的左侧和右侧呢?是赋值操作的左侧和右侧。
简单的讲就是当变量在赋值操作的左侧时会进行LHS查询,出现在右侧时会进行RHS查询。更准确点就是,RHS查询与简单地查找某个变量的值别无二致,而LHS查询则是试图找到变量的容器本身,从而可以对其赋值。从这个角度说,RHS并不是真正意义上的“赋值操作的右侧”,更准确地说是“非左侧”。你可以将RHS理解成取某某的源值。
区分
那为什么区分LHS和RHS呢?这是因为在变量还没声明(在任何作用域都无法找到)的情况下,这两种查询的行为是不一样的。
如果RHS查询在所有作用域中寻找不到所需的变量,引擎就会抛出ReferenceError异常。值得注意的是,这个一个非常重要的异常类型。相较之下,在执行LHS查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下。如果是在严格模式中,则不会返回一个全局变量,而是会抛出同RHS查询失败时类似的ReferenceError异常。
接下来,如果RHS查找到了一个变量,但是你尝试对它进行不合理的操作,比如试图对一个非函数类型的值进行函数调用,或引用null或undefined类型的值中的属性,那么引擎会抛出另一种类型的异常,叫作TypeError。
总结:作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。
参考:你不知道的JavaScript上卷
muyiy.cn/blog/
(初次写博客,有不对的地方请指正!)