JS面向對象之 創(chuàng)建對象的方法及其優(yōu)缺點

以下是基于《JavaScript高級程序設計》一書,對創(chuàng)建對象的幾種方法所進行的整理。雖然工作中經(jīng)常用到,但是概念性的東西,也是要整明白的呀,不僅會用,還要明白這是什么,為什么這么用 才是王道。

1、工廠模式

用函數(shù)來封裝以特定接口來創(chuàng)建對象。

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"

函數(shù)createPerson()能夠根據(jù)接受的參數(shù)來創(chuàng)建一個包含所有必要信息的Person對象。可以無數(shù)次的調(diào)用這個函數(shù),而每次它都會返回一個包含三個屬性一個方法的對象。

  • 缺點:雖然解決來創(chuàng)建多個相似對象的問題,但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)。

2、構造函數(shù)模式

構造函數(shù)可以用來創(chuàng)建特定類型的對象,使用構造函數(shù)重寫上個例子:

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");

可見,Person()函數(shù)與createPerson()不同之處在于:

  • 沒有顯示的創(chuàng)建對象;
  • 直接將屬性和方法賦給來this對象;
  • 沒有return語句;
  • 使用new操作符來創(chuàng)建實例。
    擴展 - 使用new操作符來創(chuàng)建實例實際上會經(jīng)歷以下4個步驟:
    1、創(chuàng)建一個新對象;
    2、將構造函數(shù)的作用域賦給新對象(因此this就指向了這個新對象);
    3、執(zhí)行構造函數(shù)中的代碼;
    4、返回新對象。

在這個例子中所創(chuàng)建的實例person1person2即是Person的實例,同時也是Object的實例,通過instanceof操作符可以進行驗證。

alert(person1 instanceof Object);  //true
alert(person1 instanceof Person);  //true
alert(person2 instanceof Object);  //true
alert(person2 instanceof Person);  //true

創(chuàng)建自定義的構造函數(shù)意味著將來可以將它的實例標識為一種特定類型,而這正是構造函數(shù)模式勝過工廠模式的地方。

  • 缺點:使用構造函數(shù)模式的主要問題,就是每個方法都要在每個實例上重新創(chuàng)建一遍。在上面例子中,person1person2都有一個名為sayName()的方法,但是那兩個方法不是同一個Function實例,會導致不同的作用域鏈和標識符解析,不同實例上的同名函數(shù)是不相等的。
alert(person1.sayName == person2.sayName);  //false 

創(chuàng)建兩個完成同樣任務的Function實例是沒有必要的,況且有this對象在,根本不用在執(zhí)行代碼前就把函數(shù)綁定在特定對象上面。因此,會有人提出把函數(shù)定義在構造函數(shù)外部來解決這個問題。

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");

person1.sayName();   //"Nicholas"
person2.sayName();   //"Greg"

alert(person1 instanceof Object);  //true
alert(person1 instanceof Person);  //true
alert(person2 instanceof Object);  //true
alert(person2 instanceof Person);  //true

alert(person1.constructor == Person);  //true
alert(person2.constructor == Person);  //true

alert(person1.sayName == person2.sayName);  //true 

sayName()函數(shù)定義到構造函數(shù)外部之后,從上面代碼可見,除了sayName()函數(shù)外,其他的實現(xiàn)與前面代碼完全一致。
而在構造函數(shù)內(nèi)部,將sayName屬性設置成等于全局的sayName()函數(shù)。由于sayName是一個指向函數(shù)的指針,因此person1person2對象就共享來在全局作用域中定義的一個sayName()函數(shù),這樣做確實解決來兩個函數(shù)做同一件事。可是又產(chǎn)生來新的問題:

  • 在全局作用域定義的函數(shù)只能被某個對象調(diào)用,這樣讓全局作用域有點名不副實;
  • 若是對象需要定義很多方法,那么需要定義多個全局函數(shù),于是這個自定義的引用類型就毫無封裝性可言了。

