网上看到几篇关于作用域及作用域链的文章觉得很好,现整理总结加以自己理解如下。
如同做外文阅读理解,首先要背记基本单词的意思。我们先解释几个基本概念:
- 作用域(scope)
(1)全局作用域(global scope):可以在代码的任何地方被访问。
(2)局部作用域(local scope):一般只在固定的代码片段内可以访问得到。比如,函数中声明的变量,只有在该函数内部可以访问的到。
- 作用域链(scope chain):是函数被创建的作用域中对象的集合。
作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。当代码在一个环境中执行时,会创建变量对象的一个作用域链。全局作用域和局部作用域中变量的访问权限,其实是由作用域链决定的。整个作用域链是由不同执行位置上的变量对象按照规则所构建的一个链表。作用域链本质是一个指向变量对象的指针列表。它只引用,但不包含变量对象。
- 执行环境(Execution Context):定义了变量或函数有权访问的其他数据,决定了他们各自的行为。
每一个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。
执行环境有全局执行环境(全局环境)和局部执行环境之分:
(1)全局执行环境:是最外围的一个执行环境。
web浏览器中,全局执行环境被认为是window对象。因此所有全局变量和函数都是作为window对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也将随之销毁。全局执行环境直到应用程序退出——例如关闭网页或关闭浏览器时,才会被销毁。
(2)局部执行环境
每个函数都有自己的执行环境:当执行流进入一个函数时,函数的环境就会被推入一个执行环境栈的顶部并获取执行权。而在函数执行后,栈将其环境从该栈顶部弹出,把控制权返回给之前的执行环境。
当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后,使用arguments和其他命名参数的值来初始化函数的活动对象(Activation Object)。活动对象在最开始只包含一个变量,就是函数内部的arguments对象。作用域链中的下一个变量对象来自该函数的包含环境,而再下一个变量对象来自再下一个包含环境,就这样一直延续到全局执行环境,全局执行环境的变量对象始终是作用域链中的最后一个对象。
函数的局部环境可以访问函数作用域中的变量和函数,也可以访问其父环境,乃至全局环境中的变量和环境。全局环境只能访问全局环境中定义的变量和函数,不能直接访问局部环境中的任何数据。
总结:
每次进入一个新的执行环境,都会创建一个用于搜索变量和函数的作用域链。
当函数被执行时,就是进入这个函数的执行环境。首先会创建一个它的活动对象(包含了this、参数arguments、局部变量-包括命名的参数)的定义和一个变量对象的作用域链,然后把这个执行环境的作用域按顺序复制到作用域链里,最后把这个活动对象推入到scope chain的顶部。这样,作用域链scope chain就是一个有序的栈,这就保证了对执行环境有权访问的所有变量和对象的有序访问。
下面引用前辈的一张图作说明,网址附于文末引用处。
总之,作用域链的最前端始终是当前正在执行的代码所在环境的的变量对象。若这个环境是函数,则将其活动对象(activation object)作为变量对象。一个函数的作用域属性中对象列表的顺序是上一层函数的activation object,然后是上上层,一直到最外层的全局对象。
内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。每个环境都可以向上搜索作用域链,以便查询变量和函数名。但任何环境不能通过向下搜索作用域链而进入另一个作用环境。
记忆两点:(1)函数的局部环境可以访问函数作用域中的变量,也可以访问和操作父环境(包含环境)乃至全局环境中的变量。(2)父环境只能访问其包含环境和自己环境中的变量和函数,不能访问其子环境中的变量和函数。全局环境只能访问全局环境中的变量和函数,不能访问局部环境中的任何数据。
- 提升(hoisting)
(1)变量提升:即把变量提升到函数的顶部,但只是提升变量的声明而已,不会把变量的值也提升上来。
例如:
由于变量提升,上图代码实际相当于:
(2)函数提升:即把函数提升到前面。但注意只有函数声明形式才能被提升,而函数表达式和Function构造器不可以。
例如:
而函数表达式形式不能被提升:
应改为如下代码才可不抛出错误:
- 最后,终于要做两道题实际感受一下作用域链啦!
(1)
var a = 1;
function fn1(){
function fn3(){
function fn2(){
console.log(a);
}
fn2();
var a = 4;
}
var a = 2;
return fn3;
}
var fn = fn1();
fn() //输出多少
(2)
var a = 1
function fn1(){
function fn2(){
console.log(a)
}
function fn3(){
var a = 4
fn2()
}
var a = 2
return fn3
}
var fn = fn1()
fn() //输出多少
答案解析如下:
(1)输出'undefined'
代码最后执行fn(),而它之前定义了fn=fn1(),故其值与fn1函数的结果一样。fn1的函数声明最终返回fn3,而fn3的函数声明里调用了fn2,fn2的函数声明里要求打印a的值,fn2自己内部没有但它的上层fn3内部有,由于变量提升,function fn3(){...}的实际代码等同于:function fn3(){ var a; function fu2(){ console.log(a)} fn2(); a=4;}因为在fn2()在a = 4语句之前就已经执行了,此时只是已经var a而已并未来得及为其赋值,故输出undefined。
函数fn2()的作用域链包含四个对象:自己的变量对象-->fn3()的变量对象-->fn1()的变量对象-->全局环境的变量对象。
(2)输出'2'
函数fn2()的作用域链包含三个对象:自己的变量对象-->fn1()的变量对象-->全局的变量对象。
本文引用出处:
JavaScript中作用域和作用域链的简单理解(变量提升) - 不重名 - 博客园www.cnblogs.com