一文吃透全部JS原型相關知識點

前言

The last time, I have learnedcss

【THE LAST TIME】一直是我想寫的一個系列,旨在厚積薄發,重溫前端。html

也是給本身的查缺補漏和技術分享。前端

歡迎你們多多評論指點吐槽。node

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

系列文章均首發於公衆號【全棧前端精選】,筆者文章集合詳見GitHub 地址:Nealyang/personalBlog。目錄和發文順序皆爲暫定react

首先我想說,【THE LAST TIME】系列的的內容,向來都是包括但不限於標題的範圍。es6

再回來講原型,老生常談的問題了。可是着實 如今很多熟練工也貌似沒有梳理清楚 Function和 Objectprototype 和__proto__的關係,本文將從原型到繼承到 es6 語法糖的實現來介紹系統性的介紹 JavaScript 繼承。若是你可以回答上來如下問題,那麼這位看官,基本這篇不用再花時間閱讀了~面試

  • 爲何 typeof 判斷 null 是 Object 類型?ajax

  • Function 和 Object 是什麼關係?express

  • new 關鍵字具體作了什麼?手寫實現。canvas

  • prototype 和__proto__是什麼關係?什麼狀況下相等?

  • ES5 實現繼承有幾種方式,優缺點是啥

  • ES6 如何實現一個類

  • ES6 extends 關鍵字實現原理是什麼

若是對以上問題有那麼一些疑惑~那麼。。。

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

THE LAST TIME 系列回顧

目錄

雖文章較長,但較爲基礎。你們酌情閱讀所需章節。

注意文末有思考題哦~~

  • 原型一把梭

    • 函數對象和普通對象

    • __proto__

    • prototype

    • constructor

  • typeof && instanceof 原理淺析

    • typeof 基本用法

    • typeof 原理淺析

    • instanceof 基本用法

    • instanceof 原理淺析

  • ES5 中的繼承實現方式

    • new 手寫版本一

    • new 手寫版本二

    • new 關鍵字

    • 類式繼承

    • 構造函數繼承

    • 組合式繼承

    • 原型式繼承

    • 寄生式繼承

    • 寄生組合式繼承

  • ES6 類的實現原理

    • _inherits

    • _possibleConstructorReturn

    • 基礎類

    • 添加屬性

    • 添加方法

    • extend 關鍵字

原型一把梭

這。。。說是最基礎沒人反駁吧,說沒有用有人反駁吧,說不少人到如今沒梳理清楚沒人反駁吧!OK~ 爲何文章那麼多,你卻尚未弄明白?

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

在概念梳理以前,咱們仍是放一張老掉牙所謂的經典神圖:

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=
  • function Foo 就是一個方法,好比JavaScript 中內置的 ArrayString 等

  • function Object 就是一個 Object

  • function Function 就是 Function

  • 以上都是 function,因此 __proto__都是Function.prototype

  • 再次強調,String、Array、Number、Function、Object都是 function

老鐵,若是對這張圖已很是清晰,那麼可直接跳過此章節

老規矩,咱們直接來梳理概念。

函數對象和普通對象

老話說,萬物皆對象。而咱們都知道在 JavaScript 中,建立對象有好幾種方式,好比對象字面量,或者直接經過構造函數 new 一個對象出來:

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

暫且咱們先無論上面的代碼有什麼意義。至少,咱們能看出,都是對象,卻存在着差別性

其實在 JavaScript 中,咱們將對象分爲函數對象和普通對象。所謂的函數對象,其實就是 JavaScript 的用函數來模擬的類實現。JavaScript 中的 Object 和 Function 就是典型的函數對象。

關於函數對象和普通對象,最直觀的感覺就是。。。咱直接看代碼:

function fun1(){};
const fun2 = function(){};
const fun3 = new Function('name','console.log(name)');

const obj1 = {};
const obj2 = new Object();
const obj3 = new fun1();
const obj4 = new new Function();


console.log(typeof Object);//function
console.log(typeof Function);//function
console.log(typeof fun1);//function
console.log(typeof fun2);//function
console.log(typeof fun3);//function
console.log(typeof obj1);//object
console.log(typeof obj2);//object
console.log(typeof obj3);//object
console.log(typeof obj4);//object

不知道你們看到上述代碼有沒有一些疑惑的地方~彆着急,咱們一點一點梳理。

上述代碼中,obj1obj2obj3obj4都是普通對象,fun1fun2fun3 都是 Function 的實例,也就是函數對象。

因此能夠看出,全部 Function 的實例都是函數對象,其餘的均爲普通對象,其中包括 Function 實例的實例

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