3、原型模式

我們創(chuàng)建的每個函數(shù)都有一個prototype(原型)屬性,這個屬性指向通過調(diào)用構造函數(shù)而創(chuàng)建的實例的原型對象,對原型還不熟悉的小伙伴可先看看這篇this、原型和作用域。
使用原型對象的好處是可以讓所有對象實例共享它所包含的屬性和方法。換句話說,不必在構造函數(shù)中定義對象實例的信息,而是可以將這些信息直接添加到原型對象中。

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()方法和所有屬性直接添加到了Personprototype屬性中,構造函數(shù)變成了空函數(shù),即是如此,也仍然可以通過調(diào)用構造函數(shù)來創(chuàng)建新對象,而且新對象還會具有相同的屬性和方法。但與構造函數(shù)模式不同的是,新對象的這些屬性和方法是由所有實例共享的,person1person2訪問的都是同一組屬性和同一個sayName()函數(shù)。

  • 缺點:
    1、它省略來為構造函數(shù)傳遞初始化參數(shù)這一環(huán)節(jié),結果所有實例在默認情況下都將取得相同的屬性值;
    2、由原型共享的本性所導致的,修改一個實例的屬性,會對另一個實例的屬性也造成影響,見以下代碼:
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

4、組合使用構造函數(shù)+原型模式

創(chuàng)建自定義類型的最常見方式,就是組合使用構造函數(shù)模式+原型模式。構造函數(shù)模式用于定義實例屬性,而原型模式用于定義方法和共享的屬性,這樣,每個實例都會有自己的一份實例屬性的副本,并且同時又共享著懟方法的引用,最大限度的節(jié)省了內(nèi)存。另外,這種混合模式還支持向構造函數(shù)傳遞參數(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

在上面例子中,實例屬性都是在構造函數(shù)中定義的,而所有實例共享的屬性constructor和方法sayName()則是在原型中定義的。而修改了person1.friends(向其中添加一個新字符串),并不會影響到person2.friends,因為他們分別引用了不同的數(shù)組。
可以說,這是目前用來定義引用類型的一種默認模式。

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

有其他OO語言經(jīng)驗的開發(fā)人員在看到獨立的構造函數(shù)和原型時,很可能會感到困惑。動態(tài)原型模式正是致力于解決這個問題的一個方案,它把所有信息都封裝在了構造函數(shù)中,而通過構造函數(shù)中初始化原型(僅在必要條件下),又保持了同時使用構造函數(shù)和原型的優(yōu)點。換句話說,可以通過檢查某個應該存在的方法是否有效,來決定是否需要初始化原型。

function Person(name, age, job){
    // 屬性
    this.name = name;
    this.age = age;
    this.job = job;
    
    // 方法
    if (typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            alert(this.name);
        };
    }
}

var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();

注意方法部分,這里只在sayName()方法不存在的情況下,才會將他添加到原型中。這段代碼只會在初次調(diào)用構造函數(shù)時才會執(zhí)行。此后,原型已經(jīng)完成初始化,不需要再做什么修改了,這里對原型所做的修改,能夠立即在所有實例中得到反映。
注意,使用動態(tài)原型模式時,不能使用對象字面量重寫原型,在this、原型和作用域中提到的,如果在已經(jīng)創(chuàng)建了實例的情況下重寫原型,會切斷現(xiàn)有實例與新原型之間的關系。

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

