还是先说说文章的由来。
今年其实立了挺多flag的,有源码阅读,有ui框架,有脚手架,有前端监控,还有react源码。完成的怎么样,暂且不提(年底一块总结),至少可以把前端的知识点梳理一遍。
所以这篇文章,来盘一盘js的上下文,彻底理解js一些奇怪的特性。
什么是上下文?
在执行一段js代码之前,会先进行编译,编译的过程中会生成上下文。
首先会生成一个全局上下文。
上下文中包含两块内容,分别是变量环境跟词法环境。

如果代码中有函数,则会生成函数上下文。
保存上下文的是一个栈结构,也就是后进先出。

为什么会变量提升?
在编译阶段,会将变量的声明存在变量环境中。
css
console.log(a)
console.log(b)
var a = 1
function b() {
var c = 2
}
b()
比如这样一段代码,编译阶段上下文情况如下。

然后开始执行代码。
1.输出a为undefined,b为function。
2.将a赋值为1
3.执行函数b,生成b函数的上下文。
此时的上下文情况。

4.将c赋值为2
等函数执行完毕,则会销毁函数上下文。
但是,如果函数中又调用了其他函数,那么就会创建新的函数上下文,压入栈中,也叫调用栈。
栈的大小是有限制的,如果说函数嵌套(一般是递归),有可能造成栈溢出。
变量提升,有什么问题?
变量提升其实不太符合常规逻辑。
而且会带来一些问题。
比如变量容易被覆盖。
css
console.log(a)
console.log(b)
var a = 1
function b() {
console.log(a)
var a = 2
}
b()
还有一个常见的坑,就是本应销毁的变量没有被销毁。
css
function b() {
for(var a = 0 ;a< 7; a++){}
console.log(a)
}
b()
所以在es6中,引入了let, const来实现块级作用域。(现在基本取代了var)
如何实现块级作用域?
在编译阶段,会将块级变量放到词法环境中。
比如下面这段代码
ini
let a = 1
var b = 2
if(true) {
let c = 3
let d = 4
var e = 5
}
此时的上下文如下所示。

并且当我访问变量的时候,查找的顺序,是先从词法环境开始找,再到变量环境。
ps(块级变量的创建会被提升,但是初始化和赋值不会被提升,所以如果在变量前打印是会报错的)
什么是作用域链?
在块级作用域这里说了,变量的查找顺序。
但是,当在自己的作用域范围内找到不到,该怎么办呢?
在上下文中,还有一个属性叫outer,指向需要查找的上下文,也叫做作用域链。
ps(作用域链是在代码中就确定了的,无需在编译中计算)
还是先看一段代码。
javascript
function a() {
console.log(aa)
}
function b() {
var aa = "2"
a()
}
var aa = "1"
b()

也就是说,代码写在哪里,outer就指向该上下文。
理解了作用域链,再来看闭包就简单多了。
如何理解闭包?
说闭包之前,先说两句话。
1.函数执行完毕,会销毁函数上下文,变量会被回收
2.内部函数总是可以访问其外部函数中声明的变量(通过作用越链)
这其实就是闭包形成的原因,外面的函数需要销毁,而函数里面的变量被占用,所以就形成了一个内部函数专属的内存包(只有它可以访问),也叫闭包。
javascript
function a() {
let a1 = "a1"
return function b() {
console.log(a1)
}
}
const b = a()
b()
当执行到b = a()的时候,此时的上下文。

因为a函数已经执行完毕,但是变量a1被b函数占用,此时的上下文如下。

ps(闭包的变量回收,果是全局,则等页面销毁,如果是局部,则等函数执行完毕)
到这里,今天的文章就结束了。
总结一下,想要彻底理解变量提升,闭包,作用域链还是需要从上下文入手。
彻底理解了上下文之后,一些不符合直觉的代码,或者特性就能对号入座了。
如果这篇文章对你有所收获,欢迎点赞评论。