国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

《JavaScript高級(jí)程序設(shè)計(jì)》(第3版)讀書筆記 第6章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

gclove / 1585人閱讀

摘要:第章將詳細(xì)討論瀏覽器對(duì)象模型將構(gòu)造函數(shù)當(dāng)作函數(shù)構(gòu)造函數(shù)與其他函數(shù)的唯一區(qū)別就在于調(diào)用方式不同。而在構(gòu)造函數(shù)內(nèi)部,我們將屬性設(shè)置成等于全局的函數(shù)。默認(rèn)情況下,所有原型對(duì)象都會(huì)自動(dòng)獲得一個(gè)構(gòu)造函數(shù)屬性,這個(gè)屬性是一個(gè)指向?qū)傩运诤瘮?shù)的指針。

面向?qū)ο螅∣bject-Oriented, OO)的語言有一個(gè)標(biāo)志,它們都有類的概念,而通過類可以創(chuàng)建任意多個(gè)具有相同屬性和方法的對(duì)象。

ECMAScript中沒有類的概念,因此它的對(duì)象也與基類的語言中的對(duì)象有所不同

ECMA-262把對(duì)象定義為:“無序?qū)傩缘募?,其屬性可以包含基本值、?duì)象或者函數(shù)?!?我們可以把ECMAScript的對(duì)象想象成散列表:無非就是一組名值對(duì),其中值可以是數(shù)據(jù)或者函數(shù)

每個(gè)對(duì)象都是基于一個(gè)引用類型創(chuàng)建的,這個(gè)引用類型可以是第5章討論的原生類型,也可以是開發(fā)者定義的類型

理解對(duì)象
// 創(chuàng)建對(duì)象,賦給屬性
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Sofware Engineer";

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

// 字面量方式創(chuàng)建對(duì)象
var person = {
  name: "Nicholas",
  age: 29,
  job: "Sofware Engineer",
  sayName: function() {
    alert(this.name);
  }
}
屬性類型

ECMAScript中有兩種屬性:數(shù)據(jù)屬性和訪問器屬性

數(shù)據(jù)屬性

數(shù)據(jù)屬性包含一個(gè)數(shù)值的位置。在這個(gè)位置可以讀取和寫入值。數(shù)據(jù)屬性有4個(gè)描述其行為的特性

[[Configurable]] 表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。像前述例子中那樣直接在對(duì)象定義的屬性,它們這個(gè)特性默認(rèn)值為true

[[Enumerable]] 表示能否通過for-in循環(huán)返回屬性。前述例子這個(gè)特性默認(rèn)為true

[[Writable]] 表示能否修改屬性的值。前述例子這個(gè)特性默認(rèn)為true

[[Value]] 包含這個(gè)屬性的數(shù)據(jù)值。讀取屬性值得實(shí)惠,從這個(gè)位置讀;寫入屬性值得實(shí)惠,把新值保存在這個(gè)位置。這個(gè)特性的默認(rèn)值為undefined

要修改屬性默認(rèn)的特性,必須使用ECMAScript5的Object.defineProperty()方法。

主要接收三個(gè)參數(shù):屬性所在的對(duì)象,屬性的名字和一個(gè)描述符對(duì)象。描述符(descriptor)對(duì)象的屬性必須是:configurable,enumerable,writalbe和value。

設(shè)置其中的一或多個(gè)值,可以修改對(duì)應(yīng)的特性值

var person = {};
// 創(chuàng)建了一個(gè)name屬性,它的值"Nicholas"是只讀的。
Object.defineProperty(person, "name", {
  writable: false,
  value: "Nicholas"
});

console.log(person.name);                // "Nicholas"
// 在非嚴(yán)格模式下,賦值操作會(huì)被忽略
// 而在嚴(yán)格模式下,會(huì)導(dǎo)致錯(cuò)誤
person.name = "Greg";
console.log(person.name);                // "Nicholas"

把configurable設(shè)置為false,表示不能從對(duì)象中刪除屬性。如果對(duì)這個(gè)屬性調(diào)用delete,則在非嚴(yán)格模式下什么也不會(huì)發(fā)生,而在嚴(yán)格模式下會(huì)導(dǎo)致錯(cuò)誤

一旦把屬性定義為不可配置的,就不能再把它變回可配置了。此時(shí)再調(diào)用Object.defineProperty()方法修改除writable之外的特性,都會(huì)導(dǎo)致錯(cuò)誤

var person = {};
Object.defineProperty(person, "name", {
  configurable: false,
  value: "Nicholas"
});

// 拋出錯(cuò)誤
Object.defineProperty(person, "name", {
  configurable: true,
  value: "Nicholas"
})

可以多次調(diào)用Object.defineProperty()方法修改同一個(gè)屬性,但在把configurable特性設(shè)置為false之后就會(huì)有限制了。

調(diào)用Object.defineProperty()如果不指定,configurable,enumerable,writalbe 特性的默認(rèn)值都是false。多數(shù)情況下,沒有必要利用Object.defineProperty()方法提供的高級(jí)功能。

訪問器屬性

訪問器屬性不包含數(shù)據(jù)值,它們包含一對(duì)getter和setter函數(shù)(不是必須的)

在讀取訪問器屬性時(shí),會(huì)調(diào)用getter函數(shù),這個(gè)函數(shù)負(fù)責(zé)返回有效的值

寫入訪問器屬性時(shí),會(huì)調(diào)用setter函數(shù)并傳入新值,這個(gè)函數(shù)負(fù)責(zé)決定如何處理數(shù)據(jù)

訪問器屬性有如下4個(gè)特性:

[[Configurable]] 表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為數(shù)據(jù)屬性。對(duì)于直接在對(duì)象上定義的屬性,這個(gè)特性的默認(rèn)值為true

[[Enumerable]] 表示能否通過for-in循環(huán)返回屬性。對(duì)于直接在對(duì)象上定義的屬性,這個(gè)特性的默認(rèn)值為true

[[Get]] 在讀取屬性時(shí)調(diào)用的函數(shù)。默認(rèn)值為undefined

[[Set]] 在寫入屬性時(shí)調(diào)用的函數(shù)。默認(rèn)值為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;
    }
  }
});

// 這是使用訪問器屬性的常見方式,即設(shè)置一個(gè)屬性的值會(huì)導(dǎo)致其他屬性發(fā)生變化。
book.year = 2005;
console.log(book.edition);   // 2

不一定非要同時(shí)制定getter和setter。只指定getter意味著屬性時(shí)不能寫,嘗試寫入屬性會(huì)被忽略。在嚴(yán)格模式下嘗試寫入只指定了getter函數(shù)的屬性會(huì)拋出錯(cuò)誤。

只指定setter意味著屬性時(shí)也不能讀,否則在非嚴(yán)格模式下回返回undefined,而在嚴(yán)格模式下會(huì)拋出錯(cuò)誤

支持ECMAScript 5這個(gè)方法的瀏覽器有IE9+(IE8只是部分實(shí)現(xiàn)),F(xiàn)irefox 4+, Safari 5+, Opera 12+, Chrome

在不支持Object.defineProperty()方法的瀏覽器中不能修改[[Configurable]] 和 [[Enumerable]]

