讀《javaScript高級(jí)程序設(shè)計(jì)-第6章》之封裝類

一、工廠模式

所謂的工廠模式就是,把創(chuàng)建具體對(duì)象的過(guò)程抽象成了一個(gè)函數(shù),每次調(diào)用這個(gè)函數(shù)都會(huì)返回一個(gè)相似的對(duì)象。

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");
person1.sayName();   //"Nicholas"
person2.sayName();   //"Greg"

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

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

js里經(jīng)常如此寫(xiě)var obj=new Object();var arr=new Array();,ObjectArray就是構(gòu)造函數(shù),使用new操作符可以創(chuàng)建相應(yīng)類型的對(duì)象,使用instanceof可以驗(yàn)證對(duì)象的類型,例如:
alert(arr instance Array); //true
構(gòu)造函數(shù)模式就是,自定義像ArrayObject等這樣的構(gòu)造函數(shù),并使用new操作符調(diào)用它來(lái)創(chuàng)建自定義類型對(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");

person1.sayName();   //"Nicholas"
person2.sayName();   //“Greg”
  • new操作符
    使用new操作符調(diào)用,Person就是一個(gè)構(gòu)造函數(shù)
    要?jiǎng)?chuàng)建Person的新實(shí)例,必須使用new操作符。以這種方式調(diào)用構(gòu)造函數(shù)實(shí)際上會(huì)經(jīng)歷一下4個(gè)步驟:
    (1)創(chuàng)建一個(gè)新對(duì)象
    (2)將構(gòu)造函數(shù)的作用域賦給新對(duì)象,即把構(gòu)造函數(shù)的this指向這個(gè)新對(duì)象
    (3)執(zhí)行構(gòu)造函數(shù)中的代碼(為這個(gè)新對(duì)象添加屬性)
    (4)返回新對(duì)象

如果不使用new,Person就是一個(gè)普通的函數(shù),可以正常調(diào)用。例如:

//作為普通函數(shù)在全局作用域下調(diào)用
Person("Greg", 27, "Doctor");  //adds to window
window.sayName();   //“Greg"
//作為普通函數(shù)在另一個(gè)對(duì)象中調(diào)用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName();    //"Kristen"
  • 檢測(cè)類型
    alert(person1 instanceof Object); //true
    alert(person1 instanceof Person);//true

綜上,創(chuàng)建自定義的構(gòu)造函數(shù),意味著將來(lái)可以將它的實(shí)例標(biāo)識(shí)為一種特定的類型(類似于Array類型,Number類型);而這正是構(gòu)造函數(shù)模式勝過(guò)工廠模式的地方。但是構(gòu)造函數(shù)模式也存在缺點(diǎn)。

  • 構(gòu)造函數(shù)模式的問(wèn)題
    使用構(gòu)造函數(shù)的主要問(wèn)題就是,每個(gè)方法都要在每個(gè)實(shí)例上重新創(chuàng)建一遍(實(shí)例化一次Function對(duì)象),浪費(fèi)內(nèi)存。例如,person1和person2都有一個(gè)sayName()的方法,但創(chuàng)建person1和person2時(shí)候,定義sayName這個(gè)方法時(shí)都實(shí)例化了一個(gè)函數(shù)對(duì)象,因此person1.sayName和person2.sayName是不相等的,而事實(shí)上它們又是做的同樣的事情。或者也可以這么說(shuō),person1和person2的sayName()方法做同樣的事情,但卻在創(chuàng)建對(duì)象時(shí)被實(shí)例化了兩次,也就占用了兩倍內(nèi)存。
    雖然可以解決,但并不完美,例如:
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”);
alert(person1.sayName == person2.sayName);  //true 

但是如果共享方法有很多,就需要定義很多個(gè)全局函數(shù),那么我們的自定義的引用類型就絲毫沒(méi)有封裝性可言了。好在,這些問(wèn)題可以通過(guò)使用原型模式解決。

3、原型模式

(1)理解原型對(duì)象

