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

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

什麼是面向對象?

囿於中文翻譯,一直覺得「對象」僅僅是爲編程而生的概念,大學那會兒老師的口頭禪就是「沒對象你 new 一個啊」,然而平成就要過去了,我卻仍是母胎 solo。前端

爆哭

winter 老師舉了以下例子來闡述對象。git

對象這一律念在人類的幼兒期造成,這遠遠早於咱們編程邏輯中經常使用的值、過程等概念。github

在幼年期,咱們老是先認識到某一個蘋果能吃(這裏的某一個蘋果就是一個對象),繼而認識到全部的蘋果均可以吃(這裏的全部蘋果,就是一個類),再到後來咱們才能意識到三個蘋果和三個梨之間的聯繫,進而產生數字「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 原型鏈與繼承

相關文章
相關標籤/搜索