Javascript是一门神奇的语言,这句话向来没错。与其它的高级语言不同,无论是从变量定义上还是类继承上等,Javascript都有自己的一套独立的风格。这一次着重理解Javascript里面的作用域。
Javascript的作用域分三种:全局作用域,函数作用域,闭包,还有一个概念,叫做作用域链,那么,分别是什么意思呢?我们一个一个来慢慢解释。
全局作用域
全局作用域,很普通的一个概念,所有语言大同小异。
//a位于全局作用域中
var a = 0;
function func1 () {}
function func2 () {}
很明显,变量a就位于全局作用域,并不属于任何一个函数所私有,大家都可以访问。
函数作用域
Javascript是一门解释性语言,它的作用域基于词法作用域的,所以,不存在块级作用域这一说法。在Javascript里面,使用函数作用域来代替所谓的块级作用域。
我们先来解释一下什么是词法作用域。
《Javascript权威教程》有一句话是这样说的:“Javascript的函数在定义它们的作用域里运行,而不是在执行它们的作用域里运行”。什么意思呢?我们来看一段代码。
var a = 'a';
getValue();
function getValue () {
console.log(a);
var a = 'b';
console.log(a);
}
试想一下,这段代码可以运行吗?运行的结果又是什么?
很明显,这一段代码肯定可以运行,输出的结果分别是undefined
和a
,这是为什么呢?
首先,我们来解释第一个现象——关于这段代码的运行问题。
我们都知道,函数必须在实现之后调用,或者说,在调用之前,必须得先声明了,但是和C语言或者其它的类C等高级语言不同,Javascript里可以先调用函数,再对函数进行功能实现,并不用声明什么的。这时候,我们就要想到那句话了:Javascript的函数在定义它们的作用域里运行,而不是在执行它们的作用域里运行。
通俗一点就是说,Javascript里的函数在调用的时候,根本不管这个函数在哪里声明,是前还是后,只要这个函数定义在这个全局空间里,那么,在调用函数的时候,就能找到这个函数的声明。这就是词法作用域。
然后,我们来看第二个现象——关于运行的结果。
初学Javascript的人,看到这段代码,一般都会回答运行结果为a
和b
,然而,事实总是残酷的,所谓too young,too simple。
我们先来解释运行结果的第二个b
。很明显,在程序输出变量b的值之前有一个赋值语句var a = 'b'
,所以,可以直接输出a的结果就是b
。这是我们都能够理解的。
然后,我们再来看第一个结果undefined
,这是为什么呢?不是已经定义了变量a
的值了吗?这时候,就要涉及到另外一个概念了——变量提升(Hoisting)。
所谓变量提升(Hoisting),意思就是,无论变量定义在哪里,只要你定义了这个变量,这个变量就会被提升到最顶部。所以,这也就不难理解为什么可以先调用函数再对函数进行声明了。
但是有另外一种情况,如果函数的声明是采用的
var func = function () {}
格式的声明的话,就必须在函数调用前声明函数。
那。。。刚才那个,按照变量提升的说法,结果不应该是两个b
吗?
别急,我还没说完呢。变量提升,提升的只是变量的定义而已。它的赋值语句并不会随着变量提升到顶部。所以,刚才的代码可以写成这样:
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的函数作用域。
我们先看一段代码:
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里是没有块级作用域的。
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);
这段代码输出的结果分别是:两次Erichain
,10
和两个报错,错误都是找不到变量i
和myName
。
根据函数作用域,后面两个报错不难理解。我们着重看10
这个结果。我们发现,在函数内部,其实没有定义i
这个变量,只是在for循环里定义了。要是在C或者类C语言里,变量i
在循环体结束之后就销毁了。但是,在Javascript里,不存在块级作用域,所以,在循环体内部定义的变量,就相当于在函数内部定义的变量,在函数内部依然可以访问。这也是Javascript和其他语言不同的一个地方。
闭包
Javascript里的最特别的功能之一,当然是它的闭包。什么是闭包呢?通俗一点说就是:在函数里面声明并且实现函数,即所谓“函数的嵌套”。看一段代码自然就明白了。
function closure () {
var newVal = 'Erichain';
function getNewVal () {
console.log(newVal);
}
getNewVal();
}
closure();
这段代码的运行结果将会输出Erichain
。
我们把函数拆分为外函数closure()
和内函数getNewVal()
,同时,在外函数的内部,调用了内函数来运行。所以,调用closure()
函数的同时也随即调用了getNewVal()
。所以,能够输出结果。这就是一个很简单的闭包的实现。但是,这不是我要说的重点。本篇文章重点在作用域,所以,我们来分析一下闭包的作用域。
我们把上面的这段代码稍微修改一下。
function closure () {
var newVal = 'Erichain';
function getNewVal () {
var newName = 'Zain';
console.log(newVal);
console.log(newName);
}
getNewVal();
console.log(newName);
}
closure();
运行这段代码,我们会得到两个输出结果:Erichain
和Zain
,另外,还有一个报错:ReferenceError: Can't find variable: newName
。
我们来解释一下这个现象。
在上文中,有讲到,函数内部定义的变量只能函数自身访问。同理,闭包是函数内部的函数,所以,闭包里面所定义的变量,也只有闭包内部能够访问。但是,闭包能够访问外部函数的变量。这就是Javascript的闭包的作用域。
作用域链
说完了Javascript的三种作用域,那么,接下来理解作用域链也就不是什么大问题了。
还是先看一段代码:
var myName = 'Erichain';
function getMyName () {
console.log(myName);
}
getMyName();
运行这段代码,输出的结果将是Erichain
。
我们发现,我们在函数里并没有定义变量myName
,但是,最终函数却没有报错,而是正常输出结果。而这个结果,恰好是在全局空间里定义的变量myName
的值。这就要涉及到Javascript的作用域链了。
调用函数的时候,函数会先从函数内部找寻变量,如果找不到,那么就会一层一层的往上寻找,直到找到这个变量为止。
在getMyName()
函数里,我们虽然没有定义变量myName
,但是,函数在全局空间里找到了这个变量,所以,可以使用这个变量输出其值。
再看一段代码来理解。
var myName = 'Erichain';
function getMyName () {
var myName = 'Zain';
console.log(myName);
}
getMyName();
那么,这段代码又会输出怎么样的结果呢?答案是Zain
。
用上面的话来解释:函数在其内部就找到了变量myName
,所以,他就不需要再往上去寻找变量。所以这里会输出Zain
。
这也就是Javascript作用域链的工作机制。
最后,加上一个小tip。关于Javascript对变量的内存分配和回收的问题。
第一,Javascript的局部变量,也就是函数内部定义的变量,在函数调用完成之后会自动销毁,不需要人工在进行手动销毁;
第二,Javascript的全局变量和闭包里的变量在定义之后,如果不对其进行销毁的话,会一直存在内存空间,污染全局空间,必要的时候,需要对其进行手动销毁,怎么做呢?
var a = 'Erichain';
console.log(a);
a = null;
console.log(a);
为变量赋值为null
即可销毁这个变量。
以上为本人所总结的有关于Javascript的作用域的知识,每天学习一点,每天进步一点。如果有什么疑问或者问题,希望大家能与我交流。