建構函式、原型、原型鏈、繼承

Paykan發表於2020-05-16

JS裡一切皆物件,物件是“無序屬性的集合,其屬性值可以是資料或函式”。

事實上,所有的物件都是由函式建立的,而常見的物件字面量則只是一種語法糖:

// let user = {name: 'paykan', age: 29} ,等同於:
let user = new Object(); user.name = 'paykan'; user.age = 29;

//let list = [1, 'hi', true],等同於:
let list = new Array(); list[0] = 1; list[1] = 'hi'; list[2] = true;

物件的特性

  • 每個物件都有constructor,用來表明是誰建立了它。

  • 每個物件都有一個__proto__屬性,該屬性是一個物件,被稱為原型物件,原型物件有一個constructor屬性,指向建立物件的那個函式(obj.constructor === obj.__proto__.constructor

  • 在物件上訪問一個屬性或方法時,會先從該物件查詢,若找不到就去原型物件上找。

    所以一個簡單的字串也有若干屬性和方法,因為它們來自原型物件:

    let str = '123-456-789';
    str.split === str.__proto__.split;	//true
    
  • 每個函式只要被建立就會有一個prototype屬性,它的值就是原型物件(所以訪問原型物件有兩條途徑:函式的prototype、例項物件的__proto__)。

  • 原型物件可以被修改,而對原型物件的修改可以立即反映到例項物件上。

建立物件

工廠模式

function Person(name){ return {name: name} };	
let paykan = Person('paykan')

這裡的paykan物件其實並非Person函式建立的,因為該函式只是使用了物件字面量——呼叫了Object()。這種方式只是封裝了使用物件字面量的過程,但並非完全無用。

建構函式模式

function Person(name){ 
  this.name = name; 
  this.say = function(){
    return this.name
  }
};	
let man = new Person('paykan');
  • 這裡有三個特點:

    1. 函式內部沒有建立物件;

    2. 屬性和方法直接傳遞給了this物件;

    3. 使用new關鍵字來呼叫。任何一個函式,只要使用了new關鍵字,它就成了建構函式

  • 使用new關鍵字呼叫函式時發生了以下事情:

    1. 建立新物件

    2. 將函式的作用域賦給新物件,從而使得this指向了該物件

    3. 執行函式程式碼(為新物件新增屬性和方法)

    4. 返回新物件

這裡的man物件才算是真正由Person函式建立的了:

man.constructor;	//ƒ Person(name){ this.name = name ... }
man.__proto__.constructor;	//ƒ Person(name){ this.name = name ... }
man.__proto__.constructor === man.constructor;	//true

構造-原型組合模式

根據物件的特性,物件上沒有的屬性會在原型物件中尋找,所以可以把公共的屬性和方法給到原型物件上去。

可以通過函式的prototype或者物件的__proto__來實現:

function Person(name){ this.name = name };	
let man = new Person('paykan');
Person.prototype.nation = 'Chinese';
man.nation;	//Chinese

man.__proto__.greeting = function(){
  return 'Hi, there';
}
man.greeting();	//Hi, there

動態原型模式

這種模式把給物件新增屬性以及給原型新增屬性的動作都放到了建構函式裡,原型的屬性只在建立第一個物件例項時新增,以後就會被跳過。

function Person(name){ 
    this.name = name;
    if(!this.nation){ Person.prototype.nation = 'Chinese' };
};	

原型鏈

函式被建立後prototype指向了預設的原型物件,如果使用new呼叫該函式來生成一個物件,就會形成函式、物件、原型之間的三角關係:

此時如果讓例項物件指向另一個建構函式的例項物件,這個關係就變成了這樣:

例項物件A和例項物件B被一個__proto__屬性連結起來了,這已經是一個具有兩個節點的鏈條了,稱為原型鏈只需要修改函式的prototype的指向或者例項物件的__proto__的指向,就可以產生原型鏈。

實際上,由於原型物件B是由Object()函式建立的,而Object()函式的prototype的__proto指向的是null,所以一條原型鏈的起點是例項物件,終點是null,中間由__proto__連結。

如果在例項物件A上訪問某個屬性或方法,JS會從例項物件A開始沿著原型鏈層層查詢,直到遇見null

繼承

有了原型鏈的概念就可以開始實現繼承了,最基本的模式就是修改原型物件:

function Father(){
  this.say = function(){return this.name}
}
function Child(name){
  this.name = name;
}
Child.prototype = new Father();	
let man = new Child('jack');
man.say();	//'jack'

由於對原型的修改會立即反映到所有例項上,例項物件會互相影響,而且在呼叫Child函式時無法給Father函式傳參,所以我們需要更加實用的繼承方式。

省略分析推導過程,這裡只介紹最實用和可靠的實現繼承的方式:組合繼承,為了方便描述,引入“父類函式”和“子類函式”這兩個概念:

//父類函式
function Father(name, age){
  this.name = name;
  this.age = age;
}
//在父類函式的prototype上定義方法
Father.prototype.say = function(){ 
  return `name: ${this.name}, age: ${this.age}, intrest: ${this.intrest}`
}
//子類函式
function Child(name, age, intrest){
  this.intrest = intrest;
  Father.call(this, name, age);	//在子類物件上呼叫父類建構函式,併為之傳參
}
//設定子類函式的prototype為父類的例項
Child.prototype = new Father();	
//修改constructor屬性,使之指向子類,此非必需,但可以讓例項物件知道是誰建立了它
Child.prototype.constructor = Child;	

let man = new Child('paykan', 29, 'coding');
man.say();	//"name: paykan, age: 29, intrest: coding"

這種繼承方式有以下幾個特點:

  • 子類繼承了父類所設定的屬性,但每個例項物件都可以有自己的屬性值,不會互相影響
  • 子類共享了父類定義的方法,因為方法是在父類的prototype上的,所以不會在每個例項物件上建立一遍
  • 如果有哪個屬性是可以被所有例項物件共享的,可以設定到父類的prototype上去。

總之利用原型鏈實現可靠繼承的步驟是:

  1. 在父類函式內設定通用的屬性
  2. 在子類函式內呼叫父類函式,並設定特有的屬性
  3. 修改子類函式的prototype,以繼承父類
  4. 修改子類函式的prototype.constructor,糾正物件識別問題
  5. 使用new關鍵字呼叫子類函式,傳遞所有必需的引數

相關文章