你不懂JS:入門與進(jìn)階 第一章:進(jìn)入編程

官方中文版原文鏈接

感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大獎(jiǎng):點(diǎn)擊這里領(lǐng)取

在前一章中,我介紹了編程的基本構(gòu)建塊兒,比如變量,循環(huán),條件,和函數(shù)。當(dāng)然,所有被展示的代碼都是JavaScript。但是在這一章中,為了作為一個(gè)JS開(kāi)發(fā)者入門和進(jìn)階,我們想要特別集中于那些你需要知道的關(guān)于JavaScript的事情。

我們將在本章中介紹好幾個(gè)概念,它們將會(huì)在后續(xù)的 YDKJS 叢書(shū)中全面地探索。你可以將這一章看作是這個(gè)系列的其他書(shū)目中將要詳細(xì)講解的話題的一個(gè)概覽。

特別是如果你剛剛接觸JavaScript,那么你應(yīng)當(dāng)希望花相當(dāng)一段時(shí)間來(lái)多次復(fù)習(xí)這里的概念和代碼示例。任何好的基礎(chǔ)都是一磚一瓦積累起來(lái)的,所以不要指望你會(huì)在第一遍通讀后就立即理解了全部?jī)?nèi)容。

你深入學(xué)習(xí)JavaScript的旅途從這里開(kāi)始。

注意: 正如我在第一章中說(shuō)過(guò)的,在你通讀這一章的同時(shí),你絕對(duì)應(yīng)該親自嘗試這里所有的代碼。要注意的是,這里的有些代碼假定最新版本的JavaScript(通常稱為“ES6”,ECMAScript的第六個(gè)版本 —— ECMAScript是JS語(yǔ)言規(guī)范的官方名稱)中引入的功能是存在的。如果你碰巧在使用一個(gè)老版本的,前ES6時(shí)代的瀏覽器,這些代碼可能不好用。應(yīng)當(dāng)使用一個(gè)更新版本的現(xiàn)代瀏覽器(比如Chrome,F(xiàn)irefox,或者IE)。

值與類型

正如我們?cè)诘谝徽轮行Q的,JavaScript擁有帶類型的值,沒(méi)有帶類型的變量。下面是可用的內(nèi)建類型:

  • string
  • number
  • boolean
  • nullundefined
  • 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"

來(lái)自typeof的返回值總是六個(gè)(ES6中是七個(gè)?。┳址抵?。也就是,typeof "abc"返回"string",不是string。

注意在這個(gè)代碼段中變量a是如何持有每種不同類型的值的,而且盡管表面上看起來(lái)很像,但是typeof a并不是在詢問(wèn)“a的類型”,而是“當(dāng)前a中的值的類型”。在JavaScript中只有值擁有類型;變量只是這些值的簡(jiǎn)單容器。

typeof null是一個(gè)有趣的例子,因?yàn)楫?dāng)你期望它返回"null"時(shí),它錯(cuò)誤地返回了"object"。

警告: 這是JS中一直存在的一個(gè)bug,但是看起來(lái)它永遠(yuǎn)都不會(huì)被修復(fù)了。在網(wǎng)絡(luò)上有太多的代碼依存于這個(gè)bug,因此修復(fù)它將會(huì)導(dǎo)致更多的bug!

另外,注意a = undefined。我們明確地將a設(shè)置為值undefined,但是在行為上這與一個(gè)還沒(méi)有被設(shè)定值的變量沒(méi)有區(qū)別,比如在這個(gè)代碼段頂部的var a;。一個(gè)變量可以用好幾種不同的方式得到這樣的“undefined”值狀態(tài),包括沒(méi)有返回值的函數(shù)和使用void操作符。

對(duì)象

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值可能會(huì)有所幫助:

fig4.png

屬性既可以使用 點(diǎn)號(hào)標(biāo)記法(例如,obj.a) 訪問(wèn),也可以使用 方括號(hào)標(biāo)記法(例如,obj["a"]) 訪問(wèn)。點(diǎn)號(hào)標(biāo)記法更短而且一般來(lái)說(shuō)更易于閱讀,因此在可能的情況下它都是首選。

如果你有一個(gè)名稱中含有特殊字符的屬性名稱,方括號(hào)標(biāo)記法就很有用,比如obj["hello world!"] —— 當(dāng)通過(guò)方括號(hào)標(biāo)記法訪問(wèn)時(shí),這樣的屬性經(jīng)常被稱為 。[ ]標(biāo)記法要求一個(gè)變量(下一節(jié)講解)或者一個(gè)string 字面量(它需要包裝進(jìn)" .. "' .. ')。

當(dāng)然,如果你想訪問(wèn)一個(gè)屬性/鍵,但是它的名稱被存儲(chǔ)在另一個(gè)變量中時(shí),方括號(hào)標(biāo)記法也很有用。例如:

var obj = {
    a: "hello world",
    b: 42
};

var b = "a";

obj[b];         // "hello world"
obj["b"];       // 42

注意: 更多關(guān)于JavaScript的object的信息,請(qǐng)參見(jiàn)本系列的 this與對(duì)象原型,特別是第三章。

在JavaScript程序中有另外兩種你將會(huì)經(jīng)常打交道的值類型:數(shù)組函數(shù)。但與其說(shuō)它們是內(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"

注意: 從零開(kāi)始計(jì)數(shù)的語(yǔ)言,比如JS,在數(shù)組中使用0作為第一個(gè)元素的索引。

可視化地考慮arr很能會(huì)有所幫助:

fig5.png

因?yàn)閿?shù)組是一種特殊的對(duì)象(正如typeof所暗示的),所以它們可以擁有屬性,包括一個(gè)可以自動(dòng)被更新的length屬性。

