JavaScript 中的 hoisting 到底是甚麼 ?

  • 前言
  • 正文
    • 到底什麼是 hoisting?
    • let const 與 hoisting
    • 為什麼要有 hoisting?
    • hoisting 到底是怎麼運作的?
  • 結語

前言

寫這篇最大的一個原因在於,有一次聽到幾位大佬們在談關於 JS 中的 hoisting,也就是狀態提升,本人實在難以聽懂他們發表的看法,因此就開始上網搜搜相關文章。結果發現,這貌似不是一個一時片刻可以弄清楚的東西,於是著手開始這篇博客,希望能再攻破 JS 的又一個知識點。這篇文章不會太短,且字會較多,但還是希望大家賞個臉,看看小白我的理解吧!

正文

到底什麼是 hoisting?

廢話不多說,直接先看個例子。如果今天嘗試對一個尚未宣告的變數取值,會發生什麼事呢?

console.log(a)
// ReferenceError: a is not defined

我們發現,瀏覽器會提示你一個引用錯誤,跟你說 a 尚未定義。但是如果這樣呢?

console.log(a) // undefined
var a

怎麼會這樣?照理來說,代碼一行行運行,報錯應該是 ReferenceError 啊?為什麼是 undefined?

這就是因為 JS 的 hoisting,狀態提升。提升體現在於,var a 這一行被「提升」到了上面。所以其實 JS 引擎在解析這段代碼可以想像成這樣:

var a
console.log(a) // undefined

但是這只是想像,並不是說 JS 引擎真的搬動了代碼!

那如果我再改下代碼成這樣呢?

console.log(a) // undefined
var a = 10

一樣是 undefined,那為什麼要提呢?因為這邊要引入另一個觀念: 只有變數的宣告會提升,賦值不會。

所以上面的代碼可以想像成這樣:

var a
console.log(a) // undefined
a = 10

所以其實可以將 var a = 10分解成兩個步驟,首先,會先提升 var = a,也就是宣告部分,而第二步 a = 10 則留下來不會參與 hoisting 的過程。

目前來說,一切都還好理解,但接下來可能就會開始混亂了。看看下面這個例子:

function func(h) {console.log(h)var h = 3
}func(10)

本人一開始覺得可笑,有甚麼好說的,不就是這樣?

function func(h) {var hconsole.log(h)h = 3
}func(10)

所以輸出當然會是 undefined 啊!結果馬上打臉,輸出了 10。

其實轉換提升的過程都對了,只是忘記了呼叫函式這步。所以其實是這樣:

function func(h) {var h = 10var hconsole.log(h)h = 3
}func(10)

但還是有點奇怪,因為即使是這樣,但在取值 h 之前還是又 var h 了一次,但沒有賦值阿?還是應該是 undefined 吧?

那直接來試試看這個更直接的例子:

var a = 10
var a
console.log(a) // 10

結果發現,輸出是 10 歐。再利用上面分解的方法,其實可以看成這樣:

var a
var a
a = 10
console.log(a) // 10

這樣輸出 10,就還可以接受。當時我的感覺就是,到底什麼鬼東西,哪裡來的這麼多沒來由的規則?這樣誰搞的清楚?再忍忍,看看最後一個例子吧:

console.log(a)
var a
function a() {}

按照上面的分解方法,想要分解成這樣:

var a
console.log(a)
function a() {}

應該會輸出 undefined 吧?結果再次打臉,輸出 [Function: a]。這時我已經槁木死灰了。原來除了變數宣告賦值有狀態提升的概念,函數宣告也有。而且,函數宣告的狀態提升的匹配優先級還比變數宣告還高級,所以呢,其實上面的代碼應該要這樣想像:

// 優先匹配
function a() {}
var a
console.log(a)

講了這麼多,先小小整理一下:

  1. 變數宣告跟函式宣告都會提升
  2. 變數只有宣告會提升,賦值不會提升
  3. 別忘了函式裡面還有傳進來的參數

let const 與 hoisting

有沒有注意到,上面在介紹 hoisting 的概念時都是用 var 這個關鍵字在宣告變數,但是,其實大家都知道 ES6 推出了 let 跟 const,且目前主流都不再推薦使用 var,改為使用 let 跟 const。對於 let 跟 const,其實對於 hoisting 這個概念差不多,在這邊拿 let 看看些例子。

