《重學(xué)前端》筆記: 模塊一: JavaScript: 類型和對象
JavaScript類型:關(guān)于類型沦零,有哪些你不知道的細(xì)節(jié)祭隔?
- 就從運行時的角度去看 JavaScript 的類型系統(tǒng)。運行時類型是代碼實際執(zhí)行過程中我們用到的類型路操。所有的類型數(shù)據(jù)都會屬于 7 個類型之一疾渴。從變量千贯、參數(shù)、返回值到表達(dá)式中間結(jié)果搞坝,任何 JavaScript 代碼運行過程中產(chǎn)生的數(shù)據(jù)搔谴,都具有運行時類型。
問題
- 為什么有的編程規(guī)范要求用
void 0
代替undefined
. - 字符串有最大長度嗎桩撮?
- 0.1 + 0.2 不是等于 0.3 么敦第?為什么 JavaScript 里不是這樣的
- ES6 新加入的 Symbol 是個什么東西?
- 為什么給對象添加的方法能用在基本類型上距境?
類型
- Undefined
- Null
- Boolean
- String
- Number
- Symbol
- Object
Undefined申尼、Null
-
Undefined
類型表示未定義,它的類型只有一個值垫桂,就是undefined
师幕。 - 任何變量在賦值前是
Undefined
類型、值為undefined
诬滩, - 一般我們可以用全局變量
undefined
(就是名為undefined
的這個變量)來表達(dá)這個值霹粥,或者void
運算來把任意一個表達(dá)式變成undefined
值。 - 為什么有的編程規(guī)范要求用
void 0
代替undefined
.- 因為 JavaScript 的代碼
undefined
是一個變量疼鸟,而并非是一個關(guān)鍵字后控,這是 JavaScript 語言公認(rèn)的設(shè)計失誤之一,所以空镜,我們?yōu)榱吮苊鉄o意中被篡改浩淘,建議使用void 0
來獲取undefined
值。
- 因為 JavaScript 的代碼
-
Undefined
跟Null
有一定的表意差別吴攒,Null
表示的是:“定義了但是為空”张抄。所以,在實際編程時洼怔,我們一般不會把變量賦值為undefined
署惯,這樣可以保證所有值為undefined
的變量,都是從未賦值的自然狀態(tài)镣隶。 -
Null
類型也只有一個值极谊,就是null
,它的語義表示空值安岂,與undefined
不同轻猖,null
是 JavaScript 關(guān)鍵字,所以在任何代碼中嗜闻,你都可以放心用null
關(guān)鍵字來獲取null
值蜕依。
Boolean
Boolean
類型有兩個值, true
和 false
羔巢,它用于表示邏輯意義上的真和假坯癣,同樣有關(guān)鍵字 true
和 false
來表示兩個值十绑。
String
我們來看看字符串是否有最大長度替久。
- String 用于表示文本數(shù)據(jù)灵迫。String 有最大長度是
2^53 - 1
宋欺,這在一般開發(fā)中都是夠用的仰美,但是有趣的是妻率,這個所謂最大長度被丧,并不完全是你理解中的字符數(shù)盟戏。
因為 String 的意義并非“字符串”,而是字符串的
UTF16
編碼甥桂,我們字符串的操作charAt
柿究、charCodeAt
、length
等方法針對的都是UTF16
編碼蝇摸。所以,字符串的最大長度啡专,實際上是受字符串的編碼長度影響的。
Note:現(xiàn)行的字符集國際標(biāo)準(zhǔn)病附,字符是以 Unicode 的方式表示的嵌戈,每一個 Unicode 的碼點表示一個字符,理論上庵朝,Unicode 的范圍是無限的椎瘟。UTF 是 Unicode 的編碼方式,規(guī)定了碼點在計算機(jī)中的表示方法宣羊,常見的有 UTF16 和 UTF8汰蜘。 Unicode 的碼點通常用 U+??? 來表示仇冯,其中 ??? 是十六進(jìn)制的碼點值。 0-65536(U+0000 - U+FFFF)的碼點被稱為基本字符區(qū)域(BMP)族操。
JavaScript 中的字符串是永遠(yuǎn)無法變更的苛坚,一旦字符串構(gòu)造出來,無法用任何方式改變字符串的內(nèi)容炕婶,所以字符串具有值類型的特征柠掂。
JavaScript 字符串把每個 UTF16 單元當(dāng)作一個字符來處理依沮,所以處理非 BMP(超出 U+0000 - U+FFFF 范圍)的字符時涯贞,你應(yīng)該格外小心。
JavaScript 這個設(shè)計繼承自 Java危喉,最新標(biāo)準(zhǔn)中是這樣解釋的宋渔,這樣設(shè)計是為了“性能和盡可能實現(xiàn)起來簡單”。因為現(xiàn)實中很少用到 BMP 之外的字符哑蔫。
Symbol
Symbol 是 ES6 中引入的新類型疹瘦,它是一切非字符串的對象 key 的集合起便,在 ES6 規(guī)范中判沟,整個對象系統(tǒng)被用 Symbol 重塑吧秕。
- Symbol 可以具有字符串類型的描述,但是即使描述相同迹炼,Symbol 也不相等砸彬。
- 我們創(chuàng)建 Symbol 的方式是使用全局的 Symbol 函數(shù)。例如:
var mySymbol = Symbol("my symbol");
- 一些標(biāo)準(zhǔn)中提到的 Symbol斯入,可以在全局的 Symbol 函數(shù)的屬性中找到拿霉。例如,我們可以使用 Symbol.iterator 來自定義 for…of 在對象上的行為:
var o = new Object
o[Symbol.iterator] = function() {
var v = 0
return {
next: function() {
return { value: v++, done: v > 10 }
}
}
};
for(var v of o)
console.log(v); // 0 1 2 3 ... 9
代碼中我們定義了 iterator 之后咱扣,用 for(var v of o) 就可以調(diào)用這個函數(shù),然后我們可以根據(jù)函數(shù)的行為涵防,產(chǎn)生一個 for…of 的行為闹伪。
這些標(biāo)準(zhǔn)中被稱為“眾所周知”的 Symbol沪铭,也構(gòu)成了語言的一類接口形式。它們允許編寫與語言結(jié)合更緊密的 API偏瓤。
Object
Object 是 JavaScript 中最復(fù)雜的類型杀怠,也是 JavaScript 的核心機(jī)制之一。Object 表示對象的意思厅克,它是一切有形和無形物體的總稱赔退。
為什么給對象添加的方法能用在基本類型上?
- 在 JavaScript 中证舟,對象的定義是“屬性的集合”硕旗。屬性分為數(shù)據(jù)屬性和訪問器屬性,二者都是
key-value
結(jié)構(gòu)女责,key 可以是字符串或者 Symbol 類型漆枚。 - 因為 C++ 和 Java 的成功,在這兩門語言中抵知,每個類都是一個類型墙基,二者幾乎等同,以至于很多人常常會把 JavaScript 的“類”與類型混淆刷喜。
- JavaScript 中的“類”僅僅是運行時對象的一個私有屬性残制,而 JavaScript 中是無法自定義類型的。
- JavaScript 中的幾個基本類型掖疮,都在對象類型中有一個“親戚”初茶。它們是:Number;String氮墨;Boolean纺蛆;Symbol。
- 所以规揪,我們必須認(rèn)識到
3
與new Number(3)
是完全不同的值桥氏,它們一個是Number 類型, 一個是對象類型猛铅。 - Number字支、String 和 Boolean,三個構(gòu)造器是兩用的奸忽,當(dāng)跟 new 搭配時堕伪,它們產(chǎn)生對象,當(dāng)直接調(diào)用時栗菜,它們表示強(qiáng)制類型轉(zhuǎn)換欠雌。
- Symbol 函數(shù)比較特殊,直接用 new 調(diào)用它會拋出錯誤疙筹,但它仍然是 Symbol 對象的構(gòu)造器富俄。
- JavaScript 語言設(shè)計上試圖模糊對象和基本類型之間的關(guān)系禁炒,我們?nèi)粘4a可以把對象的方法在基本類型上使用,比如:
console.log("abc".charAt(0)); //a
- 甚至我們在原型上添加方法霍比,都可以應(yīng)用于基本類型幕袱,比如以下代碼,在 Symbol 原型上添加了 hello 方法悠瞬,在任何 Symbol 類型變量都可以調(diào)用们豌。
Symbol.prototype.hello = () => console.log("hello");
var a = Symbol("a");
console.log(typeof a); //symbol,a 并非對象
a.hello(); //hello浅妆,有效
所以我們文章開頭的問題望迎,答案就是: 運算符提供了裝箱操作,它會根據(jù)基礎(chǔ)類型構(gòu)造一個臨時對象狂打,使得我們能在基礎(chǔ)類型上調(diào)用對應(yīng)對象的方法擂煞。
類型轉(zhuǎn)換
- 臭名昭著的是 JavaScript 中的“ == ”運算,因為試圖實現(xiàn)跨類型的比較趴乡,它的規(guī)則復(fù)雜到幾乎沒人可以記住对省。
- 其它運算,如加減乘除大于小于晾捏,也都會涉及類型轉(zhuǎn)換蒿涎。幸好的是,實際上大部分類型轉(zhuǎn)換規(guī)則是非常簡單的惦辛,如下表所示:
StringToNumber
- 字符串到數(shù)字的類型轉(zhuǎn)換劳秋,存在一個語法結(jié)構(gòu),類型轉(zhuǎn)換支持十進(jìn)制胖齐、二進(jìn)制玻淑、八進(jìn)制和十六進(jìn)制,比如:
30呀伙; 0b111补履; 0o13; 0xFF剿另。
- 此外箫锤,JavaScript 支持的字符串語法還包括正負(fù)號科學(xué)計數(shù)法,可以使用大寫或者小寫的 e 來表示:
1e3雨女; -1e-2谚攒。
- 需要注意的是,
parseInt
和parseFloat
并不使用這個轉(zhuǎn)換氛堕,所以支持的語法跟這里不盡相同馏臭。- 在不傳入第二個參數(shù)的情況下,
parseInt
只支持 16 進(jìn)制前綴“0x
”讼稚,而且會忽略非數(shù)字字符位喂,也不支持科學(xué)計數(shù)法浪耘。
- 在不傳入第二個參數(shù)的情況下,
- 多數(shù)情況下,
Number
是比parseInt
和parseFloat
更好的選擇
NumberToString
- 在較小的范圍內(nèi)塑崖,數(shù)字到字符串的轉(zhuǎn)換是完全符合你直覺的十進(jìn)制表示。當(dāng) Number 絕對值較大或者較小時痛倚,字符串表示則是使用科學(xué)計數(shù)法表示的规婆。這個算法細(xì)節(jié)繁多,我們從感性的角度認(rèn)識蝉稳,它其實就是保證了產(chǎn)生的字符串不會過長抒蚜。
- 具體的算法,你可以去參考 JavaScript 的語言標(biāo)準(zhǔn)耘戚。由于這個部分內(nèi)容嗡髓,我覺得在日常開發(fā)中很少用到,所以這里我就不去詳細(xì)地講解了收津。
裝箱轉(zhuǎn)換
- 每一種基本類型 Number饿这、String、Boolean撞秋、Symbol 在對象中都有對應(yīng)的類长捧,所謂裝箱轉(zhuǎn)換,正是把基本類型轉(zhuǎn)換為對應(yīng)的對象吻贿,它是類型轉(zhuǎn)換中一種相當(dāng)重要的種類
- 前文提到串结,全局的 Symbol 函數(shù)無法使用 new 來調(diào)用,但我們?nèi)钥梢岳醚b箱機(jī)制來得到一個 Symbol 對象舅列,我們可以利用一個函數(shù)的 call 方法來強(qiáng)迫產(chǎn)生裝箱肌割。
- 我們定義一個函數(shù),函數(shù)里面只有 return this帐要,然后我們調(diào)用函數(shù)的 call 方法到一個 Symbol 類型的值上把敞,這樣就會產(chǎn)生一個 symbolObject
- 我們可以用 console.log 看一下這個東西的 type of,它的值是 object宠叼,我們使用 symbolObject instanceof 可以看到先巴,它是 Symbol 這個類的實例,我們找它的 constructor 也是等于 Symbol 的冒冬,所以我們無論從哪個角度看伸蚯,它都是 Symbol 裝箱過的對象:
var symbolObject = (function(){ return this; }).call(Symbol("a"));
console.log(typeof symbolObject); //object
console.log(symbolObject instanceof Symbol); //true
console.log(symbolObject.constructor == Symbol); //true
- 裝箱機(jī)制會頻繁產(chǎn)生臨時對象,在一些對性能要求較高的場景下简烤,我們應(yīng)該盡量避免對基本類型做裝箱轉(zhuǎn)換剂邮。
- 使用內(nèi)置的 Object 函數(shù),我們可以在 JavaScript 代碼中顯式調(diào)用裝箱能力横侦。
var symbolObject = Object(Symbol("a"));
console.log(typeof symbolObject); //object
console.log(symbolObject instanceof Symbol); //true
console.log(symbolObject.constructor == Symbol); //true
- 每一類裝箱對象皆有私有的
Class
屬性挥萌,這些屬性可以用Object.prototype.toString
獲却乱觥:
var symbolObject = Object(Symbol("a"));
console.log(Object.prototype.toString.call(symbolObject)); //[object Symbol]
- 在 JavaScript 中,沒有任何方法可以更改私有的
Class
屬性引瀑,因此Object.prototype.toString
是可以準(zhǔn)確識別對象對應(yīng)的基本類型的方法狂芋,它比instanceof
更加準(zhǔn)確。 - 但需要注意的是憨栽,call 本身會產(chǎn)生裝箱操作帜矾,所以需要配合 typeof 來區(qū)分基本類型還是對象類型。
拆箱轉(zhuǎn)換
- 在 JavaScript 標(biāo)準(zhǔn)中屑柔,規(guī)定了
ToPrimitive
函數(shù)屡萤,它是對象類型到基本類型的轉(zhuǎn)換(即,拆箱轉(zhuǎn)換)掸宛。 - 對象到 String 和 Number 的轉(zhuǎn)換都遵循“先拆箱再轉(zhuǎn)換”的規(guī)則死陆。通過拆箱轉(zhuǎn)換,把對象變成基本類型唧瘾,再從基本類型轉(zhuǎn)換為對應(yīng)的 String 或者 Number措译。
- 拆箱轉(zhuǎn)換會嘗試調(diào)用
valueOf
和toString
來獲得拆箱后的基本類型。如果valueOf
和toString
都不存在劈愚,或者沒有返回基本類型瞳遍,則會產(chǎn)生類型錯誤TypeError
。
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
o * 2
// valueOf
// toString
// TypeError
我們定義了一個對象 o菌羽,o 有
valueOf
和toString
兩個方法掠械,這兩個方法都返回一個對象,然后我們進(jìn)行o*2
這個運算的時候注祖,你會看見先執(zhí)行了valueOf
猾蒂,接下來是toString
,最后拋出了一個TypeError
是晨,這就說明了這個拆箱轉(zhuǎn)換失敗了肚菠。
- 到 String 的拆箱轉(zhuǎn)換會優(yōu)先調(diào)用
toString
。我們把剛才的運算從o*2
換成String(o)
罩缴,那么你會看到調(diào)用順序就變了蚊逢。
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
String(o)
// toString
// valueOf
// TypeError
- 在 ES6 之后,還允許對象通過顯式指定
@@toPrimitive Symbol
來覆蓋原有的行為箫章。
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}
console.log(o + "")
// toPrimitive
// hello
還有一些語言的實現(xiàn)者更關(guān)心的規(guī)范類型
List 和 Record: 用于描述函數(shù)傳參過程烙荷。
Set:主要用于解釋字符集等。
Completion Record:用于描述異常檬寂、跳出等語句執(zhí)行過程终抽。
Reference:用于描述對象屬性訪問、delete 等。
Property Descriptor:用于描述對象的屬性昼伴。
Lexical Environment 和 Environment Record:用于描述變量和作用域匾旭。
Data Block:用于描述二進(jìn)制數(shù)據(jù)。
補(bǔ)充閱讀
事實上圃郊,“類型”在 JavaScript 中是一個有爭議的概念价涝。一方面,標(biāo)準(zhǔn)中規(guī)定了運行時數(shù)據(jù)類型持舆; 另一方面飒泻,JavaScript 語言中提供了 typeof 這樣的運算,用來返回操作數(shù)的類型吏廉,但 typeof 的運算結(jié)果,與運行時類型的規(guī)定有很多不一致的地方惰许。我們可以看下表來對照一下席覆。
在表格中,多數(shù)項是對應(yīng)的汹买,但是請注意 object——Null 和 function——Object 是特例佩伤,我們理解類型的時候需要特別注意這個區(qū)別。
JavaScript對象:面向?qū)ο筮€是基于對象晦毙?
一些新人在學(xué)習(xí) JavaScript 面向?qū)ο髸r生巡,往往也會有疑惑:
- 為什么 JavaScript(直到 ES6)有對象的概念,但是卻沒有像其他的語言那樣见妒,有類的概念呢孤荣;
- 為什么在 JavaScript 對象里可以自由添加屬性,而其他的語言卻不能呢须揣?
甚至盐股,在一些爭論中,有人強(qiáng)調(diào):JavaScript 并非“面向?qū)ο蟮恼Z言”耻卡,而是“基于對象的語言”疯汁。這個說法一度流傳甚廣,而事實上卵酪,我至今遇到的持有這一說法的人中幌蚊,無一能夠回答“如何定義面向?qū)ο蠛突趯ο蟆边@個問題。
實際上溃卡,基于對象和面向?qū)ο髢蓚€形容詞都出現(xiàn)在了 JavaScript 標(biāo)準(zhǔn)的各個版本當(dāng)中溢豆。
我們可以先看看 JavaScript 標(biāo)準(zhǔn)對基于對象的定義,這個定義的具體內(nèi)容是:“語言和宿主的基礎(chǔ)設(shè)施由對象來提供塑煎,并且 JavaScript 程序即是一系列互相通訊的對象集合”沫换。
這里的意思根本不是表達(dá)弱化的面向?qū)ο蟮囊馑迹炊潜磉_(dá)對象對于語言的重要性。
什么是面向?qū)ο螅?/h3>
我們先來說說什么是對象讯赏,因為翻譯的原因垮兑,中文語境下我們很難理解“對象”的真正含義。事實上漱挎,Object(對象)在英文中系枪,是一切事物的總稱,這和面向?qū)ο缶幊痰某橄笏季S有互通之處磕谅。
中文的“對象”卻沒有這樣的普適性私爷,我們在學(xué)習(xí)編程的過程中,更多是把它當(dāng)作一個專業(yè)名詞來理解膊夹。
但不論如何衬浑,我們應(yīng)該認(rèn)識到,對象并不是計算機(jī)領(lǐng)域憑空造出來的概念放刨,它是順著人類思維模式產(chǎn)生的一種抽象(于是面向?qū)ο缶幊桃脖徽J(rèn)為是:更接近人類思維模式的一種編程范式)工秩。
那么,我們先來看看在人類思維模式下进统,對象究竟是什么助币。
對象這一概念在人類的幼兒期形成,這遠(yuǎn)遠(yuǎn)早于我們編程邏輯中常用的值螟碎、過程等概念眉菱。
在幼年期,我們總是先認(rèn)識到某一個蘋果能吃(這里的某一個蘋果就是一個對象)掉分,繼而認(rèn)識到所有的蘋果都可以吃(這里的所有蘋果俭缓,就是一個類),再到后來我們才能意識到三個蘋果和三個梨之間的聯(lián)系叉抡,進(jìn)而產(chǎn)生數(shù)字“3”(值)的概念尔崔。
在《面向?qū)ο蠓治雠c設(shè)計》這本書中,Grady Booch 替我們做了總結(jié)褥民,他認(rèn)為季春,從人類的認(rèn)知角度來說,對象應(yīng)該是下列事物之一:
- 一個可以觸摸或者可以看見的東西消返;
- 人的智力可以理解的東西载弄;
- 可以指導(dǎo)思考或行動(進(jìn)行想象或施加動作)的東西。
有了對象的自然定義后撵颊,我們就可以描述編程語言中的對象了宇攻。在不同的編程語言中,設(shè)計者也利用各種不同的語言特性來抽象描述對象倡勇,最為成功的流派是使用“類”的方式來描述對象逞刷,這誕生了諸如 C++、Java 等流行的編程語言。
而 JavaScript 早年卻選擇了一個更為冷門的方式:原型(關(guān)于原型夸浅,我在下一篇文章會重點介紹仑最,這里你留個印象就可以了)。這是我在前面說它不合群的原因之一帆喇。
然而很不幸警医,因為一些公司政治原因,JavaScript 推出之時受管理層之命被要求模仿 Java坯钦,所以预皇,JavaScript 創(chuàng)始人 Brendan Eich 在“原型運行時”的基礎(chǔ)上引入了 new、this 等語言特性婉刀,使之“看起來更像 Java”吟温。
在 ES6 出現(xiàn)之前,大量的 JavaScript 程序員試圖在原型體系的基礎(chǔ)上突颊,把 JavaScript 變得更像是基于類的編程溯街,進(jìn)而產(chǎn)生了很多所謂的“框架”,比如 PrototypeJS洋丐、Dojo。
事實上挥等,它們成為了某種 JavaScript 的古怪方言友绝,甚至產(chǎn)生了一系列互不相容的社群,顯然這樣做的收益是遠(yuǎn)遠(yuǎn)小于損失的肝劲。
如果我們從運行時角度來談?wù)搶ο笄停褪窃谟懻?JavaScript 實際運行中的模型,這是由于任何代碼執(zhí)行都必定繞不開運行時的對象模型辞槐。
不過掷漱,幸運的是,從運行時的角度看榄檬,可以不必受到這些“基于類的設(shè)施”的困擾卜范,這是因為任何語言運行時類的概念都是被弱化的。
首先我們來了解一下 JavaScript 是如何設(shè)計對象模型的鹿榜。
JavaScript 對象的特征
在我看來海雪,不論我們使用什么樣的編程語言,我們都先應(yīng)該去理解對象的本質(zhì)特征(參考 Grandy Booch《面向?qū)ο蠓治雠c設(shè)計》)舱殿“侣悖總結(jié)來看,對象有如下幾個特點沪袭。
- 對象具有唯一標(biāo)識性:即使完全相同的兩個對象湾宙,也并非同一個對象。
- 對象有狀態(tài):對象具有狀態(tài),同一對象可能處于不同狀態(tài)之下侠鳄。
- 對象具有行為:即對象的狀態(tài)埠啃,可能因為它的行為產(chǎn)生變遷。
我們先來看第一個特征畦攘,對象具有唯一標(biāo)識性霸妹。一般而言,各種語言的對象唯一標(biāo)識性都是用內(nèi)存地址來體現(xiàn)的知押, 對象具有唯一標(biāo)識的內(nèi)存地址叹螟,所以具有唯一的標(biāo)識。
所以台盯,JavaScript 程序員都知道罢绽,任何不同的 JavaScript 對象其實是互不相等的,我們可以看下面的代碼静盅,o1 和 o2 初看是兩個一模一樣的對象良价,但是打印出來的結(jié)果卻是 false。
var o1 = { a: 1 };
var o2 = { a: 1 };
console.log(o1 == o2); // false
關(guān)于對象的第二個和第三個特征“狀態(tài)和行為”蒿叠,不同語言會使用不同的術(shù)語來抽象描述它們明垢,比如 C++ 中稱它們?yōu)椤俺蓡T變量”和“成員函數(shù)”,Java 中則稱它們?yōu)椤皩傩浴焙汀胺椒ā薄?/p>
在 JavaScript 中市咽,將狀態(tài)和行為統(tǒng)一抽象為“屬性”痊银,考慮到 JavaScript 中將函數(shù)設(shè)計成一種特殊對象(關(guān)于這點,我會在后面的文章中詳細(xì)講解施绎,此處先不用細(xì)究)溯革,所以 JavaScript 中的行為和狀態(tài)都能用屬性來抽象。
下面這段代碼其實就展示了普通屬性和函數(shù)作為屬性的一個例子谷醉,其中 o
是對象致稀,d
是一個屬性,而函數(shù) f
也是一個屬性俱尼,盡管寫法不太相同抖单,但是對 JavaScript 來說,d
和 f
就是兩個普通屬性遇八。
var o = {
d: 1,
f() {
console.log(this.d);
}
};
所以臭猜,總結(jié)一句話來看,在 JavaScript 中押蚤,對象的狀態(tài)和行為其實都被抽象為了屬性蔑歌。如果你用過 Java,一定不要覺得奇怪揽碘,盡管設(shè)計思路有一定差別次屠,但是二者都很好地表現(xiàn)了對象的基本特征:標(biāo)識性园匹、狀態(tài)和行為。
在實現(xiàn)了對象基本特征的基礎(chǔ)上, 我認(rèn)為劫灶,JavaScript 中對象獨有的特色是:對象具有高度的動態(tài)性裸违,這是因為 JavaScript 賦予了使用者在運行時為對象添改狀態(tài)和行為的能力。
我來舉個例子本昏,比如供汛,JavaScript 允許運行時向?qū)ο筇砑訉傩裕@就跟絕大多數(shù)基于類的涌穆、靜態(tài)的對象設(shè)計完全不同怔昨。如果你用過 Java 或者其它別的語言,肯定會產(chǎn)生跟我一樣的感受宿稀。
下面這段代碼就展示了運行時如何向一個對象添加屬性趁舀,一開始我定義了一個對象 o
,定義完成之后祝沸,再添加它的屬性 b
矮烹,這樣操作是完全沒問題的点弯。
var o = { a: 1 };
o.b = 2;
console.log(o.a, o.b); //1 2
為了提高抽象能力冤议,JavaScript 的屬性被設(shè)計成比別的語言更加復(fù)雜的形式,它提供了數(shù)據(jù)屬性和訪問器屬性(getter/setter
)兩類备畦。
JavaScript 對象的兩類屬性
對 JavaScript 來說涩惑,屬性并非只是簡單的名稱和值嘹吨,JavaScript 用一組特征(attribute)來描述屬性(property)。
先來說第一類屬性境氢,數(shù)據(jù)屬性。它比較接近于其它語言的屬性概念碰纬。數(shù)據(jù)屬性具有四個特征萍聊。
- value:就是屬性的值。
- writable:決定屬性能否被賦值悦析。
- enumerable:決定 for in 能否枚舉該屬性寿桨。
- configurable:決定該屬性能否被刪除或者改變特征值。
在大多數(shù)情況下强戴,我們只關(guān)心數(shù)據(jù)屬性的值即可亭螟。
第二類屬性是訪問器(getter/setter)屬性,它也有四個特征骑歹。
- getter:函數(shù)或 undefined预烙,在取屬性值時被調(diào)用。
- setter:函數(shù)或 undefined道媚,在設(shè)置屬性值時被調(diào)用扁掸。
- enumerable:決定 for in 能否枚舉該屬性翘县。
- configurable:決定該屬性能否被刪除或者改變特征值。
訪問器屬性使得屬性在讀和寫時執(zhí)行代碼谴分,它允許使用者在寫和讀屬性時锈麸,得到完全不同的值,它可以視為一種函數(shù)的語法糖牺蹄。
我們通常用于定義屬性的代碼會產(chǎn)生數(shù)據(jù)屬性忘伞,其中的 writable、enumerable沙兰、configurable 都默認(rèn)為 true氓奈。我們可以使用內(nèi)置函數(shù) Object.getOwnPropertyDescripter
來查看,如以下代碼所示:
var o = { a: 1 };
o.b = 2;
//a 和 b 皆為數(shù)據(jù)屬性
Object.getOwnPropertyDescriptor(o,"a") // {value: 1, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor(o,"b") // {value: 2, writable: true, enumerable: true, configurable: true}
我們在這里使用了兩種語法來定義屬性僧凰,定義完屬性后探颈,我們用 JavaScript 的 API 來查看這個屬性,我們可以發(fā)現(xiàn)训措,這樣定義出來的屬性都是數(shù)據(jù)屬性伪节,writeable、enumerable绩鸣、configurable 都是默認(rèn)值為 true怀大。
如果我們要想改變屬性的特征,或者定義訪問器屬性呀闻,我們可以使用 Object.defineProperty
化借,示例如下:
var o = { a: 1 };
Object.defineProperty(o, "b", {value: 2, writable: false, enumerable: false, configurable: true});
//a 和 b 都是數(shù)據(jù)屬性,但特征值變化了
Object.getOwnPropertyDescriptor(o,"a"); // {value: 1, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor(o,"b"); // {value: 2, writable: false, enumerable: false, configurable: true}
o.b = 3;
console.log(o.b); // 2
這里我們使用了 Object.defineProperty 來定義屬性捡多,這樣定義屬性可以改變屬性的 writable 和 enumerable蓖康。
我們同樣用 Object.getOwnPropertyDescriptor 來查看,發(fā)現(xiàn)確實改變了 writable 和 enumerable 特征垒手。因為 writable 特征為 false蒜焊,所以我們重新對 b 賦值,b 的值不會發(fā)生變化科贬。
在創(chuàng)建對象時泳梆,也可以使用 get 和 set 關(guān)鍵字來創(chuàng)建訪問器屬性,代碼如下所示:
var o = { get a() { return 1 } };
console.log(o.a); // 1
訪問器屬性跟數(shù)據(jù)屬性不同榜掌,每次訪問屬性都會執(zhí)行 getter 或者 setter 函數(shù)优妙。這里我們的 getter 函數(shù)返回了 1,所以 o.a 每次都得到 1憎账。
這樣套硼,我們就理解了,實際上 JavaScript 對象的運行時是一個“屬性的集合”胞皱,屬性以字符串或者 Symbol 為 key熟菲,以數(shù)據(jù)屬性特征值或者訪問器屬性特征值為 value看政。
對象是一個屬性的索引結(jié)構(gòu)(索引結(jié)構(gòu)是一類常見的數(shù)據(jù)結(jié)構(gòu),我們可以把它理解為一個能夠以比較快的速度用 key 來查找 value 的字典)抄罕。我們以上面的對象 o 為例允蚣,你可以想象一下“a”是 key, {writable:true,value:1,configurable:true,enumerable:true}
是 value。我們在前面的類型課程中呆贿,已經(jīng)介紹了 Symbol 類型嚷兔,能夠以 Symbol 為屬性名,這是 JavaScript 對象的一個特色做入。
講到了這里冒晰,如果你理解了對象的特征,也就不難理解我開篇提出來的問題竟块。
你甚至可以理解為什么會有“JavaScript 不是面向?qū)ο蟆边@樣的說法了壶运。這是由于 JavaScript 的對象設(shè)計跟目前主流基于類的面向?qū)ο蟛町惙浅4蟆?/p>
可事實上,這樣的對象系統(tǒng)設(shè)計雖然特別浪秘,但是 JavaScript 提供了完全運行時的對象系統(tǒng)蒋情,這使得它可以模仿多數(shù)面向?qū)ο缶幊谭妒剑ㄏ乱还?jié)課我們會給你介紹 JavaScript 中兩種面向?qū)ο缶幊痰姆妒剑夯陬惡突谠停运彩钦y(tǒng)的面向?qū)ο笳Z言耸携。
JavaScript 語言標(biāo)準(zhǔn)也已經(jīng)明確說明棵癣,JavaScript 是一門面向?qū)ο蟮恼Z言,我想標(biāo)準(zhǔn)中能這樣說夺衍,正是因為 JavaScript 的高度動態(tài)性的對象系統(tǒng)狈谊。
所以,我們應(yīng)該在理解其設(shè)計思想的基礎(chǔ)上充分挖掘它的能力沟沙,而不是機(jī)械地模仿其它語言河劝。
結(jié)語
要想理解 JavaScript 對象,必須清空我們腦子里“基于類的面向?qū)ο蟆毕嚓P(guān)的知識矛紫,回到人類對對象的樸素認(rèn)知和面向?qū)ο蟮恼Z言無關(guān)基礎(chǔ)理論赎瞎,我們就能夠理解 JavaScript 面向?qū)ο笤O(shè)計的思路。
在這篇文章中含衔,我從對象的基本理論出發(fā),和你理清了關(guān)于對象的一些基本概念二庵,分析了 JavaScript 對象的設(shè)計思路贪染。接下來又從運行時的角度,介紹了 JavaScript 對象的具體設(shè)計:具有高度動態(tài)性的屬性集合催享。
很多人在思考 JavaScript 對象時杭隙,會帶著已有的“對象”觀來看問題,最后的結(jié)果當(dāng)然就是“剪不斷理還亂”了因妙。
在后面的文章中痰憎,我會繼續(xù)帶你探索 JavaScript 對象的一些機(jī)制票髓,看 JavaScript 如何基于這樣的動態(tài)對象模型設(shè)計自己的原型系統(tǒng),以及你熟悉的函數(shù)铣耘、類等基礎(chǔ)設(shè)施洽沟。
JavaScript對象:我們真的需要模擬類嗎?
早期的 JavaScript 程序員一般都有過使用 JavaScript“模擬面向?qū)ο蟆钡慕?jīng)歷蜗细。
在上一篇文章我們已經(jīng)講到裆操,JavaScript 本身就是面向?qū)ο蟮模⒉恍枰M炉媒,只是它實現(xiàn)面向?qū)ο蟮姆绞胶椭髁鞯牧髋刹惶粯幼偾圆抛尯芏嗳水a(chǎn)生了誤會。
那么吊骤,隨著我們理解的思路繼續(xù)深入缎岗,這些“模擬面向?qū)ο蟆保瑢嶋H上做的事情就是“模擬基于類的面向?qū)ο蟆薄?/p>
盡管我認(rèn)為白粉,“類”并非面向?qū)ο蟮娜看矗覀儾粦?yīng)該責(zé)備社區(qū)出現(xiàn)這樣的方案,事實上蜗元,因為一些公司的政治原因或渤,JavaScript 推出之時,管理層就要求它去模仿 Java奕扣。
所以薪鹦,JavaScript 創(chuàng)始人 Brendan Eich 在“原型運行時”的基礎(chǔ)上引入了 new、this 等語言特性惯豆,使之“看起來語法更像 Java”池磁,而 Java 正是基于類的面向?qū)ο蟮拇碚Z言之一。
但是 JavaScript 這樣的半吊子模擬楷兽,缺少了繼承等關(guān)鍵特性地熄,導(dǎo)致大家試圖對它進(jìn)行修補(bǔ),進(jìn)而產(chǎn)生了種種互不相容的解決方案芯杀。
慶幸的是端考,從 ES6 開始,JavaScript 提供了 class 關(guān)鍵字來定義類揭厚,盡管却特,這樣的方案仍然是基于原型運行時系統(tǒng)的模擬,但是它修正了之前的一些常見的“坑”筛圆,統(tǒng)一了社區(qū)的方案裂明,這對語言的發(fā)展有著非常大的好處。
實際上太援,我認(rèn)為“基于類”并非面向?qū)ο蟮奈ㄒ恍螒B(tài)闽晦,如果我們把視線從“類”移開扳碍,Brendan 當(dāng)年選擇的原型系統(tǒng),就是一個非常優(yōu)秀的抽象對象的形式仙蛉。
我們從頭講起笋敞。
什么是原型?
原型是順應(yīng)人類自然思維的產(chǎn)物捅儒。中文中有個成語叫做“照貓畫虎”液样,這里的貓看起來就是虎的原型,所以巧还,由此我們可以看出鞭莽,用原型來描述對象的方法可以說是古已有之。
我們在上一節(jié)講解面向?qū)ο蟮臅r候提到了:在不同的編程語言中麸祷,設(shè)計者也利用各種不同的語言特性來抽象描述對象澎怒。
最為成功的流派是使用“類”的方式來描述對象,這誕生了諸如 C++阶牍、Java 等流行的編程語言喷面。這個流派叫做基于類的編程語言。
還有一種就是基于原型的編程語言走孽,它們利用原型來描述對象惧辈。我們的 JavaScript 就是其中代表。
“基于類”的編程提倡使用一個關(guān)注分類和類之間關(guān)系開發(fā)模型磕瓷。在這類語言中盒齿,總是先有類,再從類去實例化一個對象困食。類與類之間又可能會形成繼承边翁、組合等關(guān)系。類又往往與語言的類型系統(tǒng)整合硕盹,形成一定編譯時的能力符匾。
與此相對,“基于原型”的編程看起來更為提倡程序員去關(guān)注一系列對象實例的行為瘩例,而后才去關(guān)心如何將這些對象啊胶,劃分到最近的使用方式相似的原型對象,而不是將它們分成類垛贤。
基于原型的面向?qū)ο笙到y(tǒng)通過“復(fù)制”的方式來創(chuàng)建新對象焰坪。一些語言的實現(xiàn)中,還允許復(fù)制一個空對象南吮。這實際上就是創(chuàng)建一個全新的對象琳彩。
基于原型和基于類都能夠滿足基本的復(fù)用和抽象需求誊酌,但是適用的場景不太相同部凑。
這就像專業(yè)人士可能喜歡在看到老虎的時候露乏,喜歡用貓科豹屬豹亞種來描述它,但是對一些不那么正式的場合涂邀,“大貓”可能更為接近直觀的感受一些(插播一個冷知識:比起老虎來瘟仿,美洲獅在歷史上相當(dāng)長時間都被劃分為貓科貓屬,所以性格也跟貓更相似比勉,比較親人)劳较。
我們的 JavaScript 并非第一個使用原型的語言,在它之前浩聋,self观蜗、kevo 等語言已經(jīng)開始使用原型來描述對象了。
事實上衣洁,Brendan 更是曾透露過墓捻,他最初的構(gòu)想是一個擁有基于原型的面向?qū)ο竽芰Φ?scheme 語言(但是函數(shù)式的部分是另外的故事,這篇文章里坊夫,我暫時不做詳細(xì)講述)砖第。
在 JavaScript 之前,原型系統(tǒng)就更多與高動態(tài)性語言配合环凿,并且多數(shù)基于原型的語言提倡運行時的原型修改梧兼,我想,這應(yīng)該是 Brendan 選擇原型系統(tǒng)很重要的理由智听。
原型系統(tǒng)的“復(fù)制操作”有兩種實現(xiàn)思路:
- 一個是并不真的去復(fù)制一個原型對象羽杰,而是使得新對象持有一個原型的引用;
- 另一個是切實地復(fù)制對象瞭稼,從此兩個對象再無關(guān)聯(lián)忽洛。
歷史上的基于原型語言因此產(chǎn)生了兩個流派,顯然环肘,JavaScript 顯然選擇了前一種方式欲虚。
JavaScript 的原型
如果我們拋開 JavaScript 用于模擬 Java 類的復(fù)雜語法設(shè)施(如 new、Function Object悔雹、函數(shù)的 prototype 屬性等)复哆,原型系統(tǒng)可以說相當(dāng)簡單,我可以用兩條概括:
- 如果所有對象都有私有字段 [[prototype]]腌零,就是對象的原型梯找;
- 讀一個屬性,如果對象本身沒有益涧,則會繼續(xù)訪問對象的原型锈锤,直到原型為空或者找到為止。
這個模型在 ES 的各個歷史版本中并沒有很大改變,但從 ES6 以來久免,JavaScript 提供了一系列內(nèi)置函數(shù)浅辙,以便更為直接地訪問操縱原型。三個方法分別為:
- Object.create 根據(jù)指定的原型創(chuàng)建新對象阎姥,原型可以是 null记舆;
- Object.getPrototypeOf 獲得一個對象的原型;
- Object.setPrototypeOf 設(shè)置一個對象的原型呼巴。
利用這三個方法泽腮,我們可以完全拋開類的思維,利用原型來實現(xiàn)抽象和復(fù)用衣赶。我用下面的代碼展示了用原型來抽象貓和虎的例子诊赊。
var cat = {
say(){
console.log("meow~");
},
jump(){
console.log("jump");
}
}
var tiger = Object.create(cat, {
say:{
writable:true,
configurable:true,
enumerable:true,
value:function(){
console.log("roar!");
}
}
})
var anotherCat = Object.create(cat);
anotherCat.say();
var anotherTiger = Object.create(tiger);
anotherTiger.say();
這段代碼創(chuàng)建了一個“貓”對象,又根據(jù)貓做了一些修改創(chuàng)建了虎府瞄,之后我們完全可以用 Object.create 來創(chuàng)建另外的貓和虎對象豪筝,我們可以通過“原始貓對象”和“原始虎對象”來控制所有貓和虎的行為。
但是摘能,在更早的版本中续崖,程序員只能通過 Java 風(fēng)格的類接口來操縱原型運行時,可以說非常別扭团搞。
考慮到 new 和 prototype 屬性等基礎(chǔ)設(shè)施今天仍然有效严望,而且被很多代碼使用,學(xué)習(xí)這些知識也有助于我們理解運行時的原型工作原理逻恐,下面我們試著回到過去像吻,追溯一下早年的 JavaScript 中的原型和類。
早期版本中的類與原型
在早期版本的 JavaScript 中复隆,“類”的定義是一個私有屬性 [[class]]拨匆,語言標(biāo)準(zhǔn)為內(nèi)置類型諸如 Number、String挽拂、Date 等指定了 [[class]] 屬性惭每,以表示它們的類。語言使用者唯一可以訪問 [[class]] 屬性的方式是 Object.prototype.toString亏栈。
以下代碼展示了所有具有內(nèi)置 class 屬性的對象:
var o = new Object;
var n = new Number;
var s = new String;
var b = new Boolean;
var d = new Date;
var arg = function(){ return arguments }();
var r = new RegExp;
var f = new Function;
var arr = new Array;
var e = new Error;
console.log([o, n, s, b, d, arg, r, f, arr, e].map(v => Object.prototype.toString.call(v)));
因此台腥,在 ES3 和之前的版本,JS 中類的概念是相當(dāng)弱的绒北,它僅僅是運行時的一個字符串屬性黎侈。
在 ES5 開始,[[class]] 私有屬性被 Symbol.toStringTag 代替闷游,Object.prototype.toString 的意義從命名上不再跟 class 相關(guān)峻汉。我們甚至可以自定義 Object.prototype.toString 的行為贴汪,以下代碼展示了使用 Symbol.toStringTag 來自定義 Object.prototype.toString 的行為:
var o = { [Symbol.toStringTag]: "MyObject" }
console.log(o + ""); // [object MyObject]
這里創(chuàng)建了一個新對象,并且給它唯一的一個屬性 Symbol.toStringTag休吠,我們用字符串加法觸發(fā)了 Object.prototype.toString 的調(diào)用嘶是,發(fā)現(xiàn)這個屬性最終對 Object.prototype.toString 的結(jié)果產(chǎn)生了影響。
但是蛛碌,考慮到 JavaScript 語法中跟 Java 相似的部分,我們對類的討論不能用“new 運算是針對構(gòu)造器對象辖源,而不是類”來試圖回避蔚携。
所以,我們?nèi)匀灰?new 理解成 JavaScript 面向?qū)ο蟮囊徊糠挚巳模旅嫖揖蛠碇v一下 new 操作具體做了哪些事情酝蜒。
new 運算接受一個構(gòu)造器和一組調(diào)用參數(shù),實際上做了幾件事:
- 以構(gòu)造器的 prototype 屬性(注意與私有字段 [[prototype]] 的區(qū)分)為原型矾湃,創(chuàng)建新對象亡脑;
- 將 this 和調(diào)用參數(shù)傳給構(gòu)造器,執(zhí)行邀跃;
- 如果構(gòu)造器返回的是對象霉咨,則返回,否則返回第一步創(chuàng)建的對象拍屑。
new 這樣的行為途戒,試圖讓函數(shù)對象在語法上跟類變得相似,但是僵驰,它客觀上提供了兩種方式喷斋,一是在構(gòu)造器中添加屬性,二是在構(gòu)造器的 prototype 屬性上添加屬性蒜茴。
下面代碼展示了用構(gòu)造器模擬類的兩種方法:
function c1(){
this.p1 = 1;
this.p2 = function(){
console.log(this.p1);
}
}
var o1 = new c1;
o1.p2();
function c2(){
}
c2.prototype.p1 = 1;
c2.prototype.p2 = function(){
console.log(this.p1);
}
var o2 = new c2;
o2.p2();
第一種方法是直接在構(gòu)造器中修改 this星爪,給 this 添加屬性。
第二種方法是修改構(gòu)造器的 prototype 屬性指向的對象粉私,它是從這個構(gòu)造器構(gòu)造出來的所有對象的原型顽腾。
沒有 Object.create、Object.setPrototypeOf 的早期版本中诺核,new 運算是唯一一個可以指定 [[prototype]] 的方法(當(dāng)時的 mozilla 提供了私有屬性 proto崔泵,但是多數(shù)環(huán)境并不支持),所以猪瞬,當(dāng)時已經(jīng)有人試圖用它來代替后來的 Object.create憎瘸,我們甚至可以用它來實現(xiàn)一個 Object.create 的不完整的 polyfill,見以下代碼:
Object.create = function(prototype){
var cls = function(){}
cls.prototype = prototype;
return new cls;
}
這段代碼創(chuàng)建了一個空函數(shù)作為類陈瘦,并把傳入的原型掛在了它的 prototype幌甘,最后創(chuàng)建了一個它的實例,根據(jù) new 的行為,這將產(chǎn)生一個以傳入的第一個參數(shù)為原型的對象锅风。
這個函數(shù)無法做到與原生的 Object.create 一致酥诽,一個是不支持第二個參數(shù),另一個是不支持 null 作為原型皱埠,所以放到今天意義已經(jīng)不大了肮帐。
ES6 中的類
好在 ES6 中加入了新特性 class,new 跟 function 搭配的怪異行為終于可以退休了(雖然運行時沒有改變)边器,在任何場景训枢,我都推薦使用 ES6 的語法來定義類,而令 function 回歸原本的函數(shù)語義忘巧。下面我們就來看一下 ES6 中的類恒界。
ES6 中引入了 class 關(guān)鍵字,并且在標(biāo)準(zhǔn)中刪除了所有 [[class]] 相關(guān)的私有屬性描述砚嘴,類的概念正式從屬性升級成語言的基礎(chǔ)設(shè)施十酣,從此,基于類的編程方式成為了 JavaScript 的官方編程范式际长。
我們先看下類的基本寫法:
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
// Getter
get area() {
return this.calcArea();
}
// Method
calcArea() {
return this.height * this.width;
}
}
在現(xiàn)有的類語法中耸采,getter/setter 和 method 是兼容性最好的。
我們通過 get/set 關(guān)鍵字來創(chuàng)建 getter工育,通過括號和大括號來創(chuàng)建方法洋幻,數(shù)據(jù)型成員最好寫在構(gòu)造器里面。
類的寫法實際上也是由原型運行時來承載的翅娶,邏輯上 JavaScript 認(rèn)為每個類是有共同原型的一組對象文留,類中定義的方法和屬性則會被寫在原型對象之上。
此外竭沫,最重要的是燥翅,類提供了繼承能力。我們來看一下下面的代碼蜕提。
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(this.name + ' makes a noise.');
}
}
class Dog extends Animal {
constructor(name) {
super(name); // call the super class constructor and pass in the name parameter
}
speak() {
console.log(this.name + ' barks.');
}
}
let d = new Dog('Mitzie');
d.speak(); // Mitzie barks.
以上代碼創(chuàng)造了 Animal 類森书,并且通過 extends 關(guān)鍵字讓 Dog 繼承了它,展示了最終調(diào)用子類的 speak 方法獲取了父類的 name谎势。
比起早期的原型模擬方式凛膏,使用 extends 關(guān)鍵字自動設(shè)置了 constructor,并且會自動調(diào)用父類的構(gòu)造函數(shù)脏榆,這是一種更少坑的設(shè)計猖毫。
所以當(dāng)我們使用類的思想來設(shè)計代碼時,應(yīng)該盡量使用 class 來聲明類须喂,而不是用舊語法吁断,拿函數(shù)來模擬對象趁蕊。
一些激進(jìn)的觀點認(rèn)為,class 關(guān)鍵字和箭頭運算符可以完全替代舊的 function 關(guān)鍵字仔役,它更明確地區(qū)分了定義函數(shù)和定義類兩種意圖掷伙,我認(rèn)為這是有一定道理的。
總結(jié)
在新的 ES 版本中又兵,我們不再需要模擬類了:我們有了光明正大的新語法任柜。而原型體系同時作為一種編程范式和運行時機(jī)制存在。
我們可以自由選擇原型或者類作為代碼的抽象風(fēng)格沛厨,但是無論我們選擇哪種宙地,理解運行時的原型系統(tǒng)都是很有必要的一件事。
JavaScript對象:你知道全部的對象分類嗎俄烁?
JavaScript 中的對象分類
- 宿主對象(host Objects):由 JavaScript 宿主環(huán)境提供的對象,它們的行為完全由宿主環(huán)境決定级野。
- 內(nèi)置對象(Built-in Objects):由 JavaScript 語言提供的對象页屠。
- 固有對象(Intrinsic Objects ):由標(biāo)準(zhǔn)規(guī)定,隨著 JavaScript 運行時創(chuàng)建而自動創(chuàng)建的對象實例蓖柔。
- 原生對象(Native Objects):可以由用戶通過 Array辰企、RegExp 等內(nèi)置構(gòu)造器或者特殊語法創(chuàng)建的對象。
- 普通對象(Ordinary Objects):由{}語法况鸣、Object 構(gòu)造器或者 class 關(guān)鍵字定義類創(chuàng)建的對象牢贸,它能夠被原型繼承。
宿主對象
JavaScript 宿主對象千奇百怪镐捧,但是前端最熟悉的無疑是瀏覽器環(huán)境中的宿主了潜索。
在瀏覽器環(huán)境中,我們都知道全局對象是 window
懂酱,window
上又有很多屬性竹习,如 document
。
實際上列牺,這個全局對象 window
上的屬性整陌,一部分來自 JavaScript 語言,一部分來自瀏覽器環(huán)境瞎领。
JavaScript 標(biāo)準(zhǔn)中規(guī)定了全局對象屬性泌辫,W3C 的各種標(biāo)準(zhǔn)中規(guī)定了 Window 對象的其它屬性。
宿主對象也分為固有的和用戶可創(chuàng)建的兩種九默,比如 document.createElement 就可以創(chuàng)建一些 DOM 對象震放。
宿主也會提供一些構(gòu)造器,比如我們可以使用 new Image 來創(chuàng)建 img 元素驼修,這些我們會在瀏覽器的 API 部分詳細(xì)講解澜搅。
內(nèi)置對象·固有對象
我們在前面說過伍俘,固有對象是由標(biāo)準(zhǔn)規(guī)定,隨著 JavaScript 運行時創(chuàng)建而自動創(chuàng)建的對象實例勉躺。
固有對象在任何 JavaScript 代碼執(zhí)行前就已經(jīng)被創(chuàng)建出來了癌瘾,它們通常扮演者類似基礎(chǔ)庫的角色。我們前面提到的“類”其實就是固有對象的一種饵溅。
ECMA 標(biāo)準(zhǔn)為我們提供了一份固有對象表妨退,里面含有 150+ 個固有對象。你可以通過這個鏈接查看
但是遺憾的是蜕企,這個表格并不完整咬荷。所以在本篇的末尾,我設(shè)計了一個小實驗(小實驗:獲取全部 JavaScript 固有對象)轻掩,你可以自己嘗試一下幸乒,數(shù)一數(shù)一共有多少個固有對象。
內(nèi)置對象·原生對象
我們把 JavaScript 中唇牧,能夠通過語言本身的構(gòu)造器創(chuàng)建的對象稱作原生對象罕扎。在 JavaScript 標(biāo)準(zhǔn)中,提供了 30 多個構(gòu)造器丐重。按照我的理解腔召,按照不同應(yīng)用場景,我把原生對象分成了以下幾個種類扮惦。
通過這些構(gòu)造器臀蛛,我們可以用 new 運算創(chuàng)建新的對象,所以我們把這些對象稱作原生對象崖蜜。
幾乎所有這些構(gòu)造器的能力都是無法用純 JavaScript 代碼實現(xiàn)的浊仆,它們也無法用 class/extend 語法來繼承。
這些構(gòu)造器創(chuàng)建的對象多數(shù)使用了私有字段, 例如:
- Error: [[ErrorData]]
- Boolean: [[BooleanData]]
- Number: [[NumberData]]
- Date: [[DateValue]]
- RegExp: [[RegExpMatcher]]
- Symbol: [[SymbolData]]
- Map: [[MapData]]
這些字段使得原型繼承方法無法正常工作豫领,所以氧卧,我們可以認(rèn)為,所有這些原生對象都是為了特定能力或者性能氏堤,而設(shè)計出來的“特權(quán)對象”沙绝。
用對象來模擬函數(shù)與構(gòu)造器:函數(shù)對象與構(gòu)造器對象
我在前面介紹了對象的一般分類,在 JavaScript 中鼠锈,還有一個看待對象的不同視角闪檬,這就是用對象來模擬函數(shù)和構(gòu)造器。
事實上购笆,JavaScript 為這一類對象預(yù)留了私有字段機(jī)制粗悯,并規(guī)定了抽象的函數(shù)對象與構(gòu)造器對象的概念。
函數(shù)對象的定義是:具有 [[call]] 私有字段的對象同欠,構(gòu)造器對象的定義是:具有私有字段 [[construct]] 的對象样傍。
JavaScript 用對象模擬函數(shù)的設(shè)計代替了一般編程語言中的函數(shù)横缔,它們可以像其它語言的函數(shù)一樣被調(diào)用、傳參衫哥。任何宿主只要提供了“具有 [[call]] 私有字段的對象”茎刚,就可以被 JavaScript 函數(shù)調(diào)用語法支持。
[[call]] 私有字段必須是一個引擎中定義的函數(shù)撤逢,需要接受 this 值和調(diào)用參數(shù)膛锭,并且會產(chǎn)生域的切換,這些內(nèi)容蚊荣,我將會在屬性訪問和執(zhí)行過程兩個章節(jié)詳細(xì)講述初狰。
我們可以這樣說,任何對象只需要實現(xiàn) [[call]]互例,它就是一個函數(shù)對象奢入,可以去作為函數(shù)被調(diào)用。而如果它能實現(xiàn) [[construct]]媳叨,它就是一個構(gòu)造器對象腥光,可以作為構(gòu)造器被調(diào)用。
對于為 JavaScript 提供運行環(huán)境的程序員來說肩杈,只要字段符合柴我,我們在上文中提到的宿主對象和內(nèi)置對象(如 Symbol 函數(shù))可以模擬函數(shù)和構(gòu)造器解寝。
當(dāng)然了扩然,用戶用 function 關(guān)鍵字創(chuàng)建的函數(shù)必定同時是函數(shù)和構(gòu)造器。不過聋伦,它們表現(xiàn)出來的行為效果卻并不相同夫偶。
對于宿主和內(nèi)置對象來說豆巨,它們實現(xiàn) [[call]](作為函數(shù)被調(diào)用)和 [[construct]](作為構(gòu)造器被調(diào)用)不總是一致的徒恋。比如內(nèi)置對象 Date 在作為構(gòu)造器調(diào)用時產(chǎn)生新的對象,作為函數(shù)時吮廉,則產(chǎn)生字符串逾礁,見以下代碼:
console.log(new Date); // 1
console.log(Date())
而瀏覽器宿主環(huán)境中说铃,提供的 Image 構(gòu)造器,則根本不允許被作為函數(shù)調(diào)用嘹履。
console.log(new Image);
console.log(Image());// 拋出錯誤
再比如基本類型(String腻扇、Number、Boolean)砾嫉,它們的構(gòu)造器被當(dāng)作函數(shù)調(diào)用幼苛,則產(chǎn)生類型轉(zhuǎn)換的效果。
值得一提的是焕刮,在 ES6 之后 =>
語法創(chuàng)建的函數(shù)僅僅是函數(shù)舶沿,它們無法被當(dāng)作構(gòu)造器使用墙杯,見以下代碼:
new (a => 0) // error
對于用戶使用 function 語法或者 Function 構(gòu)造器創(chuàng)建的對象來說,[[call]] 和 [[construct]] 行為總是相似的括荡,它們執(zhí)行同一段代碼高镐。
我們看一下示例。
function f(){
return 1;
}
var v = f(); // 把 f 作為函數(shù)調(diào)用
var o = new f(); // 把 f 作為構(gòu)造器調(diào)用
我們大致可以認(rèn)為一汽,它們 [[construct]] 的執(zhí)行過程如下:
- 以 Object.protoype 為原型創(chuàng)建一個新對象避消;
- 以新對象為 this,執(zhí)行函數(shù)的 [[call]]召夹;
- 如果 [[call]] 的返回值是對象岩喷,那么,返回這個對象监憎,否則返回第一步創(chuàng)建的新對象纱意。
這樣的規(guī)則造成了個有趣的現(xiàn)象,如果我們的構(gòu)造器返回了一個新的對象鲸阔,那么 new 創(chuàng)建的新對象就變成了一個構(gòu)造函數(shù)之外完全無法訪問的對象偷霉,這一定程度上可以實現(xiàn)“私有”。
function cls(){
this.a = 100;
return {
getValue:() => this.a
}
}
var o = new cls;
o.getValue(); //100
//a 在外面永遠(yuǎn)無法訪問到
特殊行為的對象
除了上面介紹的對象之外褐筛,在固有對象和原生對象中类少,有一些對象的行為跟正常對象有很大區(qū)別。
它們常見的下標(biāo)運算(就是使用中括號或者點來做屬性訪問)或者設(shè)置原型跟普通對象不同渔扎,這里我簡單總結(jié)一下硫狞。
- Array:Array 的 length 屬性根據(jù)最大的下標(biāo)自動發(fā)生變化。
- Object.prototype:作為所有正常對象的默認(rèn)原型晃痴,不能再給它設(shè)置原型了残吩。
- String:為了支持下標(biāo)運算,String 的正整數(shù)屬性訪問會去字符串里查找倘核。
- Arguments:arguments 的非負(fù)整數(shù)型下標(biāo)屬性跟對應(yīng)的變量聯(lián)動泣侮。
- 模塊的 namespace 對象:特殊的地方非常多,跟一般對象完全不一樣紧唱,盡量只用于 import 吧活尊。
- 類型數(shù)組和數(shù)組緩沖區(qū):跟內(nèi)存塊相關(guān)聯(lián),下標(biāo)運算比較特殊漏益。
- bind 后的 function:跟原來的函數(shù)相關(guān)聯(lián)蛹锰。
結(jié)語
在這篇文章中,我們介紹了一些不那么常規(guī)的對象遭庶,并且我還介紹了 JavaScript 中用對象來模擬函數(shù)和構(gòu)造器的機(jī)制宁仔。
這是一些不那么有規(guī)律、不那么優(yōu)雅的知識峦睡,而 JavaScript 正是通過這些對象翎苫,提供了很多基礎(chǔ)的能力权埠。
我們這次課程留一個挑戰(zhàn)任務(wù):不使用 new 運算符,盡可能找到獲得對象的方法煎谍。
小實驗:獲取全部 JavaScript 固有對象
我們從 JavaScript 標(biāo)準(zhǔn)中可以找到全部的 JavaScript 對象定義攘蔽。JavaScript 語言規(guī)定了全局對象的屬性。
- 三個值: Infinity呐粘、NaN满俗、undefined。
- 九個函數(shù):
- eval
- isFinite
- isNaN
- parseFloat
- parseInt
- decodeURI
- decodeURIComponent
- encodeURI
- encodeURIComponent
- 一些構(gòu)造器:Array作岖、Date唆垃、RegExp、Promise痘儡、Proxy辕万、Map、WeakMap沉删、Set渐尿、WeakSet、Function矾瑰、Boolean砖茸、String、Number殴穴、Symbol凉夯、Object、Error推正、EvalError恍涂、RangeError宝惰、ReferenceError植榕、SyntaxError、TypeError尼夺、URIError尊残、ArrayBuffer、SharedArrayBuffer淤堵、DataView寝衫、Typed Array、Float32Array拐邪、Float64Array慰毅、Int8Array、Int16Array扎阶、Int32Array汹胃、UInt8Array婶芭、UInt16Array、UInt32Array着饥、UInt8ClampedArray犀农。
- 四個用于當(dāng)作命名空間的對象:
- Atomics
- JSON
- Math
- Reflect
我們使用廣度優(yōu)先搜索,查找這些對象所有的屬性和 Getter/Setter宰掉,就可以獲得 JavaScript 中所有的固有對象呵哨。
請你試著先不看我的代碼,在自己的瀏覽器中計算出來 JavaScript 有多少固有對象轨奄。
var set = new Set();
var objects = [
eval,
isFinite,
isNaN,
parseFloat,
parseInt,
decodeURI,
decodeURIComponent,
encodeURI,
encodeURIComponent,
Array,
Date,
RegExp,
Promise,
Proxy,
Map,
WeakMap,
Set,
WeakSet,
Function,
Boolean,
String,
Number,
Symbol,
Object,
Error,
EvalError,
RangeError,
ReferenceError,
SyntaxError,
TypeError,
URIError,
ArrayBuffer,
SharedArrayBuffer,
DataView,
Float32Array,
Float64Array,
Int8Array,
Int16Array,
Int32Array,
Uint8Array,
Uint16Array,
Uint32Array,
Uint8ClampedArray,
Atomics,
JSON,
Math,
Reflect];
objects.forEach(o => set.add(o));
for(var i = 0; i < objects.length; i++) {
var o = objects[i]
for(var p of Object.getOwnPropertyNames(o)) {
var d = Object.getOwnPropertyDescriptor(o, p)
if( (d.value !== null && typeof d.value === "object") || (typeof d.value === "function"))
if(!set.has(d.value))
set.add(d.value), objects.push(d.value);
if( d.get )
if(!set.has(d.get))
set.add(d.get), objects.push(d.get);
if( d.set )
if(!set.has(d.set))
set.add(d.set), objects.push(d.set);
}
}