本文實(shí)例講述了JS中創(chuàng)建自定義類型的常用模式。分享給大家供大家參考,具體如下:
為彝良等地區(qū)用戶提供了全套網(wǎng)頁設(shè)計(jì)制作服務(wù),及彝良網(wǎng)站建設(shè)行業(yè)解決方案。主營業(yè)務(wù)為成都網(wǎng)站建設(shè)、成都網(wǎng)站制作、彝良網(wǎng)站設(shè)計(jì),以傳統(tǒng)方式定制建設(shè)網(wǎng)站,并提供域名空間備案等一條龍服務(wù),秉承以專業(yè)、用心的態(tài)度為用戶提供真誠的服務(wù)。我們深信只要達(dá)到每一位用戶的要求,就會得到認(rèn)可,從而選擇與我們長期合作。這樣,我們也可以走得更遠(yuǎn)!
雖然在 ES6 中,已經(jīng)出了 class 的語法,貌似好像不用了解 ES5 中的這些老東西了,但是越深入學(xué)習(xí),你會發(fā)現(xiàn)理解這些模式的重要性。
在本文中,我會描述 7 種常用的創(chuàng)建自定義類型的模式:工廠模式、構(gòu)造函數(shù)模式、原型模式、組合使用構(gòu)造函數(shù)模式、動態(tài)原型模式、寄生構(gòu)造函數(shù)模式、穩(wěn)妥構(gòu)造函數(shù)模式。分別給出他們的示例代碼,并分析他們的利弊,方便讀者選擇具體的方式來構(gòu)建自己的自定義類型。
最后,我會指出 ES6 中的 class 語法,本質(zhì)上其實(shí)還是利用了組合使用構(gòu)造函數(shù)模式進(jìn)行創(chuàng)建自定義類型。
1. 工廠模式
廢話不多說,先上工廠模式的實(shí)例代碼:
function createPerson(name, age, job){ var o = new Object(); // 創(chuàng)建對象 o.name = name; // 賦予對象細(xì)節(jié) o.age = age; // 賦予對象細(xì)節(jié) o.job = job; // 賦予對象細(xì)節(jié) o.sayName = function(){ // 賦予對象細(xì)節(jié) alert(this.name); }; return o; // 返回該對象 } var person1 = createPerson("Nicholas", 29, "Software Engineer"); var person2 = createPerson("Greg", 27, "Doctor");
優(yōu)點(diǎn):解決了創(chuàng)建多個相似對象的問題;
缺點(diǎn):沒有解決對象識別的問題(即不知道這個對象是什么類型),對于對象的方法沒有做到復(fù)用。
2. 構(gòu)造函數(shù)模式
function Person(name, age, job){ this.name = name; // 對象的所有細(xì)節(jié)全部掛載在 this 對象下面 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");
說到構(gòu)造函數(shù)模式就不得不提到 new 操作符了。我們來看看 new 這個操作符到底做了什么:
① 創(chuàng)建一個對象;
② 將構(gòu)造函數(shù)內(nèi)的 this 指向這個新創(chuàng)建的對象,同時將該函數(shù)的 prototype 的引用掛載在新對象的原型下;
③ 執(zhí)行函數(shù)內(nèi)的細(xì)節(jié),也就是將屬性和方法掛載在新對象下;
④ 隱式的返回新創(chuàng)建的對象。
優(yōu)點(diǎn):解決了對象識別的問題;
缺點(diǎn):對于自定義類型的方法每次都要新創(chuàng)建一個方法函數(shù)實(shí)例,沒有做到函數(shù)復(fù)用。如果把所有方法函數(shù)寫到父級作用域中,是做到了函數(shù)復(fù)用,但同時方法函數(shù)只能在父級作用域的某個類型中進(jìn)行調(diào)用,這對于父級作用域有點(diǎn)名不副實(shí),同時對于自定義引用類型沒有封裝性可言。
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):
① 無論什么時候,只要創(chuàng)建了一個新函數(shù),就會根據(jù)一組特定規(guī)則為該函數(shù)創(chuàng)建一個 prototype 屬性,這個屬性指向函數(shù)的原型對象。
② 在默認(rèn)情況下,所有原型對象都會自動獲得一個 constructor 屬性,這個屬性包含一個指向 prototype 屬性所在函數(shù)的指針。至于原型中的其他方法則都是從 Object 繼承而來。
③ 當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建了一個新實(shí)例后,該實(shí)例的內(nèi)部將包含一個指針 [[prototype]](內(nèi)部屬性) ,指向構(gòu)造函數(shù)的原型對象。
④ 當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個新實(shí)例后,該實(shí)例的實(shí)例環(huán)境,即構(gòu)造函數(shù),會針對原型對象上的非引用類型的原型屬性,在構(gòu)造函數(shù)中自動構(gòu)建相應(yīng)的實(shí)例環(huán)境屬性。也就是說,之后根據(jù)構(gòu)造函數(shù)創(chuàng)建的實(shí)例,它的實(shí)例屬性中的非引用類型屬性,都仍是根據(jù)構(gòu)造函數(shù)中的實(shí)例環(huán)境屬性創(chuàng)建的。
但是為減少不必要的輸入,也為了從視覺上更好地封裝原型的功能,更常見的做法是用一個包含所有屬性和方法的對象字面量來重寫整個原型對象。如下所示:
function Person(){ } Person.prototype = { name : "Nicholas", age : 29, job: "Software Engineer", sayName : function () { alert(this.name); } };
但是這種寫法,其本質(zhì)上完全重寫了默認(rèn)的 prototype 對象,因此 constrctor 屬性也就變成了新對象的 constructor 屬性(指向 Object 構(gòu)造函數(shù)),不在指向 Person 函數(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 屬性會導(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 });
注意,重寫原型對象會切斷新原型與已經(jīng)存在的對象實(shí)例之間的聯(lián)系;它們引用的仍然是最初的原型。
優(yōu)點(diǎn):對自定義類型的方法解決了函數(shù)復(fù)用的問題。
缺點(diǎn):
① 不能為構(gòu)造函數(shù)傳遞初始化參數(shù);
② 原型模式中實(shí)現(xiàn)了對于包含引用類型值的屬性的共享,這就意味著一個實(shí)例中修改了該引用類型值,所有實(shí)例的該屬性都會被修改?。。?/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ù)用的沒復(fù)用。
缺點(diǎn):封裝性不好,構(gòu)造函數(shù)和原型分別獨(dú)立于父級作用域進(jìn)行申明。
5. 動態(tài)原型模式(推薦)
該模式把所有信息都封裝在構(gòu)造函數(shù)中,通過構(gòu)造函數(shù)來實(shí)現(xiàn)初始化原型 (僅在必要的情況下),又保持了同時使用構(gòu)造函數(shù)和原型的優(yōu)點(diǎn)。請看以下示例代碼:
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īng)該存在的任何屬性或方法——不必用一大堆 if 語句檢查每個屬性和每個方法;只要檢查其中一個即可。
注意,使用動態(tài)原型模式時,不能使用對象字面量重寫原型。前面已經(jīng)解釋過了,如果已經(jīng)創(chuàng)建的實(shí)例的情況下重寫原型,那么就會切斷新原型與現(xiàn)有實(shí)例之間的聯(lián)系。
優(yōu)點(diǎn):封裝性非常好;還可使用 instanceOf
操作符確定它的類型。
缺點(diǎn):無。
6. 寄生構(gòu)造函數(shù)模式
除了使用 new 操作符并把使用的包裝函數(shù)叫做構(gòu)造函數(shù)之外,這個模式跟工廠模式其實(shí)是一模一樣的。請看以下代碼:
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)會返回新對象實(shí)例。而通過在構(gòu)造函數(shù)的末尾添加一個 return 語句,可以重寫調(diào)用構(gòu)造函數(shù)時返回的值。
缺點(diǎn):沒有解決對象識別的問題(即不知道這個對象是什么類型),不能依賴 instanceOf
操作符來確定對象類型;對于對象的方法沒有做到復(fù)用。
7. 穩(wěn)妥構(gòu)造函數(shù)模式
先來了解下穩(wěn)妥對象:指的是沒有公共屬性,而且其方法也不引用 this 的對象。穩(wěn)妥對象最適合在一些安全的環(huán)境中 (這些環(huán)境中會禁止使用 this 和 new),或者再防止數(shù)據(jù)被其他應(yīng)用程序 (如 Mashup 程序) 改動時使用。穩(wěn)妥構(gòu)造函數(shù)遵循與寄生構(gòu)造函數(shù)類似的模式,但有兩點(diǎn)不同:一是新創(chuàng)建對象的實(shí)例方法不引用 this;二是不使用 new 操作符調(diào)用構(gòu)造函數(shù)。以下為示例代碼:
function Person(name, age, job){ var o = new Object(); //創(chuàng)建要返回的對象 //可以在這里定義私有變量和函數(shù) o.sayName = function(){ //添加方法 alert(name); }; return o; //返回對象 } var friend = Person("Nicholas", 29, "Software Engineer"); friend.sayName(); //"Nicholas"
其原理就是利用閉包,保有對私有變量和私有方法的引用。
優(yōu)點(diǎn):不可能有別的方法訪問到傳入到構(gòu)造函數(shù)中的原始數(shù)據(jù)。
缺點(diǎn):沒有解決對象識別的問題(即不知道這個對象是什么類型),不能依賴 instanceOf
操作符來確定對象類型;對于對象的方法沒有做到復(fù)用。
8. ES6 中的 class
咱們這塊以 class 實(shí)例來展開講述:
class Parent { name = "qck"; sex = "male"; //實(shí)例變量 sayHello(name){ console.log('qck said Hello!',name); } constructor(location){ this.location = location; } }
我們來看看這段代碼通過 babel 編譯后的 _createClass 函數(shù):
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; // 對屬性進(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è)置到第一個 Constructor 的 prototype 中 if (protoProps) defineProperties(Constructor.prototype, protoProps); // 設(shè)置 Constructor 的 static 類型屬性 if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
首先該方法是一個自執(zhí)行函數(shù),接收的一參是構(gòu)造函數(shù)本身,二參是為構(gòu)造函數(shù)的原型對象需要添加的方法或者屬性,三參是需要為構(gòu)造函數(shù)添加的靜態(tài)屬性對象。從這個函數(shù)就可以看出 class 在創(chuàng)建自定義類型時,用了原型模式。
我們看看編譯后的結(jié)果是如何調(diào)用 _createClass 的:
var Parent = function () { // 這里是自執(zhí)行函數(shù) _createClass(Parent, [{ // Parent的實(shí)例方法,通過修改Parent.prototype來完成 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í)是通過修改該類 (實(shí)際上是函數(shù)) 的 prototype 來完成的。
而通過返回的構(gòu)造函數(shù),我們可以發(fā)現(xiàn):實(shí)例屬性還是通過構(gòu)造函數(shù)方式來添加的。
最后,我們來看看 _classCallCheck 方法,它其實(shí)是一層校驗(yàn),保證了我們的實(shí)例對象是特定的類型。
所以,綜上所述,ES6 中的 class 只是個語法糖,它本質(zhì)上還是用組合使用構(gòu)造函數(shù)模式創(chuàng)建自定義類型的,這也就是為什么我們要學(xué)上面那些知識的初衷。
感興趣的朋友還可以使用本站在線HTML/CSS/JavaScript代碼運(yùn)行工具:http://tools.jb51.net/code/HtmlJsRun測試上述代碼運(yùn)行結(jié)果。
更多關(guān)于JavaScript相關(guān)內(nèi)容還可查看本站專題:《javascript面向?qū)ο笕腴T教程》、《JavaScript錯誤與調(diào)試技巧總結(jié)》、《JavaScript數(shù)據(jù)結(jié)構(gòu)與算法技巧總結(jié)》、《JavaScript遍歷算法與技巧總結(jié)》及《JavaScript數(shù)學(xué)運(yùn)算用法總結(jié)》
希望本文所述對大家JavaScript程序設(shè)計(jì)有所幫助。