重學原型與繼承

算是炒冷飯吧,最近看React源碼發現有一些原型與繼承方面的東西沒看太明白,便計劃花兩天重溫這方面的東西,以便以後有更好的腦回路。前端

概念

  • prototype 顯式原型對象,每個函數(除了bind)在建立以後都會擁有一個名爲 prototype 的內部屬性,它指向函數的原型對象。用來實現基於原型的繼承與屬性的共享。
  • __proto__ 隱式原型對象,是對象的內部屬性, 任意對象都有一個內置屬性 [[prototype]],在ES5以前沒有標準的方法訪問這個內置屬性,可是大多數瀏覽器都支持經過 __proto__ 訪問而且ES5能夠經過 Object.getPrototypeOf(target) 訪問。它指向建立這個對象的函數 constructorprototype, 對象依賴它構成原型鏈進行向上查詢。

只有函數纔有顯示原型屬性 prototypeweb

全部對象都有隱式原型屬性 __proto__ 包括函數的原型 Function.prototype === Object.__proto__express

Object

Function,String,Number,Boolean...(null 不算)全部對象都擁有 __proto__ 屬性,因此才都具備對象的特色。全部這些原生構造函數的 __proto__ 通通指向 Function.prototype。而它的__proto__ 又指向 Object.prototype。因此才稱萬物皆對象數組

一個對象的隱式原型指向構造該對象的構造函數的顯式原型對象。瀏覽器

var str = String('seven')
str.__proto__ === String.prototype // true
複製代碼

不管是經過字面量建立仍是構造器亦或者是new+構造器建立,js都會幫咱們自動裝箱完成實例對象的轉換。babel

Function

Function 是一個比較獨特的對象,便是對象,也是函數。函數

// Foo 由 Function 構造
var Foo = Function('a','b','return a+b')
// 等同於
function Foo(a,b){ return a + b}
Foo.__proto__ === Function.prototype

// foo 由 Foo 構造
var foo = new Foo();
foo.__proto__ === Foo.prototype
複製代碼

字面量建立等同於調用構造器建立,但和 new 建立出來的 實例對象 又不一樣。牽扯到「裝箱」、「拆箱」...扯遠了...性能

好比上文中 Foo 也有 __proto__ 屬性,前面提過,__proto__ 指向的是 構造函數的顯式原型FooFunction 實例化而來,因此 Foo.__proto__ 指向的是 Function.prototype,不止是Function,Object和其餘類型都是同樣的道理。測試

prototype

函數不只能作對象能作的事情以外,還有個"特權"屬性 prototype。這個屬性是一個指針,指向一個對象,這個對象的用途就是包含全部實例共享的屬性和方法ui

把上文中的 Foo.prototype 打印出來

{
    constructor: ƒ Foo()
    __proto__: Object
}
複製代碼

Foo 的顯式原型對象也是對象,原型對象的構造函數都是 Object,它的 __proto__ 屬性固然是指向 Object.prototype

Foo.prototype.__proto__ === Object.prototype
複製代碼

假如咱們想給數組原型添加一個去重排序方法 uniqueFlatWithSort,讓全部數組均可以使用。應該都知道直接往Array.prototype上加

Array.prototype.uniqueFlatWithSort = function() {
  return [...new Set(this.flat(Infinity))].sort((a,b)=> a - b)
}
var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];
arr.uniqueFlatWithSort() // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
複製代碼

咱們都知道屬性是經過隱式原型__proto__遞歸向上原型鏈查找的,而隱式原型指向的正是構造函數Array的顯式原型prototype

仍是上面的例子

var str = String('seven')
str.padEnd(10,"$") // seven$$$$$
str.padEnd === str.__proto__.padEnd === String.prototype.padEnd
複製代碼
  • str 首先進行裝箱操做,轉化成字符串對象,String {"seven"}
  • 首先在 str 上找不到 padEnd的屬性,開始進行原型鏈向上查找。
  • 便去 str.__proto__ 上找,也就是 String.prototype,發現了String.prototype.padEnd 並返回...

