變量和 函數(shù)聲明提升
js自上而下的執(zhí)行順序受到很多新手和部分老手的共識(shí),但這其實(shí)并不完全正確。這涉及到j(luò)s的編譯過程。
提升
js代碼執(zhí)行前,會(huì)先對(duì)代碼進(jìn)行編譯。編譯的一部分工作就是找到所有的聲明,然后建立作用域?qū)⑵潢P(guān)聯(lián)起來,因此,在當(dāng)前作用域內(nèi)包括變量和函數(shù)在內(nèi)的所有聲明都會(huì)在任何代碼被執(zhí)行前首先被處理。這里的聲明提前但是賦值并沒有,也就是聲明提升了,賦值還留在原地。
預(yù)解釋
在js中帶var 關(guān)鍵字或以function開頭的都要進(jìn)行預(yù)解釋。
預(yù)解釋的時(shí)候不管你條件是否成立,帶var的都要提前聲明。
預(yù)解釋的時(shí)候只預(yù)解釋等號(hào)左邊的,右邊的值不參與預(yù)解釋。
自執(zhí)行函數(shù)在全局作用域下是不進(jìn)行預(yù)解釋的。
預(yù)解釋的時(shí)候,如果名字已經(jīng)聲明過了,就不會(huì)重新聲明,但會(huì)重新賦值。
在函數(shù)體內(nèi)return下面的代碼雖然不執(zhí)行,但要進(jìn)行預(yù)解釋,return后面的都是跟著我們的返回值,不進(jìn)行預(yù)解釋。
變量提升
變量聲明,被提升在作用域頂端。如var a;
變量定義,聲明部分會(huì)被提升,賦值部分不被提升。如var b='test'
函數(shù)表達(dá)式其實(shí)就是變量定義,只不過恰好被賦值的類型是函數(shù)。所以只提升變量名,不提升函數(shù)值
console.log(a);
var a=2;
等價(jià)于:
var a;
console.log(a);//undefind
a=2;
這就用到了我們說的聲明提升了,賦值還留在原地。
function hoist(){
if(!foo){
var foo=5;
}
console.log(foo);
}
hoist();//5
如果當(dāng)前作用域中存在此變量聲明,無論它在什么地方聲明,引用此變量的時(shí)候就會(huì)在當(dāng)前作用域中查找,不會(huì)去外層作用域了。
經(jīng)過一次預(yù)編譯,上面的代碼邏輯如下:
function hoist(){
var foo;
if(!foo){
foo=5;
}
console.log(foo);
}
hoist()
變量聲明提升到了函數(shù)的頂部,初始值為undefind,自然if語句就會(huì)被執(zhí)行,foo變量賦值為5。
類似的,還有下面一個(gè)例子:
var foo=3;
function hoist(){
var foo=foo||5;
console.log(foo)//5
}
hoist();
結(jié)果是5不是3.雖然外層作用域有個(gè)foo變量,但函數(shù)內(nèi)是不會(huì)去引用的。
如果當(dāng)前作用域中聲明了多個(gè)同名變量,那么根據(jù)我們的推斷,它們的同一個(gè)標(biāo)識(shí)符會(huì)被提升至作用域的頂部,其他部分按順序執(zhí)行,比如:
function hoist(){
var foo=3;
{
var foo=5;
}
console.log(foo);//5
}
hoist();
由于js沒有塊級(jí)作用域。只有全局作用域和函數(shù)作用域。所以預(yù)編譯之后的代碼邏輯為:
function hoist(){
var foo;
foo=3;
{
foo=5;
}
console.log(foo);//5
}
hoist();
函數(shù)提升
函數(shù)聲明全部被提升,包括優(yōu)先級(jí)比變量聲明要高,名字和它相同的變量聲明會(huì)被忽略。沒有被調(diào)用就不會(huì)被解析
如下代碼:
function hoist(){
foo();//output : i am hoisted
funciton foo(){
console.log('i am hoisted')
}
}
hoist();
為什么函數(shù)聲明在調(diào)用之前就可以聲明,其實(shí)。引擎是把函數(shù)聲明的整個(gè)代碼提升到了當(dāng)前作用域的頂部。
function hoist(){
function foo(){
console.log('i am hoisted')
}
foo()//output: i am hoisted;
}
hoist();
相似的,如果在同一個(gè)作用域中存在多個(gè)同名函數(shù)聲明,后面出現(xiàn)的將會(huì)覆蓋前面的函數(shù)聲明。
function hoist(){
function foo(){
console.log(1)
}
foo();//output:2
function foo(){
console.log(2)
}
}
hoist();
當(dāng)函數(shù)聲明遇到函數(shù)表達(dá)式時(shí),會(huì)有什么樣的結(jié)果呢?先看下面這段代碼:
function hoist(){
foo();//2
var foo=function (){
console.log(1)
}
foo();//1
function foo(){
console.log(2)
}
foo();//1
}
運(yùn)行之后我們會(huì)發(fā)現(xiàn),輸出的結(jié)果依次是211,因?yàn)楹瘮?shù)是一等公民,函數(shù)聲明的優(yōu)先級(jí)最高,會(huì)被提升至當(dāng)前作用域的最頂端,所以第一次調(diào)用時(shí)實(shí)際執(zhí)行了下面定義的函數(shù)聲明,然后第二次調(diào)用時(shí),由于前面的函數(shù)表達(dá)式與之前的函數(shù)聲明同名,故將其覆蓋,以后的調(diào)用也會(huì)打印同樣的結(jié)果。
function hoist(){
var foo;
foo=function foo(){
console.log(2)
}
foo();//2
foo=function (){
console.log(1)
}
foo();//1
foo();//1
}
我們也不難理解。函數(shù)和變量重名時(shí),會(huì)如何執(zhí)行:
var foo=3;
function hoist(){
console.log(foo);//function foo(){}
var foo=5;
console.lgo(foo);//5
function foo(){}
}
hoist();
console.log(foo);//3
預(yù)編譯后是這樣的:
var foo=3;
function hoist(){
var foo;
foo=function foo(){};
console.log(foo);//function foo(){}
foo=5;
console.log(foo);//5
}
hoist();
console.log(foo);//3
參數(shù)重名
function fn(fn){
console.log(fn);
var fn=3;
console.log(fn);
}
fn(10)
function fn(10){
var fn=10;已初始化的一般變量大于未初始化的一般變量
var fn;
console.log(fn);
fn=3;
console.log(fn)//
}
在js中如果重名則已初始化的一般變量>函數(shù)>函數(shù)參數(shù)>未初始化一般變量。
var a;
function a(){
var b=0;
}//結(jié)果為function a(){var b=0}因?yàn)楹瘮?shù)的優(yōu)先級(jí)大于未初始化的一般變量。
var a=1;
function a(){
var b=0;
}//結(jié)果為1,因?yàn)橐殉跏蓟囊话阕兞績(jī)?yōu)先于函數(shù)。
面試題解析
console.log(foo);//undefined
console.log(bar);//function bar(){}
var foo=function (){};
function bar(){}
預(yù)編譯之后是:
var foo;
function bar(){}
console.log(foo);
console.log(bar);
foo=function(){};
function foo(){
a=5;
console.log(window.a);//undefind
console.log(a);//5
var a=10;
console.log(a)//10
}
foo()
預(yù)編譯如下:
function foo(){
var a;
a=5;//因?yàn)樵谧约旱淖饔糜蛴衋聲明的存在,a并不會(huì)污染全局。而是綁定到本作用域的a上。
console.log(window.a);
console.log(a);
a=10;
console.log(a)
}
foo()
function foo(){
var a=1;
function b(){
a=10;
return '';
function a(){}
}
b();
console.log(a)
}
foo();
編譯如下:
function foo(){
var a;//a和b一起提升到作用域的頂部
function b(){
function a(){}//b里面的a也提升到作用域b的頂部
a=10;//因?yàn)樯厦嬗凶兞縜,所以a也不會(huì)污染到上一層
return '';
}
a=1;
b();//這個(gè)有兩點(diǎn)要搞清楚b的a沒有污染到這個(gè)作用域;
console.log(a)//就近原則,本函數(shù)的log(a),找離自己最近的a變量
}
foo();
4.
console.log(a);
var a = 1;
console.log(b);
`console.log(a)`輸出undefined;因?yàn)閍變量提升,相當(dāng)于
var a;
console.log(a);
只聲明了沒有賦值,為undefined。
console.log(b)會(huì)報(bào)錯(cuò),因?yàn)榧葲]有聲明也沒有賦值。
5.
sayName('world');
sayAge(10);
function sayName(name){
console.log('hello ', name);
}
var sayAge = function(age){
console.log(age);
};
sayName函數(shù)會(huì)輸出'hello''world',sayAge會(huì)報(bào)錯(cuò)
原因:第一個(gè)是函數(shù)聲明,會(huì)函數(shù)聲明前置,所以函數(shù)聲明不必放在調(diào)用的前面。而第二個(gè)是函數(shù)表達(dá)式,相當(dāng)于普通變量,所以函數(shù)調(diào)用必須放在函數(shù)聲明的后面,否則會(huì)報(bào)錯(cuò)。