一、了解JS
我们知道JS属于脚本语言,是一种解释型语言,可以一边转换一边执行,不会生成可执行文件,支持跨平台执行,而它能这样做主要是归功于解释器这个中间层。
这里指的跨平台是源代码可以跨平台,不是解释器跨平台。
官方针对不同的平台开发不同的解释器,使得它们遵从相同的函数,相同的语法,有着相同功能。
在chrome浏览器中的V8引擎就充当着解释器这样一个身份,将JS源代码解释执行。
二、执行上下文
执行上下文简单的来说就是JS代码运行的环境,全局执行上下文就是全局环境,函数执行上下文就是函数内的环境了。
代码要运行首先要进入到全局执行上下文。
执行上下文分为:
全局执行上下文:全局上下文只有一个,不在函数中的代码都位于全局执行上下文中。
函数执行上下文:每个函数都有自己的函数执行上下文,但只有函数调用时才会创建函数执行上下文,函数执行上下文数量是没有限制的。
eval执行上下文:基本很少用上。
执行上下文的阶段
执行上下文分为创建、执行、回收三个阶段,创建阶段分为三步:
-
变量对象(VO):用于变量提升,函数声明,这一步也可以称为预编译,在全局环境下VO 也可看作预编译创建的GO。
-
作用域链:作用域在创建变量对象之后创建,作用域用来解析变量,当变量在自己的作用域内无法找到,就会从它的父级作用域下寻找,依次下去,直到找到或到了最外层作用域。
-
this:确定this的指向,在全局下this指向window,当然,this的值在执行时才会确定。
执行上下文的执行阶段:给变量赋值,函数调用,执行代码。
执行上下文回收阶段:将执行上下文弹出栈,等待回收。
三、执行堆栈
可以把他理解成一种栈结构,遵循先进后出原则,创建的执行上下文要执行就要将其放入执行栈中。
执行栈的运行过程
-
浏览器最开始执行代码创建全局执行上下文,将其压入执行栈的底部。
-
当遇到函数调用时,创建函数执行上下文,将其压入栈,此时位于全局执行上下文的上方,当在函数执行中又遇到函数调用,继续创建新的函数执行上下文,将其压入栈,此时它位于上一个函数上下文上方。当函数执行结束,将函数执行上下文弹出执行栈,等待回收。
-
浏览器只会调用位于栈顶的执行上下文。
-
全局执行上下文在浏览器关闭时弹出。
代码演示
我们通过一段简单的代码来演示这一过程
var a= 3
var b = 3
function output1() {
console.log(a)
output2()
}
function output2() {
console.log(b);
}
output1()
复制代码
-
浏览器创建全局执行上下文,将其压入最底层
-
函数声明不会创建函数执行上下文,直到
output1
函数调用,创建同名函数执行上下文,将它压入栈 -
在
output1
函数调用过程中,遇到了output2
的调用,创建output2
函数执行上下文,继续压入栈。 -
output2
执行完毕,将其弹出栈,将控制权交给它下方的上下文,即output1
函数执行上下文。 -
output1
也执行完毕,同样也弹出执行栈,控制权交给全局执行上下文,直到全局也最后弹出。
四、变量提升/预编译
预编译发生在函数执行之前,会对代码进行一次扫描,找到函数声明以及变量声明。
变量提升发生在执行上下文的创建阶段,对变量,函数声明进行提升。
就我个人理解来说,其实预编译和变量提升的区别不是很大,应该说是预编译就是变量提升的另一种叫法。
预编译过程:
预编译发生在全局 三部曲
- 创建
GO
对象 (在全局执行上下文中VO = GO) - 找变量声明,将变量声明作为
GO
对象的属性名,值赋予undefined
- 找全局里的函数声明,将函数名作为
GO
对象的属性名,值赋予函数体
预编译发生在函数内 四部曲
- 创建一个
AO
对象(activation object) - 找形参和变量声明,将形参和变量声明作为
AO
对象的属性名,值为undefined
- 将实参和形参统一
- 在函数体里找函数声明,将函数名作为
AO
对象的属性名,值赋予函数体
预编译规则:
-
变量声明提升:对于用var定义的变量,在预编译阶段会对变量进行提升,将变量提升到所在作用域的顶部,并初始化为undefined。
-
函数声明提升:对于函数声明来说,同样会受到提升,但是函数声明提升的是整个函数,会创建一个函数变量,并把函数中的所有东西都保存在里面,但不会执行。
-
函数声明优先级更高,当函数声明和变量声明同时提升时,函数声明会覆盖掉同名变量声明,但变量声明可以重新赋值。
-
例如
a = 2
这样的赋值语句不属于声明,在执行阶段运行,创建的变量属于全局变量,作用域是全局。
代码演示
var a = 1
function fn(a) {
var a = 2
function a() {}
var b = a
console.log(a);
}
fn(3) //2
复制代码
-
创建
GO
对象,对变量a
进行变量提升,初始化为undefined
,接着对fn
函数声明提升,提升整个fn
函数,但先不执行,接着进入代码执行,a
被赋值为1,下一步调用函数fn
。 -
遇到
fn
函数调用,创建Ao
对象,将形参及变量a
提升,初始化为undefined
,变量b提升,初始化为undefined。 -
将形参和实参值统一为3,即a为3,最后将函数a提升,由于函数优先原则,a此时被初始化为函数a,进入代码执行阶段,a被赋值为2,b被赋值为a,即2,最后输出a为2。
GO、AO对象的创建结果
go:{
a: undefined 1
fn: function
}
ao: {
a:undefined 3 function 2
b: undefined 2
}
复制代码
五、分析代码运行步骤
我们通过下面这段代码分析代码是怎样一步步运行
var a = 1
function fo(a) {
var b = 2
console.log(a); //输出2
function fo1() {
console.log(b); // 输出2
}
return fo1 // 产生闭包
}
var c = fo(2)
c()
复制代码
- 创建全局执行上下文,将其压入执行栈最底部,上下文创建阶段–创建变量对象/预编译阶段(变量声明、函数声明提升)、创建作用域链、this绑定。
- 创建
go
对象,将a
,c
提升并初始化为undefined
,fo
函数声明提升整个函数体,将它们都保存在go
对象中,this指向window。
- 全局执行上下文进入执行阶段。
- 在
go
对象中将a赋值为1,将c
赋值为fo(2)
的返回值,此时遇到函数调用,创建函数执行上下文。
- 创建
fo
函数执行上下文,将它压入执行栈,此时全局执行上下文在它下方,进入函数执行上下文创建阶段,依然进行同样的三步。
- 创建
ao
对象,将a,b
初始化为undefined
,之后将形参和实参统一,即a
赋值为2,最后进行函数声明提升,将它们都保存在ao
对象中。
fo
函数执行上下文进入执行阶段。
- 将
b
赋值为2,输出此时的a
的值,由于创建阶段进行了预编译,此时a
的值为2,于是输出2,遇到return
需要返回,将fo1
函数里面的所有内容保存在fo1
中返回,此时函数fo
调用结束,通常情况会下,将fo
执行上下文弹出执行栈,但是由于return
的是一个函数,产生闭包机制,使得作用域未被删除,里面的变量仍可以使用。
-
fo
函数调用结束,返回值是一个函数fo1
,将它赋值给c
,于是在全局上下文中,它不在是fo1
,它被叫做c
,接下来,调用c
。 -
遇到函数调用,创建
c
函数执行上下文,压入执行栈,上下文创建阶段–创建变量对象、创建作用域链、this绑定。
- 创建
ao
对象,只有一句输出语句,所以没有变量、函数提升。
fo1
执行阶段,由于在外部调用fo
的内部函数,闭包机制导致fo
作用域不释放,所以输出b时,向父级作用域查找,找到此时b
为2,输出2。
- 调用
fo1
结束,弹出函数执行栈,最后代码执行完毕,将执行栈清空。
创建的变量对象:
go: {
a: undefined 1
fo: function
c: undefined fo1
}
ao: {
b: undefined 2
a: undefined 2
fo1: function
}
ao: {
}
复制代码
六、总结
本文介绍了js、执行上下文内容、执行栈、变量提升规则以及从执行上下文方面分析代码
,由于本人也是只是位初学者,将自己的看法进行总结记录,还是希望各位大佬能多多斧正,喜欢的可以点个小小的?,感谢??。