定義多個(gè)屬性

Object.defineProperties() 定義多個(gè)屬性的方法

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() 方法,可以取得給定屬性的描述符,這個(gè)方法接收兩個(gè)參數(shù):屬性所在的對(duì)象和要讀取屬性描述符的屬性名稱。返回值時(shí)一個(gè)對(duì)象,如果是訪問器屬性,這個(gè)對(duì)象的屬性有configurable, enumerable, get, set;如果是數(shù)據(jù)屬性,這個(gè)對(duì)象的屬性有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"
創(chuàng)建對(duì)象

雖然Object構(gòu)造函數(shù)或?qū)ο笞置媪慷伎梢杂脕韯?chuàng)建單個(gè)對(duì)象,但這些方式有個(gè)明顯的缺點(diǎn):使用同一個(gè)接口創(chuàng)建很多對(duì)象,會(huì)產(chǎn)生大量的重復(fù)代碼。為解決這個(gè)問題,人們開始使用工廠模式的一種變體

工廠模式

考慮到ECMAScript中無法創(chuàng)建類,開發(fā)者就發(fā)明了一種函數(shù),用函數(shù)來封裝以特定接口創(chuàng)建對(duì)象的細(xì)節(jié)

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");

雖然工廠模式解決了創(chuàng)建多個(gè)相似對(duì)象的問題,但卻沒有解決對(duì)象識(shí)別的問題(即怎樣知道一個(gè)對(duì)象的類型)。

構(gòu)造函數(shù)模式

可創(chuàng)建自定義的構(gòu)造函數(shù),從而定義自定義對(duì)象類型的屬性和方法

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");

構(gòu)造函數(shù)模式對(duì)比工廠模式存在以下不同之處

沒有顯式的創(chuàng)造對(duì)象

直接將屬性和方法賦給了this對(duì)象

沒有return 語句

函數(shù)名首字母大寫,按照慣例,構(gòu)造函數(shù)都應(yīng)該以一個(gè)大寫字母開頭,而非構(gòu)造函數(shù)小寫

person1 和 person2 分別保存著Person的一個(gè)不同的實(shí)例。兩者都有一個(gè)constructor(構(gòu)造函數(shù))屬性,改屬性指向Person

console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true

對(duì)象的constructor屬性最初是用來標(biāo)識(shí)對(duì)象類型的。但是,檢測(cè)對(duì)象類型,還是instanceof操作符要更可靠。

這個(gè)例子中創(chuàng)建的所有對(duì)象既是Object的實(shí)例,同時(shí)也是Person的實(shí)例

console.log(person1 instanceof Object);   // true
console.log(person2 instanceof Object);   // true
console.log(person1 instanceof Person);   // true
console.log(person2 instanceof Person);   // true

創(chuàng)建自定義的構(gòu)造函數(shù)意味著將來可以將它的實(shí)例標(biāo)識(shí)為一種特定的類型,這正是構(gòu)造韓式模式勝過工廠模式的地方。這個(gè)例子中,person1, person2 之所以同時(shí)是Object的實(shí)例,是因?yàn)樗袑?duì)象均繼承自O(shè)bject

以這種方式定義的構(gòu)造是定義在Global對(duì)象(瀏覽器中是window)中的。第8章將詳細(xì)討論瀏覽器對(duì)象模型(BOM)

將構(gòu)造函數(shù)當(dāng)作函數(shù)

構(gòu)造函數(shù)與其他函數(shù)的唯一區(qū)別就在于調(diào)用方式不同。

構(gòu)造函數(shù)也是函數(shù),不存在定義構(gòu)造函數(shù)的特殊語法

任何函數(shù)只要通過new操作符來調(diào)用,那它就可以作為構(gòu)造函數(shù)

而任何函數(shù)不通過new操作符來調(diào)用,那它跟普通函數(shù)沒有區(qū)別

// 作為構(gòu)造函數(shù)使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName();   // "Nicholas"

// 作為普通函數(shù)調(diào)用,此時(shí)this指向window對(duì)象
Person("Greg", 27, "Doctor");  // 添加到window對(duì)象上
window.sayName();       // "Greg"

// 在另一個(gè)對(duì)象的作用域中調(diào)用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); // "Kristen"
構(gòu)造函數(shù)的問題

構(gòu)造函數(shù)模式的并非沒有缺點(diǎn)。主要問題就是每個(gè)方法都要在每個(gè)實(shí)例上重新創(chuàng)建一遍。在前述例子中person1和person2都有一個(gè)名為sayName()的方法,但那兩個(gè)方法不是同一個(gè)Function的實(shí)例。不要忘了ECMAScript中函數(shù)是對(duì)象,因此每定義一個(gè)函數(shù),也就是實(shí)例化了一個(gè)對(duì)象。從邏輯角度講,此時(shí)的構(gòu)造函數(shù)也可以這樣定義

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  // 與聲明函數(shù)再路基上是等價(jià)的
  this.sayName = new Function("alert(this.name)");
}

console.log(person1.sayName == person2.sayName);  // false

有this對(duì)象在,沒有必要在執(zhí)行代碼前就把函數(shù)綁定到特定對(duì)象上面,通過把函數(shù)定義轉(zhuǎn)移到函數(shù)外部來簡化

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函數(shù)的定義轉(zhuǎn)移到了函數(shù)外部。而在構(gòu)造函數(shù)內(nèi)部,我們將sayName屬性設(shè)置成等于全局的sayName函數(shù)。如此一來,由于sayName包含的是一個(gè)指向函數(shù)的指針,因此person1和person2對(duì)象就共享了全局作用域中定義的同一個(gè)sayName() 函數(shù)。

新的問題出現(xiàn)了,在全局作用域中定義的函數(shù)實(shí)際上只能被某個(gè)對(duì)象調(diào)用,這讓全局作用域有點(diǎn)名不副實(shí)。而更讓人無法接受的是:如果對(duì)象需要定義很多方法,那么就要定義很多個(gè)全局函數(shù),于是我們這個(gè)自定義的引用類型就絲毫沒有封裝性可言了。

好在可以通過原型模式來解決

原型模式

我們創(chuàng)建的每個(gè)函數(shù)都有一個(gè)prototype(原型),這個(gè)屬性是一個(gè)指針,指向一個(gè)對(duì)象,而這個(gè)對(duì)象的用途是包含可以由特定類型的所有實(shí)例共享的屬性和方法。

prototype就是通過調(diào)用構(gòu)造函數(shù)而創(chuàng)建的那個(gè)對(duì)象實(shí)例的原型對(duì)象,使用原型對(duì)象的好處是可以讓所有對(duì)象實(shí)例共享它所包含的屬性和方法。換言之,不必再構(gòu)造函數(shù)中定義對(duì)象實(shí)例的信息,而是可以將這些信息直接添加到原型對(duì)象中

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屬性中,構(gòu)造函數(shù)變成了空函數(shù)。即使如此,也仍然可以通過調(diào)用構(gòu)造函數(shù)來創(chuàng)建新對(duì)象,而且新對(duì)象還會(huì)具有相同的屬性和方法。

