[你不知道的 JavaScript 類型和語法] 第一章:類型

譯者的前言

一直都想好好研究這個在 GitHub 上頗有名氣的系列,而翻譯恰是最好的閱讀途徑之一。可讓我閱讀的時候,不那麼不求甚解。node

圖靈社區出版了該系列兩部分的中文版——《做用域和閉包》以及《this和對象原型》,我就打算從《類型和語法》這本開始作起。git

同時,我對本書的翻譯進度會在 GitHub 上同步,但願能有更多的同行參與進來,將更多的乾貨貢獻社區。github

PS:最近對於翻譯英文原版系列頗有興趣,若是有好的乾貨英文文章(且你們信得過個人水平),能夠放在評論區,有時間我必定會翻譯!設計模式

第一章:類型

大多數開發人員認爲,動態語言(如 JavaScript)並無類型。讓咱們來看看 ES5.1 的 規範 對於這部份內容是怎麼說的:數組

本規範中全部算法所操做的值都有一個類型與之對應。這些值的類型均在本規範中對應。固然,這些類型也多是 ECMAScript 語言中規定的類型的子類型。瀏覽器

在 ECMAScript 語言中,每一個 ECMAScript 類型所對應的值都被 ECMAScript 程序開發人員直接操做。ECMAScript 語言中規定的類型爲 Undefined, Null, Boolean, String, Number 以及Object。安全

若是你是強類型語言(靜態語言)的粉絲,你也許會對這樣使用「類型」感到很反感。在那些語言裏,「類型」所擁有的含義可比在 JS 裏的多得多。閉包

有人說 JS 不該該聲稱它有「類型,應該把這種東西稱爲「標籤」,或是「子類型」。

好吧。咱們將使用這一粗略的定義(相似於規範中所描述的):一個類型是一個固有的,內建的特徵集,不管是編譯引擎仍是開發人員,均可以用它來肯定一個值的行爲,並把這個值和其餘值加以區分。

簡單來講,若是在編譯引擎和開發人員眼裏,值 42(數字)和值 "42"(字符串)處理的方法不一樣,那麼咱們就說他們有不一樣的類型—— numberstring。當你處理 42 時,你將使用一些處理數字的方法,好比數學運算。而當你處理 "42" 時,你則會使用一些字符串處理方法,好比輸出到頁面,等等。這兩個值有不一樣的類型。

雖然這並非什麼嚴謹的定義,但對於咱們接下來的討論,已經綽綽有餘了。並且這樣的定義,和JS如何形容本身是一致的。

類型——或是別的什麼

不考慮學術上的爭論,咱們來想一想爲何 JavaScript 會須要類型

對於每種類型及其基本行爲都有所瞭解,有助於更高效的將值進行類型轉換(詳見第四章,類型轉換)。幾乎全部的JS程序,都存在着這樣那樣的類型轉換,因此瞭解這些,對你來講很重要。

若是你有一個值爲 42number,但想對它進行 string 類型的操做,如移除 1 位置的字符 "2",你最好先將這個值的類型從 number 轉換爲 string

這看似很簡單。

可是進行這樣的類型轉換,有不少方式。有些方式很明確,很簡單就能說出前因後果,而且也值得信賴.但若是你不夠細心,類型轉換可能以一種匪夷所思的方式展示在你面前。

類型轉換多是 JavaScript 最大的疑惑之一了。這點常常被視爲這一語言的缺陷,是應該避免使用的。

因爲有了對 JavaScript 類型的全面瞭解,咱們但願可以說明爲什麼類型轉換的壞名聲言過其實,甚至是不恰當的——咱們會改變你的傳統觀點,讓你看到類型轉換的強大力量和實用性。不過首先,咱們先來了解一下值和類型。

內建類型

JavaScript 定義了七種內建類型:

  • null

  • undefined

  • boolean

  • number

  • string

  • object

  • symbol —— ES6 中新增