JavaScript 中萬物皆對象,而對象皆出自構造(構造函數)

上圖中,你疑惑的點是否是 Function 和 new Function 的關係。實際上是這樣子的:

Function.__proto__ === Function.prototype//true

__proto__

首先咱們須要明確兩點:1️⃣__proto__constructor對象獨有的。2️⃣prototype屬性是函數獨有的;

可是在 JavaScript 中,函數也是對象,因此函數也擁有__proto__和 constructor屬性。

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

結合上面咱們介紹的 Object 和 Function 的關係,看一下代碼和關係圖

function Person(){…};
 let nealyang = new Person();
watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk= proto

再梳理上圖關係以前,咱們再來說解下__proto__

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

__proto__ 的例子,提及來比較複雜,能夠說是一個歷史問題。

ECMAScript 規範描述 prototype 是一個隱式引用,但以前的一些瀏覽器,已經私自實現了 __proto__這個屬性,使得能夠經過 obj.__proto__ 這個顯式的屬性訪問,訪問到被定義爲隱式屬性的 prototype

所以,狀況是這樣的,ECMAScript 規範說 prototype 應當是一個隱式引用:

  • 經過 Object.getPrototypeOf(obj) 間接訪問指定對象的 prototype 對象

  • 經過 Object.setPrototypeOf(obj, anotherObj) 間接設置指定對象的 prototype 對象

  • 部分瀏覽器提早開了 __proto__ 的口子,使得能夠經過 obj.__proto__ 直接訪問原型,經過 obj.__proto__ = anotherObj 直接設置原型

  • ECMAScript 2015 規範只好向事實低頭,將 __proto__ 屬性歸入了規範的一部分

從瀏覽器的打印結果咱們能夠看出,上圖對象 a 存在一個__proto__屬性。而事實上,他只是開發者工具方便開發者查看原型的故意渲染出來的一個虛擬節點。雖然咱們能夠查看,但實則並不存在該對象上。

__proto__屬性既不能被 for in 遍歷出來,也不能被 Object.keys(obj) 查找出來。

訪問對象的 obj.__proto__ 屬性,默認走的是 Object.prototype 對象上 __proto__ 屬性的 get/set 方法。

Object.defineProperty(Object.prototype,'__proto__',{
	get(){
		console.log('get')
	}
});

({}).__proto__;
console.log((new Object()).__proto__);
watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

關於更多__proto__更深刻的介紹,能夠參看工業聚大佬的《深刻理解 JavaScript 原型》一文。

這裏咱們須要知道的是,__proto__是對象所獨有的,而且__proto__一個對象指向另外一個對象,也就是他的原型對象。咱們也能夠理解爲父類對象。它的做用就是當你在訪問一個對象屬性的時候,若是該對象內部不存在這個屬性,那麼就回去它的__proto__屬性所指向的對象(父類對象)上查找,若是父類對象依舊不存在這個屬性,那麼就回去其父類的__proto__屬性所指向的父類的父類上去查找。以此類推,知道找到 null。而這個查找的過程,也就構成了咱們常說的原型鏈

prototype

object that provides shared properties for other objects

在規範裏,prototype 被定義爲:給其它對象提供共享屬性的對象。prototype 本身也是對象,只是被用以承擔某個職能罷了.

全部對象,均可以做爲另外一個對象的 prototype 來用。

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

修改__proto__的關係圖,咱們添加了 prototype,prototype是函數所獨有的。**它的做用就是包含能夠給特定類型的全部實例提供共享的屬性和方法。它的含義就是函數的遠行對象,**也就是這個函數所建立的實例的遠行對象,正如上圖:nealyang.__proto__ === Person.prototype。任何函數在建立的時候,都會默認給該函數添加 prototype 屬性.

constructor

constructor屬性也是對象所獨有的,它是一個對象指向一個函數,這個函數就是該對象的構造函數

注意,每個對象都有其對應的構造函數,自己或者繼承而來。單從constructor這個屬性來說,只有prototype對象纔有。每一個函數在建立的時候,JavaScript 會同時建立一個該函數對應的prototype對象,而函數建立的對象.__proto__ === 該函數.prototype,該函數.prototype.constructor===該函數自己,故經過函數建立的對象即便本身沒有constructor屬性,它也能經過__proto__找到對應的constructor,因此任何對象最終均可以找到其對應的構造函數。

惟一特殊的可能就是我開篇拋出來的一個問題。JavaScript 原型的老祖宗:Function。它是它本身的構造函數。因此Function.prototype === Function.__proto