理論上你可以使用你自己的命名屬性將一個(gè)數(shù)組用作一個(gè)普通對(duì)象,或者你可以使用一個(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"是一種主要類型 —— 因此也可以擁有屬性,但是你一般僅會(huì)在有限情況下才使用函數(shù)對(duì)象屬性(比如foo.bar)。

注意: 更多關(guān)于JS的值和它們的類型的信息,參見(jiàn)本系列的 類型與文法 的前兩章。

內(nèi)建類型的方法

我們剛剛討論的內(nèi)建類型和子類型擁有十分強(qiáng)大和有用的行為,它們作為屬性和方法暴露出來(lái)。

例如:

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è)方法的說(shuō)法復(fù)雜一些。

簡(jiǎn)而言之,有一個(gè)StringS大寫(xiě))對(duì)象包裝器形式,通常被稱為“原生類型”,與string基本類型配成一對(duì)兒;正是這個(gè)對(duì)象包裝器的原型上定義了toUpperCase()方法。

當(dāng)你通過(guò)引用一個(gè)屬性或方法(例如,前一個(gè)代碼段中的a.toUpperCase())將一個(gè)像"hello world"這樣的基本類型值當(dāng)做一個(gè)object來(lái)使用時(shí),JS自動(dòng)地將這個(gè)值“封箱”為它對(duì)應(yīng)的對(duì)象包裝器(這個(gè)操作是隱藏在幕后的)。

一個(gè)string值可以被包裝為一個(gè)String對(duì)象,一個(gè)number可以被包裝為一個(gè)Number對(duì)象,而一個(gè)boolean可以被包裝為一個(gè)Boolean對(duì)象。在大多數(shù)情況下,你不擔(dān)心或者直接使用這些值的對(duì)象包裝器形式 —— 在所有實(shí)際情況中首選基本類型值形式,而JavaScript會(huì)幫你搞定剩下的一切。

注意: 關(guān)于JS原生類型和“封箱”的更多信息,參見(jiàn)本系列的 類型與文法 的第三章。要更好地理解對(duì)象原型,參見(jiàn)本系列的 this與對(duì)象原型 的第五章。

值的比較

在你的JS程序中你將需要進(jìn)行兩種主要的值的比較:等價(jià)不等價(jià)。任何比較的結(jié)果都是嚴(yán)格的boolean值(truefalse),無(wú)論被比較的值的類型是什么。

強(qiáng)制轉(zhuǎn)換

在第一章中我們簡(jiǎn)單地談了一下強(qiáng)制轉(zhuǎn)換,我們?cè)诖嘶仡櫵?/p>

在JavaScript中強(qiáng)制轉(zhuǎn)換有兩種形式:明確的隱含的。明確的強(qiáng)制轉(zhuǎn)換比較簡(jiǎn)單,因?yàn)槟憧梢栽诖a中明顯地看到一個(gè)類型轉(zhuǎn)換到另一個(gè)類型將會(huì)發(fā)生,而隱含的強(qiáng)制轉(zhuǎn)換更像是另外一些操作的不明顯的副作用引發(fā)的類型轉(zhuǎn)換。

你可能聽(tīng)到過(guò)像“強(qiáng)制轉(zhuǎn)換是邪惡的”這樣情緒化的觀點(diǎn),這是因?yàn)橐粋€(gè)清楚的事實(shí) —— 強(qiáng)制轉(zhuǎn)換在某些地方會(huì)產(chǎn)生一些令人吃驚的結(jié)果。也許沒(méi)有什么能比當(dāng)一個(gè)語(yǔ)言嚇到開(kāi)發(fā)者時(shí)更能喚起他們的沮喪心情了。

強(qiáng)制轉(zhuǎn)換并不邪惡,它也不一定是令人吃驚的。事實(shí)上,你使用類型強(qiáng)制轉(zhuǎn)換構(gòu)建的絕大部分情況是十分合理和可理解的,而且它甚至可以用來(lái) 增強(qiáng) 你代碼的可讀性。但我們不會(huì)在這個(gè)話題上過(guò)度深入 —— 本系列的 類型與文法 的第四章將會(huì)進(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

在第一章中,我們簡(jiǎn)要地提到了值的“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 } (對(duì)象)
  • function foo() { .. } (函數(shù))

重要的是要記住,一個(gè)非boolean值僅在實(shí)際上被強(qiáng)制轉(zhuǎn)換為一個(gè)boolean時(shí)才遵循這個(gè)“truthy”/“falsy”強(qiáng)制轉(zhuǎn)換。把你搞糊涂并不困難 —— 當(dāng)一個(gè)場(chǎng)景看起來(lái)像是將一個(gè)值強(qiáng)制轉(zhuǎn)換為boolean,可其實(shí)它不是。

等價(jià)性

有四種等價(jià)性操作符:==,===!=,和!==。!形式當(dāng)然是與它們相對(duì)應(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)過(guò)一系列有順序的步驟將一個(gè)值或者它們兩者強(qiáng)制轉(zhuǎn)換為一個(gè)不同的類型,直到類型匹配為止,然后就可以檢查一個(gè)簡(jiǎn)單的值等價(jià)性。

如果你仔細(xì)想一想,通過(guò)強(qiáng)制轉(zhuǎn)換a == b可以有兩種方式給出true。這個(gè)比較要么最終成為42 == 42,要么成為"42" == "42"。那么是哪一種呢?

答案:"42"變成42,于是比較成為42 == 42。在一個(gè)這樣簡(jiǎn)單的例子中,只要最終結(jié)果是一樣的,處理的過(guò)程走哪一條路看起來(lái)并不重要。但在一些更復(fù)雜的情況下,這不僅對(duì)比較的最終結(jié)果很重要,而且對(duì)你 如何 得到這個(gè)結(jié)果也很重要。

a === b產(chǎn)生false,因?yàn)閺?qiáng)制轉(zhuǎn)換是不允許的,所以簡(jiǎn)單值的比較很明顯將會(huì)失敗。許多開(kāi)發(fā)者感覺(jué)===更可靠,所以他們提倡一直使用這種形式而遠(yuǎn)離==。我認(rèn)為這種觀點(diǎn)是非常短視的。我相信==是一種可以改進(jìn)程序的強(qiáng)大工具,如果你花時(shí)間去學(xué)習(xí)它的工作方式。

我們不會(huì)詳細(xì)地講解強(qiáng)制轉(zhuǎn)換在==比較中是如何工作的。它的大部分都是相當(dāng)合理的,但是有一些重要的極端用例要小心。你可以閱讀ES5語(yǔ)言規(guī)范的11.9.3部分(http://www.ecma-international.org/ecma-262/5.1/)來(lái)了解確切的規(guī)則,而且與圍繞這種機(jī)制的所有負(fù)面炒作比起來(lái),你會(huì)對(duì)這它是多么的直白而感到吃驚。

為了將這許多細(xì)節(jié)歸納為一個(gè)簡(jiǎn)單的包裝,并幫助你在各種情況下判斷是否使用=====,這是我的簡(jiǎn)單規(guī)則:

  • 如果一個(gè)比較的兩個(gè)值之一可能是truefalse值,避免==而使用===。
  • 如果一個(gè)比較的兩個(gè)值之一可能是這些具體的值(0,"",或[] —— 空數(shù)組),避免==而使用===
  • 所有 其他情況下,你使用==是安全的。它不僅安全,而且在許多情況下它可以簡(jiǎn)化你的代碼并改善可讀性。

這些規(guī)則歸納出來(lái)的東西要求你嚴(yán)謹(jǐn)?shù)乜紤]你的代碼:什么樣的值可能通過(guò)這個(gè)被比較等價(jià)性的變量。如果你可以確定這些值,那么==就是安全的,使用它!如果你不能確定這些值,就使用===。就這么簡(jiǎn)單。

!=不等價(jià)形式對(duì)應(yīng)于==,而!==形式對(duì)應(yīng)于===。我們剛剛討論的所有規(guī)則和注意點(diǎn)對(duì)這些非等價(jià)比較都是平行適用的。

如果你在比較兩個(gè)非基本類型值,比如object(包括functionarray),那么你應(yīng)當(dāng)特別小心=====的比較規(guī)則。因?yàn)檫@些值實(shí)際上是通過(guò)引用持有的,=====比較都將簡(jiǎn)單地檢查這個(gè)引用是否相同,而不是它們底層的值。

例如,array默認(rèn)情況下會(huì)通過(guò)使用逗號(hào)(,)連接所有值來(lái)被強(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ī)則的信息,參見(jiàn)ES5語(yǔ)言規(guī)范(11.9.3部分),和本系列的 類型與文法 的第四章;更多關(guān)于值和引用的信息,參見(jiàn)它的第二章。

不等價(jià)性

<,>,<=,和>=操作符用于不等價(jià)性比較,在語(yǔ)言規(guī)范中被稱為“關(guān)系比較”。一般來(lái)說(shuō)它們將與number這樣的可比較有序值一起使用。3 < 4是很容易理解的。

但是JavaScriptstring值也可進(jìn)行不等價(jià)性比較,它使用典型的字母順序規(guī)則("bar" < "foo")。

那么強(qiáng)制轉(zhuǎn)換呢?與==比較相似的規(guī)則(雖然不是完全相同?。┮策m用于不等價(jià)操作符。要注意的是,沒(méi)有像===嚴(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語(yǔ)言規(guī)范的11.8.5部分中,它說(shuō)如果<比較的兩個(gè)值都是string,就像b < c,那么這個(gè)比較將會(huì)以字典順序(也就是像字典中字母的排列順序)進(jìn)行。但如果兩個(gè)值之一不是string,就像a < b,那么兩個(gè)值就將被強(qiáng)制轉(zhuǎn)換成number,并進(jìn)行一般的數(shù)字比較。

在可能不同類型的值之間進(jìn)行比較時(shí),你可能遇到的最大的坑 —— 記住,沒(méi)有“嚴(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ù)字值”,而且語(yǔ)言規(guī)范說(shuō)NaN既不大于其他值,也不小于其他值。

==比較失敗于不同的原因。如果a == b被解釋為42 == NaN或者"42" == "foo"都會(huì)失敗 —— 正如我們前面講過(guò)的,這里是前一種情況。

注意: 關(guān)于不等價(jià)比較規(guī)則的更多信息,參見(jiàn)ES5語(yǔ)言規(guī)范的11.8.5部分,和本系列的 類型與文法 第四章。

變量

在JavaScript中,變量名(包括函數(shù)名)必須是合法的 標(biāo)識(shí)符(identifiers)。當(dāng)你考慮非傳統(tǒng)意義上的字符時(shí),比如Unicode,標(biāo)識(shí)符中合法字符的嚴(yán)格和完整的規(guī)則就有點(diǎn)兒復(fù)雜。如果你僅考慮典型的ASCII字母數(shù)字的字符,那么這個(gè)規(guī)則還是很簡(jiǎn)單的。

一個(gè)標(biāo)識(shí)符必須以a-z,A-Z$,或_開(kāi)頭。它可以包含任意這些字符外加數(shù)字0-9

一般來(lái)說(shuō),變量標(biāo)識(shí)符的規(guī)則也通用適用于屬性名稱。然而,有一些不能用作變量名,但是可以用作屬性名的單詞。這些單詞被稱為“保留字(reserved words)”,包括JS關(guān)鍵字(for,in,if,等等)和null,truefalse。

注意: 更多關(guān)于保留字的信息,參見(jiàn)本系列的 類型與文法 的附錄A。

函數(shù)作用域

你使用var關(guān)鍵字聲明的變量將屬于當(dāng)前的函數(shù)作用域,如果聲明位于任何函數(shù)外部的頂層,它就屬于全局作用域。

提升

無(wú)論var出現(xiàn)在一個(gè)作用域內(nèi)部的何處,這個(gè)聲明都被認(rèn)為是屬于整個(gè)作用域,而且在作用域的所有位置都是可以訪問(wèn)的。

這種行為稱為 提升,比喻一個(gè)var聲明在概念上 被移動(dòng) 到了包含它的作用域的頂端。技術(shù)上講,這個(gè)過(guò)程通過(guò)代碼的編譯方式進(jìn)行解釋更準(zhǔn)確,但是我們先暫且跳過(guò)那些細(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è)作用域中依靠變量提升來(lái)在var聲明出現(xiàn)之前使用一個(gè)變量是不常見(jiàn)的,也不是個(gè)好主意;它可能相當(dāng)使人困惑。而使用被提升的函數(shù)聲明要常見(jiàn)得多,也更為人所接受,就像我們?cè)?code>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();

注意cbar()的內(nèi)部是不可用的,因?yàn)樗莾H在內(nèi)部的baz()作用域中被聲明的,并且b因?yàn)橥瑯拥脑蛟?code>foo()內(nèi)是不可用的。