若是字符串 str 想調用 ObjecthasOwnProperty 方法。

  • 一樣首先進行裝箱操做,轉化成字符串對象,String {"seven"}
  • 接着在自身查找有無 hasOwnProperty 方法,發現沒有,開始進行原型鏈向上查找。
  • str.__proto__ 也就是 String.prototype 仍然沒有該屬性。
  • 再往上去 str.__proto__.__proto__ 上找,也就是 String.prototype 原型對象的構造函數 Object 顯式原型 prototype 上去找。
  • 找到 Object.prototype.hasOwnProperty 並返回。

這也就是爲何通常都會將實例方法建立前掛載在其顯式原型上,好讓子類的隱式原型經過進行原型鏈向上查找。instanceOf 即是這個原理遍歷原型鏈。

調用一個方法的時候,首先在對象自己屬性內查找,沒有則到 obj.__proto__ 隱式原型內查找,若是尚未,就到obj.__proto__.__proto__...。這條向上查找的鏈路就被稱爲原型鏈。

最終找到Object.prototype ,此時若是仍然沒有則返回 undefined,由於再往上就是終點了。

Object.prototype.__proto__ === null
複製代碼

prototype 還有一個屬性 constructor,它的指針指回構造函數。

foo.__proto__.constructor === Foo // true
複製代碼

當new一個函數的時候,執行的是原型鏈中的構造函數.

這張圖確定不會陌生,看完前面的就能明白。

alt

按照先前的總結,Foo.prototype 是一個原型對象,它有兩個屬性:__proto__constructor,前者已經熟悉

function Foo(){}
Foo.prototype.constructor === Foo
複製代碼

這一步得出函數的顯式原型的構造函數指向 函數自身。

這個好理解,循環引用

Foo === Foo.prototype.constructor === Foo.prototype.constructor.prototype.constructor
複製代碼

原型的關係

全部構造器(函數)的__proto__都指向Function.prototype

Object.__proto__   === Function.prototype;   // true
Function.__proto__ === Function.prototype;   // true
Number.__proto__   === Function.prototype;   // true
Boolean.__proto__  === Function.prototype;   // true
String.__proto__   === Function.prototype;   // true
Object.__proto__   === Function.prototype;   // true
Array.__proto__    === Function.prototype;   // true
RegExp.__proto__   === Function.prototype;   // true
Error.__proto__    === Function.prototype;   // true
Date.__proto__     === Function.prototype;   // true
複製代碼

既然是構造函數,那他就是 Function的實例,因此 原生構造函數.__proto__ === Function.prototype

也就有了

String.__proto__ === Boolean.__proto__
RegExp.__proto__ === Error.__proto__
Date.__proto__ === Number.__proto__
複製代碼

同理,函數原型的隱式原型都是對象,因此構造函數是 ObjectFunction.prototype.__proto__ === Object.prototype,也就是

Object.__proto__.__proto__   === Object.prototype;   // true
Function.__proto__.__proto__ === Object.prototype;   // true
Number.__proto__.__proto__   === Object.prototype;   // true
Boolean.__proto__.__proto__  === Object.prototype;   // true
String.__proto__.__proto__   === Object.prototype;   // true
Object.__proto__.__proto__   === Object.prototype;   // true
Array.__proto__.__proto__    === Object.prototype;   // true
RegExp.__proto__.__proto__   === Object.prototype;   // true
Error.__proto__.__proto__    === Object.prototype;   // true
Date.__proto__.__proto__     === Object.prototype;   // true
複製代碼

可是話又說回來了,既然全部對象都是經過構造器實例化出來的,可是構造器也是函數!究竟是先有 Function 仍是先有 Object

並且爲何函數的原型對象是個函數typeof Function.prototype === 'function'

而後爲何它是函數反而沒有 prototype 特權屬性: Function.prototype.prototype === undefined

按道理不該該是 Function.__proto__ === Object.prototype ?仍是說 Function 實際上是經過 Function.prototype 構造器實例化的,Function 自己只是個實例。或者說他們的關係就是一個僞命題

fn.__proto__ = obj.prototype
obj.__proto__ = fn.prototype
複製代碼

函數對象究竟是什麼?

雖然在winter的重學前端專題中第8節對函數對象的定義是擁有瀏覽器內建call方法的對象。可是解釋這個JS版的雞生蛋蛋生雞的問題仍然有點勉強,或許等之後刨析V8源碼才能一探究竟(立下Flag)。關係圖以下:

