特別說明,為便于查閱,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS
在前一章中订雾,我介紹了編程的基本構(gòu)建塊兒,比如變量矛洞,循環(huán)洼哎,條件,和函數(shù)沼本。當(dāng)然噩峦,所有被展示的代碼都是JavaScript。但是在這一章中抽兆,為了作為一個(gè)JS開發(fā)者入門和進(jìn)階识补,我們想要特別集中于那些你需要知道的關(guān)于JavaScript的事情。
我們將在本章中介紹好幾個(gè)概念辫红,它們將會在后續(xù)的 YDKJS 叢書中全面地探索凭涂。你可以將這一章看作是這個(gè)系列的其他書目中將要詳細(xì)講解的話題的一個(gè)概覽祝辣。
特別是如果你剛剛接觸JavaScript,那么你應(yīng)當(dāng)希望花相當(dāng)一段時(shí)間來多次復(fù)習(xí)這里的概念和代碼示例导盅。任何好的基礎(chǔ)都是一磚一瓦積累起來的较幌,所以不要指望你會在第一遍通讀后就立即理解了全部內(nèi)容揍瑟。
你深入學(xué)習(xí)JavaScript的旅途從這里開始白翻。
注意: 正如我在第一章中說過的,在你通讀這一章的同時(shí)绢片,你絕對應(yīng)該親自嘗試這里所有的代碼滤馍。要注意的是,這里的有些代碼假定最新版本的JavaScript(通常稱為“ES6”底循,ECMAScript的第六個(gè)版本 —— ECMAScript是JS語言規(guī)范的官方名稱)中引入的功能是存在的巢株。如果你碰巧在使用一個(gè)老版本的,前ES6時(shí)代的瀏覽器熙涤,這些代碼可能不好用阁苞。應(yīng)當(dāng)使用一個(gè)更新版本的現(xiàn)代瀏覽器(比如Chrome,F(xiàn)irefox祠挫,或者IE)那槽。
值與類型
正如我們在第一章中宣稱的,JavaScript擁有帶類型的值等舔,沒有帶類型的變量骚灸。下面是可用的內(nèi)建類型:
string
number
boolean
-
null
和undefined
object
-
symbol
(ES6新增類型)
JavaScript提供了一個(gè)typeof
操作符,它可以檢查一個(gè)值并告訴你它的類型是什么:
var a;
typeof a; // "undefined"
a = "hello world";
typeof a; // "string"
a = 42;
typeof a; // "number"
a = true;
typeof a; // "boolean"
a = null;
typeof a; // "object" -- 奇怪的bug
a = undefined;
typeof a; // "undefined"
a = { b: "c" };
typeof a; // "object"
來自typeof
的返回值總是六個(gè)(ES6中是七個(gè)慌植! —— “symbol”類型)字符串值之一甚牲。也就是,typeof "abc"
返回"string"
蝶柿,不是string
丈钙。
注意在這個(gè)代碼段中變量a
是如何持有每種不同類型的值的,而且盡管表面上看起來很像交汤,但是typeof a
并不是在詢問“a
的類型”雏赦,而是“當(dāng)前a
中的值的類型”。在JavaScript中只有值擁有類型蜻展;變量只是這些值的簡單容器喉誊。
typeof null
是一個(gè)有趣的例子,因?yàn)楫?dāng)你期望它返回"null"
時(shí)纵顾,它錯(cuò)誤地返回了"object"
伍茄。
警告: 這是JS中一直存在的一個(gè)bug,但是看起來它永遠(yuǎn)都不會被修復(fù)了施逾。在網(wǎng)絡(luò)上有太多的代碼依存于這個(gè)bug敷矫,因此修復(fù)它將會導(dǎo)致更多的bug例获!
另外,注意a = undefined
曹仗。我們明確地將a
設(shè)置為值undefined
榨汤,但是在行為上這與一個(gè)還沒有被設(shè)定值的變量沒有區(qū)別,比如在這個(gè)代碼段頂部的var a;
怎茫。一個(gè)變量可以用好幾種不同的方式得到這樣的“undefined”值狀態(tài)收壕,包括沒有返回值的函數(shù)和使用void
操作符。
對象
object
類型指的是一種復(fù)合值轨蛤,你可以在它上面設(shè)定屬性(帶名稱的位置)蜜宪,每個(gè)屬性持有各自的任意類型的值。它也許是JavaScript中最有用的類型之一祥山。
var obj = {
a: "hello world",
b: 42,
c: true
};
obj.a; // "hello world"
obj.b; // 42
obj.c; // true
obj["a"]; // "hello world"
obj["b"]; // 42
obj["c"]; // true
可視化地考慮這個(gè)obj
值可能會有所幫助:
![](fig4.png)
屬性既可以使用 點(diǎn)號標(biāo)記法(例如圃验,obj.a
) 訪問,也可以使用 方括號標(biāo)記法(例如缝呕,obj["a"]
) 訪問澳窑。點(diǎn)號標(biāo)記法更短而且一般來說更易于閱讀,因此在可能的情況下它都是首選供常。
如果你有一個(gè)名稱中含有特殊字符的屬性名稱摊聋,方括號標(biāo)記法就很有用,比如obj["hello world!"]
—— 當(dāng)通過方括號標(biāo)記法訪問時(shí)话侧,這樣的屬性經(jīng)常被稱為 鍵栗精。[ ]
標(biāo)記法要求一個(gè)變量(下一節(jié)講解)或者一個(gè)string
字面量(它需要包裝進(jìn)" .. "
或' .. '
)。
當(dāng)然瞻鹏,如果你想訪問一個(gè)屬性/鍵悲立,但是它的名稱被存儲在另一個(gè)變量中時(shí),方括號標(biāo)記法也很有用新博。例如:
var obj = {
a: "hello world",
b: 42
};
var b = "a";
obj[b]; // "hello world"
obj["b"]; // 42
注意: 更多關(guān)于JavaScript的object
的信息薪夕,請參見本系列的 this與對象原型,特別是第三章赫悄。
在JavaScript程序中有另外兩種你將會經(jīng)常打交道的值類型:數(shù)組 和 函數(shù)原献。但與其說它們是內(nèi)建類型,這些類型應(yīng)當(dāng)被認(rèn)為更像是子類型 —— object
類型的特化版本埂淮。
數(shù)組
一個(gè)數(shù)組是一個(gè)object
姑隅,它不使用特殊的帶名稱的屬性/鍵持有(任意類型的)值,而是使用數(shù)字索引的位置倔撞。例如:
var arr = [
"hello world",
42,
true
];
arr[0]; // "hello world"
arr[1]; // 42
arr[2]; // true
arr.length; // 3
typeof arr; // "object"
注意: 從零開始計(jì)數(shù)的語言讲仰,比如JS,在數(shù)組中使用0
作為第一個(gè)元素的索引痪蝇。
可視化地考慮arr
很能會有所幫助:
![](fig5.png)
因?yàn)閿?shù)組是一種特殊的對象(正如typeof
所暗示的)鄙陡,所以它們可以擁有屬性冕房,包括一個(gè)可以自動被更新的length
屬性。
理論上你可以使用你自己的命名屬性將一個(gè)數(shù)組用作一個(gè)普通對象趁矾,或者你可以使用一個(gè)object
但是給它類似于數(shù)組的數(shù)字屬性(0
耙册,1
,等等)毫捣。然而详拙,這么做一般被認(rèn)為是分別誤用了這兩種類型。
最好且最自然的方法是為數(shù)字定位的值使用數(shù)組培漏,而為命名屬性使用object
溪厘。
函數(shù)
另一個(gè)你將在JS程序中到處使用的object
子類型是函數(shù):
function foo() {
return 42;
}
foo.bar = "hello world";
typeof foo; // "function"
typeof foo(); // "number"
typeof foo.bar; // "string"
同樣地胡本,函數(shù)也是object
的子類型 —— typeof
返回"function"
牌柄,這暗示著"function"
是一種主要類型 —— 因此也可以擁有屬性,但是你一般僅會在有限情況下才使用函數(shù)對象屬性(比如foo.bar
)侧甫。
注意: 更多關(guān)于JS的值和它們的類型的信息珊佣,參見本系列的 類型與文法 的前兩章。
內(nèi)建類型的方法
我們剛剛討論的內(nèi)建類型和子類型擁有十分強(qiáng)大和有用的行為披粟,它們作為屬性和方法暴露出來咒锻。
例如:
var a = "hello world";
var b = 3.14159;
a.length; // 11
a.toUpperCase(); // "HELLO WORLD"
b.toFixed(4); // "3.1416"
使調(diào)用a.toUpperCase()
成為可能的原因,要比這個(gè)值上存在這個(gè)方法的說法復(fù)雜一些守屉。
簡而言之惑艇,有一個(gè)String
(S
大寫)對象包裝器形式,通常被稱為“原生類型”拇泛,與string
基本類型配成一對兒滨巴;正是這個(gè)對象包裝器的原型上定義了toUpperCase()
方法。
當(dāng)你通過引用一個(gè)屬性或方法(例如俺叭,前一個(gè)代碼段中的a.toUpperCase()
)將一個(gè)像"hello world"
這樣的基本類型值當(dāng)做一個(gè)object
來使用時(shí)恭取,JS自動地將這個(gè)值“封箱”為它對應(yīng)的對象包裝器(這個(gè)操作是隱藏在幕后的)。
一個(gè)string
值可以被包裝為一個(gè)String
對象熄守,一個(gè)number
可以被包裝為一個(gè)Number
對象蜈垮,而一個(gè)boolean
可以被包裝為一個(gè)Boolean
對象。在大多數(shù)情況下裕照,你不擔(dān)心或者直接使用這些值的對象包裝器形式 —— 在所有實(shí)際情況中首選基本類型值形式攒发,而JavaScript會幫你搞定剩下的一切。
注意: 關(guān)于JS原生類型和“封箱”的更多信息晋南,參見本系列的 類型與文法 的第三章惠猿。要更好地理解對象原型,參見本系列的 this與對象原型 的第五章搬俊。
值的比較
在你的JS程序中你將需要進(jìn)行兩種主要的值的比較:等價(jià) 和 不等價(jià)紊扬。任何比較的結(jié)果都是嚴(yán)格的boolean
值(true
或false
)蜒茄,無論被比較的值的類型是什么。
強(qiáng)制轉(zhuǎn)換
在第一章中我們簡單地談了一下強(qiáng)制轉(zhuǎn)換餐屎,我們在此回顧它檀葛。
在JavaScript中強(qiáng)制轉(zhuǎn)換有兩種形式:明確的 和 隱含的。明確的強(qiáng)制轉(zhuǎn)換比較簡單腹缩,因?yàn)槟憧梢栽诖a中明顯地看到一個(gè)類型轉(zhuǎn)換到另一個(gè)類型將會發(fā)生屿聋,而隱含的強(qiáng)制轉(zhuǎn)換更像是另外一些操作的不明顯的副作用引發(fā)的類型轉(zhuǎn)換。
你可能聽到過像“強(qiáng)制轉(zhuǎn)換是邪惡的”這樣情緒化的觀點(diǎn)藏鹊,這是因?yàn)橐粋€(gè)清楚的事實(shí) —— 強(qiáng)制轉(zhuǎn)換在某些地方會產(chǎn)生一些令人吃驚的結(jié)果润讥。也許沒有什么能比當(dāng)一個(gè)語言嚇到開發(fā)者時(shí)更能喚起他們的沮喪心情了。
強(qiáng)制轉(zhuǎn)換并不邪惡盘寡,它也不一定是令人吃驚的楚殿。事實(shí)上,你使用類型強(qiáng)制轉(zhuǎn)換構(gòu)建的絕大部分情況是十分合理和可理解的竿痰,而且它甚至可以用來 增強(qiáng) 你代碼的可讀性脆粥。但我們不會在這個(gè)話題上過度深入 —— 本系列的 類型與文法 的第四章將會進(jìn)行全面講解。
這是一個(gè) 明確 強(qiáng)制轉(zhuǎn)換的例子:
var a = "42";
var b = Number( a );
a; // "42"
b; // 42 -- 數(shù)字影涉!
而這是一個(gè) 隱含 強(qiáng)制轉(zhuǎn)換的例子:
var a = "42";
var b = a * 1; // 這里 "42" 被隱含地強(qiáng)制轉(zhuǎn)換為 42
a; // "42"
b; // 42 -- 數(shù)字变隔!
Truthy 與 Falsy
在第一章中,我們簡要地提到了值的“truthy”和“falsy”性質(zhì):當(dāng)一個(gè)非boolean
值被強(qiáng)制轉(zhuǎn)換為一個(gè)boolean
時(shí)蟹倾,它是變成true
還是false
匣缘。
在JavaScript中“falsy”的明確列表如下:
-
""
(空字符串) -
0
,-0
,NaN
(非法的number
) -
null
,undefined
false
任何不在這個(gè)“falsy”列表中的值都是“truthy”。這是其中的一些例子:
"hello"
42
true
-
[ ]
,[ 1, "2", 3 ]
(數(shù)組) -
{ }
,{ a: 42 }
(對象) -
function foo() { .. }
(函數(shù))
重要的是要記住鲜棠,一個(gè)非boolean
值僅在實(shí)際上被強(qiáng)制轉(zhuǎn)換為一個(gè)boolean
時(shí)才遵循這個(gè)“truthy”/“falsy”強(qiáng)制轉(zhuǎn)換肌厨。把你搞糊涂并不困難 —— 當(dāng)一個(gè)場景看起來像是將一個(gè)值強(qiáng)制轉(zhuǎn)換為boolean
,可其實(shí)它不是岔留。
等價(jià)性
有四種等價(jià)性操作符:==
夏哭,===
,!=
献联,和!==
竖配。!
形式當(dāng)然是與它們相對應(yīng)操作符平行的“不等”版本;不等(non-equality) 不應(yīng)當(dāng)與 不等價(jià)性(inequality) 相混淆里逆。
==
和===
之間的不同通常被描述為进胯,==
檢查值的等價(jià)性而===
檢查值和類型兩者的等價(jià)性。然而原押,這是不準(zhǔn)確的胁镐。描述它們的合理方式是,==
在允許強(qiáng)制轉(zhuǎn)換的條件下檢查值的等價(jià)性,而===
是在不允許強(qiáng)制轉(zhuǎn)換的條件下檢查值的等價(jià)性盯漂;因此===
常被稱為“嚴(yán)格等價(jià)”颇玷。
考慮這個(gè)隱含強(qiáng)制轉(zhuǎn)換,它在==
寬松等價(jià)性比較中允許就缆,而===
嚴(yán)格等價(jià)性比較中不允許:
var a = "42";
var b = 42;
a == b; // true
a === b; // false
在a == b
的比較中帖渠,JS注意到類型不匹配,于是它經(jīng)過一系列有順序的步驟將一個(gè)值或者它們兩者強(qiáng)制轉(zhuǎn)換為一個(gè)不同的類型竭宰,直到類型匹配為止空郊,然后就可以檢查一個(gè)簡單的值等價(jià)性。
如果你仔細(xì)想一想切揭,通過強(qiáng)制轉(zhuǎn)換a == b
可以有兩種方式給出true
狞甚。這個(gè)比較要么最終成為42 == 42
,要么成為"42" == "42"
廓旬。那么是哪一種呢哼审?
答案:"42"
變成42
,于是比較成為42 == 42
嗤谚。在一個(gè)這樣簡單的例子中棺蛛,只要最終結(jié)果是一樣的,處理的過程走哪一條路看起來并不重要巩步。但在一些更復(fù)雜的情況下,這不僅對比較的最終結(jié)果很重要桦踊,而且對你 如何 得到這個(gè)結(jié)果也很重要椅野。
a === b
產(chǎn)生false
,因?yàn)閺?qiáng)制轉(zhuǎn)換是不允許的籍胯,所以簡單值的比較很明顯將會失敗竟闪。許多開發(fā)者感覺===
更可靠,所以他們提倡一直使用這種形式而遠(yuǎn)離==
杖狼。我認(rèn)為這種觀點(diǎn)是非常短視的炼蛤。我相信==
是一種可以改進(jìn)程序的強(qiáng)大工具,如果你花時(shí)間去學(xué)習(xí)它的工作方式蝶涩。
我們不會詳細(xì)地講解強(qiáng)制轉(zhuǎn)換在==
比較中是如何工作的理朋。它的大部分都是相當(dāng)合理的,但是有一些重要的極端用例要小心绿聘。你可以閱讀ES5語言規(guī)范的11.9.3部分(http://www.ecma-international.org/ecma-262/5.1/)來了解確切的規(guī)則嗽上,而且與圍繞這種機(jī)制的所有負(fù)面炒作比起來,你會對這它是多么的直白而感到吃驚熄攘。
為了將這許多細(xì)節(jié)歸納為一個(gè)簡單的包裝,并幫助你在各種情況下判斷是否使用==
或===
,這是我的簡單規(guī)則:
- 如果一個(gè)比較的兩個(gè)值之一可能是
true
或false
值掩驱,避免==
而使用===
。 - 如果一個(gè)比較的兩個(gè)值之一可能是這些具體的值(
0
逐沙,""
,或[]
—— 空數(shù)組)洼畅,避免==
而使用===
酱吝。 - 在 所有 其他情況下,你使用
==
是安全的土思。它不僅安全务热,而且在許多情況下它可以簡化你的代碼并改善可讀性。
這些規(guī)則歸納出來的東西要求你嚴(yán)謹(jǐn)?shù)乜紤]你的代碼:什么樣的值可能通過這個(gè)被比較等價(jià)性的變量己儒。如果你可以確定這些值崎岂,那么==
就是安全的,使用它闪湾!如果你不能確定這些值冲甘,就使用===
。就這么簡單途样。
!=
不等價(jià)形式對應(yīng)于==
江醇,而!==
形式對應(yīng)于===
。我們剛剛討論的所有規(guī)則和注意點(diǎn)對這些非等價(jià)比較都是平行適用的何暇。
如果你在比較兩個(gè)非基本類型值陶夜,比如object
(包括function
和array
),那么你應(yīng)當(dāng)特別小心==
和===
的比較規(guī)則裆站。因?yàn)檫@些值實(shí)際上是通過引用持有的条辟,==
和===
比較都將簡單地檢查這個(gè)引用是否相同,而不是它們底層的值宏胯。
例如?羽嫡,array
默認(rèn)情況下會通過使用逗號(,
)連接所有值來被強(qiáng)制轉(zhuǎn)換為string
。你可能認(rèn)為兩個(gè)內(nèi)容相同的array
將是==
相等的肩袍,但它們不是:
var a = [1,2,3];
var b = [1,2,3];
var c = "1,2,3";
a == c; // true
b == c; // true
a == b; // false
注意: 更多關(guān)于==
等價(jià)性比較規(guī)則的信息杭棵,參見ES5語言規(guī)范(11.9.3部分),和本系列的 類型與文法 的第四章氛赐;更多關(guān)于值和引用的信息魂爪,參見它的第二章。
不等價(jià)性
<
鹰祸,>
甫窟,<=
,和>=
操作符用于不等價(jià)性比較蛙婴,在語言規(guī)范中被稱為“關(guān)系比較”粗井。一般來說它們將與number
這樣的可比較有序值一起使用。3 < 4
是很容易理解的。
但是JavaScriptstring
值也可進(jìn)行不等價(jià)性比較浇衬,它使用典型的字母順序規(guī)則("bar" < "foo"
)懒构。
那么強(qiáng)制轉(zhuǎn)換呢?與==
比較相似的規(guī)則(雖然不是完全相同T爬蕖)也適用于不等價(jià)操作符胆剧。要注意的是,沒有像===
嚴(yán)格等價(jià)操作符那樣不允許強(qiáng)制轉(zhuǎn)換的“嚴(yán)格不等價(jià)”操作符醉冤。
考慮如下代碼:
var a = 41;
var b = "42";
var c = "43";
a < b; // true
b < c; // true
這里發(fā)生了什么秩霍?在ES5語言規(guī)范的11.8.5部分中,它說如果<
比較的兩個(gè)值都是string
蚁阳,就像b < c
铃绒,那么這個(gè)比較將會以字典順序(也就是像字典中字母的排列順序)進(jìn)行。但如果兩個(gè)值之一不是string
螺捐,就像a < b
颠悬,那么兩個(gè)值就將被強(qiáng)制轉(zhuǎn)換成number
,并進(jìn)行一般的數(shù)字比較定血。
在可能不同類型的值之間進(jìn)行比較時(shí)赔癌,你可能遇到的最大的坑 —— 記住,沒有“嚴(yán)格不等價(jià)”可用 —— 是其中一個(gè)值不能轉(zhuǎn)換為合法的數(shù)字澜沟,例如:
var a = 42;
var b = "foo";
a < b; // false
a > b; // false
a == b; // false
等一下灾票,這三個(gè)比較怎么可能都是false
?因?yàn)樵?code><和>
的比較中倔喂,值b
被強(qiáng)制轉(zhuǎn)換為了“非法的數(shù)字值”铝条,而且語言規(guī)范說NaN
既不大于其他值,也不小于其他值席噩。
==
比較失敗于不同的原因。如果a == b
被解釋為42 == NaN
或者"42" == "foo"
都會失敗 —— 正如我們前面講過的贤壁,這里是前一種情況悼枢。
注意: 關(guān)于不等價(jià)比較規(guī)則的更多信息,參見ES5語言規(guī)范的11.8.5部分脾拆,和本系列的 類型與文法 第四章馒索。
變量
在JavaScript中,變量名(包括函數(shù)名)必須是合法的 標(biāo)識符(identifiers)名船。當(dāng)你考慮非傳統(tǒng)意義上的字符時(shí)绰上,比如Unicode,標(biāo)識符中合法字符的嚴(yán)格和完整的規(guī)則就有點(diǎn)兒復(fù)雜渠驼。如果你僅考慮典型的ASCII字母數(shù)字的字符蜈块,那么這個(gè)規(guī)則還是很簡單的。
一個(gè)標(biāo)識符必須以a
-z
,A
-Z
百揭,$
爽哎,或_
開頭。它可以包含任意這些字符外加數(shù)字0
-9
器一。
一般來說课锌,變量標(biāo)識符的規(guī)則也通用適用于屬性名稱。然而祈秕,有一些不能用作變量名渺贤,但是可以用作屬性名的單詞。這些單詞被稱為“保留字(reserved words)”请毛,包括JS關(guān)鍵字(for
志鞍,in
,if
获印,等等)和null
述雾,true
和false
。
注意: 更多關(guān)于保留字的信息兼丰,參見本系列的 類型與文法 的附錄A玻孟。
函數(shù)作用域
你使用var
關(guān)鍵字聲明的變量將屬于當(dāng)前的函數(shù)作用域,如果聲明位于任何函數(shù)外部的頂層鳍征,它就屬于全局作用域黍翎。
提升
無論var
出現(xiàn)在一個(gè)作用域內(nèi)部的何處,這個(gè)聲明都被認(rèn)為是屬于整個(gè)作用域艳丛,而且在作用域的所有位置都是可以訪問的匣掸。
這種行為稱為 提升,比喻一個(gè)var
聲明在概念上 被移動 到了包含它的作用域的頂端氮双。技術(shù)上講碰酝,這個(gè)過程通過代碼的編譯方式進(jìn)行解釋更準(zhǔn)確,但是我們先暫且跳過那些細(xì)節(jié)戴差。
考慮如下代碼:
var a = 2;
foo(); // 可以工作送爸, 因?yàn)?`foo()` 聲明被“提升”了
function foo() {
a = 3;
console.log( a ); // 3
var a; // 聲明被“提升”到了 `foo()` 的頂端
}
console.log( a ); // 2
警告: 在一個(gè)作用域中依靠變量提升來在var
聲明出現(xiàn)之前使用一個(gè)變量是不常見的,也不是個(gè)好主意暖释;它可能相當(dāng)使人困惑袭厂。而使用被提升的函數(shù)聲明要常見得多,也更為人所接受球匕,就像我們在foo()
正式聲明之前就調(diào)用它一樣纹磺。
嵌套的作用域
當(dāng)你聲明了一個(gè)變量時(shí),它就在這個(gè)作用域內(nèi)的任何地方都是可用的亮曹,包括任何下層/內(nèi)部作用域橄杨。例如:
function foo() {
var a = 1;
function bar() {
var b = 2;
function baz() {
var c = 3;
console.log( a, b, c ); // 1 2 3
}
baz();
console.log( a, b ); // 1 2
}
bar();
console.log( a ); // 1
}
foo();
注意c
在bar()
的內(nèi)部是不可用的秘症,因?yàn)樗莾H在內(nèi)部的baz()
作用域中被聲明的,并且b
因?yàn)橥瑯拥脑蛟?code>foo()內(nèi)是不可用的讥珍。
如果你試著在一個(gè)作用域內(nèi)訪問一個(gè)不可用的變量的值历极,你就會得到一個(gè)被拋出的ReferenceError
。如果你試著為一個(gè)還沒有被聲明的變量賦值衷佃,那么根據(jù)“strict模式”的狀態(tài)趟卸,你會要么得到一個(gè)在頂層全局作用域中創(chuàng)建的變量(不好!)氏义,要么得到一個(gè)錯(cuò)誤锄列。讓我們看一下:
function foo() {
a = 1; // `a` 沒有被正式聲明
}
foo();
a; // 1 -- 噢,自動全局變量 :(
這是一種非常差勁兒的做法惯悠。別這么干邻邮!總是給你的變量進(jìn)行正式聲明。
除了在函數(shù)級別為變量創(chuàng)建聲明克婶,ES6允許你使用let
關(guān)鍵字聲明屬于個(gè)別塊兒(一個(gè){ .. }
)的變量筒严。除了一些微妙的細(xì)節(jié),作用域規(guī)則將大致上與我們剛剛看到的函數(shù)相同:
function foo() {
var a = 1;
if (a >= 1) {
let b = 2;
while (b < 5) {
let c = b * 2;
b++;
console.log( a + c );
}
}
}
foo();
// 5 7 9
因?yàn)槭褂昧?code>let而非var
情萤,b
將僅屬于if
語句而不是整個(gè)foo()
函數(shù)的作用域鸭蛙。相似地,c
僅屬于while
循環(huán)筋岛。對于以更加細(xì)粒度的方式管理你的變量作用域來說娶视,塊兒作用域是非常有用的,它將使你的代碼隨著時(shí)間的推移更加易于維護(hù)睁宰。
注意: 關(guān)于作用域的更多信息肪获,參見本系列的 作用域與閉包。更多關(guān)于let
塊兒作用域的信息柒傻,參見本系列的 ES6與未來孝赫。
條件
除了我們在第一章中簡要介紹過的if
語句,JavaScript還提供了幾種其他值得我們一看的條件機(jī)制红符。
有時(shí)你可能發(fā)現(xiàn)自己在像這樣寫一系列的if..else..if
語句:
if (a == 2) {
// 做一些事情
}
else if (a == 10) {
// 做另一些事請
}
else if (a == 42) {
// 又是另外一些事情
}
else {
// 這里是備用方案
}
這種結(jié)構(gòu)好用寒锚,但有一點(diǎn)兒繁冗,因?yàn)槟阈枰獮槊恳环N情況都指明a
的測試违孝。這里有另一種選項(xiàng),switch
語句:
switch (a) {
case 2:
// 做一些事情
break;
case 10:
// 做另一些事請
break;
case 42:
// 又是另外一些事情
break;
default:
// 這里是備用方案
}
如果你想僅讓一個(gè)case
中的語句運(yùn)行泳赋,break
是很重要的雌桑。如果你在一個(gè)case
中省略了break
,并且這個(gè)case
成立或運(yùn)行祖今,那么程序的執(zhí)行將會不管下一個(gè)case
語句是否成立而繼續(xù)執(zhí)行它校坑。這種所謂的“掉落”有時(shí)是有用/期望的:
switch (a) {
case 2:
case 10:
// 一些很酷的事情
break;
case 42:
// 另一些事情
break;
default:
// 備用方案
}
這里拣技,如果a
是2
或10
,它就會執(zhí)行“一些很酷的事情”的代碼語句耍目。
在JavaScript中的另一種條件形式是“條件操作符”膏斤,經(jīng)常被稱為“三元操作符”。它像是一個(gè)單獨(dú)的if..else
語句的更簡潔的形式邪驮,比如:
var a = 42;
var b = (a > 41) ? "hello" : "world";
// 與此相似:
// if (a > 41) {
// b = "hello";
// }
// else {
// b = "world";
// }
如果測試表達(dá)式(這里是a > 41
)求值為true
莫辨,那么就會得到第一個(gè)子句("hello"
),否則得到第二個(gè)子句("world"
)毅访,而且無論結(jié)果為何都會被賦值給b
沮榜。
條件操作符不一定非要用于賦值,但是這絕對是最常見的用法喻粹。
注意: 關(guān)于測試條件和switch
與? :
的其他模式的更多信息蟆融,參見本系列的 類型與文法。
Strict模式
ES5在語言中加入了一個(gè)“strict模式”守呜,它收緊了一些特定行為的規(guī)則型酥。一般來說,這些限制被視為使代碼符合一組更安全和更合理的指導(dǎo)方針查乒。另外弥喉,堅(jiān)持strict模式一般會使你的代碼對引擎有更強(qiáng)的可優(yōu)化性。strict模式對代碼有很大的好處侣颂,你應(yīng)當(dāng)在你所有的程序中使用它档桃。
根據(jù)你擺放strict模式注解的位置,你可以為一個(gè)單獨(dú)的函數(shù)憔晒,或者是整個(gè)一個(gè)文件切換到strict模式:
function foo() {
"use strict";
// 這部分代碼是strict模式的
function bar() {
// 這部分代碼是strict模式的
}
}
// 這部分代碼不是strict模式的
將它與這個(gè)相比:
"use strict";
function foo() {
// 這部分代碼是strict模式的
function bar() {
// 這部分代碼是strict模式的
}
}
// 這部分代碼是strict模式的
使用strict模式的一個(gè)關(guān)鍵不同(改善T逡蕖)是,它不允許因?yàn)槭÷粤?code>var而進(jìn)行隱含的自動全局變量聲明:
function foo() {
"use strict"; // 打開strict模式
a = 1; // 缺少`var`拒担,ReferenceError
}
foo();
如果你在代碼中打開strict模式嘹屯,并且得到錯(cuò)誤,或者代碼開始變得有bug从撼,這可能會誘使你避免使用strict模式州弟。但是縱容這種直覺不是一個(gè)好主意。如果strict模式在你的程序中導(dǎo)致了問題低零,那么這標(biāo)志著在你的代碼中幾乎可以肯定有應(yīng)該修改的東西婆翔。
strict模式不僅將你的代碼保持在更安全的道路上,也不僅將使你的代碼可優(yōu)化性更強(qiáng)掏婶,它還代表著這種語言未來的方向啃奴。對于你來說,現(xiàn)在就開始習(xí)慣于strict模式要比一直回避它容易得多 —— 以后再進(jìn)行這種轉(zhuǎn)變只會更難雄妥!
注意: 關(guān)于strict模式的更多信息最蕾,參見本系列的 類型與文法 的第五章依溯。
函數(shù)作為值
至此,我們已經(jīng)將函數(shù)作為JavaScript中主要的 作用域 機(jī)制討論過了瘟则。你可以回想一下典型的function
聲明語法是這樣的:
function foo() {
// ..
}
雖然從這種語法中看起來不明顯黎炉,foo
基本上是一個(gè)位于外圍作用域的變量,它給了被聲明的function
一個(gè)引用醋拧。也就是說慷嗜,function
本身是一個(gè)值,就像42
或[1,2,3]
一樣趁仙。
這可能聽起來像是一個(gè)奇怪的概念洪添,所以花點(diǎn)兒時(shí)間仔細(xì)考慮一下。你不僅可以向一個(gè)function
傳遞一個(gè)值(參數(shù)值)雀费,而且 一個(gè)函數(shù)本身可以是一個(gè)值干奢,它能夠賦值給變量,傳遞給其他函數(shù)盏袄,或者從其它函數(shù)中返回忿峻。
因此,一個(gè)函數(shù)值應(yīng)當(dāng)被認(rèn)為是一個(gè)表達(dá)式辕羽,與任何其他的值或表達(dá)式很相似逛尚。
考慮如下代碼:
var foo = function() {
// ..
};
var x = function bar(){
// ..
};
第一個(gè)被賦值給變量foo
的函數(shù)表達(dá)式稱為 匿名 函數(shù)表達(dá)式,因?yàn)樗鼪]有“名稱”刁愿。
第二個(gè)函數(shù)表達(dá)式是 命名的(bar
)绰寞,它還被賦值給變量x
作為它的引用。命名函數(shù)表達(dá)式 一般來說更理想铣口,雖然 匿名函數(shù)表達(dá)式 仍然極其常見滤钱。
更多信息參見本系列的 作用域與閉包。
立即被調(diào)用的函數(shù)表達(dá)式(IIFE)
在前一個(gè)代碼段中脑题,哪一個(gè)函數(shù)表達(dá)式都沒有被執(zhí)行 —— 除非我們使用了foo()
或x()
件缸。
有另一種執(zhí)行函數(shù)表達(dá)式的方法,它通常被稱為一個(gè) 立即被調(diào)用的函數(shù)表達(dá)式 (IIFE):
(function IIFE(){
console.log( "Hello!" );
})();
// "Hello!"
圍繞在函數(shù)表達(dá)式(function IIFE(){ .. })
外部的( .. )
只是一個(gè)微妙的JS文法叔遂,我們需要它來防止函數(shù)表達(dá)式被看作一個(gè)普通的函數(shù)聲明他炊。
在表達(dá)式末尾的最后的()
—— })();
這一行 —— 才是實(shí)際立即執(zhí)行它前面的函數(shù)表達(dá)式的東西。
這看起來可能很奇怪已艰,但它不像第一眼看上去那么陌生痊末。考慮這里的foo
和IIFE
之間的相似性:
function foo() { .. }
// `foo` 是函數(shù)引用表達(dá)式哩掺,然后用`()`執(zhí)行它
foo();
// `IIFE` 是函數(shù)表達(dá)式舌胶,然后用`()`執(zhí)行它
(function IIFE(){ .. })();
如你所見,在執(zhí)行它的()
之前列出(function IIFE(){ .. })
疮丛,與在執(zhí)行它的()
之前定義foo
實(shí)質(zhì)上是相同的幔嫂;在這兩種情況下,函數(shù)引用都使用立即在它后面的()
執(zhí)行誊薄。
因?yàn)镮IFE只是一個(gè)函數(shù)履恩,而函數(shù)可以創(chuàng)建變量 作用域,以這樣的風(fēng)格使用一個(gè)IIFE經(jīng)常被用于定義變量呢蔫,而這些變量將不會影響圍繞在IIFE外面的代碼:
var a = 42;
(function IIFE(){
var a = 10;
console.log( a ); // 10
})();
console.log( a ); // 42
IIFE還可以有返回值:
var x = (function IIFE(){
return 42;
})();
x; // 42
值42
從被執(zhí)行的命名為IIFE
的函數(shù)中return
切心,然后被賦值給x
。
閉包
閉包 是JavaScript中最重要片吊,卻又經(jīng)常最少為人知的概念之一绽昏。我不會在這里涵蓋更深的細(xì)節(jié),你可以參照本系列的 作用域與閉包俏脊。但我想說幾件關(guān)于它的事情全谤,以便你了解它的一般概念。它將是你的JS技術(shù)結(jié)構(gòu)中最重要的技術(shù)之一爷贫。
你可以認(rèn)為閉包是這樣一種方法:即使函數(shù)已經(jīng)完成了運(yùn)行认然,它依然可以“記住”并持續(xù)訪問函數(shù)的作用域。
考慮如下代碼:
function makeAdder(x) {
// 參數(shù) `x` 是一個(gè)內(nèi)部變量
// 內(nèi)部函數(shù) `add()` 使用 `x`漫萄,所以它對 `x` 擁有一個(gè)“閉包”
function add(y) {
return y + x;
};
return add;
}
每次調(diào)用外部的makeAdder(..)
所返回的對內(nèi)部add(..)
函數(shù)的引用可以記住被傳入makeAdder(..)
的x
值【碓保現(xiàn)在,讓我們使用makeAdder(..)
:
// `plusOne` 得到一個(gè)指向內(nèi)部函數(shù) `add(..)` 的引用腾务,
// `add()` 函數(shù)擁有對外部 `makeAdder(..)` 的參數(shù) `x`
// 的閉包
var plusOne = makeAdder( 1 );
// `plusTen` 得到一個(gè)指向內(nèi)部函數(shù) `add(..)` 的引用毕骡,
// `add()` 函數(shù)擁有對外部 `makeAdder(..)` 的參數(shù) `x`
// 的閉包
var plusTen = makeAdder( 10 );
plusOne( 3 ); // 4 <-- 1 + 3
plusOne( 41 ); // 42 <-- 1 + 41
plusTen( 13 ); // 23 <-- 10 + 13
這段代碼的工作方式是:
- 當(dāng)我們調(diào)用
makeAdder(1)
時(shí),我們得到一個(gè)指向它內(nèi)部的add(..)
的引用岩瘦,它記住了x
是1
未巫。我們稱這個(gè)函數(shù)引用為plusOne(..)
。 - 當(dāng)我們調(diào)用
makeAdder(10)
時(shí)担钮,我們得到了另一個(gè)指向它內(nèi)部的add(..)
引用橱赠,它記住了x
是10
。我們稱這個(gè)函數(shù)引用為plusTen(..)
箫津。 - 當(dāng)我們調(diào)用
plusOne(3)
時(shí)狭姨,它在3
(它內(nèi)部的y
)上加1
(被x
記住的),于是我們得到結(jié)果4
苏遥。 - 當(dāng)我們調(diào)用
plusTen(13)
時(shí)饼拍,它在13
(它內(nèi)部的y
)上加10
(被x
記住的),于是我們得到結(jié)果23
田炭。
如果這看起來很奇怪和令人困惑师抄,不要擔(dān)心 —— 它確實(shí)是的!要完全理解它需要很多的練習(xí)教硫。
但是相信我叨吮,一旦你理解了它辆布,它就是編程中最強(qiáng)大最有用的技術(shù)之一。讓你的大腦在閉包中煎熬一會是絕對值得的茶鉴。在下一節(jié)中锋玲,我們將進(jìn)一步實(shí)踐閉包。
模塊
在JavaScript中閉包最常見的用法就是模塊模式涵叮。模塊讓你定義對外面世界不可見的私有實(shí)現(xiàn)細(xì)節(jié)(變量惭蹂,函數(shù)),和對外面可訪問的公有API割粮。
考慮如下代碼:
function User(){
var username, password;
function doLogin(user,pw) {
username = user;
password = pw;
// 做登錄的工作
}
var publicAPI = {
login: doLogin
};
return publicAPI;
}
// 創(chuàng)建一個(gè) `User` 模塊的實(shí)例
var fred = User();
fred.login( "fred", "12Battery34!" );
函數(shù)User()
作為一個(gè)外部作用域持有變量username
和password
盾碗,以及內(nèi)部doLogin()
函數(shù);它們都是User
模塊內(nèi)部的私有細(xì)節(jié)舀瓢,是不能從外部世界訪問的廷雅。
警告: 我們在這里沒有調(diào)用new User()
,這是有意為之的氢伟,雖然對大多數(shù)讀者來說那可能更常見榜轿。User()
只是一個(gè)函數(shù),不是一個(gè)要被初始化的對象朵锣,所以它只是被一般地調(diào)用了谬盐。使用new
將是不合適的,而且實(shí)際上會浪費(fèi)資源诚些。
執(zhí)行User()
創(chuàng)建了User
模塊的一個(gè) 實(shí)例 —— 一個(gè)全新的作用域會被創(chuàng)建飞傀,而每個(gè)內(nèi)部變量/函數(shù)的一個(gè)全新的拷貝也因此而被創(chuàng)建。我們將這個(gè)實(shí)例賦值給fred
诬烹。如果我們再次運(yùn)行User()
砸烦,我們將會得到一個(gè)與fred
完全分離的新的實(shí)例。
內(nèi)部的doLogin()
函數(shù)在username
和password
上擁有閉包绞吁,這意味著即便User()
函數(shù)已經(jīng)完成了運(yùn)行幢痘,它依然持有對它們的訪問權(quán)。
publicAPI
是一個(gè)帶有一個(gè)屬性/方法的對象家破,login
是一個(gè)指向內(nèi)部doLogin()
函數(shù)的引用颜说。當(dāng)我們從User()
中返回publicAPI
時(shí),它就變成了我們稱為fred
的實(shí)例汰聋。
在這個(gè)時(shí)候门粪,外部的User()
函數(shù)已經(jīng)完成了執(zhí)行。一般說來烹困,你會認(rèn)為像username
和password
這樣的內(nèi)部變量將會消失玄妈。但是在這里它們不會,因?yàn)樵?code>login()函數(shù)里有一個(gè)閉包使它們繼續(xù)存活。
這就是為什么我們可以調(diào)用fred.login(..)
—— 和調(diào)用內(nèi)部的doLogin(..)
一樣 —— 而且它依然可以訪問內(nèi)部變量username
和password
拟蜻。
這樣對閉包和模塊模式的簡單一瞥绎签,你很有可能還是有點(diǎn)兒糊涂。沒關(guān)系瞭郑!要把它裝進(jìn)你的大腦確實(shí)需要花些功夫辜御。
以此為起點(diǎn),關(guān)于更多深入細(xì)節(jié)的探索可以去讀本系列的 作用域與閉包屈张。
this
標(biāo)識符
在JavaScript中另一個(gè)經(jīng)常被誤解的概念是this
標(biāo)識符。同樣袱巨,在本系列的 this與對象原型 中有好幾章關(guān)于它的內(nèi)容阁谆,所以在這里我們只簡要地介紹一下概念。
雖然this
可能經(jīng)秤淅希看起來是與“面向?qū)ο竽J健庇嘘P(guān)的场绿,但在JS中this
是一個(gè)不同的概念。
如果一個(gè)函數(shù)在它內(nèi)部擁有一個(gè)this
引用嫉入,那么這個(gè)this
引用通常指向一個(gè)object
焰盗。但是指向哪一個(gè)object
要看這個(gè)函數(shù)是如何被調(diào)用的。
重要的是要理解this
不是 指函數(shù)本身咒林,這是最常見的誤解熬拒。
這是一個(gè)快速的說明:
function foo() {
console.log( this.bar );
}
var bar = "global";
var obj1 = {
bar: "obj1",
foo: foo
};
var obj2 = {
bar: "obj2"
};
// --------
foo(); // "global"
obj1.foo(); // "obj1"
foo.call( obj2 ); // "obj2"
new foo(); // undefined
關(guān)于this
如何被設(shè)置有四個(gè)規(guī)則,它們被展示在這個(gè)代碼段的最后四行中:
-
foo()
最終在非strict模式中將this
設(shè)置為全局對象 —— 在strict模式中垫竞,this
將會是undefined
而且你會在訪問bar
屬性時(shí)得到一個(gè)錯(cuò)誤 —— 所以this.bar
的值是global
澎粟。 -
obj1.foo()
將this
設(shè)置為對象obj1
。 -
foo.call(obj2)
將this
設(shè)置為對象obj2
欢瞪。 -
new foo()
將this
設(shè)置為一個(gè)新的空對象活烙。
底線:要搞清楚this
指向什么,你必須檢視當(dāng)前的函數(shù)是如何被調(diào)用的遣鼓。它將是我們剛剛看到的四種中的一種啸盏,而這將會回答this
是什么。
注意: 關(guān)于this
的更多信息骑祟,參見本系列的 this與對象原型 的第一和第二章回懦。
原型
JavaScript中的原型機(jī)制十分復(fù)雜。我們在這里僅僅掃它一眼曾我。要了解關(guān)于它的所有細(xì)節(jié)粉怕,你需要花相當(dāng)?shù)臅r(shí)間來學(xué)習(xí)本系列的 this與對象原型 的第四到六章。
當(dāng)你引用一個(gè)對象上的屬性時(shí)抒巢,如果這個(gè)屬性不存在贫贝,JavaScript將會自動地使用這個(gè)對象的內(nèi)部原型引用來尋找另外一個(gè)對象,在它上面查詢你想要的屬性。你可以認(rèn)為它幾乎是在屬性缺失時(shí)的備用對象稚晚。
從一個(gè)對象到它備用對象的內(nèi)部原型引用鏈接發(fā)生在這個(gè)對象被創(chuàng)建的時(shí)候崇堵。說明它的最簡單的方法是使用稱為Object.create(..)
的內(nèi)建工具。
考慮如下代碼:
var foo = {
a: 42
};
// 創(chuàng)建 `bar` 并將它鏈接到 `foo`
var bar = Object.create( foo );
bar.b = "hello world";
bar.b; // "hello world"
bar.a; // 42 <-- 委托到 `foo`
將對象foo
和bar
以及它們的關(guān)系可視化也許會有所幫助:
![](fig6.png)
屬性a
實(shí)際上不存在于對象bar
上客燕,但是因?yàn)?code>bar被原型鏈接到foo
鸳劳,JavaScript自動地退到對象foo
上去尋找a
,而且在這里找到了它也搓。
這種鏈接看起來是語言的一種奇怪的特性赏廓。這種特性最常被使用的方式 —— 我會爭辯說這是一種濫用 —— 是用來模擬/模仿“類”機(jī)制的“繼承”。
使用原型的更自然的方式是一種稱為“行為委托”的模式傍妒,在這種模式中你有意地將你的被鏈接的對象設(shè)計(jì)為可以從一個(gè)委托到另一個(gè)的部分所需的行為中幔摸。
注意: 更多關(guān)于原型和行為委托的信息,參見本系列的 this與對象原型 的第四到六章颤练。
舊的與新的
我們已經(jīng)介紹過的JS特性既忆,和將在這個(gè)系列的其他部分中講解的相當(dāng)一部分特性都是新近增加的,不一定在老版本的瀏覽器中可用嗦玖。事實(shí)上患雇,語言規(guī)范中的一些最新特性甚至在任何穩(wěn)定的瀏覽中都沒有被實(shí)現(xiàn)。
那么宇挫,你拿這些新東西怎么辦苛吱?你只能等上幾年或者十幾年直到老版本瀏覽器歸于塵土?
這確實(shí)是許多人認(rèn)為的情況捞稿,但是它不是JS健康的進(jìn)步方式又谋。
有兩種主要的技術(shù)可以將新的JavaScript特性“帶到”老版本的瀏覽器中:填補(bǔ)和轉(zhuǎn)譯。
填補(bǔ)
“填補(bǔ)(Polyfilling)”是一個(gè)人為發(fā)明的詞(由Remy Sharp創(chuàng)造)(https://remysharp.com/2010/10/08/what-is-a-polyfill)娱局。它是指拿來一個(gè)新特性的定義并制造一段行為等價(jià)的代碼彰亥,但是這段代碼可以運(yùn)行在老版本的JS環(huán)境中。
例如衰齐,ES6定義了一個(gè)稱為Number.isNaN(..)
的工具任斋,來為檢查NaN
值提供一種準(zhǔn)確無誤的方法,同時(shí)廢棄原來的isNaN(..)
工具耻涛。這個(gè)工具可以很容易填補(bǔ)废酷,因此你可開始在你的代碼中使用它,而不管最終用戶是否在一個(gè)ES6瀏覽器中抹缕。
考慮如下代碼:
if (!Number.isNaN) {
Number.isNaN = function isNaN(x) {
return x !== x;
};
}
if
語句決定著在這個(gè)工具已經(jīng)存在的ES6環(huán)境中不再進(jìn)行填補(bǔ)澈蟆。如果它還不存在,我們就定義Number.isNaN(..)
卓研。
注意: 我們在這里做的檢查利用了NaN
值的怪異之處趴俘,即它們是整個(gè)語言中唯一與自己不相等的值睹簇。所以NaN
是唯一可能使x !== x
為true
的值。
并不是所有的新特性都可以完全填補(bǔ)寥闪。有時(shí)一種特性的大部分行為可以被填補(bǔ)太惠,但是仍然存在一些小的偏差。在實(shí)現(xiàn)你自己的填補(bǔ)時(shí)你應(yīng)當(dāng)非常非常小心疲憋,來確保你盡可能嚴(yán)格地遵循語言規(guī)范凿渊。
或者更好地,使用一組你信任的缚柳,經(jīng)受過檢驗(yàn)的填補(bǔ)埃脏,比如那些由ES5-Shim(https://github.com/es-shims/es5-shim)和ES6-Shim(https://github.com/es-shims/es6-shim)提供的。
轉(zhuǎn)譯
沒有任何辦法可以填補(bǔ)語言中新增加的語法秋忙。在老版本的JS引擎中新的語法將因?yàn)椴豢勺R別/不合法而拋出一個(gè)錯(cuò)誤剂癌。
所以更好的選擇是使用一個(gè)工具將你的新版本代碼轉(zhuǎn)換為等價(jià)的老版本代碼。這個(gè)處理通常被稱為“轉(zhuǎn)譯(transpiling)”翰绊,表示轉(zhuǎn)換 + 編譯。
實(shí)質(zhì)上旁壮,你的源代碼是使用新的語法形式編寫的监嗜,但是你向?yàn)g覽器部署的是轉(zhuǎn)譯過的舊語法形式。你一般會將轉(zhuǎn)譯器插入到你的構(gòu)建過程中抡谐,與你的代碼linter和代碼壓縮器類似裁奇。
你可能想知道為什么要麻煩地使用新語法編寫程序又將它轉(zhuǎn)譯為老版本代碼 —— 為什么不直接編寫老版本代碼呢?
關(guān)于轉(zhuǎn)譯你應(yīng)當(dāng)注意幾個(gè)重要的原因:
- 在語言中新加入的語法是為了使你的代碼更具可讀性和維護(hù)性而設(shè)計(jì)的麦撵。老版本的等價(jià)物經(jīng)常會繞多得多的圈子刽肠。你應(yīng)當(dāng)首選編寫新的和干凈的語法,不僅為你自己免胃,也為了開發(fā)團(tuán)隊(duì)的其他的成員音五。
- 如果你僅為老版本瀏覽器轉(zhuǎn)譯,而給最新的瀏覽器提供新語法羔沙,那么你就可以利用瀏覽器對新語法進(jìn)行的性能優(yōu)化躺涝。這也讓瀏覽器制造商有更多真實(shí)世界的代碼來測試它們的實(shí)現(xiàn)和優(yōu)化方法。
- 提早使用新語法可以允許它在真實(shí)世界中被測試得更加健壯扼雏,這給JavaScript委員會(TC39)提供了更早的反饋坚嗜。如果問題被發(fā)現(xiàn)的足夠早,他們就可以在那些語言設(shè)計(jì)錯(cuò)誤變得無法挽回之前改變/修改它诗充。
這是一個(gè)轉(zhuǎn)譯的簡單例子苍蔬。ES6增加了一個(gè)稱為“默認(rèn)參數(shù)值”的新特性。它看起來像是這樣:
function foo(a = 2) {
console.log( a );
}
foo(); // 2
foo( 42 ); // 42
簡單蝴蜓,對吧碟绑?也很有用!但是這種新語法在前ES6引擎中是不合法的。那么轉(zhuǎn)譯器將會對這段代碼做什么才能使它在老版本環(huán)境中運(yùn)行呢蜈敢?
function foo() {
var a = arguments[0] !== (void 0) ? arguments[0] : 2;
console.log( a );
}
如你所見辜荠,它檢查arguments[0]
值是否是void 0
(也就是undefined
),而且如果是抓狭,就提供默認(rèn)值2
伯病;否則,它就賦值被傳遞的任何東西否过。
除了可以現(xiàn)在就在老版本瀏覽器中使用更好的語法以外午笛,觀察轉(zhuǎn)譯后的代碼實(shí)際上更清晰地解釋了意圖中的行為。
僅從ES6版本的代碼看來苗桂,你可能還不理解undefined
是唯一不能作為參數(shù)默認(rèn)值的明確傳遞的值药磺,但是轉(zhuǎn)譯后的代碼使這一點(diǎn)清楚的多。
關(guān)于轉(zhuǎn)譯要強(qiáng)調(diào)的最后一個(gè)細(xì)節(jié)是煤伟,現(xiàn)在它們應(yīng)當(dāng)被認(rèn)為是JS開發(fā)的生態(tài)系統(tǒng)和過程中的標(biāo)準(zhǔn)部分癌佩。JS將繼續(xù)以比以前快得多的速度進(jìn)化,所以每幾個(gè)月就會有新語法和新特性被加入進(jìn)來便锨。
如果你默認(rèn)地使用一個(gè)轉(zhuǎn)譯器围辙,那么你將總是可以在發(fā)現(xiàn)新語法有用時(shí),立即開始使用它放案,而不必為了讓今天的瀏覽器被淘汰而等上好幾年姚建。
有好幾個(gè)了不起的轉(zhuǎn)譯器供你選擇。這是一些在本書寫作時(shí)存在的好選擇:
- Babel (https://babeljs.io) (前身為 6to5): 將 ES6+ 轉(zhuǎn)譯為 ES5
- Traceur (https://github.com/google/traceur-compiler): 將 ES6吱殉,ES7掸冤,和以后特性轉(zhuǎn)譯為 ES5
非JavaScript
至此,我們討論過的所有東西都限于JS語言本身∮仰ǎ現(xiàn)實(shí)是大多數(shù)JS程序都是在瀏覽器這樣的環(huán)境中運(yùn)行并與之互動的稿湿。你所編寫的很大一部分代碼,嚴(yán)格地說沥阱,不是直接由JavaScript控制的缎罢。這聽起來可能有點(diǎn)奇怪。
你將會遇到的最常見的非JavaScript程序是DOM API考杉。例如:
var el = document.getElementById( "foo" );
當(dāng)你的代碼運(yùn)行在一個(gè)瀏覽器中時(shí)策精,變量document
作為一個(gè)全局變量存在。它不是由JS引擎提供的崇棠,也不為JavaScript語言規(guī)范所控制咽袜。它采取了某種與普通JS object
極其相似的形式,但它不是真正的object
枕稀。它是一種特殊的object
询刹,經(jīng)常被稱為“宿主對象”谜嫉。
另外,document
上的getElementById(..)
方法看起來像一個(gè)普通的JS函數(shù)凹联,但它只是一個(gè)微微暴露出來的接口雏掠,指向由瀏覽器DOM提供的內(nèi)建方法句伶。在一些(新一代的)瀏覽器中,這一層可能也是由JS實(shí)現(xiàn)的,但是傳統(tǒng)的DOM及其行為是由像C/C++這樣的語言實(shí)現(xiàn)的坏挠。
另一個(gè)例子是輸入/輸出(I/O)师骗。
大家最喜愛的alert(..)
在用戶的瀏覽器窗口中彈出一個(gè)消息框屁擅。alert(..)
是由瀏覽器提供給你的JS程序的仲义,而不是JS引擎本身。你進(jìn)行的調(diào)用將消息發(fā)送給瀏覽器內(nèi)部杠巡,它來處理消息框的繪制與顯示量窘。
console.log()
也一樣;你的瀏覽器提供這樣的機(jī)制并將它們掛在開發(fā)者工具中氢拥。
這本書蚌铜,和整個(gè)這個(gè)系列,聚焦于JavaScript語言本身嫩海。這就是為什么你看不到任何涵蓋這些非JavaScript機(jī)制的重要內(nèi)容厘线。不管怎樣,你需要小心它們出革,因?yàn)樗鼈儗⒃谀銓懙拿恳粋€(gè)JS程序中存在!
復(fù)習(xí)
學(xué)習(xí)JavaScript風(fēng)格編程的第一步是對它的核心機(jī)制有一個(gè)基本的了解渡讼,比如:值骂束,類型,函數(shù)閉包成箫,this
展箱,和原型。
當(dāng)然蹬昌,這些話題中的每一個(gè)都會衍生出比你在這里見到的多得多的內(nèi)容混驰,這也是為什么它們在這個(gè)系列剩下的部分中擁有自己的章節(jié)和書目。在你對本章中的概念和代碼示例感到相當(dāng)適應(yīng)之后皂贩,這個(gè)系列的其他部分正等著你真正地深入挖掘和了解這門語言栖榨。
這本書的最后一章將會對這個(gè)系列的每一卷的內(nèi)容,以及它們所涵蓋的我們在這里還沒有探索過的概念明刷,進(jìn)行簡單地總結(jié)婴栽。