《JavaScript高級程序設計》(第3版)讀書筆記 第6章 面向對象的程序設計

  • 面向對象(Object-Oriented, OO)的語言有一個標誌,它們都有類的概念,而經過類能夠建立任意多個具備相同屬性和方法的對象。
  • ECMAScript中沒有類的概念,所以它的對象也與基類的語言中的對象有所不一樣
  • ECMA-262把對象定義爲:「無序屬性的集合,其屬性能夠包含基本值、對象或者函數。」 咱們能夠把ECMAScript的對象想象成散列表:無非就是一組名值對,其中值能夠是數據或者函數
  • 每一個對象都是基於一個引用類型建立的,這個引用類型能夠是第5章討論的原生類型,也能夠是開發者定義的類型

理解對象

// 建立對象,賦給屬性
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Sofware Engineer";

person.sayName = function() {
  alert(this.name);
}

// 字面量方式建立對象
var person = {
  name: "Nicholas",
  age: 29,
  job: "Sofware Engineer",
  sayName: function() {
    alert(this.name);
  }
}

屬性類型

  • ECMAScript中有兩種屬性:數據屬性和訪問器屬性

數據屬性

  • 數據屬性包含一個數值的位置。在這個位置能夠讀取和寫入值。數據屬性有4個描述其行爲的特性javascript

    • [[Configurable]] 表示可否經過delete刪除屬性從而從新定義屬性,可否修改屬性的特性,或者可否把屬性修改成訪問器屬性。像前述例子中那樣直接在對象定義的屬性,它們這個特性默認值爲true
    • [[Enumerable]] 表示可否經過for-in循環返回屬性。前述例子這個特性默認爲true
    • [[Writable]] 表示可否修改屬性的值。前述例子這個特性默認爲true
    • [[Value]] 包含這個屬性的數據值。讀取屬性值得實惠,從這個位置讀;寫入屬性值得實惠,把新值保存在這個位置。這個特性的默認值爲undefined
  • 要修改屬性默認的特性,必須使用ECMAScript5的Object.defineProperty()方法。java

    • 主要接收三個參數:屬性所在的對象,屬性的名字和一個描述符對象。描述符(descriptor)對象的屬性必須是:configurable,enumerable,writalbe和value。
    • 設置其中的一或多個值,能夠修改對應的特性值
    var person = {};
    // 建立了一個name屬性,它的值"Nicholas"是隻讀的。
    Object.defineProperty(person, "name", {
      writable: false,
      value: "Nicholas"
    });
    
    console.log(person.name);                // "Nicholas"
    // 在非嚴格模式下,賦值操做會被忽略
    // 而在嚴格模式下,會致使錯誤
    person.name = "Greg";
    console.log(person.name);                // "Nicholas"
    • 把configurable設置爲false,表示不能從對象中刪除屬性。若是對這個屬性調用delete,則在非嚴格模式下什麼也不會發生,而在嚴格模式下會致使錯誤
    • 一旦把屬性定義爲不可配置的,就不能再把它變回可配置了。此時再調用Object.defineProperty()方法修改除writable以外的特性,都會致使錯誤
    var person = {};
    Object.defineProperty(person, "name", {
      configurable: false,
      value: "Nicholas"
    });
    
    // 拋出錯誤
    Object.defineProperty(person, "name", {
      configurable: true,
      value: "Nicholas"
    })
    • 能夠屢次調用Object.defineProperty()方法修改同一個屬性,但在把configurable特性設置爲false以後就會有限制了。
    • 調用Object.defineProperty()若是不指定,configurable,enumerable,writalbe 特性的默認值都是false。多數狀況下,沒有必要利用Object.defineProperty()方法提供的高級功能。

訪問器屬性

  • 訪問器屬性不包含數據值,它們包含一對getter和setter函數(不是必須的)
  • 在讀取訪問器屬性時,會調用getter函數,這個函數負責返回有效的值
  • 寫入訪問器屬性時,會調用setter函數並傳入新值,這個函數負責決定如何處理數據
  • 訪問器屬性有以下4個特性:數組

    • [[Configurable]] 表示可否經過delete刪除屬性從而從新定義屬性,可否修改屬性的特性,或者可否把屬性修改成數據屬性。對於直接在對象上定義的屬性,這個特性的默認值爲true
    • [[Enumerable]] 表示可否經過for-in循環返回屬性。對於直接在對象上定義的屬性,這個特性的默認值爲true
    • [[Get]] 在讀取屬性時調用的函數。默認值爲undefined
    • [[Set]] 在寫入屬性時調用的函數。默認值爲undefined
  • 訪問器屬性不能直接定義,必須使用Object.defineProperty()來定義
var book = {
  _year: 2004,
  edition: 1
};

Object.defineProperty(book, "year", {
  get: function () {
    return this._year;
  },
  set: function (newValue) {
    if (newValue > 2004) {
      this._year = newValue;
      this.edition += newValue - 2004;
    }
  }
});

// 這是使用訪問器屬性的常見方式,即設置一個屬性的值會致使其餘屬性發生變化。
book.year = 2005;
console.log(book.edition);   // 2
  • 不必定非要同時制定getter和setter。只指定getter意味着屬性時不能寫,嘗試寫入屬性會被忽略。在嚴格模式下嘗試寫入只指定了getter函數的屬性會拋出錯誤。
  • 只指定setter意味着屬性時也不能讀,不然在非嚴格模式下回返回undefined,而在嚴格模式下會拋出錯誤
  • 支持ECMAScript 5這個方法的瀏覽器有IE9+(IE8只是部分實現),Firefox 4+, Safari 5+, Opera 12+, Chrome
  • 在不支持Object.defineProperty()方法的瀏覽器中不能修改[[Configurable]] 和 [[Enumerable]]

定義多個屬性

  • Object.defineProperties() 定義多個屬性的方法
