关于Javascript 的作用域

时间:2021-03-15 16:08:04

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);
}

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

很明显,这一段代码肯定可以运行,输出的结果分别是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吗?

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

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);

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

根据函数作用域,后面两个报错不难理解。我们着重看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();

运行这段代码,我们会得到两个输出结果:ErichainZain,另外,还有一个报错: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的作用域的知识,每天学习一点,每天进步一点。如果有什么疑问或者问题,希望大家能与我交流。