《JS高程》面向?qū)ο蟮某绦蛟O(shè)計(jì) 學(xué)習(xí)筆記

理解對(duì)象

創(chuàng)建自定義對(duì)象的最簡(jiǎn)單方式就是創(chuàng)建一個(gè)Object實(shí)例,然后再為它添加屬性和方法。

            var person = new Object();
            person.name = "Nicholas";
            person.age = 29;
            person.job = "Software Engineer";
            person.sayName = function() {
                alert(this.name);
            }

或是對(duì)象字面量語(yǔ)法式創(chuàng)建。

            var person = {
                name:"Nicholas",
                age:29,
                job:"Software Engineer",
                sayName:function() {
                    alert(this.name);
                }
            };

這兩種例子所達(dá)到的效果是相同的,都有相同屬性和方法。這些屬性在創(chuàng)建時(shí)都帶有一些特征值(characteristic),JavaScript通過(guò)這些特征值來(lái)定義它們的行為。

屬性類(lèi)型

ECMA5在定義只有內(nèi)部采用的特性(attribute)時(shí),描述了屬性(property)的各種特征。為了表示特性是內(nèi)部值,該規(guī)范把他們放在了兩對(duì)兒方括號(hào)中,例如[[Enumerable]]
ECMAScipt中有兩種屬性:數(shù)據(jù)屬性和訪(fǎng)問(wèn)器屬性。

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

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

  • [[Configurable]]:表示能否通過(guò)delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪(fǎng)問(wèn)器屬性。如直接在對(duì)象上定義屬性,默認(rèn)值為true

  • [[Enumerable]]:表示能否通過(guò)for-in循環(huán)返回屬性。默認(rèn)值為true,即可枚舉的。

  • [[Writable]]:表示能否修改屬性的值。默認(rèn)值為true。即可寫(xiě)的。

  • [[Value]]:包含這個(gè)屬性的數(shù)據(jù)值。讀取屬性值的時(shí)候,從這個(gè)位置讀;寫(xiě)入屬性值的時(shí)候,新值就保存在這個(gè)位置。默認(rèn)值undefined。

            var person = {
                name:"Nicholas"
            };

這里創(chuàng)建了一個(gè)名為name的屬性,為它指定的值是Nicholas。也就是說(shuō),[[Value]]特性將被設(shè)置為Nicholas,而對(duì)這個(gè)值的任何修改都將反映在這個(gè)位置。

如若要修改屬性默認(rèn)特性,必須使用ECMAScript5的Object.defineProperty()方法。這個(gè)方法接受三個(gè)參數(shù):屬性所在的對(duì)象、屬性的名字和一個(gè)描述的對(duì)象。其中描述符(descriptor)對(duì)象的屬性必須是:configurable、enumerable、writable和value。設(shè)置其中的一或多個(gè)值,可以修改對(duì)應(yīng)的特性值。

            var person = {};
            Object.defineProperty(person,"name",{
                writable:false,
                value:"Nicholas"
            });
            
            console.log(person.name);    //"Nicholas"
            person.name = "Greg";
            console.log(person.name);    //"Nicholas"

這個(gè)例子創(chuàng)建了一個(gè)名為name的屬性,它的值是只讀的。這個(gè)屬性的值是不可修改的,如果嘗試的為它指定新值,則在非嚴(yán)格模式下,賦值操作將被忽視;嚴(yán)格模式下,賦值操作將會(huì)拋出錯(cuò)誤。

            "use strict"
            var person = {};
            Object.defineProperty(person,"name",{
                writable:false,
                value:"Nicholas"
            });
            
            console.log(person.name);    //"Nicholas"
            person.name = "Greg";          
            // "Uncaught TypeError: Cannot assign to read only property 'name' of #<Object>"

類(lèi)似規(guī)則也適用與不可配置的屬性。例如:

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