var book = {}
Object.defineProperties(book, {
  _year: {
    writable: true,
    value: 2004
  },

  edition: {
    writable: true,
    value: 1
  },

  year: {
    get: function() {
      return this._year;
    },

    set: function(newValue) {
      if (newValue > 2004) {
        this._year = newValue;
        this.editio += newValue - 2004;
      }
    }
  }
})

讀取屬性的特性

  • Ojbect.getOwnPropertyDescriptor() 方法,能夠取得給定屬性的描述符,這個方法接收兩個參數:屬性所在的對象和要讀取屬性描述符的屬性名稱。返回值時一個對象,若是是訪問器屬性,這個對象的屬性有configurable, enumerable, get, set;若是是數據屬性,這個對象的屬性有configurable, enumerable, writable, value
var book = {};

Object.defineProperties(book, {
  _year: {
    writable: true,
    value: 2004
  },

  edition: {
    writable: true,
    value: 1
  },

  year: {
    get: function() {
      return this._year;
    },

    set: function(newValue) {
      if (newValue > 2004) {
        this._year = newValue;
        this.editio += newValue - 2004;
      }
    }
  }
});

var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
console.log(descriptor.value);                                    // 2004
console.log(descriptor.configurable);                             // false
console.log(typeof descriptor.get);                               // undefined

var descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value);                                    // undefined
console.log(descriptor.enumerable);                               // false
console.log(typeof descriptor.get);                               // "function"

建立對象

  • 雖然Object構造函數或對象字面量均可以用來建立單個對象,但這些方式有個明顯的缺點:使用同一個接口建立不少對象,會產生大量的重複代碼。爲解決這個問題,人們開始使用工廠模式的一種變體

工廠模式

  • 考慮到ECMAScript中沒法建立類,開發者就發明了一種函數,用函數來封裝以特定接口建立對象的細節
function createPerson(name, age, job) {
  var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function () {
    alert(this.name);
  };

  return o;
}

var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
  • 雖然工廠模式解決了建立多個類似對象的問題,但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)。

構造函數模式

  • 可建立自定義的構造函數,從而定義自定義對象類型的屬性和方法
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function () {
    alert(this.name);
  };
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
  • 構造函數模式對比工廠模式存在如下不一樣之處瀏覽器

    • 沒有顯式的創造對象
    • 直接將屬性和方法賦給了this對象
    • 沒有return 語句
    • 函數名首字母大寫,按照慣例,構造函數都應該以一個大寫字母開頭,而非構造函數小寫
  • person1 和 person2 分別保存着Person的一個不一樣的實例。二者都有一個constructor(構造函數)屬性,改屬性指向Person
console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true
  • 對象的constructor屬性最初是用來標識對象類型的。可是,檢測對象類型,仍是instanceof操做符要更可靠。
  • 這個例子中建立的全部對象既是Object的實例,同時也是Person的實例
console.log(person1 instanceof Object);   // true
console.log(person2 instanceof Object);   // true
console.log(person1 instanceof Person);   // true
console.log(person2 instanceof Person);   // true
  • 建立自定義的構造函數意味着未來能夠將它的實例標識爲一種特定的類型,這正是構造韓式模式賽過工廠模式的地方。這個例子中,person1, person2 之因此同時是Object的實例,是由於全部對象均繼承自Object
  • 以這種方式定義的構造是定義在Global對象(瀏覽器中是window)中的。第8章將詳細討論瀏覽器對象模型(BOM)

將構造函數看成函數

  • 構造函數與其餘函數的惟一區別就在於調用方式不一樣。
  • 構造函數也是函數,不存在定義構造函數的特殊語法
  • 任何函數只要經過new操做符來調用,那它就能夠做爲構造函數
  • 而任何函數不經過new操做符來調用,那它跟普通函數沒有區別
// 做爲構造函數使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName();   // "Nicholas"

// 做爲普通函數調用,此時this指向window對象
Person("Greg", 27, "Doctor");  // 添加到window對象上
window.sayName();       // "Greg"

// 在另外一個對象的做用域中調用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); // "Kristen"

構造函數的問題

  • 構造函數模式的並不是沒有缺點。主要問題就是每一個方法都要在每一個實例上從新建立一遍。在前述例子中person1和person2都有一個名爲sayName()的方法,但那兩個方法不是同一個Function的實例。不要忘了ECMAScript中函數是對象,所以每定義一個函數,也就是實例化了一個對象。從邏輯角度講,此時的構造函數也能夠這樣定義
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  // 與聲明函數再路基上是等價的
  this.sayName = new Function("alert(this.name)");
}

console.log(person1.sayName == person2.sayName);  // false
  • 有this對象在,沒有必要在執行代碼前就把函數綁定到特定對象上面,經過把函數定義轉移到函數外部來簡化
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = sayName
}

function sayName () {
  alert(this.name);
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
  • 咱們把sayName函數的定義轉移到了函數外部。而在構造函數內部,咱們將sayName屬性設置成等於全局的sayName函數。如此一來,因爲sayName包含的是一個指向函數的指針,所以person1和person2對象就共享了全局做用域中定義的同一個sayName() 函數。
  • 新的問題出現了,在全局做用域中定義的函數實際上只能被某個對象調用,這讓全局做用域有點名存實亡。而更讓人沒法接受的是:若是對象須要定義不少方法,那麼就要定義不少個全局函數,因而咱們這個自定義的引用類型就絲毫沒有封裝性可言了。
  • 好在能夠經過原型模式來解決

原型模式

  • 咱們建立的每一個函數都有一個prototype(原型),這個屬性是一個指針,指向一個對象,而這個對象的用途是包含能夠由特定類型的全部實例共享的屬性和方法。
  • prototype就是經過調用構造函數而建立的那個對象實例的原型對象,使用原型對象的好處是可讓全部對象實例共享它所包含的屬性和方法。換言之,沒必要再構造函數中定義對象實例的信息,而是能夠將這些信息直接添加到原型對象中
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
  alert(this.name);
};