但與構(gòu)造函數(shù)模式不同的是,新對(duì)象的這些屬性和方法時(shí)由所有實(shí)例共享的。換言之,person1和person2訪問的都是同一組屬性和方法

要理解原型模式的工作原理,必須先理解ECMAScript中原型對(duì)象的性質(zhì)

理解原型對(duì)象

只要?jiǎng)?chuàng)建了一個(gè)函數(shù),就會(huì)根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個(gè)prototype屬性,這個(gè)屬性指向函數(shù)的原型對(duì)象。

默認(rèn)情況下,所有原型對(duì)象都會(huì)自動(dòng)獲得一個(gè)constructor(構(gòu)造函數(shù))屬性,這個(gè)屬性是一個(gè)指向prototype屬性所在函數(shù)的指針。例如上述例子,Person.prototype.constructor 指向Person。

我們可以繼續(xù)為原型對(duì)象添加其他屬性和方法

創(chuàng)建了自定義構(gòu)造函數(shù)后,其原型對(duì)象默認(rèn)只會(huì)得到constructor屬性,至于其他方法,則都是從Object繼承而來。當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)新的實(shí)例后,該實(shí)例的內(nèi)部將包含一個(gè)指針(內(nèi)部屬性),指向構(gòu)造函數(shù)的原型對(duì)象。ECMAScript 5 中管這個(gè)叫[[Prototype]]。

雖然腳本中沒有標(biāo)準(zhǔn)的方式訪問[[Prototype]],但Firefox, Safari, Chrome 在每個(gè)對(duì)象都支持一個(gè)屬性__proto__;這個(gè)屬性對(duì)腳本則是完全不可見的。

這個(gè)鏈接存在于實(shí)例與構(gòu)造函數(shù)的原型對(duì)象之間,而不是存在于實(shí)例與構(gòu)造函數(shù)之間

圖6-1(148頁)展示了Person構(gòu)造函數(shù)、Person的原型屬性以及Person現(xiàn)有的兩個(gè)實(shí)例之間的關(guān)系。在此,Person.prototype指向了原型對(duì)象,而Person.prototype.constructor又指回了Person。原型對(duì)象中除了包含constructor屬性之外,還包括后來添加的其他屬性。Person的每個(gè)實(shí)例——person1和person2都包含一個(gè)內(nèi)部屬性,該屬性僅僅指向了Person.prototype;換言之,person1和person2 與構(gòu)造函數(shù)沒有直接的關(guān)系。此外,要格外注意的是,雖然這兩個(gè)實(shí)例都不包含屬性和方法,但我們卻可以調(diào)用person1.sayName()。這是通過查找對(duì)象屬性的過程來實(shí)現(xiàn)的。

雖然在所有實(shí)現(xiàn)中都無法訪問到[[Prototype]],但可以通過isPrototypeOf()方法來確定對(duì)象之間是否存在這種關(guān)系。

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 新增了一個(gè)方法,Object.getPrototypeOf(), 在所有支持的實(shí)現(xiàn)中,這個(gè)方法返回[[Prototype]]的值。支持的瀏覽器 IE9+, Safari 5+, Opera 12+, Chrome

console.log(Object.getPrototypeOf(person1) == Person.prototype); // true
console.log(Object.getPrototypeOf(person1).name);                // "Nicholas"

每當(dāng)代碼讀取某個(gè)對(duì)象的某個(gè)屬性時(shí),都會(huì)執(zhí)行一次搜索,目標(biāo)是具有給定名字的屬性。搜索從對(duì)象實(shí)例本身開始,如果沒有找到,則繼續(xù)搜索指針指向的的原型對(duì)象,在原型對(duì)象中查找具有給定名字的屬性。也就是說,在我們調(diào)用person1.sayName()的時(shí)候,會(huì)先執(zhí)行兩次搜索,在person1中沒有找到sayName屬性,繼續(xù)在person1的原型中搜索,在Person.prototype中找到了sayName屬性,然后讀取那個(gè)保存在原型對(duì)象中的函數(shù)。

原型最初只包含constructor屬性,而該屬性也是共享的,因此可以通過對(duì)象實(shí)例訪問

雖然可以通過對(duì)象實(shí)例訪問保存在原型中的值,但卻不能通過對(duì)象實(shí)例重寫原型中的值。如果我們?cè)趯?shí)例中添加了一個(gè)屬性,而該屬性與實(shí)例原型中的一個(gè)屬性同名,那我們就在實(shí)例中創(chuàng)建該屬性,該屬性將會(huì)屏蔽原型中的那個(gè)屬性

hasOwnProperty()方法(繼承于Object)可以檢測(cè)一個(gè)屬性是存在于實(shí)例中,還是存在于原型中。

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" ——來自實(shí)例
console.log(person1.hasOwnProperty("name"));       // true

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

// 使用delete操作符刪除實(shí)例中的屬性
delete person1.name;
console.log(person1.name);                         // "Nicholas" ——來自實(shí)例
console.log(person1.hasOwnProperty("name"));       // false

ECMAScript 5 的 Object.getOwnPropertyDescriptor() 方法只能用于實(shí)例屬性,要取得原型屬性的描述符,必須直接在原型對(duì)象上調(diào)用Object.getOwnPropertyDescriptor() 方法

原型與in操作符

兩種方式使用in操作符,多帶帶使用和在for-in循環(huán)中使用。

多帶帶使用in操作符,會(huì)在通過對(duì)象能夠訪問給定屬性時(shí)返回true,無論該屬性存在于實(shí)例中還是原型中。

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操作符刪除實(shí)例中的屬性
delete person1.name;
console.log("name" in person1);                    // true

同時(shí)使用hasOwnProperty() 方法和in操作符,就可以確定該屬性存在于實(shí)例中還是存在運(yùn)行中

function hasPrototypeProperty(object, name) {
  return !object.hasOwnProperty(name) && (name in object);
}

由于in操作符只要通過能夠訪問到屬性就返回true, hasOwnProperty() 只在屬性存在于實(shí)例中才返回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循環(huán),返回的是所有能夠通過對(duì)象訪問的、可枚舉的(enumerad)屬性,其中既包括存在于實(shí)例中的屬性,也包括存在于原型中的屬性。屏蔽了原型中不可枚舉的屬性(即將[[Enumerable]]標(biāo)記為false的屬性)實(shí)例屬性也會(huì)在for-in循環(huán)返回,因?yàn)楦鶕?jù)規(guī)定,所有開發(fā)人員定的屬性,都是可枚舉的——只有在IE8及更早版本中例外。

IE早期版本的實(shí)現(xiàn)中存在一個(gè)bug,即屏蔽不可枚舉屬性的實(shí)例屬性不會(huì)出現(xiàn)在for-in循環(huán)中。

var o = {
  toString: function() {
    return  "My Object";
  }
};

for (var prop in o) {
  if (prop ==  "toString") {
    console.log("Found toString");    // 在IE中不會(huì)顯示
  }
}

在IE中,由于其實(shí)現(xiàn)認(rèn)為原型的toString()方法被打上了值為false的[[Enumerable]]標(biāo)記,因此應(yīng)該跳過該屬性,結(jié)果就不會(huì)打印。該bug會(huì)影響默認(rèn)不可枚舉的所有屬性和方法,包括:hasOwnProperty(), propertyIsEnumerable(), toLocaleString(), valueOf()