爲了直觀瞭解,咱們在上面的圖中,繼續添加上constructor

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

其中 constructor 屬性,虛線表示繼承而來的 constructor 屬性

__proto__介紹的原型鏈,咱們在圖中直觀的標出來的話就是以下這個樣子

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

typeof && instanceof 原理

問什麼好端端的說原型、說繼承會扯到類型判斷的原理上來呢。畢竟原理上有一絲的聯繫,每每面試也是由淺入深、順藤摸瓜的擰出整個知識面。因此這裏咱們也簡單說一下吧。

typeof

MDN 文檔點擊這裏:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/typeof

基本用法

typeof 的用法相比你們都比較熟悉,通常被用於來判斷一個變量的類型。咱們可使用 typeof 來判斷numberundefinedsymbolstringfunctionbooleanobject 這七種數據類型。可是遺憾的是,typeof 在判斷 object 類型時候,有些許的尷尬。它並不能明確的告訴你,該 object 屬於哪種 object

let s = new String('abc');
typeof s === 'object'// true
typeof null;//"object"

原理淺析

要想弄明白爲何 typeof 判斷 null 爲 object,其實須要從js 底層如何存儲變量類型來講起。雖說,這是 JavaScript 設計的一個 bug。

在 JavaScript 最初的實現中,JavaScript 中的值是由一個表示類型的標籤和實際數據值表示的。對象的類型標籤是 0。因爲 null 表明的是空指針(大多數平臺下值爲 0x00),所以,null 的類型標籤是 0,typeof null 也所以返回 "object"。曾有一個 ECMAScript 的修復提案(經過選擇性加入的方式),但被拒絕了。該提案會致使 typeof null === 'null'

js 在底層存儲變量的時候,會在變量的機器碼的低位1-3位存儲其類型信息:

  • 1:整數

  • 110:布爾

  • 100:字符串

  • 010:浮點數

  • 000:對象

可是,對於 undefined 和 null 來講,這兩個值的信息存儲是有點特殊的:

  • null:全部機器碼均爲0

  • undefined:用 −2^30 整數來表示

因此在用 typeof 來判斷變量類型的時候,咱們須要注意,最好是用 typeof 來判斷基本數據類型(包括symbol),避免對 null 的判斷。

typeof 只是咱在討論原型帶出的 instanceof 的附加討論區

instanceof

object instanceof constructor

instanceof 和 typeof 很是的相似。instanceof 運算符用來檢測 constructor.prototype是否存在於參數 object 的原型鏈上。與 typeof 方法不一樣的是,instanceof 方法要求開發者明確地確認對象爲某特定類型。

基本用法

// 定義構造函數
function C(){}
function D(){}

var o = new C();


o instanceof C; // true,由於 Object.getPrototypeOf(o) === C.prototype


o instanceof D; // false,由於 D.prototype 不在 o 的原型鏈上

o instanceof Object; // true,由於 Object.prototype.isPrototypeOf(o) 返回 true
C.prototype instanceof Object // true,同上

C.prototype = {};
var o2 = new C();

o2 instanceof C; // true

o instanceof C; // false,C.prototype 指向了一個空對象,這個空對象不在 o 的原型鏈上.

D.prototype = new C(); // 繼承
var o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true 由於 C.prototype 如今在 o3 的原型鏈上

如上,是 instanceof 的基本用法,它能夠判斷一個實例是不是其父類型或者祖先類型的實例。

console.log(Object instanceof Object);//true
console.log(Function instanceof Function);//true
console.log(Number instanceof Number);//false
console.log(String instanceof String);//false

console.log(Function instanceof Object);//true

console.log(Foo instanceof Function);//true
console.log(Foo instanceof Foo);//false

爲何 Object 和 Function instanceof 本身等於 true,而其餘類 instanceof 本身卻又不等於 true 呢?如何解釋?

要想從根本上了解 instanceof 的奧祕,須要從兩個方面着手:1,語言規範中是如何定義這個運算符的。2,JavaScript 原型繼承機制。

原理淺析

通過上述的分析,想必你們對這種經典神圖已經不那麼陌生了吧,那咱就對着這張圖來聊聊instanceof

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

這裏,我直接將規範定義翻譯爲 JavaScript 代碼以下:

function instance_of(L, R) {//L 表示左表達式,R 表示右表達式
 var O = R.prototype;// 取 R 的顯示原型
 L = L.__proto__;// 取 L 的隱式原型
 while (true) {
   if (L === null)
     return false;
   if (O === L)// 這裏重點:當 O 嚴格等於 L 時,返回 true
     return true;
   L = L.__proto__;
 }
}