var person1 = new Person();
person1.sayName();    // "Nicholas"

var person2 = new Person();
person2.sayName();    // "Nicholas"

console.log(person1.sayName == person2.sayName);    // true
  • 在此,咱們將sayName()方法和全部屬性直接添加到了Person的prototype屬性中,構造函數變成了空函數。即便如此,也仍然能夠經過調用構造函數來建立新對象,並且新對象還會具備相同的屬性和方法。
  • 但與構造函數模式不一樣的是,新對象的這些屬性和方法時由全部實例共享的。換言之,person1和person2訪問的都是同一組屬性和方法
  • 要理解原型模式的工做原理,必須先理解ECMAScript中原型對象的性質

理解原型對象

  • 只要建立了一個函數,就會根據一組特定的規則爲該函數建立一個prototype屬性,這個屬性指向函數的原型對象。
  • 默認狀況下,全部原型對象都會自動得到一個constructor(構造函數)屬性,這個屬性是一個指向prototype屬性所在函數的指針。例如上述例子,Person.prototype.constructor 指向Person。
  • 咱們能夠繼續爲原型對象添加其餘屬性和方法
  • 建立了自定義構造函數後,其原型對象默認只會獲得constructor屬性,至於其餘方法,則都是從Object繼承而來。當調用構造函數建立一個新的實例後,該實例的內部將包含一個指針(內部屬性),指向構造函數的原型對象。ECMAScript 5 中管這個叫[[Prototype]]。
  • 雖然腳本中沒有標準的方式訪問[[Prototype]],但Firefox, Safari, Chrome 在每一個對象都支持一個屬性__proto__;這個屬性對腳本則是徹底不可見的。
  • 這個連接存在於實例與構造函數的原型對象之間,而不是存在於實例與構造函數之間

    charptor06_6_1.jpg

  • 圖6-1(148頁)展現了Person構造函數、Person的原型屬性以及Person現有的兩個實例之間的關係。在此,Person.prototype指向了原型對象,而Person.prototype.constructor又指回了Person。原型對象中除了包含constructor屬性以外,還包括後來添加的其餘屬性。Person的每一個實例——person1和person2都包含一個內部屬性,該屬性僅僅指向了Person.prototype;換言之,person1和person2 與構造函數沒有直接的關係。此外,要格外注意的是,雖然這兩個實例都不包含屬性和方法,但咱們卻能夠調用person1.sayName()。這是經過查找對象屬性的過程來實現的。
  • 雖然在全部實現中都沒法訪問到[[Prototype]],但能夠經過isPrototypeOf()方法來肯定對象之間是否存在這種關係。
console.log(Person.prototype.isPrototypeOf(person1));     // true
console.log(Person.prototype.isPrototypeOf(person2));     // true

console.log(Person.isPrototypeOf(person1));     // false
console.log(Person.isPrototypeOf(person2));     // false
  • ECMAScript 5 新增了一個方法,Object.getPrototypeOf(), 在全部支持的實現中,這個方法返回[[Prototype]]的值。支持的瀏覽器 IE9+, Safari 5+, Opera 12+, Chrome
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true
console.log(Object.getPrototypeOf(person1).name);                // "Nicholas"
  • 每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,目標是具備給定名字的屬性。搜索從對象實例自己開始,若是沒有找到,則繼續搜索指針指向的的原型對象,在原型對象中查找具備給定名字的屬性。也就是說,在咱們調用person1.sayName()的時候,會先執行兩次搜索,在person1中沒有找到sayName屬性,繼續在person1的原型中搜索,在Person.prototype中找到了sayName屬性,而後讀取那個保存在原型對象中的函數。
  • 原型最初只包含constructor屬性,而該屬性也是共享的,所以能夠經過對象實例訪問
  • 雖然能夠經過對象實例訪問保存在原型中的值,但卻不能經過對象實例重寫原型中的值。若是咱們在實例中添加了一個屬性,而該屬性與實例原型中的一個屬性同名,那咱們就在實例中建立該屬性,該屬性將會屏蔽原型中的那個屬性
  • hasOwnProperty()方法(繼承於Object)能夠檢測一個屬性是存在於實例中,仍是存在於原型中。
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
  alert(this.name);
};

var person1 = new Person();
var person2 = new Person();

console.log(person1.hasOwnProperty("name"));       // false

person1.name = "Greg";
console.log(person1.name);                         // "Greg" ——來自實例
console.log(person1.hasOwnProperty("name"));       // true

console.log(person2.name);                         // "Nicholas" ——來自原型
console.log(person2.hasOwnProperty("name"));       // false

// 使用delete操做符刪除實例中的屬性
delete person1.name;
console.log(person1.name);                         // "Nicholas" ——來自實例
console.log(person1.hasOwnProperty("name"));       // false
  • ECMAScript 5 的 Object.getOwnPropertyDescriptor() 方法只能用於實例屬性,要取得原型屬性的描述符,必須直接在原型對象上調用Object.getOwnPropertyDescriptor() 方法

原型與in操做符

  • 兩種方式使用in操做符,單獨使用和在for-in循環中使用。
  • 單獨使用in操做符,會在經過對象可以訪問給定屬性時返回true,不管該屬性存在於實例中仍是原型中。
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
  alert(this.name);
};

var person1 = new Person();
var person2 = new Person();

console.log(person1.hasOwnProperty("name"));       // false
console.log("name" in person1);                    // true

person1.name = "Greg";
console.log("name" in person1);                    // true

console.log(person2.name);                         // "Nicholas" ——來自原型
console.log("name" in person2);                    // true

// 使用delete操做符刪除實例中的屬性
delete person1.name;
console.log("name" in person1);                    // true
  • 同時使用hasOwnProperty() 方法和in操做符,就能夠肯定該屬性存在於實例中仍是存在運行中