ECMAScript 5 也將constructor和prototype屬性的[Enumerable]]特性設(shè)置為false,但并不是所有瀏覽器都照此實(shí)現(xiàn)。

要取得對(duì)象上所有可枚舉的實(shí)例屬性,可以使用ECMAScript5的Object.keys()方法。這個(gè)方法接收一個(gè)對(duì)象作為參數(shù),返回一個(gè)包含所有可枚舉屬性的字符串?dāng)?shù)組

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"

如果你想要得到所有實(shí)例屬性,無論它是否可枚舉,都可以使用Object.getOwnPropertyNames()方法。

var keys = Object.getOwnPropertyNames(Person.prototype);     // "constructor,name,age,job,sayName"

注意結(jié)果中包含了不可枚舉的constructor屬性。Object.keys() 和 Object.getOwnPropertyNames()方法都可以用來替代for-in循環(huán)。支持這兩個(gè)方法的瀏覽器有IE9+, Firefox4+, Safari5+, Opera12+, Chrome

更簡單的原型語法

前面例子中每添加一個(gè)屬性和方法就要敲一遍 Person.prototype。 為減少不必要的輸入,也為了從視覺上更好的封裝原型的功能,更常見的做法是用一個(gè)包含所有屬性和方法的對(duì)象字面量來重寫整個(gè)原型對(duì)象

function Person() {}

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

我們將Person.prototype設(shè)置為等于一個(gè)以對(duì)象字面量形式創(chuàng)建的新對(duì)象。最終結(jié)果相同,但有一個(gè)例外:constructor屬性不再指向Person了。因?yàn)槊縿?chuàng)建一個(gè)函數(shù),就會(huì)同時(shí)創(chuàng)建它的prototype對(duì)象,整個(gè)對(duì)象也會(huì)自動(dòng)獲得constructor屬性。而我們?cè)谶@里使用的語法,本質(zhì)上完全重寫了默認(rèn)的prototype對(duì)象,因此constructor屬性也就變成了新對(duì)象的constructor屬性(指向Object構(gòu)造函數(shù)),不再指向Person函數(shù)。此時(shí)盡管instanceof操作符還能返回正確的結(jié)果,但通過constructor已經(jīng)無法確定對(duì)象的類型

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的值真的很重要,可以像下面這樣特意將它設(shè)置回適當(dāng)?shù)闹怠?/p>

function Person() {}

Person.prototype = {
  constructor: Person,                          // 讓 prototype的constructor重新指向Person
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  sayName: function () {
    console.log(this.name);
  }
};

這種方式重設(shè)constructor會(huì)導(dǎo)致它的[[Enumerable]]特性被設(shè)置為true。默認(rèn)情況下,原生的constructor屬性是不可枚舉的,因此如果你使用兼容ECMAScript 5 的 JavaScript 引擎,可以試一試 Object.defineProperty()

function Person() {}

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

// 重設(shè)構(gòu)造函數(shù),只適用于ECMASCript 5 兼容的瀏覽器
Object.defineProperty(Person.prototype, "constructor", {
  enumerable: false,
  value: Person
});
原型的動(dòng)態(tài)性

由于在原型中查找值的過程是一次搜索,因此我們對(duì)原型對(duì)象所做的任何修改都能夠立即從實(shí)例上反映出來——即使是先創(chuàng)建了實(shí)例后修改原型也照樣如此

var friend = new Person();

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

friend.sayHi();               // "Hi"

盡管可以隨時(shí)為原型添加屬性和方法,并且修改能夠立即在所有對(duì)象實(shí)例中反映出來,但如果是重寫整個(gè)原型對(duì)象,那么情況就不一樣了。調(diào)用構(gòu)造函數(shù)時(shí)會(huì)為實(shí)例添加一個(gè)指向最初原型的[[Protoype]]指針,而吧原型修改為另外一個(gè)對(duì)象,就等于切斷了構(gòu)造函數(shù)與最初原型之間的聯(lián)系。請(qǐng)記?。簩?shí)例中的指針僅指向原型,而不是指向構(gòu)造函數(shù)。

function Person() {}

var friend = new Person();

// 重寫整個(gè)原型對(duì)象,就等于切斷了構(gòu)造函數(shù)與最初原型之間的聯(lián)系
Person.prototype = {
  constructor: Person,
  age: 29,
  job: "Software Engineer",
  sayName: function () {
    console.log(this.name);
  }
};

friend.sayName();  // error
原生對(duì)象的原型

原型模式的重要性不僅體現(xiàn)在創(chuàng)建自定義類型方面,就連所有原生的引用類型,都是采用這種模式創(chuàng)建的。所有原生引用類型(Object, Array, String, 等)都在其構(gòu)造函數(shù)的原型上定義了方法。例如,在Array.prototype 中可以找到sort()方法,而在String.prototype中可以找到substring()方法修改同一個(gè)屬性

console.log(typeof Array.prototype.sort);          // "function"
console.log(typeof String.prototype.substring);    // "function"

通過原生對(duì)象的原型,不僅可以取得所有默認(rèn)方法的引用,而且也可以定義新方法??梢韵裥薷淖远x對(duì)象的原型一樣修改原生對(duì)象的原型,因此可以隨時(shí)添加方法。下面的代碼就給基本包裝類型String添加了一個(gè)名為startsWith()的方法

String.prototype.startsWith = function (text) {
  return this.indexOf(text) == 0;
};

var msg = "Hello world!";
console.log(msg.startsWith("Hello"));             // true

盡管看起來很方便,但不推薦在產(chǎn)品化的程序修改原生對(duì)象的原型。如果某個(gè)實(shí)現(xiàn)中缺少某個(gè)方法,就在原生對(duì)象的原型中添加這個(gè)方法,那么當(dāng)另一個(gè)支持該方法的實(shí)現(xiàn)中運(yùn)行代碼時(shí),就可能會(huì)導(dǎo)致命名沖突。而且這樣做也可能會(huì)意外的重寫原生方法。

原型對(duì)象的問題

原型模式也不是沒有缺點(diǎn)。

首先,它省略了為構(gòu)造函數(shù)傳遞初始化參數(shù)這一環(huán)節(jié),結(jié)果所有實(shí)例在默認(rèn)情況下都將取得相同的屬性值。雖然這回在某種程度上帶來一些不方便,但還不是原型的最大問題

原型模式最大的問題是由其共享的本性鎖導(dǎo)致的。原型中所有屬性是被很多實(shí)例共享的,這種共享對(duì)于函數(shù)非常適合。對(duì)于那些包含基本值的屬性倒也說得過去,畢竟通過在實(shí)例上添加一個(gè)同名屬性,可以隱藏原型中的對(duì)應(yīng)屬性。

然而,對(duì)于包含引用類型值得屬性來說,問題就比較突出了

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();

// 這里修改的實(shí)際上是Person.prototype.friends
person1.friends.push("Van");