把configurable設(shè)置為false,表示不能從對(duì)象中刪除屬性。如果對(duì)這個(gè)屬性調(diào)用delete,則在非嚴(yán)格模式下什么也不會(huì)發(fā)生,在嚴(yán)格模式下會(huì)導(dǎo)致錯(cuò)誤。而且,一旦把屬性定義為不可配置的,就不能再把它便會(huì)可配置了。此時(shí),再調(diào)用Object.defineProperty()方法修改除writable之外的特性,都會(huì)導(dǎo)致錯(cuò)誤:

            var person = {};
            Object.defineProperty(person,"name",{
                configurable:false,
                value:"Nicholas"
            })
            Object.defineProperty(person,"name",{
                configurable:true,
                value:"Nicholas"
            })
            //"Uncaught TypeError: Cannot redefine property: name"
            

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

栗子

            var person = {};
            Object.defineProperty(person,"name",{
                configurable:false,
                value:"Nicholas"
            })
            Object.defineProperty(person,"name",{
                writable:true
            })
       
            person.name = "Jason";
            console.log(person.name);
            // configurable為false,writable是唯一可修改的特性
            // 哪怕writable為true,也無(wú)法重新設(shè)置屬性值

可以多次調(diào)用Object.defineProperty()方法修改同一個(gè)屬性,但在把congfigurable特性設(shè)置為false之后就會(huì)有限制了。
調(diào)用Object.defineProperty()方法創(chuàng)建一個(gè)新的屬性時(shí),如果不指定,configurable、enumerable、writable特性值的默認(rèn)值都是false。

簡(jiǎn)化而言就是,按照普通的使用方法(對(duì)象字面量,構(gòu)造對(duì)象)定義對(duì)象,創(chuàng)建屬性時(shí),特性默認(rèn)值都為true,value特性默認(rèn)值undefined。
但是使用Object.defineProperty()來(lái)創(chuàng)建屬性,則三個(gè)特性值都將為false。

IE瀏覽器支持?jǐn)?shù)據(jù)屬性的最低版本支持為IE8

訪(fǎng)問(wèn)器屬性

訪(fǎng)問(wèn)器屬性不包含數(shù)據(jù)值;它們包含一對(duì)兒getter和setter函數(shù)就(不過(guò),這兩個(gè)函數(shù)都不是必須的)。在讀取訪(fǎng)問(wèn)器屬性時(shí),會(huì)調(diào)用getter函數(shù),這個(gè)函數(shù)負(fù)責(zé)返回有效的值;在寫(xiě)入訪(fǎng)問(wèn)器屬性時(shí),會(huì)調(diào)用setter函數(shù)并傳入新值,這個(gè)函數(shù)負(fù)責(zé)決定如何處理數(shù)據(jù)。訪(fǎng)問(wèn)器屬性有如下4個(gè)特性。

  • [[Configurable]]:表示能否通過(guò)delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪(fǎng)問(wèn)器屬性。如直接在對(duì)象上定義屬性,默認(rèn)值為true

  • [[Enumerable]]:表示能否通過(guò)for-in循環(huán)返回屬性。默認(rèn)值為true,即可枚舉的。

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

  • [[Set]]:在寫(xiě)入屬性時(shí)調(diào)用的函數(shù)。默認(rèn)值為undefined。

訪(fǎng)問(wèn)器屬性不能直接定義,必須使用Object.defineProperty()來(lái)定義。

            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 = this.edition+newValue-2004;
                    }
                }
            });
            
            book.year = 2005;
            console.log(book.edition);    // 2