因此如上原理,加上上文解釋的原型相關知識,咱們再來解析下爲何Object 和 Function instanceof 本身等於 true

  • Object instanceof Object

// 爲了方便表述,首先區分左側表達式和右側表達式
ObjectL = Object, ObjectR = Object;
// 下面根據規範逐步推演
O = ObjectR.prototype = Object.prototype
L = ObjectL.__proto__ = Function.prototype
// 第一次判斷
O != L
// 循環查找 L 是否還有 __proto__
L = Function.prototype.__proto__ = Object.prototype
// 第二次判斷
O == L
// 返回 true
  • Function instanceof Function

// 爲了方便表述,首先區分左側表達式和右側表達式
FunctionL = Function, FunctionR = Function;
// 下面根據規範逐步推演
O = FunctionR.prototype = Function.prototype
L = FunctionL.__proto__ = Function.prototype
// 第一次判斷
O == L
// 返回 true
  • Foo instanceof Foo

// 爲了方便表述,首先區分左側表達式和右側表達式
FooL = Foo, FooR = Foo;
// 下面根據規範逐步推演
O = FooR.prototype = Foo.prototype
L = FooL.__proto__ = Function.prototype
// 第一次判斷
O != L
// 循環再次查找 L 是否還有 __proto__
L = Function.prototype.__proto__ = Object.prototype
// 第二次判斷
O != L
// 再次循環查找 L 是否還有 __proto__
L = Object.prototype.__proto__ = null 
// 第三次判斷
L == null 
// 返回 false

ES5 中的繼承實現方式

在繼承實現上,工業聚大大在他的原型文章中,將原型繼承分爲兩大類,顯式繼承和隱式繼承。感興趣的能夠點擊文末參考連接查看。

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

可是本文仍是但願可以基於「通俗」的方式來說解幾種常見的繼承方式和優缺點。你們可多多對比查看,其實原理都是同樣,名詞也只是所謂的代稱而已。

關於繼承的文章,不少書本和博客中都有很詳細的講解。如下幾種繼承方式,均總結與《JavaScript 設計模式》一書。也是筆者三年前寫的一篇文章了。

new 關鍵字

在講解繼承以前呢,我以爲 new 這個東西頗有必要介紹下~

一個例子看下new 關鍵字都幹了啥

function Person(name,age){
  this.name = name;
  this.age = age;
  
  this.sex = 'male';
}

Person.prototype.isHandsome = true;

Person.prototype.sayName = function(){
  console.log(`Hello , my name is ${this.name}`);
}

let handsomeBoy = new Person('Nealyang',25);

console.log(handsomeBoy.name) // Nealyang
console.log(handsomeBoy.sex) // male
console.log(handsomeBoy.isHandsome) // true

handsomeBoy.sayName(); // Hello , my name is Nealyang

從上面的例子咱們能夠看到:

  • 訪問到 Person 構造函數裏的屬性

  • 訪問到 Person.prototype 中的屬性

new 手寫版本一

function objectFactory() {

    const obj = new Object(),//從Object.prototype上克隆一個對象

    Constructor = [].shift.call(arguments);//取得外部傳入的構造器

    const F=function(){};
    F.prototype= Constructor.prototype;
    obj=new F();//指向正確的原型

    Constructor.apply(obj, arguments);//借用外部傳入的構造器給obj設置屬性

    return obj;//返回 obj

};
  • new Object() 的方式新建了一個對象 obj

  • 取出第一個參數,就是咱們要傳入的構造函數。此外由於 shift 會修改原數組,因此 arguments 會被去除第一個參數

  • 將 obj 的原型指向構造函數,這樣 obj 就能夠訪問到構造函數原型中的屬性

  • 使用 apply,改變構造函數 this 的指向到新建的對象,這樣 obj 就能夠訪問到構造函數中的屬性

  • 返回 obj

下面咱們來測試一下:

function Person(name,age){
  this.name = name;
  this.age = age;
  
  this.sex = 'male';
}

Person.prototype.isHandsome = true;

Person.prototype.sayName = function(){
  console.log(`Hello , my name is ${this.name}`);
}

function objectFactory() {

    let obj = new Object(),//從Object.prototype上克隆一個對象

    Constructor = [].shift.call(arguments);//取得外部傳入的構造器
    
    console.log({Constructor})

    const F=function(){};
    F.prototype= Constructor.prototype;
    obj=new F();//指向正確的原型

    Constructor.apply(obj, arguments);//借用外部傳入的構造器給obj設置屬性

    return obj;//返回 obj

};