function hasPrototypeProperty(object, name) {
  return !object.hasOwnProperty(name) && (name in object);
}
  • 因爲in操做符只要經過可以訪問到屬性就返回true, hasOwnProperty() 只在屬性存在於實例中才返回true,所以只要in操做符返回true,而hasOwnproperty()返回false,就能夠肯定屬性是原型中的屬性。
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
  alert(this.name);
};

var person = new Person();
console.log(hasPrototypeProperty(person, "name"));      // true

person.name = "Greg";
console.log(hasPrototypeProperty(person, "name"));      // false
  • 使用for-in循環,返回的是全部可以經過對象訪問的、可枚舉的(enumerad)屬性,其中既包括存在於實例中的屬性,也包括存在於原型中的屬性。屏蔽了原型中不可枚舉的屬性(即將[[Enumerable]]標記爲false的屬性)實例屬性也會在for-in循環返回,由於根據規定,全部開發人員定的屬性,都是可枚舉的——只有在IE8及更早版本中例外。
  • IE早期版本的實現中存在一個bug,即屏蔽不可枚舉屬性的實例屬性不會出如今for-in循環中。
var o = {
  toString: function() {
    return  "My Object";
  }
};

for (var prop in o) {
  if (prop ==  "toString") {
    console.log("Found toString");    // 在IE中不會顯示
  }
}
  • 在IE中,因爲其實現認爲原型的toString()方法被打上了值爲false的[[Enumerable]]標記,所以應該跳過該屬性,結果就不會打印。該bug會影響默認不可枚舉的全部屬性和方法,包括:hasOwnProperty(), propertyIsEnumerable(), toLocaleString(), valueOf()
  • ECMAScript 5 也將constructor和prototype屬性的[Enumerable]]特性設置爲false,但並非全部瀏覽器都照此實現。
  • 要取得對象上全部可枚舉的實例屬性,可使用ECMAScript5的Object.keys()方法。這個方法接收一個對象做爲參數,返回一個包含全部可枚舉屬性的字符串數組
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "SoftWare Engineer";
Person.prototype.sayName = function() {
  console.log(this.name);
};

var keys = object.keys(Person.prototype);
console.log(keys);                            // "name,age,job,sayName"

var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1kyes = Object.keys(p1);
console.log(p1kyes);                         // "name,age"
  • 若是你想要獲得全部實例屬性,不管它是否可枚舉,均可以使用Object.getOwnPropertyNames()方法。
var keys = Object.getOwnPropertyNames(Person.prototype);     // "constructor,name,age,job,sayName"
  • 注意結果中包含了不可枚舉的constructor屬性。Object.keys() 和 Object.getOwnPropertyNames()方法均可以用來替代for-in循環。支持這兩個方法的瀏覽器有IE9+, Firefox4+, Safari5+, Opera12+, Chrome

更簡單的原型語法

  • 前面例子中每添加一個屬性和方法就要敲一遍 Person.prototype。 爲減小沒必要要的輸入,也爲了從視覺上更好的封裝原型的功能,更常見的作法是用一個包含全部屬性和方法的對象字面量來重寫整個原型對象
function Person() {}

Person.prototype = {
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  sayName: function () {
    console.log(this.name);
  }
};
  • 咱們將Person.prototype設置爲等於一個以對象字面量形式建立的新對象。最終結果相同,但有一個例外:constructor屬性再也不指向Person了。由於每建立一個函數,就會同時建立它的prototype對象,整個對象也會自動得到constructor屬性。而咱們在這裏使用的語法,本質上徹底重寫了默認的prototype對象,所以constructor屬性也就變成了新對象的constructor屬性(指向Object構造函數),再也不指向Person函數。此時儘管instanceof操做符還能返回正確的結果,但經過constructor已經沒法肯定對象的類型
var friend = new Person();

console.log(friend instanceof Object);                     // true
console.log(friend instanceof Person);                     // true
console.log(friend.constructor == Person);                 // false
console.log(friend.constructor == Object);                 // Object
  • 若是constructor的值真的很重要,能夠像下面這樣特地將它設置回適當的值。
function Person() {}

Person.prototype = {
  constructor: Person,                          // 讓 prototype的constructor從新指向Person
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  sayName: function () {
    console.log(this.name);
  }
};
  • 這種方式重設constructor會致使它的[[Enumerable]]特性被設置爲true。默認狀況下,原生的constructor屬性是不可枚舉的,所以若是你使用兼容ECMAScript 5 的 JavaScript 引擎,能夠試一試 Object.defineProperty()
function Person() {}

Person.prototype = {
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  sayName: function () {
    console.log(this.name);
  }
};

// 重設構造函數,只適用於ECMASCript 5 兼容的瀏覽器
Object.defineProperty(Person.prototype, "constructor", {
  enumerable: false,
  value: Person
});

原型的動態性

  • 因爲在原型中查找值的過程是一次搜索,所以咱們對原型對象所作的任何修改都可以當即從實例上反映出來——即便是先建立了實例後修改原型也照樣如此
var friend = new Person();

Person.prototype.sayHi = function() {
  console.log("Hi");
};

friend.sayHi();               // "Hi"
  • 儘管能夠隨時爲原型添加屬性和方法,而且修改可以當即在全部對象實例中反映出來,但若是是重寫整個原型對象,那麼狀況就不同了。調用構造函數時會爲實例添加一個指向最初原型的[[Protoype]]指針,而吧原型修改成另一個對象,就等於切斷了構造函數與最初原型之間的聯繫。請記住:實例中的指針僅指向原型,而不是指向構造函數。
function Person() {}

var friend = new Person();

// 重寫整個原型對象,就等於切斷了構造函數與最初原型之間的聯繫
Person.prototype = {
  constructor: Person,
  age: 29,
  job: "Software Engineer",
  sayName: function () {
    console.log(this.name);
  }
};

friend.sayName();  // error

