最近在拜讀 winter 大神的《重學前端》系列,果真是大佬的手筆,追本溯源,娓娓道來。感受不只是在重學前端,更是在學習一套方法論。這篇文章是對原型/原型鏈的一個總結,從生活實際入手,攻克 JavaScript 所謂最難理解的一部分。html
囿於中文翻譯,一直覺得「對象」僅僅是爲編程而生的概念,大學那會兒老師的口頭禪就是「沒對象你 new 一個啊」,然而平成就要過去了,我卻仍是母胎 solo。前端
winter 老師舉了以下例子來闡述對象。git
對象這一律念在人類的幼兒期造成,這遠遠早於咱們編程邏輯中經常使用的值、過程等概念。github
在幼年期,咱們老是先認識到某一個蘋果能吃(這裏的某一個蘋果就是一個對象),繼而認識到全部的蘋果均可以吃(這裏的全部蘋果,就是一個類),再到後來咱們才能意識到三個蘋果和三個梨之間的聯繫,進而產生數字「3」(值)的概念。面試
因此說,面向對象編程強調的是數據和操做數據的行爲本質上是互相關聯的,所以好的設計就是把數據以及和它相關的行爲封裝起來。編程
舉例來講,用來表示一個單詞或者短語的一串字符一般被稱爲字符串。字符就是數據。可是你關心的每每不是數據是什麼,而是能夠對數據作什麼,因此能夠應用在這種數據上的行爲(計算長度、添加數據、搜索,等等)都被設計成 String 類的方法。數組
對象具備惟一標識性:即便徹底相同的兩個對象,也並不是同一個對象。瀏覽器
對象有狀態:對象具備狀態,同一對象可能處於不一樣狀態之下。微信
對象具備行爲:即對象的狀態,可能由於它的行爲產生變遷。函數
第一點很好理解,對象存放在堆內存中,具備惟一標識的內存地址,因此具備惟一的標識。而對於「對象有狀態和行爲」,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.getPrototypeOf
和 Object.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 全解析》 系列,更詳細的用法能夠直接去裏面查看,點擊下面各方法的標題也能夠直接跳轉。
用於建立一個新的對象,它使用現有對象做爲新對象的 __proto__
。第一個參數爲原型對象,第二個參數可選,能夠傳入屬性描述符對象或 null,其餘類型直接報錯。
沒錯,這就是「照貓畫虎」!
const cat = { type: '貓科' };
const tiger = Object.create(cat);
tiger.tooth = '大牙';
複製代碼
該方法返回一個由指定對象的全部自身屬性的屬性名組成的數組。
包括不可枚舉屬性
但不包括 Symbol 值做爲名稱的屬性
不會獲取到原型鏈上的屬性
當不存在普通字符串做爲名稱的屬性時返回一個空數組
// 它只會獲取自身屬性,而不去關心原型鏈上的屬性
Object.getOwnPropertyNames(tiger); // ['tooth']
複製代碼
這兩個用於獲取和設置一個對象的原型,它主要用來代替 __proto__
。
用來判斷一個對象自己是否含有該屬性,返回一個 Boolean 值。
原型鏈上的屬性 一概返回 false
Symbol
類型的屬性也能夠被檢測
tiger.hasOwnProperty('tooth'); // true
tiger.hasOwnProperty('type'); // false
複製代碼
該方法用於檢測一個對象是否存在於另外一個對象的原型鏈上,返回一個 Boolean 值。
cat.isPrototypeOf(tiger); // true
複製代碼
下一篇會着重介紹繼承和 ES6 新增的 class,敬請期待。
歡迎關注個人微信公衆號:進擊的前端
《JavaScript 高級程序設計 (第三版)》 —— Nicholas C. Zakas
《深刻理解 ES6》 —— Nicholas C. Zakas
《你不知道的 JavaScript (上卷)》—— Kyle Simpson