無(wú)論什么時(shí)候,只要?jiǎng)?chuàng)建了一個(gè)新函數(shù),就會(huì)根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個(gè)prototype屬性,這個(gè)屬性就是該函數(shù)的原型對(duì)象。每個(gè)函數(shù)都有一個(gè)原型對(duì)象,所有原型對(duì)象都會(huì)自動(dòng)獲得constructor屬性,constructor指向該函數(shù)(擁有該prototype屬性的函數(shù))。
例如,Person.prototype.constructor指向Person。
創(chuàng)建構(gòu)造函數(shù)后,其原型對(duì)象默認(rèn)只會(huì)取得constructor屬性;至于其他的方法都是從Object繼承來(lái)的(__proto__)。當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)新實(shí)例后,該實(shí)例內(nèi)部將包含一個(gè)指針(__proto__),指向構(gòu)造函數(shù)的原型對(duì)象。(ECMA-262第5版中管這個(gè)指針叫[[Prototype]],但在腳本中沒(méi)有標(biāo)準(zhǔn)的方式訪問(wèn)它。在chrome,safari和firefox中都支持一個(gè)屬性__proto__,但在其他實(shí)現(xiàn)中__proto__對(duì)腳本是不可見(jiàn)的)。所以和實(shí)例有直接關(guān)系的是構(gòu)造函數(shù)的原型對(duì)象,而不是構(gòu)造函數(shù)。


上圖展示了Person構(gòu)造函數(shù)、Person的原型對(duì)象和Person現(xiàn)有的兩個(gè)實(shí)例之間的關(guān)系。

(2)實(shí)例屬性和原型屬性:

原型屬性即構(gòu)造函數(shù)的原型對(duì)象的屬性;實(shí)例屬性即在實(shí)例對(duì)象上直接添加的屬性。
例如:person1.name=“Jone”。
通過(guò)點(diǎn)運(yùn)算符可以訪問(wèn)到實(shí)例的實(shí)例屬性和原型屬性。實(shí)例訪問(wèn)屬性時(shí),腳本會(huì)先搜索實(shí)例屬性,如果找到了,則停止搜索返回實(shí)例屬性的值;如果沒(méi)找到就繼續(xù)搜索原型屬性。所以如果實(shí)例屬性和原型屬性同名,那么原型屬性就會(huì)被屏蔽掉,無(wú)法訪問(wèn)到。
需要注意的是:實(shí)例無(wú)法修改他的原型屬性的值,也無(wú)法修改原型對(duì)象(即不能修改、刪除和增加一個(gè)原型屬性)
(注意:實(shí)例不能修改的是原型屬性的值,但是如果原型屬性指向一個(gè)引用類型,原型屬性的值是存儲(chǔ)這個(gè)引用類型的地址,即不能修改原型屬性指向另一個(gè)對(duì)象,但卻能修改原型屬性指向的對(duì)象里的屬性。下面原型對(duì)象的問(wèn)題里還會(huì)再講到)
如果person1.name=“Jone”這樣寫(xiě),腳本只會(huì)在實(shí)例屬性里創(chuàng)建或修改一個(gè)name=“Jone”的屬性,delete person1.name只會(huì)刪除person1的實(shí)例屬性name(就算實(shí)例沒(méi)有name的實(shí)例屬性,也不會(huì)刪除實(shí)例的原型屬性)。

(3)和原型對(duì)象有關(guān)的幾個(gè)方法

  • isPrototypeOf()
    alert(Person.prototype.isPrototypeOf(person1)); //true
    如果person1[[prototype]] (即__proto__)指向調(diào)用isPrototypeOf的對(duì)象即Person.prototype就會(huì)返回true。
    即判斷Person.prototype是否是person1[[prototype]]

  • Object.getPrototypeOf()
    alert(Object.getPrototypeOf(person1)==Person.prototype); //true
    返回person1這個(gè)對(duì)象的原型[[prototype]]

  • hasOwnProperty()
    person1.hasOwnProperty(“name”); 如果person1.name是來(lái)自于person1的實(shí)例屬性,返回true;如果來(lái)自于person1的原型屬性,則返回false。

(4)原型與in操作符

有兩種方式使用in操作符:
單獨(dú)使用in:alert(“name” in person1); //true
在通過(guò)person1能夠訪問(wèn)給定屬性是返回true,無(wú)論屬性是實(shí)例屬性還是原型屬性。
在for-in循環(huán)中使用:返回的是所有能夠通過(guò)對(duì)象訪問(wèn)的、可枚舉的屬性,其中包括實(shí)例屬性也包括原型屬性。

  • Object.keys()
    接受一個(gè)對(duì)象作為參數(shù),返回一個(gè)包含對(duì)象的所有可枚舉屬性的字符串?dāng)?shù)組。
    如果對(duì)象是一個(gè)實(shí)例,則只返回實(shí)例的實(shí)例屬性而不包含原型屬性
  • Object.getOwnPropertyNames()
 var keys = Object.getOwnPropertyNames(Person.prototype);
 alert(keys);   //"constructor,name,age,job,sayName”

得到對(duì)象的所有實(shí)例屬性,無(wú)論它是否可枚舉

(5)更簡(jiǎn)單的原型語(yǔ)法

所謂的更簡(jiǎn)單的原型寫(xiě)法就是用字面量的形式來(lái)定義構(gòu)造函數(shù)的原型對(duì)象,如下:

function Person(){
}

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

};

var friend = new Person();

alert(friend instanceof Object);  //true
alert(friend instanceof Person);  //true
alert(friend.constructor == Person);  //false
alert(friend.constructor == Object);  //true

這樣定義完了之后,Person.prototype這個(gè)對(duì)象就被重寫(xiě)了,導(dǎo)致它的constructor這個(gè)屬性的指向變成了Object,而不是Person
(解釋:Person.prototypeObject的一個(gè)實(shí)例,所以它有一個(gè)原型屬性constructor指向Object。Person被創(chuàng)建時(shí),它的原型對(duì)象Person.prototype自動(dòng)獲得了一個(gè)constructor的屬性,指向Person,這個(gè)屬性是對(duì)象的實(shí)例的實(shí)例屬性,所以會(huì)屏蔽掉對(duì)象的原型屬性,所以說(shuō)Person.prototype.constructor是指向Person的。但是用字面量重寫(xiě)了Person.prototype后,Person.prototype仍是Object的一個(gè)實(shí)例,所以它有一個(gè)原型屬性constructor指向Object,但它沒(méi)有了指向Person的實(shí)例屬性constructor,所以在訪問(wèn)Person.prototype.constructor時(shí),就是訪問(wèn)了Person.prototype對(duì)象的原型屬性,指向了Object)。
但我們可以再把它定義進(jìn)這個(gè)對(duì)象字面量里手動(dòng)指向Person,即給Person.prototype這個(gè)對(duì)象的實(shí)例加一個(gè)實(shí)例屬性constructor,指向Person。如下:

function Person(){
}

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

我們知道如此定義對(duì)象,對(duì)象的屬性的[[enumerable]]特性默認(rèn)是true。而默認(rèn)情況下,原聲的原型對(duì)象的constructor屬性是不可枚舉的,因此如果你使用兼容ES5的javaScript引擎,可以使用Object.defineProperty()來(lái)設(shè)置constructor屬性。如下:

//重設(shè)構(gòu)造函數(shù),只適用于ES5兼容的瀏覽器
Object.difineProperty(Person.prototype,”constructor”,{
    enumerable:false,
    value:Person
});

(6)原型的動(dòng)態(tài)性

簡(jiǎn)單點(diǎn)來(lái)說(shuō),就是實(shí)例的[[prototype]]是指向構(gòu)造函數(shù)的原型對(duì)象,而不是構(gòu)造函數(shù)。只要你明白這一點(diǎn),原型的動(dòng)態(tài)性就好理解了。
第一種情況:Person.prototype可以在任意地方增加修改或刪除屬性,實(shí)例可以實(shí)時(shí)的訪問(wèn)最新的原型屬性。因?yàn)槊看螌?shí)例訪問(wèn)屬性,都是一次搜索的過(guò)程,搜索原型屬性時(shí)是到實(shí)例的[[prototype]]指向的對(duì)象里查找。實(shí)例的[[prototype]]是一個(gè)指針,Person.prototype也是一個(gè)指針,指向的是同一個(gè)地址,也就是說(shuō)修改和查找都在同一個(gè)地方,那么查找到的值自然就是最新實(shí)時(shí)的了。

function Person(){
}
var friend = new Person();
Person.prototype.sayHi = function(){
    alert("hi");
};
friend.sayHi();   //"hi"

第二種情況:在實(shí)例被創(chuàng)建之后,Person.prototype被重寫(xiě)了

function Person(){
}

var friend = new Person();
       
Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};
friend.sayName();   //error

這種情況是因?yàn)椋簩?shí)例一旦被創(chuàng)建,實(shí)例的[[prototype]]存儲(chǔ)的地址就確定了,指向的對(duì)象地址就確定了,如果你改變這個(gè)地址里的對(duì)象,實(shí)例都可以訪問(wèn)的到。但是如果在實(shí)例被創(chuàng)建之后,重寫(xiě)Person.prototype,就相當(dāng)于是把Person.prototype指向了一個(gè)新的對(duì)象,而實(shí)例的[[prototype]]還是指向原來(lái)的對(duì)象,所以實(shí)例訪問(wèn)的原型屬性還是要在原來(lái)的對(duì)象里查找,原來(lái)的對(duì)象里并沒(méi)有sayName這個(gè)方法,因此會(huì)報(bào)錯(cuò)。

(7)原生對(duì)象的原型