如果你試著在一個(gè)作用域內(nèi)訪問(wèn)一個(gè)不可用的變量的值,你就會(huì)得到一個(gè)被拋出的ReferenceError。如果你試著為一個(gè)還沒(méi)有被聲明的變量賦值,那么根據(jù)“strict模式”的狀態(tài),你會(huì)要么得到一個(gè)在頂層全局作用域中創(chuàng)建的變量(不好?。?,要么得到一個(gè)錯(cuò)誤。讓我們看一下:

function foo() {
    a = 1;  // `a` 沒(méi)有被正式聲明
}

foo();
a;          // 1 -- 噢,自動(dòng)全局變量 :(

這是一種非常差勁兒的做法。別這么干!總是給你的變量進(jìn)行正式聲明。

除了在函數(shù)級(jí)別為變量創(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語(yǔ)句而不是整個(gè)foo()函數(shù)的作用域。相似地,c僅屬于while循環(huán)。對(duì)于以更加細(xì)粒度的方式管理你的變量作用域來(lái)說(shuō),塊兒作用域是非常有用的,它將使你的代碼隨著時(shí)間的推移更加易于維護(hù)。

注意: 關(guān)于作用域的更多信息,參見(jiàn)本系列的 作用域與閉包。更多關(guān)于let塊兒作用域的信息,參見(jiàn)本系列的 ES6與未來(lái)。

條件

除了我們?cè)诘谝徽轮泻?jiǎn)要介紹過(guò)的if語(yǔ)句,JavaScript還提供了幾種其他值得我們一看的條件機(jī)制。

有時(shí)你可能發(fā)現(xiàn)自己在像這樣寫(xiě)一系列的if..else..if語(yǔ)句:

if (a == 2) {
    // 做一些事情
}
else if (a == 10) {
    // 做另一些事請(qǐng)
}
else if (a == 42) {
    // 又是另外一些事情
}
else {
    // 這里是備用方案
}

這種結(jié)構(gòu)好用,但有一點(diǎn)兒繁冗,因?yàn)槟阈枰獮槊恳环N情況都指明a的測(cè)試。這里有另一種選項(xiàng),switch語(yǔ)句:

switch (a) {
    case 2:
        // 做一些事情
        break;
    case 10:
        // 做另一些事請(qǐng)
        break;
    case 42:
        // 又是另外一些事情
        break;
    default:
        // 這里是備用方案
}

如果你想僅讓一個(gè)case中的語(yǔ)句運(yùn)行,break是很重要的。如果你在一個(gè)case中省略了break,并且這個(gè)case成立或運(yùn)行,那么程序的執(zhí)行將會(huì)不管下一個(gè)case語(yǔ)句是否成立而繼續(xù)執(zhí)行它。這種所謂的“掉落”有時(shí)是有用/期望的:

switch (a) {
    case 2:
    case 10:
        // 一些很酷的事情
        break;
    case 42:
        // 另一些事情
        break;
    default:
        // 備用方案
}

這里,如果a210,它就會(huì)執(zhí)行“一些很酷的事情”的代碼語(yǔ)句。

在JavaScript中的另一種條件形式是“條件操作符”,經(jīng)常被稱為“三元操作符”。它像是一個(gè)單獨(dú)的if..else語(yǔ)句的更簡(jiǎn)潔的形式,比如:

var a = 42;

var b = (a > 41) ? "hello" : "world";

// 與此相似:

// if (a > 41) {
//    b = "hello";
// }
// else {
//    b = "world";
// }

如果測(cè)試表達(dá)式(這里是a > 41)求值為true,那么就會(huì)得到第一個(gè)子句("hello"),否則得到第二個(gè)子句("world"),而且無(wú)論結(jié)果為何都會(huì)被賦值給b。

條件操作符不一定非要用于賦值,但是這絕對(duì)是最常見(jiàn)的用法。

注意: 關(guān)于測(cè)試條件和switch? :的其他模式的更多信息,參見(jiàn)本系列的 類型與文法。

Strict模式

ES5在語(yǔ)言中加入了一個(gè)“strict模式”,它收緊了一些特定行為的規(guī)則。一般來(lái)說(shuō),這些限制被視為使代碼符合一組更安全和更合理的指導(dǎo)方針。另外,堅(jiān)持strict模式一般會(huì)使你的代碼對(duì)引擎有更強(qiáng)的可優(yōu)化性。strict模式對(duì)代碼有很大的好處,你應(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)鍵不同(改善?。┦?,它不允許因?yàn)槭÷粤?code>var而進(jìn)行隱含的自動(dòng)全局變量聲明:

function foo() {
    "use strict";   // 打開(kāi)strict模式
    a = 1;          // 缺少`var`,ReferenceError
}

foo();

如果你在代碼中打開(kāi)strict模式,并且得到錯(cuò)誤,或者代碼開(kāi)始變得有bug,這可能會(huì)誘使你避免使用strict模式。但是縱容這種直覺(jué)不是一個(gè)好主意。如果strict模式在你的程序中導(dǎo)致了問(wèn)題,那么這標(biāo)志著在你的代碼中幾乎可以肯定有應(yīng)該修改的東西。

