從感性角度談原型 / 原型鏈

YanceyOfficial發表於2019-04-09

最近在拜讀 winter 大神的《重學前端》系列,果然是大佬的手筆,追本溯源,娓娓道來。感覺不僅是在重學前端,更是在學習一套方法論。這篇文章是對原型/原型鏈的一個總結,從生活實際入手,攻克 JavaScript 所謂最難理解的一部分。

什麼是物件導向?

囿於中文翻譯,一直以為“物件”僅僅是為程式設計而生的概念,大學那會兒老師的口頭禪就是“沒物件你 new 一個啊”,然而平成就要過去了,我卻還是母胎 solo。

爆哭

winter 老師舉了如下例子來闡述物件。

物件這一概念在人類的幼兒期形成,這遠遠早於我們程式設計邏輯中常用的值、過程等概念。

在幼年期,我們總是先認識到某一個蘋果能吃(這裡的某一個蘋果就是一個物件),繼而認識到所有的蘋果都可以吃(這裡的所有蘋果,就是一個類),再到後來我們才能意識到三個蘋果和三個梨之間的聯絡,進而產生數字“3”(值)的概念。

所以說,物件導向程式設計強調的是資料和運算元據的行為本質上是互相關聯的,因此好的設計就是把資料以及和它相關的行為封裝起來。

舉例來說,用來表示一個單詞或者短語的一串字元通常被稱為字串。字元就是資料。但是你關心的往往不是資料是什麼,而是可以對資料做什麼,所以可以應用在這種資料上的行為(計算長度、新增資料、搜尋,等等)都被設計成 String 類的方法。

JavaScript 的物件特徵

  • 物件具有唯一標識性:即使完全相同的兩個物件,也並非同一個物件。

  • 物件有狀態:物件具有狀態,同一物件可能處於不同狀態之下。

  • 物件具有行為:即物件的狀態,可能因為它的行為產生變遷。

第一點很好理解,物件存放在堆記憶體中,具有唯一標識的記憶體地址,所以具有唯一的標識。而對於“物件有狀態和行為”,this 似乎最能闡述這一點,不同方式呼叫函式讓 this 在執行時有不同的指向,從而產生不同的行為。

建構函式

建構函式本身就是一個函式,與普通函式沒有任何區別。但為了做些區分,使用 new 生成例項的函式我們把它稱為建構函式(形式上我們一般將建構函式的名稱首字母大寫),而直接呼叫的就是普通函式。

與傳統的面嚮物件語言不同,JavaScript 沒有 的概念,即便是 ES6 增加了 class 關鍵字,也無非是原型的語法糖。當年 JavaScript 為了模仿 Java,也加入了 new 操作符,但它後面直接跟的是 建構函式 而非 class

function Dog(name, age) {
  this.name = name;
  this.age = age;
  this.bark = function() {
    return 'wangwang~';
  };
}

const husky = new Dog('Lolita', 2);
const alaska = new Dog('Roland', 3);
複製程式碼

屬性和方法都放在建構函式裡

雖然上面的程式碼有了物件導向的味道,但它卻有一個缺陷。我們根據 Dog 建立了兩個例項,導致 bark 方法被建立了兩次,這無疑造成了浪費。所以有沒有一種辦法將 bark 方法單獨放到一個地方,讓所有的例項都能訪問到呢?沒錯,就是接下來要說到的原型。

原型

下面是一張神圖,原型/原型鏈之精髓融匯於此。很多面試官要求你手畫原型鏈,它是個很好的參照。

圖解原型鏈

生活中的原型

何為“原型”? 從感性的角度來講,原型是順應人類自然思維的產物。有個成語叫做“照貓畫虎”,這裡的貓就是虎的原型,另一個俗語“比著葫蘆畫瓢”亦是如此。可見,“原型”可以是一個具體的、現實存在的事物。

而我們再看“類”。以房屋和圖紙為例,這裡圖紙就是“類”。圖紙的意義在於“指導”工人創造出真實的房子(例項)。因此“類”更傾向於是一種具有指導意義的理論和思想。

所以,JavaScript 才是真正應該被稱為“物件導向”的語言,因為它是少有的可以不通過類,直接建立物件的語言。

技術上的原型

在 JavaScript 中,每個函式都有一個 prototype 屬性(這個說法並不嚴謹,像 Symbol 和 Math 就沒有),該屬性指向一個物件,稱為 原型物件,當使用建構函式建立例項時,prototype 屬性指向的原型物件就成為例項的原型物件。

原型物件預設有一個 constructor 屬性,它指向該原型物件對應的建構函式。由於例項物件可以繼承原型物件的屬性,所以例項物件也可以直接呼叫constructor 屬性,同樣指向原型物件對應的建構函式。

建構函式和原型物件的關係

function Foo() {}

const foo = new Foo();

// 原型物件的 constructor 屬性指向建構函式
Foo.prototype.constructor === Foo; // true