提示:以上類型,除 object 的被稱爲基本類型。

typeof 運算符會檢測所給值得類型,並返回如下其中字符串類型的值——然而奇怪的是,返回的結果和咱們剛剛列出的的內建類型並不一一對應。

typeof undefined     === "undefined"; // true
typeof true          === "boolean";   // true
typeof 42            === "number";    // true
typeof "42"          === "string";    // true
typeof { life: 42 }  === "object";    // true

// ES6新增!
typeof Symbol()      === "symbol";    // true

列出的六種類型的值都會返回一個對應類型名稱的字符串。Symbol 是 ES6 中新增的數據類型,咱們會在第三章詳細介紹。

你也許注意到了,我將 null 從列表中除去了。由於他很特殊——當使用 typeof 運算符時,它表現的就像 bug 同樣:

typeof null === "object"; // true

若是它返回的是 "null" 的話,那可真是件好事,惋惜的是,這個 bug 已經存在了 20 年,並且因爲有太多的 web 程序依賴這一 bug 運行,修復這一 bug 的話,將會創造更多的 bug,而且使不少 web 應用沒法運行,因此估計未來也不會修復。

若是你想要肯定一個 null 類型的值是這一類型,你須要使用複合斷定:

var a = null;

(!a && typeof a === "object"); // true

null 是基本類型中惟一值表現的像 false 同樣的類型(詳見第四章),但若是運行 typeof 進行檢查,返回的仍是 "object"

那麼,typeof 返回的第七種字符串類型的值是什麼?

typeof function a(){ /* .. */ } === "function"; // true

單拍腦殼想的話,很容易理解 function(函數)會是 JS 中頂級的內建類型,尤爲是它針對 typeof 運算符的表現。然而,若是你閱讀相關的標準,會發現它其實是對象類型(object)的子類型。更確切的說,函數是一種「能夠被調用的對象」——一類擁有名爲 [[Call]] 的內建屬性且能夠被調用的對象。

函數其實是對象這點其實頗有用。最重要的一點就是,它能夠有屬性。例如:

function a(b,c) {
    /* .. */
}

該函數具備一個 length 屬性,值爲函數形式參數的個數。

a.length; // 2

本例中,函數聲明中包括兩個形參(bc),因此「函數的長度」是 2

那麼數組呢?他們也是 JS 內置的類型,會不會有什麼特殊的表現?

typeof [1,2,3] === "object"; // true

然而並無,只是普通的對象罷了。通常將它們也視爲對象的「子類型」(詳見第三章),與普通對象不一樣的是,它們能夠經過數字來序列化(就像普通對象那樣能夠經過字符串類型的 key(鍵)來序列化同樣),而且操做有能夠自動更新的 length 屬性。

值和類型

在 JavaScript 中,變量不具備類型——值有類型。變量能夠在任什麼時候刻保存任何值。

換句話說,JS 並非強類型的語言,編譯引擎不會讓一個變量始終保存和這個變量最開始所保存的值擁有相同的類型。變量能夠保存一個 string 類型的值,並在接下來的賦值操做中保存一個number類型,以此類推。

一個42number 類型的,並且這個類型是不能改變的。另外一個值,如 "42"string 類型,能夠經過對 number 類型的 42 進行類型轉換(詳見第四章)來獲得。

若是你用 typeof 運算符去操做一個變量,看上去就像是在求「變量是什麼類型?」,然而 JS 中的變量並不具備類型。因此,實際上是在求「變量中保存的值是什麼類型?」。

var a = 42;
typeof a; // "number"

a = true;
typeof a; // "boolean"

typeof 運算符返回的必然是字符串類型:

typeof typeof 42; // "string"

其中typeof 42會返回"number",而後typeof "number"就會返回"string"

undefined vs "undeclared"(未定義和未聲明)

當變量沒有被賦值的時候,其值爲 undefined。調用 typeof 運算符對它進行操做會返回 "undefined"

