紅寶書筆記-第6章-面向對象的程序設計

本章內容數組

  • 理解對象屬性
  • 理解並建立對象
  • 理解繼承

ECMA-262 把對象定義爲:「無序屬性的集合,其屬性能夠包含基本值、對象或者函數。」嚴格來說,這就至關於說對象是一組沒有特定順序的值。app

每一個對象都是基於一個引用類型建立的,既能夠是原生類型,也能夠是開發人員定義的類型。函數

6.1 理解對象

建立對象最簡單的方式就是建立一個 Object 的實例,而後爲它添加屬性和方法。測試

var person = {
    name: "Jack",
    age: 29,
    sayName: function() {
        alert(this.name);
    }
}

這些屬性在建立時都帶有一些特徵值(characteristic),JS 經過這些特徵值來定義它們的行爲。this

6.1.1 屬性類型

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

1. 數據屬性

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

  • [[Configurable]]
  • [[Enumerable]]
  • [[Writable]]
  • [[Value]]

對於直接在對象上定義的屬性,它們的 [[Configurable]]、[[Enumerable]]、[[Writable]] 特性都被設置爲 true,而 [[Value]] 特性被設置爲指定的值。code

要修改屬性默認的特性,必須使用 ECMAScript 5 的 Object.defineProperty() 方法。接收3個參數:屬性所在的對象、屬性名字、一個描述符對象。其中描述符(descriptor)對象的屬性必須是:configurable/enumerable/writable/value。設置其中的一個或多個值,能夠修改對應的特性值。對象

var person = {}
Object.defineProperty(person, "name", {
    writable: false,
    configurable: false,
    value: "Nick"
})
alert(person.name); // Nick
person.name = Jack;
alert(person.name); // Nick
delete person.name;
alert(person.name); // Nick

注意:一旦把屬性定義爲不可配置的,就不能再把它變回可配置了。也就是說,能夠屢次調用 Object.defineProperty()方法修改同一個屬性,但在把 configurable 設置爲 false 後,就不能了。繼承

在調用 Object.defineProperty() 時,若是不指定,則 configurable/writable/enumerable 都爲 false。

2. 訪問器屬性

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

  • [[Configurable]]
  • [[[Enumerable]]
  • [[Get]]:在讀取屬性時調用的函數,默認值 undefined。
  • [[Set]]:在寫入屬性時調用的函數,默認值 undefined。

訪問器屬性不能直接定義,必須使用 Object.defineProperty() 。

var book = {
    _year: 2004,
    edition: 1
};

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

book.year = 2005;
alert(book.edition); // 2

下劃線是一種經常使用的記號,用於表示只能經過對象方法來訪問的屬性。
以上是使用訪問器屬性的常見方式,即設置一個屬性的值會致使其餘屬性的變化。
不必定要同時指定 getter 和 setter。只指定 getter 表示屬性是不能寫,反之則表示屬性不能讀。

6.1.2 定義多個屬性

Object.defineProperties() 能夠經過描述符一次性定義多個屬性。接收2個參數:一、第一個對象是要添加和修改其屬性的對象;二、第二個對象的屬性與第一個對象中要添加或修改的屬性一一對應。

var book = {};
Object.defineProperties(book, { // IE9+
 _year: {
     writable: true,
     value: 2004
 },
 edition: {
     writable: true,
     value: 1
 },
 year: {
     get: function() {
         return this._year;
     },
     set: function(newValue) {
         this._year = newValue;
         this.edition++;
     }
 }
})

6.1.3 讀取屬性的特性

使用 ECMAScript 5 中的 Object.getOwnPropertyDescriptor() IE9+ 方法,能夠取得給定屬性的描述符。這個方法接收兩個參數:一、屬性所在的對象;二、要讀取器描述符的屬性名稱。返回值是一個對象,若是是數據屬性,這個對象的屬性有 configurable/enumerable/writable/value,若是是訪問器屬性,則這個對象的屬性有 configurable/enumerable/get/set

// 使用前面的例子
var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
alert(descriptor.value); // 2004
alert(descriptor.configurable); //false
alert(typeof descriptor.get); //"undefined"

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

6.2 建立對象

問題:使用同一個接口建立不少對象,會產生大量的重複代碼。

6.2.1 工廠模式

工廠模式抽象了建立具體對象的過程。用函數來封裝以特定接口建立對象的細節。

function createPerson(name, age) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.sayName = function() {
        alert(this.name);
    };
    return o;
}

