- 你不知道的JavaScript(中卷)
- (美)凱爾·辛普森
- 3581字
- 2019-01-05 10:14:48
第1章 類型
大多數開發者認為,像JavaScript這樣的動態語言是沒有類型(type)的。讓我們來看看ES5.1規范(http://www.ecma-international.org/ecma-262/5.1/)對此是如何界定的:
本規范中的運算法則所操縱的值均有相應的類型。本節中定義了所有可能出現的類型。ECMAScript類型又進一步細分為語言類型和規范類型。
ECMAScript語言中所有的值都有一個對應的語言類型。ECMAScript語言類型包括Undefined、Null、Boolean、String、Number和Object。
喜歡強類型(又稱靜態類型)語言的人也許會認為“類型”一詞用在這里不妥。“類型”在強類型語言中的涵義要廣很多。
也有人認為,JavaScript中的“類型”應該稱為“標簽”(tag)或者“子類型”(subtype)。
本書中,我們這樣來定義“類型”(與規范類似):對語言引擎和開發人員來說,類型是值的內部特征,它定義了值的行為,以使其區別于其他值。
換句話說,如果語言引擎和開發人員對42(數字)和"42"(字符串)采取不同的處理方式,那就說明它們是不同的類型,一個是number,一個是string。通常我們對數字42進行數學運算,而對字符串"42"進行字符串操作,比如輸出到頁面。它們是不同的類型。
上述定義并非完美,不過對于本書已經足夠,也和JavaScript語言對自身的描述一致。
1.1 類型
撇開學術界對類型定義的分歧,為什么說JavaScript是否有類型也很重要呢?
要正確合理地進行類型轉換(參見第4章),我們必須掌握JavaScript中的各個類型及其內在行為。幾乎所有的JavaScript程序都會涉及某種形式的強制類型轉換,處理這些情況時我們需要有充分的把握和自信。
如果要將42作為string來處理,比如獲得其中第二個字符"2",就需要將它從number(強制類型)轉換為string。
這看似簡單,但是強制類型轉換形式多樣。有些方式簡明易懂,也很安全,然而稍不留神,就會出現意想不到的結果。
強制類型轉換是JavaScript開發人員最頭疼的問題之一,它常被詬病為語言設計上的一個缺陷,太危險,應該束之高閣。
全面掌握JavaScript的類型之后,我們旨在改變對強制類型轉換的成見,看到它的好處并且意識到它的缺點被過分夸大了。現在先讓我們來深入了解一下值和類型。
1.2 內置類型
JavaScript有七種內置類型:
? 空值(null)
? 未定義(undefined)
? 布爾值(boolean)
? 數字(number)
? 字符串(string)
? 對象(object)
? 符號(symbol, ES6中新增)
除對象之外,其他統稱為“基本類型”。
我們可以用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
以上六種類型均有同名的字符串值與之對應。符號是ES6中新加入的類型,我們將在第3章中介紹。
你可能注意到null類型不在此列。它比較特殊,typeof對它的處理有問題:
typeof null === "object"; // true
正確的返回結果應該是"null",但這個bug由來已久,在JavaScript中已經存在了將近二十年,也許永遠也不會修復,因為這牽涉到太多的Web系統,“修復”它會產生更多的bug,令許多系統無法正常工作。
我們需要使用復合條件來檢測null值的類型:
var a = null; (! a && typeof a === "object"); // true
null是“假值”(falsy或者false-like,參見第4章),也是唯一一個用typeof檢測會返回"object"的基本類型值。
還有一種情況:
typeof function a(){ /* .. */ } === "function"; // true
這樣看來,function(函數)也是JavaScript的一個內置類型。然而查閱規范就會知道,它實際上是object的一個“子類型”。具體來說,函數是“可調用對象”,它有一個內部屬性[[Call]],該屬性使其可以被調用。
函數不僅是對象,還可以擁有屬性。例如:
function a(b, c) { /* .. */ }
函數對象的length屬性是其聲明的參數的個數:
a.length; // 2
因為該函數聲明了兩個命名參數,b和c,所以其length值為2。
再來看看數組。JavaScript支持數組,那么它是否也是一個特殊類型?
typeof [1,2,3] === "object"; // true
不,數組也是對象。確切地說,它也是object的一個“子類型”(參見第3章),數組的元素按數字順序來進行索引(而非像普通對象那樣通過字符串鍵值),其length屬性是元素的個數。
1.3 值和類型
JavaScript中的變量是沒有類型的,只有值才有。變量可以隨時持有任何類型的值。
換個角度來理解就是,JavaScript不做“類型強制”;也就是說,語言引擎不要求變量總是持有與其初始值同類型的值。一個變量可以現在被賦值為字符串類型值,隨后又被賦值為數字類型值。
42的類型為number,并且無法更改。而"42"的類型為string。數字42可以通過強制類型轉換(coercion)為字符串"42"(參見第4章)。
在對變量執行typeof操作時,得到的結果并不是該變量的類型,而是該變量持有的值的類型,因為JavaScript中的變量沒有類型。
var a = 42; typeof a; // "number" a = true; typeof a; // "boolean"
typeof運算符總是會返回一個字符串:
typeof typeof 42; // "string"
typeof 42首先返回字符串"number",然后typeof "number"返回"string"。
1.3.1 undefined和undeclared
變量在未持有值的時候為undefined。此時typeof返回"undefined":
var a; typeof a; // "undefined" var b = 42; var c; // later
b = c; typeof b; // "undefined" typeof c; // "undefined"
大多數開發者傾向于將undefined等同于undeclared(未聲明),但在JavaScript中它們完全是兩回事。
已在作用域中聲明但還沒有賦值的變量,是undefined的。相反,還沒有在作用域中聲明過的變量,是undeclared的。
例如:
var a; a; // undefined b; // ReferenceError: b is not defined
瀏覽器對這類情況的處理很讓人抓狂。上例中,“b is not defined”容易讓人誤以為是“b is undefined”。這里再強調一遍,“undefined”和“is not defined”是兩碼事。此時如果瀏覽器報錯成“b is not found”或者“b is not declared”會更準確。
更讓人抓狂的是typeof處理undeclared變量的方式。例如:
var a; typeof a; // "undefined" typeof b; // "undefined"
對于undeclared(或者not defined)變量,typeof照樣返回"undefined"。請注意雖然b是一個undeclared變量,但typeof b并沒有報錯。這是因為typeof有一個特殊的安全防范機制。
此時typeof如果能返回undeclared(而非undefined)的話,情況會好很多。
1.3.2 typeof Undeclared
該安全防范機制對在瀏覽器中運行的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" ); }
這不僅對用戶定義的變量(比如DEBUG)有用,對內建的API也有幫助:
if (typeof atob === "undefined") { atob = function() { /*..*/ }; }
如果要為某個缺失的功能寫polyfill(即襯墊代碼或者補充代碼,用來補充當前運行環境中缺失的功能),一般不會用var atob來聲明變量atob。如果在if語句中使用var atob,聲明會被提升(hoisted,參見《你不知道的JavaScript(上卷)》
中的“作用域和閉包”部分)到作用域(即當前腳本或函數的作用域)的最頂層,即使if條件不成立也是如此(因為atob全局變量已經存在)。在有些瀏覽器中,對于一些特殊的內建全局變量(通常稱為“宿主對象”, host object),這樣的重復聲明會報錯。去掉var則可以防止聲明被提升。
還有一種不用通過typeof的安全防范機制的方法,就是檢查所有全局變量是否是全局對象的屬性,瀏覽器中的全局對象是window。所以前面的例子也可以這樣來實現:
if (window.DEBUG) { // .. } if (! window.atob) { // .. }
與undeclared變量不同,訪問不存在的對象屬性(甚至是在全局對象window上)不會產生ReferenceError錯誤。
一些開發人員不喜歡通過window來訪問全局對象,尤其當代碼需要運行在多種JavaScript環境中時(不僅僅是瀏覽器,還有服務器端,如node.js等),因為此時全局對象并非總是window。
從技術角度來說,typeof的安全防范機制對于非全局變量也很管用,雖然這種情況并不多見,也有一些開發人員不大愿意這樣做。如果想讓別人在他們的程序或模塊中復制粘貼你的代碼,就需要檢查你用到的變量是否已經在宿主程序中定義過:
function doSomethingCool() { var helper = (typeof FeatureXYZ ! == "undefined") ? FeatureXYZ : function() { /*.. default feature ..*/ }; var val = helper(); // .. }
其他模塊和程序引入doSomethingCool()時,doSomethingCool()會檢查FeatureXYZ變量是否已經在宿主程序中定義過;如果是,就用現成的,否則就自己定義:
// 一個立即執行函數表達式(IIFE,參見《你不知道的JavaScript(上卷)》“作用域和閉包” // 部分的3.3.2節) (function(){ function FeatureXYZ() { /*.. my XYZ feature ..*/ } // 包含doSomethingCool(..) function doSomethingCool() { var helper = (typeof FeatureXYZ ! == "undefined") ? FeatureXYZ : function() { /*.. default feature ..*/ }; var val = helper(); // .. } doSomethingCool(); })();
這里,FeatureXYZ并不是一個全局變量,但我們還是可以使用typeof的安全防范機制來做檢查,因為這里沒有全局對象可用(像前面提到的window.)。
還有一些人喜歡使用“依賴注入”(dependency injection)設計模式,就是將依賴通過參數顯式地傳遞到函數中,如:
function doSomethingCool(FeatureXYZ) { var helper = FeatureXYZ || function() { /*.. default feature ..*/ }; var val = helper(); // .. }
上述種種選擇和方法各有利弊。好在typeof的安全防范機制為我們提供了更多選擇。
1.4 小結
JavaScript有七種內置類型:null、undefined、boolean、number、string、object和symbol,可以使用typeof運算符來查看。
變量沒有類型,但它們持有的值有類型。類型定義了值的行為特征。
很多開發人員將undefined和undeclared混為一談,但在JavaScript中它們是兩碼事。undefined是值的一種。undeclared則表示變量還沒有被聲明過。
遺憾的是,JavaScript卻將它們混為一談,在我們試圖訪問"undeclared"變量時這樣報錯:ReferenceError: a is not defined,并且typeof對undefined和undeclared變量都返回"undefined"。
然而,通過typeof的安全防范機制(阻止報錯)來檢查undeclared變量,有時是個不錯的辦法。
- Google Apps Script for Beginners
- Java EE 6 企業級應用開發教程
- SoapUI Cookbook
- ASP.NET Core 5.0開發入門與實戰
- Python Deep Learning
- Java從入門到精通(第5版)
- Cassandra Design Patterns(Second Edition)
- x86匯編語言:從實模式到保護模式(第2版)
- HTML5+CSS3+JavaScript Web開發案例教程(在線實訓版)
- Reactive Programming With Java 9
- GeoServer Beginner's Guide(Second Edition)
- 學習OpenCV 4:基于Python的算法實戰
- Linux Shell核心編程指南
- Python爬蟲、數據分析與可視化:工具詳解與案例實戰
- JavaScript應用開發實踐指南