strict模式不僅將你的代碼保持在更安全的道路上,也不僅將使你的代碼可優(yōu)化性更強(qiáng),它還代表著這種語(yǔ)言未來(lái)的方向。對(duì)于你來(lái)說(shuō),現(xiàn)在就開(kāi)始習(xí)慣于strict模式要比一直回避它容易得多 —— 以后再進(jìn)行這種轉(zhuǎn)變只會(huì)更難!

注意: 關(guān)于strict模式的更多信息,參見(jiàn)本系列的 類型與文法 的第五章。

函數(shù)作為值

至此,我們已經(jīng)將函數(shù)作為JavaScript中主要的 作用域 機(jī)制討論過(guò)了。你可以回想一下典型的function聲明語(yǔ)法是這樣的:

function foo() {
    // ..
}

雖然從這種語(yǔ)法中看起來(lái)不明顯,foo基本上是一個(gè)位于外圍作用域的變量,它給了被聲明的function一個(gè)引用。也就是說(shuō),function本身是一個(gè)值,就像42[1,2,3]一樣。

這可能聽(tīng)起來(lái)像是一個(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)樗鼪](méi)有“名稱”。

第二個(gè)函數(shù)表達(dá)式是 命名的bar),它還被賦值給變量x作為它的引用。命名函數(shù)表達(dá)式 一般來(lái)說(shuō)更理想,雖然 匿名函數(shù)表達(dá)式 仍然極其常見(jiàn)。

更多信息參見(jiàn)本系列的 作用域與閉包。

立即被調(diào)用的函數(shù)表達(dá)式(IIFE)

在前一個(gè)代碼段中,哪一個(gè)函數(shù)表達(dá)式都沒(méi)有被執(zhí)行 —— 除非我們使用了foo()x()

有另一種執(zhí)行函數(shù)表達(dá)式的方法,它通常被稱為一個(gè) 立即被調(diào)用的函數(shù)表達(dá)式 (IIFE):

(function IIFE(){
    console.log( "Hello!" );
})();
// "Hello!"

圍繞在函數(shù)表達(dá)式(function IIFE(){ .. })外部的( .. )只是一個(gè)微妙的JS文法,我們需要它來(lái)防止函數(shù)表達(dá)式被看作一個(gè)普通的函數(shù)聲明。

在表達(dá)式末尾的最后的() —— })();這一行 —— 才是實(shí)際立即執(zhí)行它前面的函數(shù)表達(dá)式的東西。

這看起來(lái)可能很奇怪,但它不像第一眼看上去那么陌生??紤]這里的fooIIFE之間的相似性:

function foo() { .. }

// `foo` 是函數(shù)引用表達(dá)式,然后用`()`執(zhí)行它
foo();

// `IIFE` 是函數(shù)表達(dá)式,然后用`()`執(zhí)行它
(function IIFE(){ .. })();

如你所見(jiàn),在執(zhí)行它的()之前列出(function IIFE(){ .. }),與在執(zhí)行它的()之前定義foo實(shí)質(zhì)上是相同的;在這兩種情況下,函數(shù)引用都使用立即在它后面的()執(zhí)行。

因?yàn)镮IFE只是一個(gè)函數(shù),而函數(shù)可以創(chuàng)建變量 作用域,以這樣的風(fēng)格使用一個(gè)IIFE經(jīng)常被用于定義變量,而這些變量將不會(huì)影響圍繞在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)常最少為人知的概念之一。我不會(huì)在這里涵蓋更深的細(xì)節(jié),你可以參照本系列的 作用域與閉包。但我想說(shuō)幾件關(guān)于它的事情,以便你了解它的一般概念。它將是你的JS技術(shù)結(jié)構(gòu)中最重要的技術(shù)之一。

你可以認(rèn)為閉包是這樣一種方法:即使函數(shù)已經(jīng)完成了運(yùn)行,它依然可以“記住”并持續(xù)訪問(wèn)函數(shù)的作用域。

考慮如下代碼:

function makeAdder(x) {
    // 參數(shù) `x` 是一個(gè)內(nèi)部變量

    // 內(nèi)部函數(shù) `add()` 使用 `x`,所以它對(duì) `x` 擁有一個(gè)“閉包”
    function add(y) {
        return y + x;
    };

    return add;
}

每次調(diào)用外部的makeAdder(..)所返回的對(duì)內(nèi)部add(..)函數(shù)的引用可以記住被傳入makeAdder(..)x值?,F(xiàn)在,讓我們使用makeAdder(..)

// `plusOne` 得到一個(gè)指向內(nèi)部函數(shù) `add(..)` 的引用,
// `add()` 函數(shù)擁有對(duì)外部 `makeAdder(..)` 的參數(shù) `x`
// 的閉包
var plusOne = makeAdder( 1 );

// `plusTen` 得到一個(gè)指向內(nèi)部函數(shù) `add(..)` 的引用,
// `add()` 函數(shù)擁有對(duì)外部 `makeAdder(..)` 的參數(shù) `x`
// 的閉包
var plusTen = makeAdder( 10 );

plusOne( 3 );       // 4  <-- 1 + 3
plusOne( 41 );      // 42 <-- 1 + 41

plusTen( 13 );      // 23 <-- 10 + 13

這段代碼的工作方式是:

  1. 當(dāng)我們調(diào)用makeAdder(1)時(shí),我們得到一個(gè)指向它內(nèi)部的add(..)的引用,它記住了x1。我們稱這個(gè)函數(shù)引用為plusOne(..)。
  2. 當(dāng)我們調(diào)用makeAdder(10)時(shí),我們得到了另一個(gè)指向它內(nèi)部的add(..)引用,它記住了x10。我們稱這個(gè)函數(shù)引用為plusTen(..)。
  3. 當(dāng)我們調(diào)用plusOne(3)時(shí),它在3(它內(nèi)部的y)上加1(被x記住的),于是我們得到結(jié)果4。
  4. 當(dāng)我們調(diào)用plusTen(13)時(shí),它在13(它內(nèi)部的y)上加10(被x記住的),于是我們得到結(jié)果23

