JavaScript核心難點之變量提升、作用域、Promise、this與類型轉(zhuǎn)換解析
前言
在前端開發(fā)中,JavaScript的變量提升、作用域、Promise異步、this指向以及隱式類型轉(zhuǎn)換,是高頻面試考點,也是實際開發(fā)中容易踩坑的核心知識點。很多開發(fā)者在入門時會被這些概念繞暈,本文將用“通俗類比+專業(yè)拆解”的方式,結(jié)合具體代碼案例,把每個難點講透,幫你徹底理清邏輯、避開陷阱。
一、變量提升:var的“穿透性”與函數(shù)提升的特殊規(guī)則
變量提升(Hoisting)是JS引擎在代碼執(zhí)行前的“預(yù)編譯”行為——簡單說,就是JS會把變量和函數(shù)的聲明“提前”到當(dāng)前作用域的頂部,但賦值不會提升。這里重點區(qū)分var變量提升和函數(shù)提升的差異,尤其是if塊內(nèi)的函數(shù)提升,是高頻易錯點。
1.1 核心規(guī)則:var沒有塊級作用域,會“穿透”所有塊
在ES6之前,JS中只有“函數(shù)作用域”和“全局作用域”,沒有“塊級作用域”——也就是說,if(){}、for(){}、while(){}、單獨{}這些代碼塊,根本“關(guān)不住”var聲明的變量。var聲明的變量會穿透這些塊,提升到最近的函數(shù)頂部(如果在函數(shù)內(nèi))或全局頂部(如果在函數(shù)外)。
舉個通俗的例子:var就像一個“不受約束的調(diào)皮蛋”,不管你把它放在哪個小房間(代碼塊)里,它都會偷偷跑到房間的最外層(最近函數(shù)/全局),提前“占位置”。
1.2 易錯點:if內(nèi)的函數(shù)提升(關(guān)鍵案例解析)
函數(shù)提升分兩種情況:普通函數(shù)聲明(function fn(){})會提升,而立即執(zhí)行函數(shù)((function(){})())本身不會提升。其中,if塊內(nèi)的函數(shù)提升最為特殊,瀏覽器會做特殊處理,我們結(jié)合具體案例看:
var a = 0;
console.log("第一次輸出a: ", a); // 輸出0
if (true) {
a = 1;
console.log("第二次輸出a: ", a); // 輸出1
function a() {} // 函數(shù)聲明,會觸發(fā)特殊提升
a = 2;
console.log("第三次輸出a: ", a); // 輸出2
}
console.log("第四次輸出a: ", a); // 輸出1
很多人會疑惑:第四次輸出為什么是1?不是應(yīng)該是2嗎?這里瀏覽器的執(zhí)行邏輯分3步,順序至關(guān)重要(記牢這3步,避免踩坑):
先把【當(dāng)前塊內(nèi)a的值】同步給外部a(此時塊內(nèi)a已經(jīng)被賦值為1)→ 外部a從0變成1;
再把函數(shù)a賦值給塊內(nèi)a → 塊內(nèi)a從1變成函數(shù)a;
最后立刻斷開塊內(nèi)a和外部a的關(guān)聯(lián) → 后續(xù)塊內(nèi)a賦值為2,不會影響外部a。
簡單總結(jié):if塊內(nèi)的函數(shù)聲明,會先同步塊內(nèi)變量值到外部,再賦值函數(shù),最后斷開關(guān)聯(lián),這是瀏覽器的特殊處理邏輯,也是面試常考的“坑點”。
1.3 延伸案例:var聲明的“全局污染”陷阱
(function(){
var x = y = 1; // 關(guān)鍵:y沒有var聲明
})();
var z;
console.log(y); // 輸出1
console.log(z); // 輸出undefined
console.log(x); // 報錯:Uncaught ReferenceError: x is not defined
解析(通俗版):var x = y = 1; 等價于 y = 1; var x = y; —— y沒有var聲明,會自動變成全局變量(相當(dāng)于window.y = 1),而x有var聲明,是函數(shù)內(nèi)的局部變量,函數(shù)執(zhí)行完后銷毀,所以外部訪問不到x。
專業(yè)重點:沒有var/let/const聲明的變量,會自動掛載到全局對象(window)上,造成全局污染,這是開發(fā)中要堅決避免的寫法。
1.4 再練一個:函數(shù)內(nèi)var提升的“undefined陷阱”
var friendName = 'World';
(function() {
if (typeof friendName === 'undefined') {
var friendName = 'Jack';
console.log('Goodbye ' + friendName); // 輸出Goodbye Jack
} else {
console.log('Hello ' + friendName);
}
})();
解析:函數(shù)內(nèi)的var friendName會提升到函數(shù)頂部,相當(dāng)于“var friendName;”(只聲明,未賦值),所以typeof friendName === 'undefined’為true,進(jìn)入if塊賦值為Jack并輸出。這里的關(guān)鍵是:函數(shù)內(nèi)的var提升,會覆蓋外部的全局變量(作用域優(yōu)先級:局部>全局)。
二、作用域與作用域鏈:函數(shù)“出生地”決定能訪問哪些變量
作用域的核心:函數(shù)能訪問哪些變量,取決于它的“定義時”的作用域,而不是“調(diào)用時”的作用域。作用域鏈就是函數(shù)定義時形成的“變量查找鏈條”,從自身作用域開始,逐級向上查找,直到全局作用域。
通俗類比:作用域就像“你出生的家庭”,你能拿到家里的東西(變量),取決于你出生在哪個家(定義時的作用域),而不是你后來去了哪個家(調(diào)用時的作用域)。
2.1 基礎(chǔ)案例:函數(shù)定義在內(nèi)部,能訪問外部變量
function a() {
var temp = 10;
function b() {
console.log(temp); // 輸出10
}
b();
}
a();
解析:函數(shù)b定義在函數(shù)a內(nèi)部,所以b的作用域鏈包含a的作用域和全局作用域,能訪問到a內(nèi)的temp變量。
2.2 反例:函數(shù)定義在全局,訪問不到局部變量
function a() {
var temp = 10;
b(); // 調(diào)用全局的b
}
function b() {
console.log(temp); // 報錯:temp is not defined
}
a();
解析:函數(shù)b定義在全局作用域,它的作用域鏈只有全局作用域,找不到a內(nèi)的局部變量temp,所以報錯。這就是“定義時決定作用域”的核心體現(xiàn)。
2.3 進(jìn)階:閉包與作用域的結(jié)合(高頻面試題)
閉包的本質(zhì):函數(shù)嵌套函數(shù),內(nèi)部函數(shù)引用外部函數(shù)的變量,外部函數(shù)執(zhí)行后,內(nèi)部函數(shù)依然能訪問到外部函數(shù)的變量(因為閉包保存了外部函數(shù)的作用域)。每次調(diào)用外部函數(shù),都會生成一個新的閉包環(huán)境,互不影響。
function fun(n, o) {
console.log(o)
return {
fun: function(m){
return fun(m, n); // 內(nèi)部函數(shù)引用外部函數(shù)的n,形成閉包
}
};
}
// 三個案例,重點區(qū)分閉包環(huán)境
var a = fun(0); a.fun(1); a.fun(2); a.fun(3); // 輸出:undefined、0、0、0
var b = fun(0).fun(1).fun(2).fun(3); // 輸出:undefined、0、1、2
var c = fun(0).fun(1); c.fun(2); c.fun(3); // 輸出:undefined、0、1、1
解析(通俗版):
var a = fun(0):調(diào)用fun(0),o是undefined,返回一個對象,這個對象的fun方法引用了外部fun的n=0(閉包保存n=0),所以后續(xù)a.fun(1)、a.fun(2),都是調(diào)用fun(m, 0),o始終是0;
var b = fun(0).fun(1).fun(2).fun(3):每次調(diào)用fun都會生成新閉包——fun(0)返回的對象fun引用n=0,調(diào)用fun(1)時,fun(1,0)返回的對象fun引用n=1,以此類推,o依次是0、1、2;
var c = fun(0).fun(1):fun(0)返回的對象fun引用n=0,調(diào)用fun(1)得到fun(1,0),返回的對象fun引用n=1,后續(xù)c.fun(2)、c.fun(3),都是調(diào)用fun(m,1),o始終是1。
核心記住:閉包保存的是“定義時”的作用域,每次調(diào)用外部函數(shù),都會生成新的閉包環(huán)境,變量值互不干擾。
三、Promise與異步執(zhí)行:微任務(wù)、宏任務(wù)的執(zhí)行順序
Promise是JS處理異步操作的核心,很多開發(fā)者會被它的執(zhí)行順序、狀態(tài)變化、鏈?zhǔn)秸{(diào)用繞暈。核心要點:同步代碼先執(zhí)行,再執(zhí)行所有微任務(wù),最后執(zhí)行宏任務(wù);每執(zhí)行一個宏任務(wù)后,都會清空當(dāng)前所有微任務(wù)。
先明確兩個概念(通俗版):
微任務(wù):“優(yōu)先級高”的異步任務(wù),比如Promise.then/catch/finally、async/await(本質(zhì)也是Promise)、process.nextTick(Node環(huán)境);
宏任務(wù):“優(yōu)先級低”的異步任務(wù),比如setTimeout、setInterval、DOM事件、AJAX請求。
3.1 基礎(chǔ)案例1:Promise未resolve/reject,狀態(tài)不變
const promise = new Promise((resolve, reject) => {
console.log(1); // 同步代碼,先執(zhí)行
console.log(2); // 同步代碼,繼續(xù)執(zhí)行
});
promise.then(() => {
console.log(3); // 不會執(zhí)行,因為Promise未resolve/reject
});
console.log(4); // 同步代碼,執(zhí)行
輸出結(jié)果:1 2 4。解析:Promise構(gòu)造函數(shù)內(nèi)的代碼是同步執(zhí)行的,而then方法只有在Promise狀態(tài)變?yōu)閞esolved(成功)或rejected(失?。r才會執(zhí)行,這里沒有調(diào)用resolve/reject,狀態(tài)一直是pending,所以then回調(diào)不執(zhí)行。
3.2 基礎(chǔ)案例2:Promise狀態(tài)變化與then回調(diào)執(zhí)行
const promise1 = new Promise((resolve, reject) => {
console.log('promise1'); // 同步執(zhí)行
resolve('resolve1'); // 改變狀態(tài)為resolved
})
const promise2 = promise1.then(res => {
console.log(res); // 微任務(wù),同步代碼執(zhí)行完后執(zhí)行
})
console.log('1', promise1); // 同步執(zhí)行,promise1狀態(tài)為fulfilled
console.log('2', promise2); // 同步執(zhí)行,promise2狀態(tài)為pending
輸出結(jié)果:promise1 → 1 fulfilled resolve1 → 2 pending → resolve1。解析:promise1調(diào)用resolve后,狀態(tài)變?yōu)閒ulfilled,then回調(diào)進(jìn)入微任務(wù)隊列;同步代碼執(zhí)行完后,執(zhí)行微任務(wù),打印res的值。
3.3 進(jìn)階案例:微任務(wù)與宏任務(wù)的執(zhí)行順序(高頻面試題)
Promise.resolve().then(() => {
console.log('promise1'); // 微任務(wù)1
const timer2 = setTimeout(() => {
console.log('timer2') // 宏任務(wù)2
}, 0)
});
const timer1 = setTimeout(() => {
console.log('timer1') // 宏任務(wù)1
Promise.resolve().then(() => {
console.log('promise2') // 微任務(wù)2
})
}, 0)
console.log('start'); // 同步代碼
輸出結(jié)果:start → promise1 → timer1 → promise2 → timer2。解析步驟(記牢這個順序,搞定所有異步面試題):
執(zhí)行所有同步代碼:console.log(‘start’);
執(zhí)行所有微任務(wù):Promise.resolve().then(),打印promise1,同時創(chuàng)建宏任務(wù)timer2;
執(zhí)行宏任務(wù)隊列中的第一個宏任務(wù):timer1,打印timer1;
執(zhí)行當(dāng)前宏任務(wù)產(chǎn)生的所有微任務(wù):Promise.resolve().then(),打印promise2;
執(zhí)行下一個宏任務(wù):timer2,打印timer2。
3.4 常見陷阱:Promise.then的值穿透、return錯誤不進(jìn)catch
陷阱1:then傳非函數(shù),發(fā)生值穿透
Promise.resolve(1) .then(2) // 傳數(shù)字,非函數(shù) → 值穿透 .then(Promise.resolve(3)) // 傳Promise,非函數(shù) → 值穿透 .then(console.log) // 傳函數(shù),打印1
解析:Promise.then()必須接收一個函數(shù)作為回調(diào),如果傳的是數(shù)字、Promise、對象等非函數(shù),會發(fā)生“值穿透”——把前一個then的有效結(jié)果,直接傳給下一個then,所以最終打印1。
陷阱2:return錯誤對象,不會進(jìn)入catch
Promise.resolve().then(() => {
return new Error('error!!!') // return非Promise錯誤對象
}).then(res => {
console.log("then: ", res) // 打印then: Error: error!!!
}).catch(err => {
console.log("catch: ", err) // 不會執(zhí)行
})
核心原理(一句話記牢):return任意非Promise值(包括錯誤對象),都會把這個值當(dāng)作成功結(jié)果,傳給下一個then;只有throw錯誤,或return Promise.reject(),才會進(jìn)入catch。
3.5 補(bǔ)充:async/await的執(zhí)行邏輯(Promise的語法糖)
async/await本質(zhì)是Promise的語法糖,await會“讓出線程”,把后面的代碼包裝成微任務(wù),放到微任務(wù)隊列,等待await后面的Promise完成后執(zhí)行。注意:如果await后面的Promise永遠(yuǎn)不resolve,會永久阻塞。
async function async1() {
console.log("async1 start"); // 同步執(zhí)行
await async2(); // 等待async2完成,后面的代碼變成微任務(wù)
console.log("async1 end"); // 微任務(wù),同步代碼執(zhí)行完后執(zhí)行
}
async function async2() {
console.log("async2"); // 同步執(zhí)行
}
async1();
console.log('start');
輸出結(jié)果:async1 start → async2 → start → async1 end。解析:async1調(diào)用后,先執(zhí)行同步代碼async1 start,再執(zhí)行async2(同步),await讓出線程,執(zhí)行外部同步代碼start,最后執(zhí)行微任務(wù)async1 end。
四、this指向:誰調(diào)用,指向誰(核心規(guī)則+易錯案例)
this指向是JS中最靈活也最易錯的知識點,核心規(guī)則只有一句話:誰調(diào)用函數(shù),this就指向誰;沒有調(diào)用者(獨立函數(shù)調(diào)用),this指向全局對象(window,嚴(yán)格模式下是undefined)。但有幾個特殊情況(箭頭函數(shù)、new、apply/call/bind)需要重點記。
4.1 基礎(chǔ)案例1:獨立函數(shù)調(diào)用,this指向window
function foo() {
console.log( this.a );
}
function doFoo() {
foo(); // 獨立函數(shù)調(diào)用,沒有調(diào)用者
}
var obj = {
a: 1,
doFoo: doFoo
};
var a = 2; // 全局變量a,掛載到window上
obj.doFoo(); // 輸出2
解析:obj.doFoo()調(diào)用doFoo,doFoo內(nèi)部調(diào)用foo()——foo是獨立調(diào)用,沒有任何前綴(比如obj.foo()),所以this指向window,window.a是2,輸出2。
4.2 特殊情況1:箭頭函數(shù)沒有自己的this,繼承外層作用域
var a = 10;
var obj = {
a: 20,
say: () => {
console.log(this.a); // 輸出10
}
}
obj.say();
var anotherObj = { a: 30 };
obj.say.apply(anotherObj); // 輸出10
解析:箭頭函數(shù)沒有自己的this,this繼承外層作用域(這里外層是全局作用域),所以this指向window,window.a是10;而且箭頭函數(shù)的this一旦確定,無法通過apply、call、bind修改(即使傳了參數(shù),也無效)。
4.3 特殊情況2:new調(diào)用構(gòu)造函數(shù),this指向新對象
var obj = {
name : 'cuggz',
fun : function(){
console.log(this.name);
}
}
obj.fun(); // 輸出cuggz(this指向obj)
new obj.fun(); // 輸出undefined(this指向新創(chuàng)建的空對象)
解析:new關(guān)鍵字的作用是創(chuàng)建一個新對象,然后讓構(gòu)造函數(shù)的this指向這個新對象。obj.fun()是obj調(diào)用fun,this指向obj;new obj.fun()是new調(diào)用,this指向新創(chuàng)建的空對象,空對象沒有name屬性,所以輸出undefined。
4.4 易錯案例:立即執(zhí)行函數(shù)(IIFE)的this指向
var myObject = {
foo: "bar",
func: function() {
var self = this; // 保存當(dāng)前this(指向myObject)
console.log(this.foo); // 輸出bar(this指向myObject)
console.log(self.foo); // 輸出bar(self指向myObject)
(function() {
console.log(this.foo); // 輸出undefined(this指向window)
console.log(self.foo); // 輸出bar(self指向myObject)
}());
}
};
myObject.func();
解析:立即執(zhí)行函數(shù)((function(){})())是獨立函數(shù)調(diào)用,沒有調(diào)用者,所以this指向window,window沒有foo屬性,輸出undefined;而self是提前保存的myObject的this,所以能訪問到foo。
4.5 進(jìn)階案例:arguments調(diào)用函數(shù),this指向arguments
var length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function(fn) {
fn(); // 獨立調(diào)用,this指向window,輸出10
arguments[0](); // arguments調(diào)用fn,this指向arguments,輸出2
}
};
obj.method(fn, 1); // arguments長度是2(fn和1兩個參數(shù))
解析:arguments是函數(shù)的內(nèi)置對象,保存了函數(shù)的參數(shù)。arguments0相當(dāng)于“arguments調(diào)用fn”,所以this指向arguments,arguments的length是2(傳入了fn和1兩個參數(shù)),所以輸出2。
五、隱式類型轉(zhuǎn)換:JS的“自動轉(zhuǎn)換”陷阱(高頻面試)
JS是弱類型語言,當(dāng)不同類型的值進(jìn)行比較或運算時,會自動進(jìn)行類型轉(zhuǎn)換(隱式轉(zhuǎn)換),這是很多bug的來源,也是面試常考的點。核心記住:對象轉(zhuǎn)原始值的規(guī)則、字符串比較規(guī)則、與=的差異。
5.1 核心規(guī)則:對象轉(zhuǎn)原始值(ToPrimitive)
任何對象(包括數(shù)組、普通對象)在轉(zhuǎn)換為原始值時,遵循“先valueOf()后toString()”的優(yōu)先級:
調(diào)用obj.valueOf():默認(rèn)返回對象自身(除非被重寫,比如Date對象的valueOf()返回時間戳);
若valueOf()返回的還是對象,則調(diào)用obj.toString():
普通對象默認(rèn)返回"[object Object]";
數(shù)組的toString()返回元素拼接的字符串(如[] → “”,[1,2] → “1,2”);
函數(shù)的toString()返回函數(shù)源碼字符串。
5.2 高頻面試題:6道經(jīng)典轉(zhuǎn)換案例(必掌握)
案例1:![] == 0 的結(jié)果(true)
解析步驟:
![]:邏輯非運算,先把[]轉(zhuǎn)布爾值——任意對象/數(shù)組轉(zhuǎn)布爾值都是true,所以![] → false;
false == 0:布爾值與數(shù)字比較,先把false轉(zhuǎn)數(shù)字(Number(false) → 0);
0 == 0 → true。
案例2:{} + [] 的結(jié)果(0)
解析步驟:
JS引擎會把左邊的{}當(dāng)作“空代碼塊”(不是對象),直接忽略;
剩下+[]:+是一元運算符,會把[]轉(zhuǎn)數(shù)字——先調(diào)用valueOf()返回[](對象),再調(diào)用toString()返回"“(空字符串),Number(”") → 0;
最終結(jié)果:0。
案例3:[] + {} 的結(jié)果(“[object Object]”)
解析步驟:
+兩邊都是引用類型(數(shù)組、對象),優(yōu)先進(jìn)行字符串拼接;
[]轉(zhuǎn)字符串:toString() → “”;
{}轉(zhuǎn)字符串:toString() → “[object Object]”;
“” + “[object Object]” → “[object Object]”。
案例4:“a” > “b”(false)、“10” > “2”(false)
字符串比較規(guī)則(核心):從第一個字符開始,依次比較對應(yīng)位置的字符,基于Unicode編碼(編碼值小的字符更?。?,一旦比較出結(jié)果,就提前結(jié)束;如果短字符串是長字符串的前綴,則短字符串更小。
解析:
“a” > “b”:"a"的Unicode編碼是97,"b"是98,97<98 → false;
“10” > “2”:先比較第一個字符,"1"的編碼是49,"2"是50,49<50 → false(不會繼續(xù)比較第二個字符)。
案例5:Object(123)、Object(“abc”) 的返回值類型
解析:Object()是包裝對象構(gòu)造函數(shù),傳入基本類型值,會返回對應(yīng)的包裝對象:
Object(123) → Number包裝對象,typeof 結(jié)果是"object";
Object(“abc”) → String包裝對象,typeof 結(jié)果是"object"。
注意:包裝對象是對象類型,不是基本類型,所以typeof返回"object"。
案例6:let a = {}; a == true(false)、a === true(false)
解析(結(jié)合對象轉(zhuǎn)原始值規(guī)則):
a == true:
對象a轉(zhuǎn)原始值:valueOf()返回{}(對象),再調(diào)用toString()返回"[object Object]";
布爾值true轉(zhuǎn)數(shù)字:Number(true) → 1;
比較變?yōu)?quot;[object Object]" == 1:字符串轉(zhuǎn)數(shù)字,“[object Object]” → NaN,NaN == 1 → false。
a === true:嚴(yán)格相等,要求類型和值都相同,a是object類型,true是boolean類型,類型不同 → false。
總結(jié):核心要點速記
本文梳理了JS中最核心、最易錯的5個知識點,每個知識點都結(jié)合了案例和通俗解析,方便大家理解和記憶,最后用幾句話速記核心:
變量提升:var穿透塊,函數(shù)聲明提升,立即執(zhí)行函數(shù)不提升;
作用域:函數(shù)能訪問的變量,取決于定義時的作用域,閉包保存定義時的環(huán)境;
Promise:同步→微任務(wù)→宏任務(wù),每執(zhí)行一個宏任務(wù)清空微任務(wù),then傳非函數(shù)會值穿透;
this指向:誰調(diào)用指向誰,箭頭函數(shù)繼承外層this,new指向新對象;
隱式轉(zhuǎn)換:對象轉(zhuǎn)原始值先valueOf后toString,字符串比較按Unicode編碼,==會自動轉(zhuǎn)換類型,===不轉(zhuǎn)換。
到此這篇關(guān)于JavaScript核心難點之變量提升、作用域、Promise、this與類型轉(zhuǎn)換解析的文章就介紹到這了,更多相關(guān)JS變量提升、作用域、Promise、this與類型轉(zhuǎn)換內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
js 實現(xiàn)省市區(qū)三級聯(lián)動菜單效果
本文主要分享了js實現(xiàn)省市區(qū)三級聯(lián)動菜單效果的示例代碼。具有很好的參考價值,下面跟著小編一起來看下吧2017-02-02
微信小程序?qū)W習(xí)總結(jié)(四)事件與冒泡實例分析
這篇文章主要介紹了微信小程序?qū)W習(xí)總結(jié)(四)事件與冒泡,結(jié)合實例形式分析了微信小程序事件、冒泡、數(shù)據(jù)獲取相關(guān)機(jī)制、原理與操作注意事項,需要的朋友可以參考下2020-06-06
JavaScript阻止事件冒泡和默認(rèn)行為的方法舉例
JavaScript事件的默認(rèn)行為指瀏覽器自動執(zhí)行的操作,如鏈接跳轉(zhuǎn)或表單提交,阻止這些行為可以使用event.preventDefault()、return?false或event.returnValue屬性,event.stopPropagation()用于阻止事件傳播,不直接阻止默認(rèn)行為,需要的朋友可以參考下2024-10-10
使用firebug進(jìn)行調(diào)試javascript的示例
調(diào)試javascript的方法有很多,在本文為大家介紹下使用firebug是如何做到的,感興趣的朋友可以參考下2013-12-12

