摘要:所以形式參數(shù)是本地的,不是外部的或者全局的。這叫做函數(shù)聲明,函數(shù)聲明會(huì)連通命名和函數(shù)體一起被提升至作用域頂部。這叫做函數(shù)表達(dá)式,函數(shù)表達(dá)式只有命名會(huì)被提升,定義的函數(shù)體則不會(huì)。
Scoping & Hoisting
var a = 1; function foo() { if (!a) { var a = 2; } alert(a); }; foo();
上面這段代碼在運(yùn)行時(shí)會(huì)產(chǎn)生什么結(jié)果?
盡管對(duì)于有經(jīng)驗(yàn)的程序員來說這只是小菜一碟,不過我還是順著初學(xué)者常見的思路做一番描述:
創(chuàng)建了全局變量 a,定義其值為 1
創(chuàng)建了函數(shù) foo
在 foo 的函數(shù)體內(nèi),if 語句將不會(huì)執(zhí)行,因?yàn)?!a 會(huì)將變量 a 轉(zhuǎn)變成布爾的假值,也就是 false
跳過條件分支,alert 變量 a,最終的結(jié)果應(yīng)該是輸出 1
嗯,看起來無懈可擊的推理啊,但讓人驚訝的是:答案竟然是 2!為什么?
別著急,我會(huì)解釋給你聽。首先我要告訴你這不是什么錯(cuò)誤,而是 JavaScript 語言解釋器的一個(gè)(非官方的)特性,某人(Ben Cherry)把這個(gè)特性叫做:Hoisting(目前尚未有標(biāo)準(zhǔn)的翻譯,比較常見的是提升)。
聲明與定義為了理解 Hoisting,我們先來看一個(gè)簡(jiǎn)單的情況:
var a = 1;
你是否想過,上面這句代碼在運(yùn)行的時(shí)候到底發(fā)生了什么?
你是否知道,就這句代碼而言,“聲明變量 a” 和 “定義變量 a”這兩個(gè)說法哪一個(gè)才是正確的?
下例叫做 “聲明變量”:
var a;
下例叫做 “定義變量”:
var a = 1;
聲明:是指你聲稱某樣?xùn)|西的存在,比如一個(gè)變量或一個(gè)函數(shù);但你沒有說明這樣?xùn)|西到底是什么,僅僅是告訴解釋器這樣?xùn)|西存在而已;
定義:是指你指明了某樣?xùn)|西的具體實(shí)現(xiàn),比如一個(gè)變量的值是多少,一個(gè)函數(shù)的函數(shù)體是什么,確切的表達(dá)了這樣?xùn)|西的意義。
總結(jié)一下:
var a; // 這是聲明 a = 1; // 這是定義(賦值) var a = 1; // 合二為一:聲明變量的存在并賦值給它
這和 Hoisting 有何關(guān)系?重點(diǎn)來了:當(dāng)你以為你只做了一件事情的時(shí)候(var a = 1),實(shí)際上解釋器把這件事情分解成了兩個(gè)步驟,一個(gè)是聲明(var a),另一個(gè)是定義(a = 1)。
回到最開始的那個(gè)令人困惑的例子,我告訴你解釋器是如何分析你的代碼的:
var a; a = 1; function foo() { var a; // 關(guān)鍵在這里 if (!a) { a = 2; } alert(a); // 此時(shí)的 a 并非函數(shù)體外的那個(gè)全局變量 }
如代碼所示,在進(jìn)入函數(shù)體后解釋器聲明了新的變量 a,當(dāng)時(shí)其值為 undefined,于是 if 語句條件判斷結(jié)果為真,接著為新的變量 a 賦值為 2。你若不相信可以在函數(shù)體外面 alert(a),然后再執(zhí)行 foo() 對(duì)比一下結(jié)果就知道了。
Scoping(作用域)有人可能會(huì)問了:“為什么不是在 if 語句內(nèi)聲明變量 a?”
因?yàn)?JavaScript 沒有塊級(jí)作用域(Block Scoping),只有函數(shù)作用域(Function Scoping),所以說不是看見一對(duì)花括號(hào) {} 就代表產(chǎn)生了新的作用域,和 C 不一樣!
當(dāng)解析器讀到 if 語句的時(shí)候,它發(fā)現(xiàn)此處有一個(gè)變量聲明和賦值,于是解析器會(huì)將其聲明提升至當(dāng)前作用域的頂部(這是默認(rèn)行為,并且無法更改),這個(gè)行為就叫做 Hoisting。
OK,大家都懂了,你懂了嗎……
懂了不代表就會(huì)用了,就拿最開始的例子來說,如果我就是想要 alert(a) 出那個(gè) 1 可咋整呢?
創(chuàng)建新的作用域alert(a) 在執(zhí)行的時(shí)候,會(huì)去尋找變量 a 的位置,它從當(dāng)前作用域開始向上(或者說向外)一直查找到頂層作用域?yàn)橹?,若是找不到就?bào) undefined。
因?yàn)樵?alert(a) 的同級(jí)作用域里,我們?cè)俅温暶髁吮镜刈兞?a,所以它報(bào) 2;所以我們可以把本地變量 a 的聲明向下(或者說向內(nèi))移動(dòng),這樣 alert(a) 就找不到它了。
記?。篔avaScript 只有函數(shù)作用域!
var a = 1; function foo() { if (!a) { (function() { // 這是上一篇說到過的 IIFE,它會(huì)創(chuàng)建一個(gè)新的函數(shù)作用域 var a = 2; // 并且該作用域在 foo() 的內(nèi)部,所以 alert 訪問不到 }()); // 不過這個(gè)作用域可以訪問上層作用域哦,這就叫:“閉包” }; alert(a); }; foo();
你或許在無數(shù)的 JavaScript 書籍和文章里讀到過:“請(qǐng)始終保持作用域內(nèi)所有變量的聲明放置在作用域的頂部”,現(xiàn)在你應(yīng)該明白為什么有此一說了吧?因?yàn)檫@樣可以避免 Hoisting 特性給你帶來的困擾(我不是很情愿這么說,因?yàn)?Hoisting 本身并沒有什么錯(cuò)),也可以很明確的告訴所有閱讀代碼的人(包括你自己)在當(dāng)前作用域內(nèi)有哪些變量可以訪問。但是,變量聲明的提升并非 Hoisting 的全部。在 JavaScript 中,有四種方式可以讓命名進(jìn)入到作用域中(按優(yōu)先級(jí)):
語言定義的命名:比如 this 或者 arguments,它們?cè)谒凶饔糜騼?nèi)都有效且優(yōu)先級(jí)最高,所以在任何地方你都不能把變量命名為 this 之類的,這樣是沒有意義的
形式參數(shù):函數(shù)定義時(shí)聲明的形式參數(shù)會(huì)作為變量被 hoisting 至該函數(shù)的作用域內(nèi)。所以形式參數(shù)是本地的,不是外部的或者全局的。當(dāng)然你可以在執(zhí)行函數(shù)的時(shí)候把外部變量傳進(jìn)來,但是傳進(jìn)來之后就是本地的了
函數(shù)聲明:函數(shù)體內(nèi)部還可以聲明函數(shù),不過它們也都是本地的了
變量聲明:這個(gè)優(yōu)先級(jí)其實(shí)還是最低的,不過它們也都是最常用的
另外,還記得之前我們討論過 聲明 和 定義 的區(qū)別吧?當(dāng)時(shí)我并沒有說為什么要理解這個(gè)區(qū)別,不過現(xiàn)在是時(shí)候了,記?。?/p>
Hosting 只提升了命名,沒有提升定義
這一點(diǎn)和我們接下來要講到的東西息息相關(guān),請(qǐng)看:
函數(shù)聲明與函數(shù)表達(dá)式的差別先看兩個(gè)例子:
function test() { foo(); function foo() { alert("我是會(huì)出現(xiàn)的啦……"); } } test();
function test() { foo(); var foo = function() { alert("我不會(huì)出現(xiàn)的哦……"); } } test();
同學(xué),在了解了 Scoping & Hoisting 之后,你知道怎么解釋這一切了吧?
在第一個(gè)例子里,函數(shù) foo 是一個(gè)聲明,既然是聲明就會(huì)被提升(我特意包裹了一個(gè)外層作用域,因?yàn)槿肿饔糜蛐枰愕南胂?,不是那么直觀,但是道理是一樣的),所以在執(zhí)行 foo() 之前,作用域就知道函數(shù) foo 的存在了。這叫做函數(shù)聲明(Function Declaration),函數(shù)聲明會(huì)連通命名和函數(shù)體一起被提升至作用域頂部。
然而在第二個(gè)例子里,被提升的僅僅是變量名 foo,至于它的定義依然停留在原處。因此在執(zhí)行 foo() 之前,作用域只知道 foo 的命名,不知道它到底是什么,所以執(zhí)行會(huì)報(bào)錯(cuò)(通常會(huì)是:undefined is not a function)。這叫做函數(shù)表達(dá)式(Function Expression),函數(shù)表達(dá)式只有命名會(huì)被提升,定義的函數(shù)體則不會(huì)。
尾記:Ben Cherry 的原文解釋的更加詳細(xì),只不過是英文而已。我這篇是借花獻(xiàn)佛,主要是更淺顯的解釋給初學(xué)者聽,若要看更多的示例,請(qǐng)移步原作,謝謝。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/77967.html
摘要:講作用域鏈?zhǔn)紫纫獜淖饔糜蛑v起,下面是百度百科里對(duì)作用域的定義作用域在許多程序設(shè)計(jì)語言中非常重要。原文出處談?wù)務(wù)Z法里一些難點(diǎn)問題二 3) 作用域鏈相關(guān)的問題 作用域鏈?zhǔn)莏avascript語言里非常紅的概念,很多學(xué)習(xí)和使用javascript語言的程序員都知道作用域鏈?zhǔn)抢斫鈐avascript里很重要的一些概念的關(guān)鍵,這些概念包括this指針,閉包等等,它非常紅的另一個(gè)重要原因就...
摘要:并沒有類繼承模型,而是使用原型對(duì)象進(jìn)行原型式繼承。我們舉例說明原型鏈查找機(jī)制當(dāng)訪問一個(gè)對(duì)象的屬性時(shí),會(huì)從對(duì)象本身開始往上遍歷整個(gè)原型鏈,直到找到對(duì)應(yīng)屬性為止。原始類型有以下五種型。此外,試圖查找一個(gè)不存在屬性時(shí)將會(huì)遍歷整個(gè)原型鏈。 Javascript 并沒有類繼承模型,而是使用原型對(duì)象 prototype 進(jìn)行原型式繼承。 盡管人們經(jīng)常將此看做是 Javascript 的一個(gè)缺點(diǎn),然...
摘要:原型的概念不同于構(gòu)造函數(shù)模式創(chuàng)建對(duì)象只能單級(jí)即成,得益于原型鏈的概念,原型模式可實(shí)現(xiàn)類似其他語言的多級(jí)繼承。組合使用構(gòu)造函數(shù)與原型模式單一使用原型對(duì)象的問題在于所有屬性皆共享,若不想共享某屬性,則可放入構(gòu)造函數(shù)中。 之前提到,構(gòu)造函數(shù)方法創(chuàng)建對(duì)象存在著方法不共享的問題,因此引申出了原型模式創(chuàng)建對(duì)象 原型模式 原型模式旨在創(chuàng)建一個(gè)模版對(duì)象,該對(duì)象的所有屬性和方法被其實(shí)例所共享。 原型的概...
摘要:有這樣一個(gè)熱門問題其實(shí)這個(gè)問題很好理解,關(guān)鍵要弄清下面兩個(gè)知識(shí)點(diǎn)引擎對(duì)賦值表達(dá)式的處理過程賦值運(yùn)算的右結(jié)合性一賦值表達(dá)式形如的表達(dá)式稱為賦值表達(dá)式。賦值表達(dá)式是右結(jié)合的。 有這樣一個(gè)熱門問題: var a = {n: 1}; var b = a; a.x = a = {n: 2}; alert(a.x); // --> undefined alert(b.x); // --> {n: ...
摘要:心血來潮地跑到做了個(gè),結(jié)果當(dāng)然是慘不忍睹,發(fā)現(xiàn)自己對(duì)不少的基礎(chǔ)知識(shí)的理解是模模糊糊,甚至是錯(cuò)的。讀到又琢磨了一會(huì)兒,才搞明白為啥補(bǔ)碼這么反直覺。二補(bǔ)碼只能腦補(bǔ),或者用代碼打印腦補(bǔ)的內(nèi)容。寫了半天,一言以蔽之相當(dāng)于調(diào)用參考鏈接 心血來潮地跑到Upworks做了個(gè)JavaScript Test,結(jié)果當(dāng)然是慘不忍睹,發(fā)現(xiàn)自己對(duì)不少JavaScript的基礎(chǔ)知識(shí)的理解是模模糊糊,甚至是錯(cuò)的。 ...
閱讀 2551·2021-10-08 10:17
閱讀 1904·2021-09-06 15:02
閱讀 2596·2019-08-29 17:30
閱讀 2721·2019-08-29 13:24
閱讀 1580·2019-08-29 11:12
閱讀 3410·2019-08-28 17:52
閱讀 718·2019-08-26 11:30
閱讀 3656·2019-08-26 11:01