console.log(a)
let a
// ReferenceError: a is not defined

神奇的事情發生了!這樣子輸出竟是 ReferenceError: a is not defined。不過,其實這樣比較符合常理吧?那是不是說用 let 宣告函數就都沒有 hoisting 這回事了?如果是這樣就太好了,可惜,並不是的。看看下面例子:

var a = 10
function func() {console.log(a)let a
}

如果說 let 都沒有狀態提升,那這個輸出應該是 10 吧?因為外面有 var a = 10,然後 let a 也沒有提升。再次再次被打臉,輸出是 ReferenceError: a is not defined。所以說其實 let 也是有提升的,只是可能 let 提升的過程行為跟 var 不太一樣,所以乍看下像是沒有提升。至於到底怎麼運作,回頭再來討論。

在這邊先暫停一下。如果只是想要稍微了解下 hoisting 大概是個什麼東西的人,其實到這邊就可以停了,因為其實好好使用 let const,然後好好聲明賦值,其實也不見得要了解太多,也不會遇到甚麼問題。但如果想要了解得更透徹,那就跟著我堅持下,繼續看下去。接下來,我們先討論兩個關於 hoisting 重要的問題。

  1. 為什麼要有 hoisting?
  2. hoisting 到底是怎麼運作的?

為什麼要有 hoisting?

回顧上面所說的有關 hoisting 的一些規則和概念,我們可以感覺到一些它帶來的好處。回答這個問題,可以從反面來思考:「如果沒有 hoisting 會怎樣?」

  1. 沒有 hoisting 的話,我們就必須在使用某個變數前,一定宣告這個變數。但這其實很好啊,畢竟大家編程時就是這麼寫的吧,你不會因為想到 JS 有 hoisting 的機制,所以就不宣告變量就直接使用吧?
  2. 沒有 hoisting 的話, 那就也規定說,我們在使用一個函式時,它一定要在上面被宣告定義過。乍看下來好像也沒什麼毛病,但其實是有點麻煩的。因為,這就意味著,除非把每一個函式都放在最上面,才能完全確保下面呼叫任何函式都可以正常執行。
  3. 最後一個點較為有趣。沒有 hoisting 的話,那我們就不能在不同函式之間呼叫對方了。這什麼意思?看看下面的代碼:
function loop_1() {console.log('loop 1')loop_2()
}function loop_2() {console.log('loop 2')loop_1()
}

這段代碼不難理解,反正就是 loop_1 跟 loop_2 相互呼叫。但是有個問題,沒有 hoisting 的話,怎麼可能 loop_1 在 loop_2 上面,而同時,loop_2 也在 loop_1 上面。沒有 hoisting 的話,這段代碼不可能可行。

所以,hoisting 就是為了要解決這些問題的!

hoisting 到底是怎麼運作的?

首先,要先介紹一個概念,那就是 JavaScript 的 Execution Context,以下用 EC 做簡稱。EC 的概念是每次進入一個函式,這個函式就會有一個 EC,然後會將這個 EC 壓到棧中,當函數執行完畢,該 EC 就會被 pop 出來。

網上找到了一個很清楚的示意圖:

注意,最下面還有一個全局的 EC (粉色部分)。

總的來說,EC 就是存著各自函數的信息,當函數需要什麼東西,就是去自己的那個 EC 找。

有了 EC 的概念後,進入重點。每個 EC 都有相對應的 VO(Variable Object)。這個 VO 就是存儲所有信息的東西,包含該函數裡的變量,函數,還有函數裡面的參數。查找 VO 的機制就是說,以上面 var a = 10 為例,第一步就是先去 VO 裡新增一個屬性 a,再來再找到名為 a 的屬性,並設定成 10。

Step1: var a
Step2: a = 10

那一個函數裡面那麼多東西,他是怎麼放進每個 EC 的 VO 呢?規則就是,對於參數,它會直接被放到 VO 裡面去,如果有些參數沒有值的話,那它的值會被初始化成 undefined。看下面這個例子:

function func(a, b, c) {......
}func(10)

上面這個函數以及呼叫,它的 VO 會是下面這個樣子:

// VO
{a: 10,b: undefined,c: undefined
}

對於函數裡面如果又有函數宣告,一樣也是加入到 VO 哩,沒什麼問題。但是萬一函數的名字,跟某一個變量名重名了呢?像下面這樣:

function func(a) {function a() {......}
}func(10)

上面這個函數以及呼叫,它的 VO 會是下面這個樣子:

// VO
{a: function a
}

所以可以知道,函數宣告會優先於變數宣告,就像上面例子一樣,參數 a 被 函數 a 覆蓋掉了。

對於函數內部的變數宣告則會最後放進 VO,如果 VO 裡已經有重名的屬性了的話,直接忽略這個變量,原有的值也不會被改變。

概括一下,我們可以把上面所提到的 VO 的動作想成是要執行一個函數以前的前置作業。順序如下:

Step1: 把參數放進 VO,然後看看有無傳入參數。按照參數聲明次序依序匹配,如果沒有被匹配到,會被賦值為 undefined。
Step2: 去找函數裡面的成員方法,也就是其他函數,然後把它也放進 VO,如果跟當前 VO 中的任一屬性重名,就把舊的覆蓋掉。
Step3: 最後才去找函數裡面的變量宣告,並放進 VO 裡。如果跟當前 VO 中的任一屬性重名,會以當前狀態為主。

講了這麼多,回來看看上面一個我們提到的例子:

function func(h) {console.log(h)var h = 3
}func(10)

所以每個函數的執行其實可以分成兩個階段。首先,會先進入該函數的 Execution Context,接著要開始準備自己的 VO。對於上面的例子,首先因為呼叫時有傳參,因此會先在 VO 裡宣告一個叫做 h 的變量,且賦值為 10。接著因為函數裡沒有找到成員函數,所以不變。最後找到 var h = 3,它是一個變量聲明的語句,所以要把它加入到 VO 哩,但是因為此時的 VO 已經以一個叫做 h 的變量了,所以 VO 不變。至此,這個函數的 VO 都建立完成了。

// VO
{h: 3
}

建立完 VO 後,就開始執行這個函數。運行到 console.log(h) 時,查找 VO 發現有一個叫做 h 的變量,且值為 10,所以輸出了 10。所以上面的問題得到了解答,確實是輸出 10 沒錯!

如果把代碼改成這樣呢?

function func(h) {console.log(h)var h = 3console.log(h)
}func(10)

那麼第一次輸出將會是 10,而第二次輸出會是 3。其實建立 VO 的過程也都跟上面一樣,所以執行時第一次輸出確實是 10 沒問題。而因為執行到第 3 行時,它又去更改了 VO 裡的 h,因此第二次輸出就當然會是 3 啦!

結語

終於寫完了這篇博客,其實心裡還是小有成就感的。說真的,搞懂 JS 裡的這個 hoisting(狀態提升) 似乎不會對編程有太大的幫助,但想著以後如果面試被問到了,應該能說出點東西挺好的,畢竟感覺不是大家都有搞明白這個知識點。在這邊要感謝 GitHub 上的這篇博文章 我知道你懂 hoisting,可是你了解到多深?。本人也是參考這篇理解後,才完成了自己的博客,如果想要瞭解更深,歡迎前往閱讀。對於想要了解 hoisting(狀態提升) 的人們,希望看完這篇能有所收穫,也歡迎大神們多多指教啦!