原生對象的原型

  • 原型模式的重要性不只體如今建立自定義類型方面,就連全部原生的引用類型,都是採用這種模式建立的。全部原生引用類型(Object, Array, String, 等)都在其構造函數的原型上定義了方法。例如,在Array.prototype 中能夠找到sort()方法,而在String.prototype中能夠找到substring()方法修改同一個屬性
console.log(typeof Array.prototype.sort);          // "function"
console.log(typeof String.prototype.substring);    // "function"
  • 經過原生對象的原型,不只能夠取得全部默認方法的引用,並且也能夠定義新方法。能夠像修改自定義對象的原型同樣修改原生對象的原型,所以能夠隨時添加方法。下面的代碼就給基本包裝類型String添加了一個名爲startsWith()的方法
String.prototype.startsWith = function (text) {
  return this.indexOf(text) == 0;
};

var msg = "Hello world!";
console.log(msg.startsWith("Hello"));             // true
  • 儘管看起來很方便,但不推薦在產品化的程序修改原生對象的原型。若是某個實現中缺乏某個方法,就在原生對象的原型中添加這個方法,那麼當另外一個支持該方法的實現中運行代碼時,就可能會致使命名衝突。並且這樣作也可能會意外的重寫原生方法。

原型對象的問題

  • 原型模式也不是沒有缺點。安全

    • 首先,它省略了爲構造函數傳遞初始化參數這一環節,結果全部實例在默認狀況下都將取得相同的屬性值。雖然這回在某種程度上帶來一些不方便,但還不是原型的最大問題
    • 原型模式最大的問題是由其共享的本性鎖致使的。原型中全部屬性是被不少實例共享的,這種共享對於函數很是適合。對於那些包含基本值的屬性倒也說得過去,畢竟經過在實例上添加一個同名屬性,能夠隱藏原型中的對應屬性。
    • 然而,對於包含引用類型值得屬性來講,問題就比較突出了
    function Person() {}
    
    Person.prototype = {
      constructor: Person,                          // 讓 prototype的constructor從新指向Person
      name: "Nicholas",
      age: 29,
      job: "Software Engineer",
      friend: ["Shelby", "Court"],
      sayName: function () {
        console.log(this.name);
      }
    };
    
    var person1 = new Person();
    var person2 = new Person();
    
    // 這裏修改的其實是Person.prototype.friends
    person1.friends.push("Van");
    
    // 不但person1的friends屬性被修改,person2也作了一樣的改動
    console.log(person1.friends);                               // "Shelby,Court,Van"
    console.log(person1.friends);                               // "Shelby,Court,Van"
    // 由於兩個實例的friends屬性指向的都是Person.prototype.friends
    console.log(person1.friends === person2.friends);           // true
    • 上述問題正是不多有人單獨使用原型模式的緣由的所在

組合使用構造函數模式和原型模式

  • 建立自定義類型的最多見方式,就是組合使用構造函數模式與原型模式。構造函數模式用於定義實例屬性,而原型模式用於定義方法和共享的屬性。這樣每一個實例都會有本身的一份實例屬性的副本,但又同時共享着對方法的引用,最大限度的節省了內存。
  • 另外這種混成模式還支持向構造函數傳遞參數
  • 這種構造函數與原型混成的模式,是目前在ECMAScript 中使用最普遍,認同度最高的一種建立自定義類型的方法。能夠說是用來定義引用類型的一種默認模式。
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.friends = ["Shelby", "Court"];
}

Person.prototype = {
  constructor: Person,
  sayName: function() {
    console.log(this.name);
  }
};

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

person1.friends.push("Van");
console.log(person1.friends);                               // "Shelby,Court,Van"
console.log(person1.friends);                               // "Shelby,Court"
console.log(person1.friends === person2.friends);           // false
console.log(person1.sayName === person2.sayName);           // true

動態原型模式

  • 動態原型模型把全部信息都封裝在了構造函數中,而經過在構造函數中初始化原型(僅在必要的狀況下),又保持了同時使用構造函數和原型的優勢。換言之,能夠經過檢查某個應該存在的方法是否有效,來決定是否須要初始化原型
function Person(name, age, job) {
  // 屬性
  this.name = name;
  this.age = age;
  this.job = job;
  // 方法
  if (typeof this.sayName != "function") {
    Person.perototype.sayName = function() {
      console.log(this.name);
    };
  }
}

var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();                 // "Nicholas"
  • 使用動態原型模型時,不能使用對象字面量重寫原型。若是在已經建立了實例的狀況下重寫原型,那麼就會切斷現有實例與新原型之間的聯繫。

寄生構造函數模式

  • 在前述幾種模式都不適用的狀況下,可使用寄生(parasitic)構造函數模式。建立一個函數,該函數的做用僅僅是封裝建立的對象代碼,而後再返回新建立的對象;但從表面上看,這個函數又很像是典型的構造函數
function Person(name, age, job) {
  var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function() {
    console.log(this.name);
  };
  return o
}

var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();                 // "Nicholas"
  • 除了使用new操做符並把使用的包裝函數叫作構造函數以外,這個模式跟工廠模式實際上是如出一轍的。構造函數再不返回值得狀況下,默認會返回新對象實例。而經過一個return語句,能夠重寫調用構造函數時返回的值。
  • 這個模式能夠在特殊狀況下用來爲對象建立構造函數。假設咱們想建立一個具備額外方法的特殊數組。因爲不能直接修改Array構造函數,所以可使用這個模式。
function SpecialArray() {

  // 建立數組
  var values = new Array();

  // 添加值
  values.push.apply(values, arguments);

  // 添加方法
  values.toPipedString = function() {
    return this.join("|");
  };

  // 返回數組
  return values;
}

var colors = new SpecialArray("red", "blue", "green");
console.log(colors.toPipedString());                      // "red|blue|green"
  • 關於寄生構造函數模式,有一點須要說明:首先,返回的對象與構造函數或者與構造函數的原型屬性之間沒有關係;也就是說,構造函數返回的對象與構造函數外部建立的對象沒有什麼不一樣。爲此不能依賴instanceof操做符來肯定對象類型。因爲上述問題,咱們建議在可使用其餘模式的狀況下,不要使用寄生模式