// 例項的 constructor 屬性同樣指向建構函式
foo.constructor === Foo; // true
複製程式碼

每個例項都有一個隱藏的屬性 [[prototype]],指向它的原型物件,我們可以使用下面兩種方式的任意一種來獲取例項的原型物件。

instance.__proto__;

Object.getPrototypeOf(instance);
複製程式碼

注意:在 ES5 之前,為了能訪問到 [[Prototype]],瀏覽器廠商創造了 __proto__ 屬性。但在 ES5 之後有了標準方法 Object.getPrototypeOfObject.setPrototypeOf。儘管為了瀏覽器的相容性,已經將 __proto__ 屬性新增到 ES6 規範中,但它已被不推薦使用。

例項、原型物件和建構函式之間的關係

至此,原型就介紹完了,實際並沒那麼複雜。通過上面這張圖片,我們很容易得到下面這個公式。

Object.getPrototypeOf(例項) === 建構函式.prototype;
複製程式碼

所以說,原型物件類似於一座“橋樑”,連通例項和建構函式,因此我們可以把公共的屬性或方法放在原型物件裡,這樣就能解決建構函式例項化產生多個重複方法的問題了。我們修改一下建構函式那個例子,將 bark 方法放到 Dog 建構函式的原型中,這樣無論 new 多少個例項都只會建立一份 bark 方法。

function Dog(name, age) {
  this.name = name;
  this.age = age;
}

Dog.prototype.bark = function() {
  return 'wangwang~';
};

const husky = new Dog('Lolita', 2);
const alaska = new Dog('Roland', 3);

husky.bark(); // 'wangwang~'
alaska.bark(); // 'wangwang~'
複製程式碼

方法放在原型裡

原型鏈

每個物件都擁有一個原型物件,通過 __proto__ 指標指向上一個原型 ,並從中繼承方法和屬性,同時原型物件也可能擁有原型,這樣一層一層,逐級向上,最終指向 null(null 沒有原型)。這種關係被稱為原型鏈 (prototype chain),通過原型鏈,一個物件會擁有定義在其他物件中的屬性和方法。

function Parent(name) {
  this.name = name;
}

const p = new Parent();

p.__proto__ === Parent.prototype; // true
p.__proto__.__proto__ === Object.prototype; // true
p.__proto__.__proto__.__proto__ === null; // true
複製程式碼

原型鏈

一些關於原型/原型鏈的方法

這裡簡單列舉一些關於原型/原型鏈常用的內建方法,最近在寫一個 《JavaScript API 全解析》 系列,更詳細的用法可以直接去裡面檢視,點選下面各方法的標題也可以直接跳轉。

Object.create()

用於建立一個新的物件,它使用現有物件作為新物件的 __proto__。第一個引數為原型物件,第二個引數可選,可以傳入屬性描述符物件或 null,其他型別直接報錯。

沒錯,這就是“照貓畫虎”!

const cat = { type: '貓科' };

const tiger = Object.create(cat);

tiger.tooth = '大牙';
複製程式碼

“照貓畫虎”

Object.getOwnPropertyNames()

該方法返回一個由指定物件的所有自身屬性的屬性名組成的陣列。

  • 包括不可列舉屬性

  • 但不包括 Symbol 值作為名稱的屬性

  • 不會獲取到原型鏈上的屬性

  • 當不存在普通字串作為名稱的屬性時返回一個空陣列

// 它只會獲取自身屬性,而不去關心原型鏈上的屬性
Object.getOwnPropertyNames(tiger); // ['tooth']
複製程式碼

Object.getPrototypeOf() / Object.setPrototypeOf()

這兩個用於獲取和設定一個物件的原型,它主要用來代替 __proto__

hasOwnProperty

用來判斷一個物件本身是否含有該屬性,返回一個 Boolean 值。

  • 原型鏈上的屬性 一律返回 false

  • Symbol 型別的屬性也可以被檢測

tiger.hasOwnProperty('tooth'); // true
tiger.hasOwnProperty('type'); // false
複製程式碼

isPrototypeOf

該方法用於檢測一個物件是否存在於另一個物件的原型鏈上,返回一個 Boolean 值。

cat.isPrototypeOf(tiger); // true
複製程式碼

最後

下一篇會著重介紹繼承和 ES6 新增的 class,敬請期待。

歡迎關注我的微信公眾號:進擊的前端

進擊的前端

參考

《JavaScript 高階程式設計 (第三版)》 —— Nicholas C. Zakas

《深入理解 ES6》 —— Nicholas C. Zakas

《你不知道的 JavaScript (上卷)》—— Kyle Simpson

三分鐘看完 JavaScript 原型與原型鏈

[進階 5-1 期] 重新認識建構函式、原型和原型鏈

[進階 5-2 期] 圖解原型鏈及其繼承

詳解 JS 原型鏈與繼承

相關文章