JavaScript 中的 hoisting 到底是甚麼 ?相关推荐

  1. should, could, would, will, be going to, may, might到底有甚麼不同,又該怎麼用?

    转:http://english.tw/space-7918-do-blog-id-6832.html 以下又是我在奇摩部落格裡的一篇老文章,講的是should, could, would, will ...

  2. JavaScript 中的 Hoisting (变量提升和函数声明提升)

    如何将 函数声明 / 变量 "移动" 到作用域的顶部. 术语 Hoisting(提升) 在很多 JavaScript 博文中被用来解释标识符的解析.其实 Hoisting(提升) ...

  3. 浅聊JavaScript中的Hoisting(变量提升)

    一直有写博客的想法但因为懒惰等各种情况没有付出实际行动,择日不如撞日,那就今天让我给大家简单归纳总结一下JavaScript中的Hoisting(变量提升)吧! 1.对于变量 //variablesc ...

  4. 【JS】Javascript中的this到底是什么

    JavaScript中的this是一个对于新手来说特别吓人并且不友好的概念.对于一个前端萌新来说,这个概念既模糊,又看不到它存在的意义.本文将深度解析JavaScript中的this,并且逐一分析他在 ...

  5. 如何延长作用域链_通过实例理解javaScript中的this到底是什么和它的词法作用域...

    最近,听到李笑来说,讲解编程的过程中,举例子很重要. 而且,我最近看的各种javaScript工具书中的例子,也都有点复杂. 所以啊,我试着举一些简单又直观的例子,与各位苦学javaScript的同学 ...

  6. javascript中的this到底是指什么(一)?

    写js也有两年多了,在平时工作中也经常会用到this关键字,但是仅局限于用它,如果要求我讲明白this到底是什么的话可能就有点懵逼了,相信大家在面试的时候面试官也经常会问你this是神马东东,为了避免 ...

  7. 科普向--详解JavaScript中的数据类型

    对于前端的小伙伴而言,JS的数据类型可谓是必懂的知识点.虽然这个知识点很是基础了,不过仍然有不少人会在这一块犯些小错误.比如网上流传的"JavaScriptS一切皆对象",其实是个 ...

  8. JavaScript中hoisting(悬置/置顶解析/预解析) 实例解释,全局对象,隐含的全局概念...

    JavaScript中hoisting(悬置/置顶解析/预解析) 实例解释,全局对象,隐含的全局概念 <html><body><script type="tex ...

  9. Javascript中的循环变量声明,到底应该放在哪儿?

    不放走任何一个细节.相信很多Javascript开发者都在声明循环变量时犹 豫过var i到底应该放在哪里:放在不同的位置会对程序的运行产生怎样的影响?哪一种方式符合Javascript的语言规范?哪 ...

最新文章

  1. 当程序出Bug时,程序员最喜欢说的30句话
  2. 柿子不能和什么同吃?柿子相克食物大盘点
  3. C#调用C++ memcpy实现各种参数类型的内存拷贝 VS marshal.copy的实现 效率对比
  4. 一套高可用、易伸缩、高并发的IM群聊架构方案设计实践
  5. [Usaco2008 Feb]Eating Together麻烦的聚餐
  6. arduino蓝牙通讯代码_蓝牙4.0模块 无线数据传输模块 无线蓝牙串口 Arduino
  7. php的declare,php 中的declare
  8. js 根据固定位置获取经纬度--腾讯地图
  9. 技术状态管理(四)-技术状态控制
  10. 软通动力华为外包_软通动力外包到百度?
  11. java宠物商店_Java如何实现宠物商店管理 Java实现宠物商店管理代码示例
  12. 电信光猫 TEWA 500AG 破解 超密 2020-3-21
  13. ARC093F - Dark Horse
  14. android surface 平板,Surface体验:完胜Android平板 有望替代iPad
  15. H5页面input输入框,在ios手机中被顶出页面解决方案
  16. 如何用laragon框架运行php文件
  17. java程序员培训学习需要多长时间
  18. CSS中 *{ }、*zoom,各种 * 代表的意思
  19. php竞赛,PHP实现炸金花游戏比赛
  20. 何山无石,何水无鱼,何女无夫,何子无父,何树无枝,何城无市

热门文章

  1. vue + Luckysheet 实现在线Excel表格操作
  2. 【771. 宝石与石头】
  3. R<每日一题>极速版
  4. Spring原理篇(2)--BeanPostProcessor or BeanDefinition or Aware or InitializingBean
  5. win10系统新电脑用VMware运行Ubuntu电脑就蓝屏死机
  6. 有刷与无刷电机的原理
  7. Swift : 逃逸闭包 和 @escaping 属性
  8. 计算机毕业设计Java学生校内兼职管理平台(源码+系统+mysql数据库+lw文档)
  9. python实现人脸识别代码_手把手教你用1行代码实现人脸识别——Python Face_recogni...
  10. javaweb JAVA JSP学校宿舍公寓管理系统(JSP宿舍管理系统)java寝室管理网站源码