var person1 = createPerson("Jack", 29);
var person2 = createPerson("Nick", 22);

工廠模式雖然解決了建立多個類似對象的問題,可是沒有解決對象識別的問題(即怎樣知道一個對象的類型)。

6.2.2 構造函數模式

能夠建立自定義的構造函數,從而定義自定義對象類型的屬性和方法。

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayName = function() {
        alert(this.name);
    }
}
var person1 = new Person("Jack", 23);
var person2 = new Person("Nick", 22);

構造函數模式有如下幾個特色:

  • 沒有顯示地建立對象;
  • 直接將屬性和方法賦給了this對象;
  • 沒有return語句。
  • 函數名開頭必須大寫。
  • 構造函數自己也是函數,只不過能夠用來建立對象而已。

要建立 Person 的新實例,必須使用 new 操做符。以這種方式調用構造函數實際上會經歷如下4個過程:

  • 建立一個新對象;
  • 將構造函數的做用域賦給新對象(所以 this 就指向了這個新對象);
  • 執行構造函數中的代碼(爲這個新對象添加屬性);
  • 返回新對象。

使用 instanceof 檢測對象類型:

alert(person1 instanceof Object); // true
alert(person1 instanceof Person); // true
alert(person2 instanceof Object); // true
alert(person2 instanceof Person); // true

建立自定義的構造函數意味着未來能夠將它的實例標識爲一種特定的類型;而這正是構造函數模式賽過工廠模式的地方。

1. 將構造函數當作函數

// 當作構造函數來使用
var person = new Person("Nick", 29);
person.sayName(); // "Nick"

// 當作普通函數調用
Person("Nick", 29); // 添加到 window 對象
window.sayName(); // "Nick"

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

2. 構造函數的問題

每一個方法都要在每一個實例上從新建立一遍。在前面的例子中,person1 和 person2 的 sayName() 方法並非同一個 Function 的實例。由於函數是對象,因此每定義一個函數,也就實例化了一個對象。(new Function())。

解決的辦法,能夠把函數定義移到構造函數外部。

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayName = sayName;
}

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

var person1 = new Person("Jack", 23);
var person2 = new Person("Nick", 22);

但新問題是:在全局做用域定義的函數實際上只能被某個對象調用,這讓全局做用域名存實亡。並且,若是對象須要定義不少方法,那麼就要定義多個全局函數,因而這個自定義的引用類型就沒有絲毫封裝性可言。

6.2.3 原型模式

每一個函數都有一個 prototype(原型)屬性,這個屬性是一個指針,指向一個對象,這個對象的用途是包含能夠由特定類型的全部實例共享的屬性和方法。

也能夠說 prototype 就是經過調用構造函數而建立的對象實例的原型對象。使用原型對象的好處是可讓全部「對象實例」共享「原型對象」所包含的屬性和方法。

6. 原型對象的問題

  1. 它省略了爲構造函數傳遞初始化參數這一環節,結果全部實例在默認狀況下都將取得相同的屬性值。
  2. 原型模式的最大問題是它的共享的本性所致使的。這個問題在包含引用類型值的屬性上顯而易見。
function Person() {}

Person.prototype = {
    constructor: Person,
    friends: ["Jack"]
};

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

person1.friends.push("Nick");

alert(person1.friends); // "Jack, Nick"
alert(person2.friends); // "Jack, Nick"
alert(person1.friends === person2.friends); // true

實例通常都是要有本身的所有屬性的,然而因爲 person1.friends 和 person2.friends 都指向同一個數組,致使修改其中一個,就會在另外一個上同步共享。

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

構造函數模式用於定義實例屬性,原型模式用於定義方法和共享的屬性。

  • 每一個實例都會擁有本身的一份實例屬性的副本,但同時又共享着對「方法」的引用,最大限度地節約了內存。
  • 這種混成模式還支持向構造函數傳遞參數。
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.friends = ["Jack"];
}

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