// 不但person1的friends屬性被修改,person2也做了同樣的改動(dòng)
console.log(person1.friends);                               // "Shelby,Court,Van"
console.log(person1.friends);                               // "Shelby,Court,Van"
// 因?yàn)閮蓚€(gè)實(shí)例的friends屬性指向的都是Person.prototype.friends
console.log(person1.friends === person2.friends);           // true

上述問題正是很少有人多帶帶使用原型模式的原因的所在

組合使用構(gòu)造函數(shù)模式和原型模式

創(chuàng)建自定義類型的最常見方式,就是組合使用構(gòu)造函數(shù)模式與原型模式。構(gòu)造函數(shù)模式用于定義實(shí)例屬性,而原型模式用于定義方法和共享的屬性。這樣每個(gè)實(shí)例都會(huì)有自己的一份實(shí)例屬性的副本,但又同時(shí)共享著對(duì)方法的引用,最大限度的節(jié)省了內(nèi)存。

另外這種混成模式還支持向構(gòu)造函數(shù)傳遞參數(shù)

這種構(gòu)造函數(shù)與原型混成的模式,是目前在ECMAScript 中使用最廣泛,認(rèn)同度最高的一種創(chuàng)建自定義類型的方法。可以說是用來定義引用類型的一種默認(rèn)模式。

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
動(dòng)態(tài)原型模式

動(dòng)態(tài)原型模型把所有信息都封裝在了構(gòu)造函數(shù)中,而通過在構(gòu)造函數(shù)中初始化原型(僅在必要的情況下),又保持了同時(shí)使用構(gòu)造函數(shù)和原型的優(yōu)點(diǎn)。換言之,可以通過檢查某個(gè)應(yīng)該存在的方法是否有效,來決定是否需要初始化原型

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"

使用動(dòng)態(tài)原型模型時(shí),不能使用對(duì)象字面量重寫原型。如果在已經(jīng)創(chuàng)建了實(shí)例的情況下重寫原型,那么就會(huì)切斷現(xiàn)有實(shí)例與新原型之間的聯(lián)系。

寄生構(gòu)造函數(shù)模式

在前述幾種模式都不適用的情況下,可以使用寄生(parasitic)構(gòu)造函數(shù)模式。創(chuàng)建一個(gè)函數(shù),該函數(shù)的作用僅僅是封裝創(chuàng)建的對(duì)象代碼,然后再返回新創(chuàng)建的對(duì)象;但從表面上看,這個(gè)函數(shù)又很像是典型的構(gòu)造函數(shù)

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操作符并把使用的包裝函數(shù)叫做構(gòu)造函數(shù)之外,這個(gè)模式跟工廠模式其實(shí)是一模一樣的。構(gòu)造函數(shù)再不返回值得情況下,默認(rèn)會(huì)返回新對(duì)象實(shí)例。而通過一個(gè)return語句,可以重寫調(diào)用構(gòu)造函數(shù)時(shí)返回的值。

這個(gè)模式可以在特殊情況下用來為對(duì)象創(chuàng)建構(gòu)造函數(shù)。假設(shè)我們想創(chuàng)建一個(gè)具有額外方法的特殊數(shù)組。由于不能直接修改Array構(gòu)造函數(shù),因此可以使用這個(gè)模式。

function SpecialArray() {

  // 創(chuàng)建數(shù)組
  var values = new Array();

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

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

  // 返回?cái)?shù)組
  return values;
}

var colors = new SpecialArray("red", "blue", "green");
console.log(colors.toPipedString());                      // "red|blue|green"

關(guān)于寄生構(gòu)造函數(shù)模式,有一點(diǎn)需要說明:首先,返回的對(duì)象與構(gòu)造函數(shù)或者與構(gòu)造函數(shù)的原型屬性之間沒有關(guān)系;也就是說,構(gòu)造函數(shù)返回的對(duì)象與構(gòu)造函數(shù)外部創(chuàng)建的對(duì)象沒有什么不同。為此不能依賴instanceof操作符來確定對(duì)象類型。由于上述問題,我們建議在可以使用其他模式的情況下,不要使用寄生模式

穩(wěn)妥構(gòu)造函數(shù)模式

道格拉斯·克羅克福德(Douglas Crockford)發(fā)明了JavaScript中的穩(wěn)妥對(duì)象(durable objects)這個(gè)概念。穩(wěn)妥對(duì)象,指的是沒有公共屬性而且其方法不引用this的對(duì)象。穩(wěn)妥對(duì)象最適合在一些安全的環(huán)境中(這些環(huán)境中會(huì)禁止使用this和new),或者在防止數(shù)據(jù)被其他應(yīng)用程序(如Mashup程序)改動(dòng)時(shí)使用。穩(wěn)妥構(gòu)造函數(shù)遵循與寄生構(gòu)造函數(shù)類似的模式,但有兩點(diǎn)不同:

一是創(chuàng)建對(duì)象的實(shí)例方法不引用this

二是不適用new操作符調(diào)用構(gòu)造函數(shù)

function Person(name, age, job) {

  // 創(chuàng)建要返回的對(duì)象
  var o = new Object();

  // 這里定義私有變量和函數(shù)
  ...

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

  // 返回對(duì)象
  return o;
}

var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName();   // "Nicholas"

在這種模式創(chuàng)建的對(duì)象中,除了使用sayName()方法之外,沒有其他辦法訪問name的值。

與寄生構(gòu)造函數(shù)模式類似,使用穩(wěn)妥構(gòu)造函數(shù)模式創(chuàng)建的對(duì)象與構(gòu)造函數(shù)之間也沒有什么關(guān)系,因此instanceof操作符對(duì)這種對(duì)象也沒有意義

繼承

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

接口繼承,只繼承方法簽名

實(shí)現(xiàn)繼承,繼承實(shí)際方法

如前所述,在ECMAScript中無法實(shí)現(xiàn)接口繼承,只支持實(shí)現(xiàn)繼承,而且其實(shí)現(xiàn)繼承主要是依靠原型鏈來實(shí)現(xiàn)的。

原型鏈

原型鏈 實(shí)現(xiàn)繼承的主要方法。基本思想是利用原型讓一個(gè)引用類型繼承另一個(gè)引用類型的屬性和方法。簡單回顧一下構(gòu)造函數(shù)、原型和實(shí)例的關(guān)系:

每個(gè)構(gòu)造函數(shù)都有一個(gè)原型對(duì)象

原型對(duì)象都包含一個(gè)指向構(gòu)造函數(shù)的指針

而實(shí)例都包含一個(gè)指向原型對(duì)象的內(nèi)部指針。

假如我們讓原型對(duì)象等于另一個(gè)類型的實(shí)例,顯然,此時(shí)的原型對(duì)象將包含一個(gè)指向另一個(gè)原型的指針,相應(yīng)地,另一個(gè)原型中也包含著指向另一個(gè)構(gòu)造函數(shù)的指針。假如另一個(gè)原型又是另一個(gè)類型的實(shí)例,那么上述關(guān)系依然成立,如此層層遞進(jìn),就構(gòu)成了實(shí)例與原型的鏈條。這就是所謂原型鏈的基本概念

