执行上下文,也叫做代码运行时环境。
理解了它,能让你对js代码的执行逻辑有更清晰的认识。不用在死记硬背面试题,妈妈再也不用担心我的面试了。
什么是执行上下文
简单地说,任何 JavaScript 代码片段在执行前都要进行编译(通常就在执行前)。因此,JavaScript 编译器首先会对 var a
= 2; 这段程序进行编译,然后做好执行它的准备,并且通常马上就会执行它。
这些准备内容就是执行上下文。
执行上下文的生命周期有
- 创建阶段
- 执行阶段
- 销毁阶段
我们主要分析创建阶段,看下编译器在执行上下文的创建阶段都做了哪些工作。
全局执行上下文
先看一个简单的例子
console.log(a)
var a = 10
想必大部分同学都知道结果。为什么会这样呢?
js在当前的执行上下文的准备阶段,会先进行变量声明(变量提升),这导致在代码的任何阶段你都可以直接使用已经声明的变量,而不用担心报错。
再看一个例子
//函数在准备阶段已经声明
test()
function test(){
console.log(123)
}
//this在准备阶段已经赋值
console.log(this)
这段代码同样不会报错。因为函数声明也是在执行上下文的准备阶段就已经声明了。
同样在声明阶段也完成了对this
的赋值
我们来总结一下,准备阶段都干了什么
- 变量的声明
- 函数的声明
this
的赋值
注意这里只是全局执行上下文的准备工作,函数执行上下文要比这个复杂一些。
提升优先级
变量声明和函数声明都会被提升,那谁的优先级更高呢?
function test(){
console.log("hello")
}
var test
test()
如上图,说明函数的要优先提升。这里需要强调一下,优先提升的含义是,当出现冲突的时候函数声明会覆盖变量声明,而非声明顺序在前。
其实规则要复杂一些,我们再看另一种情况
function test(){
console.log("a")
}
function test(){
console.log("b")
}
var test
test()
如上图,函数重复声明的情况下,新声明的会覆盖老的。
实际上编译器的提升规则是:重复的var声明会忽略,函数优先提升,后面的函数声明会覆盖前面的。
隐式声明
前面说的变量提升,函数提升都是执行上下文的创建阶段完成的。实际上在执行阶段也会发生变量声明,这显然不是最佳实践,因为这回让你的代码出现奇怪的问题。
LHS 和 RHS
在了解隐式声明之前我们要先学习两个概念。LHS和RHS,可以理解成变量的使用方式,赋值还是被赋值。
var a = 1 // a= 使用LHS
var b = a // b= 使用LHS =a 使用RHS
console.log(b) //console.log( 使用LHS (a) 使用RHS
其实很好理解,当我要使用某个变量时需要从作用域链找到该变量,这便是查询。
当我要给这个变量进行赋值操作时我们认为这是一次LHS查询,当我们只是取值时,认为是一次RHS查询。
函数调用可以认为是先去查找函数引用,再使用,所以认为是LHS查询
理解这个概念有什么用呢?
非严格模式下,js引擎执行时,如果从作用域链查询不到某个变量,LHS和RHS下的表现方式不同。
- LHS:会隐式声明一个变量
- RHS:会报错 ReferenceError
在LHS下js引擎帮我们声明一个变量并不会影响代码逻辑,但是RHS下会影响。
LHS下的隐式声明
那我们看个例子
function testA(){
b = 1 // b= 是LHS,从作用域链无法找到变量b,便会在全局作用域隐式声明变量b
console.log(b)
}
function testB(){
console.log(b) // (b) 是RHS,从作用域链可以找到全局作用域下的变量b
}
testA()
testB()
是不是很神奇,可以看代码里的注释理一下思路。
再看下报错的例子
function testA(){
console.log(b) //(b)是RHS,从作用域链无法找到该变量,自然会报错。
}
testA()
函数执行上下文
在函数上下文的创建阶段也会有变量提升,函数提升,this的赋值,但是要把形参考虑进去。
我们可以把形参看作是隐式的变量声明,只不过形参的赋值在创建阶段,而普通变量的赋值在执行阶段。
看个例子
function test(x,y){
var x
console.log(x + y)
}
test(1,2)
有的人会被 var x
迷惑,认为 x
的值是undefined
。
我们拿之前的结论来分析一下,重复的var声明无效,对于变量x
在编译阶段相当于被声明了两次,一次是形参,一次是函数内部变量,所以第二次是无效的
再看一个例子
function test(x,y){
x =10
console.log(x + y)
}
test(1,2)
console.log(x)
这里的x
并未在全局作用域下隐式声明,因为在当前作用域的形参里能找到变量。
执行上下文栈
有了执行上下文,js又是如何组织并回收这些上下文呢?这时候就该执行上下文栈上场了。
全局上下文始终存在,所以刚开始栈底是全局上下文,遇到函数调用时会生成一个新的函数上下文并入栈。
程序始终执行栈顶的上下文,当上下文执行完毕则上下文出栈。
举个例子
var a =10
var b =100
function Test(x){
console.log(x)
}
Test(a)
Test(b)
上下文栈如图,请注意图示的变量并非当前上下文的变量全集。
需要注意的是函数在调用的时候会生成新的上下文,会对变量重新赋值。
This的赋值规则
尽然说到了这里,我们在来探讨下 this
的赋值规则。
function Test(name){
this.name = name
this.sayHello = function(){
console.log("hello I am " + this.name)
}
}
lb = new Test("迪迦奥特曼")
//这时候 this 指向 lb
lb.sayHello()
// 这时候 this指向了 window
tmp = lb.sayHello
tmp()
通过 Object.method
的方式调用函数的时候,this
始终指向Object
,至于上图中的第二中情况,可以理解成window.tmp()
。从这个例子里也可以看出来,函数调用时会生成新的执行上下文。
再看下其他的例子
function Test(name){
this.name = name
this.sayHello = function(){
console.log(this)
}
}
dj = new Test("迪迦奥特曼")
sw = new Test("赛文奥特曼")
dj.sayHello.call(sw)
通过 call
方法和apply
方法可以将 this
指向相应的对象。
总结下来就是
obj.method
的方式调用方法,this
指向obj
call
和apply
的方式调用,this
指向指定对象- 直接调用函数,
this
指向window