let handsomeBoy = objectFactory(Person,'Nealyang',25);

console.log(handsomeBoy.name) // Nealyang
console.log(handsomeBoy.sex) // male
console.log(handsomeBoy.isHandsome) // true

handsomeBoy.sayName(); // Hello , my name is Nealyang

注意上面咱們沒有直接修改 obj 的__proto__隱式掛載。

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

new 手寫版本二

考慮構造函數又返回值的狀況:

  • 若是構造函數返回一個對象,那麼咱們也返回這個對象

  • 如上不然,就返回默認值

function objectFactory() {

    var obj = new Object(),//從Object.prototype上克隆一個對象

    Constructor = [].shift.call(arguments);//取得外部傳入的構造器

    var F=function(){};
    F.prototype= Constructor.prototype;
    obj=new F();//指向正確的原型

    var ret = Constructor.apply(obj, arguments);//借用外部傳入的構造器給obj設置屬性

    return typeof ret === 'object' ? ret : obj;//確保構造器老是返回一個對象

};

關於 call、apply、bind、this 等用法和原理講解:【THE LAST TIME】this:call、apply、bind

類式繼承

function SuperClass() {
  this.superValue = true;
}
SuperClass.prototype.getSuperValue = function() {
  return this.superValue;
}

function SubClass() {
  this.subValue = false;
}
SubClass.prototype = new SuperClass();

SubClass.prototype.getSubValue = function() {
  return this.subValue;
}

var instance = new SubClass();

console.log(instance instanceof SuperClass)//true
console.log(instance instanceof SubClass)//true
console.log(SubClass instanceof SuperClass)//false

從咱們以前介紹的 instanceof 的原理咱們知道,第三個 console 若是這麼寫就返回 true 了console.log(SubClass.prototype instanceof SuperClass)

雖然實現起來清晰簡潔,可是這種繼承方式有兩個缺點:

  • 因爲子類經過其原型prototype對父類實例化,繼承了父類,因此說父類中若是共有屬性是引用類型,就會在子類中被全部的實例所共享,所以一個子類的實例更改子類原型從父類構造函數中繼承的共有屬性就會直接影響到其餘的子類

  • 因爲子類實現的繼承是靠其原型prototype對父類進行實例化實現的,所以在建立父類的時候,是沒法向父類傳遞參數的。於是在實例化父類的時候也沒法對父類構造函數內的屬性進行初始化

構造函數繼承

function SuperClass(id) {
  this.books = ['js','css'];
  this.id = id;
}
SuperClass.prototype.showBooks = function() {
  console.log(this.books);
}
function SubClass(id) {
  //繼承父類
  SuperClass.call(this,id);
}
//建立第一個子類實例
var instance1 = new SubClass(10);
//建立第二個子類實例
var instance2 = new SubClass(11);

instance1.books.push('html');
console.log(instance1)
console.log(instance2)
instance1.showBooks();//TypeError

SuperClass.call(this,id)固然就是構造函數繼承的核心語句了.因爲父類中給this綁定屬性,所以子類天然也就繼承父類的共有屬性。因爲這種類型的繼承沒有涉及到原型prototype,因此父類的原型方法天然不會被子類繼承,而若是想被子類繼承,就必須放到構造函數中,這樣建立出來的每個實例都會單獨的擁有一份而不能共用,這樣就違背了代碼複用的原則,因此綜合上述兩種,咱們提出了組合式繼承方法

組合式繼承

function SuperClass(name) {
  this.name = name;
  this.books = ['Js','CSS'];
}
SuperClass.prototype.getBooks = function() {
    console.log(this.books);
}
function SubClass(name,time) {
  SuperClass.call(this,name);
  this.time = time;
}
SubClass.prototype = new SuperClass();

SubClass.prototype.getTime = function() {
  console.log(this.time);
}

如上,咱們就解決了以前說到的一些問題,可是是否是從代碼看,仍是有些不爽呢?至少這個SuperClass的構造函數執行了兩遍就感受很是的不妥.

原型式繼承

function inheritObject(o) {
    //聲明一個過渡對象
  function F() { }
  //過渡對象的原型繼承父對象
  F.prototype = o;
  //返回過渡對象的實例,該對象的原型繼承了父對象
  return new F();
}

原型式繼承大體的實現方式如上,是否是想到了咱們new關鍵字模擬的實現?

其實這種方式和類式繼承很是的類似,他只是對類式繼承的一個封裝,其中的過渡對象就至關於類式繼承的子類,只不過在原型繼承中做爲一個普通的過渡對象存在,目的是爲了建立要返回的新的實例對象。