如果這看起里很奇怪和令人困惑,不要擔(dān)心 —— 它確實(shí)是的!要完全理解它需要很多的練習(xí)。

但是相信我,一旦你理解了它,它就是編程中最強(qiáng)大最有用的技術(shù)之一。讓你的大腦在閉包中煎熬一會(huì)是絕對(duì)值得的。在下一節(jié)中,我們將進(jìn)一步實(shí)踐閉包。

模塊

在JavaScript中閉包最常見(jiàn)的用法就是模塊模式。模塊讓你定義對(duì)外面世界不可見(jiàn)的私有實(shí)現(xiàn)細(xì)節(jié)(變量,函數(shù)),和對(duì)外面可訪問(wèn)的公有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è)外部作用域持有變量usernamepassword,以及內(nèi)部doLogin()函數(shù);它們都是User模塊內(nèi)部的私有細(xì)節(jié),是不能從外部世界訪問(wèn)的。

警告: 我們?cè)谶@里沒(méi)有調(diào)用new User(),這是有意為之的,雖然對(duì)大多數(shù)讀者來(lái)說(shuō)那可能更常見(jiàn)。User()只是一個(gè)函數(shù),不是一個(gè)要被初始化的對(duì)象,所以它只是被一般地調(diào)用了。使用new將是不合適的,而且實(shí)際上會(huì)浪費(fèi)資源。

執(zhí)行User()創(chuàng)建了User模塊的一個(gè) 實(shí)例 —— 一個(gè)全新的作用域會(huì)被創(chuàng)建,而每個(gè)內(nèi)部變量/函數(shù)的一個(gè)全新的拷貝也因此而被創(chuàng)建。我們將這個(gè)實(shí)例賦值給fred。如果我們?cè)俅芜\(yùn)行User(),我們將會(huì)得到一個(gè)與fred完全分離的新的實(shí)例。

內(nèi)部的doLogin()函數(shù)在usernamepassword上擁有閉包,這意味著即便User()函數(shù)已經(jīng)完成了運(yùn)行,它依然持有對(duì)它們的訪問(wèn)權(quán)。

publicAPI是一個(gè)帶有一個(gè)屬性/方法的對(duì)象,login是一個(gè)指向內(nèi)部doLogin()函數(shù)的引用。當(dāng)我們從User()中返回publicAPI時(shí),它就變成了我們稱為fred的實(shí)例。

在這個(gè)時(shí)候,外部的User()函數(shù)已經(jīng)完成了執(zhí)行。一般說(shuō)來(lái),你會(huì)認(rèn)為像usernamepassword這樣的內(nèi)部變量將會(huì)消失。但是在這里它們不會(huì),因?yàn)樵?code>login()函數(shù)里有一個(gè)閉包使它們繼續(xù)存活。

這就是為什么我們可以調(diào)用fred.login(..) —— 和調(diào)用內(nèi)部的doLogin(..)一樣 —— 而且它依然可以訪問(wèn)內(nèi)部變量usernamepassword。

這樣對(duì)閉包和模塊模式的簡(jiǎn)單一瞥,你很有可能還是有點(diǎn)兒糊涂。沒(méi)關(guān)系!要把它裝進(jìn)你的大腦確實(shí)需要花些功夫。

以此為起點(diǎn),關(guān)于更多深入細(xì)節(jié)的探索可以去讀本系列的 作用域與閉包。

this 標(biāo)識(shí)符

在JavaScript中另一個(gè)經(jīng)常被誤解的概念是this標(biāo)識(shí)符。同樣,在本系列的 this與對(duì)象原型 中有好幾章關(guān)于它的內(nèi)容,所以在這里我們只簡(jiǎn)要的介紹一下概念。

雖然this可能經(jīng)常看起來(lái)是與“面向?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ù)本身,這是最常見(jiàn)的誤解。

這是一個(gè)快速的說(shuō)明:

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è)代碼段的最后四行中:

  1. foo()最終在非strict模式中將this設(shè)置為全局對(duì)象 —— 在strict模式中,this將會(huì)是undefined而且你會(huì)在訪問(wèn)bar屬性時(shí)得到一個(gè)錯(cuò)誤 —— 所以this.bar的值是global
  2. obj1.foo()this設(shè)置為對(duì)象obj1。
  3. foo.call(obj2)this設(shè)置為對(duì)象obj2
  4. new foo()this設(shè)置為一個(gè)新的空對(duì)象。

底線:要搞清楚this指向什么,你必須檢視當(dāng)前的函數(shù)是如何被調(diào)用的。它將是我們剛剛看到的四種中的一種,而這將會(huì)回答this是什么。

注意: 關(guān)于this的更多信息,參見(jiàn)本系列的 this與對(duì)象原型 的第一和第二章。

原型

JavaScript中的原型機(jī)制十分復(fù)雜。我們?cè)谶@里近僅僅掃它一眼。要了解關(guān)于它的所有細(xì)節(jié),你需要花相當(dāng)?shù)臅r(shí)間來(lái)學(xué)習(xí)本系列的 this與對(duì)象原型 的第四到六章。

當(dāng)你引用一個(gè)對(duì)象上的屬性時(shí),如果這個(gè)屬性不存在,JavaScript將會(huì)自動(dòng)地使用這個(gè)對(duì)象的內(nèi)部原型引用來(lái)尋找另外一個(gè)對(duì)象,在它上面查詢你想要的屬性。你可以認(rèn)為它幾乎是在屬性缺失時(shí)的備用對(duì)象。