alt

Instanceof

Instanceof 一般用來判斷一個實例是否屬於某種類型。

好比

function Foo(){}
var foo = new Foo();
console.log(foo instanceof Foo) // true
複製代碼

又亦如原型繼承的多層繼承關係。

function Bar(){}
function Foo(){}
Foo.prototype = new Bar();

var foo = new Foo();
console.log(foo instanceof Foo)//true
console.log(foo instanceof Bar)//true
複製代碼

以爲很簡單?

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

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

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

console.log(Number instanceof Number);      // false
console.log(String instanceof String);      // false
console.log(Boolean instanceof Boolean);    // false
console.log(Array instanceof Array);        // false
console.log(RegExp instanceof RegExp);      // false
console.log(Symbol instanceof Symbol);      // false
console.log(Error instanceof Error);      // false

console.log(Foo instanceof Function);       // true
console.log(Foo instanceof Foo);            // false
複製代碼

關於 instanceof 運算符的定義,厚着臉皮把人家註釋好的粘過來。連接在底部。

11.8.6 The instanceof operator
The production RelationalExpression: RelationalExpression instanceof ShiftExpression is evaluated as follows:

1. Evaluate RelationalExpression.
2. Call GetValue(Result(1)).// 調用 GetValue 方法獲得 Result(1) 的值,設爲 Result(2)
3. Evaluate ShiftExpression.
4. Call GetValue(Result(3)).// 同理,這裏設爲 Result(4)
5. If Result(4) is not an object, throw a TypeError exception.// 若是 Result(4) 不是 object,拋出異常
/* 若是 Result(4) 沒有 [[HasInstance]] 方法,拋出異常。規範中的全部 [[...]] 方法或者屬性都是內部的, 在 JavaScript 中不能直接使用。而且規範中說明,只有 Function 對象實現了 [[HasInstance]] 方法。 因此這裏能夠簡單的理解爲:若是 Result(4) 不是 Function 對象,拋出異常 */
6. If Result(4) does not have a [[HasInstance]] method, throw a TypeError exception.
// 至關於這樣調用:Result(4).[[HasInstance]](Result(2))
7. Call the [[HasInstance]] method of Result(4) with parameter Result(2).
8. Return Result(7).

// 相關的 HasInstance 方法定義
15.3.5.3 [[HasInstance]] (V)
Assume F is a Function object.// 這裏 F 就是上面的 Result(4),V 是 Result(2)
When the [[HasInstance]] method of F is called with value V,the following steps are taken:
1. If V is not an object, return false.// 若是 V 不是 object,直接返回 false
2. Call the [[Get]] method of F with property name "prototype".// 用 [[Get]] 方法取 F 的 prototype 屬性
3. Let O be Result(2).//O = F.[[Get]]("prototype")
4. If O is not an object, throw a TypeError exception.
5. Let V be the value of the [[Prototype]] property of V.//V = V.[[Prototype]]
6. If V is null, return false. // 這裏是關鍵,若是 O 和 V 引用的是同一個對象,則返回 true;不然,到 Step 8 返回 Step 5 繼續循環
7. If O and V refer to the same object or if they refer to objects
 joined to each other (section 13.1.2), return true.
8. Go to step 5.
複製代碼

看起來比較難以理解,邏輯最終經過左側 L.__proto__ 隱式原型的向上查找。

function instance_of(L, R) { // L 爲 instanceof 左側,R爲右側
    var Right = R.prototype;// 取 R 的顯示原型
    L = L.__proto__;    // 取 L 的隱式原型
    while (true) {
        if (L === null)
            return false;
        if (L === Right) // !!! 當 Right 等 L 時,返回 true
            return true;
        L = L.__proto__;
    }
}
複製代碼

結合原型的關係一節,爲何 FunctionObject 會有這麼奇怪的關係就懂了。

建立對象

建立新對象一般有三種方式,new、字面量建立、Object.create()。在平常開發用的最可能是字面量建立,可是字面量是爲了方便開發人員而設置的語法糖,故只有兩種方法。而Object.create是ES5新增的方法。

當你想複用這條原型鏈的時候,能夠用 Object.create()

function Bar (){}
Bar.prototype.getOwner = function(){ return this.name}
Bar.prototype.name = 'Floyd'

