javascript中作用域是指变量与函数可访问的范围。作用域分为两类,一种是全局作用域,一种是局部作用域。全局变量拥有全局作用域,在JavaScript代码中的任何地方都有定义。局部变量是在函数体内声明而且只作用在函数体内部以及该函数体的子函数的变量。下面我们对全局作用域和局部作用域来做一个深入的理解。
全局变量拥有全局作用域,在代码的任何地方都有定义。一般有两种情况变量会拥有全局作用域:
var scope="global"; //声明一个全局变量
function checksope(){
outScop = 'out';//为定义直接赋值,默认为全局变量
function showglobal(){
alert(scope); //弹窗全局变量
}
showglobal();
}
checksope() // global 内部函数可以访问全局变量
其实还有一个比较特殊的也拥有全局作用域,他就是window对象的内置属性。比如window.location。
局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部,所有在一些地方也会看到有人把这种作用域称为函数作用域,我们吧上面代码稍作修改
var scope="global"; //声明一个全局变量
function checksope(){
var outScop = 'out';//局部变量,拥有局部作用域
function showglobal(){
alert(scope); //弹窗全局变量
}
showglobal();
}
checksope() // global 内部函数可以访问全局变量
在上面代码中outScop的作用域是在checkscope中,出了这个函数就访问不到了。
在ES6中新增了一种作用域就是块级作用域,块级作用域和变量的声明方式有关系,那就是使用let命令用来进行变量声明,使用let命令声明的变量只在let命令所在代码块内有效。来看一下下面的代码:
{
let kuaiLet = 'oecom.cn';
}
console.log(kuaiLet)
我们会发现报错了,原因就在于kuaiLet
这个变量只在他所在的大括号内有效,这个大括号就是他的块级作用域。当然,如果我们修改为var
声明,在下面就可以访问到,会直接输出oecom.cn。
Javascript中有一个执行上下文(execution context)的概念,它定义了变量或函数有权访问的其它数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链。
作用域链和原型继承查找时的区别:如果去查找一个普通对象的属性,但是在当前对象和其原型中都找不到时,会返回undefined;但查找的属性在作用域链中不存在的话就会抛出ReferenceError。
作用域链的顶端是全局对象,在全局环境中定义的变量就会绑定到全局对象中。
我们来看一下下面这个例子:
var scope="global"; //声明一个全局变量
function checksope(){
var outScop = 'out';//局部变量,拥有局部作用域
function showglobal(){
alert(scope); //弹窗全局变量
}
showglobal();
}
checksope() // global 内部函数可以访问全局变量
这个例子还是之前的例子,我们分析一下在执行checksope
这个函数的时候对于scope变量的作用域链。
当我们执行到showglobal
这个函数时,会有一个alert弹出scope
,解释器首先会在showglobal
方法里面查找scope
,发现这个作用域里面没有,于是就会到他的上一层checksope
这个函数作用域中查找,发现也没有,然后会再一次的向上一层查找,再上一层就是全局作用域了,在这里发现了scope
,至此,查找结束,将之弹出。
说到作用域和作用域链,对此比较复杂的应用就是在闭包上面。我们来看一下一个经常看到的一个关于闭包的问题:
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
对于理解作用域和作用域链的人都能很快给出答案,就是全部都输出3。那么为什么会输出3呢,下面我们来解释一下。
从全局执行上下文来看,循环结束后的VO是
globalContext = {
VO: {
data: [...],
i: 3
}
}
执行 data[0] 函数的时候,data[0] 函数的作用域链为:
data[0]Context = {
Scope: [AO, globalContext.VO]
}
由于其自身没有i变量,就会向上查找,所有从全局上下文查找到i为3,data[1] 和 data[2] 是一样的。为什么会从全局作用域里找到i呢?原因就在于var声明的变量没有块级作用域,在for中声明和直接声明全局变量是没有区别的,那么来看一下解决办法:
1.使用闭包
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0](); // 0
data[1](); // 1
data[2](); // 2
将其改写成一个用匿名函数包裹,并返回一个函数的方法来实现,这是一个典型的闭包。简单说一下在红宝书--《JavaScript高级程序设计》中指出:闭包是指有权访问另外一个函数作用域中的变量的函数。MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。其中自由变量,指在函数中使用的,但既不是函数参数arguments也不是函数的局部变量的变量,其实就是另外一个函数作用域中的变量。
在上面的例子中匿名函数形成一个独立的函数作用域,虽然循环结束后的全局执行上下文没有变化,但是执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:
data[0]Context = {
Scope: [AO, 匿名函数Context.AO globalContext.VO]
}
匿名函数执行上下文的AO为:
匿名函数Context = {
AO: {
arguments: {
0: 0,
length: 1
},
i: 0
}
}
由于变量的按值传递,所以会将变量i的值复制给实参num,在匿名函数的内部又创建了一个用于访问num的匿名函数,这样每个函数都有了一个num的副本,互不影响了。闭包执行上下文中贮存了变量i,所以根据作用域链会在globalContext.VO中查找到变量i,并输出0。
2.使用let声明变量i
在上面我们提到了,let声明的变量有块级作用域一说,来看一下他的运行原理:
var data = [];// 创建一个数组data;
// 进入第一次循环
{
let i = 0; // 注意:因为使用let使得for循环为块级作用域
// 此次 let i = 0 在这个块级作用域中,而不是在全局环境中
data[0] = function() {
console.log(i);
};
}
循环时,let声明i,所以整个块是块级作用域,那么data[0]这个函数就成了一个闭包。这里用{}表达并不符合语法,只是希望通过它来说明let存在时,这个for循环块是块级作用域,而不是全局作用域。
上面的块级作用域,就像函数作用域一样,函数执行完毕,其中的变量会被销毁,但是因为这个代码块中存在一个闭包,闭包的作用域链中引用着块级作用域,所以在闭包被调用之前,这个块级作用域内部的变量不会被销毁。
// 进入第二次循环
{
let i = 1; // 因为 let i = 1 和上面的 let i = 0
// 在不同的作用域中,所以不会相互影响
data[1] = function(){
console.log(i);
};
}
当执行data1时,进入下面的执行环境。
{
let i = 1;
data[1] = function(){
console.log(i);
};
}
在上面这个执行环境中,它会首先寻找该执行环境中是否存在i,没有找到,就沿着作用域链继续向上到了其所在的块作用域执行环境,找到了i = 1,于是输出了1。