前言
作为js必须搞懂的基本原理之一,作用域链常常以其抽象的概念,让人头大,接下来,我将以图文结合的方式,为大家详解js的作用域究竟为何物 !
1.作用域
要理解作用域链,首先要理解,何为作用域,我们都知道,变量是有范围的,定义在全局的变量叫全局变量,作用域是全局,定义在函数体内部或者在 {} 内的块级代码中的变量,则可称为局部变量,作用域是它所在的花括号,例如:
var a = 100
function b(){
var c =200
console.log(c)
}
b()
a的作用域是全局,c的作用域是函数b内
2.执行期上下文对象
编译器是自上而下逐行执行,先编译全局,遇到函数执行时,再编译函数体
编译器在编译的时候,每处在一个作用域,便会创建一个AO(activation object)对象(又称为执行期上下文的内部对象),然后将遇到的变量当作AO对象的属性,插入AO对象中,所以上述变量a,也是 "AO.a",在全局时时,还会创建一个GO(global object)对象,若编译器执行到了函数b,遇到了函数b作用域,便创建bAO对象,在b执行完后,bAO对象会被回收销毁,下次调用b时再重新创建
其实编译器在遇到函数的时候,会创建一个 [[scope]] 属性,它即是该函数作用域属性,但是该属性不可访问,所以无法用console.log打印,但是通过xxx.prototype函数查询函数原型,可以看到该属性,例如查询下列函数a的原型
var a=100
function b(){
var b =222
}
b()
b.prototype //查询函数原型
执行结果如下图
图中的[[scopes]] 是函数b的作用域内的属性名的集合,因为函数b是在全局变量下的,它的作用域就是全局作用域,所以AO = GO
那么问题来了,如果函数b不在全局,而是在其他函数体内呢? 例如:
function a(){
var floorA =100
function c(){
var floorC =300
function d(){
var floorD =400
function b(){
var floorB =200
console.log(floorB); //打印200
console.log(floorD); //打印400
} b()
console.log(floorD); //打印400
} d()
} c()
console.log(floorB); //报错
} a()
上述作用域范围 a > c > d > b,函数b在最内层,当想要在函数 a 内,函数c外打印floorB时,却发生了报错,这是为什么呢,其实这就是作用域链原理的问题了,上述代码编译时,执行逻辑如下
按理说,输出结果就应该是 200 400 400 200 但实际上它却会报错,且b函数还打印了不在其内定义的floorD,这一切都是作用域链在作祟
上面提到,编译器每遇到一个作用域,就会在生成一个对饮的[[scope]]属性,如函数a有aAO,c有cAO,d有dAO,b有bAO,而这几个执行期上下文是包容关系的,即 bAO >dAO > cAO >aAO 也可以理解为 bAO.dAO.cAO.aAO scope属性是作用域内的属性名的集合,图解如下
3.作用域链
用链式的存储结构将函数的作用域链接起来,就形成了作用域链
函数的作用域既包括它自身,还包括它的上级函数,直到全局
每个函数都有一条scope chain(作用域链),越内嵌的函数,作用域链越长,可知,上级函数的作用域内并不包函其下级函数的作用域,因而,函数a里的 console.log(floorB)无法访问到floorB,故报错,同样的,函数b的作用域链内包含了函数d的作用域,所以它可以访问到floorD,所以函数b里的console.log(floorD)能够打印。
此外,越内嵌的函数,销毁得越早,所以,在函数c还未执行完之前,函数b已经执行完并销毁了。
相信看到这里,你应该已经理解了何为作用域链了,如有疑惑,欢迎在评论区探讨!!!