以上代碼創(chuàng)建了一個(gè)book對(duì)象,并給它定義兩個(gè)默認(rèn)屬性:_yearedition_year前面的下劃線(xiàn)是一種常用的記號(hào),用于表示只能通過(guò)對(duì)象方法訪(fǎng)問(wèn)的屬性。而訪(fǎng)問(wèn)器屬性year則包含一個(gè)getter函數(shù)和一個(gè)setter函數(shù)。getter函數(shù)返回_year的值,setter函數(shù)通過(guò)計(jì)算來(lái)確定正確的版本,因此,把year屬性修改為2005會(huì)導(dǎo)致_year變成2005,而edition變?yōu)?。這是使用訪(fǎng)問(wèn)器屬性的常見(jiàn)方式,即設(shè)置一個(gè)屬性的值會(huì)導(dǎo)致其他屬性發(fā)生變化。
不一定非要同時(shí)指定getter和setter。只指定getter意味屬性是不能寫(xiě),嘗試寫(xiě)入屬性會(huì)被忽略。在嚴(yán)格模式下,嘗試寫(xiě)入只指定了getter函數(shù)的屬性會(huì)拋出錯(cuò)誤。類(lèi)似地,只指定setter函數(shù)的屬性也不能讀,否則在非嚴(yán)格模式下會(huì)返回undefined,嚴(yán)格模式下會(huì)拋出錯(cuò)誤。

            "use strict"
            var book = {
                _year:2004,
                edition:1
            };
            
            Object.defineProperty(book,"year",{
                get:function(){
                    return this._year;
                }
            });
            book.year = 2004;
            // "Uncaught TypeError: Cannot set property year of #<Object> which has only a getter"
            // 嘗試寫(xiě)入失敗

在這些方法之前,一般使用兩個(gè)非標(biāo)準(zhǔn)方法:__defineGetter__(),__defineSetter__()

            var book = {
                _year:2004,
                edition:1
            };
            
            // 定義訪(fǎng)問(wèn)器屬性的舊有方法
            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;    // 寫(xiě)入屬性
            console.log(book.edition);    // 2 
            // 修改了_year

在不支持Object.defineProperty()方法的瀏覽器中不能修改ConfigurableEnumerable

定義多個(gè)屬性

由于為對(duì)象定義多個(gè)屬性的可能性很大,ES5定義了一個(gè)Object.defineProperties()方法。利用這個(gè)方法可以通過(guò)描述符一次定義多個(gè)屬性。這個(gè)方法接收兩個(gè)對(duì)象參數(shù):第一個(gè)對(duì)象是要添加和修改其屬性的對(duì)象,第二個(gè)對(duì)象的屬性與第一個(gè)對(duì)象中要添加或修改的屬性一一對(duì)應(yīng)。

            var book = {};
            Object.defineProperties(book,{
                _year:{
                    value:2004,
                    writable:true
                },
                edition:{
                    value:1,
                    writable:true
                },
                year:{
                    get:function() {
                        return this._year;
                    },
                    set:function(newValue) {
                        if (newValue>2004) {
                            this._year = newValue;
                            this.edition += newValue - 2004;
                        }
                    }
                }
            });
            
            book.year = 2018;
            console.log(book.edition);    //15

支持Object.defineProperties()方法的瀏覽器有IE9+、FireFox4+、Safari5+、Opera12+和Chrome。

支持屬性的特性

使用ECMAScript5的Object.getOwnPropertyDescriptor()方法,可以取得給定屬性的描述符。這個(gè)方法接收兩個(gè)參數(shù):屬性所在的對(duì)象和要讀取其描述符的屬性名稱(chēng)。返回值是一個(gè)對(duì)象,如果是訪(fǎng)問(wèn)器屬性,這個(gè)對(duì)象的屬性有configurable、enumerable、get和set;如果是數(shù)據(jù)屬性,這個(gè)對(duì)象的屬性有configurable,enumerable,writable,value。

            var book = {};
            Object.defineProperties(book,{
                _year:{
                    value:2004
                },
                edition:{
                    value:1
                },
                year:{
                    get:function() {
                        return this._year;
                    },
                    set:function(newValue) {
                        if (newValue>2004) {
                            this._year = newValue;
                            this.edition += newValue - 2004;
                        }
                    }
                }
            });
            
            var descriptor = Object.getOwnPropertyDescriptor(book,"_year");
            console.log(descriptor.value);    // 2004
            console.log(descriptor.configurable);    // false
            
            console.log(typeof descriptor.get);    // undefined 對(duì)象是數(shù)據(jù)屬性
            
            var descriptor = Object.getOwnPropertyDescriptor(book,"year");    // 對(duì)象是訪(fǎng)問(wèn)器屬性
            console.log(descriptor.value);    // undefined
            console.log(descriptor.enumerable);    // false
            console.log(typeof descriptor.get);    // function

