1. 作用域(Scope)
储存变量的规则叫作用域,用来限定代码的使用范围。
JS采用的是词法作用域(静态作用域),即在定义时就已确定,关注点在代码在何处声明。与之相反的是动态作用域,即在调用时确定,关注点在代码在何处调用。这样就理解为什么下面代码在词法作用域情况下输出3,在动态作用域下输出4了。
var a = 3
function f(){
console.log(a)
}
function g(){
var a = 4
f()
}
g()在ES6之前,JS只有全局作用域和函数作用域,ES6的带来了块级作用域。
(1)全局作用域
全局作用域可以理解为在页面的任何地方都能被访问到,页面不关闭,就一直不会释放,一直占用内存。
(2)函数作用域
函数作用域即为局部作用域,只能在函数内部被访问。
(3)块级作用域
ES6的新特性之一,花括号的内部就是块级作用域,定义在块级作用域中的变量在块外部不能被访问,拥有和函数作用域相同的行为。
2. 作用域链
了解作用域链,先了解JS运行前的词法分析,词法分析分三个步骤:分析参数 => 分析变量声明 => 分析函数声明。
代码片段一:
function f(a){
console.log(a)
var a = 3
console.log(a)
function a(){}
console.log(a)
}
f(4)函数在执行的过程中会生成活动对象(Active Object),简称AO。上面代码的词法分析如下:
分析参数:
(1)分析形参:AO.a = undefined (函数接受形参,并作为AO的属性,值为undefined)
(2)分析实参: AO.a = 4 (函数接受形参,添加到AO.a,覆盖之前的undefined)
分析变量声明:
AO中已经存在属性a,不做修改。(如果在上一步中没有属性a,则为AO.a赋值为undefined,如果有则不做任何修改)
分析函数声明:
AO.a = function a(){} (AO上如果有与函数名同名的属性,属性名会被函数覆盖)
函数f执行的时候,在第一个console.log(a)之前,AO.a = function a(){},所以结果为 function a(){}。在第二个console.log(a)之前,a重新被赋值为3,所以结果为3。在第三个console.log(a)之前a没有被修改,所以结果为3。如下图所示:
代码片段二:
function g(b){
console.log(b)
var b = function (){
console.log(b)
}
b()
}
g(6)分析g()
分析参数:
(1)形参分析:AO.b = undefined
(2)实参分析: AO.b = 6
分析变量声明:AO中已经有b属性,并且有值,不做任何修改。
分析函数声明:无函数声明。
分析阶段最终结果是:AO = {b : 6 }
分析b()
分析参数:无参数。
分析变量声明:无变量声明。
分析函数声明:无函数声明。
分析阶段最终结果是:AO = {}
当b()的AO为空时,会向父级调用,父级的AO对象是g,g.AO.b = function (){ console.log(b) },所以会输出 function (){ console.log(b),(同一个变量,前面的赋值会被后面的赋值覆盖,所以输出的不是6)。函数整体输出结果如下图所示:
从代码片段二的分析过程可以看出。JS的每一个函数执行的时候,会先在自己创建的AO上找对应的属性值,如果找不到,则往父函数的AO上找,如果依然找不到,就继续向上层父级的AO中找,直到找到全局作用域windows,这条寻找AO的链就是JS中的作用域链。