JavaScript:作用域与作用域链
1.什么是作用域(scope)?
简单来讲,作用域(scope)就是变量访问规则的有效范围。
- 作用域外,无法引用作用域内的变量;
- 离开作用域后,作用域的变量的内存空间会被清除,比如执行完函数或者关闭浏览器
- 作用域与执行上下文是完全不同的两个概念。我曾经也混淆过他们,但是一定要仔细区分。
JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。
函数作用域是在函数声明的时候就已经确定了,而函数执行上下文是在函数调用时创建的。假如一个函数被调用多次,那么它就会创建多个函数执行上下文,但是函数作用域显然不会跟着函数被调用的次数而发生什么变化。
1.1 全局作用域
var foo = 'foo'; console.log(window.foo); // => 'foo'
在浏览器环境中声明变量,该变量会默认成为window对象下的属性。
function foo() { name = "bar" } foo(); console.log(window.name) // bar
在函数中,如果不加 var 声明一个变量,那么这个变量会默认被声明为全局变量,如果是严格模式,则会报错。
全局变量会造成命名污染,如果在多处对同一个全局变量进行操作,那么久会覆盖全局变量的定义。同时全局变量数量过多,非常不方便管理。
这也是为什么jquery要在全局建立变量 ,其余私有方法属性挂在,其余私有方法属性挂在 下的原因。
1.2 函数作用域
假如在函数中定义一个局部变量,那么该变量只可以在该函数作用域中被访问。
function doSomething () { var thing = '吃早餐'; } console.log(thing); // Uncaught ReferenceError: thing is not defined
嵌套函数作用域:
function outer () { var thing = '吃早餐'; function inner () { console.log(thing); } inner(); } outer(); // 吃早餐
在外层函数中,嵌套一个内层函数,那么这个内层函数可以向上访问到外层函数中的变量。
既然内层函数可以访问到外层函数的变量,那如果把内层函数return出来会怎样?
function outer () { var thing = '吃早餐'; function inner () { console.log(thing); } return inner; } var foo = outer(); foo(); // 吃早餐
函数执行完后,函数作用域的变量就会被垃圾回收。而这段代码看出当返回了一个访问了外部函数变量的内部函数,最后外部函数的变量得以保存。
这种当变量存在的函数已经执行结束,但扔可以再次被访问到的方式就是“闭包”。后期会继续对闭包进行梳理。
1.3 块级作用域
很多书上都有一句话,javascript没有块级作用域的概念。所谓块级作用域,就是{}包裹的区域。但是在ES6出来以后,这句话并不那么正确了。因为可以用 let 或者 const 声明一个块级作用域的变量或常量。
比如:
for (let i = 0; i < 10; i++) { // ... } console.log(i); // Uncaught ReferenceError: i is not defined
发现这个例子就会和函数作用域中的第一个例子一样的错误提示。因为变量i只可以在 for循环的{ }块级作用域中被访问了。
扩散思考:
究竟什么时候该用let?什么时候该用const?
默认使用 const,只有当确实需要改变变量的值的时候才使用let。因为大部分的变量的值在初始化之后不应再改变,而预料之外的变量的修改是很多bug的源头。
1.4 词法作用域
词法作用域,也可以叫做静态作用域。意思是无论函数在哪里调用,词法作用域都只在由函数被声明时所处的位置决定。
既然有静态作用域,那么也有动态作用域。
而动态作用域的作用域则是由函数被调用时执行的位置所决定。
var a = 123; function fn1 () { console.log(a); } function fn2 () { var a = 456; fn1(); } fn2(); // 123
以上代码,最后输出结果 a 的值,来自于 fn1 声明时所在位置访问到的 a 值 123。
所以JS的作用域是静态作用域,也叫词法作用域。
上面的1.1-1.3可以看做作用域的类型。而这一小节,其实跟上面三小节还是有差别的,并不属于作用域的类型,只是关于作用域的一个补充说明吧。
2. 什么是作用域链(scope chain)
在JS引擎中,通过标识符查找标识符的值,会从当前作用域向上查找,直到作用域找到第一个匹配的标识符位置。就是JS的作用域链。
var a = 1; function fn1 () { var a = 2; function fn2 () { var a = 3; console.log(a); } fn2 (); } fn1(); // 3
console.log(a) 语句中,JS在查找 a变量标识符的值的时候,会从 fn2 内部向外部函数查找变量声明,它发现fn2内部就已经有了a变量,那么它就不会继续查找了。那么最终结果也就会打印3了。
代码分析如下:
<script type="text/javascript"> var a = 100; function fun(){ var b = 200 function fun2(){ var c = 300 } function fun3(){ var d = 400 } fun2() fun3() } fun() </script>
首先预编译,一开始生成一个GO{
a:underfined
fun:function fun(){//fun的函数体
var b = 200
function fun2(){
var c = 300
}
function fun3(){
var d = 400
}
fun2()
fun3()
}
}
逐行执行代码,GO{
a:100
fun:function fun(){//fun的函数体
var b = 200
function fun2(){
var c = 300
}
function fun3(){
var d = 400
}
fun2()
fun3()
}
}
当fun函数执行时,首先预编译会产生一个AO{
b:underfined
fun2:function fun2(){
var c = 300
}
fun3:function fun3(){
var d = 400
}
}
这里注意的是fun函数是在全局的环境下产生的,所以自己身上挂载这一个GO,由于作用域链是栈式结构,先产生的先进去,最后出来,
在这个例子的情况下,AO是后于GO产生的,所以对于fun函数本身来说,执行代码的时候,会先去自己本身的AO里找找看,如果没有找到要用的东西,就去父级查找,此题的父级是GO
此刻fun的作用域链是 第0位 fun的AO{}
第1位 GO{}
fun函数开始逐行执行AO{
b:200
fun2:function fun2(){
var c = 300
}
fun3:function fun3(){
var d = 400
}
}
注意:函数每次调用才会产生AO,每次产生的AO还都是不一样的
然后遇到fun2函数的执行,预编译产生自己的AO{
c:underfined
}
此刻fun2的作用域链是第0位 fun2的AO{}
第1位 fun的AO{}
第2位 GO{}
然后遇到fun3函数的执行,预编译产生自己的AO{
d:underfined
}
此刻fun3的作用域链是第0位 fun3的AO{}
第1位 fun的AO{}
第2位 GO{}
fun2和fun3的作用域链没有什么联系。
当函数fun2和fun3执行完毕,自己将砍掉自己和自己的AO的联系,
最后就是fun函数执行完毕,它也是砍掉自己和自己AO的联系。
这就是一个我们平时看到不是闭包的函数。
闭包
1.闭包在红宝书中的解释就是:有权访问另一个函数作用域中的变量的函数。
2.写法:
1 <script type="text/javascript"> 2 function fun1(){ 3 var a = 100; 4 function fun2(){ 5 a++; 6 console.log(a); 7 } 8 return fun2; 9 } 10 11 var fun = fun1(); 12 fun() 13 fun() 14 </script>
3.效果如下:
4.分析:
执行代码
GO{
fun:underfined
fun1:function fun1()
{
var a = 100;
function fun2()
{
a++;
console.log(a);
}
return fun2;
}
}
然后第十一行开始这里,就是fun1函数执行,然后把fun1的return返回值赋给fun,这里比较复杂,我们分开来看,
这里fun1函数执行,产生AO{
a:100
fun2:function fun2(){
a++;
console.log(a);
}
}
此刻fun1的作用域链为 第0位 AO
第1位 GO
此刻fun2的作用域链为 第0位 fun1的AO
第1位 GO
解释一下,fun2只是声明了,并没有产生调用,所以没有产生自己的AO,
正常的,我们到第7行代码我们就结束了,但是这个时候来了一个return fun2,把fun2这个函数体抛给了全局变量fun,好了,fun1函数执行完毕,消除自己的AO,
此刻fun2的作用域链为 第0位 fun1的AO
第1位 GO
第十二行就是fun执行,然后,它本身是没有a的,但是它可以用fun1的AO,然后加,然后打印,
因为fun中的fun1的AO本来是应该在fun1销毁时,去掉,但是被抛给fun,所以现在fun1的AO没办法销毁,所以现在a变量相当于一个只能被fun访问的全局变量。
所以第十三行再调用一次fun函数,a被打印的值为102。