對(duì)于數(shù)據(jù)屬性_year,value等于最初的值,configurable是false,而get等于undefined。
對(duì)于訪(fǎng)問(wèn)器屬性year,value等于undefined,enumerable是false,而get是一個(gè)指向getter函數(shù)的指針。

創(chuàng)建對(duì)象

工廠模式

            function createPerson(name,age,job) {
                var o = new Object();
                o.name = name;
                o.age = age;
                o.job = job;
                o.sayName = function() {
                    console.log(this.name);
                };
                return 0;
            }
            
            var person1 = createPerson("Nicholas",29,"Software Engineer");
            var person2 = createPerson("Greg",27,"Doctor");

函數(shù)createPerson()能夠根據(jù)接受的參數(shù)來(lái)構(gòu)建一個(gè)包含所有必要信息的person對(duì)象??梢詿o(wú)數(shù)次調(diào)用這個(gè)函數(shù),而每次它都返回一個(gè)包含三個(gè)屬性一個(gè)方法的對(duì)象。工廠模式雖然解決了創(chuàng)建多個(gè)相似對(duì)象的問(wèn)題,但卻沒(méi)有解決對(duì)象識(shí)別的問(wèn)題。

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

ECMAScript中的構(gòu)造函數(shù)可用來(lái)創(chuàng)建特定類(lèi)型的對(duì)象。像Object和Array這樣的原生構(gòu)造函數(shù),在運(yùn)行時(shí)會(huì)自動(dòng)出現(xiàn)在執(zhí)行環(huán)境中。此外,也可以創(chuàng)建自定義的構(gòu)造函數(shù),從而定義自定義對(duì)象類(lèi)型的屬性和方法。

            function Person(name,age,job) {
                this.name= name;
                this.age = age;
                this.job = job;
                this.sayName = function() {
                    console.log(this.name);
                };
            }
            
            var person1 = new Person("Nicholas",29,"Software Engineer");
            var person2 = new Person("Greg",27,"Doctor");

Person()函數(shù)取代了createPerson()函數(shù)。Person()函數(shù)中的代碼除了與createPerson()相同的部分外,還存在一些不同之初。

  • 沒(méi)有顯式地創(chuàng)建對(duì)象
  • 直接將屬性和方法賦給了this對(duì)象
  • 沒(méi)有return語(yǔ)句

此外還應(yīng)該注意到函數(shù)名Person使用的是大寫(xiě)字母P。按照慣例,構(gòu)造函數(shù)始終都應(yīng)該以大寫(xiě)字母開(kāi)頭,而非構(gòu)造函數(shù)則應(yīng)該以小寫(xiě)字母開(kāi)頭。這個(gè)做法借鑒其他OO語(yǔ)言,主要是為了區(qū)別于ECMAScript中的其他函數(shù);因?yàn)闃?gòu)造函數(shù)本身也是函數(shù),只不過(guò)可以用來(lái)創(chuàng)建對(duì)象而已。
要?jiǎng)?chuàng)建Person的新實(shí)例,必須使用new操作符。以這種方式調(diào)用構(gòu)造函數(shù)會(huì)經(jīng)歷4個(gè)步驟:

  • 創(chuàng)建一個(gè)新對(duì)象;
  • 將構(gòu)造函數(shù)的作用域給新對(duì)象(因此this就指向了新對(duì)象);
  • 執(zhí)行構(gòu)造函數(shù)中的代碼(為這個(gè)新對(duì)象添加屬性);
  • 返回新對(duì)象。

person1和person2分別保存著Person的一個(gè)不同的實(shí)例。這兩個(gè)對(duì)象都有一個(gè)constructor(構(gòu)造函數(shù))屬性,該屬性指向Person。
對(duì)象的constructor屬性最初是用來(lái)表示對(duì)象類(lèi)型的。但是提到檢測(cè)對(duì)象類(lèi)型,還是instanceof操作符可靠一些。

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

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

