什么是执行上下文?
js在执行语句前,经过了一系列的“准备”,为代码执行创造了一个“教室“————执行上下文
执行上下文是 JavaScript 代码执行时的环境。你可以把它想象成一个“教室”,在这个教室里,JavaScript 引擎为代码执行做好了所有准备工作。执行上下文包含了代码执行所需的所有信息,比如变量、函数、作用域链等。
js在哪里查找执行上下文呢?
JavaScript 通过 执行栈(Call Stack)来管理执行上下文。执行栈是一个先进后出的数据结构,当前执行上下文总是位于栈顶。
-
执行栈栈顶的执行上下文称为当前执行上下文
-
js代码总是在当前执行上下文中运行
- 意思是js代码运行时所用到的资源总是在当前执行上下文中查找
什么情况下会创建新的执行上下文?
在 JavaScript 中,有四种情况会创建新的执行上下文:
- 全局代码执行:创建全局执行上下文。
- 函数调用:创建函数执行上下文。
- eval 函数调用:创建 eval 执行上下文(不推荐使用)。
- 模块代码执行:创建模块执行上下文。
下面我们重点分析全局执行上下文和函数执行上下文的创建过程。
全局执行上下文
执行步骤
步骤一:
-
创建全局执行上下文,压入执行栈
全局执行上下文的文本环境由两部分组成: 全局对象(window)和 全局scope (script)
步骤二:分析
- 找到所有非函数中的var声明
- 找到顶级的函数声明
- 找到顶级的let,const,class声明
- 找到块级中的函数声明({}),函数名不与上述的变量名字重复
步骤三:
-
变量名重复处理
1.let const class 声明的名字不能重复,自身重复也不行
2.let const class 和var function的名字不能重复
3.var和function名字重复,function声明的函数优先
步骤四:创建绑定
- 登记并初始化var为undefined
- 顶级函数声明:登记functiion名字,并初始化为新创建的函数对象
- 块级中的函数声明:登记名字,初始化为undefined
- 登记let,const,class,但未初始化
1.var和function声明创建在全局对象中,而let,const,class声明的变量在全局scope中
2.先到全局scope中查找,查不到再去全局对象中查找
步骤四:
- 执行语句
进入function函数体代码
- 绑定函数对象时,函数对象体内会保存函数创建时的执行上下文的文本
- 函数执行上下文的文本环境只有一个函数scope,与全局执行上下文不同
- 函数执行上下文的文本环境会链接到体内保存的文本环境
所以这段代码结果会报错,因为代码运行时,在当前执行上下文中查找变量a,找到了a,但a没有初始化
从这里可以看出,其实let,const声明的变量都有提升,只是没有初始化,不能使用,也被称为暂时性死区(Temporal dead zone)
这里聊一下作用域的概念
-
作用域就是解析(查找)变量名的一个集合,就是当前执行上下文(也可以是当前上下文的词法环境(Lexical Environment))
- 全局作用域就是全局执行上下文
- 函数作用域就是函数执行上下文
-
变量不会单独存在,属于一个作用域(编译阶段)
- 作用域相当于变量的查找规则,首先在当前作用域查找,如果没有找到,就向上一级作用域查找,一直冒泡,直到全局作用域,还没找到——未定义(执行阶段)
另外还有十分重要的点:
- 函数调用时的执行上下文看”身世“—函数在哪里创建,就保存那里的运行上下文
- 函数的作用域是在函数创建的时候决定的,而不是调用的时候决定的
结果输出 2
- 并非根据调用嵌套(运行上下文)形成作用域链,而是根据函数创建嵌套形成作用域链,也就是函数的书写位置,因此称为词法作用域
执行代码块时
不会创建新的执行上下文,只会创建新的记录环境
步骤一:
- 创建新的记录环境,链接在原来记录之前
步骤二:
- 找到块中let,const声明
- 块中所有的顶级函数声明
步骤三:
- 检查名字重复
步骤四:创建绑定
- 在新的记录环境中绑定
步骤五:
- 执行语句
块内语句执行完之后,js引擎会销毁新创建的记录环境,并链接回原来的文本环境
所以第一次打印 in if statement ,第二次打印out if statement
在之前提到的全局执行上下文中,记录块中的函数声明有些细节
顶级函数声明和块中的函数声明在记录到文本环境时的差异:
1.顶级函数声明会被初始化为函数对象
2.块中的函数声明若与之前记录的变量名存在重复,就对块中的函数声明不做任何处理
3.块中声明的函数声明没有重名,在全局对象中创建一个以函数名为名的变量,并初始化为undefined
在运行代码块时,如之前所说会创建新的记录环境,并且进行变量记录:
在代码块运行结束之后,与之前不一样的是,除了链接回原来的文本环境,if块作用域不会被销毁,因为foo函数内部保存了对这块的引用。除此之外,在执行到foo函数声明时还会将块作用域中foo函数对象赋值给全局对象中的同名变量,如果不存在同名,则不做操作。
在运行foo函数时,创建foo函数执行上下文:
总结一下就是
代码块中的函数声明也有提升,并且初始值为undefined,它真正被实例化为函数对象是在执行代码块时,创建了新的块级作用域中完成的。
小练习:
console.log(foo)//undefined 并不会报错
if(false) {
function foo() {}
}
2.
console.log(foo) //undefined
if(false) {
function foo() {
console.log("foo function")
}
}
foo() // Uncaught TypeError: foo is not a function
因为判断为假,不会执行if代码块,而foo函数对象的创建又是执行代码块时完成的 3.
console.log(foo) //undefined
if(true) {
function foo() {
console.log("foo function")
}
}
foo() // foo function
4.
var foo
if(true) {
function foo() {
console.log("foo function")
}
}
foo() //foo function
5.
let foo
if(true) {
function foo() {
console.log("foo function")
}
}
foo() // Uncaught TypeError: foo is not a function
循环中的执行上下文
var liList =[]
for(var i=0;i<5;i++){
liList[i] = function(){
console.log(i)
}
}
liList[0]()
liList[1]()
liList[2]()
liList[3]()
liList[4]()
结果是输出五个5
第一次进入循环时
然后给liList[0]赋值,这里不是函数声明,而是赋值操作,所以不会将此函数对象保存在块级作用域中。同时,之前反复强调的一点,函数创建时会保存当前执行上下文的文本环境。
当循环结束之后:
每个函数执行时,都是到全局对象中找到的i,因此全部输出5
var liList =[]
for(let i=0;i<5;i++){
liList[i] = function(){
console.log(i)
}
}
liList[0]()
liList[1]()
liList[2]()
liList[3]()
liList[4]()
结果正常输出0,1,2,3,4
如果是用let声明的i,在执行到for的()时,会创建一个新的记录环境。
之后每次i的迭代都会创建一个副本
函数执行时:
总结下来就是for中 () 内定义的循环变量的作用域由关键字决定:var 会在函数或全局范围内,let 和 const 只在循环内有效。