JavaScript變數函式宣告提升(Hoisting)是在 Javascript 中執行上下文工作方式的一種認識(也可以說是一種預編譯),從字面意義上看,“變數提升”意味著變數和函式的宣告會在物理層面移動到程式碼的最前面,在程式碼裡的位置是不會動的,而是在編譯階段被放入記憶體中會和程式碼順序不一樣。變數函式宣告提升雖然對於實際編碼影響不大,特別是現在ES6的普及,但作為前端算是一個基礎知識,必須掌握的,是很多大廠的前端面試必問的知識點之一。在這裡分享,不是什麼新鮮的內容,只是作為一個自己的學習筆記,加速對其的理解。
變數知道是ES5中的 var 和 function 中的產物,ES6中的 let 、 const 則不存在有變數提升。
變數提升
JavaScript引擎的工作方式是先解析程式碼,獲取所有宣告的變數和函式,然後再一行一行地執行。這造成的結果,就是所有的變數的宣告語句,都會被提升到程式碼的頭部,這就叫做變數提升(Hoisting)。
這裡說的變數宣告,包括函式的宣告,接下來看看程式碼:
function hoistingVariable() {
if (!devpoint) {
var devpoint = 1;
}
console.log(devpoint);
}
hoistingVariable();
// 下面是輸出結果
// 1
變數所處的作用域為函式體內,解析的時候查詢該作用域中的宣告的變數,devpoint
在if雖然未宣告,根據變數提升規則,變數的宣告提升到函式的第一行,但未賦值。實際的效果等同於下面的程式碼:
function hoistingVariable() {
var devpoint;
if (!devpoint) {
devpoint = 1;
}
console.log(devpoint);
}
hoistingVariable();
接下再增加一些迷惑的程式碼,如下:
var devpoint = "out";
function hoistingVariable() {
var devpoint;
if (!devpoint) {
devpoint = "in";
}
console.log(devpoint);
}
hoistingVariable();
console.log(devpoint);
// 下面是輸出結果
// in
// out
對於同名變數宣告,個人理解是先找作用域,就近原則,函式體內宣告(前提是有宣告 var ),就只找函式內查詢,並不受函式外宣告的影響。
把上面函式體內的宣告語句去掉,輸出情況也就不一樣。
var devpoint = "out";
function hoistingVariable() {
if (!devpoint) {
devpoint = "in";
}
console.log(devpoint);
}
hoistingVariable();
console.log(devpoint);
// 下面是輸出結果
// out
// out
函式體內宣告語句去掉後,這是就需要去函式體外找宣告,根據這一條,函式外宣告並賦值了,函式體內的 if 語句就不會執行。
下面程式碼調整了賦值的順序,程式碼如下:
var devpoint;
function hoistingVariable() {
if (!devpoint) {
devpoint = "in";
}
console.log(devpoint);
}
devpoint = "out";
hoistingVariable();
console.log(devpoint);
// 下面是輸出結果
// out
// out
根據上面說的,函式體內的變數是外部宣告的,但未賦值,函式是提升了,併為執行。在函式執行前賦值給devpoint
,再執行就變成了out
。
函式提升
上面介紹過,變數提升,同樣包括函式的宣告,不同方式的函式宣告,執行也有所不同。這種問題就是直接上程式碼。
function hoistingFun() {
hello();
function hello() {
console.log("hello");
}
}
hoistingFun();
// 下面是輸出結果
// hello
上面的程式碼能夠正常執行是因為函式宣告被提升,函式 hello
被提升到頂部,執行效果跟下面程式碼一致:
function hoistingFun() {
function hello() {
console.log("hello");
}
hello();
}
hoistingFun();
// 下面是輸出結果
// hello
如果在同一個作用域中對同一個函式進行宣告,後面的函式會覆蓋前面的函式宣告。
function hoistingFun() {
hello();
function hello() {
console.log("hello");
}
function hello() {
console.log("hello2");
}
}
hoistingFun();
// 下面是輸出結果
// hello2
兩個函式宣告都被提升了,按照宣告的順序,後面的宣告覆蓋前面的宣告。
函式宣告常見的方式有兩種,還有一種是匿名函式表示式宣告方式,這種方式可以視為是變數的宣告來處理,當作用域中有函式宣告和變數宣告時,函式宣告的優先順序最高,將上面的程式碼更改後,結果就不一樣了,如下:
function hoistingFun() {
hello();
function hello() {
console.log("hello");
}
var hello = function () {
console.log("hello2");
};
}
hoistingFun();
// 下面是輸出結果
// hello
上面的程式碼,編譯邏輯如下:
function hoistingFun() {
function hello() {
console.log("hello");
}
hello();
hello = function () {
console.log("hello2");
};
}
hoistingFun();
// 下面是輸出結果
// hello
接下來再來看下,外部使用變數宣告,函式體內使用函式宣告的示例:
var hello = 520;
function hoistingFun() {
console.log(hello);
hello = 521;
console.log(hello);
function hello() {
console.log("hello");
}
}
hoistingFun();
console.log(hello);
// 下面是輸出結果
// [Function: hello]
// 521
// 520
上面說過,在函式體內宣告過的變數或者函式,只作用於函式體內,受限於函式體內,不受外部宣告的影響,相當於函式體內作用域與外部隔離。上面程式碼的編譯後的邏輯如下:
var hello = 520;
function hoistingFun() {
function hello() {
console.log("hello");
}
console.log(hello);
hello = 521;
console.log(hello);
}
hoistingFun();
console.log(hello);
在變數宣告中,函式的優先權最高,永遠提升到作用域最頂部,然後才是函式表示式和變數的執行順序。
來看下面的程式碼:
var hello = 520;
function hello() {
console.log("hello");
}
console.log(hello);
// 下面是輸出結果
// 520
根據函式宣告優先順序最高的原則,上面程式碼的執行邏輯如下:
function hello() {
console.log("hello");
}
hello = 520;
console.log(hello);
為什麼要提升?
至於為什麼要提升,這裡不做詳細介紹,提供一些參考文章,有興趣的可以去查閱
最佳實踐
現代JavaScript中,已經有很多方式避免變數提升帶來的問題,使用let
、const
替代var
,使用eslint
等工具避免變數重複定義,在一些前端開發團隊中,可以針對團隊做一些規範化的腳手架,如專案初始化強制專案的目錄、eslint的最佳配置等,用程式規範的過程比人督促要靠譜。
下面的程式碼可以看到const 和 var 宣告的變數的區別,const 宣告的變數不會提升,具體的區別可以查閱《細說javascript中變數宣告var、let、const的區別》
console.log("1a", myTitle1);
if (1) {
console.log("1b", myTitle1);
var myTitle1 = "devpoint";
}
if (1) { // 這裡的程式碼是有錯誤無法執行
console.log("3c", myTitle2);
const myTitle2 = "devpoint";
}
// 下面是輸出結果
// 1a undefined
// 1b undefined
總結
通過自我學習變數函式提升,加深了對其理解,對於前端面試所涉及的類似問題可以自信的給出答案,算是一種收穫。