實(shí)現(xiàn)原型鏈有一種基本模式

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默認(rèn)使用的原型,而是給它換了個(gè)新原型,SuperType的實(shí)例。于是新原型不僅具有作為一個(gè)SuperType的實(shí)例所擁有的全部屬性和方法,而且其內(nèi)部還有一個(gè)指針,指向了SuperType的原型。

最終:

instance指向SubType的原型

SubType的原型又指向SuperType的原型

getSuperValue() 方法仍然還在SuperType.prototype中,但property則位于SubType.prototype中。這是因?yàn)閜roperty是一個(gè)實(shí)例屬性,而getSuperValue()則是一個(gè)原型方法。 既然 SubType.prototype 現(xiàn)在是SuperType的實(shí)例,那么property當(dāng)然就位于該實(shí)例中了。

此外,要注意instance.constructor 現(xiàn)在指向的是SuperType,這是因?yàn)樵瓉鞸ubType.prototype 中的 constructor被重寫了的緣故

通過實(shí)現(xiàn)原型鏈,本質(zhì)上拓展了原型搜索機(jī)制。當(dāng)讀取模式訪問一個(gè)實(shí)例屬性時(shí),首先會(huì)在實(shí)例中搜索該屬性。如果沒有找到該屬性,則會(huì)繼續(xù)搜索實(shí)例的原型。在通過原型鏈實(shí)現(xiàn)繼承的情況下,搜索過程就得以沿著原型鏈繼續(xù)向上。調(diào)用instance.getSuperValue()會(huì)經(jīng)歷三個(gè)搜索步驟

搜索實(shí)例

搜索SubType.prototype

搜索SuperType.prototype 最終找到方法

別忘記默認(rèn)的原型

事實(shí)上,前述例子的原型鏈少了一環(huán)。所有引用類型默認(rèn)都繼承了Object,而這個(gè)繼承也是通過原型鏈實(shí)現(xiàn)的。所有函數(shù)的默認(rèn)原型都是Object的實(shí)例,因?yàn)槟J(rèn)原型都會(huì)包含一個(gè)內(nèi)部指針,指向Object.prototype。這也正是所有自定義類型都會(huì)繼承toString(), valueOf()等默認(rèn)方法的根本原因。

SubType繼承了SuperType,而 SuperType繼承了Object。當(dāng)調(diào)用instance.toString()方法,實(shí)際上調(diào)用的是保存在Object.prototype中的那個(gè)方法

確定原型和實(shí)例的關(guān)系

第一種方式,使用instanceof操作符,只要用這個(gè)操作符來測(cè)試實(shí)例與原型鏈中出現(xiàn)過的構(gòu)造函數(shù),結(jié)果就會(huì)返回true。

// 由于原型鏈,我們可以是instance是Object,SuperType, SubType中任何一個(gè)類型的實(shí)例
console.log(instance instanceof Object);         // true
console.log(instance instanceof SuperType);      // true
console.log(instance instanceof SubType);        // true

第二種方式,使用isPrototypeOf()方法。同樣,只要是原型鏈中出現(xiàn)過的原型,都可以說是該原型鏈所派生的實(shí)例的原型。

console.log(Object.prototype.isPrototypeOf(instance));         // true
console.log(SuperType.prototype.isPrototypeOf(instance));      // true
console.log(SubType.prototype.isPrototypeOf(instance));        // true
謹(jǐn)慎的定義方法

子類型有時(shí)候需要覆蓋超類型中的一個(gè)方法,或者需要添加超類型中不存在的某個(gè)方法。但不管怎么樣,給原型鏈添加方法的代碼一定要放在替換原型的語句之后

function SuperType() {
  this.property = true;
}

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

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

// 繼承了SuperType,原來默認(rèn)的原型被替換
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

還有一點(diǎn)需要提醒讀者,即在通過原型鏈實(shí)現(xiàn)繼承時(shí),不能使用對(duì)象字面量創(chuàng)建原型方法。因?yàn)檫@樣就會(huì)重寫原型鏈

function SuperType() {
  this.property = true;
}

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

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

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

// 使用字面量添加新方法,會(huì)導(dǎo)致上一行代碼無效
// 原型鏈被切斷——SubType 和 SuperType 之間已經(jīng)沒有關(guān)系
SubType.prototype = {
  getSubValue: function () {
    return this.subproperty;
  },

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

var instance = new SubType();
console.log(instance.getSuperValue()));    // error
原型鏈的問題

最主要的問題,來自包含引用類型值的原型。包含引用類型值的原型屬性,會(huì)被所有實(shí)例共享;所以要在構(gòu)造函數(shù)中定義值而不是原型對(duì)象。在通過原型來實(shí)現(xiàn)繼承時(shí),原型實(shí)際上會(huì)變成另一個(gè)類型的實(shí)例。于是,原先的 實(shí)例屬性也就順理成章的變成了 現(xiàn)在的原型屬性了。

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

function SubType() {
}

// SubType 繼承了 SuperType 之后
// SubType.prototype 就變成了 SuperType的一個(gè)實(shí)例
// 因此 SubType.prototype 也擁有了自己的colors屬性 等價(jià)于創(chuàng)建了一個(gè)SubType.prototype.colors
SubType.protype = new SuperType();

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

// 結(jié)果就是所有SubType實(shí)例都會(huì)共享這個(gè)colors屬性
var instance2 = new SubType();
console.log(instance2.colors);                // "red,blue,green,black"

原型鏈的第二個(gè)問題:在創(chuàng)建子類型的實(shí)力時(shí),不能向超類型的構(gòu)造函數(shù)中傳遞參數(shù)。準(zhǔn)確的說是沒有辦法在不影響所有對(duì)象實(shí)例的情況下,給超類型的構(gòu)造函數(shù)傳遞參數(shù)。

有鑒于此,實(shí)踐中很少會(huì)多帶帶使用原型鏈

借用構(gòu)造函數(shù)

在解決原型中包含引用類型值所帶來的問題過程中,開發(fā)人員開始使用一種叫做 借用構(gòu)造函數(shù)(constructor stealing) 的技術(shù)(有時(shí)候也叫偽造對(duì)象或經(jīng)典繼承)。

思想相當(dāng)簡單,即在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類型構(gòu)造函數(shù)。函數(shù)只不過是在特定環(huán)境中執(zhí)行代碼的對(duì)象,因此通過使用apply() call()方法也可以在(將來)新創(chuàng)建對(duì)象上執(zhí)行構(gòu)造函數(shù)。實(shí)際上是在(未來將要)新創(chuàng)建的SubType實(shí)例環(huán)境下,調(diào)用了SuperType構(gòu)造函數(shù),就會(huì)在新 SubType 對(duì)象上執(zhí)行 SuperType() 函數(shù)中定義的所有對(duì)象初始化代碼。結(jié)果每個(gè)SubType的實(shí)例都會(huì)具有自己的colors屬性的副本了。

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

function SubType() {
  // 繼承了SuperType
  // "借調(diào)“了超類型的構(gòu)造函數(shù)
  SuperType.call(this);
}

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

// SubType實(shí)例都不會(huì)共享這個(gè)colors屬性
var instance2 = new SubType();
console.log(instance2.colors);                // "red,blue,green"
傳遞參數(shù)

