在说明执行上下文和作用域之前,我们需要了解一下LHS和RHS查询,这样才能更好的理解为什么JavaScript存在执行上下文和作用域。
LHS 和 RHS
我们都知道JavaScript是一门解释型语言/动态/脚本语言,他会在执行之前一刻进行编译,然后交给JS引擎执行。
编译
编译器负责把代码解析成机器指令,通常会有三个步骤:
- 分词/词法解析:将JavaScript字符串分解为
词法单元(token)
,如var a = 2
=>var
、a
、=
和2
。 - 解析/语法分析:将一个个
token
的流(数组)转为抽象语法树(AST)
- 代码生成:将
AST
转为机器指令,等待执行。
执行
JS引擎拿到一堆机器指令开始执行,这个时候需要查询变量,LHS
和RHS
就登场了。
- LHS (Left-hand Side):查询目的是变量赋值,如
a=1
,是为了将值1
赋给变量a
。 - RHS (Right-hand Side):查询的目的就是查询实际值,如
foo()
,查找foo
是函数,才能执行;如果不是函数就会抛出TypeError
异常;找不到则会抛出ReferenceError
异常。
而两种查询方法获取变量的都规则,就叫做 **作用域(Scope),而执行上下文(Execution Context)**则包含作用域。下面我们分别介绍他们。
执行上下文
什么是执行上下文
执行上下文,其包含定义变量的 环境记录(Environment Record)和 上下文(this),同时也控制着代码对变量的访问规则(这就是作用域),简单点说就是“代码执行的环境”。
所有的 JavaScript 代码在运行时都是在执行上下文中进行的,每创建一个执行上下文,就会将当前执行上下文放到一个栈顶,这就就是我们常说的执行栈。
JavaScript 中有三种情形会创建新的执行上下文:
- 全局执行上下文,进入全局代码的时候,也就是执行全局代码之前。
- 函数执行上下文,函数被调用之前。
- Eval 执行上下文,eval 函数调用之前。
执行上下文的组成
- 环境记录 (Environment Record)
它包含函数声明、参数和变量。之前也被成为为词法环境(Lexical Environment)
- 作用域
由词法环境决定,也称为静态作用域,变量在哪里定义就在哪里确定。每当 JavaScript 引擎尝试访问变量或函数时,它首先会查找当前执行上下文的变量对象。如果它在那里找不到标识,它就会沿着作用域链向上移动,检查每个父上下文的变量对象,直到找到标识符或到达全局执行上下文。如果在任何上下文中都找不到标识符,则会引发 ReferenceError。
**this**
当前的代码在哪个对象下被调用,如果没有则默认是window
(严格模式、箭头函数除外…)
执行上下文的生命周期
运行 JavaScript 代码时,当代码执行进入一个环境时,就会为该环境创建一个执行上下文,它会在你运行代码前做一些准备工作。
创建阶段
执行上下文的创建大体步骤如下:
-
创建环境记录 (Environment Record),包含变量环境(VO:variable object)
- 确定函数的形参(并赋值)
- 函数环境会初始化创建
_arguments_
_ _对象(并赋值) - 确定字面量形式的函数声明(并赋值)
var
定义的变量、函数表达式声明(未赋值,变量提升)- 记录
let
、const
定义的变量(不会声明!)
-
确定作用域(链)
词法环境决定,哪里声明定义,就在哪里确定
- 确定 this 指向
this 由调用者确定,箭头函数是词法决定
伪代码:
javascript">executionContextObj = {variableObject : {}, // 变量对象,里面包含 Arguments 对象,形式参数,函数和局部变量scopeChain : {},// 作用域链,包含内部上下文所有变量对象的列表this : {}// 上下文中 this 的指向对象
}
执行阶段
- 变量对象赋值
- 变量赋值
- 函数表达式赋值
- 调用函数
- 顺序执行其它代码
举个例子:
javascript">const foo = function(num){var a = "Hello";var b = function varB(){};function c(){}let d = "World"
}
foo(10);
- 创建阶段
javascript">executionContextObj = {variableObject : {num: 1, //确定形参并且赋值arguments: {0:10, length:1}, //确定argumens c: function c(){}, //确定字面变量定义的函数a: undefined,// var 定义的局部变量,初始值为undefinedb: undefined,// var 定义的局部变量,初始值为undefined// let 定义的变量只会记录,到执行赋值的阶段这里为暂死区}, scopeChain : {},//词法解释阶段已经确定了作用域,该阶段相当于引用this : {}
}
- 执行阶段
javascript">executionContextObj = {variableObject : {num: 1, //确定形参并且赋值arguments: {0:10, length:1}, //确定argumens c: function c(){}, //确定字面变量定义的函数a: "hello",// var 定义的局部变量,赋值b: function varB(){},// var 定义的局部变量,赋值d: 'world' //let 定义的局部变量,声明并赋值}, scopeChain : {},//词法解释阶段已经确定了作用域,该阶段相当于引用this : window //假定是全局定义
}
作用域
在MDN中,我们可以找到定义:
The scope is the current context of execution in which value and expressions are “visible” or can be referenced.
翻译一下:作用域(Scope)指的是在执行上下文中可见(或者说是可用)变量的范围。
也就是它跟执行上下文看起来很像,但又不同。通常分为:
- 全局作用域
指不在任何大括号或者函数中定义的变量
- 函数作用域
指在函数括号内定义的变量,只能在函数内部访问,不能在函数外部访问
- 块级作用域
块级作用域比较特殊,指在任意大括号内使用let
和 cosnt
定义的变量,其定义的变量只能在块级内部作用域中访问。同时它是没有this
的,可以认为它只保存了词法环境,保存这标识及其引用关系。
作用域链
当访问一个变量时,解释器会首先在当前作用域查找标示符,如果找到了,则使用当前作用域下的变量,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链。
那这个父作用域又是那个呢?实际上是要到创建这个函数的那个域。 作用域中取值,这里强调的是“创建”,而不是“调用”,切记切记——这种类型的作用域又称为静态作用域,也被称为词法作用域,因为在词法分析时就确定了查找关系(这也是和执行上下文最大的不同!)。
javascript">function foo(){console.log(a)
}function bar(){var a = 3;foo();
}
var a = 1;
bar();
上面代码会打印 1
,为什么呢?因为此处foo
的函数是在全局作用域window
上定义的,所以查找时现在foo
函数中查找a
,找不到会去window
上查找,所以此处a=1
。
顺便在看下this
的,感受下其中的不同,虽然这个与作用域无关…
javascript">function foo(){console.log(this.a)
}function bar(){var a = 2foo();
}
var a = 1;bar() // 1
bar.call({a:3}); //1
此处的两个输出会打印 1
,第一个大家可能容易理解,为什么第二个也是1
呢?此处foo
被调用时,其执行上下文指向的依然是全局执行上下文,所以这里的this
也指向window
,所以此处a=1
。
变量提升
上面说过,JavaScript中,存在函数声明和变量声明总是会被解释器悄悄地被"提升"到方法体的最顶部,这就是我们常说的变量提升,注意:
- 只有声明的变量会提升,值不会。
- 严格模式下不存在变量提升。
let
和const
也存在变量提升,但是let
和const
定义的变量会造成暂时性死区,定义变量后一开始就形成了封闭作用域,凡是在声明之前就使用这些变量,就会报错。
var
和let
的声明提升:
javascript">console.log(a) //undefined
var a = 1var b = 1
{//报错,如果没有提升,不是应该显示成1?,所以是有提升console.log(b)let b = 2
}
当前作用域下只要存在变量,就算是变量提升得到,也相当于找到了,不会再去父作用域中找:
javascript">var a = 1
function foo(){console.log(a)var a = 2
}
foo()//undefined
函数定义也存在变量提升,而且是整体提升,如果是函数变量则看定义的关键字是var
还是其他,这和上文保持一致:
javascript">console.log(age);
var age = 20
console.log(age);// 1.提升到最前面
function age() {}
// 2.将function改成匿名函数的方式,则不会提升
//var age = function(){}console.log(age);
// 1. 函数提升输出
//f age(){}
// 20
// 20//2. 将function改成匿名函数的方式,则不会提升(注意要重新在一个新的环境下运行)
//undefined
//20
//ƒ age(){}
参考
了解词法环境吗?它和闭包有什么联系?
[
](https://www.jianshu.com/p/4b83b97cb39e)