《JavaScript物件導向精要》之五:繼承

明易發表於2018-12-16

5.1 原型物件鏈和 Object.prototype

JavaScript 內建的繼承方法被稱為 原型物件鏈(又叫原型物件繼承)。

原型物件的屬性可經由物件例項訪問,這就是繼承的一種形式。

物件例項繼承了原型物件的屬性,而原型物件也是一個物件,它也有自己的原型物件並繼承其屬性,以此類推。這就是原型物件鏈。

所有物件(包括自義定的)都自動繼承自 Object,除非你另有指定。更確切地說,所有物件都繼承自 Object.prototype。任何以物件字面量形式定義的物件,其 [[Prototype]] 的值都被設為 Object.prototype,這意味著它繼承 Object.prototype 的屬性。

5.1.1 繼承自 Object.prototype 的方法

Object.prototype 一般有以下幾個方法:

  • hasOwnProperty() 檢測是否存在一個給定名字的自有屬性
  • propertyIsemumerable() 檢查一個自有屬性是否可列舉
  • isPrototypeOf 檢查一個物件是否是另一個物件的原型物件
  • valueOf() 返回一個物件的值表達
  • toString() 返回一個物件的字串表達

這 5 種方法經由繼承出現在所有物件中。 因為所有物件都預設繼承自 Object.prototype,所以改變它就會影響所有的物件。所以不建議。

5.2 繼承

物件繼承是最簡單的繼承型別。你唯需要做的是指定哪個物件是新物件的 [[Prototype]]。物件字面量形式會隱式指定 Object.prototype 為其 [[Protoype]]。當然我們可以用 ES5 的 Object.create() 方法顯式指定。該方法接受兩個引數,第一個是新物件的 [[Prototype]] 所指向的物件。第二個引數是可選的一個屬性描述物件,其格式與 Object.definePrototies()一樣。

var obj = {
  name: "Ljc"
};

// 等同於
var obj = Object.create(Object.prototype, {
  name: {
    value: "Ljc",
    configurable: true,
    enumberable: true,
    writable: true
  }
});
複製程式碼

下面是繼承其它物件:

var person = {
  name: "Jack",
  sayName: function(){
    console.log(this.name);
  }
}

var student = Object.create(person, {
  name:{
    value: "Ljc"
  },
  grade: {
    value: "fourth year of university",
    enumerable: true,
    configurable: true,
    writable: true
  }
});

person.sayName(); // "Jack"
student.sayName(); // "Ljc"

console.log(person.hasOwnProperty("sayName")); // true
console.log(person.isPrototypeOf(student)); // true
console.log(student.hasOwnProperty("sayName")); // false
console.log("sayName" in student); // true
複製程式碼

當訪問一個物件屬性時,JavaScript 引擎會執行一個搜尋過程。如果在物件例項存在該自有屬性,則返回,否則,根據其私有屬性 [[Protoype]] 所指向的原型物件進行搜尋,找到返回,否則繼承上述操作,知道繼承鏈末端。末端通常是 Object.prototype,其 [[Prototype]]null

當然,也可以用 Object.create() 常見一個 [[Prototype]]null 的物件。

var obj = Object.create(null);

console.log("toString" in obj); // false
複製程式碼

該物件是一個沒有原型物件鏈的物件,即是一個沒有預定義屬性的白板。

5.3 建構函式繼承

JavaScript 中的物件繼承也是建構函式繼承的基礎。

第四章提到,幾乎所有函式都有 prototype 屬性,它可被修改或替換。該 prototype 屬性被自動設定為一個新的繼承自 Object.prototype 的泛用物件,該物件(原型物件)有一個自有屬性 constructor

實際上,JavaScript 引擎為你做了下面的事情。

// 你寫成這樣
function YourConstructor(){
  // initialization
}

// JavaScript引擎在背後為你做了這些處理
YourConstructor.prototype = Object.create(Object.prototype, {
  constructor: {
    configurable: true,
    enumerable: true,
    value: YourConstructor,
    writable: true
  }
})
複製程式碼

你不需要做額外的工作,這段程式碼幫你把建構函式的 prototype 屬性設定為一個繼承自 Object.prototype 的物件。這意味著 YourConstructor 建立出來的任何物件都繼承自 Object.prototype

由於 prototype 可寫,你可以通過改變它來改變原型物件鏈。

instanceof 運算子可以用來判斷某個建構函式的 prototype 屬性是否存在另外一個要檢測物件的原型鏈上。

function Rectangle(length, width){
  this.length = length;
  this.width = width
}

Rectangle.prototype.getArea = function(){
  return this.length * this.width
}

Rectangle.prototype.toString = function(){
  return "[Rectangle " + this.length + "x" + this.width + "]";
}

// inherits from Rectangle
function Square(size){
  this.length = size;
  this.width = size;
}

Square.prototype = new Rectangle(); // 儘管是 Square.prototype是指向了 Rectangle的物件例項,即Square的例項物件也能訪問該例項的屬性(如果你提前宣告瞭該物件,且給該物件新增屬性)。
// Square.prototype = Rectangle.prototype; // 這種實現沒有上面這種好,因為Square.prototype 指向了 Rectangle.prototype,導致修改Square.prototype時,實際就是修改Rectangle.prototype。
console.log(Square.prototype.constructor); // 輸出 Rectangle 建構函式

Square.prototype.constructor = Square; // 重置回 Square 建構函式
console.log(Square.prototype.constructor); // 輸出 Square 建構函式

Square.prototype.toString = function(){
  return "[Square " + this.length + "x" + this.width + "]";
}

var rect = new Rectangle(5, 10);
var square = new Square(6);

console.log(rect.getArea()); // 50
console.log(square.getArea()); // 36

console.log(rect.toString()); // "[Rectangle 5 * 10]", 但如果是Square.prototype = Rectangle.prototype,則這裡會"[Square 5 * 10]"
console.log(square.toString()); // "[Square 6 * 6]"

console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
console.log(square instanceof Object); // true
複製程式碼

Square.prototype 並不真的需要被改成為一個 Rectangle 物件。事實上,是 Square.prototype 需要指向 Rectangle.prototype 使得繼承得以實現。這意味著可以用 Object.create() 簡化例子。

// inherits from Rectangle
function Square(size){
  this.length = size;
  this.width = size;
}

Square.prototype= Object.create(Rectangle.prototype, {
  constructor: {
    configurable: true,
    enumerable: true,
    value: Square,
    writable: true
  }
})
複製程式碼

在對原型物件新增屬性前要確保你已經改成了原型物件,否則在改寫時會丟失之前新增的方法(因為繼承是將被繼承物件賦值給需要繼承的原型物件,相當於重寫了需要繼承的原型物件)。

5.4 建構函式竊取

由於 JavaScript 中的繼承是通過原型物件鏈來實現的,因此不需要呼叫物件的父類的建構函式。如果確實需要在子類建構函式中呼叫父類建構函式,那就可以在子類的建構函式中利用 callapply 方法呼叫父類的建構函式。

// 在上面的程式碼基礎上作出修改
// inherits from Rectangle
function Square(size){
  Rectangle.call(this, size, size);

  // optional: add new properties or override existing ones here
}
複製程式碼

一般來說,需要修改 prototype 來繼承方法並用建構函式竊取來設定屬性,由於這種做法模仿了那些基於類的語言的類繼承,所以這通常被稱為偽類繼承。

5.5 訪問父類方法

其實也是通過指定 callapply 的子物件呼叫父類方法。

相關文章