var bar = new Bar()
var fiz = Object.create(bar.__proto__)
console.log(fiz.__proto__ === bar.__proto__) // true
console.log(fiz.getOwner()) // 'Floyd'
複製代碼

在早期開發,沒法經過直接訪問原型的方式複用原型鏈。

Object.create(proto,[propertiesObject])接受兩個參數,proto 是新建立對象的原型對象,propertiesObject 可選屬性。要添加到新對象的可枚舉的屬性描述符以及相應的屬性名稱,須要注意的是,添加的屬性不會被掛載到原型鏈上去,僅僅做用於自己屬性。

好比

var opt = Object.prototype

var o = Object.create(opt,{
    foo: {
    writable:true,
    configurable:true,
    value: "hello"
  }
})

o.__proto__ === opt.prototype   // true

o.hasOwnProperty('foo') // true
複製代碼

alt

看到這其實咱們很容易實現一個簡單版本的 polyfill.

var isObject = (obj) => obj && typeof obj === 'object' && !Array.isArray(obj)

function myCreate(proto, Properties){
    function F() {}
    F.prototype = proto;
    var obj =  new F();
    if (isObject(Properties)) {
        Object.defineProperties(obj, Properties);
    }
    return obj
}


// test
var o = myCreate(opt,{
    foo: {
    writable:true,
    configurable:true,
    value: "hello"
  }
})
o.__proto__ === opt.prototype   // true
o.hasOwnProperty('foo') // true
複製代碼

在函數內部建立一個臨時性的構造函數,將傳入的對象做爲這個構造函數的原型,最後返回臨時函數的新實例。

爲何要說是簡單版本,暫時還不支持傳 null;

在Vue的源碼裏使用了大量的Object.create(null)。這麼有什麼好處?

咱們分別打印下Object.create(null)Object.create({}) 的結果。

alt

使用create建立的對象,沒有任何屬性,顯式No properties,咱們能夠制定一個很純淨的對象,全部的方法包括toStringhasOwnProperty 等方法。沒有了"包袱"表明使用for in能夠徹底避免遍歷原型鏈上的屬性,節約了性能損耗,而且也能夠當成一個乾淨的數據字典來使用。

知道了原理,咱們就好辦多了。

function myCreate(proto, Properties){
    // 處理爲proto爲null
    if(proto === null){
        var pureObj = new Object({})
        pureObj.__proto__ = null // 原型鏈必須使用null空指針,不能使用undefined
        return pureObj
    }

    function F() {}
    F.prototype = proto;
    var obj = new F();

    if (isObject(Properties)) {
        Object.defineProperties(obj, Properties);
    }
    return obj
}
複製代碼

測試用例經過

alt

繼承

在JS中,被繼承的函數稱爲超類型(父類,基類也行),繼承的函數稱爲子類型(子類,派生類)。

繼承也沒想象中那麼繞,那麼難以理解。只須要記住繼承的原則

  • 複用超類的原型對象上的私有屬性和方法
  • 屬性隔離,實例之間互不影響
  • 明確子類與超類的繼承關係

複用原型對象這個都明白,無非是屬性與方法的複用;屬性隔離是表示相互不影響,a是A的實例,修改了a就不能影響到A;明確繼承關係則是:好比a是A的實例,那我就要有辦法知道a和A的關係。搞明白這三點,相信你就有了更好的腦回路去理解它。

繼承的方式有不少種,外界對此也沒有準確的認定到底有多少種方式,褒貶不一,主流一般有7種方式:

  • 原型鏈繼承
  • 借用構造函數繼承
  • 組合模式繼承
  • 共享原型繼承
  • 原型式繼承
  • 寄生式繼承
  • 寄生式組合繼承
  • (題外)ES6中class 的繼承

原型鏈繼承

原型鏈繼承前面例子已經用到屢次。

function Foo(){
    this.name = 'seven'
}
Foo.prototype.getName = function(){ return this.name }
var foo = new Foo();
foo.getName() // 'seven'
複製代碼

經過實例化一個新的函數,子類的原型指向了父類的實例,子類就能夠調用其父類原型對象上的私有屬性和公有方法。

原型陷阱

仍是上面的例子,當咱們嘗試調用一個不存在的屬性

