【THE LAST TIME】一文吃透所有JS原型相關知識點

  • 2019 年 11 月 5 日
  • 筆記

前言

The last time, I have learned

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

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

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

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

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

再回來說原型,老生常談的問題了。但是著實 現在不少熟練工也貌似沒有梳理清楚 FunctionObjectprototype__proto__的關係,本文將從原型到繼承到 es6 語法糖的實現來介紹系統性的介紹 JavaScript 繼承。如果你能夠回答上來以下問題,那麼這位看官,基本這篇不用再花時間閱讀了~

  • 為什麼 typeof 判斷 nullObject 類型?
  • FunctionObject 是什麼關係?
  • new 關鍵字具體做了什麼?手寫實現。
  • prototype__proto__是什麼關係?什麼情況下相等?
  • ES5 實現繼承有幾種方式,優缺點是啥
  • ES6 如何實現一個類
  • ES6 extends 關鍵字實現原理是什麼

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

THE LAST TIME 系列回顧

原型一把梭

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

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

  • function Foo 就是一個方法,比如JavaScript 中內置的 ArrayString
  • function Object 就是一個 Object
  • function Function 就是 Function
  • 以上都是 function,所以 __proto__都是Function.prototype
  • 再次強調,String、Array、Number、Function、Object都是 function

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

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

函數對象和普通對象

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

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

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

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

上圖中,你疑惑的點是不是 Functionnew Function 的關係。其實是這樣子的:

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

__proto__

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

但是在 JavaScript 中,函數也是對象,所以函數也擁有__proto__constructor屬性。

結合上面我們介紹的 ObjectFunction 的關係,看一下程式碼和關係圖

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

proto

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

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

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

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

prototype

object that provides shared properties for other objects

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

所有對象,都可以作為另一個對象的 prototype 來用。

修改__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

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

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

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 判斷 nullobject,其實需要從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:對象

但是,對於 undefinednull 來說,這兩個值的資訊存儲是有點特殊的:

  • null:所有機器碼均為0
  • undefined:用 −2^30 整數來表示

所以在用 typeof 來判斷變數類型的時候,我們需要注意,最好是用 typeof 來判斷基本數據類型(包括symbol),避免對 null 的判斷。

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

instanceof

object instanceof constructor

instanceoftypeof 非常的類似。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

為什麼 ObjectFunction instanceof 自己等於 true,而其他類 instanceof 自己卻又不等於 true 呢?如何解釋?

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

原理淺析

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

這裡,我直接將規範定義翻譯為 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__;   }  }

所以如上原理,加上上文解釋的原型相關知識,我們再來解析下為什麼ObjectFunction 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 中的繼承實現方式

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

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

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

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 如果這麼寫就返回 trueconsole.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();

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

ES6 類的實現原理

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

基礎類

我們就會按照這個類,來回摩擦。然後再來分析編譯後的程式碼。

"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了.

添加屬性

"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也就是來判斷下是否屬性名重複而已。

添加方法

"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(感覺是廢話,不過的確如此)。默認 enumerablefalseconfigurabletrue

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

extend 關鍵字

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

圖片來自冴羽的部落格

關於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,列印出來結果如下:

這裡可能對Parent.call(this,name)有些疑惑,沒關係,我們可以在 Chrome 下調試下。

可以看到,當我們 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的模擬實現