將構(gòu)造函數(shù)當(dāng)做函數(shù)
構(gòu)造函數(shù)與其他函數(shù)唯一區(qū)別,在于調(diào)用它們的方式不同。不過(guò),構(gòu)造函數(shù)畢竟也是函數(shù),不存在定義構(gòu)造函數(shù)的特殊語(yǔ)法。任何函數(shù),只要通過(guò)new操作符來(lái)調(diào)用,那它就可以作為構(gòu)造函數(shù);而任何函數(shù),如果不通過(guò)new操作符來(lái)調(diào)用,那它跟普通函數(shù)也沒(méi)任何區(qū)別。

            function Person(name,age,job) {
                this.name= name;
                this.age = age;
                this.job = job;
                this.sayName = function() {
                    console.log(this.name);
                };
            }
           
            // 當(dāng)做構(gòu)造函數(shù)調(diào)用
            var person = new Person("Nicholas",29,"Software Engineer");
            person.sayName();    // "Nicholas"
            
            // 作為普通函數(shù)調(diào)用
            Person("Greg",28,"Doctor");   //添加到window
            window.sayName();    // "Greg"
            
            // 在另一個(gè)對(duì)象的作用域中調(diào)用
            var o = new Object();
            Person.call(o,"Kristen",25,"Nurse");
            o.sayName();    // "Kristen"

可以看到,不使用new操作符調(diào)用Person()的情況,屬性和方法都被添加給window對(duì)象。
最后,也可以使用call()或者apply()在某個(gè)特殊對(duì)象的作用域中調(diào)用Person()函數(shù),這里是在對(duì)象o的作用域中調(diào)用的,因此調(diào)用后的o就擁有了所有屬性和sayName()方法。

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

function Person(name,age,job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function(
        "console.log(this.name)"
    )
}

從這個(gè)角度上看構(gòu)造函數(shù),更容易明白每個(gè)Person實(shí)例都包含一個(gè)不同的Function實(shí)例(以顯示name屬性)。以這種方式創(chuàng)建函數(shù),會(huì)導(dǎo)致不同的作用域鏈和標(biāo)識(shí)符解析,但創(chuàng)建Function新實(shí)例的機(jī)制仍然是相同的。因此,不同實(shí)例上的同名函數(shù)時(shí)不相等的,以下證明:

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

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

但是創(chuàng)建兩個(gè)一樣的功能的Function實(shí)例沒(méi)有的確沒(méi)有必要;況且有this對(duì)象在,根本不同在執(zhí)行代碼前就把函數(shù)綁定到特定對(duì)象上面。因此,大可像下面這樣,通過(guò)把函數(shù)定義轉(zhuǎn)移到外部來(lái)解決問(wèn)題。

function sayName() {            // 全局函數(shù)sayName,任何Perzon的實(shí)例均可使用sayName函數(shù)
    console.log(this.name);
}

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

person1.sayName();
person2.sayName();

將sayName屬性設(shè)置成全局的sayName函數(shù),這樣一來(lái),sayName包含的是一個(gè)指向函數(shù)的指針,因此person1和person2對(duì)象就共享了全局作用域中定義的同一個(gè)sayName函數(shù)。
但是新問(wèn)題是:在全局作用域中定義的函數(shù)實(shí)際上只能被某個(gè)對(duì)象調(diào)用,這讓全局作用域有點(diǎn)名不副實(shí)。而更讓人無(wú)法接受的是:如果對(duì)象需要定義很多方法,那么就要定義很多個(gè)全局函數(shù),這樣這個(gè)自定義引用類(lèi)型就絲毫沒(méi)有封裝性可言了,但我們可以用原型模式來(lái)解決。

原型模式

