[筆記]物件導向的程式設計

賴嘉豪發表於2018-11-16

逐漸瞭解到物件導向思想的重要性,這方面我瞭解的不夠多,既然屬於基礎知識,那麼多看幾次書鞏固總是對的。考慮到該知識的重要性,對於書上的知識我基本都碼下來畫出重點。全文廢話較多篇幅較長,只能作為個人學習筆記用途,如有錯漏歡迎指出。

二刷《JavaScript高階程式設計》第六章——物件導向的程式設計

本文章的學習目標:

  1. 理解物件屬性
  2. 理解並建立物件
  3. 理解繼承

——————————————————————————————————————

物件導向是什麼

物件導向(Object-Oriented,OO)的語言有一個標誌,那就是它們都有類的概念,而通過類可以建立任意多個具有相同屬性和方法的物件。

ECMAScript中沒有類的概念,因此它的物件也與基於類的語言中的物件有所不同。

ECMA-262把物件定義為:“無需屬性的集合,其屬性可以包含基本值、物件或者函式。”嚴格來講,這就相當於說物件是一組沒有特定順序的值。物件的每個屬性或方法都有一個名字,而每個名字都對映到一個值。正因為這樣(以及其他將要討論的原因),我們可以把ECMAScript的物件想象成雜湊表:無非就是一組名值對,其中值可以是資料或函式。

每個物件都是基於一個引用型別建立的,這個引用型別可以是原生型別,也可以是開發人員定義的型別。

——————————————————————————————————————


1.理解物件

建立自定義物件的最簡單方式就是建立Object的例項,然後在為它新增屬性和方法,如下所示。

var person = new Object();
person.name = 'Nicholas';
person.age = 29;
person.job = 'Software Engineer';

person.sayName = function(){
    alert(this.name);
};複製程式碼

上面的例子建立了一個名為person的物件,併為它新增了三個屬性(name/age和job)和一個方法( sayName()  )。其中,sayName()方法用於顯示this.name(將被解析為person.name)的值。

考慮到這種建立物件的方法語句較多,效能並沒有物件字面量語法那麼好,幾年後,物件字面量成為建立這種物件的首選模式。前面的例子用物件字面量語法可以寫成這樣:

var person = {
    name: "Nicholas',
    age: 29,
    job: 'Software Engineer',

    sayName:function(){
        alert(this.name);
    }
};複製程式碼

這個例子的person物件與前面例子中的person物件是一樣的,都有相同的屬性和方法。這些屬性在建立時都帶有一些特徵值,JavaScript通過這些特徵值來定義它們的行為。

1.1 屬性型別

ECMA-262第5版在定義只有內部才用的特性(attribute)時,描述了屬性(property)的各種特徵。ECMA-262定義這些特性是為了實現JavaScript引擎用的,因此在JavaScript中不能直接訪問它們。為了表示特性是內部值,該規範把他們放在了兩對方括號中,例如[[Enumerable]]。儘管ECMA-262第3版的定義有些不同,但此處只參考第5版的描述。

ECMAScript中有兩種屬性:資料屬性和訪問器屬性。

1.資料屬性

資料屬性包含一個資料值的位置。在這個位置可以讀取和寫入值。資料屬性有四個描述其行為的特性

  • [[Configurable]]:表示能否通過 delete 刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。像前面的例子中那樣直接在物件上定義的屬性,它們的這個特性預設為true。
  • [[Enumerable]]:表示能否通過for-in迴圈返回屬性。像前面例子中那樣直接在物件上定義的屬性,它們的這個特性預設值為true。
  • [[writable]]:表示能否修改屬性的值。想前面例子中那樣直接在物件上定義的屬性,它們的這個特性預設值為true。
  • [[Value]]:包含這個屬性的資料值。讀取屬性值的時候,從這個位置讀;寫入屬性值的時候,把新值儲存在這個位置。這個特性的預設值為undefined。

對於像前面例子中那樣直接在物件上定義的屬性,它們的[[Configurable]]、[[Enumerable]]和[[Writable]]特性都被設定為true,而[[value]]]特性被設定為特定的值。例如:

var person = {
    name: 'Nicholas'
};複製程式碼

這裡建立了一個名為name的屬性,為它指定的值是“Nicholas”。也就是說,[[Value]]特性將被設定為“Nicholas”,而對這個值的任何修改都將反應在這個位置。