穩妥構造函數模式

  • 道格拉斯·克羅克福德(Douglas Crockford)發明了JavaScript中的穩妥對象(durable objects)這個概念。穩妥對象,指的是沒有公共屬性並且其方法不引用this的對象。穩妥對象最適合在一些安全的環境中(這些環境中會禁止使用this和new),或者在防止數據被其餘應用程序(如Mashup程序)改動時使用。穩妥構造函數遵循與寄生構造函數相似的模式,但有兩點不一樣:app

    • 一是建立對象的實例方法不引用this
    • 二是不適用new操做符調用構造函數
function Person(name, age, job) {

  // 建立要返回的對象
  var o = new Object();

  // 這裏定義私有變量和函數
  ...

  // 添加方法
  o.sayName = function() {
    console.log(name);
  };

  // 返回對象
  return o;
}

var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName();   // "Nicholas"
  • 在這種模式建立的對象中,除了使用sayName()方法以外,沒有其餘辦法訪問name的值。
  • 與寄生構造函數模式相似,使用穩妥構造函數模式建立的對象與構造函數之間也沒有什麼關係,所以instanceof操做符對這種對象也沒有意義

繼承

  • 許多OO語言都支持兩種繼承方式函數

    • 接口繼承,只繼承方法簽名
    • 實現繼承,繼承實際方法
  • 如前所述,在ECMAScript中沒法實現接口繼承,只支持實現繼承,並且其實現繼承主要是依靠原型鏈來實現的。

原型鏈

  • 原型鏈 實現繼承的主要方法。基本思想是利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法。簡單回顧一下構造函數、原型和實例的關係:測試

    • 每一個構造函數都有一個原型對象
    • 原型對象都包含一個指向構造函數的指針
    • 而實例都包含一個指向原型對象的內部指針。
  • 假如咱們讓原型對象等於另外一個類型的實例,顯然,此時的原型對象將包含一個指向另外一個原型的指針,相應地,另外一個原型中也包含着指向另外一個構造函數的指針。假如另外一個原型又是另外一個類型的實例,那麼上述關係依然成立,如此層層遞進,就構成了實例與原型的鏈條。這就是所謂原型鏈的基本概念
  • 實現原型鏈有一種基本模式
function SuperType() {
  this.property = true;
}

SuperType.prototype.getSuperValue = function() {
  return this.property;
};

function SubType() {
  this.subproperty = false;
}

// 繼承了SuperType
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function () {
  return this.subproperty;
};

var instance = new SubType();
console.log(instance.getSuperValue());        //true
  • 上述代碼中,咱們沒有使用SubType默認使用的原型,而是給它換了個新原型,SuperType的實例。因而新原型不只具備做爲一個SuperType的實例所擁有的所有屬性和方法,並且其內部還有一個指針,指向了SuperType的原型。
  • 最終:this

    • instance指向SubType的原型
    • SubType的原型又指向SuperType的原型
    • getSuperValue() 方法仍然還在SuperType.prototype中,但property則位於SubType.prototype中。這是由於property是一個實例屬性,而getSuperValue()則是一個原型方法。 既然 SubType.prototype 如今是SuperType的實例,那麼property固然就位於該實例中了。
    • 此外,要注意instance.constructor 如今指向的是SuperType,這是由於原來SubType.prototype 中的 constructor被重寫了的緣故
  • 經過實現原型鏈,本質上拓展了原型搜索機制。當讀取模式訪問一個實例屬性時,首先會在實例中搜索該屬性。若是沒有找到該屬性,則會繼續搜索實例的原型。在經過原型鏈實現繼承的狀況下,搜索過程就得以沿着原型鏈繼續向上。調用instance.getSuperValue()會經歷三個搜索步驟spa

    1. 搜索實例
    2. 搜索SubType.prototype
    3. 搜索SuperType.prototype 最終找到方法

別忘記默認的原型

  • 事實上,前述例子的原型鏈少了一環。全部引用類型默認都繼承了Object,而這個繼承也是經過原型鏈實現的。全部函數的默認原型都是Object的實例,由於默認原型都會包含一個內部指針,指向Object.prototype。這也正是全部自定義類型都會繼承toString(), valueOf()等默認方法的根本緣由。
  • SubType繼承了SuperType,而 SuperType繼承了Object。當調用instance.toString()方法,實際上調用的是保存在Object.prototype中的那個方法

肯定原型和實例的關係

  • 第一種方式,使用instanceof操做符,只要用這個操做符來測試實例與原型鏈中出現過的構造函數,結果就會返回true。
// 因爲原型鏈,咱們能夠是instance是Object,SuperType, SubType中任何一個類型的實例
console.log(instance instanceof Object);         // true
console.log(instance instanceof SuperType);      // true
console.log(instance instanceof SubType);        // true
  • 第二種方式,使用isPrototypeOf()方法。一樣,只要是原型鏈中出現過的原型,均可以說是該原型鏈所派生的實例的原型。
console.log(Object.prototype.isPrototypeOf(instance));         // true
console.log(SuperType.prototype.isPrototypeOf(instance));      // true
console.log(SubType.prototype.isPrototypeOf(instance));        // true

謹慎的定義方法

  • 子類型有時候須要覆蓋超類型中的一個方法,或者須要添加超類型中不存在的某個方法。但無論怎麼樣,給原型鏈添加方法的代碼必定要放在替換原型的語句以後
function SuperType() {
  this.property = true;
}

SuperType.prototype.getSuperValue = function() {
  return this.property;
};

function SubType() {
  this.subproperty = false;
}

// 繼承了SuperType,原來默認的原型被替換
SubType.protoype = new SuperType();

// 添加新方法
SubType.prototype.getSubValue = function () {
  return this.subproperty;
};

