JavaScript 是一门神奇的语言,这句话向来没错。与其它的高级语言不同,无论是从变量定义上还是类继承上等,JavaScript 都有自己的一套独立的风格。这一次着重理解JavaScript里面的作用域。

JavaScript 的作用域分三种:全局作用域,函数作用域,闭包,还有一个概念,叫做作用域链,那么,分别是什么意思呢?我们一个一个来慢慢解释。


全局作用域

全局作用域,很普通的一个概念,所有语言大同小异。

1
2
3
4
5
//a位于全局作用域中
var a = 0;
function func1() {}
function func2() {}

很明显,变量 a 就位于全局作用域,并不属于任何一个函数所私有,大家都可以访问。


函数作用域

JavaScript 是一门解释性语言,它的作用域基于词法作用域的,所以,不存在块级作用域这一说法。在 JavaScript 里面,使用函数作用域来代替所谓的块级作用域。

我们先来解释一下什么是词法作用域

《JavaScript 权威教程》有一句话是这样说的:“JavaScript 的函数在定义它们的作用域里运行,而不是在执行它们的作用域里运行”。什么意思呢?我们来看一段代码。

1
2
3
4
5
6
7
8
9
var a = 'a';
getValue();
function getValue() {
console.log(a);
var a = 'b';
console.log(a);
}

试想一下,这段代码可以运行吗?运行的结果又是什么?

很明显,这一段代码肯定可以运行,输出的结果分别是 undefineda,这是为什么呢?

首先,我们来解释第一个现象——关于这段代码的运行问题。

我们都知道,函数必须在实现之后调用,或者说,在调用之前,必须得先声明了,但是和 C 语言或者其它的类C等高级语言不同,JavaScript 里可以先调用函数,再对函数进行功能实现,并不用声明什么的。这时候,我们就要想到那句话了:JavaScript 的函数在定义它们的作用域里运行,而不是在执行它们的作用域里运行

通俗一点就是说,JavaScript 里的函数在调用的时候,根本不管这个函数在哪里声明,是前还是后,只要这个函数定义在这个全局空间里,那么,在调用函数的时候,就能找到这个函数的声明。这就是词法作用域

然后,我们来看第二个现象——关于运行的结果。

初学 JavaScript 的人,看到这段代码,一般都会回答运行结果为 ab,然而,事实总是残酷的,所谓 too young,too simple。

我们先来解释运行结果的第二个 b。很明显,在程序输出变量b的值之前有一个赋值语句 var a = 'b',所以,可以直接输出 a 的结果就是 b。这是我们都能够理解的。

然后,我们再来看第一个结果 undefined,这是为什么呢?不是已经定义了变量 a 的值了吗?这时候,就要涉及到另外一个概念了——变量提升 (Hoisting)

所谓变量提升 (Hoisting),意思就是,无论变量定义在哪里,只要你定义了这个变量,这个变量就会被提升到最顶部。所以,这也就不难理解为什么可以先调用函数再对函数进行声明了。

但是有另外一种情况,如果函数的声明是采用的 var func = function () {} 格式的声明的话,就必须在函数调用前声明函数。

那。。。刚才那个,按照变量提升的说法,结果不应该是两个 b 吗?

别急,我还没说完呢。变量提升,提升的只是变量的定义而已。它的赋值语句并不会随着变量提升到顶部。所以,刚才的代码可以写成这样:

1
2
3
4
5
6
7
8
9
10
var a = 'a';
getValue();
function getValue() {
var a;
console.log(a);
a = 'b';
console.log(a);
}

这样,就一清二楚了吧。第一个结果 undefined,变量 a 只是定义了,还没赋值,当然会输出 undefined。而后面又给 a 赋值为 'b',所以,当然会输出 'b' 啦。这就是变量提升 (Hoisting) 的作用。

OK,我们在这里多说了两个概念,分别是词法作用域变量提升 (Hoisting) 。现在,我们应该能够理解这两个概念了。所以,现在来理解 JavaScript 的函数作用域

我们先看一段代码:

1
2
3
4
5
6
function myName() {
var me = 'Erichain';
console.log(me);
}
myName();
console.log(me);

运行这段代码,结果是输出 Erichain 和收到一个报错 ReferenceError: Can't find variable: me

我们来分析一下,其实这个道理很简单。在函数内部,定义了一个变量 me,赋值为 'Erichain',然后,我们输出,很正确的一个流程。但是,当我们再到函数外部去输出这个变量的时候,却报错了。

JavaScript 的变量,定义在函数体内,那么,这个变量就只能在函数体内访问,就如同私有变量一样。一旦这个函数运行结束,这个变量也随之销毁。——这就是 JavaScript 的函数作用域