修改屬性預設的特性,必須使用ECMAScript5的Object.defineProperty()方法。這個方法接受三個引數:屬性所在的物件、屬性的名字和一個描述符物件其中,描述符(descriptor)物件的屬性必須是:configurable、enumerable、writable和value。設定其中的一或多個值,可以修改對應的特性值。例如:

var person = {};
Object.defineProperty(person, "name", {
    writable: false,    //設定為屬性的值不可修改
    value: "Nicholas"
});

alert(person.name); //"Nicholas"
person.name = "Greg";   //嘗試修改屬性的值
alert(person.name); //"Nicholas"複製程式碼

這個例子建立了一個名為name的屬性,它的值“Nicholas”是隻讀的。這個屬性的值是不可修改的,如果嘗試為它指定新值,則在費嚴格模式下,賦值操作將被忽略;在嚴格模式下,賦值操作將會導致丟擲錯誤。

類似的規則也適用於不可配置的屬性。例如:

var person = {};
Object.defineProperty(person, "name", {
    configurable: false,    //設定為不可刪改
    value: "Nicholas"
});

alert(person.name);     //"Nicholas"
delete person.name;          //嘗試刪除屬性
alert(person.name);     //"Nicholas"      //(刪改失敗)複製程式碼

把configurable設定為false,表示不能從物件中刪除屬性。如果對這個屬性呼叫delete,則在非嚴格模式下什麼也不會發生,而在嚴格模式下會導致錯誤。而且,一旦把屬性定義為不可配置的,就不能再把它變回可配置了。此時,再呼叫Object.defineProperty()方法修改除writable之外的特性,都會導致錯誤。

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

//丟擲錯誤
Object.defineProperty(person, "name", {
    configurable: true, //無法修改
    value: "Nicholas"
});複製程式碼

也就是說,可以多次呼叫Object.defineProperty()方法修改同一個屬性,但在把configurable特性設定false之後就會有限制了(無法變回可配置)。

在呼叫Object.defineProperty()方法建立一個新的屬性時,如果不指定,configurable、enumerable和writable特性的預設值都是false。如果呼叫Object.defineProperty()方法只是修改已定義的屬性的特性,則無此限制。例如:

var person = {};
Object.defineProperty(a,'age',{
    value:29
})

a.age = 19; //無效
console.log(a.age) //29    //因為屬性預設的三個特性都是false
複製程式碼


多數情況下,可能都沒有必要利用Object.defineProperty()方法提供的這些高階功能。不過,理解這些概念對理解JavaScript物件卻非常有用。

(不建議在IE8中使用Object.defineProperty方法)


2.訪問器屬性

訪問器屬性不包含資料值;它們包含一對getter和setter函式(不過,這兩個函式都不是必須的)。在讀取訪問器屬性時,會呼叫getter函式,這個函式負責返回有效的值;在寫入訪問器屬性時,會呼叫setter函式並傳入新值,這個函式負責決定如何處理資料。

訪問器屬性有如下四個特性:

  • [[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;
alert(book.edition);  //2
alert(book.year); //2004複製程式碼

以上程式碼建立了一個 book 物件,並給他定義兩個預設的屬性: _year 和 edition。 _year 前面的下劃線是一種常用的記號,用於表示只能通過物件方法訪問的屬性。而訪問器屬性 year 則包含一個 getter 函式和一個 setter 函式。getter函式返回_year 的值,setter 函式通過計算來確定正確的版本。因此,把year 屬性修改為 2005 會導致_year 變成 2005 ,而 edition 變為2。這是使用訪問器屬性的常見方式,即設定一個屬性的值會導致其他屬性發生變化

不一定非要同時指定 getter 和 setter。 只指定 getter 意味著屬性是不能寫,嘗試寫入屬性會被忽略。在嚴格模式下,嘗試寫入只指定了 getter 函式的屬性會丟擲錯誤。類似地,只指定 setter 函式的屬性也不能讀,否則在非嚴格模式下回返回undefined,而在嚴格模式下會丟擲錯誤。

支援 ECMAScript5 的這個方法的瀏覽器有IE9+(IE8只是部分實現)、Firefox 4+、 Safari 5+、Opera12+ 和 Chrome。 在這個方法之前,要建立訪問器屬性,一般都使用兩個非標準的方法:__defineGeter__()和__defineSetter__()。這兩個方法最初是由Firefox引入的,後來Safari3、Chrome1 和 Opera9.5也給出了相同的實現。 使用這兩個一流的方法,可以像下面這樣重寫前面的例子。

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

//定義訪問器的舊有方法
book.__defineGetter__("year", function(){
    return this._year;
});

book.__defineSetter__("year", function(newValue){
    if (newValue > 2004) {
        this._year = newValue;
        this.edition += newValue - 2004;
    }
});

book.year = 2005;
alert(book.edition);  //2
       複製程式碼

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


1.2 定義多個屬性

由於為物件定義多個屬性的可能性很大,ECMAScript 5 又定義了一個 Object.definePro-perties()方法。利用這個方法可以通過描述符一次定義多個屬性。這個方法接收兩個物件引數:第一個物件是要新增和修改其屬性的物件,第二個物件的屬性與第一個物件中要新增或修改的屬性一一對應。例如:

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;
                hits.edition += newValue - 2004;
            }
        } 
    }
});
複製程式碼

