深刻原型

原型是個玄學,學了好久的js都沒有搞清楚究竟是個啥,老是隻知其一;不知其二,今天系統的總結下(按我的理解),歡迎批評指正。
說到原型,那麼咱們搞清楚這些名詞先:構造函數、原型、實例、__proto__、new操做等。html

1. es5 構造函數(類)

構造函數、原型、實例的關係

// 構造函數
function Foo(name) {
    // 私有屬性
    var age = 1
    // 公有屬性
    this.name = name
}
// 原型上的屬性
Foo.prototype.getName = function() {
    return this.name
}
// 靜態屬性
Foo.id = 123

// 實例foo
var foo = new Foo('Tom')
foo.name // Tom
foo.age // undefined
foo.getName() // Tom
Foo.id // 123
複製代碼
  • 問:爲何foo能訪問到name、getName, 訪問不到age?
    答:es6

    1. 首先搞清楚 new 操做幹了哪些事情? 1. 開闢一個對象obj,2. obj.__proto__ = Foo.prototype, 3. 強制改變this。
    // 模擬 new 操做
    function myNew(Foo){
        var obj = {}
        // 解釋爲何能訪問 getName
        obj.__proto__ = Foo.prototype
        // 將構造函數裏的公有屬性,強制綁定到obj, 解釋問什麼能訪問 name
        Foo.call(obj)
        return obj
    }
    複製代碼
    1. 訪問規則:當 foo 訪問某屬性時,首先會去尋找foo對象自己是否存在改屬性,若存在,直接返回;若不存在,則根據__proto__的指向去尋找直到指向null。
    2. 結論:能夠訪問到name、getName, 而訪問不到age、id。id 是靜態屬性,直接經過構造函數名訪問Foo.id。

2. es5 繼承

其基本思想是利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法。設計模式

2.1 類式繼承, 也叫原型鏈繼承

實例 foo 能訪問到構造函數 Foo 裏的公有屬性和原型上的屬性, 實例 parent 能訪問到構造函數 Parent 裏的公有屬性和原型上的屬性, 將構造函數Foo的原型指向實例parent,則 foo 也能夠訪問 parent 所能訪問的內容, 從而實現繼承。bash

function Parent(){
    this.aaa = 'aaa'
    this.books = ['1', '2']
}
Parent.prototype.getAAA = function(){
    return this.aaa
}
// 這個時候會覆蓋以前的 Foo.prototype.getName, 解決:子類原型上自定義的方法後移
Foo.prototype = new Parent()

Foo.prototype.getName = function() {
    return this.name
}

var foo = new Foo('Tom')
foo.getAAA() // 'aaa'
複製代碼

原型關係:app

缺點:1. 父類Parent裏的公有引用數據類型屬性,會互相影響;2. 沒法向父類傳參。

var foo1 = new Foo('Tom')
var foo2 = new Foo('Tom1')
foo1.books.push('3')
foo2.books // ['1','2','3']
複製代碼

2.2 構造函數繼承

解決父類Parent裏的公有引用數據類型屬性,會互相影響
缺點:沒法訪問Parent.prototype上的內容。函數

function Parent(name){
    this.books = ['1','2']
}
function Foo(name) {
    Parent.call(this,name)
}
var foo1 = new Foo('Tom1')
var foo2 = new Foo('Tom2')
foo1.books.push('3')
foo2.books // ['1','2']
複製代碼

2.3 組合繼承

Parent裏的公有引用數據類型屬性互不影響,也可訪問Parent.prototype上的內容。
缺點:要調用兩次父類構造函數,而且books會存在於foo和Foo.prototype上性能

function Parent(){
    this.books = ['1','2']
}
function Foo() {
    Parent.call(this) // 第二次調用 new Foo() 時。
}
// 子類的原型指向父類的實例
Foo.prototype = new Parent() // 第一次調用。
var foo = new Foo()
複製代碼

原型關係: ui

2.4 原型式繼承

藉助第三方構造函數F實現繼承, 本質上是經過__proto__牽橋搭線。this

function inherit(o) {
    function F(){}
    F.prototype = o
    return new F()
}
obj = {
    age: 10
}
me = inhreit(obj) // me.__proto__指向F.prototype也就是o

// 等同於
const me = Object.create(obj);
// Object.create原理
const me = Object.create(obj); ===> me.__proto__ = obj
複製代碼

2.5 寄生組合式繼承