相對(duì)于原型鏈而言,借用構(gòu)造函數(shù)有一個(gè)很大的優(yōu)勢(shì),即可以在子類型構(gòu)造函數(shù)中向超類型構(gòu)造函數(shù)傳遞參數(shù)。

為了確保SuperType 構(gòu)造函數(shù)不會(huì)重寫子類型的屬性,可以在調(diào)用超類型構(gòu)造函數(shù)后,再添加應(yīng)該在子類型中定義的屬性。

function SuperType(name) {
  this.name = name;
}

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

  // 實(shí)例屬性
  this.age = 29;
}

var instance = new SubType();
console.log(instance.anme);             // "Nicholas"
console.log(instance.age);              // 29
借用構(gòu)造函數(shù)的問題

僅僅是借用構(gòu)造函數(shù),也將無法避免構(gòu)造函數(shù)模式存在的問題——方法都在構(gòu)造函數(shù)中定義,因此函數(shù)復(fù)用就無從談起了。

在超類型的原型中定義方法,對(duì)子類型而言也是不可見的,結(jié)果所有類型都只能使用構(gòu)造函數(shù)模式。

有鑒于此,借用構(gòu)造函數(shù)也是很少多帶帶使用的

組合繼承

組合繼承(combination inheritance),有時(shí)候也叫作偽經(jīng)典繼承,是將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合到一起,從而發(fā)揮二者之長。其背后的思路是使用原型鏈實(shí)現(xiàn)對(duì)原型屬性和方法的繼承,而通過借用構(gòu)造函數(shù)來實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承。既通過在原型上定義方法實(shí)現(xiàn)了函數(shù)復(fù)用,又能保證每個(gè)實(shí)例都有它自己的屬性

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

組合繼承避免了原型鏈和借用構(gòu)造函數(shù)的缺陷,融合了它們的優(yōu)點(diǎn),成為JavaScript中最常用的繼承模式。而且instanceof 和 isPrototypeOf() 也能夠用于識(shí)別基于組合繼承創(chuàng)建的對(duì)象。

原型式繼承

道格拉斯·克羅克福德2006年在文章中介紹了一種實(shí)現(xiàn)繼承的方法,這種方法并沒有使用嚴(yán)格意義上的構(gòu)造函數(shù)。他的想法是借助原型可以基于已有對(duì)象創(chuàng)建新對(duì)象,同時(shí)還不必因此創(chuàng)建自定義類型。為達(dá)到這個(gè)目的,給出了如下函數(shù)

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

在object() 函數(shù)內(nèi)部,先創(chuàng)建了一個(gè)臨時(shí)性的構(gòu)造函數(shù),然后將傳入的對(duì)象作為這個(gè)構(gòu)造函數(shù)的原型,最后返回了這個(gè)臨時(shí)類型的新實(shí)例。從本質(zhì)上講,object() 對(duì)傳入其中的對(duì)象執(zhí)行了一次淺復(fù)制

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"

這種原型式繼承,要求你必須有一個(gè)對(duì)象可以作為另一個(gè)對(duì)象的基礎(chǔ)。把它傳遞給object()函數(shù),然后再根據(jù)具體需求對(duì)得到的對(duì)象加以修改即可。這意味著,person.friends不僅屬于person,而且也會(huì)被anotherPerson, yetAnotherPerson共享。實(shí)際上就相當(dāng)于又創(chuàng)建了person對(duì)象的兩個(gè)副本(淺拷貝)。

ECMAScript 5 通過新增Object.create() 方法規(guī)范化了原型式繼承。這個(gè)方法接受兩個(gè)參數(shù):一個(gè)用作新對(duì)象原型的對(duì)象和(可選)一個(gè)新對(duì)象定義額外的屬性的對(duì)象。在傳入一個(gè)參數(shù)的情況下,Object.create() 與 Object()方法的行為相同(原著如此表述,但實(shí)際兩者并不相同,參照第五章 Object類型 的相關(guān)補(bǔ)充說明

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() 方法的第二個(gè)參數(shù)與Object.defineProperties() 方法的第二個(gè)參數(shù)格式相同:每個(gè)屬性都是通過自己的描述符定義的。以這種方式制定的任何屬性都會(huì)覆蓋對(duì)象上同名的屬性。

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

在沒有必要興師動(dòng)眾的創(chuàng)建構(gòu)造函數(shù),而只想讓一個(gè)對(duì)象與另一個(gè)對(duì)象保持類似的情況下,原型式繼承是完全可以勝任的。但別忘了,包含引用類型值(如上面的friends屬性是一個(gè)數(shù)組)始終都會(huì)共享相應(yīng)的值,就像使用原型模式一樣。

寄生式繼承

寄生式繼承(parasitic)是與原型式繼承緊密相關(guān)的一種思路,并且同樣也是由克羅克福德推而廣之的。

思路與構(gòu)造函數(shù)和工廠模式類似,既創(chuàng)建了一個(gè)僅用于封裝繼承過程的函數(shù),該函數(shù)在內(nèi)部以某種方式來增強(qiáng)對(duì)象,最后再像真的是它做了所有工作一樣返回對(duì)象。

function createAnother(original) {
  var clone = object(original);         // 通過調(diào)用函數(shù)創(chuàng)建一個(gè)新對(duì)象
  clone.sayHi = function() {            // 以某種方式增強(qiáng)這個(gè)對(duì)象
    console.log("hi");
  };
  return clone;                         // 返回這個(gè)對(duì)象
}
寄生組合式繼承

組合繼承是最常用的繼承模式;不過最大的問題就是無論什么情況下,都會(huì)調(diào)用兩次超類型構(gòu)造函數(shù):

一次是在創(chuàng)建子類型原型的時(shí)候

另一次是在子類型構(gòu)造函數(shù)內(nèi)部

也就是說,子類型最終會(huì)包含超類型對(duì)象的全部實(shí)例屬性,但我們不得不在調(diào)用子類型構(gòu)造函數(shù)時(shí),重寫這些屬性。

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);               // 第二次調(diào)用SuperType()

  this.age = age;
}

// 實(shí)例化SuperType作為SubType的原型
// 立即觸發(fā)
SubType.prototype = new SuperType();        // 第一次調(diào)用SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
  console.log(this.age);
};

// 此時(shí)觸發(fā)第二次調(diào)用
var instance = new SubType("Nicholas", 29);

console.log(instance.name);                // "Nicholas"
console.log(SubType.prototype.name);       // undefined

在第一次調(diào)用SuperType構(gòu)造函數(shù)時(shí),SubType.prototype會(huì)得到兩個(gè)屬性:name和colors;它們都是SuperType的實(shí)例屬性,只不過現(xiàn)在位于SubType的原型中。當(dāng)調(diào)用SubType構(gòu)造函數(shù)時(shí),又會(huì)調(diào)用一次 SuperType的構(gòu)造函數(shù),這一次又在新對(duì)象上創(chuàng)建了實(shí)例屬性name和colors屬性。于是這兩個(gè)屬性就屏蔽了原型中的兩個(gè)同名屬性。(圖6-6)

有兩組name和colors屬性,一組在實(shí)例instance上,一組在SubType的原型中。這就是調(diào)用兩次SuperType構(gòu)造函數(shù)的結(jié)果

