任何語言的核心所描述的都是這門語言在最基本的層面上如何工作硫椰,涉及語法、操作符舅桩、數(shù)據(jù)類型
以及內(nèi)置功能,在此基礎(chǔ)之上才可以構(gòu)建復(fù)雜的解決方案雨膨。如前所述擂涛,ECMA-262以一個名為ECMAScript
的偽語言的形式,定義了 JavaScript 的所有這些方面聊记。
ECMA-262 第 5 版(ES5)定義的 ECMAScript撒妈,是目前為止實(shí)現(xiàn)得最為廣泛(即受瀏覽器支持最好)
的一個版本。第 6 版(ES6)在瀏覽器中的實(shí)現(xiàn)(即受支持)程度次之排监。到 2017 年底狰右,大多數(shù)主流瀏覽
器幾乎或全部實(shí)現(xiàn)了這一版的規(guī)范。為此舆床,本章接下來的內(nèi)容主要基于 ECMAScript 第 6 版棋蚌。
3.1 語法
ECMAScript 的語法很大程度上借鑒了 C 語言和其他類 C 語言嫁佳,如 Java和 Perl。熟悉這些語言的開
發(fā)者谷暮,應(yīng)該很容易理解 ECMAScript 寬松的語法蒿往。
3.1.1 區(qū)分大小寫
首先要知道的是,ECMAScript 中一切都區(qū)分大小寫湿弦。無論是變量瓤漏、函數(shù)名還是操作符,都區(qū)分大
小寫颊埃。換句話說蔬充,變量 test 和變量 Test 是兩個不同的變量。類似地竟秫, typeof 不能作為函數(shù)名,因
為它是一個關(guān)鍵字(后面會介紹)跷乐。但 Typeof 是一個完全有效的函數(shù)名肥败。
3.1.2 標(biāo)識符
所謂標(biāo)識符,就是變量愕提、函數(shù)馒稍、屬性或函數(shù)參數(shù)的名稱。標(biāo)識符可以由一或多個下列字符組成:
?? 第一個字符必須是一個字母浅侨、下劃線( _ )或美元符號( $ )纽谒;
?? 剩下的其他字符可以是字母、下劃線如输、美元符號或數(shù)字鼓黔。
標(biāo)識符中的字母可以是擴(kuò)展 ASCII(Extended ASCII)中的字母,也可以是 Unicode 的字母字符不见,
如 à 和 ?(但不推薦使用)澳化。
按照慣例,ECMAScript 標(biāo)識符使用駝峰大小寫形式稳吮,即第一個單詞的首字母小寫缎谷,后面每個單詞
的首字母大寫,如:firstSecond
myCar
doSomethingImportant
雖然這種寫法并不是強(qiáng)制性的灶似,但因?yàn)檫@種形式跟 ECMAScript 內(nèi)置函數(shù)和對象的命名方式一致列林,
所以算是最佳實(shí)踐。
注意 關(guān)鍵字酪惭、保留字希痴、 true 、 false 和 null 不能作為標(biāo)識符春感。具體內(nèi)容請參考 3.2 節(jié)润梯。
3.1.3 注釋
ECMAScript 采用 C 語言風(fēng)格的注釋,包括單行注釋和塊注釋。單行注釋以兩個斜杠字符開頭纺铭,如:
// 單行注釋
塊注釋以一個斜杠和一個星號( /* )開頭寇钉,以它們的反向組合( */ )結(jié)尾,如:
/* 這是多行
注釋 */
3.1.4 嚴(yán)格模式
ECMAScript 5 增加了嚴(yán)格模式(strict mode)的概念舶赔。嚴(yán)格模式是一種不同的 JavaScript 解析和執(zhí)
行模型扫倡,ECMAScript 3 的一些不規(guī)范寫法在這種模式下會被處理,對于不安全的活動將拋出錯誤竟纳。要對
整個腳本啟用嚴(yán)格模式撵溃,在腳本開頭加上這一行:
"use strict";
雖然看起來像個沒有賦值給任何變量的字符串,但它其實(shí)是一個預(yù)處理指令锥累。任何支持的 JavaScript
引擎看到它都會切換到嚴(yán)格模式缘挑。選擇這種語法形式的目的是不破壞 ECMAScript 3語法。
也可以單獨(dú)指定一個函數(shù)在嚴(yán)格模式下執(zhí)行桶略,只要把這個預(yù)處理指令放到函數(shù)體開頭即可:
function doSomething() {
"use strict";
// 函數(shù)體
}
嚴(yán)格模式會影響 JavaScript 執(zhí)行的很多方面语淘,因此本書在用到它時會明確指出來。所有現(xiàn)代瀏覽器
都支持嚴(yán)格模式际歼。
3.1.5 語句
ECMAScript 中的語句以分號結(jié)尾惶翻。省略分號意味著由解析器確定語句在哪里結(jié)尾,如下面的例子
所示:
let sum = a + b // 沒有分號也有效鹅心,但不推薦
let diff = a - b; // 加分號有效吕粗,推薦
即使語句末尾的分號不是必需的,也應(yīng)該加上旭愧。記著加分號有助于防止省略造成的問題颅筋,比如可以
避免輸入內(nèi)容不完整。此外输枯,加分號也便于開發(fā)者通過刪除空行來壓縮代碼(如果沒有結(jié)尾的分號垃沦,只
刪除空行,則會導(dǎo)致語法錯誤)用押。加分號也有助于在某些情況下提升性能肢簿,因?yàn)榻馕銎鲿L試在合適的
位置補(bǔ)上分號以糾正語法錯誤。多條語句可以合并到一個 C 語言風(fēng)格的代碼塊中蜻拨。代碼塊由一個左花括號( { )標(biāo)識開始池充,一個右
花括號( } )標(biāo)識結(jié)束:
if (test) {
test = false;
console.log(test);
}
if 之類的控制語句只在執(zhí)行多條語句時要求必須有代碼塊。不過缎讼,最佳實(shí)踐是始終在控制語句中
使用代碼塊收夸,即使要執(zhí)行的只有一條語句,如下例所示:
// 有效血崭,但容易導(dǎo)致錯誤卧惜,應(yīng)該避免
if (test)
console.log(test);
// 推薦
if (test) {
console.log(test);
}
在控制語句中使用代碼塊可以讓內(nèi)容更清晰厘灼,在需要修改代碼時也可以減少出錯的可能性。
3.2 關(guān)鍵字與保留字
ECMA-262 描述了一組保留的關(guān)鍵字咽瓷,這些關(guān)鍵字有特殊用途设凹,比如表示控制語句的開始和結(jié)束,
或者執(zhí)行特定的操作茅姜。按照規(guī)定闪朱,保留的關(guān)鍵字不能用作標(biāo)識符或?qū)傩悦CMA-262 第 6 版規(guī)定的所
有關(guān)鍵字如下:
break do in typeof
case else instanceof var
catch export new void
class extends return while
const finally super with
continue for switch yield
debugger function this
default if throw
delete import try
規(guī)范中也描述了一組未來的保留字钻洒,同樣不能用作標(biāo)識符或?qū)傩悦茏恕km然保留字在語言中沒有特定
用途,但它們是保留給將來做關(guān)鍵字用的素标。
以下是 ECMA-262 第 6 版為將來保留的所有詞匯称诗。
始終保留:
enum
嚴(yán)格模式下保留:
implements package public
interface protected static
let private
模塊代碼中保留:
await這些詞匯不能用作標(biāo)識符,但現(xiàn)在還可以用作對象的屬性名头遭。一般來說寓免,最好還是不要使用關(guān)鍵字
和保留字作為標(biāo)識符和屬性名,以確保兼容過去和未來的 ECMAScript 版本任岸。
3.3 變量
ECMAScript 變量是松散類型的再榄,意思是變量可以用于保存任何類型的數(shù)據(jù)狡刘。每個變量只不過是一
個用于保存任意值的命名占位符享潜。有 3 個關(guān)鍵字可以聲明變量: var 、 const 和 let 嗅蔬。其中剑按, var 在
ECMAScript 的所有版本中都可以使用,而 const 和 let 只能在 ECMAScript 6及更晚的版本中使用澜术。
3.3.1? var 關(guān)鍵字
要定義變量艺蝴,可以使用 var 操作符(注意 var 是一個關(guān)鍵字),后跟變量名(即標(biāo)識符鸟废,如前所述):
var message;
這行代碼定義了一個名為 message 的變量猜敢,可以用它保存任何類型的值。(不初始化的情況下盒延,變
量會保存一個特殊值 undefined 缩擂,下一節(jié)討論數(shù)據(jù)類型時會談到。)ECMAScript 實(shí)現(xiàn)變量初始化添寺,因
此可以同時定義變量并設(shè)置它的值:
var message = "hi";
這里胯盯, message 被定義為一個保存字符串值 hi 的變量。像這樣初始化變量不會將它標(biāo)識為字符串
類型计露,只是一個簡單的賦值而已博脑。隨后憎乙,不僅可以改變保存的值,也可以改變值的類型:
var message = "hi";
message = 100; //? 合法叉趣,但不推薦
在這個例子中泞边,變量 message 首先被定義為一個保存字符串值 hi 的變量,然后又被重寫為保存了
數(shù)值 100君账。雖然不推薦改變變量保存值的類型繁堡,但這在 ECMAScript 中是完全有效的。
1.? var 聲明作用域
關(guān)鍵的問題在于乡数,使用 var 操作符定義的變量會成為包含它的函數(shù)的局部變量椭蹄。比如,使用 var
在一個函數(shù)內(nèi)部定義一個變量净赴,就意味著該變量將在函數(shù)退出時被銷毀:
function test() {
var message = "hi"; // 局部變量
}
test();
console.log(message); // 出錯绳矩!
這里, message 變量是在函數(shù)內(nèi)部使用 var 定義的玖翅。函數(shù)叫 test() 翼馆,調(diào)用它會創(chuàng)建這個變量并給
它賦值。調(diào)用之后變量隨即被銷毀金度,因此示例中的最后一行會導(dǎo)致錯誤应媚。不過,在函數(shù)內(nèi)定義變量時省
略 var 操作符猜极,可以創(chuàng)建一個全局變量:
function test() {
message = "hi"; //? 全局變量
}
test();
console.log(message); // "hi"
去掉之前的 var 操作符之后中姜, message 就變成了全局變量。只要調(diào)用一次函數(shù) test() 跟伏,就會定義
這個變量丢胚,并且可以在函數(shù)外部訪問到。注意 雖然可以通過省略 var 操作符定義全局變量受扳,但不推薦這么做携龟。在局部作用域中定
義的全局變量很難維護(hù),也會造成困惑勘高。這是因?yàn)椴荒芤幌伦訑喽ㄊ÷?var 是不是有意而
為之峡蟋。在嚴(yán)格模式下,如果像這樣給未聲明的變量賦值华望,則會導(dǎo)致拋出 ReferenceError 招刹。
如果需要定義多個變量杠步,可以在一條語句中用逗號分隔每個變量(及可選的初始化):
var message = "hi",
found = false,
age = 29;
這里定義并初始化了 3 個變量。因?yàn)?ECMAScript 是松散類型的,所以使用不同數(shù)據(jù)類型初始化的
變量可以用一條語句來聲明淹禾。插入換行和空格縮進(jìn)并不是必需的雪标,但這樣有利于閱讀理解。
在嚴(yán)格模式下,不能定義名為 eval 和 arguments 的變量裕偿,否則會導(dǎo)致語法錯誤。
2.? var 聲明提升
使用 var 時痛单,下面的代碼不會報(bào)錯嘿棘。這是因?yàn)槭褂眠@個關(guān)鍵字聲明的變量會自動提升到函數(shù)作用域
頂部:
function foo() {
console.log(age);
var age = 26;
}
foo(); // undefined
之所以不會報(bào)錯,是因?yàn)?ECMAScript 運(yùn)行時把它看成等價于如下代碼:
function foo() {
var age;
console.log(age);
age = 26;
}
foo(); // undefined
這就是所謂的“提升”(hoist)旭绒,也就是把所有變量聲明都拉到函數(shù)作用域的頂部鸟妙。此外,反復(fù)多次
使用 var 聲明同一個變量也沒有問題:
function foo() {
var age = 16;
var age = 26;
var age = 36;
console.log(age);
}
foo(); // 36
3.3.2? let 聲明
let 跟 var 的作用差不多挥吵,但有著非常重要的區(qū)別重父。最明顯的區(qū)別是, let 聲明的范圍是塊作用域忽匈,
而 var 聲明的范圍是函數(shù)作用域房午。
if (true) {
var name = 'Matt';
console.log(name); // Matt
}
console.log(name); // Mattif (true) {
let age = 26;
console.log(age); // 26
}
console.log(age); // ReferenceError: age 沒有定義
在這里, age 變量之所以不能在 if 塊外部被引用丹允,是因?yàn)樗淖饔糜騼H限于該塊內(nèi)部郭厌。塊作用域
是函數(shù)作用域的子集,因此適用于 var 的作用域限制同樣也適用于 let 雕蔽。
let 也不允許同一個塊作用域中出現(xiàn)冗余聲明折柠。這樣會導(dǎo)致報(bào)錯:
var name;
var name;
let age;
let age; // SyntaxError;標(biāo)識符 age 已經(jīng)聲明過了
當(dāng)然批狐,JavaScript 引擎會記錄用于變量聲明的標(biāo)識符及其所在的塊作用域扇售,因此嵌套使用相同的標(biāo)
識符不會報(bào)錯,而這是因?yàn)橥粋€塊中沒有重復(fù)聲明:
var name = 'Nicholas';
console.log(name); // 'Nicholas'
if (true) {
var name = 'Matt';
console.log(name); // 'Matt'
}
let age = 30;
console.log(age); // 30
if (true) {
let age = 26;
console.log(age); // 26
}
對聲明冗余報(bào)錯不會因混用 let 和 var 而受影響贾陷。這兩個關(guān)鍵字聲明的并不是不同類型的變量缘眶,
它們只是指出變量在相關(guān)作用域如何存在嘱根。
var name;
let name; // SyntaxError
let age;
var age; // SyntaxError
1. 暫時性死區(qū)
let 與 var 的另一個重要的區(qū)別髓废,就是 let 聲明的變量不會在作用域中被提升。
// name 會被提升
console.log(name); // undefined
var name = 'Matt';
// age 不會被提升
console.log(age); // ReferenceError:age 沒有定義
let age = 26;
在解析代碼時该抒,JavaScript 引擎也會注意出現(xiàn)在塊后面的 let 聲明慌洪,只不過在此之前不能以任何方
式來引用未聲明的變量。在 let 聲明之前的執(zhí)行瞬間被稱為“暫時性死區(qū)”(temporal dead zone)凑保,在此
階段引用任何后面才聲明的變量都會拋出 ReferenceError 冈爹。2. 全局聲明
與 var 關(guān)鍵字不同,使用 let 在全局作用域中聲明的變量不會成為 window 對象的屬性( var 聲
明的變量則會)欧引。
var name = 'Matt';
console.log(window.name); // 'Matt'
let age = 26;
console.log(window.age); // undefined
不過频伤, let 聲明仍然是在全局作用域中發(fā)生的,相應(yīng)變量會在頁面的生命周期內(nèi)存續(xù)芝此。因此憋肖,為了
避免 SyntaxError 因痛,必須確保頁面不會重復(fù)聲明同一個變量。
3. 條件聲明
在使用 var 聲明變量時岸更,由于聲明會被提升鸵膏,JavaScript 引擎會自動將多余的聲明在作用域頂部合
并為一個聲明。因?yàn)?let 的作用域是塊怎炊,所以不可能檢查前面是否已經(jīng)使用 let 聲明過同名變量谭企,同
時也就不可能在沒有聲明的情況下聲明它。
<script>
var name = 'Nicholas';
let age = 26;
</script>
<script>
// 假設(shè)腳本不確定頁面中是否已經(jīng)聲明了同名變量
// 那它可以假設(shè)還沒有聲明過
var name = 'Matt';
// 這里沒問題评肆,因?yàn)榭梢员蛔鳛橐粋€提升聲明來處理
// 不需要檢查之前是否聲明過同名變量
let age = 36;
// 如果 age 之前聲明過债查,這里會報(bào)錯
</script>
使用 try / catch 語句或 typeof 操作符也不能解決,因?yàn)闂l件塊中 let 聲明的作用域僅限于該塊瓜挽。
<script>
let name = 'Nicholas';
let age = 36;
</script>
<script>
// 假設(shè)腳本不確定頁面中是否已經(jīng)聲明了同名變量
// 那它可以假設(shè)還沒有聲明過
if (typeof name === 'undefined') {
let name;
}
// name 被限制在 if {} 塊的作用域內(nèi)
// 因此這個賦值形同全局賦值
name = 'Matt';
try {
console.log(age); // 如果 age 沒有聲明過攀操,則會報(bào)錯
}
catch(error) {
let age;}
// age 被限制在 catch {}塊的作用域內(nèi)
// 因此這個賦值形同全局賦值
age = 26;
</script>
為此,對于 let 這個新的 ES6 聲明關(guān)鍵字秸抚,不能依賴條件聲明模式速和。
注意 不能使用 let 進(jìn)行條件式聲明是件好事,因?yàn)闂l件聲明是一種反模式剥汤,它讓程序變
得更難理解颠放。如果你發(fā)現(xiàn)自己在使用這個模式,那一定有更好的替代方式吭敢。
4.? for 循環(huán)中的 let 聲明
在 let 出現(xiàn)之前碰凶, for 循環(huán)定義的迭代變量會滲透到循環(huán)體外部:
for (var i = 0; i < 5; ++i) {
// 循環(huán)邏輯
}
console.log(i); // 5
改成使用 let 之后,這個問題就消失了鹿驼,因?yàn)榈兞康淖饔糜騼H限于 for 循環(huán)塊內(nèi)部:
for (let i = 0; i < 5; ++i) {
// 循環(huán)邏輯
}
console.log(i); // ReferenceError: i 沒有定義
在使用 var 的時候欲低,最常見的問題就是對迭代變量的奇特聲明和修改:
for (var i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
// 你可能以為會輸出 0、1畜晰、2砾莱、3、4
// 實(shí)際上會輸出 5凄鼻、5腊瑟、5、5块蚌、5
之所以會這樣闰非,是因?yàn)樵谕顺鲅h(huán)時,迭代變量保存的是導(dǎo)致循環(huán)退出的值:5峭范。在之后執(zhí)行超時
邏輯時财松,所有的 i 都是同一個變量,因而輸出的都是同一個最終值纱控。
而在使用 let 聲明迭代變量時辆毡,JavaScript 引擎在后臺會為每個迭代循環(huán)聲明一個新的迭代變量政敢。
每個 setTimeout 引用的都是不同的變量實(shí)例,所以 console.log 輸出的是我們期望的值胚迫,也就是循
環(huán)執(zhí)行過程中每個迭代變量的值喷户。
for (let i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
// 會輸出 0、1访锻、2褪尝、3、4
這種每次迭代聲明一個獨(dú)立變量實(shí)例的行為適用于所有風(fēng)格的 for 循環(huán)期犬,包括 for-in 和 for-of
循環(huán)河哑。
3.3.3? const 聲明
const 的行為與 let 基本相同,唯一一個重要的區(qū)別是用它聲明變量時必須同時初始化變量龟虎,且
嘗試修改 const 聲明的變量會導(dǎo)致運(yùn)行時錯誤璃谨。
const age = 26;
age = 36; // TypeError: 給常量賦值// const 也不允許重復(fù)聲明
const name = 'Matt';
const name = 'Nicholas'; // SyntaxError
// const 聲明的作用域也是塊
const name = 'Matt';
if (true) {
const name = 'Nicholas';
}
console.log(name); // Matt
const 聲明的限制只適用于它指向的變量的引用。換句話說鲤妥,如果 const 變量引用的是一個對象佳吞,
那么修改這個對象內(nèi)部的屬性并不違反 const 的限制。
const person = {};
person.name = 'Matt'; // ok
JavaScript 引擎會為 for 循環(huán)中的 let 聲明分別創(chuàng)建獨(dú)立的變量實(shí)例棉安,雖然 const 變量跟 let 變
量很相似底扳,但是不能用 const 來聲明迭代變量(因?yàn)榈兞繒栽觯?/p>
for (const i = 0; i < 10; ++i) {} // TypeError:給常量賦值
不過,如果你只想用 const 聲明一個不會被修改的 for 循環(huán)變量贡耽,那也是可以的衷模。也就是說,每
次迭代只是創(chuàng)建一個新變量蒲赂。這對 for-of 和 for-in 循環(huán)特別有意義:
let i = 0;
for (const j = 7; i < 5; ++i) {
console.log(j);
}
// 7, 7, 7, 7, 7
for (const key in {a: 1, b: 2}) {
console.log(key);
}
// a, b
for (const value of [1,2,3,4,5]) {
console.log(value);
}
// 1, 2, 3, 4, 5
3.3.4 聲明風(fēng)格及最佳實(shí)踐
ECMAScript 6 增加 let 和 const 從客觀上為這門語言更精確地聲明作用域和語義提供了更好的支
持阱冶。行為怪異的 var 所造成的各種問題,已經(jīng)讓 JavaScript 社區(qū)為之苦惱了很多年滥嘴。隨著這兩個新關(guān)鍵
字的出現(xiàn)木蹬,新的有助于提升代碼質(zhì)量的最佳實(shí)踐也逐漸顯現(xiàn)。
1. 不使用 var
有了 let 和 const 氏涩,大多數(shù)開發(fā)者會發(fā)現(xiàn)自己不再需要 var 了届囚。限制自己只使用 let 和 const
有助于提升代碼質(zhì)量有梆,因?yàn)樽兞坑辛嗣鞔_的作用域是尖、聲明位置,以及不變的值泥耀。
2.? const 優(yōu)先饺汹, let 次之
使用 const 聲明可以讓瀏覽器運(yùn)行時強(qiáng)制保持變量不變,也可以讓靜態(tài)代碼分析工具提前發(fā)現(xiàn)不
合法的賦值操作痰催。因此兜辞,很多開發(fā)者認(rèn)為應(yīng)該優(yōu)先使用 const 來聲明變量迎瞧,只在提前知道未來會有修改時,再使用 let 逸吵。這樣可以讓開發(fā)者更有信心地推斷某些變量的值永遠(yuǎn)不會變凶硅,同時也能迅速發(fā)現(xiàn)因
意外賦值導(dǎo)致的非預(yù)期行為。
3.4 數(shù)據(jù)類型
ECMAScript 有 6 種簡單數(shù)據(jù)類型(也稱為原始類型): Undefined 扫皱、 Null 足绅、 Boolean 、 Number 韩脑、
String 和 Symbol 氢妈。 Symbol (符號)是 ECMAScript 6 新增的。還有一種復(fù)雜數(shù)據(jù)類型叫 Object (對
象)段多。 Object 是一種無序名值對的集合首量。因?yàn)樵?ECMAScript 中不能定義自己的數(shù)據(jù)類型,所有值都可
以用上述 7 種數(shù)據(jù)類型之一來表示进苍。只有 7 種數(shù)據(jù)類型似乎不足以表示全部數(shù)據(jù)加缘。但 ECMAScript的數(shù)
據(jù)類型很靈活,一種數(shù)據(jù)類型可以當(dāng)作多種數(shù)據(jù)類型來使用觉啊。
3.4.1? typeof 操作符
因?yàn)?ECMAScript 的類型系統(tǒng)是松散的生百,所以需要一種手段來確定任意變量的數(shù)據(jù)類型。 typeof
操作符就是為此而生的柄延。對一個值使用 typeof 操作符會返回下列字符串之一:
? "undefined" 表示值未定義蚀浆;
? "boolean" 表示值為布爾值;
? "string" 表示值為字符串搜吧;
? "number" 表示值為數(shù)值市俊;
? "object" 表示值為對象(而不是函數(shù))或 null ;
? "function" 表示值為函數(shù)滤奈;
? "symbol" 表示值為符號摆昧。
下面是使用 typeof 操作符的例子:
let message = "some string";
console.log(typeof message); // "string"
console.log(typeof(message)); // "string"
console.log(typeof 95); // "number"
在這個例子中,我們把一個變量( message )和一個數(shù)值字面量傳給了 typeof 操作符蜒程。注意绅你,因
為 typeof 是一個操作符而不是函數(shù),所以不需要參數(shù)(但可以使用參數(shù))昭躺。
注意 typeof 在某些情況下返回的結(jié)果可能會讓人費(fèi)解忌锯,但技術(shù)上講還是正確的。比如领炫,調(diào)用 typeof
null 返回的是 "object" 偶垮。這是因?yàn)樘厥庵?null 被認(rèn)為是一個對空對象的引用。
注意 嚴(yán)格來講,函數(shù)在 ECMAScript 中被認(rèn)為是對象似舵,并不代表一種數(shù)據(jù)類型脚猾。可是砚哗,
函數(shù)也有自己特殊的屬性龙助。為此,就有必要通過 typeof 操作符來區(qū)分函數(shù)和其他對象蛛芥。
3.4.2? Undefined 類型
Undefined 類型只有一個值泌参,就是特殊值 undefined 。當(dāng)使用 var 或 let 聲明了變量但沒有初始
化時常空,就相當(dāng)于給變量賦予了 undefined 值:
let message;
console.log(message == undefined); // true在這個例子中沽一,變量 message 在聲明的時候并未初始化。而在比較它和 undefined 的字面值時漓糙,
兩者是相等的铣缠。這個例子等同于如下示例:
let message = undefined;
console.log(message == undefined); // true
這里,變量 message 顯式地以 undefined 來初始化昆禽。但這是不必要的蝗蛙,因?yàn)槟J(rèn)情況下,任何未
經(jīng)初始化的變量都會取得 undefined 值醉鳖。
注意 一般來說捡硅,永遠(yuǎn)不用顯式地給某個變量設(shè)置 undefined 值。字面值 undefined
主要用于比較盗棵,而且在 ECMA-262 第 3 版之前是不存在的壮韭。增加這個特殊值的目的就是為
了正式明確空對象指針( null )和未初始化變量的區(qū)別。
注意纹因,包含 undefined 值的變量跟未定義變量是有區(qū)別的喷屋。請看下面的例子:
let message; // 這個變量被聲明了,只是值為 undefined
// 確保沒有聲明過這個變量
// let age
console.log(message); // "undefined"
console.log(age); // 報(bào)錯
在上面的例子中瞭恰,第一個 console.log 會指出變量 message 的值屯曹,即 "undefined" 。而第二個
console.log 要輸出一個未聲明的變量 age 的值惊畏,因此會導(dǎo)致報(bào)錯恶耽。對未聲明的變量,只能執(zhí)行一個
有用的操作颜启,就是對它調(diào)用 typeof 偷俭。(對未聲明的變量調(diào)用 delete 也不會報(bào)錯,但這個操作沒什么用农曲,
實(shí)際上在嚴(yán)格模式下會拋出錯誤社搅。)
在對未初始化的變量調(diào)用 typeof 時驻债,返回的結(jié)果是 "undefined" 乳规,但對未聲明的變量調(diào)用它時形葬,
返回的結(jié)果還是 "undefined" ,這就有點(diǎn)讓人看不懂了暮的。比如下面的例子:
let message; // 這個變量被聲明了笙以,只是值為 undefined
// 確保沒有聲明過這個變量
// let age
console.log(typeof message); // "undefined"
console.log(typeof age); // "undefined"
無論是聲明還是未聲明, typeof 返回的都是字符串 "undefined" 冻辩。邏輯上講這是對的猖腕,因?yàn)殡m然
嚴(yán)格來講這兩個變量存在根本性差異,但它們都無法執(zhí)行實(shí)際操作恨闪。
注意 即使未初始化的變量會被自動賦予 undefined 值倘感,但我們?nèi)匀唤ㄗh在聲明變量的
同時進(jìn)行初始化。這樣咙咽,當(dāng) typeof 返回 "undefined" 時老玛,你就會知道那是因?yàn)榻o定的變
量尚未聲明,而不是聲明了但未初始化钧敞。
undefined 是一個假值蜡豹。因此,如果需要溉苛,可以用更簡潔的方式檢測它镜廉。不過要記住,也有很多
其他可能的值同樣是假值愚战。所以一定要明確自己想檢測的就是 undefined 這個字面值娇唯,而不僅僅是
假值。3.4.4 Boolean 類型
Boolean (布爾值)類型是 ECMAScript 中使用最頻繁的類型之一寂玲,有兩個字面值: true 和 false 视乐。
這兩個布爾值不同于數(shù)值,因此 true 不等于 1敢茁, false 不等于 0佑淀。下面是給變量賦布爾值的例子:
let found = true;
let lost = false;
注意,布爾值字面量 true 和 false 是區(qū)分大小寫的彰檬,因此 True 和 False (及其他大小混寫形式)
是有效的標(biāo)識符伸刃,但不是布爾值。
雖然布爾值只有兩個逢倍,但所有其他 ECMAScript 類型的值都有相應(yīng)布爾值的等價形式捧颅。要將一個其
他類型的值轉(zhuǎn)換為布爾值,可以調(diào)用特定的 Boolean() 轉(zhuǎn)型函數(shù):
let message = "Hello world!";
let messageAsBoolean = Boolean(message);
在這個例子中较雕,字符串 message 會被轉(zhuǎn)換為布爾值并保存在變量 messageAsBoolean 中碉哑。
Boolean() 轉(zhuǎn)型函數(shù)可以在任意類型的數(shù)據(jù)上調(diào)用挚币,而且始終返回一個布爾值。什么值能轉(zhuǎn)換為 true
或 false 的規(guī)則取決于數(shù)據(jù)類型和實(shí)際的值扣典。下表總結(jié)了不同類型與布爾值之間的轉(zhuǎn)換規(guī)則妆毕。
數(shù)據(jù)類型? 轉(zhuǎn)換為 true 的值? 轉(zhuǎn)換為 false 的值
Boolean true false
String 非空字符串? "" (空字符串)
Number 非零數(shù)值(包括無窮值)? 0 、 NaN (參見后面的相關(guān)內(nèi)容)
Object 任意對象? null
Undefined N/A (不存在)? undefined
理解以上轉(zhuǎn)換非常重要贮尖,因?yàn)橄?if 等流控制語句會自動執(zhí)行其他類型值到布爾值的轉(zhuǎn)換笛粘,例如:
let message = "Hello world!";
if (message) {
console.log("Value is true");
}
在這個例子中, console.log 會輸出字符串 "Value is true" 湿硝,因?yàn)樽址?message 會被自動
轉(zhuǎn)換為等價的布爾值 true 薪前。由于存在這種自動轉(zhuǎn)換,理解流控制語句中使用的是什么變量就非常重要关斜。
錯誤地使用對象而不是布爾值會明顯改變應(yīng)用程序的執(zhí)行流示括。
3.4.5? Number 類型
ECMAScript 中最有意思的數(shù)據(jù)類型或許就是 Number 了。 Number 類型使用 IEEE 754格式表示整
數(shù)和浮點(diǎn)值(在某些語言中也叫雙精度值)痢畜。不同的數(shù)值類型相應(yīng)地也有不同的數(shù)值字面量格式垛膝。最基本的數(shù)值字面量格式是十進(jìn)制整數(shù),直接寫出來即可:
let intNum = 55; // 整數(shù)
整數(shù)也可以用八進(jìn)制(以 8 為基數(shù))或十六進(jìn)制(以 16 為基數(shù))字面量表示裁着。對于八進(jìn)制字面量繁涂,
第一個數(shù)字必須是零(0),然后是相應(yīng)的八進(jìn)制數(shù)字(數(shù)值 0~7)二驰。如果字面量中包含的數(shù)字超出了應(yīng)
有的范圍扔罪,就會忽略前綴的零,后面的數(shù)字序列會被當(dāng)成十進(jìn)制數(shù)桶雀,如下所示:
let octalNum1 = 070; // 八進(jìn)制的 56
let octalNum2 = 079; // 無效的八進(jìn)制值矿酵,當(dāng)成 79 處理
let octalNum3 = 08; // 無效的八進(jìn)制值,當(dāng)成 8 處理
八進(jìn)制字面量在嚴(yán)格模式下是無效的矗积,會導(dǎo)致 JavaScript 引擎拋出語法錯誤全肮。
①
要創(chuàng)建十六進(jìn)制字面量,必須讓真正的數(shù)值前綴 0x (區(qū)分大小寫)棘捣,然后是十六進(jìn)制數(shù)字(0~9 以
及 A~F)辜腺。十六進(jìn)制數(shù)字中的字母大小寫均可。下面是幾個例子:
let hexNum1 = 0xA; // 十六進(jìn)制 10
let hexNum2 = 0x1f; // 十六進(jìn)制 31
使用八進(jìn)制和十六進(jìn)制格式創(chuàng)建的數(shù)值在所有數(shù)學(xué)操作中都被視為十進(jìn)制數(shù)值乍恐。
注意 由于 JavaScript 保存數(shù)值的方式评疗,實(shí)際中可能存在正零(+0)和負(fù)零(?0)。正零和
負(fù)零在所有情況下都被認(rèn)為是等同的茵烈,這里特地說明一下百匆。
1. 浮點(diǎn)值
要定義浮點(diǎn)值,數(shù)值中必須包含小數(shù)點(diǎn)呜投,而且小數(shù)點(diǎn)后面必須至少有一個數(shù)字加匈。雖然小數(shù)點(diǎn)前面不
是必須有整數(shù)存璃,但推薦加上。下面是幾個例子:
let floatNum1 = 1.1;
let floatNum2 = 0.1;
let floatNum3 = .1; // 有效雕拼,但不推薦
因?yàn)榇鎯Ω↑c(diǎn)值使用的內(nèi)存空間是存儲整數(shù)值的兩倍纵东,所以 ECMAScript 總是想方設(shè)法把值轉(zhuǎn)換為
整數(shù)。在小數(shù)點(diǎn)后面沒有數(shù)字的情況下悲没,數(shù)值就會變成整數(shù)篮迎。類似地男图,如果數(shù)值本身就是整數(shù)示姿,只是小
數(shù)點(diǎn)后面跟著 0(如 1.0),那它也會被轉(zhuǎn)換為整數(shù)逊笆,如下例所示:
let floatNum1 = 1.; // 小數(shù)點(diǎn)后面沒有數(shù)字栈戳,當(dāng)成整數(shù) 1 處理
let floatNum2 = 10.0; // 小數(shù)點(diǎn)后面是零,當(dāng)成整數(shù) 10 處理
對于非常大或非常小的數(shù)值难裆,浮點(diǎn)值可以用科學(xué)記數(shù)法來表示子檀。科學(xué)記數(shù)法用于表示一個應(yīng)該乘以
10 的給定次冪的數(shù)值乃戈。ECMAScript 中科學(xué)記數(shù)法的格式要求是一個數(shù)值(整數(shù)或浮點(diǎn)數(shù))后跟一個大
寫或小寫的字母 e褂痰,再加上一個要乘的 10 的多少次冪。比如:
let floatNum = 3.125e7; // 等于 31250000
在這個例子中症虑, floatNum 等于 31 250 000缩歪,只不過科學(xué)記數(shù)法顯得更簡潔。這種表示法實(shí)際上相
當(dāng)于說:“以 3.125 作為系數(shù)谍憔,乘以 10 的 7 次冪匪蝙。”
科學(xué)記數(shù)法也可以用于表示非常小的數(shù)值习贫,例如 0.000 000 000 000 000 03逛球。這個數(shù)值用科學(xué)記數(shù)法
可以表示為 3e?17。默認(rèn)情況下苫昌,ECMAScript 會將小數(shù)點(diǎn)后至少包含 6 個零的浮點(diǎn)值轉(zhuǎn)換為科學(xué)記數(shù)法(例如颤绕,0.000 000 3 會被轉(zhuǎn)換為 3e?7)。
浮點(diǎn)值的精確度最高可達(dá) 17 位小數(shù)祟身,但在算術(shù)計(jì)算中遠(yuǎn)不如整數(shù)精確奥务。例如,0.1 加 0.2 得到的不
是 0.3月而,而是 0.300 000 000 000 000 04汗洒。由于這種微小的舍入錯誤,導(dǎo)致很難測試特定的浮點(diǎn)值父款。比如下
面的例子:
if (a + b == 0.3) { // 別這么干溢谤!
console.log("You got 0.3.");
}
這里檢測兩個數(shù)值之和是否等于 0.3瞻凤。如果兩個數(shù)值分別是 0.05 和 0.25,或者 0.15 和 0.15世杀,那沒問
題阀参。但如果是 0.1 和 0.2,如前所述瞻坝,測試將失敗蛛壳。因此永遠(yuǎn)不要測試某個特定的浮點(diǎn)值。
注意 之所以存在這種舍入錯誤所刀,是因?yàn)槭褂昧?IEEE 754數(shù)值衙荐,這種錯誤并非 ECMAScript
所獨(dú)有。其他使用相同格式的語言也有這個問題浮创。
2. 值的范圍
由于內(nèi)存的限制忧吟,ECMAScript 并不支持表示這個世界上的所有數(shù)值。ECMAScript 可以表示的最小
數(shù)值保存在 Number.MIN_VALUE 中斩披,這個值在多數(shù)瀏覽器中是 5e?324溜族;可以表示的最大數(shù)值保存在
Number.MAX_VALUE 中,這個值在多數(shù)瀏覽器中是 1.797 693 134 862 315 7e+308垦沉。如果某個計(jì)算得到的
數(shù)值結(jié)果超出了 JavaScript 可以表示的范圍煌抒,那么這個數(shù)值會被自動轉(zhuǎn)換為一個特殊的 Infinity (無
窮)值。任何無法表示的負(fù)數(shù)以 -Infinity (負(fù)無窮大)表示厕倍,任何無法表示的正數(shù)以 Infinity (正
無窮大)表示寡壮。
如果計(jì)算返回正 Infinity 或負(fù) Infinity ,則該值將不能再進(jìn)一步用于任何計(jì)算绑青。這是因?yàn)?/p>
Infinity 沒有可用于計(jì)算的數(shù)值表示形式诬像。要確定一個值是不是有限大(即介于 JavaScript 能表示的
最小值和最大值之間),可以使用 isFinite() 函數(shù)闸婴,如下所示:
let result = Number.MAX_VALUE + Number.MAX_VALUE;
console.log(isFinite(result)); // false
雖然超出有限數(shù)值范圍的計(jì)算并不多見坏挠,但總歸還是有可能的。因此在計(jì)算非常大或非常小的數(shù)值
時邪乍,有必要監(jiān)測一下計(jì)算結(jié)果是否超出范圍降狠。
注意 使用 Number.NEGATIVE_INFINITY 和 Number.POSITIVE_INFINITY 也可以獲
取正、負(fù) Infinity 庇楞。沒錯榜配,這兩個屬性包含的值分別就是 -Infinity 和 Infinity 。
3.? NaN
有一個特殊的數(shù)值叫 NaN 吕晌,意思是“不是數(shù)值”(Not a Number)蛋褥,用于表示本來要返回?cái)?shù)值的操作
失敗了(而不是拋出錯誤)。比如睛驳,用 0 除任意數(shù)值在其他語言中通常都會導(dǎo)致錯誤烙心,從而中止代碼執(zhí)
行膜廊。但在 ECMAScript 中,0淫茵、+0 或?0 相除會返回 NaN :
console.log(0/0); // NaN
console.log(-0/+0); // NaN
如果分子是非 0 值爪瓜,分母是有符號 0 或無符號 0,則會返回 Infinity 或 -Infinity :
console.log(5/0); // Infinity
console.log(5/-0); // -InfinityNaN 有幾個獨(dú)特的屬性匙瘪。首先铆铆,任何涉及 NaN 的操作始終返回 NaN (如 NaN/10 ),在連續(xù)多步計(jì)算
時這可能是個問題丹喻。其次薄货, NaN 不等于包括 NaN 在內(nèi)的任何值。例如驻啤,下面的比較操作會返回 false :
console.log(NaN == NaN); // false
為此菲驴,ECMAScript 提供了 isNaN() 函數(shù)荐吵。該函數(shù)接收一個參數(shù)骑冗,可以是任意數(shù)據(jù)類型,然后判斷
這個參數(shù)是否“不是數(shù)值”先煎。把一個值傳給 isNaN() 后贼涩,該函數(shù)會嘗試把它轉(zhuǎn)換為數(shù)值。某些非數(shù)值的
值可以直接轉(zhuǎn)換成數(shù)值薯蝎,如字符串 "10" 或布爾值遥倦。任何不能轉(zhuǎn)換為數(shù)值的值都會導(dǎo)致這個函數(shù)返回
true 。舉例如下:
console.log(isNaN(NaN)); // true
console.log(isNaN(10)); // false占锯,10 是數(shù)值
console.log(isNaN("10")); // false袒哥,可以轉(zhuǎn)換為數(shù)值 10
console.log(isNaN("blue")); // true,不可以轉(zhuǎn)換為數(shù)值
console.log(isNaN(true)); // false消略,可以轉(zhuǎn)換為數(shù)值 1
上述的例子測試了 5 個不同的值。首先測試的是 NaN 本身,顯然會返回 true 您单。接著測試了數(shù)值 10
和字符串 "10" 贱傀,都返回 false ,因?yàn)樗鼈兊臄?shù)值都是 10胎撤。字符串 "blue" 不能轉(zhuǎn)換為數(shù)值晓殊,因此函數(shù)返
回 true 。布爾值 true 可以轉(zhuǎn)換為數(shù)值 1伤提,因此返回 false 巫俺。
注意 雖然不常見,但 isNaN() 可以用于測試對象肿男。此時介汹,首先會調(diào)用對象的 valueOf()
方法砚著,然后再確定返回的值是否可以轉(zhuǎn)換為數(shù)值。如果不能痴昧,再調(diào)用 toString() 方法稽穆,
并測試其返回值。這通常是 ECMAScript 內(nèi)置函數(shù)和操作符的工作方式赶撰,本章后面會討論舌镶。
4. 數(shù)值轉(zhuǎn)換
有 3 個函數(shù)可以將非數(shù)值轉(zhuǎn)換為數(shù)值: Number() 、 parseInt() 和 parseFloat() 豪娜。 Number() 是
轉(zhuǎn)型函數(shù)餐胀,可用于任何數(shù)據(jù)類型。后兩個函數(shù)主要用于將字符串轉(zhuǎn)換為數(shù)值瘤载。對于同樣的參數(shù)否灾,這 3 個
函數(shù)執(zhí)行的操作也不同。
Number() 函數(shù)基于如下規(guī)則執(zhí)行轉(zhuǎn)換鸣奔。
?? 布爾值墨技, true 轉(zhuǎn)換為 1, false 轉(zhuǎn)換為 0挎狸。
?? 數(shù)值扣汪,直接返回。
? null 锨匆,返回 0崭别。
? undefined ,返回 NaN 恐锣。
?? 字符串茅主,應(yīng)用以下規(guī)則。
?? 如果字符串包含數(shù)值字符土榴,包括數(shù)值字符前面帶加诀姚、減號的情況,則轉(zhuǎn)換為一個十進(jìn)制數(shù)值鞭衩。
因此学搜, Number("1") 返回 1, Number("123") 返回 123论衍, Number("011") 返回 11(忽略前面
的零)瑞佩。
?? 如果字符串包含有效的浮點(diǎn)值格式如 "1.1" ,則會轉(zhuǎn)換為相應(yīng)的浮點(diǎn)值(同樣坯台,忽略前面的零)炬丸。
?? 如果字符串包含有效的十六進(jìn)制格式如 "0xf" ,則會轉(zhuǎn)換為與該十六進(jìn)制值對應(yīng)的十進(jìn)制整
數(shù)值。
?? 如果是空字符串(不包含字符)稠炬,則返回 0焕阿。
?? 如果字符串包含除上述情況之外的其他字符,則返回 NaN 首启。
?? 對象暮屡,調(diào)用 valueOf() 方法,并按照上述規(guī)則轉(zhuǎn)換返回的值毅桃。如果轉(zhuǎn)換結(jié)果是 NaN 褒纲,則調(diào)用
toString() 方法,再按照轉(zhuǎn)換字符串的規(guī)則轉(zhuǎn)換钥飞。從不同數(shù)據(jù)類型到數(shù)值的轉(zhuǎn)換有時候會比較復(fù)雜莺掠,看一看 Number() 的轉(zhuǎn)換規(guī)則就知道了。下面是
幾個具體的例子:
let num1 = Number("Hello world!"); // NaN
let num2 = Number(""); // 0
let num3 = Number("000011"); // 11
let num4 = Number(true); // 1
可以看到读宙,字符串 "Hello world" 轉(zhuǎn)換之后是 NaN 彻秆,因?yàn)樗也坏綄?yīng)的數(shù)值〗嵴ⅲ空字符串轉(zhuǎn)換后
是 0唇兑。字符串 000011 轉(zhuǎn)換后是 11,因?yàn)榍懊娴牧惚缓雎粤税蚬馈W詈螅?true 轉(zhuǎn)換為 1幔亥。
注意 本章后面會討論到的一元加操作符與 Number() 函數(shù)遵循相同的轉(zhuǎn)換規(guī)則。
考慮到用 Number() 函數(shù)轉(zhuǎn)換字符串時相對復(fù)雜且有點(diǎn)反常規(guī)察纯,通常在需要得到整數(shù)時可以優(yōu)先使
用 parseInt() 函數(shù)。 parseInt() 函數(shù)更專注于字符串是否包含數(shù)值模式针肥。字符串最前面的空格會被
忽略饼记,從第一個非空格字符開始轉(zhuǎn)換。如果第一個字符不是數(shù)值字符慰枕、加號或減號具则, parseInt() 立即
返回 NaN 。這意味著空字符串也會返回 NaN (這一點(diǎn)跟 Number() 不一樣具帮,它返回 0)博肋。如果第一個字符
是數(shù)值字符、加號或減號蜂厅,則繼續(xù)依次檢測每個字符匪凡,直到字符串末尾,或碰到非數(shù)值字符掘猿。比如病游,
"1234blue" 會被轉(zhuǎn)換為 1234,因?yàn)?"blue" 會被完全忽略稠通。類似地衬衬, "22.5" 會被轉(zhuǎn)換為 22买猖,因?yàn)樾?shù)
點(diǎn)不是有效的整數(shù)字符。
假設(shè)字符串中的第一個字符是數(shù)值字符滋尉, parseInt() 函數(shù)也能識別不同的整數(shù)格式(十進(jìn)制玉控、八
進(jìn)制、十六進(jìn)制)狮惜。換句話說奸远,如果字符串以 "0x" 開頭,就會被解釋為十六進(jìn)制整數(shù)讽挟。如果字符串以 "0"
開頭懒叛,且緊跟著數(shù)值字符,在非嚴(yán)格模式下會被某些實(shí)現(xiàn)解釋為八進(jìn)制整數(shù)耽梅。
下面幾個轉(zhuǎn)換示例有助于理解上述規(guī)則:
let num1 = parseInt("1234blue"); // 1234
let num2 = parseInt(""); // NaN
let num3 = parseInt("0xA"); // 10薛窥,解釋為十六進(jìn)制整數(shù)
let num4 = parseInt(22.5); // 22
let num5 = parseInt("70"); // 70,解釋為十進(jìn)制值
let num6 = parseInt("0xf"); // 15眼姐,解釋為十六進(jìn)制整數(shù)
不同的數(shù)值格式很容易混淆诅迷,因此 parseInt() 也接收第二個參數(shù),用于指定底數(shù)(進(jìn)制數(shù))众旗。如
果知道要解析的值是十六進(jìn)制罢杉,那么可以傳入 16 作為第二個參數(shù),以便正確解析:
let num = parseInt("0xAF", 16); // 175
事實(shí)上贡歧,如果提供了十六進(jìn)制參數(shù)滩租,那么字符串前面的 "0x" 可以省掉:
let num1 = parseInt("AF", 16); // 175
let num2 = parseInt("AF"); // NaN
在這個例子中,第一個轉(zhuǎn)換是正確的利朵,而第二個轉(zhuǎn)換失敗了律想。區(qū)別在于第一次傳入了進(jìn)制數(shù)作為參
數(shù),告訴 parseInt() 要解析的是一個十六進(jìn)制字符串绍弟。而第二個轉(zhuǎn)換檢測到第一個字符就是非數(shù)值字
符技即,隨即自動停止并返回 NaN 。
通過第二個參數(shù)樟遣,可以極大擴(kuò)展轉(zhuǎn)換后獲得的結(jié)果類型而叼。比如:
let num1 = parseInt("10", 2); // 2,按二進(jìn)制解析
let num2 = parseInt("10", 8); // 8豹悬,按八進(jìn)制解析
let num3 = parseInt("10", 10); // 10葵陵,按十進(jìn)制解析
let num4 = parseInt("10", 16); // 16,按十六進(jìn)制解析因?yàn)椴粋鞯讛?shù)參數(shù)相當(dāng)于讓 parseInt() 自己決定如何解析屿衅,所以為避免解析出錯埃难,建議始終傳給
它第二個參數(shù)。
注意 多數(shù)情況下解析的應(yīng)該都是十進(jìn)制數(shù),此時第二個參數(shù)就要傳入 10涡尘。
parseFloat() 函數(shù)的工作方式跟 parseInt() 函數(shù)類似忍弛,都是從位置 0 開始檢測每個字符。同樣考抄,
它也是解析到字符串末尾或者解析到一個無效的浮點(diǎn)數(shù)值字符為止细疚。這意味著第一次出現(xiàn)的小數(shù)點(diǎn)是有
效的,但第二次出現(xiàn)的小數(shù)點(diǎn)就無效了川梅,此時字符串的剩余字符都會被忽略疯兼。因此, "22.34.5" 將轉(zhuǎn)換
成 22.34贫途。
parseFloat() 函數(shù)的另一個不同之處在于吧彪,它始終忽略字符串開頭的零。這個函數(shù)能識別前面討
論的所有浮點(diǎn)格式丢早,以及十進(jìn)制格式(開頭的零始終被忽略)姨裸。十六進(jìn)制數(shù)值始終會返回 0。因?yàn)?/p>
parseFloat() 只解析十進(jìn)制值怨酝,因此不能指定底數(shù)傀缩。最后,如果字符串表示整數(shù)(沒有小數(shù)點(diǎn)或者小
數(shù)點(diǎn)后面只有一個零)农猬,則 parseFloat() 返回整數(shù)赡艰。下面是幾個示例:
let num1 = parseFloat("1234blue"); // 1234,按整數(shù)解析
let num2 = parseFloat("0xA"); // 0
let num3 = parseFloat("22.5"); // 22.5
let num4 = parseFloat("22.34.5"); // 22.34
let num5 = parseFloat("0908.5"); // 908.5
let num6 = parseFloat("3.125e7"); // 31250000
3.4.6? String 類型
String (字符串)數(shù)據(jù)類型表示零或多個 16 位 Unicode 字符序列斤葱。字符串可以使用雙引號(")慷垮、
單引號(')或反引號(`)標(biāo)示,因此下面的代碼都是合法的:
let firstName = "John";
let lastName = 'Jacob';
let lastName = `Jingleheimerschmidt`
跟某些語言中使用不同的引號會改變對字符串的解釋方式不同苦掘,ECMAScript 語法中表示字符串的
引號沒有區(qū)別换帜。不過要注意的是,以某種引號作為字符串開頭鹤啡,必須仍然以該種引號作為字符串結(jié)尾。
比如蹲嚣,下面的寫法會導(dǎo)致語法錯誤:
let firstName = 'Nicholas"; // 語法錯誤:開頭和結(jié)尾的引號必須是同一種
1. 字符字面量
字符串?dāng)?shù)據(jù)類型包含一些字符字面量递瑰,用于表示非打印字符或有其他用途的字符,如下表所示:
字 面 量? 含 義
\n? 換行
\t? 制表
\b? 退格
\r? 回車
\f? 換頁
\\? 反斜杠( \ )
\'? 單引號( ' )隙畜,在字符串以單引號標(biāo)示時使用抖部,例如 'He said, \'hey.\''字 面 量 含 義
\"? 雙引號( " ),在字符串以雙引號標(biāo)示時使用议惰,例如 "He said, \"hey.\""
\`? 反引號( ` )慎颗,在字符串以反引號標(biāo)示時使用,例如 `He said, \`hey.\``
\xnn? 以十六進(jìn)制編碼 nn 表示的字符(其中 n 是十六進(jìn)制數(shù)字 0~F),例如 \x41 等于 "A"
\unnnn? 以十六進(jìn)制編碼 nnnn 表示的 Unicode 字符(其中 n 是十六進(jìn)制數(shù)字 0~F)俯萎,例如 \u03a3 等于希臘字
符 "Σ"
這些字符字面量可以出現(xiàn)在字符串中的任意位置傲宜,且可以作為單個字符被解釋:
let text = "This is the letter sigma: \u03a3.";
在這個例子中,即使包含 6 個字符長的轉(zhuǎn)義序列夫啊,變量 text 仍然是 28 個字符長函卒。因?yàn)檗D(zhuǎn)義序列表
示一個字符,所以只算一個字符撇眯。
字符串的長度可以通過其 length 屬性獲缺ㄇ丁:
console.log(text.length); // 28
這個屬性返回字符串中 16 位字符的個數(shù)。
注意 如果字符串中包含雙字節(jié)字符熊榛,那么 length 屬性返回的值可能不是準(zhǔn)確的字符數(shù)锚国。
第 5 章將具體討論如何解決這個問題。
2. 字符串的特點(diǎn)
ECMAScript 中的字符串是不可變的(immutable)玄坦,意思是一旦創(chuàng)建血筑,它們的值就不能變了。要修改
某個變量中的字符串值营搅,必須先銷毀原始的字符串云挟,然后將包含新值的另一個字符串保存到該變量,如
下所示:
let lang = "Java";
lang = lang + "Script";
這里转质,變量 lang 一開始包含字符串 "Java" 园欣。緊接著, lang 被重新定義為包含 "Java" 和 "Script"
的組合休蟹,也就是 "JavaScript" 沸枯。整個過程首先會分配一個足夠容納 10 個字符的空間,然后填充上
"Java" 和 "Script" 赂弓。最后銷毀原始的字符串 "Java" 和字符串 "Script" 绑榴,因?yàn)檫@兩個字符串都沒有用
了。所有處理都是在后臺發(fā)生的盈魁,而這也是一些早期的瀏覽器(如 Firefox 1.0 之前的版本和 IE6.0)在
拼接字符串時非常慢的原因翔怎。這些瀏覽器在后來的版本中都有針對性地解決了這個問題。
3. 轉(zhuǎn)換為字符串
有兩種方式把一個值轉(zhuǎn)換為字符串杨耙。首先是使用幾乎所有值都有的 toString() 方法赤套。這個方法唯
一的用途就是返回當(dāng)前值的字符串等價物。比如:
let age = 11;
let ageAsString = age.toString(); // 字符串"11"
let found = true;
let foundAsString = found.toString(); // 字符串"true"
toString() 方法可見于數(shù)值珊膜、布爾值容握、對象和字符串值。(沒錯车柠,字符串值也有 toString() 方法剔氏,
該方法只是簡單地返回自身的一個副本塑猖。) null 和 undefined 值沒有 toString() 方法。
多數(shù)情況下谈跛, toString() 不接收任何參數(shù)羊苟。不過,在對數(shù)值調(diào)用這個方法時币旧, toString() 可以接收一個底數(shù)參數(shù)践险,即以什么底數(shù)來輸出數(shù)值的字符串表示。默認(rèn)情況下吹菱, toString() 返回?cái)?shù)值的十
進(jìn)制字符串表示巍虫。而通過傳入?yún)?shù),可以得到數(shù)值的二進(jìn)制鳍刷、八進(jìn)制占遥、十六進(jìn)制,或者其他任何有效基
數(shù)的字符串表示输瓜,比如:
let num = 10;
console.log(num.toString()); // "10"
console.log(num.toString(2)); // "1010"
console.log(num.toString(8)); // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"
這個例子展示了傳入底數(shù)參數(shù)時瓦胎, toString() 輸出的字符串值也會隨之改變。數(shù)值 10 可以輸出為
任意數(shù)值格式尤揣。注意搔啊,默認(rèn)情況下(不傳參數(shù))的輸出與傳入?yún)?shù) 10 得到的結(jié)果相同。
如果你不確定一個值是不是 null 或 undefined 北戏,可以使用 String() 轉(zhuǎn)型函數(shù)负芋,它始終會返回表
示相應(yīng)類型值的字符串。 String() 函數(shù)遵循如下規(guī)則嗜愈。
?? 如果值有 toString() 方法旧蛾,則調(diào)用該方法(不傳參數(shù))并返回結(jié)果。
?? 如果值是 null 蠕嫁,返回 "null" 锨天。
?? 如果值是 undefined ,返回 "undefined" 剃毒。
下面看幾個例子:
let value1 = 10;
let value2 = true;
let value3 = null;
let value4;
console.log(String(value1)); // "10"
console.log(String(value2)); // "true"
console.log(String(value3)); // "null"
console.log(String(value4)); // "undefined"
這里展示了將 4 個值轉(zhuǎn)換為字符串的情況:一個數(shù)值病袄、一個布爾值、一個 null 和一個 undefined 赘阀。
數(shù)值和布爾值的轉(zhuǎn)換結(jié)果與調(diào)用 toString() 相同陪拘。因?yàn)?null 和 undefined 沒有 toString() 方法,
所以 String() 方法就直接返回了這兩個值的字面量文本纤壁。
注意 用加號操作符給一個值加上一個空字符串 "" 也可以將其轉(zhuǎn)換為字符串(加號操作符
本章后面會介紹)。
4. 模板字面量
ECMAScript 6 新增了使用模板字面量定義字符串的能力捺信。與使用單引號或雙引號不同酌媒,模板字面量
保留換行字符欠痴,可以跨行定義字符串:
let myMultiLineString = 'first line\nsecond line';
let myMultiLineTemplateLiteral = `first line
second line`;
console.log(myMultiLineString);
// first line
// second line"
console.log(myMultiLineTemplateLiteral);
// first line// second line
console.log(myMultiLineString === myMultiLinetemplateLiteral); // true
顧名思義,模板字面量在定義模板時特別有用秒咨,比如下面這個 HTML 模板:
let pageHTML = `
<div>
<a href="#">
<span>Jake</span>
</a>
</div>`;
由于模板字面量會保持反引號內(nèi)部的空格喇辽,因此在使用時要格外注意。格式正確的模板字符串看起
來可能會縮進(jìn)不當(dāng):
// 這個模板字面量在換行符之后有 25 個空格符
let myTemplateLiteral = `first line
second line`;
console.log(myTemplateLiteral.length); // 47
// 這個模板字面量以一個換行符開頭
let secondTemplateLiteral = `
first line
second line`;
console.log(secondTemplateLiteral[0] === '\n'); // true
// 這個模板字面量沒有意料之外的字符
let thirdTemplateLiteral = `first line
second line`;
console.log(thirdTemplateLiteral);
// first line
// second line
5. 字符串插值
模板字面量最常用的一個特性是支持字符串插值雨席,也就是可以在一個連續(xù)定義中插入一個或多個
值菩咨。技術(shù)上講,模板字面量不是字符串陡厘,而是一種特殊的 JavaScript 句法表達(dá)式抽米,只不過求值后得到的
是字符串。模板字面量在定義時立即求值并轉(zhuǎn)換為字符串實(shí)例糙置,任何插入的變量也會從它們最接近的作
用域中取值云茸。
字符串插值通過在 ${} 中使用一個 JavaScript 表達(dá)式實(shí)現(xiàn):
let value = 5;
let exponent = 'second';
// 以前,字符串插值是這樣實(shí)現(xiàn)的:
let interpolatedString =
value + ' to the ' + exponent + ' power is ' + (value * value);
// 現(xiàn)在谤饭,可以用模板字面量這樣實(shí)現(xiàn):
let interpolatedTemplateLiteral =
`${ value } to the ${ exponent } power is ${ value * value }`;
console.log(interpolatedString); // 5 to the second power is 25
console.log(interpolatedTemplateLiteral); // 5 to the second power is 25
所有插入的值都會使用 toString() 強(qiáng)制轉(zhuǎn)型為字符串标捺,而且任何 JavaScript 表達(dá)式都可以用于插
值。嵌套的模板字符串無須轉(zhuǎn)義:console.log(`Hello, ${ `World` }!`); // Hello, World!
將表達(dá)式轉(zhuǎn)換為字符串時會調(diào)用 toString() :
let foo = { toString: () => 'World' };
console.log(`Hello, ${ foo }!`); // Hello, World!
在插值表達(dá)式中可以調(diào)用函數(shù)和方法:
function capitalize(word) {
return `${ word[0].toUpperCase() }${ word.slice(1) }`;
}
console.log(`${ capitalize('hello') }, ${ capitalize('world') }!`); // Hello, World!
此外揉抵,模板也可以插入自己之前的值:
let value = '';
function append() {
value = `${value}abc`
console.log(value);
}
append(); // abc
append(); // abcabc
append(); // abcabcabc
6. 模板字面量標(biāo)簽函數(shù)
模板字面量也支持定義標(biāo)簽函數(shù)(tag function)亡容,而通過標(biāo)簽函數(shù)可以自定義插值行為。標(biāo)簽函數(shù)
會接收被插值記號分隔后的模板和對每個表達(dá)式求值的結(jié)果冤今。
標(biāo)簽函數(shù)本身是一個常規(guī)函數(shù)闺兢,通過前綴到模板字面量來應(yīng)用自定義行為,如下例所示辟汰。標(biāo)簽函數(shù)
接收到的參數(shù)依次是原始字符串?dāng)?shù)組和對每個表達(dá)式求值的結(jié)果列敲。這個函數(shù)的返回值是對模板字面量求
值得到的字符串。
最好通過一個例子來理解:
let a = 6;
let b = 9;
function simpleTag(strings, aValExpression, bValExpression, sumExpression) {
console.log(strings);
console.log(aValExpression);
console.log(bValExpression);
console.log(sumExpression);
return 'foobar';
}
let untaggedResult = `${ a } + ${ b } = ${ a + b }`;
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;
// ["", " + ", " = ", ""]
// 6
// 9
// 15
console.log(untaggedResult); // "6 + 9 = 15"
console.log(taggedResult); // "foobar"
因?yàn)楸磉_(dá)式參數(shù)的數(shù)量是可變的帖汞,所以通常應(yīng)該使用剩余操作符(rest operator)將它們收集到一個
數(shù)組中:let a = 6;
let b = 9;
function simpleTag(strings, ...expressions) {
console.log(strings);
for(const expression of expressions) {
console.log(expression);
}
return 'foobar';
}
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;
// ["", " + ", " = ", ""]
// 6
// 9
// 15
console.log(taggedResult); // "foobar"
對于有 n 個插值的模板字面量戴而,傳給標(biāo)簽函數(shù)的表達(dá)式參數(shù)的個數(shù)始終是 n,而傳給標(biāo)簽函數(shù)的第
一個參數(shù)所包含的字符串個數(shù)則始終是 n+1翩蘸。因此所意,如果你想把這些字符串和對表達(dá)式求值的結(jié)果拼接
起來作為默認(rèn)返回的字符串,可以這樣做:
let a = 6;
let b = 9;
function zipTag(strings, ...expressions) {
return strings[0] +
expressions.map((e, i) => `${e}${strings[i + 1]}`)
.join('');
}
let untaggedResult = `${ a } + ${ b } = ${ a + b }`;
let taggedResult = zipTag`${ a } + ${ b } = ${ a + b }`;
console.log(untaggedResult); // "6 + 9 = 15"
console.log(taggedResult); // "6 + 9 = 15"
7. 原始字符串
使用模板字面量也可以直接獲取原始的模板字面量內(nèi)容(如換行符或 Unicode 字符)催首,而不是被轉(zhuǎn)
換后的字符表示扶踊。為此,可以使用默認(rèn)的 String.raw 標(biāo)簽函數(shù):
// Unicode 示例
// \u00A9 是版權(quán)符號
console.log(`\u00A9`); // ?
console.log(String.raw`\u00A9`); // \u00A9
// 換行符示例
console.log(`first line\nsecond line`);
// first line
// second line
console.log(String.raw`first line\nsecond line`); // "first line\nsecond line"
// 對實(shí)際的換行符來說是不行的
// 它們不會被轉(zhuǎn)換成轉(zhuǎn)義序列的形式
console.log(`first linesecond line`);
// first line
// second line
console.log(String.raw`first line
second line`);
// first line
// second line
另外郎任,也可以通過標(biāo)簽函數(shù)的第一個參數(shù)秧耗,即字符串?dāng)?shù)組的 .raw 屬性取得每個字符串的原始內(nèi)容:
function printRaw(strings) {
console.log('Actual characters:');
for (const string of strings) {
console.log(string);
}
console.log('Escaped characters;');
for (const rawString of strings.raw) {
console.log(rawString);
}
}
printRaw`\u00A9${ 'and' }\n`;
// Actual characters:
// ?
//(換行符)
// Escaped characters:
// \u00A9
// \n
3.4.7? Symbol 類型
Symbol (符號)是 ECMAScript 6 新增的數(shù)據(jù)類型。符號是原始值舶治,且符號實(shí)例是唯一分井、不可變的车猬。
符號的用途是確保對象屬性使用唯一標(biāo)識符,不會發(fā)生屬性沖突的危險尺锚。
盡管聽起來跟私有屬性有點(diǎn)類似珠闰,但符號并不是為了提供私有屬性的行為才增加的(尤其是因?yàn)?/p>
Object API 提供了方法,可以更方便地發(fā)現(xiàn)符號屬性)瘫辩。相反伏嗜,符號就是用來創(chuàng)建唯一記號,進(jìn)而用作非
字符串形式的對象屬性伐厌。
1. 符號的基本用法
符號需要使用 Symbol() 函數(shù)初始化承绸。因?yàn)榉柋旧硎窃碱愋停?typeof 操作符對符號返回
symbol 弧械。
let sym = Symbol();
console.log(typeof sym); // symbol
調(diào)用 Symbol() 函數(shù)時八酒,也可以傳入一個字符串參數(shù)作為對符號的描述(description),將來可以通
過這個字符串來調(diào)試代碼刃唐。但是羞迷,這個字符串參數(shù)與符號定義或標(biāo)識完全無關(guān):
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(genericSymbol == otherGenericSymbol); // false