var book = {
    name:'js book',
    likeBook:['css Book','html book']
}
var newBook = inheritObject(book);
newBook.name = 'ajax book';
newBook.likeBook.push('react book');
var otherBook = inheritObject(book);
otherBook.name = 'canvas book';
otherBook.likeBook.push('node book');
console.log(newBook,otherBook);

如上代碼咱們能夠看出,原型式繼承和類式繼承一個樣子,對於引用類型的變量,仍是存在子類實例共享的狀況。

因此,咱們還有下面的寄生式繼

寄生式繼承

var book = {
    name:'js book',
    likeBook:['html book','css book']
}
function createBook(obj) {
    //經過原型方式建立新的對象
  var o = new inheritObject(obj);
  // 拓展新對象
  o.getName = function(name) {
    console.log(name)
  }
  // 返回拓展後的新對象
  return o;
}

其實寄生式繼承就是對原型繼承的拓展,一個二次封裝的過程,這樣新建立的對象不只僅有父類的屬性和方法,還新增了別的屬性和方法。

寄生組合式繼承

回到以前的組合式繼承,那時候咱們將類式繼承和構造函數繼承組合使用,可是存在的問題就是子類不是父類的實例,而子類的原型是父類的實例,因此纔有了寄生組合式繼承

而寄生組合式繼承是寄生式繼承和構造函數繼承的組合。可是這裏寄生式繼承有些特殊,這裏他處理不是對象,而是類的原型。

function inheritObject(o) {
  //聲明一個過渡對象
  function F() { }
  //過渡對象的原型繼承父對象
  F.prototype = o;
  //返回過渡對象的實例,該對象的原型繼承了父對象
  return new F();
}

function inheritPrototype(subClass,superClass) {
    // 複製一份父類的原型副本到變量中
  var p = inheritObject(superClass.prototype);
  // 修正由於重寫子類的原型致使子類的constructor屬性被修改
  p.constructor = subClass;
  // 設置子類原型
  subClass.prototype = p;
}

組合式繼承中,經過構造函數繼承的屬性和方法都是沒有問題的,因此這裏咱們主要探究經過寄生式繼承從新繼承父類的原型。

咱們須要繼承的僅僅是父類的原型,不用去調用父類的構造函數。換句話說,在構造函數繼承中,咱們已經調用了父類的構造函數。所以咱們須要的就是父類的原型對象的一個副本,而這個副本咱們能夠經過原型繼承拿到,可是這麼直接賦值給子類會有問題,由於對父類原型對象複製獲得的複製對象p中的constructor屬性指向的不是subClass子類對象,所以在寄生式繼承中要對複製對象p作一次加強,修復起constructor屬性指向性不正確的問題,最後將獲得的複製對象p賦值給子類原型,這樣子類的原型就繼承了父類的原型而且沒有執行父類的構造函數。

function SuperClass(name) {
  this.name = name;
  this.books=['js book','css book'];
}
SuperClass.prototype.getName = function() {
  console.log(this.name);
}
function SubClass(name,time) {
  SuperClass.call(this,name);
  this.time = time;
}
inheritPrototype(SubClass,SuperClass);
SubClass.prototype.getTime = function() {
  console.log(this.time);
}
var instance1 = new SubClass('React','2017/11/11')
var instance2 = new SubClass('Js','2018/22/33');

instance1.books.push('test book');

console.log(instance1.books,instance2.books);
instance2.getName();
instance2.getTime();
watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

這種方式繼承其實如上圖所示,其中最大的改變就是子類原型中的處理,被賦予父類原型中的一個引用,這是一個對象,所以有一點你須要注意,就是子類在想添加原型方法必須經過prototype.來添加,不然直接賦予對象就會覆蓋從父類原型繼承的對象了.

ES6 類的實現原理

關於 ES6 中的 class 的一些基本用法和介紹,限於篇幅,本文就不作介紹了。該章節,咱們主要經過 babel的 REPL來查看分析 es6 中各個語法糖包括繼承的一些實現方式。

基礎類

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

咱們就會按照這個類,來回摩擦。而後再來分析編譯後的代碼。

"use strict";

function _instanceof(left, right) {
  if (
    right != null &&
    typeof Symbol !== "undefined" &&
    right[Symbol.hasInstance]
  ) {
    return !!right[Symbol.hasInstance](left);
  } else {
    return left instanceof right;
  }
}

