首頁 > web前端 > js教程 > 主體

JavaScript的執行機制-變數提升(實例詳解)

WBOY
發布: 2022-03-11 10:58:14
轉載
1854 人瀏覽過

本篇文章為大家帶來了關於javascript變數提升的相關知識,什麼是變數提升,為什麼會有變數提升,下面一起來看吧,希望對大家有幫助。

JavaScript的執行機制-變數提升(實例詳解)

相關推薦:javascript學習教學

#咱們先看段落程式碼,你覺得下面這段程式碼輸出的結果是什麼?

showName()
console.log(myname)
var myname = '极客时间'
function showName() {
    console.log('函数showName被执行');
}
登入後複製
登入後複製

使用過 JavaScript 開發的程式設計師應該都知道,JavaScript 是依照順序執行的。若按照這個邏輯來理解的話,那麼:

  • 當執行到第1 行的時候,由於函數showName 還沒有定義,所以執行應該會報錯;
  • 同樣執行第2 行的時候,由於變數myname 也未定義,所以同樣也會報錯。

然而實際執行結果卻並非如此, 如下圖:
JavaScript的執行機制-變數提升(實例詳解)
第1 行輸出“函數showName 被執行”,第2 行輸出“undefined”,這和前面想像中的順序執行有點不一樣!

透過上面的執行結果,你應該已經知道函數或變數可以在定義之前使用,那麼如果使用沒有定義的變數或函數,JavaScript 程式碼還能繼續執行了嗎?為了驗證這一點,我們可以刪除第3 行變數myname 的定義,如下所示:

showName()
console.log(myname)
function showName() {
    console.log('函数showName被执行');
}
登入後複製

然後再次執行這段程式碼時,JavaScript 引擎就會報錯,結果如下:
JavaScript的執行機制-變數提升(實例詳解)
從上面兩段程式碼的執行結果來看,我們可以得到如下三個結論:

  • 在執行過程中,若使用了未宣告的變量,那麼JavaScript 執行會報錯。
  • 在一個變數定義之前使用它,不會出錯,但是該變數的值會為 undefined,而不是定義時的值。
  • 在一個函數定義之前使用它,不會出錯,且函數能正確執行。

第一個結論很好理解,因為變數沒有定義,這樣在執行 JavaScript 程式碼時,就找不到該變數,所以 JavaScript 會拋出錯誤。

但是對於第二個和第三個結論,就挺讓人費解的:

  • 變數和函數為什麼能在其定義之前使用?這似乎表明 JavaScript 程式碼並不是一行一行執行的。
  • 同樣的方式,變數和函數的處理結果為什麼不一樣?例如上面的執行結果,提前使用的 showName 函數能列印出來完整結果,但是提前使用的 myname 變數值卻是 undefined,而不是定義時使用的「極客時間」這個值。

變數提升(Hoisting)

要解釋這兩個問題,你就需要先了解下什麼是變數提升。

不過在介紹變數提升之前,我們先透過下面這段程式碼,來看看什麼是 JavaScript 中的宣告和賦值。

var myname = '极客时间'
登入後複製

這段程式碼你可以把它看成是兩行程式碼組成的:

var myname    //声明部分
myname = '极客时间'  //赋值部分
登入後複製

如下圖所示:
JavaScript的執行機制-變數提升(實例詳解)
上面是變數的宣告和賦值,那接下來我們再來看看函數的宣告和賦值,結合下面這段程式碼:

function foo(){
  console.log('foo')
}

var bar = function(){
  console.log('bar')
}
登入後複製

第一個函數foo 是一個完整的函數聲明,也就是說沒有涉及到賦值運算;第二個函數是先宣告變數bar,再把function(){console.log('bar')}賦值給bar。為了直覺理解,你可以參考下圖:
JavaScript的執行機制-變數提升(實例詳解)
好了,了解宣告和賦值運算,那接下來我們就可以聊聊什麼是變數提升了。