我們創(chuàng)建的每一個(gè)函數(shù)都要一個(gè)prototype原型屬性,這個(gè)屬性是一個(gè)指針,指向一個(gè)對(duì)象,而這個(gè)對(duì)象的用途是包含可以由特定類(lèi)型的所有實(shí)例共享的屬性和方法。如果按照字面意思理解,那么prototype就是通過(guò)調(diào)用構(gòu)造函數(shù)而創(chuàng)建的那個(gè)實(shí)例對(duì)象的原型對(duì)象。
換句話(huà)說(shuō),不必在構(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() {
        console.log(this.name);
    };
};

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

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

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

我們將sayName函數(shù)方法和所有屬性直接添加到了Person的prototype屬性中,構(gòu)造函數(shù)變成了空函數(shù)。即便如此,也仍然可以通過(guò)調(diào)用構(gòu)造函數(shù)來(lái)創(chuàng)建新對(duì)象,而且新對(duì)象還會(huì)具有相同的屬性和方法。但與構(gòu)造函數(shù)不同的是,新對(duì)象的這些屬性和方法是由所有實(shí)例共享的。換句話(huà)說(shuō),person1和person2訪(fǎng)問(wèn)的都是同一組屬性和同一個(gè)sayName函數(shù)。要理解原型模式的工作原理,必須先理解ECMAScript中原型對(duì)象的性質(zhì)。

理解原型對(duì)象
無(wú)法什么時(shí)候,只要?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è)屬性指向prototype屬性所在函數(shù)的指針。
如前面的就是:

console.log(Person.prototype.constructor == Person);     // true

創(chuàng)建了自定義的構(gòu)造函數(shù)之后,其原型對(duì)象默認(rèn)只會(huì)取得constructor屬性;至于其他方法,則都是從Object繼承而來(lái)的。當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)新實(shí)例之后,該實(shí)例內(nèi)部將包含一個(gè)指針,指向構(gòu)造函數(shù)對(duì)象原型對(duì)象。ECMA-262第5版管這個(gè)指針叫[[Prototype]] 。雖然在腳本中沒(méi)有標(biāo)準(zhǔn)方式訪(fǎng)問(wèn)prototype,但FireFox、Safari和Chrome在每個(gè)對(duì)象上都支持一個(gè)屬性proto;而在其他實(shí)現(xiàn)中,這個(gè)屬性對(duì)腳本則是完全不可見(jiàn)的。不過(guò),要明確的真正重要的一點(diǎn)就是,這個(gè)鏈接存在于實(shí)例與構(gòu)造函數(shù)之間,而不是存在于實(shí)例與構(gòu)造函數(shù)之間。如圖:

上圖展示了Person構(gòu)造函數(shù)、Person的原型屬性以及Person現(xiàn)有的兩個(gè)實(shí)例之間的關(guān)系。
Person.prototype指向了原型對(duì)象,而Person.prototype.constructor又指回了Person。原型對(duì)象中除了包含constru屬性之外,還包括后來(lái)添加的其他屬性。Person的每個(gè)實(shí)例person1和person2都包含一個(gè)內(nèi)部屬性,該屬性?xún)H僅指向了Person.prototype;換句話(huà)說(shuō),它們和構(gòu)造函數(shù)沒(méi)有直接關(guān)系。此外,雖然這兩個(gè)實(shí)例都不包含屬性和方法,但我們可以調(diào)用person1.sayName(),這是通過(guò)查找對(duì)象屬性的過(guò)程來(lái)實(shí)現(xiàn)的。

雖然在所有實(shí)現(xiàn)中都無(wú)法訪(fǎng)問(wèn)到[[Prototype]],但可以通過(guò)isPrototypeOf()方法來(lái)確定對(duì)象之間是否存在這種關(guān)系。從本質(zhì)講,如果[[Prototype]]指向調(diào)用isPrototypeOf()方法的對(duì)象,那么這個(gè)方法就返回true。

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

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

console.log(Person.prototype.isPrototypeOf(person1));    // true
console.log(Person.prototype.isPrototypeOf(person2));    // true

MDN-isPrototypeOf()