function _classCallCheck(instance, Constructor) {
  if (!_instanceof(instance, Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Person = function Person(name) {
  _classCallCheck(this, Person);

  this.name = name;
};

_instanceof就是來判斷實例關係的的。上述代碼就比較簡單了,_classCallCheck的做用就是檢查 Person 這個類,是不是經過new 關鍵字調用的。畢竟被編譯成 ES5 之後,function 能夠直接調用,可是若是直接調用的話,this 就指向 window 對象,就會Throw Error了.

添加屬性

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=
"use strict";

function _instanceof(left, right) {...}

function _classCallCheck(instance, Constructor) {...}

function _defineProperty(obj, key, value) {
    if (key in obj) {
        Object.defineProperty(obj, key, {
            value: value,
            enumerable: true,
            configurable: true,
            writable: true
        });
    } else {
        obj[key] = value;
    }
    return obj;
}

var Person = function Person(name) {
    _classCallCheck(this, Person);

    _defineProperty(this, "shili", '實例屬性');

    this.name = name;
};

_defineProperty(Person, "jingtai", ' 靜態屬性');

其實就是講屬性賦值給誰的問題。若是是實例屬性,直接賦值到 this 上,若是是靜態屬性,則賦值類上。_defineProperty也就是來判斷下是否屬性名重複而已。

添加方法

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=
"use strict";

function _instanceof(left, right) {...}

function _classCallCheck(instance, Constructor) {...}

function _defineProperty(obj, key, value) {...}

function _defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
        var descriptor = props[i];
        descriptor.enumerable = descriptor.enumerable || false;
        descriptor.configurable = true;
        if ("value" in descriptor) descriptor.writable = true;
        Object.defineProperty(target, descriptor.key, descriptor);
    }
}

function _createClass(Constructor, protoProps, staticProps) {
    if (protoProps) _defineProperties(Constructor.prototype, protoProps);
    if (staticProps) _defineProperties(Constructor, staticProps);
    return Constructor;
}

var Person =
    /*#__PURE__*/
    function () {
        function Person(name) {
            _classCallCheck(this, Person);

            _defineProperty(this, "shili", '實例屬性');

            this.name = name;
        }

        _createClass(Person, [{
            key: "sayName",
            value: function sayName() {
                return this.name;
            }
        }, {
            key: "name",
            get: function get() {
                return 'Nealyang';
            },
            set: function set(newName) {
                console.log('new name is :' + newName);
            }
        }], [{
            key: "eat",
            value: function eat() {
                return 'eat food';
            }
        }]);

        return Person;
    }();

_defineProperty(Person, "jingtai", ' 靜態屬性');

看起來代碼量還很多,其實就是一個_createClass函數和_defineProperties函數而已。

首先看_createClass這個函數的三個參數,第一個是構造函數,第二個是須要添加到原型上的函數數組,第三個是添加到類自己的函數數組。其實這個函數的做用很是的簡單。就是增強一下構造函數,所謂的增強構造函數就是給構造函數或者其原型上添加一些函數。

_defineProperties就是多個_defineProperty(感受是廢話,不過的確如此)。默認 enumerable 爲 falseconfigurable 爲 true

其實如上就是 es6 class 的實現原理。

extend 關鍵字

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=
"use strict";

function _instanceof(left, right) {...}

function _classCallCheck(instance, Constructor) {...}

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

function _typeof(obj) {
    if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
        _typeof = function _typeof(obj) {
            return typeof obj;
        };
    } else {
        _typeof = function _typeof(obj) {
            return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
        };
    }
    return _typeof(obj);
}

function _possibleConstructorReturn(self, call) {
    if (call && (_typeof(call) === "object" || typeof call === "function")) {
        return call;
    }
    return _assertThisInitialized(self);
}

function _assertThisInitialized(self) {
    if (self === void 0) {
        throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
    }
    return self;
}

function _getPrototypeOf(o) {
    _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
        return o.__proto__ || Object.getPrototypeOf(o);
    };
    return _getPrototypeOf(o);
}

function _inherits(subClass, superClass) {
    if (typeof superClass !== "function" && superClass !== null) {
        throw new TypeError("Super expression must either be null or a function");
    }
    subClass.prototype = Object.create(superClass && superClass.prototype, {
        constructor: {
            value: subClass,
            writable: true,
            configurable: true
        }
    });
    if (superClass) _setPrototypeOf(subClass, superClass);
}

function _setPrototypeOf(o, p) {
    _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
        o.__proto__ = p;
        return o;
    };
    return _setPrototypeOf(o, p);
}

