js中的执行上下文--声明提升,暂时性死区等
在JavaScript代码开始执行前,js解析器做好了一系列的准备工作,并且规定了变量、函数的访问路径。(这个为个人的理解,如有理解错误,请大佬们指正!)
JavaScript在代码运行前做了什么?会创建执行上下文,也就是准备好代码运行时的环境,包括变量对象、作用域链、this
代码运行阶段,变量、函数、this的访问,均是从其执行上下文中获取。其中变量的获取分为:执行上下文创建阶段变量初始化的获取,和运行阶段变量赋值后的获取。函数和this在创建阶段已经绑定了引用。
一、执行上下文
在js代码执行前,会创建执行上下文,也就是所谓的代码运行环境。那么该运行环境有什么作用?————实际上,代码运行时,关于变量的访问、函数的调用及this
的指向绑定,都和执行上下文有着密切的关系,可以认为,都是从执行上下文中获得它们的值。
1.1运行环境
JavaScript代码有三种运行环境,对应着三种执行上下文
- global code:全局作用域下的代码。不包括任何function体内的代码
- Function Code:函数调用执行的代码,不包括其自身内部的函数的代码
- Eval Code:eval()执行的代码
1.2EC三个属性
执行上下文是一个对象,它具有三个属性:scope chain
,variable object
,this
。
- 变量对象(variable object):
vars、function declarations,arguments...
- 作用域链(scope chain):
variable object + all parent scopes
- this:
context object
1.2.1变量对象variable object
变量对象是与执行上下文相关的数据作用域。存储了在上下文定义的变量和函数声明。
- variable declaration,初始化为
undefined
- Function declaration,保存函数名称并持有函数体的引用
- 函数的形参,初始化为
undefined
- 函数执行上下文中,没有使用var声明的变量为全局变量,所以不在变量对象中
- 函数表达式也不包含在变量对象中
- let,const声明的变量初始化为
uninitialed
1.2.2作用域链scope chain
全局执行上下文没有外部的作用域,因此定义其作用域链为自身的变量对象。
函数执行上下文中的作用域链:实际上,当函数定义的时候,函数所有的外层变量对象(即集合)都会保存在函数的内部属性[[scope]]
中,当创建函数执行上下文时,首先创建了该对象的属性--作用域链,并把[[scope]]
复制到scope chain
中,但这并不是完整的作用域链。接着便是变量对象variable object
的创建,创建过程见下文——JavaScript代码的执行前后做了什么--执行上下文创建的完整过程。创建完成后,把变量对象复制到scope chain
的顶端,形成完整的作用域链。
1.2.3this
this
在创建执行上下文时进行绑定
1.2.3.1全局代码中的this
this
的绑定指向始终为window
对象
console.log(this)//window,非严格模式下
//严格模式下为undefined
1.2.3.2函数代码的this
-
默认绑定:函数调用时,若无前缀,则
this
绑定为window
function test() { console.log(this) } test()//window function f1(){ function f2(){ console.log(this) } f2() } f1()//window
-
隐式绑定:函数调用时,前面存在调用它的对象,则
this
会隐式绑定到这个对象上function f1(){ console.log(this.name,this) } f1()//window let obj = { name:'hello', fn:f1 } obj.fn()//
-
显式绑定:call,apply,bind改变this
-
new绑定:
new
一个函数,实际进行了的操作:- 创建一个空对象,将它的引用赋给
this
,继承函数的原型 - 通过
this
将属性和方法添加到这个对象 - 若没有手动返回其他的对象,则最后返回
this
指向的对象(即实例) -
- 以构造器的
prototype
属性为原型创建一个对象 - 将这个对象和调用参数传给构造器执行,apply。。
- 如果构造器没有手动返回对象,则返回第一步的对象
这个实例中的方法中(即对象的方法内的代码中)的
this
指向所创建的实例 - 创建一个空对象,将它的引用赋给
-
箭头函数中的this
箭头函数没有自己的this,
this
指向取决于外层作用域中的this
,一旦箭头函数的this绑定成功,也无法被再次修改,但可以修改外层函数的this指向达到间接修改箭头函数this的目的。
题外话:定时器中的回调函数,如果是匿名函数,那么this指向window对象,可以改用箭头函数,那么this则默认指向上层作用域中的this,
二.JavaScript代码的执行前后做了什么----EC的创建过程
当一段JavaScript代码执行的时候,JavaScript解释器会创建执行上下文,包含了两个阶段:
- 创建阶段(函数被调用,但在开始执行函数内代码之前)
- 创建scope chain:复制函数属性
[[scope]]
到scope chain
,在变量对象创建完后,将其添加到scope chain
的前端,形成完整的作用域链 - 创建VO/AO
- 根据函数的参数,创建并初始化arguments object
- 扫描函数内部代码,查找函数声明
- 对于所有找到的函数声明,将函数名和函数引用存入到VO/AO
- 如果VO/AO中已有同名的函数,那么就进行覆盖
- 扫描函数内部代码,查找变量声明
- 对于所有找到的var变量声明,都存入到VO/AO中,并初始化为
undefined
- let,const变量声明,初始化为
uninitialed
- 如果变量名称和已经声明的形式参数或函数相同,那么变量声明将不起作用,保留后者,也就是说后者不会受前者声明的干扰
- 对于所有找到的var变量声明,都存入到VO/AO中,并初始化为
- 设置this的值
- 创建scope chain:复制函数属性
- 激活/代码执行阶段
- 设置变量的值、函数的引用,解释/执行代码
三、结合执行上下文,解释变量及函数的访问规则--声明提升
js代码开始执行时,浏览器便会创建全局执行上下文(详见EC的创建过程),每有一次函数调用,则创建一次函数执行上下文。
进行变量、函数及this
值的访问时,都会在当前执行上下文中的作用域链进行访问。
声明提升:
console.log(var1);//undefined
var var1 = 1;
console.log(var1);//1
console.log(fn);
function fn(){
console.log('1111');
}
解析:访问时,都是从执行上下文中的作用域链进行取值,创建执行上下文时,var声明的变量被初始化为undefined
,因此在变量被赋值前进行访问,打印了undefined
,赋值后访问,打印1
;
函数的声明提升同理,只不过创建执行上下文时,函数的标识符初始化为函数体的引用,故打印的为函数体。
let,const的暂时性死区:声明前不能进行访问
console.log(a);
let a = 1;
let b;
console.log(b)
解析:创建执行上下文时,被初始化为uninitialed
,访问uninitialed
值会抛出错误,而赋值后,则可以访问let,const声明的变量。(代码运行到let声明语句时,若没有进行赋值操作,则默认赋值为undefined
,const声明变量时必须初始化)