以上程式碼在 book 物件上定義了兩個資料屬性( _year 和 edition ) 和一個訪問器屬性( year )。最終的物件與上一節中定義的物件相同。唯一的區別是這裡的屬性都是在同一時間建立的。

支援 Object.defineProperties() 方法的瀏覽器有 IE9+、Firefox 4+、Safari 5+、 Opera 12+和Chrome。


1.3 讀取屬性的特性

使用ECMAScript 5 的 Object.getOwnPropertyDescriptor() 方法,可以取得給定屬性的描述符。這個方法接收兩個引數: 屬性所在的物件要讀取其描述符的屬性名稱。返回值是一個物件,如果是訪問器屬性,這個物件的屬性有 configurable、 enumerable、 get 和 set;如果是資料屬性,這個物件的屬性有 configurable、enumerable、writable和value。例如:

var book = {};

Object.defineProperties(book, {
    _year: {
        //注意此處沒有配置的屬性特性
        value: 2004
    },

    edition: {
        //注意此處沒有配置的屬性特性
        value: 1
    },
    
    year: {
        get: function(){
            return this._year; //注意這裡的this指向book
        },

        set: function(newValue){
            if (newValue > 2004) {
                this._year = newValue; //注意這裡的this指向book
                this.edition += newValue - 2004;
            }
        }
    }
});

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

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

對於資料屬性_year.value 等於最初的值,configurable 是 false, 而 get 等於 undefined。

對於訪問器屬性year,value 等於 undefined, enumerable 是 false, 而 get 是一個指向 getter 函式的指標。

在JavaScript中,可以針對任何物件——包括DOM和BOM物件,使用Object.getOwnProperty-Descriptor()方法。支援這個方法的瀏覽器有IE9+、Firefox 4+、 Safari 5+、 Opera 12+和 Chrome。


2. 建立物件

雖然Object 建構函式或物件字面量都可以用來建立單個物件,但這些方式有個明顯的缺點:使用同一個介面建立很多物件,會產生大量的重複程式碼。為解決這個問題,人們開始使用工廠模式的一種變體。也就是說——工廠模式是為了解決重複程式碼。

2.1 工廠模式

工廠模式是軟體工程領域一種廣為人知的設定模式,這種模式抽象了建立具體物件的過程。考慮到在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");複製程式碼

函式 createPerson() 能夠根據接受的引數來構建一個包含所有必要資訊的 Person 物件。可以無數次地呼叫這個引數,而每次它都會返回一個包含三個屬性一個方法的物件,工廠模式雖然解決了建立多個相似物件的問題,但卻沒有解決物件識別的問題(即怎樣知道一個物件的型別)。隨著JavaScript的發展,又一個新模式出現了。

2.2 建構函式模式

ECMAScript中的建構函式可用來建立特定型別的物件。像 Object 和 Array 這樣的原生建構函式,在執行時會自動出現在執行環境中。此外,也可以建立自定義的建構函式,從而定義自定義物件型別的屬性和方法。例如,可以使用建構函式模式將前面的例子重寫如下。

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("Grey", 27, "Doctor");複製程式碼

在這個例子中, Person()函式取代了 createPerson() 函式。 我們注意到,Person() 中的程式碼除了與 createPerson() 中相同的部分外,還存在以下不同之處:

  • 沒有顯式地建立物件;
  • 直接將屬性和方法賦給了 this 物件;
  • 沒有 return 語句。

此外,還應該注意到函式名 Person 使用的是大寫字母 P 。按照慣例,建構函式始終都應該以一個大寫字母開頭,而非建構函式則應該以一個小寫字母開頭這個做法借鑑自其他 OO 語言,主要是為了區別於 ECMAScript 的新例項,必須使用 new 操作符。以這種方式呼叫建構函式實際上會經歷以下四個步驟:

  1. 建立一個新物件
  2. 將建構函式的作用域賦給新物件(因此this就指上了這個新物件);
  3. 執行建構函式中的程式碼(為這個新物件新增屬性);
  4. 返回新物件

