关于JS中的执行上下文和作用域链的介绍,本篇是进阶篇,关于变量提升的原理和上下文的创建相关内容
先看两段代码:
function test1() {
console.log(a)
console.log(b)
var b="变量b的值"
var a
console.log(a)
console.log(b)
a=1
console.log(a)
//a()
function a() {
console.log("hello");
};
a=function (){
console.log("world")
}
console.log(a)
a()
}
test1()
function test2(i) {
if(i == 3) {
return;
}
test2(i+1);
console.log(i);
}
test2(0)
先思考一下控制台的打印结果:…
-----------(下面会有详细解析过程)
如果这个结果不在你的意料之中,那么恭喜你,这篇文章很适合你。当然,如果有大佬看到有理解偏差的地方也可以指正。
如果不知道什么是上下文、作用域链、上下文栈的话可以去看上一篇,连接在顶部。
要向弄清原因,我们首先要知道函数上下文创建前后的具体过程
当代码执行流进入对应的代码片段,但在代码执行前,会有个预加载的过程,这个时候会创建对应的上下文环境(这里想表达的意思就是上下文创建过程是在代码执行前完成的)。
创建阶段:
创建上下文一共分为(包含)三部分:
-
创建变量对象(VO)(按照下面顺序创建变量对象;注意:变量对象(VO)是JS引擎上实现的,并不能在JS环境中直接访问)**
- 创建arguments伪数组对象,搜索实参并赋值(函数上下文中的变量对象会创建arguments,在全局上下文中是没有arguments的)
- 扫描函数声明:扫描代码中的函数(用function定义),将对应的函数名作为VO的一个属性,该属性保存了一个指针,该指针指向函数在内存中的位置。扫描时如果有同名的属性,则对应的属性值会被覆盖(覆盖掉原有的)。
- 扫描变量声明(var):扫描代码中的变量,会将变量名作为VO对象的一个属性,不过不同的是属性值会被初始化为undefined。扫描时如果有同名的属性则会直接跳过(不会覆盖了)。
-
创建作用域链
作用域链里面每一级上下文的变量对象,最顶端是正在执行上下文的变量对象,往下依次是上一级上下文的变量对象。
-
确定上下文中this指向
全局上下文中 this指向window对象
函数上下文中 this指向取决于函数调用方式
执行阶段:
此时会为VO变量对象被激活成了AO活动对象,我们所访问的数据都是AO对象上的,随着代码的执行,不断有变量被赋值(原本的变量保存的值是undefined,会随着代码执行被赋予新值)。
执行阶段结束,出栈等待回收
举个例子:
function test(a,b){
console.log(a) //f a()
console.log(b) //6
console.log(num) //undefined
console.log(fn) //undefined
var a=1
console.log(a) //1
var num=2
function a(){
}
var fn=function(){
}
console.log(num) //2
console.log(fn) //f (){}
}
test(5,6)
创建全局上下文(伪代码)
ExecutionContext={
VO:{...},
scopeChain:{...},
this:null
}
代码执行流进入first,创建函数上下文(伪代码):
firstExecutionContext={
//变量对象
VO:{
arguments:{
0:f a(), //注意:属性名相同时,函数会覆盖掉原有的属性;原本的形参变量为a和函数名相同
1:6,
length:2
},
a:f a(),
b:6 //原本的形参
num:undefined,
fn:undefined
},
scopeChain:{VO(first),window},
this:window
}
随着代码执行,第6行a被赋值1,第7行打印出1,第八行num原本的值为undefined,被赋值为2,同理代码执行到第12行,fn被赋值一个匿名函数。
如果上述内容你看懂了的话(如果没看懂那一定是我没讲清楚hhh),对于第一个代码片段应该就有了思路了,其实很简单的。下面先回顾一下第一个代码片段
function test1() {
console.log(a)
console.log(b)
var b="变量b的值"
var a
console.log(a)
console.log(b)
a=1
console.log(a)
//a()
function a() {
console.log("hello");
};
a=function (){
console.log("world")
}
console.log(a)
a()
}
test1()
解析:
代码执行流进入test1函数,创建对应上下文环境:
firstExecutionContext:{
VO:{
arguments:{...}
a:pointer to function a(...console.log("hello")...) //注意函数申明优先于变量申明
b:undifined
},
scopeChain:{...},
this:{...}
}
代码执行:
console.log(a)
//ƒ a() {
console.log("hello");
}
打印的a为保存在变量对象中的属性a,这个a保存了函数在内存中的位置,所以打印出a函数(函数体内部有 console.log(hello)的函数)
console.log(b) //undefined
此时在变量对象中的b并没有赋值,打印undefined
var b="变量b的值"
var a
console.log(a) //同样是 打印函数体内部有 console.log(hello)的函数
console.log(b) //变量b的值
此时为变量b赋值为“变量b的值”,b的值不在是undefined·,打印结果为 “变量b的值”
此处的var a 可以理解成无效(就算删掉了var关键字,打印结果也是一样的),因为代码执行到这以前对于变量a的申明,函数申明优先于变量申明,所以这个地方不会打印undefined 而是打印hello的那一个函数。
a=1
console.log(a) //1
这里为a 赋值为1,打印a,a不再是函数了,而是一个保存数值1的变量
//a() // a is not a function
如果这里把注释解开,真的去执行这行代码一定会报错的,a is not a function
a=function (){
console.log("world")
}
console.log(a) // 打印函数体内部有 console.log(world)的函数
a() //world
在这里,变量a保存了一个函数,(这里相当于对变量a的一个赋值,和上面提到的函数申明是不一样的)
所以打印出一个函数(函数体内部有 console.log(world)的函数),当然此时可以执行a(), 执行函数体内部代码,打印出world
说完了第一个代码片段,下面来说说第二个代码片段:
function test2(i) {
if(i == 3) {
return;
}
test2(i+1);
console.log(i);
}
test2(0)
第二个代码片段就涉及到了上下文栈的相关内容,代码执行流进入test2(0),创建test2(0)函数上下文并将其压如栈顶,此时看函数内部:i的值为0不等于3,所以代码继续执行,代码执行流进入test2(1),创建上下文,并压入栈顶(原本的test2(0)上下文被压入了栈中),i的值为1不等于3,函数继续执行,test2(2)…test2(3),代码执行流进入test2(3)后,创建上下文,压入栈顶,代码执行…函数内部 i等于3满足条件,执行return语句,该上下文被弹出上下文栈。上下文栈控制权交给test2(2),i的值为2,执行打印语句打印2,函数执行完毕出栈,将控制权交给test2(1), 打印1…控制权交给test2(0),打印0,test2(0)被弹出上下文栈被销毁。