所謂寄生組合式繼承,即通過借用構(gòu)造函數(shù)來繼承屬性,通過原型鏈的混成形式來繼承方法。其背后的思路是:不必為了指定子類型的原型而調(diào)用超類型的構(gòu)造函數(shù),我們所需要 的無非就是超類型原型的一個(gè)副本。本質(zhì)上,就是使用寄生式繼承來繼承超類型的原型,然后再將結(jié)果指定給子類型的原型

function inheritPrototype(subType, superType) {

  // 創(chuàng)建對(duì)象 - 超類型的對(duì)象原型的副本
  // 這里沒有使用new操作符,所以沒有生成SuperType的實(shí)例
  // 這里沒有調(diào)用SuperType構(gòu)造函數(shù)
  var prototype = Object(superType.prototype)
  console.log(prototype == superType.prototype)

  // 增強(qiáng)對(duì)象 - 彌補(bǔ)因重寫原型而失去的默認(rèn)constructor屬性
  // 而這也將導(dǎo)致supert.prototype.constructor指向了subType
  prototype.constructor = subType;

  // 指定對(duì)象 - 將新創(chuàng)建的對(duì)象(即副本)賦值給子類型的原型。
  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() 函數(shù)實(shí)現(xiàn)了寄生組合式繼承的最簡單形式。這個(gè)函數(shù)接受兩個(gè)參數(shù): 子類型構(gòu)造函數(shù)和超類型構(gòu)造函數(shù)。

這樣就只調(diào)用了一次SuperType構(gòu)造函數(shù),并且因此便了SubType.prototype 上創(chuàng)建不必要的、多余的屬性。與此同時(shí),原型鏈還能保持不變;因此,還能夠正常使用instanceof 和 isPrototypeOf().

開發(fā)人員普遍認(rèn)為寄生組合式繼承是引用類型最理想的繼承范式。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/109479.html

相關(guān)文章

  • JavaScript高級(jí)程序設(shè)計(jì)》(3讀書筆記 1~2

    摘要:表示應(yīng)該立即下載腳本,但不應(yīng)妨礙頁面中的其他操作可選。表示通過屬性指定的代碼的字符集。表示腳本可以延遲到文檔完全被解析和顯示之后再執(zhí)行。實(shí)際上,服務(wù)器在傳送文件時(shí)使用的類型通常是,但在中設(shè)置這個(gè)值卻可能導(dǎo)致腳本被忽略。 第1章 JavaScript 簡介 雖然JavaScript和ECMAScript通常被人們用來表達(dá)相同的含義,但JavaScript的含義比ECMA-262要多得多...

    Corwien 評(píng)論0 收藏0
  • JavaScript高級(jí)程序設(shè)計(jì)》(3讀書筆記 4 變量、作用域和內(nèi)存問題

    摘要:具體說就是執(zhí)行流進(jìn)入下列任何一個(gè)語句時(shí),作用域鏈就會(huì)得到加長語句的塊。如果局部環(huán)境中存在著同名的標(biāo)識(shí)符,就不會(huì)使用位于父環(huán)境中的標(biāo)識(shí)符訪問局部變量要比訪問全局變量更快,因?yàn)椴挥孟蛏纤阉髯饔糜蜴湣? 基本類型和引用類型的值 ECMAscript變量包含 基本類型值和引用類型值 基本類型值值的是基本數(shù)據(jù)類型:Undefined, Null, Boolean, Number, String ...

    lidashuang 評(píng)論0 收藏0
  • JavaScript高級(jí)程序設(shè)計(jì)》(3讀書筆記 7 函數(shù)表達(dá)式

    摘要:定義函數(shù)表達(dá)式的方式有兩種函數(shù)聲明。不過,這并不是匿名函數(shù)唯一的用途。可以使用命名函數(shù)表達(dá)式來達(dá)成相同的結(jié)果閉包匿名函數(shù)和閉包是兩個(gè)概念,容易混淆。匿名函數(shù)的執(zhí)行環(huán)境具有全局性,因此其對(duì)象通常指向通過改變函數(shù)的執(zhí)行環(huán)境的情況除外。 定義函數(shù)表達(dá)式的方式有兩種: 函數(shù)聲明。它的重要特征就是 函數(shù)聲明提升(function declaration hoisting) 即在執(zhí)行代碼之前會(huì)...

    鄒立鵬 評(píng)論0 收藏0
  • JavaScript高級(jí)程序設(shè)計(jì)》(3讀書筆記 5 引用類型

    摘要:引用類型的值對(duì)象是引用類型的一個(gè)實(shí)例。引用類型是一種數(shù)據(jù)結(jié)構(gòu),用于將數(shù)據(jù)和功能組織在一起。對(duì)數(shù)組中的每一項(xiàng)運(yùn)行給定函數(shù),如果該函數(shù)對(duì)任一項(xiàng)返回,則返回。組零始終代表整個(gè)表達(dá)式。所以,使用非捕獲組較使用捕獲組更節(jié)省內(nèi)存。 引用類型的值(對(duì)象)是引用類型的一個(gè)實(shí)例。 引用類型是一種數(shù)據(jù)結(jié)構(gòu),用于將數(shù)據(jù)和功能組織在一起。它同行被稱為類,但這種稱呼并不妥當(dāng),盡管ECMAScript從技術(shù)上講...

    zero 評(píng)論0 收藏0
  • Ajax與Comet-JavaScript高級(jí)程序設(shè)計(jì)21讀書筆記(1)

    摘要:技術(shù)的核心是對(duì)象即。收到響應(yīng)后,響應(yīng)的數(shù)據(jù)會(huì)自動(dòng)填充對(duì)象的屬性,相關(guān)的屬性有作為響應(yīng)主體被返回的文本。收到響應(yīng)后,一般來說,會(huì)先判斷是否為,這是此次請(qǐng)求成功的標(biāo)志。中的版本會(huì)將設(shè)置為,而中原生的則會(huì)將規(guī)范化為。會(huì)在取得時(shí)報(bào)告的值為。 Ajax(Asynchronous Javascript + XML)技術(shù)的核心是XMLHttpRequest對(duì)象,即: XHR。雖然名字中包含XML,但...

    imingyu 評(píng)論0 收藏0
  • JavaScript高級(jí)程序設(shè)計(jì)》(3讀書筆記 9 客戶端檢測(cè)

    摘要:用戶代理檢測(cè)用戶代理檢測(cè)是爭議最大的客戶端檢測(cè)技術(shù)。第二個(gè)要檢測(cè)是。由于實(shí)際的版本號(hào)可能會(huì)包含數(shù)字小數(shù)點(diǎn)和字母,所以捕獲組中使用了表示非空格的特殊字符。版本號(hào)不在后面,而是在后面。除了知道設(shè)備,最好還能知道的版本號(hào)。 檢測(cè)Web客戶端的手段很多,各有利弊,但不到萬不得已就不要使用客戶端檢測(cè)。只要能找到更通用的方法,就應(yīng)該優(yōu)先采用更通用的方法。一言蔽之,先設(shè)計(jì)最通用的方案,然后再使用特定...

    ispring 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<