所謂的變數提升,是指在 JavaScript 程式碼執行過程中,JavaScript 引擎會將變數的宣告部分和函數的宣告部分提升到程式碼開頭的「行為」。變數被提升後,會為變數設定預設值,這個預設值就是我們熟悉的 undefined。

我們來模擬下實作:

/*
* 变量提升部分
*/// 把变量 myname提升到开头,// 同时给myname赋值为undefinedvar myname = undefined// 把函数showName提升到开头function showName() {
    console.log('showName被调用');}/*
 * 可执行代码部分
*/showName()console.log(myname)// 去掉var声明部分,保留赋值语句myname = '极客时间'
登入後複製

為了模擬變數提升的效果,我們對程式碼做了以下調整,如下圖:
JavaScript的執行機制-變數提升(實例詳解)
從圖中可以看出,原來的程式碼主要做了兩處調整:

  • 第一处是把声明的部分都提升到了代码开头,如变量 myname 和函数 showName,并给变量设置默认值 undefined;
  • 第二处是移除原本声明的变量和函数,如var myname = '极客时间’的语句,移除了 var 声明,整个移除 showName 的函数声明。

通过这两步,就可以实现变量提升的效果。你也可以执行这段模拟变量提升的代码,其输出结果和第一段代码应该是完全一样的。

通过这段模拟的变量提升代码,相信你已经明白了可以在定义之前使用变量或者函数的原因——函数和变量在执行之前都提升到了代码开头

JavaScript 代码的执行流程

从概念的字面意义上来看,“变量提升”意味着变量和函数的声明会在物理层面移动到代码的最前面,正如我们所模拟的那样。但,这并不准确。实际上变量和函数声明在代码里的位置是不会改变的,而且是在编译阶段被 JavaScript 引擎放入内存中。对,你没听错,一段 JavaScript 代码在执行之前需要被 JavaScript 引擎编译,编译完成之后,才会进入执行阶段。大致流程你可以参考下图:
JavaScript的執行機制-變數提升(實例詳解)

1. 编译阶段

那么编译阶段和变量提升存在什么关系呢?

为了搞清楚这个问题,我们还是回过头来看上面那段模拟变量提升的代码,为了方便介绍,可以把这段代码分成两部分。

第一部分:变量提升部分的代码。

var myname = undefined
function showName() {
    console.log('函数showName被执行');
}
登入後複製

第二部分:执行部分的代码。

showName()
console.log(myname)
myname = '极客时间'
登入後複製
登入後複製

下面我们就可以把 JavaScript 的执行流程细化,如下图所示:

JavaScript的執行機制-變數提升(實例詳解)
从上图可以看出,输入一段代码,经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码。

执行上下文是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等。

关于执行上下文的细节,我会在下一篇文章《08 | 调用栈:为什么 JavaScript 代码会出现栈溢出?》做详细介绍,现在你只需要知道,在执行上下文中存在一个变量环境的对象(Viriable Environment),该对象中保存了变量提升的内容,比如上面代码中的变量 myname 和函数 showName,都保存在该对象中。

你可以简单地把变量环境对象看成是如下结构:

VariableEnvironment:
     myname -> undefined, 
     showName ->function : {console.log(myname)
登入後複製

了解完变量环境对象的结构后,接下来,我们再结合下面这段代码来分析下是如何生成变量环境对象的。

showName()
console.log(myname)
var myname = '极客时间'
function showName() {
    console.log('函数showName被执行');
}
登入後複製
登入後複製

我们可以一行一行来分析上述代码:

  • 第 1 行和第 2 行,由于这两行代码不是声明操作,所以 JavaScript 引擎不会做任何处理;
  • 第 3 行,由于这行是经过 var 声明的,因此 JavaScript 引擎将在环境对象中创建一个名为 myname 的属性,并使用 undefined 对其初始化;
  • 第 4 行,JavaScript 引擎发现了一个通过 function 定义的函数,所以它将函数定义存储到堆 (HEAP)中,并在环境对象中创建一个 showName 的属性,然后将该属性值指向堆中函数的位置(不了解堆也没关系,JavaScript 的执行堆和执行栈我会在后续文章中介绍)。

这样就生成了变量环境对象。接下来 JavaScript 引擎会把声明以外的代码编译为字节码,至于字节码的细节,我也会在后面文章中做详细介绍,你可以类比如下的模拟代码:

showName()
console.log(myname)
myname = '极客时间'
登入後複製
登入後複製

好了,现在有了执行上下文和可执行代码了,那么接下来就到了执行阶段了。

2. 执行阶段

JavaScript 引擎开始执行“可执行代码”,按照顺序一行一行地执行。下面我们就来一行一行分析下这个执行过程:

  • 当执行到 showName 函数时,JavaScript 引擎便开始在变量环境对象中查找该函数,由于变量环境对象中存在该函数的引用,所以 JavaScript 引擎便开始执行该函数,并输出“函数 showName 被执行”结果。
  • 接下来打印“myname”信息,JavaScript 引擎继续在变量环境对象中查找该对象,由于变量环境存在 myname 变量,并且其值为 undefined,所以这时候就输出 undefined。
  • 接下来执行第 3 行,把“极客时间”赋给 myname 变量,赋值后变量环境中的 myname 属性值改变为“极客时间”,变量环境如下所示:
VariableEnvironment:
     myname -> "极客时间", 
     showName ->function : {console.log(myname)
登入後複製

好了,以上就是一段代码的编译和执行流程 。

代码中出现相同的变量或者函数怎么办?

现在你已经知道了,在执行一段 JavaScript 代码之前,会编译代码,并将代码中的函数和变量保存到执行上下文的变量环境中,那么如果代码中出现了重名的函数或者变量,JavaScript 引擎会如何处理?

我们先看下面这样一段代码:

function showName() {
    console.log('极客邦');
}
showName();
function showName() {
    console.log('极客时间');
}
showName();
登入後複製

在上面代码中,我们先定义了一个 showName 的函数,该函数打印出来“极客邦”;然后调用 showName,并定义了一个 showName 函数,这个 showName 函数打印出来的是“极客时间”;最后接着继续调用 showName。那么你能分析出来这两次调用打印出来的值是什么吗?

我们来分析下其完整执行流程:

  • 首先是编译阶段。遇到了第一个 showName 函数,会将该函数体存放到变量环境中。接下来是第二个 showName 函数,继续存放至变量环境中,但是变量环境中已经存在一个 showName 函数了,此时,第二个 showName 函数会将第一个 showName 函数覆盖掉。这样变量环境中就只存在第二个 showName 函数了。
  • 接下来是执行阶段。先执行第一个 showName 函数,但由于是从变量环境中查找 showName 函数,而变量环境中只保存了第二个 showName 函数,所以最终调用的是第二个函数,打印的内容是“极客时间”。第二次执行 showName 函数也是走同样的流程,所以输出的结果也是“极客时间”。

综上所述,一段代码如果定义了两个相同名字的函数,那么最终生效的是最后一个函数。

总结

好了,今天就到这里,下面我来简单总结下今天的主要内容:

  • JavaScript 代码执行过程中,需要先做变量提升,而之所以需要实现变量提升,是因为 JavaScript 代码在执行之前需要先编译。
  • 在编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为 undefined;在代码执行阶段,JavaScript 引擎会从变量环境中去查找自定义的变量和函数。
  • 如果在编译阶段,存在两个相同的函数,那么最终存放在变量环境中的是最后定义的那个,这是因为后定义的会覆盖掉之前定义的。

相关推荐:javascript学习教程

以上是JavaScript的執行機制-變數提升(實例詳解)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:csdn.net
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!