大家在學(xué)JavaScript 面向?qū)ο髸r(shí),往往會(huì)有幾個(gè)疑惑:
1:為什么 JavaScript(直到 ES6)有對(duì)象的概念,但是卻沒有像其他的語言那樣,有類的概念呢;
2:為什么在 JavaScript 對(duì)象里可以自由添加屬性,而其他的語言卻不能呢?
甚至,在一些爭(zhēng)論中,有人強(qiáng)調(diào):JavaScript 并非 "面向?qū)ο蟮恼Z言",而是"基于對(duì)象的語言"。究竟是面向?qū)ο筮€是基于對(duì)象這兩派誰都說服不了誰。
實(shí)際上,基于對(duì)象和面向?qū)ο髢蓚€(gè)形容詞都出現(xiàn)在了 JavaScript 標(biāo)準(zhǔn)的各個(gè)版本當(dāng)中。我們可以先看看 JavaScript 標(biāo)準(zhǔn)對(duì)基于對(duì)象的定義,這個(gè)定義的具體內(nèi)容是:"語言和宿主的基礎(chǔ)設(shè)施由對(duì)象來提供,并且 JavaScript 程序即是一系列互相通訊的對(duì)象集合"。
這里的意思根本不是表達(dá)弱化的面向?qū)ο蟮囊馑?,反而是表達(dá)對(duì)象對(duì)于語言的重要性。
我們首要任務(wù)就是去理解面向?qū)ο蠛?JavaScript 中的面向?qū)ο缶烤故鞘裁础?/p>
什么是面向?qū)ο螅?/strong>
在《面向?qū)ο蠓治雠c設(shè)計(jì)》這本書中,作者 替我們做了總結(jié),他認(rèn)為,從人類的認(rèn)知角度來說,對(duì)象應(yīng)該是下列事物之一:
一個(gè)可以觸摸或者可以看見的東西;
- 人的智力可以理解的東西;
- 可以指導(dǎo)思考或行動(dòng)(進(jìn)行想象或施加動(dòng)作)的東西。
- 有了對(duì)象的自然定義后,我們就可以描述編程語言中的對(duì)象了。在不同的編程語言中,設(shè)計(jì)者也利用各種不+ 同的語言特性來抽象描述對(duì)象,最為成功的流派是使用"類" 的方式來描述對(duì)象,這誕生了諸如 C++、
Java 等流行的編程語言。而 JavaScript 早年卻選擇了一個(gè)更為冷門的方式:原型。這是我在前面說它不合群的原因之一。
JavaScript 推出之時(shí)受管理層之命被要求模仿 Java,所以,JavaScript 創(chuàng)始人 Brendan Eich 在 "原型運(yùn)行時(shí)" 的基礎(chǔ)上引入了 new、this 等語言特性,使之"看起來更像 Java"。 這也就造就了JavaScript這個(gè)古怪的語言。
首先我們來了解一下 JavaScript 是如何設(shè)計(jì)對(duì)象模型的。
JavaScript 對(duì)象的特征
不論我們使用什么樣的編程語言,我們都先應(yīng)該去理解對(duì)象的本質(zhì)特征(參考 Grandy Booch《面向?qū)ο蠓治雠c設(shè)計(jì)》)??偨Y(jié)來看,對(duì)象有如下幾個(gè)特點(diǎn)。
- 對(duì)象具有唯一標(biāo)識(shí)性:即使完全相同的兩個(gè)對(duì)象,也并非同一個(gè)對(duì)象。
- 對(duì)象有狀態(tài):對(duì)象具有狀態(tài),同一對(duì)象可能處于不同狀態(tài)之下。
- 對(duì)象具有行為:即對(duì)象的狀態(tài),可能因?yàn)樗男袨楫a(chǎn)生變遷。
- 我們先來看第一個(gè)特征,對(duì)象具有唯一標(biāo)識(shí)性。一般而言,各種語言的對(duì)象唯一標(biāo)識(shí)性都是用內(nèi)存地址來體現(xiàn)的, 對(duì)象具有唯一標(biāo)識(shí)的內(nèi)存地址,所以具有唯一的標(biāo)識(shí)。
所以我們都應(yīng)該知道,任何不同的 JavaScript 對(duì)象其實(shí)是互不相等的,我們可以看下面的代碼,o1 和 o2 初看是兩個(gè)一模一樣的對(duì)象,但是打印出來的結(jié)果卻是 false。
var o1 = { a: 1 };
var o2 = { a: 1 };
console.log(o1 == o2); // false
關(guān)于對(duì)象的第二個(gè)和第三個(gè)特征"狀態(tài)和行為",不同語言會(huì)使用不同的術(shù)語來抽象描述它們,比如 C++ 中稱它們?yōu)?strong>"成員變量"和"成員函數(shù)",Java 中則稱它們?yōu)?strong>"屬性"和"方法"。
在 JavaScript 中,將狀態(tài)和行為統(tǒng)一抽象為** "屬性"**,這是因?yàn)榭紤]到 JavaScript 中將函數(shù)設(shè)計(jì)成一種特殊對(duì)象所以 JavaScript 中的行為和狀態(tài)都能用屬性來抽象。
下面這段代碼其實(shí)就展示了普通屬性和函數(shù)作為屬性的一個(gè)例子,其中 o 是對(duì)象,count 是一個(gè)屬性,而函數(shù) render 也是一個(gè)屬性,盡管寫法不太相同,但是對(duì) JavaScript 來說,count 和 render 就是兩個(gè)普通屬性。
var o = {
conut: 1,
render() {
console.log(this.d);
}
};
所以,總結(jié)一句話來看,在 JavaScript 中,對(duì)象的狀態(tài)和行為其實(shí)都被抽象為了屬性。
在實(shí)現(xiàn)了對(duì)象基本特征的基礎(chǔ)上, 我認(rèn)為,JavaScript 中對(duì)象獨(dú)有的特色是:對(duì)象具有高度的動(dòng)態(tài)性,這是因?yàn)?JavaScript 賦予了使用者在運(yùn)行時(shí)為對(duì)象添改狀態(tài)和行為的能力。
比如,JavaScript 允許運(yùn)行時(shí)向?qū)ο筇砑訉傩裕@就跟絕大多數(shù)基于類的、靜態(tài)的對(duì)象設(shè)計(jì)完全不同。如果你用過 Java 或者其它別的語言,肯定會(huì)產(chǎn)生跟我一樣的感受。
下面這段代碼就展示了運(yùn)行時(shí)如何向一個(gè)對(duì)象添加屬性,一開始我定義了一個(gè)對(duì)象 o,定義完成之后,再添加它的屬性 b,這樣操作是完全沒問題的。
var o = { a: 1 };
o.b = 2;
console.log(o.a, o.b); //1 2
為了提高抽象能力,JavaScript 的屬性被設(shè)計(jì)成比別的語言更加復(fù)雜的形式,它提供了數(shù)據(jù)屬性和訪問器屬性(getter/setter)兩類。
- JavaScript 對(duì)象的兩類屬性
- 對(duì) JavaScript 來說,屬性并非只是簡(jiǎn)單的名稱和值,JavaScript 用一組特征(attribute)來描述屬性(property)。
先來說第一類屬性,數(shù)據(jù)屬性。它比較接近于其它語言的屬性概念。數(shù)據(jù)屬性具有四個(gè)特征。
- value:就是屬性的值。
- writable:決定屬性能否被賦值。
- enumerable:決定 for in 能否枚舉該屬性。
- configurable:決定該屬性能否被刪除或者改變特征值。
在大多數(shù)情況下,我們只關(guān)心數(shù)據(jù)屬性的值即可。第二類屬性是訪問器(getter/setter)屬性,它也有四個(gè)特征。
- getter:函數(shù)或 undefined,在取屬性值時(shí)被調(diào)用。
- setter:函數(shù)或 undefined,在設(shè)置屬性值時(shí)被調(diào)用。
訪問器屬性使得屬性在讀和寫時(shí)執(zhí)行代碼,它允許使用者在寫和讀屬性時(shí),得到完全不同的值,它可以視為一種函數(shù)的語法糖。
講到了這里,如果你理解了對(duì)象的特征,也就可以理解為什么會(huì)有 **"JavaScript 不是面向?qū)ο? ** 這樣的說法了。
這是由于 JavaScript 的對(duì)象設(shè)計(jì)跟目前主流基于類的面向?qū)ο蟛町惙浅4蟆?墒聦?shí)上,這樣的對(duì)象系統(tǒng)設(shè)計(jì)雖然特別,JavaScript 語言標(biāo)準(zhǔn)也已經(jīng)明確說明,JavaScript 是一門面向?qū)ο蟮恼Z言。
類型系統(tǒng)
接下來繼續(xù)來聊另一個(gè)非常重要的概念,同時(shí)也是很容易被大家忽略的內(nèi)容,那就是 JavaScript 中的'類型系統(tǒng)'。
對(duì)機(jī)器語言來說,所有的數(shù)據(jù)都是一堆二進(jìn)制代碼,CPU 處理這些數(shù)據(jù)的時(shí)候,并沒有類型的概念,CPU 所做的僅僅是移動(dòng)數(shù)據(jù),比如對(duì)其進(jìn)行移位,相加或相乘。
而在高級(jí)語言中,我們都會(huì)為操作的數(shù)據(jù)賦予指定的類型,類型可以確認(rèn)一個(gè)值或者一組值具有特定的意義和目的。所以,類型是高級(jí)語言中的概念。
比如在 C/C++ 中,你需要為要處理的每條數(shù)據(jù)指定類型,這樣定義變量:
int count = 100;
char* name = "zwj";
C/C++ 編譯器負(fù)責(zé)將這些數(shù)據(jù)片段轉(zhuǎn)換為供 CPU 處理的正確命令,通常是二進(jìn)制的機(jī)器代碼。
在JavaScript中引擎可以根據(jù)數(shù)據(jù)自動(dòng)推導(dǎo)出類型,因此就不需要直接指定變量的類型。
var counter = 100;
const name = "ZWJ";
通用的類型有數(shù)字類型、字符串、Boolean 類型等等,引入了這些類型之后,編譯器或者解釋器就可以根據(jù)類型來限制一些有害的或者沒有意義的操作。
比如在 Python 語言中,如果使用字符串和數(shù)字相加就會(huì)報(bào)錯(cuò),因?yàn)?Python 覺得這是沒有意義的。而在 JavaScript 中,字符串和數(shù)字相加是有意義的,可以使用字符串和數(shù)字進(jìn)行相加的。
再比如,你讓一個(gè)字符串和一個(gè)字符串相乘,這個(gè)操作是沒有意義的,所有語言幾乎都會(huì)禁止該操作。
每種語言都定義了自己的類型,還定義了如何操作這些類型,另外還定義了這些類型應(yīng)該如何相互作用,我們就把這稱為類型系統(tǒng)。
關(guān)于類型系統(tǒng)
直觀地理解,一門語言的類型系統(tǒng)定義了各種類型之間應(yīng)該如何相互操作,比如,兩種不同類型相加應(yīng)該如何處理,兩種相同的類型相加又應(yīng)該如何處理等。還規(guī)定了各種不同類型應(yīng)該如何相互轉(zhuǎn)換,比如字符串類型如何轉(zhuǎn)換為數(shù)字類型。
V8是怎么認(rèn)為字符串和數(shù)字相加是有意義?
接下來我們就可以來看看 V8 是怎么處理 1+"2"的了。 之前我們提到過它并不會(huì)報(bào)錯(cuò)而是輸出字符串"12".
當(dāng)有兩個(gè)值相加的時(shí)候,比如:
a+b
V8 會(huì)嚴(yán)格根據(jù) ECMAScript 規(guī)范來執(zhí)行操作。ECMAScript 是一個(gè)語言標(biāo)準(zhǔn),JavaScript 就是 ECMAScript 的一個(gè)實(shí)現(xiàn),比如在 ECMAScript 就定義了怎么執(zhí)行加法操作,如下所示:
通俗地理解:
如果 Type(lprim) 和 Type(rprim) 中有一個(gè)是 String則:
- 把 ToString(lprim) 的結(jié)果賦給左字符串 (lstr);
- 把 ToString(rprim) 的結(jié)果賦給右字符串 (rstr);
- 返回左字符串 (lstr) 和右字符串 (rstr) 拼接的字符串。
如果是其他的(對(duì)象) V8 會(huì)提供了一個(gè) ToPrimitve 方法,其作用是將 a 和 b 轉(zhuǎn)換為原生數(shù)據(jù)類型,其轉(zhuǎn)換流程如下:
- 先檢測(cè)該對(duì)象中是否存在 valueOf 方法,如果有并返回了原始類型,那么就使用該值進(jìn)行強(qiáng)制類型轉(zhuǎn)換;
- 如果 valueOf 沒有返回原始類型,那么就使用 toString 方法的返回值;
- 如果 vauleOf 和 toString 兩個(gè)方法都不返回基本類型值,便會(huì)觸發(fā)一個(gè) TypeError 的錯(cuò)誤。
當(dāng) V8 執(zhí)行 1+"2" 時(shí),因?yàn)檫@是兩個(gè)原始值相加,原始值相加的時(shí)候,如果其中一項(xiàng)是字符串,那么 V8 會(huì)默認(rèn)將另外一個(gè)值也轉(zhuǎn)換為字符串,相當(dāng)于執(zhí)行了下面的操作:
Number(1).toString() + "2"
這個(gè)過程還有另外一個(gè)名詞叫裝箱轉(zhuǎn)換。
關(guān)于裝箱轉(zhuǎn)換
每一種基本類型 Number、String、Boolean在對(duì)象中都有對(duì)應(yīng)的構(gòu)造函數(shù),所謂裝箱轉(zhuǎn)換,正是把基本類型轉(zhuǎn)換為對(duì)應(yīng)的對(duì)象,它是類型轉(zhuǎn)換中一種相當(dāng)重要的種類。
在看一個(gè)例子:
1.toString();
這里會(huì)直接報(bào)錯(cuò),原因如下。
數(shù)字直接量
原因是JavaScript 規(guī)范中規(guī)定的數(shù)字直接量可以支持四種寫法:十進(jìn)制數(shù)、二進(jìn)制整數(shù)、八進(jìn)制整數(shù)和十六進(jìn)制整數(shù)。
十進(jìn)制的 Number 可以帶小數(shù),小數(shù)點(diǎn)前后部分都可以省略,但是不能同時(shí)省略,我們看幾個(gè)例子:
.01
12.
12.01
這都是合法的數(shù)字直接量。這里就有一個(gè)問題,也是我們剛剛提出的報(bào)錯(cuò)問題:
1.toString();
這時(shí)候1. toString()會(huì)被當(dāng)作成一個(gè)帶有小數(shù)的數(shù)字整體。所以我們要把點(diǎn)單獨(dú)成為一個(gè) token(語義單元),就要加入空格,這樣寫:
1 .toString();
//或者
(1).toString();
此時(shí)就不會(huì)報(bào)錯(cuò)了。
但是為什么1能調(diào)用tostring方法, 1不是原始值嗎?
這個(gè)過程就是經(jīng)歷了裝箱轉(zhuǎn)換,在遇到(1).toString() 根據(jù)基本類型 Number 這個(gè)構(gòu)造函數(shù)轉(zhuǎn)換成一個(gè)對(duì)象。
圍繞拆箱 裝箱 轉(zhuǎn)換可以寫出很多有意思的代碼。
{}+[]
- 以{}開頭的會(huì)被解析為語句塊
- 此時(shí)+為一元操作符,非字符串拼接符
- []會(huì)隱式調(diào)用toString()方法,將[]轉(zhuǎn)化為原始值 ''
- +'' 被轉(zhuǎn)化為數(shù)字0
- 擴(kuò)展:如果將其用()括起來,即({}+[]),此時(shí)會(huì)顯示"[object Object]",因?yàn)榇藭r(shí){}不再被解析為語句塊
[]+{}
- []會(huì)隱式調(diào)用toString()方法,將[]轉(zhuǎn)化為原始值 ''
- {}會(huì)隱式調(diào)用toString()方法,將{}轉(zhuǎn)化為原始值"[object Object]"
- +為字符串拼接符
[]+[]
- []會(huì)隱式調(diào)用toString()方法,將[]轉(zhuǎn)化為原始值 ''
{}+{}
- 以{}開頭的會(huì)被解析為語句塊,即第一個(gè){}為語句塊
- 此時(shí)+為一元操作符,非字符串拼接符
- 第二個(gè){}會(huì)隱式調(diào)用toString()方法,將{}轉(zhuǎn)化為原始值"[object Object]"
- +"[object Object]" 輸出 NaN
- 擴(kuò)展 在chrome 瀏覽器中輸出"[object Object][object Object]"
前幾年比較惡心的面試題。
([][[]]+[])[+!![]]+([]+{})[!+[]+!![]]
問題分解:
左 ([][[]]+[])[+!![]]
拆分
[+!![]]
!![] => true
[+!![]] => 1
拆分
([][[]]+[])
[][0] => undefined
undefined+[] =>"undefined"
輸出:"undefined"[1]
右
([]+{})[!+[]+!![]]
([]+{}) => "[object Object]"
拆
[!+[]+!![]]
!![] => true => 1
+[] => 0
!0 => 1
[1+1] => 2
輸出: "[object Object]"[2]
最后: "undefined"[1]+"[object Object]"[2] ==> nb
更多資訊盡在 Ant Vue