我們用原型模式創(chuàng)建自定義類型,讓自定義類型和原生類型一樣使用。其實(shí)所有的原生的對(duì)象(Object、Array、String,等等)也是采用的原型模式創(chuàng)建的。所有原生的引用類型都在其構(gòu)造函數(shù)的原型上定義了方法。
例如,在Array.prototype中可以找到sort()方法,而在String.prototype中可以找到substring()方法。
通過(guò)原生對(duì)象的原型,不僅可以取得所有默認(rèn)方法的引用,也可以定義新的方法??梢韵裥薷淖远x對(duì)象的原型一樣修改原生對(duì)象的原型,因此可以隨時(shí)添加方法。但是不建議如此做(在支持該方法的實(shí)現(xiàn)中運(yùn)行代碼時(shí)會(huì)導(dǎo)致命名沖突,或者意外重寫(xiě)了原生方法)。

(7)原型對(duì)象的問(wèn)題

首先,原型模式省略了為構(gòu)造函數(shù)傳遞參數(shù),初始化實(shí)例的環(huán)節(jié),使得所有實(shí)例默認(rèn)時(shí)都是一樣的。
其次,原型模式的共享本性使得所有的實(shí)例都能共享它的屬性。

如果屬性值是函數(shù)或者是基本值時(shí),實(shí)例不能修改原型屬性的值,只會(huì)為該實(shí)例增加一個(gè)同名屬性,然后屏蔽掉同名原型屬性,這樣其它的實(shí)例都不會(huì)受到影響,使用的仍然是原型屬性原來(lái)的值。
如果屬性值是引用類型,實(shí)例雖不能修改原型屬性的值(這個(gè)值就是指向的對(duì)象的地址),即實(shí)例不能讓這個(gè)原型屬性重新指向另一個(gè)對(duì)象,但是卻可以修改指向的對(duì)象的屬性,這就會(huì)導(dǎo)致其它實(shí)例再訪問(wèn)這個(gè)對(duì)象時(shí),對(duì)象已被修改了。
例如:

function Person(){
}

Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    friends : ["Shelby", "Court"],
    sayName : function () {
        alert(this.name);
    }
};

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

person1.friends.push("Van");

alert(person1.friends);    //"Shelby,Court,Van"
alert(person2.friends);    //"Shelby,Court,Van"
alert(person1.friends === person2.friends);  //true

這樣就違反了我們希望實(shí)例擁有屬于自己的全部屬性的初衷

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

綜合前面所說(shuō)的,我們發(fā)現(xiàn)構(gòu)造函數(shù)模式優(yōu)點(diǎn)在于能向構(gòu)造函數(shù)傳遞,定義屬于實(shí)例自己的實(shí)例屬性。原型模式優(yōu)點(diǎn)在于共享著對(duì)方法的引用,原型屬性是所有實(shí)例所共享的。
所以創(chuàng)建自定義類型的最常見(jiàn)方式,就是組合使用構(gòu)造函數(shù)模式與原型模式
例如:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
}

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

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

person1.friends.push("Van");

alert(person1.friends);    //"Shelby,Court,Van"
alert(person2.friends);    //"Shelby,Court"
alert(person1.friends === person2.friends);  //false
alert(person1.sayName === person2.sayName);  //true

5、動(dòng)態(tài)原型模式

這一小節(jié),私以為了解了解就好,只要你理解了上面所說(shuō)的構(gòu)造函數(shù)模式和原型模式的原理,那么原型屬性的定義你可以隨心所欲,只要符合你的預(yù)期就好。你高興就好,代碼高興就好。

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

與工廠模式的區(qū)別是使用new 調(diào)用。不使用new調(diào)用,它就是工廠模式。
這一小節(jié),私以為了解了解就好。

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

與工廠模式的區(qū)別是對(duì)象定義的方法不使用this,構(gòu)造函數(shù)傳進(jìn)來(lái)的參數(shù)不向外直接暴露。
這一小節(jié),私以為了解了解就好。

好了,封裝類的幾種方式已經(jīng)介紹完了。我的觀點(diǎn)是理解了對(duì)象和構(gòu)造函數(shù)模式以及原型模式,就可以隨機(jī)應(yīng)變了。不需要記住什么什么各種模式的,無(wú)非就是使用對(duì)象的場(chǎng)景不同。要理解對(duì)象和構(gòu)造函數(shù)以及原型對(duì)象,靈活變換,無(wú)招勝有招才好。

這是我讀《javaScript高級(jí)程序設(shè)計(jì)》這本書(shū)的第6章面向?qū)ο蟮某绦蛟O(shè)計(jì),做的筆記,在本篇之前還有一篇理解對(duì)象的筆記,后面還有一篇繼承的筆記。發(fā)現(xiàn)問(wèn)題的小伙伴歡迎指出。

最后編輯于
?著作權(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)容