在前面例子的最後,person1 和 person2 分別儲存著Person 的一個不同的例項。這兩個物件都有一個constructor(建構函式)屬性,該屬性指向 Person,如下所示。

alert(person1.constructor === Person); //true
alert(person2.constructor === Person); //true複製程式碼

物件的 constructor 屬性最初是用來表示物件型別的。但是,提到檢測物件型別,還是instanceof操作符要更可靠一些。我們在這個例子中建立的所有物件既是 Object 的例項,同時也是 Person 的例項,這一點通過 instanceof 操作符可以得到驗證。

alert(person1 instanceof Object); //true
alert(person2 instanceof Person); //true
alert(person1 instanceof Object); //true
alert(person2 instanceof Person); //true複製程式碼

建立自定義的建構函式意味著將來可以將它的例項標識為一種特定的型別;而這正是建構函式模式勝過工廠模式的地方。在這個例子中,person1 和 person2 之所以同時是 Object 的例項,是因為所有物件均繼承自 Object。


1.將建構函式當做函式

建構函式與其他函式的唯一區別,就在於呼叫它們的方式不同。不過,建構函式畢竟也是函式,不存在定義建構函式的特殊語法。任何函式,只要通過 new 操作符來呼叫,那它就可以作為建構函式;而任何函式,如果不通過 new 操作符來呼叫,那它跟普通函式也不會有什麼兩樣。例如,前面例子中定義的 Person() 函式可以通過下列任何一種方式來呼叫。

// 當做建構函式使用
var person = new Person ("Nicholas", 29, "Software Engineer"); //作用域會賦給person
person.sayName(); //"Nicholas"

// 作為普通函式呼叫
Person("Greg", 27, "Doctor");  //新增到window
window.sayName(); //"Greg"

// 再另一個物件的作用域中呼叫
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"複製程式碼

這個例子中的前兩行程式碼展示了建構函式的典型用法,即使用 new 操作符來建立一個新物件。接下來的兩行程式碼展示了不使用 new 操作符呼叫Person()會出現什麼結果:屬性和方法都被新增給window物件了。有讀者可能還記得,當在全域性作用域中呼叫一個函式時,this 物件總是指向 Global 物件(在瀏覽器中就是window物件)。因此,也可以使用 call() (或者 apply() )在某個特殊物件的作用域中呼叫Person()函式。這裡是在物件 o 的作用域中呼叫的,因此呼叫後 o 就擁有了所有屬性和 sayName() 方法。


2.建構函式的問題

建構函式模式雖然好用,但也並非沒有缺點。使用建構函式的主要問題,就是每個方法都要在每個例項上重新建立一遍。在前面的例子中,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)"); //與宣告函式在邏輯上是等價的
}複製程式碼

從這個角度上來看建構函式,更容易明白每個 Person 例項都包含一個不同的 Function 例項(以顯示 name 屬性)的本質。說明白些,以這種方式建立函式,會導致不同的作用域鏈和識別符號解析,但建立 Function 新例項的機制仍然是相同的。因此,不同例項上的同名函式是不相等的,以下程式碼可以證明這一點。

alert(person1.sayName == person2.sayName); //false複製程式碼

然而,建立兩個完成同樣任務的 Function 例項的確沒有必要;況且有 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() 函式。這個樣做確實解決了兩個函式做同一件事的問題,可是新問題又來了:在全域性作用域中定義的函式實際上只能被某個物件呼叫,這讓全域性作用域有點名不副實。而更讓人無法接受的是:如果物件需要定義很多方法沒那麼就要定義很多個全域性函式,於是我們這個自定義的引用型別就絲毫沒有封裝性可言了。好在,這些問題可以通過原型模式來解決。

2.3 原型模式

我們建立的每個函式都有一個 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"

alert(person1.sayName === person2.sayName); //true複製程式碼

再次,我們將 sayName() 方法和所有屬性直接新增到了 Person 的 prototype 屬性中,建構函式變成了空函式。即使如此,也仍然可以通過呼叫建構函式來建立新物件,而且新物件還會具有相同的屬性和方法。但與建構函式模式不同的是,新物件的這些屬性和方法是由所有例項共享的。換句話說,person1 和 person2 訪問的都是同一組屬性和同一個 sayName() 函式。要理解原型模式的工作原理,必須先理解 ECMAScript中原型物件的性質。