// MDN栗子
function Foo() {}
function Bar() {}
function Baz() {}

Bar.prototype = Object.create(Foo.prototype);
Baz.prototype = Object.create(Bar.prototype);

var baz = new Baz();

// console.log(Baz.prototype.isPrototypeOf(baz)); // true
// console.log(Bar.prototype.isPrototypeOf(baz)); // true
// console.log(Foo.prototype.isPrototypeOf(baz)); // true
// console.log(Object.prototype.isPrototypeOf(baz)); // true

// 將Foo的原型創(chuàng)建到Bar的原型中,將Bar原型創(chuàng)建到Baz的原型中,構(gòu)造原型鏈
// 所以Baz的實(shí)例可以通過(guò)__proto__查找Baz.prototype,并一直向上搜尋到Bar和Foo的原型中。

ES5新增一個(gè)新方法,叫Object.getPrototypeOf(),在所有支持的實(shí)現(xiàn)中,這個(gè)方法返回[[Prototype]]的值。

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

這里第一類(lèi)代碼只是確定Object.getPrototypeOf()返回的對(duì)象實(shí)際就是這個(gè)對(duì)象的原型。
第二行第二類(lèi)代碼取得原型中name屬性的值。

使用Object.getPrototypeOf()可以方便地取得一個(gè)對(duì)象的原型,而這在利用原型實(shí)現(xiàn)繼承的情況下是十分重要的。

代碼讀取某個(gè)對(duì)象屬性時(shí),都是按照一個(gè)搜索鏈執(zhí)行的,首先從對(duì)象本身開(kāi)始,有,則返回,沒(méi)有,繼續(xù)沿著原型鏈向上......這就是原型鏈的通俗抽象描述。

雖然可以通過(guò)對(duì)象實(shí)例訪(fǎng)問(wèn)保存在原型中的值,但是卻不能通過(guò)對(duì)象實(shí)例重寫(xiě)原型中的值。如果我們?cè)趯?shí)例中添加一個(gè)屬性,和實(shí)例原型中的屬性同名,那屬性就會(huì)自動(dòng)屏蔽原型中的那個(gè)屬性。

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

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

person1.name = "Greg";

console.log(person1.name);  // "Greg"————來(lái)自實(shí)例
console.log(person2.name);  // "Nicholas"————來(lái)自原型

添加一個(gè)屬性只會(huì)阻止我們?cè)L問(wèn)原型中的屬性,但不會(huì)修改它。即使將它設(shè)置為null,也只會(huì)在實(shí)例中設(shè)置這個(gè)屬性,而不會(huì)恢復(fù)其指向原型的鏈接。

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

person1.name = null;

console.log(person1.name);  // null
console.log(person1.__proto__.name);    // "Nicholas"

不過(guò),可以使用delete操作符刪除實(shí)例屬性,從而重新訪(fǎng)問(wèn)到原型中的屬性。

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

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

person1.name = "Greg";
console.log(person1.name);    //"Greg" 來(lái)自原型
console.log(person2.name);    // "Nicholas" 來(lái)自原型

delete person1.name;
console.log(person1.name);    // "Nicholas"

delete person1.name;          // 再次刪除name屬性會(huì)發(fā)生什么?
console.log(person1.name);    // "Nicholas"

delete person1.__proto__.name; // 必須通過(guò)__proto__才能訪(fǎng)問(wèn)person1的原型(prototype)
console.log(person1.name);    // undefined

hasOwnProperty
使用hasOwnProperty()方法可以檢測(cè)一個(gè)屬性是存在于實(shí)例中,還是存在于原型中。這個(gè)方法(不要忘了它是從Object繼承來(lái)的)只在給定屬性存在于對(duì)象實(shí)例中時(shí),才會(huì)返回true。

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

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

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

person1.name = "Greg";   // 來(lái)自實(shí)例
console.log(person1.name);
console.log(person1.hasOwnProperty("name"));    // true

console.log(person2.name);    // 來(lái)自原型
console.log(person2.hasOwnProperty("name"));    // false