經過借用構造函數來繼承屬性(call),經過原型鏈來繼承方法。相比較組合繼承,則其基本思路是:沒必要爲了指定子類的原型而調用父類的構造函數(避免調用兩次父類構造函數),而是將父類原型的副本放到子類原型上。es5

function inherit(o) {
    function F(){}
    F.prototype = o
    return new F()
}
function inheritPrototype(Child, Parent) {
    var p = inherit(Parent.prototype) // p.__proto__ = Parent.prototype
    p.constructor = Child
    Child.prototype = p
}

function Parent(age){
    this.age = age
}
function Child(name, age) {
    this.name = name
    Parent.call(this, age)
}

inheritPrototype(Child, Parent)
// 爲避免被覆蓋,定義子類原型上的方法,要寫在 inheritPrototype 以後
Child.prototype.getAge = function() {
    return this.age
}
var child = new Child('Tom', 12)
child.getAge() // 12
複製代碼

以上全部繼承方式,原型上的引用數據類型被更改時會互相影響。 使用約定 --- 通常原型上只用來存方法,而不存數據,來規避。

3. es6 類 class

3.1 類的全部方法都定義在類的prototype屬性上面。

// es6
class Point {
    // 靜態屬性
    static id = 1
    
    // 私有屬性,約定用_加以區分,但實例仍然能夠訪問
    _count = 1

    // 原型上的方法
    constructor() {
        // 公有屬性
        this.x = 1
    }
    
    toString() {
    // ...
    }
    
    toValue() {
    // ...
    }
}
const point = new Point()
point.hasOwnProperty('x') // true
point.hasOwnProperty('_count') // true

// class裏的方法 等同於
Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

// es5 的方法
function Point() {
    this.toValue = function() {}
}
// toValue方法在實例 point 上,而不是 Point.prototype 上。
var point = new Point()
複製代碼

3.2 父類的靜態方法,能夠被子類繼承。

相比於es5, es6多作了這步操做Child.__proto__ = Parent(子類的__proto__指向父類)。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
}

Bar.classMethod() // 'hello'
Bar.__proto__ === Foo // true
複製代碼

4. es6 繼承

4.1 super

  • 做爲方法
    1. 表明父類的構造函數,至關於A.prototype.constructor.call(this)(A是父類),
    2. 子類必須調用super方法,而且是在constructor方法中,不然新建實例時會報錯。
  • 做爲對象
    1. 指向父類的原型對象,父類實例上的方法或屬性,是沒法經過super調用的。
    2. 在子類普通方法中經過super調用父類的方法時,父類方法內部的this指向當前的子類實例。
  • 用在靜態方法之中 && 做爲對象
    1. 這時super將指向父類,而不是父類的原型對象。
    2. 在子類的靜態方法中經過super調用父類的方法時,方法內部的this指向當前的子類,而不是子類的實例。

ES5 的繼承,實質是先創造子類的實例對象this(肯定this指向),而後再將父類的方法添加到this上面(Parent.apply(this))。ES6 的繼承機制徹底不一樣,實質是先將父類實例對象的屬性和方法,加到this上面(因此必須先調用super方法),而後再用子類的構造函數修改this(肯定this指向)。

4.2 類的 prototype 屬性和 proto 屬性

  • 子類的__proto__屬性,老是指向父類。(能夠解釋 static 屬性能被繼承)
  • 子類prototype屬性的__proto__屬性,表示方法的繼承,老是指向父類的prototype屬性。
class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true


// 等同於
class A {
}
class B {
}
// B 的實例繼承 A 的實例
Object.setPrototypeOf(B.prototype, A.prototype);
// 等同於
B.prototype = Object.create(A.prototype)

// B 繼承 A 的靜態屬性
Object.setPrototypeOf(B, A);
// 等同於
B = Object.create(A)

const b = new B();

// ------------------------------------------------------
// setPrototypeOf 原理
Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}
複製代碼

5. 總結

  • js 能訪問到屬性、方法的是經過 __proto__ 一層一層的向上查找,直到null。
  • 類實例的__proto__指向類的原型(foo.__proto__ = Foo.prototype), 由於new操做的原理。
  • js 繼承的核心思路是子類原型上的__proto__指向父類原型(訪問方法) && call強制改變this來實現互不影響(訪問屬性)。

參考文獻

js設計模式-張容銘
js高程-第三版
es6入門-阮一峯

相關文章
相關標籤/搜索