此文章著作權(quán)歸饑人谷_Lyndon和饑人谷所有,轉(zhuǎn)載請(qǐng)注明
前言
比較繞的并不是作用域與變量提升,而是作用域鏈,經(jīng)常容易在寫(xiě)偽碼時(shí)遇到死循環(huán)/(ㄒoㄒ)/~~相對(duì)于作用域來(lái)說(shuō),變量提升會(huì)稍微繞一些,不過(guò)只需牢記原則就不會(huì)出錯(cuò),熟悉變量提升的機(jī)制能夠更好地理解作用域鏈,降低犯錯(cuò)風(fēng)險(xiǎn)。
一、作用域
在大多數(shù)編程語(yǔ)言中,會(huì)用花括號(hào){}
來(lái)形成一個(gè)作用域,俗稱(chēng)“塊作用域”,例如C語(yǔ)言、C++等。但是在JS中{}
并不能產(chǎn)生塊作用域,JS中的作用域是依靠函數(shù)形成的。
在ECMAScript5中,JS只有兩類(lèi)作用域:全局作用域、函數(shù)作用域。
全局作用域:全局對(duì)象的作用域,在代碼的任何地方都可訪問(wèn),但有時(shí)會(huì)被函數(shù)作用域覆蓋
函數(shù)作用域:作用于整個(gè)函數(shù)范圍內(nèi),不管到底是在函數(shù)中的何處進(jìn)行聲明
// 全局變量
var i = 100;
// 函數(shù)聲明,outer是一個(gè)外部函數(shù)
function outer(){
// 訪問(wèn)全局變量
console.log(i); // 100
// 函數(shù)聲明,inner是一個(gè)內(nèi)部函數(shù)
function inner(){
// 內(nèi)部函數(shù)的內(nèi)部進(jìn)行了變量提升,也就是第二部分?jǐn)⑹龅膬?nèi)容
console.log(i); // undefined
// 這里的i是局部變量,作用域僅在函數(shù)內(nèi)
var i = 1;
// 局部變量覆蓋全局變量,或者說(shuō)是函數(shù)作用域覆蓋全局作用域
console.log(i); // 1
}
inner();
// 這里的i是全局變量
console.log(i); // 100
}
outer();
定義變量時(shí),如果不寫(xiě)var
,那么就會(huì)相當(dāng)于聲明了一個(gè)全局變量,作用域?yàn)槿肿饔糜颍环駝t聲明的是局部變量,作用域?yàn)楹瘮?shù)作用域。在以上代碼段中,第一行的var i = 0
是全局變量,雖然它添加var
,但是在全局范疇中聲明,而且不在函數(shù)范圍內(nèi),因此效果等同于i = 0
。但是在JS編程中應(yīng)該盡力避免不加var
,即使真的需要全局變量,也應(yīng)該在最外層作用域中使用var
聲明。
二、變量提升
關(guān)于變量提升(hoisting)的定義,Kenneth Truyers
曾經(jīng)在博客中這樣寫(xiě)道
In Javascript, you can have multiple var-statements in a function. All of these statements act as if they were declared at the top of the function. Hoisting is the act of moving the declarations to the top of the function.
變量的聲明會(huì)被自動(dòng)移到函數(shù)或者全局代碼的最頂上。移動(dòng)的僅僅是declarations
,變量的定義并不會(huì)隨之提升。
因?yàn)樽兞刻嵘浅5膚eird,所以很多代碼的欺騙性非常強(qiáng),尤其是在前端面試或者筆試中考官非常青睞于類(lèi)似的題目,因此我建議理解代碼的等價(jià)形式,也就是在紙上根據(jù)變量提升原則來(lái)書(shū)寫(xiě)新的等價(jià)代碼從而找出正確答案。如以下代碼段:
var date = new Date();
function fn(){
console.log(date);
if(true){
var date = 'hello';
}
}
fn();
結(jié)果并不是date
的toString
方法返回的結(jié)果,而是undefined
,因?yàn)橐陨洗a等價(jià)于:
// 變量聲明提升
var date;
date = new Date();
function fn(){
// 變量聲明提升,但是此時(shí)未定義變量的值
var date;
console.log(date);
if(true){
date = "hello";
}
}
fn();
但是在變量提升中還存在著一些特殊情況,因?yàn)樵贓S5中,變量聲明、函數(shù)聲明都會(huì)被提升,這就衍生出很多值得辨析的問(wèn)題。
在ES6中,function *
, let
, class
, const
也會(huì)被提升,但是提升機(jī)制又與變量提升、函數(shù)提升有所區(qū)別。
>>> 情境1:重復(fù)聲明
如以下代碼,重復(fù)聲明變量a
:
var a = 10;
console.log(a); // 10
if(true){
var a = 20;
console.log(a); // 20
}
console.log(a); // 20
分析這一段代碼時(shí),記住兩點(diǎn):JS變量只有全局作用域、函數(shù)作用域兩種作用域形態(tài),a
只會(huì)在代碼頂部聲明一次,而var a = 20
的作用僅是賦值,因此以上代碼等價(jià)于:
var a;
var a; // 是流程控制語(yǔ)句中的a,實(shí)際上在JS解析中這一句是不存在的,因?yàn)樽兞縛a`已經(jīng)聲明過(guò)了
a = 10;
console.log(a);
if(true){
a = 20;
console.log(a);
}
console.log(a);
>>> 情境2:命名沖突
當(dāng)console.log
處于需要提升的變量與方法的下方時(shí),如果在同一個(gè)作用域中定義了名字相同的變量與方法,那么無(wú)論順序如何,變量的賦值都會(huì)覆蓋掉方法的賦值。其實(shí)用正常思維方式就可以理解。
var fn = 3;
function fn(){};
console.log(fn); // 3
以上代碼等價(jià)于:
var fn;
function fn(){};
fn = 3;
console.log(fn);
可以明顯看出:經(jīng)過(guò)轉(zhuǎn)換后是很容易被理解的。但是還需要考慮到當(dāng)函數(shù)執(zhí)行有命名沖突的時(shí)候,函數(shù)執(zhí)行的載入順序是變量、函數(shù)、參數(shù)
,如以下代碼:
function f(f){
console.log(f);
var f = 100;
console.log(f);
}
f(20);
這是一段復(fù)雜的代碼,但是結(jié)合載入順序來(lái)理解,可以將代碼等價(jià)轉(zhuǎn)換為如下形式:
function f(f){
var f;
console.log(f);
f = 100;
console.log(f);
}
傳入?yún)?shù)f = 20
后,函數(shù)內(nèi)部相當(dāng)于虛擬形成:var f = 20
,這個(gè)變量聲明其實(shí)可以認(rèn)為是覆蓋了函數(shù)內(nèi)部變量提升的var f
,因此第一個(gè)console.log(f)
的結(jié)果為20
,接下來(lái)是第二個(gè)f = 100
覆蓋之前的變量值,那么第二個(gè)console.log(f)
的結(jié)果為100
,所以在執(zhí)行這個(gè)很annoying的函數(shù)的時(shí)候,先提升變量,再執(zhí)行函數(shù)體,接下來(lái)傳入?yún)?shù)20
,事實(shí)上也很好理解。
>>> 情況3: 函數(shù)與變量同時(shí)提升
如下代碼示例:
console.log(f);
function f(){};
var f = 'text';
輸出結(jié)果是:function f(){}
但是稍作轉(zhuǎn)變成如下形式:
console.log(f);
var f = function(){};
var f = 'text';
輸出結(jié)果是:undefined
這里涉及到的知識(shí)實(shí)際上和變量提升關(guān)系不大,而是和函數(shù)聲明方式有關(guān):
ECMAScript里面規(guī)定三種聲明函數(shù)方式,常用的有以下兩種:
// 第一種:函數(shù)聲明
function f(){
statement;
}
// 第二種:函數(shù)表達(dá)式
var f = function(){
statement;
}
針對(duì)第一段代碼,其中運(yùn)用函數(shù)聲明,函數(shù)聲明的方式所能保證的是:即使函數(shù)寫(xiě)在最后也能在之前語(yǔ)句中進(jìn)行調(diào)用,但是函數(shù)聲明部分必須已經(jīng)被下載至本地;
而第二段代碼,其中運(yùn)用函數(shù)表達(dá)式,實(shí)質(zhì)上是定義了一個(gè)變量f
,然后把function(){}
賦給變量,因此第二段代碼實(shí)際上等價(jià)于:
var f;
console.log(f);
f = function(){};
f = 'text';
第一段代碼,函數(shù)聲明在提升的時(shí)候,實(shí)際上是會(huì)把整個(gè)函數(shù)提升上去,包括函數(shù)定義的部分,所以第一段代碼的等價(jià)形式是:
var f;
function f(){};
console.log(f);
f = 'text';
但是再將代碼進(jìn)行轉(zhuǎn)換,會(huì)得到不一樣的結(jié)果:
console.log(foo);
var foo = 'text';
function foo(){};
返回的結(jié)果是:function foo(){}
>>> 情況4:函數(shù)與函數(shù)重復(fù)聲明
當(dāng)兩個(gè)函數(shù)聲明重復(fù)時(shí),其原則是后者覆蓋前者。以下代碼段:
console.log(foo);
function foo(n){return n+2};
function foo(n){return n+1};
輸出結(jié)果是:function foo(n){return n+1}
原理:在函數(shù)聲明提升時(shí),遵循先來(lái)后到的函數(shù)聲明提升原則,之后后者會(huì)覆蓋前者,因此以上代碼等價(jià)于:
function foo(n){return n+2};
function foo(n){return n+1};
console.log(foo);
如果調(diào)換代碼秩序,那么代碼輸出結(jié)果會(huì)變化:
function foo(n){return n+1};
function foo(n){return n+2};
console.log(foo); // function foo(n){return n+2};
總結(jié)
經(jīng)過(guò)以上操作,可以歸納出四項(xiàng)原則:
- 所有聲明都會(huì)被提升到對(duì)應(yīng)作用域的頂上(
as if they were declared at the top of the function
) - 同一個(gè)變量聲明只進(jìn)行一次,其他重復(fù)聲明會(huì)被JS解析忽略
- 函數(shù)聲明進(jìn)行提升時(shí)會(huì)連帶函數(shù)定義一起提升
- 遵循前三項(xiàng)原則多多動(dòng)手寫(xiě)等價(jià)轉(zhuǎn)換,就一定不會(huì)出錯(cuò)