var person1 = new Person("Nick", 22);
var person2 = new Person("Mike", 21);

person1.friends.push("Jane");

alert(person1.friends); // "Jack, Jane"
alert(person2.friends); // "Jack"
alert(person1.friends === person2.friends); // false
alert(person1.sayName === person2.sayName); // true

混成模式中,不一樣實例引用了不一樣的數組,所以原型對象的問題解決了。

6.2.5 動態原型模式

function Person(name, age) {
    // 屬性
    this.name = name;
    this.age = age;
    
    // 方法
    if (typeof this.sayName != "function") {
        Person.prototype.sayName: function() {
            alert(this.name);
        }
    }
}

var person1 = new Person("Nick", 22);
person1.sayName();

if 語句檢查的能夠是初始化以後應該存在的任何屬性或方法——沒必要用一大堆 if 語句檢查每一個屬性和每一個方法,只要檢查其中一個便可。

對於採用這種模式建立的對象,可使用 instanceof 操做符肯定它的類型。

使用動態原型模式時,不能使用對象字面量重寫原型,若是重寫,則會切斷現有實例與新原型之間的聯繫。

6.2.6 寄生構造函數模式

6.2.7 穩妥構造函數模式

6.3 繼承

因爲函數沒有簽名,在ECMAScript 中沒法實現【接口繼承】。ECMAScript 只支持【實現繼承】,並且其實現繼承主要依靠【原型鏈】來實現。

6.3.1 原型鏈

基本思想

利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法。

構造函數、原型、實例之間的關係:

  • 每一個構造函數都有一個原型對象;
  • 原型對象都有一個指向構造函數的指針;
  • 實例都包含一個指向原型對象的內部指針[[Prototype]]

實現原型鏈的基本模式:

function A() {
    this.aproperty = true;
}

A.prototype.getAValue = function() {
    return this.property;
};

function B() {
    this.bproperty = false;
}

// 繼承了 A,建立了 B 的實例,並將實例賦給 B.prototype
B.prototype = new A();

B.prototype.getBValue = function() {
    return this.bproperty;
}

var instance = new B();
alert(instance.getAValue); // true

實現的本質是重寫原型對象,代之以一個新實例的類型。原來存在於 A 的實例中的全部屬性和方法,如今也存在於 B.prototype 中。

1. 默認原型

全部應用類型默認都繼承了 Object,而這個繼承也是經過原型鏈實現的。全部函數的默認原型都是 Object 的實例,所以默認原型都會包含一個內部指針,指向 Object.prototype。這也是全部自定義類型都會繼承 toString()valueOf() 等默認方法的根本緣由。

2. 肯定原型和實例的關係

能夠經過兩種方式來肯定原型和實例之間的關係。
方法一:instanceof,只要用這個操做符來測試實例和原型鏈中出現過的構造函數,結果就會返回 true。

alert(instance instanceof Object); // true
alert(instance instanceof A); // true
alert(isntance instanceof B); // true

因爲原型鏈的關係,instance 是 Object、A、B 中任何一個類型的實例。

方法二:isPropertyOf,只要是原型鏈中出現過的原型,均可以說是該原型鏈所派生的實例的原型,所以該方法也會返回 true。

alert(Object.prototype.isPropertyOf(instance)); // true

3. 謹慎地定義方法

子類型有時候須要覆蓋超類型中的某個方法,或者須要添加超類型中不存在的某個方法。給原型添加方法的代碼必定要放在替換原型的語句以後。

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

A.prototype.getAValue = function() {
    return this.property;
};

function B() {
    this.bproperty = false;
}

// 繼承了 A
B.prototype = new A();

// 添加新方法
B.prototype.getBValue = function() {
    return this.bproperty;
}

// 重寫超類型方法
B.prototype.getAValue = function() {
    return false;
}

注意,經過 A 的實例調用 getAValue() 方法時,仍然繼續調用原來的方法。

在經過原型鏈實現繼承時,不能使用對象字面量建立原型方法。由於這樣作會重寫原型鏈。

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

A.prototype.getAValue = function() {
    return this.property;
};

function B() {
    this.bproperty = false;
}

// 繼承了 A
B.prototype = new A();

// 添加新方法
B.prototype = {
  getBValue: function() {
      return this.bproperty;
  }
};

