JavaScript 到底是面向物件還是基於物件?
你好,我是前阿里手淘前端負責人 winter,這篇文章來自於我在極客時間專欄「重學前端 」中講解 JavaScript 的部分。
與其它的語言相比,JavaScript 中的“物件”總是顯得不是那麼合群。一些新人在學習 JavaScript 面向物件時,往往也會有疑惑:為什麼 JavaScript(直到 ES6)有物件的概念,但是卻沒有像其他的語言那樣,有類的概念呢?為什麼在 JavaScript 物件裡可以自由新增屬性,而其他的語言卻不能呢?
甚至一些爭論中,有人強調,JavaScript 並非“面向物件的語言”,而是“基於物件的語言”,這個說法一度流傳甚廣,而事實上,我至今遇到的持有這一說法的人中,無一能夠回答“如何定義面向物件和基於物件”這個問題。
實際上,基於物件和麵向物件兩個形容詞都出現在了 JavaScript 標準的各個版本當中。我們可以先看看 JavaScript 標準對基於物件的定義,這個定義的具體內容是:“語言和宿主的基礎設施由物件來提供,並且 ECMAScript 程式即是一系列互相通訊的物件集合”。這裡的意思根本不是表達弱化的面向物件的意思,反而是表達物件對於語言的重要性。
那麼,在本篇文章中,我會嘗試讓你去理解面向物件和 JavaScript 中的面向物件究竟是什麼。
什麼是物件?
我們先來說說什麼是物件,因為翻譯的原因,中文語境下我們很難理解“物件”的真正含義。事實上,Object(物件)在英文中,是一切事物的總稱,這和麵向物件程式設計的抽象思維有互通之處。中文的“物件”卻沒有這樣的普適性,我們在學習程式設計的過程中,更多是把它當作一個專業名詞來理解。
但不論如何,我們應該認識到,物件並不是計算機領域憑空造出來的概念,它是順著人類思維模式產生的一種抽象(於是面向物件程式設計也被認為是:更接近人類思維模式的一種程式設計正規化)。
那麼,我們先來看看在人類思維模式下,物件究竟是什麼。
物件這一概念在人類的幼兒期形成,這遠遠早於我們程式設計邏輯中常用的值、過程等概念。在幼年期,我們總是先認識到某一個蘋果能吃(這裡的某一個蘋果就是一個物件),繼而認識到所有的蘋果都可以吃(這裡的所有蘋果,就是一個類),再到後來我們才能意識到三個蘋果和三個梨之間的聯絡,進而產生數字“3”(值)的概念。
在《面向物件分析與設計》這本書中,Grady Booch 替我們做了總結,他認為,從人類的認知角度來說,物件應該是下列事物之一:
- 一個可以觸控或者可以看見的東西;
- 人的智力可以理解的東西;
- 可以指導思考或行動(進行想象或施加動作)的東西。
有了物件的自然定義後,我們就可以描述程式語言中的物件了。在不同的程式語言中,設計者也利用各種不同的語言特性來抽象描述物件,最為成功的流派是使用“類”的方式來描述物件,這誕生了諸如 C++、Java 等流行的程式語言。而 JavaScript 早年卻選擇了一個更為冷門的方式:原型(關於原型,我在下一篇文章會重點介紹,這裡你留個印象就可以了)。這是我在前面說它不合群的原因之一。
然而很不幸,因為一些公司政治原因,JavaScript 推出之時受管理層之命被要求模仿 Java,所以,JavaScript 創始人 Brendan Eich 在“原型執行時”的基礎上引入了 new、this 等語言特性,使之“看起來更像 Java”。
在 ES6 出現之前,大量 JavaScript 程式設計師試圖在原型體系的基礎上,把 JavaScript 變得更像是基於類的程式設計,進而產生了很多所謂的“框架”,比如 PrototypeJS、Dojo。事實上,它們成為了某種 JavaScript 的古怪方言,甚至產生了一系列互不相容的社群,顯然這樣做的收益遠遠小於損失。
如果我們從執行時角度來談論物件,就是在討論 JavaScript 實際執行中的模型,這是由於任何程式碼執行都必定繞不開執行時的物件模型,不過,幸運的是,從執行時的角度看,可以不必受到這些“基於類的設施”的困擾,這是因為任何語言執行時類的概念都是被弱化的。
首先我們來了解一下 JavaScript 是如何設計物件模型的。
JavaScript 物件的特徵
在我看來,不論我們使用什麼樣的程式語言,我們都先應該去理解物件的本質特徵(參考 Grandy Booch《面向物件分析與設計》)。總結來看,物件有如下幾個特點。
- 物件具有唯一標識性:即使完全相同的兩個物件,也並非同一個物件。
- 物件有狀態:物件具有狀態,同一物件可能處於不同狀態下。
- 物件具有行為:即物件的狀態可能因為它的行為產生變遷。
我們先來看第一個特徵,物件具有唯一標識性。一般而言,各種語言的物件唯一標識性都是用記憶體地址來體現的,所以,JavaScript 程式設計師都知道,任何不同的 JavaScript 物件其實是互不相等,我們可以看下面的程式碼,o1 和 o2 初看是兩個一模一樣的物件,但是打印出來的結果卻是 false。
複製程式碼
varo1 ={ a: 1 }; varo2 ={ a: 1 }; console.log(o1 == o2);// false
關於物件的第二個和第三個特徵“狀態和行為”,不同語言會使用不同的術語來抽象描述它們,比如 C++ 中稱它們為“成員變數”和“成員函式”,Java 中則稱它們為“屬性”和“方法”。
在 JavaScript 中,將狀態和行為統一抽象為“屬性”,考慮到 JavaScript 中將函式設計成一種特殊物件(關於這點,我會在後文中詳細講解,此處先不用細究),所以 JavaScript 中的行為和狀態都能用屬性來抽象。
下面這段程式碼其實就展示了普通屬性和函式作為屬性的一個例子,其中 o 是物件,d 是一個屬性,而函式 f 也是一個屬性,儘管寫法不太相同,但是對 JavaScript 來說,d 和 f 就是兩個普通屬性。
複製程式碼
var o = { d: 1, f() { console.log(this.d); } };
所以,總結一句話來看,在 JavaScript 中,物件的狀態和行為其實都被抽象為了屬性。如果你用過 Java,一定不要覺得奇怪,儘管設計思路有一定差別,但是二者都很好地表現了物件的基本特徵:標識性、狀態和行為。
在實現了物件基本特徵的基礎上, 我認為,JavaScript 中物件獨有的特色是:物件具有高度的動態性,這是因為 JavaScript 賦予了使用者在執行時為物件添改狀態和行為的能力。
我來舉個例子,比如,JavaScript 允許執行時向物件新增屬性,這就跟絕大多數基於類的、靜態的物件設計完全不同。如果你用過 Java 或者其它別的語言,肯定會產生跟我一樣的感受。
下面這段程式碼就展示了執行時如何向一個物件新增屬性,一開始我定義了一個物件 o,定義完成之後,再新增它的屬性 b,這樣操作,是完全沒問題的。這一點你要理解。
複製程式碼
var o = { a:1}; o.b =2; console.log(o.a, o.b);//1 2
為了提高抽象能力,JavaScript 的屬性被設計成比別的語言更加複雜的形式,它提供了資料屬性和訪問器屬性(getter/setter)兩類。
JavaScript 物件的兩類屬性
對 JavaScript 來說,屬性並非只是簡單的名稱和值,JavaScript 用一組特徵(attribute)來描述屬性(property)。
先來說第一類屬性,資料屬性。它比較接近於其它語言的屬性概念。資料屬性具有四個特徵。
- value:就是屬性的值。
- writable:決定屬效能否被賦值。
- enumerable:決定 for in 能否列舉該屬性。
- configurable:決定該屬效能否被刪除或者改變特徵值。
在大多數情況下,我們只關心資料屬性的值即可。
第二類屬性是訪問器(getter/setter)屬性,它也有四個特徵。
- getter:函式或 undefined,在取屬性值時被呼叫。
- setter:函式或 undefined,在設定屬性值時被呼叫。
- enumerable:決定 for in 能否列舉該屬性。
- configurable:決定該屬效能否被刪除或者改變特徵值。
訪問器屬性使得屬性在讀和寫時執行程式碼,它允許使用者寫入和讀出屬性時得到完全不同的值,它可以視為一種函式的語法糖。
我們通常用於定義屬性的程式碼會產生資料屬性 ,其中的 writable、enumerable、configurable 都預設為 true。我們可以使用內建函式 Object.getOwnPropertyDescripter 來檢視,如以下程式碼所示:
複製程式碼
varo={a:1}; o.b=2; //a和b皆為資料屬性 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 來檢視這個屬性,我們可以發現,這樣定義出來的屬性都是資料屬性,writeable、enumerable、configurable 都是預設值為 true。
如果我們要想改變屬性的特徵,或者定義訪問器屬性,可以使用 Object.defineProperty,示例如下:
複製程式碼
varo={a:1}; Object.defineProperty(o,"b",{value:2,writable:false,enumerable:false,configurable:true}); //a和b都是資料屬性,但特徵值變化了 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 來檢視,發現確實改變了 writable 和 enumerable 特徵。因為 writable 特徵為 false,所以我們重新對 b 賦值,b 的值不會發生變化。
在建立物件時,也可以使用 get 和 set 關鍵字來建立訪問器屬性,程式碼如下所示:
複製程式碼
varo = {geta(){return1} }; console.log(o.a);// 1
訪問器屬性跟資料屬性不同,每次訪問屬性都會執行 getter 或者 setter 函式。這裡我們的 getter 函式返回了 1,所以 o.a 每次都得到 1。
這樣,我們就理解了,實際上 JavaScript 物件的執行時是一個“屬性的集合”,屬性以字串或者 Symbol 為 key,以資料屬性特徵值或者訪問器屬性特徵值為 value。物件是一個屬性的索引結構(索引結構是一類常見的資料結構,我們可以把它理解為一個能夠以比較快的速度用 key 來查詢 value 的字典)。我們以上面的物件 o 為例,你可以想象一下“a”是 key。
這裡{writable:true,value:1,configurable:true,enumerable:true}是 value。我們在前面的型別課程中,已經介紹了 Symbol 型別,能夠以 Symbol 為屬性名,這是 JavaScript 物件的一個特色。
講到了這裡,如果你理解了物件的特徵,也就不難理解我開篇提出來的問題。
你甚至可以理解為什麼會有“JavaScript 不是面向物件”這樣的說法:JavaScript 的物件設計跟目前主流基於類的面向物件差異非常大。而事實上,這樣的物件系統設計雖然特別,但是 JavaScript 提供了完全執行時的物件系統,這使得它可以模仿多數面向物件程式設計正規化(下一節課我們會給你介紹 JavaScript 中兩種面向物件程式設計的正規化:基於類和基於原型),所以它也是正統的面嚮物件語言。
JavaScript 語言標準也已經明確說明,JavaScript 是一門面向物件的語言,我想標準中能這樣說正因為 JavaScript 的高度動態性的物件系統。
所以,我們應該在理解其設計思想的基礎上充分挖掘它的能力,而不是機械地模仿其它語言。
結語
要想理解 JavaScript 物件,必須清空我們腦子裡“基於類的面向物件”相關的知識,回到人類對物件的樸素認知和麵向物件的語言無關基礎理論,我們就能夠理解 JavaScript 面向物件設計的思路。
在這篇文章中,我從物件的基本理論出發,和你理清了關於物件的一些基本概念,分析了 JavaScript 物件的設計思路。接下來又從執行時的角度,介紹了 JavaScript 物件的具體設計:具有高度動態性的屬性集合。
很多人在思考 JavaScript 物件時,會帶著已有的“物件”觀來看問題,最後的結果當然就是“剪不斷理還亂”了。
在「重學前端 」專欄中,我會繼續帶大家探索 JavaScript 物件的一些機制,看 JavaScript 如何基於這樣的動態物件模型設計自己的原型系統,以及大家熟悉的函式、類等基礎設施,期待你的到來。
拓展閱讀: