天天看點

所有JS原型相關知識點

所有JS原型相關知識點

前言

The last time, I have learned

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

也是給自己的查缺補漏和技術分享。

歡迎大家多多評論指點吐槽。

所有JS原型相關知識點
系列文章均首發于公衆号【全棧前端精選】,筆者文章集合詳見GitHub 位址:Nealyang/personalBlog。目錄和發文順序皆為暫定

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

再回來說原型,老生常談的問題了。但是着實 現在不少熟練工也貌似沒有梳理清楚

Function

Object

prototype

__proto__

的關系,本文将從原型到繼承到 es6 文法糖的實作來介紹系統性的介紹 JavaScript 繼承。如果你能夠回答上來以下問題,那麼這位看官,基本這篇不用再花時間閱讀了~

  • 為什麼

    typeof

    判斷

    null

    Object

    類型?
  • Function

    Object

    是什麼關系?
  • new

    關鍵字具體做了什麼?手寫實作。
  • prototype

    __proto__

    是什麼關系?什麼情況下相等?
  • ES5 實作繼承有幾種方式,優缺點是啥
  • ES6 如何實作一個類
  • ES6

    extends

    關鍵字實作原理是什麼

如果對以上問題有那麼一些疑惑~那麼。。。

所有JS原型相關知識點

THE LAST TIME 系列回顧

  • 【THE LAST TIME】徹底吃透 JavaScript 執行機制
  • 【THE LAST TIME】this:call、apply、bind

目錄

雖文章較長,但較為基礎。大家酌情閱讀所需章節。

注意文末有思考題哦~~

  • 原型一把梭
    • 函數對象和普通對象
    • __proto__

    • prototype

    • constructor

  • typeof

    &&

    instanceof

    原理淺析
    • typeof

      基本用法
    • typeof

      原理淺析
    • instanceof

      基本用法
    • instanceof

      原理淺析
  • ES5 中的繼承實作方式
    • new

      手寫版本一
    • new

      手寫版本二
    • new

      關鍵字
    • 類式繼承
    • 構造函數繼承
    • 組合式繼承
    • 原型式繼承
    • 寄生式繼承
    • 寄生組合式繼承
  • ES6 類的實作原理
    • _inherits

    • _possibleConstructorReturn

    • 基礎類
    • 添加屬性
    • 添加方法
    • extend

      關鍵字

原型一把梭

這。。。說是最基礎沒人反駁吧,說沒有用有人反駁吧,說很多人到現在沒梳理清楚沒人反駁吧!OK~ 為什麼文章那麼多,你卻還沒有弄明白?

所有JS原型相關知識點

在概念梳理之前,我們還是放一張老掉牙所謂的經典神圖:

所有JS原型相關知識點
  • function Foo

    就是一個方法,比如JavaScript 中内置的

    Array

    String

  • function Object

    就是一個

    Object

  • function Function

    就是

    Function

  • 以上都是

    function

    ,是以

    __proto__

    都是

    Function.prototype

  • 再次強調,

    String、Array、Number、Function、Object

    都是

    function

老鐵,如果對這張圖已非常清晰,那麼可直接跳過此章節

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

函數對象和普通對象

老話說,萬物皆對象。而我們都知道在 JavaScript 中,建立對象有好幾種方式,比如對象字面量,或者直接通過構造函數

new

一個對象出來:

所有JS原型相關知識點

暫且我們先不管上面的代碼有什麼意義。至少,我們能看出,都是對象,卻存在着差異性

其實在 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

不知道大家看到上述代碼有沒有一些疑惑的地方~别着急,我們一點一點梳理。

上述代碼中,

obj1

obj2

obj3

obj4

都是普通對象,

fun1

fun2

fun3

都是

Function

的執行個體,也就是函數對象。

是以可以看出,所有 Function 的執行個體都是函數對象,其他的均為普通對象,其中包括 Function 執行個體的執行個體。

所有JS原型相關知識點

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

上圖中,你疑惑的點是不是

Function

new Function

的關系。其實是這樣子的:

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

__proto__

首先我們需要明确兩點:1️⃣

__proto__

constructor

是對象獨有的。2️⃣

prototype

屬性是函數獨有的;

但是在 JavaScript 中,函數也是對象,是以函數也擁有