// 重寫超類型中的方法
SubType.protoype.getSuperValue = function () {
  return false;
};

var instance = new SubType();
console.log(instance.getSuperValue());       // false
  • 還有一點須要提醒讀者,即在經過原型鏈實現繼承時,不能使用對象字面量建立原型方法。由於這樣就會重寫原型鏈
function SuperType() {
  this.property = true;
}

SuperType.prototype. getSuperValue = function() {
  return this.property;
};

function SubType() {
  this.subproperty = false;
}

// 繼承了SuperType
SubType.protype = new SuperType();

// 使用字面量添加新方法,會致使上一行代碼無效
// 原型鏈被切斷——SubType 和 SuperType 之間已經沒有關係
SubType.prototype = {
  getSubValue: function () {
    return this.subproperty;
  },

  someOtherMethod: function () {
    return false;
  }
};

var instance = new SubType();
console.log(instance.getSuperValue()));    // error

原型鏈的問題

  • 最主要的問題,來自包含引用類型值的原型。包含引用類型值的原型屬性,會被全部實例共享;因此要在構造函數中定義值而不是原型對象。在經過原型來實現繼承時,原型實際上會變成另外一個類型的實例。因而,原先的 實例屬性也就瓜熟蒂落的變成了 如今的原型屬性了。
function SuperType() {
  this.colors = ["red", "blue", "green"];
}

function SubType() {
}

// SubType 繼承了 SuperType 以後
// SubType.prototype 就變成了 SuperType的一個實例
// 所以 SubType.prototype 也擁有了本身的colors屬性 等價於建立了一個SubType.prototype.colors
SubType.protype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);                // "red,blue,green,black"

// 結果就是全部SubType實例都會共享這個colors屬性
var instance2 = new SubType();
console.log(instance2.colors);                // "red,blue,green,black"
  • 原型鏈的第二個問題:在建立子類型的實力時,不能向超類型的構造函數中傳遞參數。準確的說是沒有辦法在不影響全部對象實例的狀況下,給超類型的構造函數傳遞參數。
  • 有鑑於此,實踐中不多會單獨使用原型鏈

借用構造函數

  • 在解決原型中包含引用類型值所帶來的問題過程當中,開發人員開始使用一種叫作 借用構造函數(constructor stealing) 的技術(有時候也叫僞造對象或經典繼承)。
  • 思想至關簡單,即在子類型構造函數的內部調用超類型構造函數。函數只不過是在特定環境中執行代碼的對象,所以經過使用apply() call()方法也能夠在(未來)新建立對象上執行構造函數。其實是在(將來將要)新建立的SubType實例環境下,調用了SuperType構造函數,就會在新 SubType 對象上執行 SuperType() 函數中定義的全部對象初始化代碼。結果每一個SubType的實例都會具備本身的colors屬性的副本了。
function SuperType() {
  this.colors = ["red", "blue", "green"];
}

function SubType() {
  // 繼承了SuperType
  // "借調「了超類型的構造函數
  SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);                // "red,blue,green,black"

// SubType實例都不會共享這個colors屬性
var instance2 = new SubType();
console.log(instance2.colors);                // "red,blue,green"

傳遞參數

  • 相對於原型鏈而言,借用構造函數有一個很大的優點,便可以在子類型構造函數中向超類型構造函數傳遞參數。
  • 爲了確保SuperType 構造函數不會重寫子類型的屬性,能夠在調用超類型構造函數後,再添加應該在子類型中定義的屬性。
function SuperType(name) {
  this.name = name;
}

function SubType() {
  // 繼承了SuperType 同時傳遞了參數
  SuperType.call(this. "Nicholas");

  // 實例屬性
  this.age = 29;
}

var instance = new SubType();
console.log(instance.anme);             // "Nicholas"
console.log(instance.age);              // 29

借用構造函數的問題

  • 僅僅是借用構造函數,也將沒法避免構造函數模式存在的問題——方法都在構造函數中定義,所以函數複用就無從談起了。
  • 在超類型的原型中定義方法,對子類型而言也是不可見的,結果全部類型都只能使用構造函數模式。
  • 有鑑於此,借用構造函數也是不多單獨使用的

組合繼承

  • 組合繼承(combination inheritance),有時候也叫做僞經典繼承,是將原型鏈和借用構造函數的技術組合到一塊兒,從而發揮兩者之長。其背後的思路是使用原型鏈實現對原型屬性和方法的繼承,而經過借用構造函數來實現對實例屬性的繼承。既經過在原型上定義方法實現了函數複用,又能保證每一個實例都有它本身的屬性
function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
  console.log(this.name);
}

function SubType(name, age) {
  // 繼承屬性
  SuperType.call(this, name);

  // 子類型本身的屬性
  this.age = age
}

// 繼承方法
SubType.prototype = new SuperType();
// 若是不指定constructor,SubType.prototype.constructor 爲 SuperType
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
  console.log(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors);                 // "red,blue,green,black"
instance1.sayName();                           // "Nicholas"
instance1.sayAge();                            // 29

var instance2 = new SubType("Greg", 27);
console.log(instance2.colors);                 // "red,blue,green"
instance2.sayName();                           // "Greg"
instance2.sayAge();                            // 27
  • 組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優勢,成爲JavaScript中最經常使用的繼承模式。並且instanceof 和 isPrototypeOf() 也可以用於識別基於組合繼承建立的對象。

原型式繼承

  • 道格拉斯·克羅克福德2006年在文章中介紹了一種實現繼承的方法,這種方法並無使用嚴格意義上的構造函數。他的想法是藉助原型能夠基於已有對象建立新對象,同時還沒必要所以建立自定義類型。爲達到這個目的,給出了以下函數
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}
  • 在object() 函數內部,先建立了一個臨時性的構造函數,而後將傳入的對象做爲這個構造函數的原型,最後返回了這個臨時類型的新實例。從本質上講,object() 對傳入其中的對象執行了一次淺複製
