什么是作用域
简单说作用域指的是一个变量被定义的区域范围,它决定了一个变量可被使用的范围,当前代码访问该变量的权限。
在JavaScript中提到作用域总是会涉及到几个关键词:执行环境,全局变量,局部变量,变量声明提升,作用域链,词法作用域,动态作用域,块级作用域
执行环境
执行环境(execution context)是JavaScript中最为重要的一个概念,执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为(JavaScript高级程序设计第三版)。每个执行环境都有一个与之关联的变量对象(variable object),环境中会定义的所有变量和函数都会保存在这个变量对象中。
执行环境又可分为全局执行环境和局部执行环境,在浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都被认为是window对象的属性或者方法。所以通常我们跟客户端定义接口的时候,如果客户端需要调用js方法以达到某种交互,我们可以在全局对象window下定义一个方法,如:
window.onStart=function(args){
//这里是web逻辑
}
每个函数都有自己的执行环境,当执行流进入到一个函数时,函数的环境就会被推入到一个环境栈中,函数执行完毕后,环境栈将其环境弹出,这也是为什么一般情况下函数执行完后,内部的变量会被销毁,完成垃圾收集。
全局变量
全局变量指的是当前程序中所有地方都可以访问的变量。它实际上是全局对象的一个属性。在JavaScript解释器开始运行时,它首先要做的就是创建一个全局对象,这个对象的属性就是JavaScript程序的全局变量。对于web程序来说,Window就是一个全局对象,Window定义了一个浏览器窗口全局的核心属性。
var a=1;
console.log(window.a);//1
局部变量
如果全局变量是全局对象的属性,那么局部变量就是局部对象(调用对象call object)的一个属性。虽然调用对象的生命周期比全局对象生命周期短,但是他们的作用是相同的。当执行一个函数时,函数的参数和局部变量是作为调用对象的属性而存储的。
function func(){
var a=1;
console.log(a);//1
}
console.log(a);//undefined
全局变量的作用域是全局的,它可以在程序的任何地方被访问,而局部变量的作用域是局部的,它只能在被定义的函数或方法内被访问(闭包除外)。
变量声明提升
JavaScript中函数和变量的声明都会被提升到函数的最顶部。也就是说函数和变量可以先使用再声明。
function func(){
console.log(a);//a在声明前被使用,输出undefined
var a=1;
}
局部变量a是在使用后声明的,JavaScript没有报错,而是输出undefined,上面代码等同于:
function func(){
var a=undefined;
console.log(a);
a=1;
}
也就是上面所说的,变量的声明被提升到了函数最顶部。为避免不必要的bug,我们通常需要把变量声明写在在被使用前。
作用域链
当代码在一个执行环境中执行时,会创建变量对象(variable object,通常简称VO)(存储了执行环境中的变量声明和函数声明)的一个作用域链(scope chain)。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。
作用域链是变量对象有序访问的链接关系
作用域链就好比一根链条,链条的最前端,始终是当前执行环境的变量对象(VO)。如果这个执行环境是一个函数,则将其活动对象(activity object,通常简称AO)作为变量对象。活动对象在最开始只包含一个对象,即arguments对象(在全局环境中是不存在的)。作用域链的下一个变量对象来自其包含环境,而下一个变量继续来自下一个包含环境,一直延续到全局变量对象。链条的最尾端就是这个全局变量对象。
变量对象VO是与执行上下文相对应的概念,它存储着以下内容:
- 变量声明
- 函数声明
- 函数参数
活动对象AO定义在函数执行阶段,它存储以下内容:
- 1.arguments.callee和arguments.length;
- 2.内部定义的函数
- 3.内部定义的变量
- 4.绑定对应的环境变量
借用《JavaScript权威指南》中的一张图:

词法作用域
JavaScript采用的是词法作用域,那什么是词法作用域呢?
词法作用域即静态作用域,是相对动态作用域来说的,即函数的作用域在函数定义的时候就已经确定了,而动态作用域是在函数调用的时候才定义的。
如何理解“函数的作用域在函数定义的时候就已经确定了”这句话呢?看下面这段代码:
var a=1;
function parent(){
console.log(a); //结果是1还是2?
}
function child(){
var a=2;
parent();
console.log(a);//这里呢?
}
child()
上述代码定义了两个函数parent和child,child函数中调用了parent函数,如果JavaScript是动态作用域,那么变量a的作用域应该在child函数中调用parent的时候确定,值应该是2,但实际上parent函数中输出值为1,根据作用域链我们分析下执行过程:
执行函数parent的时候,在parent函数中查找变量a,发现没有,于是接着找定义parent函数的外层,发现a=1,于是输出1。
我们再看《JavaScript权威指南》中的一个例子:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
先思考一下,上述2段代码结果是什么?
都是"local scope",为什么?很简单,因为JavaScript的作用域是词法作用域,是在函数定义的时候就确定了。两段代码中函数f定义的时候scope的值都是外层函数定义的变量var scope='local scope',无论函数f()如何执行,scope值都是'local scope'。
那么上述2段代码有何不同?
第1段代码函数checkscope返回的是f(),也就是f()函数执行后的值即字符串‘local scope’。
第2段代码函数checkscope返回的是f,它是一个函数名称,执行checkscope()返回的是f函数名称,checkscope()()这里多了一个括号,即执行被返回的f()函数。
JavaScript没有块级作用域(ES5)
相信在ES6之前,大家都知道这么一句话“JavaScript没有块级作用域”。
在类C语言中,由花括号封闭的代码都有自己的作用域,而且支持根据条件定义变量,但是在JavaScript中并不是:
if(true){
var name="沉默术士";
}
console.log(name);//沉默术士
在其他类C语言中name是在if语句中定义,if语句执行完毕后即销毁,但是在JavaScript中if语句中定义的变量作用域在当前的执行环境中都有效。
“没有块级作用域”在for循环中表现的尤为让人困惑:
for(var i=0;i<3;i++){
console.log(i);//1,2,3
}
console.log(i);//3
对于JavaScript来说,由for语句创建的变量i在for循环执行结束后,依然存在于循环外的执行环境中。
举一个初学者常遇到的问题:
<ul id="list">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
var ul=document.getElementById("list");
var li=ul.querySelectorAll("li");
for(var i=0,len=li.length;i<len;i++){
console.log(i);//依次输出0,1,2
li[i].onclick=function(){
alert(i);//3
}
}
alert(i);//3
正常情况下,我们肯定都是期望分别弹出0,1,2,但实际上每次弹出都是3,为什么?我们根据之前的知识分析一下: 先看最后一行代码
alert(i);//3
for语句会循环3次,i值因为i++从0累加到3,因为没有块级作用域,所以循环执行完毕后i依然存在执行环境中,此时i值等3,所以弹出3。
那为什么for循环里面也全是3呢?
onclick事件等于一个匿名函数,根据我们之前说的变量的作用域在定义的时候就确定了,匿名函数的变量i在函数中没用定义,于是找外一层,外一层的i即for循环中定义的i,点击的时候此时i值因为累加了3次,变成了3,所以每次弹出都是3。