console.log(foo.getOwner) // undefined 原型鏈上沒有這個方法
複製代碼

原型鏈上沒有這個方法,去修改它的原型

// 建立一個新的構造函數
function Bar (){}
Bar.prototype.getOwner = function(){
    return this.name
}

// 原型繼承
Foo.prototype = new Bar() //修改原型指向 Bar.prototype
console.log(foo.getOwner) // undefined 仍是沒有
console.log(foo.constructor) // ƒ Foo(){}
複製代碼

都已經替換原型了仍是沒有更新,表示原型鏈沒有實時性,再測試下新建

var fizz = new Foo()
console.log(fizz.getOwner()) // seven
console.log(fizz.constructor) // ƒ Bar()
console.log(fizz.__proto__) // { getOwner: ƒ, constructor: ƒ}
複製代碼

這時新建的對象能夠訪問更新後的原型,由於完整替換了 prototype,構造函數又不對了,原本constructor 屬性應該指向Foo,結果卻指向了Bar(訪問了bar.__proto__.constructor),這就是原型陷阱。完整的替換了原型對象致使訪問了新對象的構造函數。

咱們只須要從新指定bar的構造函數便可。

var bar = new Bar()
bar.__proto__.constructor = Foo

Foo.prototype = bar.__proto__ //修改原型指向 = Bar.prototype

var fizz = new Foo()
console.log(fizz.getOwner()) // seven
console.log(fizz.constructor) // ƒ Foo()
複製代碼

如今就恢復正常了,此時原型鏈爲

.__proto__ .__proto__ .__proto__ .__proto__
fizz fizz.__proto__ fizz.__proto__.__proto__ fizz.__proto__.__proto__.__proto__
Bar.prototype Bar.prototype.__proto__ Bar.prototype.__proto__.__proto__
Object.prototype null

實際上最終不會訪問到Object.__proto__,例如foo.freeze === undefined

搞明白原型陷阱以後,咱們複習一下,把原型繼承搞得稍微複雜一些

function Parent(name,age){
    this.name = name;
    this.age = age;
    this.skill = ['cook','clean','run']
    this.say = function(){ console.log(this.name) }
}
Parent.prototype.setName = function() {}

function Children (name){
    this.children = name;
    this.speak = function() {
        console.log(this.childrenName)
    }
}

Children.prototype = new Parent('Seven',24)

var c1 = new Children('c1')
var c2 = new Children('c2')
複製代碼

alt

當調用 c1.skill.push('swimming') 的時候,引用類型的值被共享

alt

若是父類的私有屬性中有引用類型的屬性,那它被子類繼承的時候會做爲公有屬性,這樣子類1操做這個屬性的時候,就會影響到子類2,違反了第二條:屬性隔離,實例之間互不影響.

原型繼承的優勢

  • 簡單,易實現
  • 父類新增原型方法/原型屬性,子類都能訪問

原型繼承的缺點

  • 沒法實現多繼承
  • 引用類型的值會被實例共享
  • 子類型還沒法給超類型傳遞參數

借用構造函數(對象冒充)

經過call將超類的this指向子類內部,從而達到隔離的效果。

function Parent(name){
    this.name = name;
    this.skill = ['cook','clean','run']
}

function Children(name){
    Parent.call(this, name);
    this.age = 24
}

var c1 = new Children('c1')
var c2 = new Children('c2')
c1.skill.push('swimming'); // ok

c1.skill // ["cook", "clean", "run", "swimming"]
c2.skill // ["cook", "clean", "run"]

c1 instanceof Parent // false
c1 instanceof Children // true
複製代碼

和借用構造函數相似,原理也是使用子類的this冒充父類的this執行其構造函數,因此把它概括在一塊兒。

function Parent(name){
    this.name = name;
    this.skill = ['cook']
    this.getSkill = function(){
        return this.skill
    }
}

function Child(name, age){
    this.c = Parent;
    this.c(name,age);
    delete this.c;

    this.job = '廚師'
    this.getAge = function(){
        return this.age;
    }
}
複製代碼

引用類型的問題是解決了,可是缺點也很明顯,只能繼承超類的屬性和方法,而沒法複用其原型上的屬性和方法。並且實例c1 不是 Parent 超類的子類。並且方法都在構造函數中定義,函數沒法達到複用,違反了第一條和第三條原則。