var instance = new B();
alert(instance.getAValue); // error!

4. 原型鏈的問題

  1. 最主要的問題來自包含引用類型值的原型。包含引用類型值的原型屬性會被全部實例共享;而這也正是爲何要在構造函數中,而不是在原型對象中定義屬性的緣由。在經過原型來實現繼承時,原型實際上會變成另外一個類型的實例。因而,原先的實例屬性也就瓜熟蒂落地變成了如今的原型屬性了。
  2. 在建立子類型的實例時,沒有辦法在不影響全部對象實例的狀況下,不能向超類型的構造函數中傳遞參數。所以,實踐中不多會單獨使用原型鏈

6.3.2 借用構造函數(constructor stealing)

又叫「僞造對象」或「經典繼承」。
基本思想
在子類型構造函數的內部調用超類型構造函數。函數只不過是在特定環境中執行代碼的對象,所以經過使用 apply() 和 call() 也能夠在(未來)新建立的對象上執行構造函數。

function A() {
    this.colors = ["red"];
}

function B() {
    // 繼承了 A
    A.call(this);
}

var instance1 = new B();
instance1.colors.push("blue");
alert(instance1.colors); // "red, blue"

var instance2 = new B();
alert(instance2.colors); // "red"

1. 傳遞參數

相對於原型鏈而言,借用構造函數有一個很大的有時,能夠在子類型構造函數中向超類型構造函數傳遞參數。

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

function B() {
    // 繼承了 A
    A.call(this, "Jack");
}

var instance1 = new B();
alert(instance1.name); // "Jack"

爲了確保 A 構造函數不會重寫子類型的屬性,能夠在調用超類型構造函數後,再添加應該在子類型中定義的屬性。

2. 借用構造函數的問題

  1. 方法都在構造函數中定義,所以函數複用就無從談起;
  2. 在超類型的原型中定義的方法,對子類型而言也是不可見的,結果全部類型都只能使用構造函數模式。所以借用構造函數也不多單獨使用

6.3.3 組合繼承(combination inheritance)

又叫「僞經典繼承」,組合了原型鏈繼承和借用構造函數繼承。既經過在原型上定義方法實現了函數服用,又能保證每一個實例都擁有本身的屬性。

function A(name) {
    this.name = name;
    this.colors = ["red"];
}

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

function B(name, age) {
    // 繼承屬性
    A.call(this, name); // 第二次調用 A
    this.age = age;
}

// 繼承方法
B.prototype = new A(); // 第一次調用 A
B.prototype.constructor = B;
B.prototype.sayAge = function() {
    alert(this.age);
}

var instance1 = new B("Jack", 22);
instance1.colors.push("blue");
alert(instance1.colors); // "red, blue"
instance1.sayName(); // "Jack"
instance1.sayAge(); // 22

var instance2 = new B("Nick", 21);
alert(instance2.colors); // "red"
instance2.sayName(); // "Nick"
instance2.sayAge(); // 21

組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優勢,成爲JS中最經常使用的繼承模式。並且,instanceof 和 isPropertyOf() 也可以用於識別基於組合繼承建立的對象。

組合模式的問題

不管什麼狀況下,都會調用兩次超類型構造函數:一次是在建立子類型原型的時候,一次是在子類型構造函數內部。

6.3.4 原型式繼承

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

原型式繼承要求必須有一個對象做爲另外一個對象的基礎。

ECMAScript 5 中新增了 Object.create() 來規範原型式繼承。接收2個參數:一、一個用作新對象原型的對象;二、(可選)一個爲新對象定義額外屬性的對象。在傳入一個參數的狀況下,Object.create() 和 object() 的行爲相同。

var person = {};
var anotherPerson = Object.create(person);

若是隻想讓一個對象與另外一個對象保持相似的狀況下,原型式繼承徹底能夠勝任。可是包含引用類型值的屬性始終都會共享相應的值,這點與原型模式同樣。

6.3.5 寄生式繼承(parasitic)

它的思路與寄生構造函數和工廠模式類似,即建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後再像真地是它作了全部工做同樣返回對象。

