[筆記]面向對象的程序設計

逐漸瞭解到面向對象思想的重要性,這方面我瞭解的不夠多,既然屬於基礎知識,那麼多看幾回書鞏固老是對的。考慮到該知識的重要性,對於書上的知識我基本都碼下來畫出重點。全文廢話較多篇幅較長,只能做爲我的學習筆記用途,若有錯漏歡迎指出。瀏覽器

二刷《JavaScript高級程序設計》第六章——面向對象的程序設計bash

本文章的學習目標:app

  1. 理解對象屬性
  2. 理解並建立對象
  3. 理解繼承

——————————————————————————————————————函數

面向對象是什麼

面向對象(Object-Oriented,OO)的語言有一個標誌,那就是它們都有類的概念,而經過類能夠建立任意多個具備相同屬性和方法的對象。性能

ECMAScript中沒有類的概念,所以它的對象也與基於類的語言中的對象有所不一樣。學習

ECMA-262把對象定義爲:「無需屬性的集合,其屬性能夠包含基本值、對象或者函數。」嚴格來說,這就至關於說對象是一組沒有特定順序的值。對象的每一個屬性或方法都有一個名字,而每一個名字都映射到一個值。正由於這樣(以及其餘將要討論的緣由),咱們能夠把ECMAScript的對象想象成散列表:無非就是一組名值對,其中值能夠是數據或函數。測試

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

——————————————————————————————————————this


1.理解對象

建立自定義對象的最簡單方式就是建立Object的實例,而後在爲它添加屬性和方法,以下所示。spa

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引入的,後來Safari三、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 及更早版本中例外。


未完結。


未作總結

相關文章
相關標籤/搜索