原型、原型鏈、new做了什麼、繼承

bingqise5193發表於2020-09-25

原型和原型鏈

比我們通過一個建構函式new了一個新物件,建構函式的原型prototype指向一個物件,所有通過該建構函式new的新物件可以共享它所包含的屬性和方法。

建構函式的原型prototype是一個物件,那麼它也可以有自己的建構函式原型prototype,通過這樣,形成一個原型鏈。原型鏈最終都可以上溯到Object.prototype。Object.prototype的__proto__是null。null沒有任何屬性和方法,也沒有自己的原型。因此,原型鏈的盡頭就是null。

來看一個例子:

function Test() {}
var test = new Test();
test.__proto__===Test.prototype
Test.prototype.__proto__===Object.protype
Object.protype.__proto__===null
test.__proto__.__proto__.__proto__ === null
Test.__proto__===Function.prototype

獲取物件原型API: Object.getPrototypeOf(a) 即a.proto (tip: 方法在Object上而不是Object.prototype上)

為了更清晰地理解,看以下練習:

		var obj= {}
		obj.__proto__ === Object.prototype // 為 true

		var fn = function(){}
		fn.__proto__ === Function.prototype  // 為 true
		fn.__proto__.__proto__ === Object.prototype // 為 true

		var array = []
		array.__proto__ === Array.prototype // 為 true
		array.__proto__.__proto__ === Object.prototype // 為 true
		Array.__proto__ === Function.prototype // 為 true, Array的本質為一個建構函式

實際上,看a.__proto__是什麼,就看a的本質是什麼:
1.new出來的物件,則指向其建構函式的prototype;
2.建構函式,則Function.prototype

new

new的過程:
初始化一個新物件
該物件的__proto__屬性指向建構函式的原型prototype
將建構函式的this指向新物件,並執行函式
將新物件返回
[如果建構函式有返回一個物件(null除外),則將建構函式內的物件返回]

手寫new:

function myNew(func, ...params) {
    const obj = {};
    obj.__proto__ = func.prototype
    const temp = func.apply(obj, params)
    if(temp && typeof temp === 'object') {
        return temp
    }else{
        return obj
    }
}

繼承

既然通過原型鏈例項能訪問到上層的一些方法和屬性,那麼,自然而然繼承可以由他來實現。

##原型鏈繼承
最容易想到的是,讓建構函式的原型指向另一個你想要繼承的建構函式的例項,如下:

// 原型鏈繼承
    // 缺點1:任何一個例項改變原型的屬性,所有例項的原型屬性都會變,因為指向的是同一個
    // 缺點2:沒有辦法在不影響所有物件例項的情況下,給超型別的建構函式傳遞引數
    function Father(firstName) {
        this.firstName = firstName
        //屬性為引用型別會被共享
        this.ver = ['1.0', '2.0']
    }
    function Son(lastName) {
        this.lastName = lastName
    }
    Son.prototype = new Father('lee')
    const a = new Son('mei')
    const b = new Son('lei')
    a.ver[0] = '3.0'
    console.log(a)
    console.log(b)

<script>
      // 借用建構函式,解決了上面的兩個問題
      // 缺點1:方法都在建構函式中定義,函式無法複用
      // 缺點2:在超型別的原型中定義的方法,對子型別而言也是不可見的
      function Father(firstName) {
        this.firstName = firstName
        //屬性為引用型別會被共享
        this.ver = ['1.0', '2.0']
      }
      function Son(firstName, lastName) {
        Father.call(this, firstName)
        this.lastName = lastName
      }
      const a = new Son('lee', 'mei')
      const b = new Son('han', 'lei')
      a.ver[0] = '3.0'
      console.log(a)
      console.log(b)
    </script>

    <script>
      // 組合原型鏈和建構函式,解決了上面的四個問題
      // 缺點1:會執行建構函式兩遍,父類的屬性會建立兩遍
      function Father(firstName) {
        this.firstName = firstName
        //屬性為引用型別會被共享
        this.ver = ['1.0', '2.0']
      }
      function Son(firstName, lastName) {
        //第二次呼叫
        Father.call(this, firstName)
        this.lastName = lastName
      }
      //第一次呼叫
      Son.prototype = new Father('tan')
      const a = new Son('lee', 'mei')
      const b = new Son('han', 'lei')
      a.ver[0] = '3.0'
      console.log(a)
      console.log(b)
    </script>

    <script>
      // 原型式繼承
      // 如果目的是讓一個物件與另一個物件保持類似的情況.
      // 在傳入一個引數的情況下,Object.create()與 object()方法的行為相同。
      // Object.create(null)建立一個空物件,不繼承object的任何方法
      function object(o) {
        function F() {}
        F.prototype = o
        return new F()
      }
      let person = { 
        name: "Nicholas", 
        friends: ["Shelby", "Court", "Van"] 
      }
      const a = object(person)
      console.log(a)
    </script>

    <script>
      // 終極版,利用原型式繼承,讓子建構函式原型能夠呼叫父建構函式原型的方法
      function Father(firstName) {
        this.firstName = firstName
        this.ver = ['1.0', '2.0']
      }
      function Son(firstName, lastName) {
        Father.call(this, firstName)
        this.lastName = lastName
      }
      Father.prototype.say = function () {
        console.log(this.firstName)
      }
      // 不直接指向父類的原型,因為這樣兩者指向了同一個物件,實現了繼承,但改變任何一個,另一個也會受影響
      Son.prototype = Object.create(Father.prototype)
      const a = new Son('lee', 'mei')
      const b = new Son('han', 'lei')
      a.ver[0] = '3.0'
      console.log(a)
      console.log(b)
    </script>

通過上面的流程也可以很好地去理解ES6中的extends。

es6 extends super

class Father {
      constructor(firstName) {
        this.firstName = firstName
        this.ver = ['1.0', '2.0']
      }
      update(firstName) {
        this.firstName = firstName
      }
    }
    class Son extends Father{
      constructor(firstName, lastName) {
        // 繼承父類屬性,必須有這句之後才能呼叫父類方法
        super(firstName)
        this.lastName = lastName
      }
      update(firstName, lastName) {
        // 呼叫父類方法
        super.update(firstName) 
				this.lastName = lastName
      }
    }
    // 通過觀察xiao的結構可以看出,原理大概是終極版
    let xiao = new Son('lee', 'mei')
    ```

相關文章