1.理解原型物件

無論什麼時候,只要建立了一個新函式,就會根據一組特定的規則為該函式建立一個 prototype 屬性,這個屬性指向函式的原型物件在預設情況下,所有原型物件都會自動獲得一個 constructor(建構函式)屬性,這個屬性是一個指向 prototype 屬性所在函式的指標。就拿前面的例子來說,Person.prototype.constructor 指向 Person。而通過這個建構函式,我們還可繼續為原型物件新增其他屬性和方法。

建立了自定義的建構函式之後,其原型物件預設只會取得 constructor 屬性;至於其他方法,則都是從 Object 繼承而來的。當呼叫建構函式建立一個新例項後,該例項的內部都將包含一個指標(內部屬性),指向建構函式的原型物件。ECMA-262第5版中管這個指標叫[[Prototype]]。雖然在指令碼中沒有標準的方式訪問[[Prototype]],但 Firefox、 Safari 和 Chrome 在每個物件上都支援一個屬性__proto__;而在其他實現中,這個屬性對指令碼則是完全不可見的。不過,要明確的真正重要的一點就是,這個連線存在於例項與建構函式的原型物件之間,而不是存在於例項與建構函式之間

在之前的例子中,Person.prototype 指向了原型物件,而 Person.prototype.constructor 又指回了 Person原型物件中除了包含 constructor 屬性之外,還包括後來新增的其他屬性。 Person 的每個例項——person1 和person2 都包含一個內部屬性,該屬性僅僅指向了 Person.prototype;換句話說,它們與建構函式沒有直接的關係。此外,要格外注意的是,雖然這兩個例項都不包含屬性和方法,但我們卻可以呼叫person1.sayName()。這是通過查詢物件屬性的過程來實現的。

雖然在所有實現中都無法訪問到[[prototype]],但可以通過 isPrototypeOf()方法來確定物件之間是否存在這種關係。從本質上講,如果[[Prototype]]指向呼叫 isPrototypeOf() 方法的物件(Person.Prototype),那麼這個方法就返回true,如下所示:

alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true複製程式碼

這裡,我們用原型物件的 isPrototypeOf()方法測試了 person1 和 person2。 因為它們內部都有一個指向 Person.prototype 的指標,因此都返回了 true。

ECMAScript 5 增加了一個新方法,叫 Object.getPrototypeOf(),在所有支援的實現中,這個方法返回[[Prototype]]的值。例如:

alert(Object.getPrototypeOf(person1) == Person.Prototype); //true
alert(Object.getPrototypeOf(person1).name);  //"Nicholas"複製程式碼

這裡的第一行程式碼只是確定 Object.getPrototypeOf() 返回的物件實際就是這個物件的原型。第二行程式碼取得了原型物件中 name 屬性的值,也就是“Nicholas”。使用Object.getPrototypeOf()可以方便地去的一個物件的原型,而這在利用原型實現繼承(本章稍後會討論)的情況下是非常重要的。支援這個方法的瀏覽器有 IE9+、 Firefox 3.5+、 Safari 5+、 Opera 12+ 和 Chrome。

每當程式碼讀取某個物件的某個屬性時,都會執行一次搜尋,目標是具有給定名字的屬性。搜尋首先從物件例項本身開始。如果在例項中找到了具有給定名字的屬性,則返回該屬性的值;如果沒有找到,則繼續搜尋指定指向的原型物件,在原型物件中查詢具有給定名字的屬性。如果在原型物件中戰鬥奧了這個屬性,則返回該屬性的值。

也就是說,在我們呼叫person1.sayName()的時候,會先後執行兩次搜尋。 

首先,解析器會問:“例項 person1 有sayName 屬性嗎?”

答:“沒有。”

然後,它繼續搜尋,再問:“person1 的原型有 sayName 屬性嗎?” 

答:“有。”

於是,它就讀取那個儲存在原型物件中的函式 。當我們呼叫person2.sayName() 時,將會重現相同的搜尋過程,得到相同的結果。而這正是多個物件例項共享原型所儲存的屬性和方法的基本原理。

前面提到過,原型最初只包含 constructor 屬性,而該屬性也是共享的,因此可以通過物件例項訪問。

雖然可以通過物件例項訪問儲存在原型中的值,但卻不能通過物件例項重寫原型中的值。如果我們在例項中新增了一個屬性,而該屬性與例項原型中的一個屬性同名,那我們就在例項中建立該屬性,該屬性將會遮蔽原型中的那個屬性。來看下面的例子。

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