從一個(gè)對(duì)象到它備用對(duì)象的內(nèi)部原型引用鏈接發(fā)生在這個(gè)對(duì)象被創(chuàng)建的時(shí)候。說(shuō)明它的最簡(jiǎn)單的方法是使用稱為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`

將對(duì)象foobar以及它們的關(guān)系可視化也許會(huì)有所幫助:

fig6.png

屬性a實(shí)際上不存在于對(duì)象bar上,但是因?yàn)?code>bar被原型鏈接到foo,JavaScript自動(dòng)地退到對(duì)象foo上去尋找a,而且在這里找到了它。

這種鏈接看起來(lái)是語(yǔ)言的一種奇怪的特性。這種特性最常被使用的方式 —— 我會(huì)爭(zhēng)辯說(shuō)這是一種濫用 —— 是用來(lái)模擬/模仿“類”機(jī)制的“繼承”。

使用原型的更自然的方式是一種稱為“行為委托”的模式,在這種模式中你有意地將你的被鏈接的對(duì)象設(shè)計(jì)為可以從一個(gè)委托到另一個(gè)的部分所需的行為中。

注意: 更多關(guān)于原型和行為委托的信息,參見(jiàn)本系列的 this與對(duì)象原型 的第四到六章。

舊的與新的

以我們已經(jīng)介紹過(guò)的JS特性,和將在這個(gè)系列的其他部分中講解的相當(dāng)一部分特性都是新近增加的,不一定在老版本的瀏覽器中可用。事實(shí)上,語(yǔ)言規(guī)范中的一些最新特性甚至在任何穩(wěn)定的瀏覽中都沒(méi)有被實(shí)現(xiàn)。

那么,你拿這些新東西怎么辦?你只能等上幾年或者十幾年直到老版本瀏覽器歸于塵土?

這確實(shí)是許多人認(rèn)為的情況,但是它不是JS健康的進(jìn)步方式。

有兩種主要的技術(shù)可以將新的JavaScript特性“帶到”老版本的瀏覽器中:填補(bǔ)和轉(zhuǎn)譯。

填補(bǔ)

“填補(bǔ)(Polyfilling)”是一個(gè)認(rèn)為發(fā)明的詞(由Remy Sharp創(chuàng)造)(https://remysharp.com/2010/10/08/what-is-a-polyfill)。它是指拿來(lái)一個(gè)新特性的定義并制造一段行為等價(jià)的代碼,但是這段代碼可以運(yùn)行在老版本的JS環(huán)境中。

例如,ES6定義了一個(gè)稱為Number.isNaN(..)的工具,來(lái)為檢查NaN值提供一種準(zhǔn)確無(wú)誤的方法,同時(shí)廢棄原來(lái)的isNaN(..)工具。這個(gè)工具可以很容易填補(bǔ),因此你可開(kāi)始在你的代碼中使用它,而不管最終用戶是否在一個(gè)ES6瀏覽器中。

考慮如下代碼:

if (!Number.isNaN) {
    Number.isNaN = function isNaN(x) {
        return x !== x;
    };
}

if語(yǔ)句決定著在這個(gè)工具已經(jīng)存在的ES6環(huán)境中不再進(jìn)行填補(bǔ)。如果它還不存在,我們就定義Number.isNaN(..)

注意: 我們?cè)谶@里做的檢查利用了NaN值的怪異之處,即它們是整個(gè)語(yǔ)言中唯一與自己不相等的值。所以NaN是唯一可能使x !== xtrue的值。

并不是所有的新特性都可以完全填補(bǔ)。有時(shí)一種特性的大部分行為可以被填補(bǔ),但是仍然存在一些小的偏差。在實(shí)現(xiàn)你自己的添補(bǔ)時(shí)你應(yīng)當(dāng)非常非常小心,來(lái)確保你盡可能嚴(yán)格地遵循語(yǔ)言規(guī)范。

或者更好地,使用一組你信任的,經(jīng)受過(guò)檢驗(yàn)的添補(bǔ),比如那些由ES5-Shim(https://github.com/es-shims/es5-shim)和ES6-Shim(https://github.com/es-shims/es6-shim)提供的。

轉(zhuǎn)譯

沒(méi)有任何辦法可以添補(bǔ)語(yǔ)言中新增加的語(yǔ)法。在老版本的JS引擎中新的語(yǔ)法將因?yàn)椴豢勺R(shí)別/不合法而拋出一個(gè)錯(cuò)誤。

所以更好的選擇是使用一個(gè)工具將你的新版本代碼轉(zhuǎn)換為等價(jià)的老版本代碼。這個(gè)處理通常被稱為“轉(zhuǎn)譯(transpiling)”,表示轉(zhuǎn)換 + 編譯。

實(shí)質(zhì)上,你的源代碼是使用新的語(yǔ)法形式編寫(xiě)的,但是你向?yàn)g覽器部署的是轉(zhuǎn)譯過(guò)的舊語(yǔ)法形式。你一般會(huì)將轉(zhuǎn)譯器插入到你的構(gòu)建過(guò)程中,與你的代碼linter和代碼壓縮器類似。

你可能想知道為什么要麻煩地使用新語(yǔ)法編寫(xiě)程序又將它轉(zhuǎn)譯為老版本代碼 —— 為什么不直接編寫(xiě)老版本代碼呢?

關(guān)于轉(zhuǎn)譯你應(yīng)當(dāng)注意幾個(gè)重要的原因:

  • 在語(yǔ)言中新加入的語(yǔ)法是為了使你的代碼更具可讀性和維護(hù)性而設(shè)計(jì)的。老版本的等價(jià)物經(jīng)常會(huì)繞多得多的圈子。你應(yīng)當(dāng)首選編寫(xiě)新的和干凈的語(yǔ)法,不僅為你自己,也為了開(kāi)發(fā)團(tuán)隊(duì)的其他的成員。
  • 如果你僅為老版本瀏覽器轉(zhuǎn)譯,而給最新的瀏覽器提供新語(yǔ)法,那么你就可以利用瀏覽器對(duì)新語(yǔ)法進(jìn)行的性能優(yōu)化。這也讓瀏覽器制造商有更多真實(shí)世界的代碼來(lái)測(cè)試它們的實(shí)現(xiàn)和優(yōu)化方法。
  • 提早使用新語(yǔ)法可以允許它在真實(shí)世界中被測(cè)試得更加健壯,這給JavaScript協(xié)會(huì)(TC39)提供了更早的反饋。如果問(wèn)題被發(fā)現(xiàn)的足夠早,他們就可以在那些語(yǔ)言設(shè)計(jì)錯(cuò)誤變得無(wú)法挽回之前改變/修改它。

這是一個(gè)轉(zhuǎn)譯的簡(jiǎn)單例子。ES6增加了一個(gè)稱為“默認(rèn)參數(shù)值”的新特性。它看起來(lái)像是這樣:

function foo(a = 2) {
    console.log( a );
}

foo();      // 2
foo( 42 );  // 42

簡(jiǎn)單,對(duì)吧?也很有用!但是這種新語(yǔ)法在前ES6引擎中是不合法的。那么轉(zhuǎn)譯器將會(huì)對(duì)這段代碼做什么才能使它在老版本環(huán)境中運(yùn)行呢?

function foo() {
    var a = arguments[0] !== (void 0) ? arguments[0] : 2;
    console.log( a );
}

如你所見(jiàn),它檢查arguments[0]值是否是void 0(也就是undefined),而且如果是,就提供默認(rèn)值2;否則,它就賦值被傳遞的任何東西。

除了可以現(xiàn)在就在老版本瀏覽器中使用更好的語(yǔ)法以外,觀察轉(zhuǎn)譯后的代碼實(shí)際上更清晰地解釋了意圖中的行為。

僅從ES6版本的代碼看來(lái),你可能還不理解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開(kāi)發(fā)的生態(tài)系統(tǒng)和過(guò)程中的標(biāo)準(zhǔn)部分。JS將繼續(xù)以比以前快得多的速度進(jìn)化,所以每幾個(gè)月就會(huì)有新語(yǔ)法和新特性被加入進(jìn)來(lái)。

如果你默認(rèn)地使用一個(gè)轉(zhuǎn)譯器,那么你將總是可以在你發(fā)現(xiàn)新語(yǔ)法有用時(shí),立即開(kāi)始使用它,而不必為了讓今天的瀏覽器被淘汰而等上好幾年。

有好幾個(gè)了不起的轉(zhuǎn)譯器供你選擇。這是一些在本書(shū)寫(xiě)作時(shí)存在的好選擇:

非JavaScript

至此,我們討論過(guò)的所有東西都限于JS語(yǔ)言本身?,F(xiàn)實(shí)是大多數(shù)JS程序都是在瀏覽器這樣的環(huán)境中運(yùn)行并與之互動(dòng)的。你在你的代碼中編寫(xiě)的很大一部分東西,嚴(yán)格地說(shuō),不是直接由JavaScript控制的。這聽(tīng)起來(lái)可能有點(diǎn)奇怪。

你將會(huì)遇到的最常見(jiàn)的非JavaScript程序是DOM API。例如:

var el = document.getElementById( "foo" );

當(dāng)你的代碼運(yùn)行在一個(gè)瀏覽器中時(shí),變量document作為一個(gè)全局變量存在。它不是由JS引擎提供的,也不為JavaScript語(yǔ)言規(guī)范所控制。它采取了某種與普通JSobject極其相似的形式,但它不是真正的object。它是一種特殊的object,經(jīng)常被稱為“宿主對(duì)象”。

另外,document上的getElementById(..)方法看起來(lái)像一個(gè)普通的JS函數(shù),但它只是一個(gè)微微暴露出來(lái)的接口,指向由瀏覽器DOM提供的內(nèi)建方法。在一些(新一代的)瀏覽器中,這一層可能也是由JS實(shí)現(xiàn)的,但是傳統(tǒng)的DOM和它的行為是由像C/C++這樣的語(yǔ)言實(shí)現(xiàn)的。

另一個(gè)例子是輸入/輸出(I/O)。

大家最喜愛(ài)的alert(..)在用戶的瀏覽器窗口中彈出一個(gè)消息框。alert(..)是由瀏覽器提供給你的JS程序的,而不是JS引擎本身。你進(jìn)行的調(diào)用將消息發(fā)送給瀏覽器內(nèi)部,它來(lái)處理消息框的繪制與顯示。

console.log()也一樣;你的瀏覽器提供這樣的機(jī)制并將它們掛在開(kāi)發(fā)者工具中。

這本書(shū),和整個(gè)這個(gè)系列,聚焦于JavaScript這種語(yǔ)言。這就是為什么你看不到任何涵蓋這些非JavaScript機(jī)制的重要內(nèi)容。不管怎樣,你需要小心它們,因?yàn)樗鼈儗⒃谀銓?xiě)的每一個(gè)JS程序中存在!

復(fù)習(xí)

學(xué)習(xí)JavaScript風(fēng)格編程的第一步是對(duì)它的核心機(jī)制有一個(gè)基本的了解,比如值,類型,函數(shù)閉包,this,和原型。

當(dāng)然,這些話題中的每一個(gè)都會(huì)衍生出比你在這里見(jiàn)到的多得多的內(nèi)容,這也是為什么它們?cè)谶@個(gè)系列剩下的部分中擁有自己的章節(jié)和書(shū)目。在你對(duì)本章中的概念和代碼示例感到相當(dāng)適應(yīng)之后,這個(gè)系列的其他部分正等著你真正地深入挖掘和了解這門語(yǔ)言。

這本書(shū)的最后一章將會(huì)對(duì)這個(gè)系列的每一卷的內(nèi)容,以及它們所涵蓋的我們?cè)谶@里還沒(méi)有探索過(guò)的概念,進(jìn)行簡(jiǎn)單地總結(jié)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容