借用構造函數的優勢

  • 解決了引用類型的值被實例共享的問題
  • 能夠向超類傳遞參數
  • 能夠實現多繼承(call若干個超類)

借用構造函數的缺點

  • 不能繼承超類原型上的屬性和方法
  • 沒法實現函數複用,因爲call有多個父類實例的副本,性能損耗。
  • 原型鏈丟失

組合模式繼承

看完了前兩種方式,有聰明的小夥伴一會兒就能想到點什麼。原型繼承將父類實例做爲子類原型實現函數複用,主要針對原型鏈繼承;借用父類構造函數繼承父類屬性並保留傳參,同時針對屬性隔離,把兩種方式結合起來去其糟粕取其精華,豈不美哉?

這種模式就是組合繼承。

function Parent(name){
    this.name = name;
    this.skill = ['cook','clean','run']
}
Parent.prototype.getName = function(){
    return this.name
}
function Children(name){
    Parent.call(this, name);
    this.age = 24
}

Children.prototype = new Parent('seven')
var c1 = new Children('c1')
var c2 = new Children('c2')

c1.hasOwnProperty('name') // true
c1.getName() // c1

c1.skill.push('swimming') // ok
c1.skill // ["cook", "clean", "run", "swimming"]

c2.skill // ["cook", "clean", "run"]
複製代碼

看起來彷佛沒有問題,可是它卻調用了2次構造函數,一次在子類構造函數內,另外一次是將子類的原型指向父類構造的實例,致使生成了2次name和skill,只不過實例屏蔽了原型上的。雖然達成了目的,卻不是咱們最想要的。

alt

這個問題將在寄生組合式繼承裏獲得解決。

共享原型繼承

這種方式下子類和父類共享一個原型。

function Parent(){}
Parent.prototype.skill = ['cook']

function Children(name, age){
    this.name = name;
    this.age = age;
}
Children.prototype = Parent.prototype

var c1 = new Children("c1", 20)
var c2 = new Children("c2", 24)

c1.skill.push("run")
c1.skill // ["cook", "run"]
複製代碼

共享原型繼承的優勢

簡單

共享原型繼承的缺點

  • 只能繼承父類原型屬性方法,不能繼承構造函數屬性方法
  • 與原型繼承同樣,存在引用類型問題

原型式繼承

這種繼承方式廣泛用於基於當前已有對象建立新對象,在ES5以前實現方法:

function object(o){
    function F(){}
    F.prototype = o
    return new F()
}

var obj = {
    name: 'seven'
}

var o1 = object(obj)
obj.name // 'seven'
複製代碼

看完這段代碼,是否是以爲和上文Object.create的 polyfill 雷同?是的,Object.create 的確ES5爲了規範原型式繼承。

寄生式繼承

寄生則是結合原型式繼承和工廠模式,將建立的邏輯進行封裝,邏輯上與原型式繼承沒有什麼區別。

function create(o){
    var f = object(o);
    f.getSkill = function () {
        return this.skill;//一樣,會共享引用
    };
    return f;
}

var obj = {
    name: 'seven',
    skill: ['cook','clean','run']
}

var c1 = create(obj);
c1.name // 'seven'
複製代碼

簡單而言,寄生式繼承就是不用實例化父類了,直接實例化一個臨時副本實現了相同的原型鏈繼承。

寄生式繼承的優勢

沒啥優勢

寄生式繼承的缺點

原型式繼承有的缺點它都有,對此我很疑惑爲何外面裝個殼就是另外一種繼承模式了。

寄生式組合繼承

顧名思義,寄生式+組合(原型繼承+借用構造函數)式繼承。總結了上面的幾種方式,相信你已經明白了怎麼去實現一個寄生組合式繼承。

關於在 Babel loose模式下 inherit 的實現方法:

"use strict";

function _inheritsLoose(subClass, superClass) {
    subClass.prototype = Object.create(superClass.prototype);
    subClass.prototype.constructor = subClass;
    subClass.__proto__ = superClass;
}
複製代碼

而在正常模式下