person1.name = "Greg";
alert(person1.name);    //"Greg"——來自例項 例項屬性遮蔽了原型中的那個屬性
alert(person2.name);    //"Nicholas"——來自原型複製程式碼

在這個例子中,person1 的 name 被一個新值給遮蔽了。但無論訪問 person1.name 還是訪問 person2.name 都能夠正常地返回值,即分別是“Greg”(來自物件例項)和“Nicholas”(來自原型)。當在alert()中訪問person1.name 時,需要讀取它的值,因此就會在這個例項上搜尋一個名為name的屬性。這個屬性確實存在,於是就返回它的值而不必再搜尋原型了。當以同樣的方式訪問person2.name 時,並沒有在例項上發現該屬性,因此就會繼續搜尋原型,結果在那裡找到了 name 屬性。

 當為物件例項新增一個屬性時,這個屬性就會遮蔽原型物件中儲存的同名屬性;換句話說,新增這個屬性只阻止我們訪問原型中的那個屬性,但不會修改那個屬性。即使這個屬性設定為null,也只會在例項中設定這個屬性,而不會恢復其指向原型的連線。不過,使用 delete 操作符則可以完全刪除例項屬性,從而讓我們能夠重新訪問原型中的屬性,如下所示。

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

person1.name = "Greg";
alert(person1.name);     //"Greg" ——來自例項
alert(person2.name);     //"Nicholas"——來自原型

delete person1.name;
alert(person1.name);     //"Nicholas"——來自原型複製程式碼

在這個修改後的例子中,我們使用 delete 操作符刪除了 person1.name,之前它儲存的“Greg”值遮蔽了同名的原型屬性。把它刪除以後,就恢復了對原型中 name 屬性的連線。因此,接下來再呼叫person1.name 時,返回的就是原型中 name 屬性的值了。

使用hasOwnProperty()方法可以檢測一個屬性是存在於例項中,還是存在於原型中。這個方法(不要忘了它是從 Object 繼承來的)值在給定屬性存在於物件例項中時,才會返回 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();

alert(person1.hasOwnProperty("name"));  //false

person1.name = "Greg";
alert(person1.name);   //"Greg"  ——來自例項
alert(person1.hasOwnProperty("name"));  //true

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

delete person1.name;
alert(person1.name);   //"Nicholas"——來自原型
alert(person1.hasOwnProperty("name")); //false複製程式碼


通過使用 hasOwnProperty() 方法,什麼時候訪問的是例項屬性,什麼時候訪問的是原型屬性就一清二楚了。呼叫person1.hasOwnProperty("name")是,只有當 person1 重寫 name 屬性後才會返回true,因為只有這時候 name 才是一個例項屬性,而非原型屬性。

2.原型與 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();

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

person1.name = "Greg";
alert(person1.name);   //"Greg"——來自例項
alert(person1.hasOwnProperty("name")); //true
alert("name" in person1);  //true

alert(person2.name);     //"Nicholas"——來自原型
alert(person2.hasOwnProperty("name")); //false
alert("name" in person2);  //true

delete person1.name;
alert(person1.name);   //"Nicholas"——來自原型
alert(person1.hasOwnProperty("name"));   //false
alert("name" in person1); //true複製程式碼

在以上程式碼執行的整個過程中, name 屬性要麼是直接在物件上訪問到的,要麼是通過原型訪問到的。因此,呼叫“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,就可以確定屬性是原型中的屬性。下面來看一看上面定義的函式 hasPrototypeProperty() 的用法。

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();
alert(hasPrototypeProperty(person, "name")); //true

person.name = "Greg";
alert(hasPrototypeProperty(person, "name"));   //false複製程式碼

在這裡,name 屬性先是存在於原型中,因此hasPrototypeProperty() 返回 true。當在例項中重寫 name 屬性後,該屬性就存在於例項中了,因此 hasPrototypeProperty()返回 false。即使原型中仍然有 name 屬性,但由於現在例項中也有了這個屬性,因此原型中的 name 屬性就用不到了。

在使用 for-in 迴圈時,返回的是所有能夠通過物件訪問的、可列舉的(enumerated)屬性,其中既包括存在於例項中的屬性,也包括存在於原型中的屬性。遮蔽了原型中不可列舉屬性(將[[Enumerable]]標記為false的屬性)的例項屬性也會在 for-in 迴圈中返回,因為根據規定,所有開發人員定義的屬性都是可列舉的——只有在 IE8 及更早版本中例外。


未完結。


未做總結


相關文章