我们的变量 me 是定义在函数体内部的,所以,myName() 函数一旦运行完成,变量me随之销毁,所以,在外面输出的话,当然会报错了。

再看一段代码来理解函数作用域,并且,理解 JavaScript 里是没有块级作用域的。

1
2
3
4
5
6
7
8
9
10
function testScope() {
var myName = 'Erichain';
for ( var i = 8; i < 10; i++ ) {
console.log(myName);
}
console.log(i);
}
testScope();
console.log(i);
console.log(myName);

这段代码输出的结果分别是:两次 Erichain10 和两个报错,错误都是找不到变量 imyName

根据函数作用域,后面两个报错不难理解。我们着重看 10 这个结果。我们发现,在函数内部,其实没有定义 i 这个变量,只是在for循环里定义了。要是在 C 或者类 C 语言里,变量 i 在循环体结束之后就销毁了。但是,在 JavaScript 里,不存在块级作用域,所以,在循环体内部定义的变量,就相当于在函数内部定义的变量,在函数内部依然可以访问。这也是 JavaScript 和其他语言不同的一个地方。


闭包

JavaScript 里的最特别的功能之一,当然是它的闭包。什么是闭包呢?通俗一点说就是:在函数里面声明并且实现函数,即所谓“函数的嵌套”。看一段代码自然就明白了。

1
2
3
4
5
6
7
8
function closure() {
var newVal = 'Erichain';
function getNewVal() {
console.log(newVal);
}
getNewVal();
}
closure();

这段代码的运行结果将会输出Erichain

我们把函数拆分为外函数closure()和内函数getNewVal(),同时,在外函数的内部,调用了内函数来运行。所以,调用closure()函数的同时也随即调用了getNewVal()。所以,能够输出结果。这就是一个很简单的闭包的实现。但是,这不是我要说的重点。本篇文章重点在作用域,所以,我们来分析一下闭包的作用域。

我们把上面的这段代码稍微修改一下。

1
2
3
4
5
6
7
8
9
10
11
function closure () {
var newVal = 'Erichain';
function getNewVal () {
var newName = 'Zain';
console.log(newVal);
console.log(newName);
}
getNewVal();
console.log(newName);
}
closure();

运行这段代码,我们会得到两个输出结果:ErichainZain,另外,还有一个报错:ReferenceError: Can't find variable: newName

我们来解释一下这个现象。

在上文中,有讲到,函数内部定义的变量只能函数自身访问。同理,闭包是函数内部的函数,所以,闭包里面所定义的变量,也只有闭包内部能够访问。但是,闭包能够访问外部函数的变量。这就是JavaScript的闭包的作用域。


作用域链

说完了JavaScript的三种作用域,那么,接下来理解作用域链也就不是什么大问题了。

还是先看一段代码:

1
2
3
4
5
var myName = 'Erichain';
function getMyName () {
console.log(myName);
}
getMyName();

运行这段代码,输出的结果将是Erichain

我们发现,我们在函数里并没有定义变量myName,但是,最终函数却没有报错,而是正常输出结果。而这个结果,恰好是在全局空间里定义的变量myName的值。这就要涉及到JavaScript的作用域链了。

调用函数的时候,函数会先从函数内部找寻变量,如果找不到,那么就会一层一层的往上寻找,直到找到这个变量为止

getMyName()函数里,我们虽然没有定义变量myName,但是,函数在全局空间里找到了这个变量,所以,可以使用这个变量输出其值。

再看一段代码来理解。

1
2
3
4
5
6
var myName = 'Erichain';
function getMyName () {
var myName = 'Zain';
console.log(myName);
}
getMyName();

那么,这段代码又会输出怎么样的结果呢?答案是Zain

用上面的话来解释:函数在其内部就找到了变量myName,所以,他就不需要再往上去寻找变量。所以这里会输出Zain

这也就是JavaScript作用域链的工作机制。


最后,加上一个小tip。关于JavaScript对变量的内存分配和回收的问题

第一,JavaScript的局部变量,也就是函数内部定义的变量,在函数调用完成之后会自动销毁,不需要人工在进行手动销毁;

第二,JavaScript的全局变量和闭包里的变量在定义之后,如果不对其进行销毁的话,会一直存在内存空间,污染全局空间,必要的时候,需要对其进行手动销毁,怎么做呢?

1
2
3
4
var a = 'Erichain';
console.log(a);
a = null;
console.log(a);

为变量赋值为null即可销毁这个变量。


以上为本人所总结的有关于JavaScript的作用域的知识,每天学习一点,每天进步一点。如果有什么疑问或者问题,希望大家能与我交流。