什么是执行上下文环境
执行上下文环境是代码运行时跟踪记录代码运行时环境的一种抽象说法。对于后面的let/const暂存性死区、变量提升、闭包等概念的理解很重要。分为:
- 全局执行上下文
- 函数执行上下文
- eval执行上下文:eval函数执行时,生成的专属它的上下文。
有了上下文环境,就需要执行栈来管理。执行栈是用来管理执行上下文的数据结构,它是一个先进后出的栈,也是我们熟知的JS程序运行过程中的调用栈。
在程序运行时,会先创建一个全局执行上下文并压入到执行栈中,之后每当有函数被调用,都会创建一个新的函数执行上下文并压入栈中。那么在创建执行上下文的时候都发生了什么?在创建执行上下文的时候会做两件事情:
- 创建词法环境
- 创建变量环境
词法环境和变量环境
1. 词法环境
- 词法环境是ECMA中的一个规范类型。词法环境里面建立了变量名称或函数名称--实际变量值或对象函数引用地址的一个映射。
- 词法环境由环境记录和外层引用构成,环境记录是存放变量和函数声明的地方。外层引用提供了父词法环境的引用,可能为null。
- 词法环境类型:全局环境(内置的全局对象/全局函数,比如:Math,Object,Array,eval,parsieInt),模块环境(在node程序中运用的较多,比如export,module变量),函数环境(调用函数时产生,通常涉及this指向,super调用,函数的length和arguments属性)
2.变量环境
首先变量环境本质上还是词法环境,只不过var有变量提升特性,而let/const没有变量提升。为了区分这两种情况,所以变量环境里面专门存放着var声明的变量,所以在var声明的变量在初始化时可以赋值为undefined。
let/const与var
在早期只有var 没有let的时代,var会穿透for,if等语句。同时为了避免这种情况诞生了一个技巧:立即执行函数表达式。通过创建一个立即执行函数,来构造一个新的域,从而控制var的范围。
(function(){
var a;
//code
}());
(function(){
var a;
//code
})();
这种写法有一个问题,当上一行代码没有分号的时候,这个函数会被认为是上一行代码最末的函数调用。所以最好采用下面这种写法
void function(){
var a;
}();
在ES6出现后,引入了let,为了实现let,JavaScript 在运行时引入了块级作用域。以下是var、let、const的区别:
- 存放位置: let/const声明的变量是归属于词法环境,var声明的变量是归于变量环境
- 初始化: 因为let/const声明的变量在没有执行到具体的赋值操作时提前读取变量会报错,这既是暂时性死区。而var在初始化时会被赋值为undefined,即使没有执行赋值行,任然可以读取变量(undefined)。
- 块作用域: let/const有块作用域,因为ECMA规定在遇到{}时会新创建一个执行上下文。let/const声明的变量、函数都放在这个新环境中,而var不受这个限制。 除了var、let、const,在ES9中新引入了Realm,Realm主要是指通过iframe等方式创建多window环境,里面包含了一组完整的内置对象,与js是镜像关系。
对于闭包的理解
闭包简单理解就是一个自带执行环境的函数,与普通函数的区别就是它携带了执行环境。所以定义闭包可以包含两部分:
- 环境部分(函数的词法环境)
- 表达式部分(函数体) 当函数可以记住并访问所在的词法作用域时,就产生了闭包。从广泛的角度说,普通函数就属于闭包,但这对于我们真正理解闭包毫无意义。真正的闭包应该是即使函数是在当前词法作用域之外执行,仍访问到函数内部属性。
值得注意的是一个函数的上下文执行环境不是调用时的环境,而是遵循了“继承定义时环境”的规则。但是JavaScript支持运行环境动态切换。一旦上下文执行被切换,那么整个语句的执行效果都会不一样。所以切换上下文的时机就很重要了。在JavaScript中切换上下文最主要的场景就是函数的调用。
函数中this的指向
大致可以分为以下几种情况:
- 普通函数:this指向函数调用的对象
- 箭头函数:this指向箭头函数声明时候的对象(技巧:看有没有外层函数,没有外层函数this指向window;否则外层函数的this指向谁,箭头函数的this就指向谁)