var Child =
    /*#__PURE__*/
    function (_Parent) {
        _inherits(Child, _Parent);

        function Child(name, age) {
            var _this;

            _classCallCheck(this, Child);

            _this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name)); // 調用父類的 constructor(name)

            _this.age = age;
            return _this;
        }

        return Child;
    }(Parent);

var child1 = new Child('全棧前端精選', '0.3');
console.log(child1);

刪去類相關的代碼生成,剩下的就是繼承的語法糖剖析了。其中super 關鍵字表示父類的構造函數,至關於 ES5 的 Parent.call(this),而後再根據咱們上文說到的繼承方式,有沒有感受該集成的實現跟咱們說的寄生組合式繼承很是的類似呢?

在 ES6 class 中,子類必須在 constructor 方法中調用 super 方法,不然新建實例時會報錯。這是由於子類沒有本身的 this 對象,而是繼承父類的 this 對象,而後對其進行加工。若是不調用 super 方法,子類就得不到 this 對象。

也正是由於這個緣由,在子類的構造函數中,只有調用 super 以後,纔可使用 this 關鍵字,不然會報錯。

關於 ES6 中原型鏈示意圖能夠參照以下示意圖:

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=圖片來自冴羽的博客

關於ES6 中的 extend 關鍵字,上述代碼咱們徹底能夠根據執行來看。其實重點代碼無非就兩行:

_inherits(Child, _Parent);
  _this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name));

咱們分別來分析下具體的實現:

_inherits

代碼比較簡單,都是上文提到的內容,就是創建 Child 和 Parent 的原型鏈關係。代碼解釋已備註在代碼內

function _inherits(subClass, superClass) {
    if (typeof superClass !== "function" && superClass !== null) {//subClass 類型判斷
        throw new TypeError("Super expression must either be null or a function");
    }
    subClass.prototype = Object.create(superClass && superClass.prototype, {
        constructor: {//Object.create 第二個參數是給subClass.prototype添加了 constructor 屬性
            value: subClass,
            writable: true,
            configurable: true//注意這裏enumerable沒有指名,默認是 false,也就是說constructor爲不可枚舉的。
        }
    });
    if (superClass) _setPrototypeOf(subClass, superClass);
}

function _setPrototypeOf(o, p) {
    _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
        o.__proto__ = p;
        return o;
    };
    return _setPrototypeOf(o, p);
}

_possibleConstructorReturn

_this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name));

根據上圖咱們整理的 es6 原型圖可知:

Child.prototype === Parent

因此上面的代碼咱們能夠翻譯爲:

_this = _possibleConstructorReturn(this, Parent.call(this, name));

而後咱們再一層一層撥源碼的實現

function _possibleConstructorReturn(self, call) {
    if (call && (_typeof(call) === "object" || typeof call === "function")) {
        return call;
    }
    return _assertThisInitialized(self);
}

function _assertThisInitialized(self) {
    if (self === void 0) {
        throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
    }
    return self;
}

上述代碼,self其實就是 Child 的 IIFE返回的 function new 調用的 this,打印出來結果以下:

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

這裏可能對Parent.call(this,name)有些疑惑,不要緊,咱們能夠在 Chrome 下調試下。

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk= watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

能夠看到,當咱們 Parent 的構造函數這麼寫

class Parent {
    constructor(name) {
        this.name = name;
    }
}

那麼最終,傳遞給_possibleConstructorReturn函數的第二參數 call就是一個undefined。因此在_possibleConstructorReturn函數裏面會對 call進行判斷,返回正確的this 指向:Child

因此總體代碼的目的就是根據 Parent 構造函數的返回值類型肯定子類構造函數 this 的初始值 _this

最後

【THE LAST TIME】系列關於 JavaScript 基礎的文章目前更新三篇,咱們最後再來一道經典的面試題吧!

function Foo() {
  getName = function() {
    alert(1);
  };
  return this;
}
Foo.getName = function() {
  alert(2);
};
Foo.prototype.getName = function() {
  alert(3);
};
var getName = function() {
  alert(4);
};
function getName() {
  alert(5);
}

//請寫出如下輸出結果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

老鐵,評論區留下你的思考吧~

參考文獻

  • 深刻理解 JavaScript 原型

  • 幫你完全搞懂JS中的prototype、__proto__與constructor

  • JavaScript instanceof 運算符深刻剖析

  • JavaScript深刻之建立對象的多種方式以及優缺點

  • ES6 系列之 Babel 是如何編譯 Class 的(上)

  • ES6—類的實現原理

  • es6類和繼承的實現原理

  • JavaScript深刻之new的模擬實現

相關文章
相關標籤/搜索