JS中創(chuàng)建自定義類型的常用模式總結(jié)【工廠模式,構(gòu)造函數(shù)模式,原型模式,動(dòng)態(tài)原型模式等】
本文實(shí)例講述了JS中創(chuàng)建自定義類型的常用模式。分享給大家供大家參考,具體如下:
雖然在 ES6 中,已經(jīng)出了 class 的語(yǔ)法,貌似好像不用了解 ES5 中的這些老東西了,但是越深入學(xué)習(xí),你會(huì)發(fā)現(xiàn)理解這些模式的重要性。
在本文中,我會(huì)描述 7 種常用的創(chuàng)建自定義類型的模式:工廠模式、構(gòu)造函數(shù)模式、原型模式、組合使用構(gòu)造函數(shù)模式、動(dòng)態(tài)原型模式、寄生構(gòu)造函數(shù)模式、穩(wěn)妥構(gòu)造函數(shù)模式。分別給出他們的示例代碼,并分析他們的利弊,方便讀者選擇具體的方式來(lái)構(gòu)建自己的自定義類型。
最后,我會(huì)指出 ES6 中的 class 語(yǔ)法,本質(zhì)上其實(shí)還是利用了組合使用構(gòu)造函數(shù)模式進(jìn)行創(chuàng)建自定義類型。
1. 工廠模式
廢話不多說(shuō),先上工廠模式的實(shí)例代碼:
function createPerson(name, age, job){ var o = new Object(); // 創(chuàng)建對(duì)象 o.name = name; // 賦予對(duì)象細(xì)節(jié) o.age = age; // 賦予對(duì)象細(xì)節(jié) o.job = job; // 賦予對(duì)象細(xì)節(jié) o.sayName = function(){ // 賦予對(duì)象細(xì)節(jié) alert(this.name); }; return o; // 返回該對(duì)象 } var person1 = createPerson("Nicholas", 29, "Software Engineer"); var person2 = createPerson("Greg", 27, "Doctor");
優(yōu)點(diǎn):解決了創(chuàng)建多個(gè)相似對(duì)象的問(wèn)題;
缺點(diǎn):沒(méi)有解決對(duì)象識(shí)別的問(wèn)題(即不知道這個(gè)對(duì)象是什么類型),對(duì)于對(duì)象的方法沒(méi)有做到復(fù)用。
2. 構(gòu)造函數(shù)模式
function Person(name, age, job){ this.name = name; // 對(duì)象的所有細(xì)節(jié)全部掛載在 this 對(duì)象下面 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");
說(shuō)到構(gòu)造函數(shù)模式就不得不提到 new 操作符了。我們來(lái)看看 new 這個(gè)操作符到底做了什么:
① 創(chuàng)建一個(gè)對(duì)象;
② 將構(gòu)造函數(shù)內(nèi)的 this 指向這個(gè)新創(chuàng)建的對(duì)象,同時(shí)將該函數(shù)的 prototype 的引用掛載在新對(duì)象的原型下;
③ 執(zhí)行函數(shù)內(nèi)的細(xì)節(jié),也就是將屬性和方法掛載在新對(duì)象下;
④ 隱式的返回新創(chuàng)建的對(duì)象。
優(yōu)點(diǎn):解決了對(duì)象識(shí)別的問(wèn)題;
缺點(diǎn):對(duì)于自定義類型的方法每次都要新創(chuàng)建一個(gè)方法函數(shù)實(shí)例,沒(méi)有做到函數(shù)復(fù)用。如果把所有方法函數(shù)寫到父級(jí)作用域中,是做到了函數(shù)復(fù)用,但同時(shí)方法函數(shù)只能在父級(jí)作用域的某個(gè)類型中進(jìn)行調(diào)用,這對(duì)于父級(jí)作用域有點(diǎn)名不副實(shí),同時(shí)對(duì)于自定義引用類型沒(méi)有封裝性可言。
3. 原型模式
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
理解要點(diǎn):
① 無(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è)屬性包含一個(gè)指向 prototype 屬性所在函數(shù)的指針。至于原型中的其他方法則都是從 Object 繼承而來(lái)。
③ 當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建了一個(gè)新實(shí)例后,該實(shí)例的內(nèi)部將包含一個(gè)指針 [[prototype]](內(nèi)部屬性) ,指向構(gòu)造函數(shù)的原型對(duì)象。
④ 當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)新實(shí)例后,該實(shí)例的實(shí)例環(huán)境,即構(gòu)造函數(shù),會(huì)針對(duì)原型對(duì)象上的非引用類型的原型屬性,在構(gòu)造函數(shù)中自動(dòng)構(gòu)建相應(yīng)的實(shí)例環(huán)境屬性。也就是說(shuō),之后根據(jù)構(gòu)造函數(shù)創(chuàng)建的實(shí)例,它的實(shí)例屬性中的非引用類型屬性,都仍是根據(jù)構(gòu)造函數(shù)中的實(shí)例環(huán)境屬性創(chuàng)建的。
但是為減少不必要的輸入,也為了從視覺(jué)上更好地封裝原型的功能,更常見的做法是用一個(gè)包含所有屬性和方法的對(duì)象字面量來(lái)重寫整個(gè)原型對(duì)象。如下所示:
function Person(){ } Person.prototype = { name : "Nicholas", age : 29, job: "Software Engineer", sayName : function () { alert(this.name); } };
但是這種寫法,其本質(zhì)上完全重寫了默認(rèn)的 prototype 對(duì)象,因此 constrctor 屬性也就變成了新對(duì)象的 constructor 屬性(指向 Object 構(gòu)造函數(shù)),不在指向 Person 函數(shù)。盡管此時(shí),instanceOf 操作符還能返回正確的結(jié)果。
如果 constructor 屬性真的很重要,可以像下面這樣特意將它設(shè)置回適當(dāng)?shù)闹担?/p>
function Person(){ } Person.prototype = { constructor : Person, name : "Nicholas", age : 29, job: "Software Engineer", sayName : function () { alert(this.name); } };
注意,以這種方式重設(shè) constructor 屬性會(huì)導(dǎo)致他的 [[Enumerable]] 特性被設(shè)置為 true 。默認(rèn)情況下,原生的 constructor 屬性是不可枚舉的,因此,如果你使用兼容 ECMAScript 5 的 JavaScript 引擎,你可以試試 Object.defineProperty()
方法:
function Person(){ } Person.prototype = { name : "Nicholas", age : 29, job : "Software Engineer", sayName : function () { alert(this.name); } }; //重設(shè)構(gòu)造函數(shù),只適用于 ECMAScript 5 兼容的瀏覽器 Object.defineProperty( Person.prototype, "constructor", { enumerable: false, value: Person });
注意,重寫原型對(duì)象會(huì)切斷新原型與已經(jīng)存在的對(duì)象實(shí)例之間的聯(lián)系;它們引用的仍然是最初的原型。
優(yōu)點(diǎn):對(duì)自定義類型的方法解決了函數(shù)復(fù)用的問(wèn)題。
缺點(diǎn):
① 不能為構(gòu)造函數(shù)傳遞初始化參數(shù);
② 原型模式中實(shí)現(xiàn)了對(duì)于包含引用類型值的屬性的共享,這就意味著一個(gè)實(shí)例中修改了該引用類型值,所有實(shí)例的該屬性都會(huì)被修改!?。?/p>
4. 組合使用構(gòu)造函數(shù)模式和原型模式
在組合使用構(gòu)造函數(shù)模式和原型模式中,構(gòu)造函數(shù)模式用于定義實(shí)例屬性,而原型模式用于定義方法和共享的屬性,而且還支持向構(gòu)造函數(shù)傳遞參數(shù)。如以下示例代碼所示:
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.friends = ["Shelby", "Court"]; } Person.prototype = { sayName : function(){ alert(this.name); } } Object.defineProperty( Person.prototype, "constructor", { enumerable: false, value: Person ); var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor"); person1.friends.push("Van"); alert(person1.friends); //"Shelby,Count,Van" alert(person2.friends); //"Shelby,Count" alert(person1.friends === person2.friends); //false alert(person1.sayName === person2.sayName); //true
優(yōu)點(diǎn):能為構(gòu)造函數(shù)傳遞初始化參數(shù);該復(fù)用復(fù)用,不該復(fù)用的沒(méi)復(fù)用。
缺點(diǎn):封裝性不好,構(gòu)造函數(shù)和原型分別獨(dú)立于父級(jí)作用域進(jìn)行申明。
5. 動(dòng)態(tài)原型模式(推薦)
該模式把所有信息都封裝在構(gòu)造函數(shù)中,通過(guò)構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)初始化原型 (僅在必要的情況下),又保持了同時(shí)使用構(gòu)造函數(shù)和原型的優(yōu)點(diǎn)。請(qǐng)看以下示例代碼:
function Person(name, age, job){ //屬性 this.name = name; this.age = age; this.job = job; //方法 if (typeof this.sayAge != "function"){ // 此處應(yīng)該永遠(yuǎn)去判斷新添加的屬性和方法 Person.prototype.sayName = function(){ alert(this.name); }; Person.prototype.sayAge = function(){ alert(this.age); }; } } var friend = new Person("Nicholas", 29, "Software Engineer"); friend.sayName();
if 語(yǔ)句檢查的可以是初始化之后應(yīng)該存在的任何屬性或方法——不必用一大堆 if 語(yǔ)句檢查每個(gè)屬性和每個(gè)方法;只要檢查其中一個(gè)即可。
注意,使用動(dòng)態(tài)原型模式時(shí),不能使用對(duì)象字面量重寫原型。前面已經(jīng)解釋過(guò)了,如果已經(jīng)創(chuàng)建的實(shí)例的情況下重寫原型,那么就會(huì)切斷新原型與現(xiàn)有實(shí)例之間的聯(lián)系。
優(yōu)點(diǎn):封裝性非常好;還可使用 instanceOf
操作符確定它的類型。
缺點(diǎn):無(wú)。
6. 寄生構(gòu)造函數(shù)模式
除了使用 new 操作符并把使用的包裝函數(shù)叫做構(gòu)造函數(shù)之外,這個(gè)模式跟工廠模式其實(shí)是一模一樣的。請(qǐng)看以下代碼:
function Person(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 friend = new Person("Nicholas", 29, "Software Engineer"); friend.sayName(); //"Nicholas"
在使用 new
操作符下,構(gòu)造函數(shù)在不返回值的情況下,默認(rèn)會(huì)返回新對(duì)象實(shí)例。而通過(guò)在構(gòu)造函數(shù)的末尾添加一個(gè) return 語(yǔ)句,可以重寫調(diào)用構(gòu)造函數(shù)時(shí)返回的值。
缺點(diǎn):沒(méi)有解決對(duì)象識(shí)別的問(wèn)題(即不知道這個(gè)對(duì)象是什么類型),不能依賴 instanceOf
操作符來(lái)確定對(duì)象類型;對(duì)于對(duì)象的方法沒(méi)有做到復(fù)用。
7. 穩(wěn)妥構(gòu)造函數(shù)模式
先來(lái)了解下穩(wěn)妥對(duì)象:指的是沒(méi)有公共屬性,而且其方法也不引用 this 的對(duì)象。穩(wěn)妥對(duì)象最適合在一些安全的環(huán)境中 (這些環(huán)境中會(huì)禁止使用 this 和 new),或者再防止數(shù)據(jù)被其他應(yīng)用程序 (如 Mashup 程序) 改動(dòng)時(shí)使用。穩(wěn)妥構(gòu)造函數(shù)遵循與寄生構(gòu)造函數(shù)類似的模式,但有兩點(diǎn)不同:一是新創(chuàng)建對(duì)象的實(shí)例方法不引用 this;二是不使用 new 操作符調(diào)用構(gòu)造函數(shù)。以下為示例代碼:
function Person(name, age, job){ var o = new Object(); //創(chuàng)建要返回的對(duì)象 //可以在這里定義私有變量和函數(shù) o.sayName = function(){ //添加方法 alert(name); }; return o; //返回對(duì)象 } var friend = Person("Nicholas", 29, "Software Engineer"); friend.sayName(); //"Nicholas"
其原理就是利用閉包,保有對(duì)私有變量和私有方法的引用。
優(yōu)點(diǎn):不可能有別的方法訪問(wèn)到傳入到構(gòu)造函數(shù)中的原始數(shù)據(jù)。
缺點(diǎn):沒(méi)有解決對(duì)象識(shí)別的問(wèn)題(即不知道這個(gè)對(duì)象是什么類型),不能依賴 instanceOf
操作符來(lái)確定對(duì)象類型;對(duì)于對(duì)象的方法沒(méi)有做到復(fù)用。
8. ES6 中的 class
咱們這塊以 class 實(shí)例來(lái)展開講述:
class Parent { name = "qck"; sex = "male"; //實(shí)例變量 sayHello(name){ console.log('qck said Hello!',name); } constructor(location){ this.location = location; } }
我們來(lái)看看這段代碼通過(guò) babel 編譯后的 _createClass 函數(shù):
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; // 對(duì)屬性進(jìn)行數(shù)據(jù)特性設(shè)置 descriptor.enumerable = descriptor.enumerable || false; // enumerable設(shè)置 descriptor.configurable = true; // configurable設(shè)置 if ("value" in descriptor) descriptor.writable = true; // 如果有value,那么可寫 Object.defineProperty(target, descriptor.key, descriptor); // 調(diào)用defineProperty() 進(jìn)行屬性設(shè)置 } } return function (Constructor, protoProps, staticProps) { // 設(shè)置到第一個(gè) Constructor 的 prototype 中 if (protoProps) defineProperties(Constructor.prototype, protoProps); // 設(shè)置 Constructor 的 static 類型屬性 if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
首先該方法是一個(gè)自執(zhí)行函數(shù),接收的一參是構(gòu)造函數(shù)本身,二參是為構(gòu)造函數(shù)的原型對(duì)象需要添加的方法或者屬性,三參是需要為構(gòu)造函數(shù)添加的靜態(tài)屬性對(duì)象。從這個(gè)函數(shù)就可以看出 class 在創(chuàng)建自定義類型時(shí),用了原型模式。
我們看看編譯后的結(jié)果是如何調(diào)用 _createClass 的:
var Parent = function () { // 這里是自執(zhí)行函數(shù) _createClass(Parent, [{ // Parent的實(shí)例方法,通過(guò)修改Parent.prototype來(lái)完成 key: "sayHello", value: function sayHello(name) { console.log('qck say Hello!', name); } }]); function Parent(location) { //在Parent構(gòu)造函數(shù)中添加實(shí)例屬性 _classCallCheck(this, Parent); this.name = "qck"; this.sex = "male"; this.location = location; } return Parent; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
這里調(diào)用 _createClass 的地方就證實(shí)了我們剛才的想法——確實(shí)應(yīng)用了原型模式:我們的 class 上的方法,其實(shí)是通過(guò)修改該類 (實(shí)際上是函數(shù)) 的 prototype 來(lái)完成的。
而通過(guò)返回的構(gòu)造函數(shù),我們可以發(fā)現(xiàn):實(shí)例屬性還是通過(guò)構(gòu)造函數(shù)方式來(lái)添加的。
最后,我們來(lái)看看 _classCallCheck 方法,它其實(shí)是一層校驗(yàn),保證了我們的實(shí)例對(duì)象是特定的類型。
所以,綜上所述,ES6 中的 class 只是個(gè)語(yǔ)法糖,它本質(zhì)上還是用組合使用構(gòu)造函數(shù)模式創(chuàng)建自定義類型的,這也就是為什么我們要學(xué)上面那些知識(shí)的初衷。
感興趣的朋友還可以使用本站在線HTML/CSS/JavaScript代碼運(yùn)行工具:http://tools.jb51.net/code/HtmlJsRun測(cè)試上述代碼運(yùn)行結(jié)果。
更多關(guān)于JavaScript相關(guān)內(nèi)容還可查看本站專題:《javascript面向?qū)ο笕腴T教程》、《JavaScript錯(cuò)誤與調(diào)試技巧總結(jié)》、《JavaScript數(shù)據(jù)結(jié)構(gòu)與算法技巧總結(jié)》、《JavaScript遍歷算法與技巧總結(jié)》及《JavaScript數(shù)學(xué)運(yùn)算用法總結(jié)》
希望本文所述對(duì)大家JavaScript程序設(shè)計(jì)有所幫助。
- JavaScript創(chuàng)建對(duì)象方式總結(jié)【工廠模式、構(gòu)造函數(shù)模式、原型模式等】
- 詳解js產(chǎn)生對(duì)象的3種基本方式(工廠模式,構(gòu)造函數(shù)模式,原型模式)
- js面向?qū)ο笾R妱?chuàng)建對(duì)象的幾種方式(工廠模式、構(gòu)造函數(shù)模式、原型模式)
- JS面向?qū)ο蠡A(chǔ)講解(工廠模式、構(gòu)造函數(shù)模式、原型模式、混合模式、動(dòng)態(tài)原型模式)
- JS創(chuàng)建對(duì)象常用設(shè)計(jì)模式工廠構(gòu)造函數(shù)及原型
相關(guān)文章
從setTimeout看js函數(shù)執(zhí)行過(guò)程
這篇文章主要介紹了從setTimeout看js函數(shù)執(zhí)行過(guò)程,需要的朋友可以參考下2017-12-12在JavaScript中獲取請(qǐng)求的URL參數(shù)
在ASP.NET后臺(tái)代碼中,對(duì)于這樣的URL請(qǐng)求地址:http://www.abc.com?id=001,我們可以通過(guò)Request.QueryString["id"]的方法很容易的獲取到URL中請(qǐng)求的參數(shù)的值,但是要在前臺(tái)js代碼中獲取請(qǐng)求的參數(shù)的值,應(yīng)該怎么做呢?2010-12-12JavaScript實(shí)現(xiàn)基礎(chǔ)排序算法的示例詳解
這篇文章主要為大家詳細(xì)介紹了如何利用JavaScript實(shí)現(xiàn)基礎(chǔ)排序算法,如:冒泡排序、選擇排序、插入排序和快速排序,感興趣的可以了解一下2022-06-06js實(shí)現(xiàn)的簡(jiǎn)單radio背景顏色選擇器代碼
這篇文章主要介紹了js實(shí)現(xiàn)的簡(jiǎn)單radio背景顏色選擇器代碼,利用鼠標(biāo)事件及頁(yè)面元素動(dòng)態(tài)操作實(shí)現(xiàn)頁(yè)面背景顏色的改變功能,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-08-08Canvas 制作動(dòng)態(tài)進(jìn)度加載水球詳解及實(shí)例代碼
這篇文章主要介紹了Canvas 制作動(dòng)態(tài)進(jìn)度加載水球詳解及實(shí)例代碼的相關(guān)資料,這里附有實(shí)例代碼及實(shí)現(xiàn)效果圖,需要的朋友可以參考下2016-12-12js實(shí)現(xiàn)計(jì)算器和計(jì)時(shí)器功能
這篇文章主要為大家詳細(xì)介紹了js實(shí)現(xiàn)計(jì)算器和計(jì)時(shí)器功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07鼠標(biāo)拖拽移動(dòng)子窗體的JS實(shí)現(xiàn)
這篇文章主要介紹了鼠標(biāo)拖拽移動(dòng)子窗體的JS實(shí)現(xiàn),需要的朋友可以參考下2014-02-02JS實(shí)現(xiàn)的相冊(cè)圖片左右滾動(dòng)完整實(shí)例
這篇文章主要介紹了JS實(shí)現(xiàn)的相冊(cè)圖片左右滾動(dòng)效果,結(jié)合完整實(shí)例形式分析了javascript事件觸發(fā)與頁(yè)面元素屬性動(dòng)態(tài)變換的相關(guān)操作技巧,需要的朋友可以參考下2016-11-11