《重學(xué)前端》筆記: 101 JavaScript: 類型和對象

《重學(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 值。
  • UndefinedNull 有一定的表意差別吴攒,Null 表示的是:“定義了但是為空”张抄。所以,在實際編程時洼怔,我們一般不會把變量賦值為 undefined署惯,這樣可以保證所有值為 undefined 的變量,都是從未賦值的自然狀態(tài)镣隶。
  • Null 類型也只有一個值极谊,就是 null,它的語義表示空值安岂,與 undefined 不同轻猖,null 是 JavaScript 關(guān)鍵字,所以在任何代碼中嗜闻,你都可以放心用 null 關(guān)鍵字來獲取 null 值蜕依。

Boolean

Boolean 類型有兩個值, truefalse羔巢,它用于表示邏輯意義上的真和假坯癣,同樣有關(guān)鍵字 truefalse 來表示兩個值十绑。

String

我們來看看字符串是否有最大長度替久。

  • String 用于表示文本數(shù)據(jù)灵迫。String 有最大長度是 2^53 - 1宋欺,這在一般開發(fā)中都是夠用的仰美,但是有趣的是妻率,這個所謂最大長度被丧,并不完全是你理解中的字符數(shù)盟戏。

因為 String 的意義并非“字符串”,而是字符串的 UTF16 編碼甥桂,我們字符串的操作 charAt柿究、charCodeAtlength 等方法針對的都是 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)識到 3new 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)粘4a可以把對象的方法在基本類型上使用,比如: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ī)則是非常簡單的惦辛,如下表所示:
image.png

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谚攒。
  • 需要注意的是,parseIntparseFloat不使用這個轉(zhuǎn)換氛堕,所以支持的語法跟這里不盡相同馏臭。
    • 在不傳入第二個參數(shù)的情況下,parseInt 只支持 16 進(jìn)制前綴“0x”讼稚,而且會忽略非數(shù)字字符位喂,也不支持科學(xué)計數(shù)法浪耘。
  • 多數(shù)情況下,Number 是比 parseIntparseFloat 更好的選擇

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)用 valueOftoString 來獲得拆箱后的基本類型。如果 valueOftoString 都不存在劈愚,或者沒有返回基本類型瞳遍,則會產(chǎn)生類型錯誤 TypeError
var o = {
    valueOf : () => {console.log("valueOf"); return {}},
    toString : () => {console.log("toString"); return {}}
}
o * 2
// valueOf
// toString
// TypeError

我們定義了一個對象 o菌羽,o 有 valueOftoString 兩個方法掠械,這兩個方法都返回一個對象,然后我們進(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ī)定有很多不一致的地方惰许。我們可以看下表來對照一下席覆。

image.png

在表格中,多數(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 來說,df 就是兩個普通屬性遇八。

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)用場景,我把原生對象分成了以下幾個種類扮惦。


image.png

通過這些構(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);
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末孟害,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子挪拟,更是在濱河造成了極大的恐慌纹坐,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,843評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舞丛,死亡現(xiàn)場離奇詭異耘子,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)球切,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評論 3 392
  • 文/潘曉璐 我一進(jìn)店門谷誓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人吨凑,你說我怎么就攤上這事捍歪。” “怎么了鸵钝?”我有些...
    開封第一講書人閱讀 163,187評論 0 353
  • 文/不壞的土叔 我叫張陵糙臼,是天一觀的道長。 經(jīng)常有香客問我恩商,道長变逃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,264評論 1 292
  • 正文 為了忘掉前任怠堪,我火速辦了婚禮揽乱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘粟矿。我一直安慰自己凰棉,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,289評論 6 390
  • 文/花漫 我一把揭開白布陌粹。 她就那樣靜靜地躺著撒犀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上或舞,一...
    開封第一講書人閱讀 51,231評論 1 299
  • 那天隧膏,我揣著相機(jī)與錄音,去河邊找鬼嚷那。 笑死胞枕,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的魏宽。 我是一名探鬼主播腐泻,決...
    沈念sama閱讀 40,116評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼队询!你這毒婦竟也來了派桩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,945評論 0 275
  • 序言:老撾萬榮一對情侶失蹤蚌斩,失蹤者是張志新(化名)和其女友劉穎铆惑,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體送膳,經(jīng)...
    沈念sama閱讀 45,367評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡员魏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,581評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了叠聋。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撕阎。...
    茶點故事閱讀 39,754評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖碌补,靈堂內(nèi)的尸體忽然破棺而出虏束,到底是詐尸還是另有隱情,我是刑警寧澤厦章,帶...
    沈念sama閱讀 35,458評論 5 344
  • 正文 年R本政府宣布镇匀,位于F島的核電站,受9級特大地震影響袜啃,放射性物質(zhì)發(fā)生泄漏汗侵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,068評論 3 327
  • 文/蒙蒙 一囊骤、第九天 我趴在偏房一處隱蔽的房頂上張望晃择。 院中可真熱鬧冀值,春花似錦也物、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春告材,著一層夾襖步出監(jiān)牢的瞬間坤次,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評論 1 269
  • 我被黑心中介騙來泰國打工斥赋, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留缰猴,地道東北人。 一個月前我還...
    沈念sama閱讀 47,797評論 2 369
  • 正文 我出身青樓疤剑,卻偏偏與公主長得像滑绒,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子隘膘,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,654評論 2 354