JavaScript對象:面向對象仍是基於對象?

與其它的語言相比,JavaScript 中的「對象」老是顯得不那麼合羣。程序員

一些新人在學習 JavaScript 面向對象時,每每也會有疑惑:編程

  • 爲何 JavaScript(直到 ES6)有對象的概念,可是卻沒有像其餘的語言那樣,有類的概念呢;
  • 爲何在 JavaScript 對象裏能夠自由添加屬性,而其餘的語言卻不能呢?

甚至,在一些爭論中,有人強調:JavaScript 並不是「面向對象的語言」,而是「基於對象的語言」。這個說法一度流傳甚廣,而事實上,我至今遇到的持有這一說法的人中,無一可以回答「如何定義面向對象和基於對象」這個問題。bash

實際上,基於對象和麪向對象兩個形容詞都出如今了 JavaScript 標準的各個版本當中。數據結構

咱們能夠先看看 JavaScript 標準對基於對象的定義,這個定義的具體內容是:「語言和宿主的基礎設施由對象來提供,而且 JavaScript 程序便是一系列互相通信的對象集合」。框架

這裏的意思根本不是表達弱化的面向對象的意思,反而是表達對象對於語言的重要性。編程語言

那麼,在本篇文章中,我會嘗試讓你去理解面向對象和 JavaScript 中的面向對象到底是什麼。函數

什麼是面向對象?

咱們先來講說什麼是對象,由於翻譯的緣由,中文語境下咱們很難理解「對象」的真正含義。事實上,Object(對象)在英文中,是一切事物的總稱,這和麪向對象編程的抽象思惟有互通之處。性能

中文的「對象」卻沒有這樣的普適性,咱們在學習編程的過程當中,更可能是把它看成一個專業名詞來理解。學習

但不論如何,咱們應該認識到,對象並非計算機領域憑空造出來的概念,它是順着人類思惟模式產生的一種抽象(因而面向對象編程也被認爲是:更接近人類思惟模式的一種編程範式)。ui

那麼,咱們先來看看在人類思惟模式下,對象到底是什麼。

對象這一律念在人類的幼兒期造成,這遠遠早於咱們編程邏輯中經常使用的值、過程等概念。在幼年期,咱們老是先認識到某一個蘋果能吃(這裏的某一個蘋果就是一個對象),繼而認識到全部的蘋果均可以吃(這裏的全部蘋果,就是一個類),再到後來咱們才能意識到三個蘋果和三個梨之間的聯繫,進而產生數字「3」(值)的概念。

在《面向對象分析與設計》這本書中,Grady Booch 替咱們作了總結,他認爲,從人類的認知角度來講,對象應該是下列事物之一:

  1. 一個能夠觸摸或者能夠看見的東西;
  2. 人的智力能夠理解的東西;
  3. 能夠指導思考或行動(進行想象或施加動做)的東西。

有了對象的天然定義後,咱們就能夠描述編程語言中的對象了。在不一樣的編程語言中,設計者也利用各類不一樣的語言特性來抽象描述對象,最爲成功的流派是使用「類」的方式來描述對象,這誕生了諸如 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。

var o1 = { a: 1 }; 
var o2 = { 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。咱們可使用內置函數 getOwnPropertyDescripter 來查看,如如下代碼所示:

var o = { 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,示例以下:

var o = { 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 關鍵字來建立訪問器屬性,代碼以下所示:

var o = { get a() { return 1 } };

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 對象時,會帶着已有的「對象」觀來看問題,最後的結果固然就是「剪不斷理還亂」了。

轉自:極客時間,若有侵權請聯繫刪除。

相關文章
相關標籤/搜索