var a;

typeof a; // "undefined"

var b = 42;
var c;

// 而後另
b = c;

typeof b; // "undefined"
typeof c; // "undefined"

對於許多開發者都認爲「未定義(undefined)」至關因而「未聲明」的代名詞,然而在 JS 中,這兩個概念大相徑庭。

一個「未定義(undefined)」的變量是已經在當前做用域中聲明瞭的,只不過是目前它並無保存其餘的值而已。而「未聲明(undeclared)」則是指在當前做用域中沒有聲明的變量。

考慮以下的示例:

var a;

a; // undefined
b; // ReferenceError: b is not defined(錯誤的中文大意是:引用錯誤:b 還沒有定義)

瀏覽器對於這一錯誤的描述能夠說至關讓人困惑。「b 還沒有定義」很容易讓人理解成「b 是未定義」。而後,「未定義」和「還沒有定義」間的差異實在是太大了。若是瀏覽器要是能報個像「未找到變量 b」或是「b 還沒有聲明」之類的錯誤,就不會這麼讓人迷糊了。

一樣的,typeof 運算符的特殊行爲加劇了這一困惑,請看例子:

var a;

typeof a; // "undefined"

typeof b; // "undefined"

對於「未聲明」或着說「還沒有定義」的變量,typeof 會返回 "undefined"。你會發現,雖然 b 是一個沒有聲明的變量,可是當咱們執行 typeof b 的時候卻沒有報錯。會出現這種狀況,源於 typeof 運算符特殊的安全機制。

和前面的例子同樣,若是對於沒有聲明的變量,typeof 會返回一個「未聲明」之類的東西,而不是將其和「undefined」混爲一談的話,就不會有這麼多麻煩了。

typeof 對處理未聲明的處理

然而,在瀏覽器端這種,多個腳本文件都可以在全局命名空間下加載變量的 JavaScript 環境中,這種安全機制反而頗有用。

提示:許多開發者堅信,在全局命名空間下不該該有任何變量,全部的東西都應該在模塊或者是私有/分離的命名空間中。理論上,這很棒,並且確實是咱們追求的一個目標,然而在實踐中,這幾乎是不可能的。不過 ES6 中加入了對模塊的支持,這使得咱們可以更接近這一目標。

例如,在你的程序中,你經過一個全局變量 DEBUG 實現了一個調試模式。你但願在開始進行 debug,如在控制檯輸出一條調試信息以前,檢查這個變量是否已經聲明。你能夠將全局的 var DEBUG = true 聲明寫在一個名爲"debug.js"的文件夾下,當你在進行開發/測試下才在瀏覽器中引入,而不是在生產環境。

而你須要注意的,就是如何去在你的其餘代碼中檢查這個全局的 DEBUG 變量,畢竟你可不但願報一個 ReferenceError。在這種場景下,typeof 運算符就成了咱們的好幫手。

// 注意,這種方法會報錯!
if (DEBUG) {
    console.log( "Debugging is starting" );
}

// 更爲安全的檢查方式
if (typeof DEBUG !== "undefined") {
    console.log( "Debugging is starting" );
}

這類檢查不只對於用戶定義的變量頗有用,當你在見此一個內建的 API 的時候,這種不會拋出錯誤的檢查也很是棒:

if (typeof atob === "undefined") {
    atob = function() { /*..*/ };
}

提示:當你在對一個目前不存在的特性寫「polyfill(膩子腳本)」的時候,你須要避免用 var 來聲明變量 atob。若是你在 if 語句裏面使用 var atob 來聲明,即便 if 語句的條件不知足,變量的聲明也會被提高到做用域的最頂級(詳見本系列中的《做用域和閉包》)。在部分瀏覽器中,對一些特殊的全局的內建對象類型(常稱爲「宿主對象」,如瀏覽器中的 DOM 對象),這種重複的聲明會報錯。因此最好避免使用 var 來阻止變量提高。

另外一種不使用 typeof 安全機制,進行檢查的方法,就是利用全部的全局變量都是(global)全局對象(在瀏覽器中就是 window 對象)這一點。因此,上面的檢查還有以下等價的寫法(一樣很安全):

if (window.DEBUG) {
    // ..
}

if (!window.atob) {
    // ..
}

和引用一個未聲明的變量不一樣,當你嘗試獲取一個對象(即使是 window 對象)不存在的屬性的時候,並不會拋出什麼 ReferenceError

而另外一方面,一些開發者極力避免使用 window 對象來引用全局變量,尤爲是當你的代碼運行在多種 JS 環境(不光是瀏覽器,好比服務端的 node.js)時,全局(global)對象可不必定叫 window

即使當你不使用全局變量的時候,typeof 的安全機制也有它的用武之地,雖然這種狀況不多見,也有一些開發人員認爲這種設計並不值得。好比你準備寫一個可供他人複製粘貼的通用函數,想要知道程序中是否認義了某一特定的變量(將會影響你函數的執行),你能夠這樣:

function doSomethingCool() {
    var helper =
        (typeof FeatureXYZ !== "undefined") ?
        FeatureXYZ :
        function() { /*.. 默認值 ..*/ };

    var val = helper();
    // ..
}

doSomethingCool() 會檢查是否存在一個名爲 FeatureXYZ 的變量,有的話就使用,沒有的話,就使用默認值。如今,若是有人在他的程序/模塊中使用了這一公共函數,檢查它們是否認義了 FeatureXYZ 就顯得尤其重要:

// IIFE (詳見本系列《做用域和閉包》一書中的當即執行函數表達式)
(function(){
    function FeatureXYZ() { /*.. my XYZ feature ..*/ }

    // include `doSomethingCool(..)`
    function doSomethingCool() {
        var helper =
            (typeof FeatureXYZ !== "undefined") ?
            FeatureXYZ :
            function() { /*.. default feature ..*/ };

        var val = helper();
        // ..
    }

    doSomethingCool();
})();

在這裏,FeatureXYZ 並非一個全局變量,但咱們仍然使用 typeof 運算符的安全機制來檢查。注意到,在這種狀況下,咱們可沒有全局對象用於這一檢查(像使用 window.___ 那樣),因此 typeof 真的頗有幫助。

有些開發者可能會喜歡一種叫作「依賴注入」的設計模式,讓 doSomethingCool() 不去檢查 FeatureXYZ 是否在它外部/附近被定義,而是經過顯示的判斷來肯定,如:

function doSomethingCool(FeatureXYZ) {
    var helper = FeatureXYZ ||
        function() { /*.. 默認值 ..*/ };

    var val = helper();
    // ..
}

要實現這一功能,其實有不少解決方案。沒有一種模式是「對的」或「錯的」——要對各類方法進行權衡。不過總的來講,typeof 的安全機制確實給了咱們更多的選擇。

總結

JavaScript 擁有七種內建類型:nullundefinedbooleannumberstringobjectsymbol。能夠經過使用 typeof 運算符來對它們進行區分。

變量不具備類型,但值有。這些類型定義了值的行爲。

許多開發者會將「未定義(undefined)」和「未聲明」混爲一談,可是在 JavaScript 它們徹底不一樣。undefined是一個可供已經聲明的變量保存的值。「未聲明」意味着一個未經聲明的變量。

不幸的是,JavaScript 中不少地方都將二者混爲一談,好比錯誤信息("ReferenceError: a is not defined"),以及用 typeof 操做,二者都返回 "undefined"

不過,typeof 這種安全機制(阻止報錯)在某些場景中,如須要檢查一個變量是否存在的時候仍是頗有用的。


原書 《You Don't Know JS: Types & Grammar》
本章原文 Chapter 1: Types

相關文章
相關標籤/搜索