__proto__

constructor

屬性。

所有JS原型相關知識點

結合上面我們介紹的

Object

Function

的關系,看一下代碼和關系圖

function Person(){…}; let nealyang = new Person();

所有JS原型相關知識點

proto

再梳理上圖關系之前,我們再來講解下

__proto__

所有JS原型相關知識點

__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__);

所有JS原型相關知識點

關于更多

__proto__

更深入的介紹,可以參看工業聚大佬的《深入了解 JavaScript 原型》一文。

這裡我們需要知道的是,

__proto__

是對象所獨有的,并且

__proto__

是一個對象指向另一個對象,也就是他的原型對象。我們也可以了解為父類對象。它的作用就是當你在通路一個對象屬性的時候,如果該對象内部不存在這個屬性,那麼就回去它的

__proto__

屬性所指向的對象(父類對象)上查找,如果父類對象依舊不存在這個屬性,那麼就回去其父類的

__proto__

屬性所指向的父類的父類上去查找。以此類推,知道找到

null

。而這個查找的過程,也就構成了我們常說的原型鍊。

prototype

object that provides shared properties for other objects

在規範裡,prototype 被定義為:給其它對象提供共享屬性的對象。

prototype

自己也是對象,隻是被用以承擔某個職能罷了.

所有對象,都可以作為另一個對象的

prototype

來用。

所有JS原型相關知識點

修改

__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

所有JS原型相關知識點

其中

constructor

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

__proto__

介紹的原型鍊,我們在圖中直覺的标出來的話就是如下這個樣子
所有JS原型相關知識點

typeof && instanceof 原理

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

typeof

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

基本用法

typeof

的用法相比大家都比較熟悉,一般被用于來判斷一個變量的類型。我們可以使用

typeof

來判斷

number

undefined

symbol

string

function

boolean

object

這七種資料類型。但是遺憾的是,

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

所有JS原型相關知識點

這裡,我直接将規範定義翻譯為 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 中的繼承實作方式

在繼承實作上,工業聚大大在他的原型文章中,将原型繼承分為兩大類,顯式繼承和隐式繼承。感興趣的可以點選文末參考連結檢視。

所有JS原型相關知識點

但是本文還是希望能夠基于“通俗”的方式來講解幾種常見的繼承方式和優缺點。大家可多多對比檢視,其實原理都是一樣,名詞也隻是所謂的代稱而已。

關于繼承的文章,很多書本和部落格中都有很詳細的講解。以下幾種繼承方式,均總結與《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__

隐式挂載。

所有JS原型相關知識點

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();

所有JS原型相關知識點

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

ES6 類的實作原理

關于 ES6 中的 class 的一些基本用法和介紹,限于篇幅,本文就不做介紹了。該章節,我們主要通過 babel的 REPL來檢視分析 es6 中各個文法糖包括繼承的一些實作方式。

基礎類

所有JS原型相關知識點
我們就會按照這個類,來回摩擦。然後再來分析編譯後的代碼。

"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

了.

添加屬性

所有JS原型相關知識點

"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

也就是來判斷下是否屬性名重複而已。

添加方法

所有JS原型相關知識點

"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 = 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

false

configurable

true

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

extend 關鍵字

所有JS原型相關知識點

"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 = 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 中原型鍊示意圖可以參照如下示意圖:

所有JS原型相關知識點

圖檔來自冴羽的部落格

關于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

,列印出來結果如下:

所有JS原型相關知識點

這裡可能對

Parent.call(this,name)

有些疑惑,沒關系,我們可以在 Chrome 下調試下。

所有JS原型相關知識點
所有JS原型相關知識點

可以看到,當我們

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的模拟實作

❤️ 看完三件事

如果你覺得這篇内容對你挺有啟發,我想邀請你幫我三個小忙:

  • 點個【在看】,或者分享轉發,讓更多的人也能看到這篇内容
  • 關注公衆号【全棧前端精選】,不定期分享原創&精品技術文章。
  • 公衆号内回複:【 1 】。加入全棧前端精選公衆号交流群。
所有JS原型相關知識點

歡迎評論區留下你的精彩評論~

覺得文章不錯可以分享到朋友圈讓更多的小夥伴看到哦~

客官!在看一下呗

繼續閱讀