var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"],
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
console.log(anotherPerson.friends);             // "Shelby,Court,Van,Rob"

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(yetAnotherPerson.friends);          // "Shelby,Court,Van,Rob,Barbie"

console.log(person.friends);                    // "Shelby,Court,Van,Rob,Barbie"
  • 這種原型式繼承,要求你必須有一個對象能夠做爲另外一個對象的基礎。把它傳遞給object()函數,而後再根據具體需求對獲得的對象加以修改便可。這意味着,person.friends不只屬於person,並且也會被anotherPerson, yetAnotherPerson共享。實際上就至關於又建立了person對象的兩個副本(淺拷貝)。
  • ECMAScript 5 經過新增Object.create() 方法規範化了原型式繼承。這個方法接受兩個參數:一個用做新對象原型的對象和(可選)一個新對象定義額外的屬性的對象。在傳入一個參數的狀況下,Object.create() 與 Object()方法的行爲相同(原著如此表述,但實際二者並不相同,參照第五章 Object類型 的相關補充說明
var person = {
  name: "Nicholoas",
  friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
console.log(anotherPerson.friends);             // "Shelby,Court,Van,Rob"

var yetAnotherPerson = Object.object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(yetAnotherPerson.friends);          // "Shelby,Court,Van,Rob,Barbie"

console.log(person.friends);                    // "Shelby,Court,Van,Rob,Barbie"
  • Object.create() 方法的第二個參數與Object.defineProperties() 方法的第二個參數格式相同:每一個屬性都是經過本身的描述符定義的。以這種方式制定的任何屬性都會覆蓋對象上同名的屬性。
var person = {
  name: "Nicholoas",
  friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = Object.create(person, {
  name: {
    value: "Greg"
  }
});

console.log(anotherPerson.name);           // "Greg"
  • 支持Object.create()方法的瀏覽器:IE9+, Firefox4+, Opera12+, Chrome
  • 在沒有必要興師動衆的建立構造函數,而只想讓一個對象與另外一個對象保持相似的狀況下,原型式繼承是徹底能夠勝任的。但別忘了,包含引用類型值(如上面的friends屬性是一個數組)始終都會共享相應的值,就像使用原型模式同樣。

寄生式繼承

  • 寄生式繼承(parasitic)是與原型式繼承緊密相關的一種思路,而且一樣也是由克羅克福德推而廣之的。
  • 思路與構造函數和工廠模式相似,既建立了一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後再像真的是它作了全部工做同樣返回對象。
function createAnother(original) {
  var clone = object(original);         // 經過調用函數建立一個新對象
  clone.sayHi = function() {            // 以某種方式加強這個對象
    console.log("hi");
  };
  return clone;                         // 返回這個對象
}

寄生組合式繼承

  • 組合繼承是最經常使用的繼承模式;不過最大的問題就是不管什麼狀況下,都會調用兩次超類型構造函數:

    • 一次是在建立子類型原型的時候
    • 另外一次是在子類型構造函數內部
  • 也就是說,子類型最終會包含超類型對象的所有實例屬性,但咱們不得不在調用子類型構造函數時,重寫這些屬性。
function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
  console.log(this.name);
};

function SubType(name, age) {
  SuperType.call(this, name);               // 第二次調用SuperType()

  this.age = age;
}

// 實例化SuperType做爲SubType的原型
// 當即觸發
SubType.prototype = new SuperType();        // 第一次調用SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
  console.log(this.age);
};

// 此時觸發第二次調用
var instance = new SubType("Nicholas", 29);

console.log(instance.name);                // "Nicholas"
console.log(SubType.prototype.name);       // undefined
  • 在第一次調用SuperType構造函數時,SubType.prototype會獲得兩個屬性:name和colors;它們都是SuperType的實例屬性,只不過如今位於SubType的原型中。當調用SubType構造函數時,又會調用一次 SuperType的構造函數,這一次又在新對象上建立了實例屬性name和colors屬性。因而這兩個屬性就屏蔽了原型中的兩個同名屬性。(圖6-6)

6-6

  • 有兩組name和colors屬性,一組在實例instance上,一組在SubType的原型中。這就是調用兩次SuperType構造函數的結果
  • 所謂寄生組合式繼承,即經過借用構造函數來繼承屬性,經過原型鏈的混成形式來繼承方法。其背後的思路是:沒必要爲了指定子類型的原型而調用超類型的構造函數,咱們所須要 的無非就是超類型原型的一個副本。本質上,就是使用寄生式繼承來繼承超類型的原型,而後再將結果指定給子類型的原型
function inheritPrototype(subType, superType) {

  // 建立對象 - 超類型的對象原型的副本
  // 這裏沒有使用new操做符,因此沒有生成SuperType的實例
  // 這裏沒有調用SuperType構造函數
  var prototype = Object(superType.prototype)
  console.log(prototype == superType.prototype)

  // 加強對象 - 彌補因重寫原型而失去的默認constructor屬性
  // 而這也將致使supert.prototype.constructor指向了subType
  prototype.constructor = subType;

  // 指定對象 - 將新建立的對象(即副本)賦值給子類型的原型。
  subType.prototype = prototype;
}

function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
  console.log(this.name);
};

function SubType(name, age) {
  SuperType.call(this, name);

  this.age = age;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function() {
  console.log(this.age);
};
  • inheritPrototype() 函數實現了寄生組合式繼承的最簡單形式。這個函數接受兩個參數: 子類型構造函數和超類型構造函數。
  • 這樣就只調用了一次SuperType構造函數,而且所以便了SubType.prototype 上建立沒必要要的、多餘的屬性。與此同時,原型鏈還能保持不變;所以,還可以正常使用instanceof 和 isPrototypeOf().
  • 開發人員廣泛認爲寄生組合式繼承是引用類型最理想的繼承範式。
相關文章
相關標籤/搜索