function createAnother(original) {
    var clone = object(original); // 經過調用函數建立一個新對象
    clone.sayHi = function() { // 以某種方式加強對象
        alert("Hi");
    };
    return clone; // 返回對象
}

var person = {
    name: "Jack",
    friends: ["Nick", "Tony"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "Hi"

新對象不只具備 person 的全部屬性和方法,還有本身的方法。

在主要考慮「對象」而不是「自定義類型」和「構造函數」的狀況下,寄生式繼承也是一種有用的模式。object() 並非必需的;任何可以返回新對象的函數都適用於該模式。

注意:使用寄生式繼承來爲對象添加函數,會因爲不能作到函數複用而下降效率;這一點與構造函數模式相似。

6.3.6 寄生組合式繼承

本質上,就是使用「寄生式繼承」來繼承超類型的原型,再將結果指定給子類型的原型。

function inheritPrototype(sub, super) {
    var prototype = Object(super); // 建立對象
    prototype.constructor = sub; // 加強對象
    sub.prototype = prototype; // 指定對象
}
  1. 建立超類型原型的一個副本;
  2. 爲建立的副本添加 constructor 屬性,從而彌補因【重寫原型】而失去的默認的 constructor 屬性;
  3. 將新建立的對象(即副本)賦值給子類型的原型。

修改以前的例子:

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

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

function B(age) {
    A.call(this, "Jack");
    this.age = age;
}

inheritPrototype(B,A);

B.prototype.sayAge = function() {
    alert(this.age);
}

該模式的高效率體如今它只調用了一次 A 構造函數,而且所以避免了在 B 的 prototype 上面建立沒必要要的、多餘的屬性。與此同時,原型鏈還能保持不變;所以,還可以正常使用 instanceof 和 isPrototypeOf() 方法。開發人員廣泛認爲寄生組合式繼承是引用類型最理想的繼承範式。

6.4 小結

ECMAScript 支持面向對象(OO)變成,但不使用類或者接口。對象能夠在代碼執行過程當中建立和加強,所以具備動態性而非嚴格定義的實體。在沒有類的狀況下,能夠採用下列模式建立對象:

  • 工廠模式,使用簡單的函數建立對象,爲對象添加屬性和方法,而後返回對象。這個模式後來被構造函數所取代。
  • 構造函數模式,能夠建立自定義引用類型,能夠像建立內置對象實例同樣使用 new 操做符。不過,構造函數模式的缺點是:它的每一個成員都沒法獲得複用,包括函數。因爲函數能夠不侷限於任何對象(即與對象具備鬆散耦合的特色),所以沒有理由不在多個對象間共享函數。
  • 原型模式,使用構造函數的 prototype 屬性來指定那些應該共享的屬性和方法。組合使用構造函數模式和原型模式時,使用構造函數定義實例屬性,使用原型模式定義共享的屬性和方法。

JS 主要經過原型鏈實現繼承。原型鏈的構建是經過將一個類型的實例賦值給另一個構造函數的原型實現的。這樣,子類型就能夠繼承超類型的屬性和方法,這一點與基於類的繼承很類似。

原型鏈的問題是:對象實例共享全部繼承的屬性和方法,所以不適宜單獨使用。解決這個問題的技術是借用構造函數,即在子類型構造函數的內部調用超類型構造函數。這樣就能夠作到每一個實例都具備本身的屬性,同時還能保證只使用構造函數模式來定義類型。

使用最多的繼承模式是組合繼承,這種模式使用原型鏈繼承共享的屬性和方法,經過借用構造函數繼承實例屬性。
此外,還存在下列可供選擇的繼承模式:

  • 原型式繼承,能夠在沒必要預先定義構造函數的狀況下實現繼承,其本質是執行對給定對象的淺複製。而複製獲得的副本還能夠獲得進一步改造。
  • 寄生式繼承,與原型式繼承很是類似,也是基於某個對象或某些信息建立一個對象,而後加強該對象,最後返回對象。爲了解決組合繼承模式因爲屢次調用超類型構造函數而致使的低效率問題,能夠將這個模式與組合繼承模式一塊兒使用。
  • 寄生組合式繼承,集寄生式繼承與組合繼承的優勢於一身,是實現基於類型繼承的最有效方式。
相關文章
相關標籤/搜索