delete person1.name;
console.log(person1.name);    // "Nicholas" 刪除了實(shí)例中的name屬性,則會(huì)讀取原型中的
console.log(person1.hasOwnProperty("name"));    // false

通過(guò)這個(gè)方法,什么時(shí)候訪(fǎng)問(wèn)的實(shí)例屬性,什么時(shí)候訪(fǎng)問(wèn)的是原型屬性就一清二楚了。調(diào)用person1.hasOwnProperty("name")時(shí),只有當(dāng)person1自身內(nèi)部具有name屬性才會(huì)返回true,因?yàn)橹挥羞@時(shí)候name才是一個(gè)實(shí)例屬性,而非原型屬性。
下圖展示實(shí)現(xiàn)與原型的關(guān)系。



原型與in操作符
有兩種方式使用in操作符:?jiǎn)为?dú)使用和在for in循環(huán)中使用。在單獨(dú)使用中,in操作符會(huì)在通過(guò)對(duì)象能夠訪(fǎng)問(wèn)給定屬性時(shí)返回true,無(wú)論該屬性存在于實(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 person1 = new Person();
var person2 = new Person();

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

person1.name = "Greg";
console.log(person1.name);                      // "Greg" 自有屬性
console.log(person1.hasOwnProperty("name"));    // true
console.log("name" in person1);                 // true

console.log(person2.name);                      // "Nicholas" 繼承屬性
console.log(person2.hasOwnProperty("name"));    // false
console.log("name" in person2);                 // true

delete person1.name;                            // 刪除了自有屬性name
console.log(person1.name);                      // "Nicholas" 繼承屬性
console.log(person1.hasOwnProperty("name"));    // false
console.log("name" in person1);                 // true

以上,name屬性要么是直接在對(duì)象上訪(fǎng)問(wèn)到,要么是通過(guò)原型訪(fǎng)問(wèn)到的。因此調(diào)用in始終返回true,無(wú)論該屬性存在于實(shí)例中還是原型匯總。
同時(shí)使用hasOwnProperty()方法和in操作符,就可以確定該屬性到底是存在于對(duì)象中,還是存在于原型中。

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

在使用for in循環(huán)時(shí),返回的是所有能夠通過(guò)對(duì)象訪(fǎng)問(wèn)、可枚舉的(enumerable)屬性,其中,既包括存在于實(shí)例中的屬性,也包括存在于原型中的屬性。屏蔽了原型中不可枚舉屬性(enumerable屬性為false)的實(shí)例屬性也會(huì)在for-in循環(huán)中返回,因此根據(jù)規(guī)定,所有開(kāi)發(fā)人員定義的屬性都是可枚舉的,只有IE8及更早版本存在例外。
IE早期版本有一個(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ì)顯示
    }
}

當(dāng)以上代碼運(yùn)行時(shí),應(yīng)該會(huì)有一個(gè)警告框,表明找到了toString方法。這里的對(duì)象o定義了一個(gè)名為toString()的方法,該方法屏蔽了原型中(不可枚舉)的toString()方法。在IE中,由于其實(shí)現(xiàn)認(rèn)為原型的toString方法被打上了值為false的enumerable標(biāo)記,因此應(yīng)該跳過(guò)該屬性,結(jié)果我們不會(huì)看到警告框。

要取得對(duì)象上所有可枚舉的實(shí)例屬性,可以使用ECMAScript5的object.keys()方法。這個(gè)方法接收一個(gè)對(duì)象為參數(shù),返回一個(gè)包含所有可枚舉屬性的字符數(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 p1keys = Object.keys(p1);
console.log(p1keys);    // ["name","age"]

變量keys保存一個(gè)數(shù)組,數(shù)組中是可枚舉屬性的集合,順序和在for-in循環(huán)中是一樣的。如果通過(guò)Person的實(shí)例調(diào)用,則object.keys()返回的數(shù)組只包含"name"和”age"兩個(gè)實(shí)例屬性。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容