這種模式的基本思想是創(chuàng)建一個函數(shù),該函數(shù)的作用僅僅是封裝創(chuàng)建對象的代碼,然后再返回新創(chuàng)建的對象;但從表面上看,這個函數(shù)又很像經(jīng)典的構造函數(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"

在這個例子中,Person函數(shù)創(chuàng)建了一個新對象,并以相應的屬性和方法初始化該對象,然后又返回了這個對象。除了使用 new操作符并把使用的包裝函數(shù)叫做構造函數(shù)之外,這個模式跟工廠模式其實是一模一樣的。構造函數(shù)在不返回值的情況下,默認會返回新對象實例。而通過在構造函數(shù)的末尾添加一個return語句,可以重寫調(diào)用構造函數(shù)時返回的值。
這個模式可以在特殊情況下用來為對象創(chuàng)建構造函數(shù)。假設我們想創(chuàng)建一個具有額外方法的特殊數(shù)組,由于不能直接修改Array構造函數(shù),因此可以使用這個模式。

function SpecialArray(){       
    // 創(chuàng)建數(shù)組
    var values = new Array();
    
    // 添加值
    values.push.apply(values, arguments);
    
    //添加方法
    values.toPipedString = function(){
        return this.join("|");
    };
    
    // 返回數(shù)組
    return values;        
}

var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); // "red|blue|green"

alert(colors instanceof SpecialArray);

關于寄生構造函數(shù)模式,有一點需要說明:返回的對象與構造函數(shù)或者與構造函數(shù)的原型屬性之間沒有關系;也就是說,構造函數(shù)返回的對象與在構造函數(shù)外部創(chuàng)建的對象沒有什么不同。為此,不能依賴instanceof操作符來確定對象類型。由于存在上述問題,在可以使用其他模式的情況下,不要使用這種模式

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

首先,要明白什么是穩(wěn)妥對象:指沒有公共屬性,而其他方法也不引用this的對象。
穩(wěn)妥對象最適合在一些安全的環(huán)境中(這些環(huán)境中會禁止使用thisnew),或者防止數(shù)據(jù)被其他應用程序(如Mashup程序)改動時使用。
穩(wěn)妥構造函數(shù)遵循與寄生構造函數(shù)類似的模式,但有兩點不同:一是新創(chuàng)建對象的實例方法不引用this;二是不適用new操作符調(diào)用構造函數(shù)。按照穩(wěn)妥構造函數(shù)的要求,可以將前面的Person構造函數(shù)重寫:

function Person(name, age, job){
    // 創(chuàng)建要返回的對象
    var o = new Object();
    // 可以在這里定義私有變量和函數(shù)

    // 添加方法
    o.sayName = function(){
        alert(this.name);
    };

    // 返回對象
    return o;
}

var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName();  //"Nicholas"

注意,在這種模式創(chuàng)建的對象中,除了使用sayName()方法之外,沒有其他方法訪問name的值。即使有其他代碼會給這個對象添加方法和數(shù)據(jù)成員,但也不可能有別的方法訪問傳入到構造函數(shù)中的原始數(shù)據(jù)。
穩(wěn)妥構造函數(shù)模式提供的這種安全性,使得它非常適合在某些安全執(zhí)行環(huán)境下使用。
與寄生構造函數(shù)模式類似,使用穩(wěn)妥構造函數(shù)模式創(chuàng)建的對象與構造函數(shù)之間也沒有什么關系,因此 instanceof操作符對這種對象也沒有意義。

總結

  • 工廠模式:
    使用簡單的函數(shù)創(chuàng)建對象,為對象添加屬性和方法,然后返回對象。這個模式后來被構造函數(shù)模式所取代。
  • 構造函數(shù)模式:
    可以創(chuàng)建自定義引用類型,可以像創(chuàng)建內(nèi)置對象實例一樣使用new操作符。不過,構造函數(shù)模式也有缺點,即它的每個成員都無法得到復用,包括函數(shù)。由于函數(shù)可以不局限于任何對象(即與對象具有松散耦合的特點),因此在多個對象間共享函數(shù)。
  • 原型模式:
    使用構造函數(shù)prototype屬性來指定那些應該共享的屬性和方法。組合使用構造函數(shù)模式和原型模式時,使用構造函數(shù)定義實例屬性,而使用原型定義共享的方法和屬性。
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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