function _setPrototypeOf(o, p) {
    _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
        o.__proto__ = p;
        return o;
    };
    return _setPrototypeOf(o, p);
}
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);
}
複製代碼

大同小異,都是子類的原型繼承自父類的原型,申明一個用於繼承原型的 inheritPrototype 方法,經過這個方法咱們可以將子類的原型指向超類的原型,從而避免超類二次實例化。

function object(o){
    function F(){}
    F.prototype = o
    return new F()
}

function inheritPrototype(subType, suberType) {
  var prototype = object(suberType.prototype); // 建立副本
  prototype.constructor = subType; // 指定構造函數
  subType.prototype = prototype;  // 指定原型對象
}

function Parent(name){
    this.name = name;
    this.skill = ['cook', 'clean', 'run']
}
Parent.prototype.getSkill = function(){
    return this.name
}
function Children(name){
    Parent.call(this, name);
    this.age = 24
}

inheritPrototype(Children, Parent)

var c1 = new Children('c1')
var c2 = new Children('c2')
複製代碼

能夠更簡短一些,inheritPrototype 原理上就是 Object.create 的實現。

function Parent(name){
    this.name = name;
    this.skill = ['cook', 'clean', 'run']
}
Parent.prototype.getSkill = function(){
    return this.getSkill
}
function Children(name){
    Parent.call(this, name);
    this.age = 24
}

Children.prototype = Object.create(Parent.prototype)
Children.prototype.constructor = Children;

// 測試
var c1 = new Children('c1')
var c2 = new Children('c2')

console.log(c1 instanceof Children) // true
console.log(c1 instanceof Parent) // true
console.log(c1.constructor) // Children
console.log(Children.prototype.__proto__ === Parent.prototype) // true
console.log(Parent.prototype.__proto__ === Object.prototype) // true

c1.skill.push('swimming') // ok
c1.getSkill() // ["cook", "clean", "run", "swimming"]
c2.getSkill() // ["cook", "clean", "run"]
複製代碼

alt

這也是目前最完美的繼承方案,也是以爲它與ES6的class的實現方式最爲接近。

寄生式組合繼承的優勢

堪稱完美

寄生式組合繼承的缺點

代碼多

class繼承

ES6中,經過class關鍵字來定義類,子類能夠經過extends繼承父類。

class Parent{
    constructor(name){
        this.name = name;
        this.skill = ['cook', 'clean', 'run']
    }

    getSkill(){
        return this.skill
    }

    static getCurrent(){
        console.log(this)
    }
}

class Children extends Parent{
    constructor(name){
        super(name)
    }
}

var c1 = new Children('c1')
var c2 = new Children('c2')

console.log(c1 instanceof Children) // true
console.log(c1 instanceof Parent) // true
複製代碼

alt

總結

  • constructor 爲構造函數,即便未定義也會自動建立。
  • 在父類構造函數內this定義的都是實例屬性和方法,其餘方法包括 constructor,getSkill都是原型方法。
  • static 關鍵字定義的靜態方法都必須經過類名調用,其this指向調用者而並不是實例。
  • 經過 extends 能夠繼承父類的全部原型屬性及 static 類方法,子類 constructor 調用 super 父類構造函數實現實例屬性和方法的繼承。

最後咱們看下經過babel編譯後的代碼,也不是那麼難以理解了。

"use strict";
// loose模式相對比normal模式更易於理解。
function _inheritsLoose(subClass, superClass) {
    subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass;
    subClass.__proto__ = superClass;
}

var Parent = function () {
  function Parent(name) {
    this.name = name;
    this.skill = ['cook', 'clean', 'run'];
  }

  var _proto = Parent.prototype;

  _proto.getSkill = function getSkill() {
    return this.skill;
  };

  Parent.getCurrent = function getCurrent() {
    console.log(this);
  };

  return Parent;
}();

var Children = function (_Parent) {
  _inheritsLoose(Children, _Parent);

  function Children(name) {
    return _Parent.call(this, name) || this;
  }

  return Children;
}(Parent);
複製代碼

寫在後面

文中爲了節約時間參考了一些文章,若是有哪寫的不對或者有更好的方式,請留言告訴我,洗耳恭聽。

本文連接:我的博客

參考資料

JavaScript instanceof 運算符深刻剖析

相關文章
相關標籤/搜索