js建立物件的各種方法以及優缺點

夏天來嘍發表於2019-05-06

整理《javascript高階程式設計》中建立物件的7種方式以及優缺點, 還是要再說一句,這本書是寫的真好啊。

1. 工廠模式

工廠模式是軟體工程領域一種廣為人知的設計模式,這種模式抽象了建立具體物件的過程

function createPerson(name, age, job) {
    let o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function () {
        console.log(this.name);
    }
    return o;
}
let person1 = createPerson("Nicholas", 29, "Software Engineer");
let person2 = createPerson("Greg", 27, "Doctor");
複製程式碼

工廠模式雖然解決了建立多個相似物件的問題,但卻沒有解決物件識別的問題(即怎樣知道一個物件的型別)

2. 建構函式模式

function Person(name,age,job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function () {
        console.log(this.name);
    }
}
let person1 = createPerson("Nicholas", 29, "Software Engineer");
let person2 = createPerson("Greg", 27, "Doctor");
複製程式碼

使用建構函式的主要問題,就是每個方法都要在每個例項上重新建立一遍 (person1.sayName !== person2.sayName)

3. 原型模式

我們建立的每個函式都有一個prototype(原型)屬性,這個屬性是一個指標,指向一個物件,而這個物件的用途是包含可以由特定型別的所有例項共享的屬性和方法

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
複製程式碼

原型中所有屬性是被例項共享的, 引用型別的屬性會出問題

function Person(){
}

Person.prototype = {
    constructor: Person, // 重寫原型一定要將constructor屬性賦值為原建構函式,否則原型丟失
    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");

console.log(person1.friends);    //"Shelby,Court,Van"
console.log(person2.friends);    //"Shelby,Court,Van"
console.log(person1.friends === person2.friends);  //true
複製程式碼

4. 組合使用建構函式模式和原型模式

建立自定義型別的最常見方式,就是組合使用建構函式模式與原型模式。建構函式模式用於定義例項屬性,而原型模式用於定義方法和共享的屬性。結果,每個例項都會有自己的一份例項屬性的副本,但同時又共享著對方法的引用,最大限度地節省了記憶體

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
}
Person.prototype = {
    constructor : Person,
    sayName : function(){
        console.log(this.name);
    }
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

person1.friends.push("Van");
console.log(person1.friends);    //"Shelby,Count,Van"
console.log(person2.friends);    //"Shelby,Count"
console.log(person1.friends === person2.friends);    //false
console.log(person1.sayName === person2.sayName);    //true

複製程式碼

在這個例子中,例項屬性都是在建構函式中定義的,而由所有例項共享的屬性constructor和方法sayName()則是在原型中定義的。而修改了person1.friends(向其中新增一個新字串),並不會影響到person2.friends,因為它們分別引用了不同的陣列。

這種建構函式與原型混成的模式,是目前在ECMAScript中使用最廣泛、認同度最高的一種建立自定義型別的方法。可以說,這是用來定義引用型別的一種預設模式。

5.動態原型模式

把所有資訊都封裝在了建構函式中,而通過在建構函式中初始化原型(僅在必要的情況下),又保持了同時使用建構函式和原型的優點。換句話說,可以通過檢查某個應該存在的方法是否有效,來決定是否需要初始化原型

function Person(name, age, job){

    //屬性
    this.name = name;
    this.age = age;
    this.job = job;
    if (typeof this.sayName != "function"){
        console.log(1);
        Person.prototype.sayName = function(){
            console.log(this.name);
        };
    }
}
var person1 = new Person("Nicholas", 29, "Software Engineer"); //1
var person2 = new Person("Greg", 27, "Doctor");

person1.sayName();
person2.sayName();
複製程式碼

這裡只在sayName()方法不存在的情況下,才會將它新增到原型中。這段程式碼只會在初次呼叫建構函式時才會執行。此後,原型已經完成初始化,不需要再做什麼修改了。不過要記住,這裡對原型所做的修改,能夠立即在所有例項中得到反映。因此,這種方法確實可以說非常完美其中,if語句檢查的可以是初始化之後應該存在的任何屬性或方法——不必用一大堆if語句檢查每個屬性和每個方法;只要檢查其中一個即可。對於採用這種模式建立的物件,還可以使用instanceof操作符確定它的型別。

6. 寄生建構函式模式

這種模式的基本思想是建立一個函式,該函式的作用僅僅是封裝建立物件的程式碼,然後再返回新建立的物件;但從表面上看,這個函式又很像是典型的建構函式

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 p1 = new Person("Nicholas", 29, "Software Engineer");
p1.sayName();  //"Nicholas"
複製程式碼

Person函式建立了一個新物件,並以相應的屬性和方法初始化該物件,然後又返回了這個物件。除了使用new操作符並把使用的包裝函式叫做建構函式之外,這個模式跟工廠模式其實是一模一樣的, 建構函式在不返回值的情況下,預設會返回新物件例項。而通過在建構函式的末尾新增一個return語句,可以重寫呼叫建構函式時返回的值。 這個模式可以在特殊的情況下用來為物件建立建構函式。假設我們想建立一個具有額外方法的特殊陣列。由於不能直接修改Array建構函式,因此可以使用這個模式。

function SpecialArray(){

    //建立陣列
    var values = new Array();

    //新增值
    values.push.apply(values, arguments);

    //新增方法
    values.toPipedString = function(){
        return this.join("|");
    };

    //返回陣列
    return values;
}

var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); //"red|blue|green"
複製程式碼

關於寄生建構函式模式,有一點需要說明:首先,返回的物件與建構函式或者與建構函式的原型屬性之間沒有關係;也就是說,建構函式返回的物件與在建構函式外部建立的物件沒有什麼不同。為此,不能依賴instanceof操作符來確定物件型別。由於存在上述問題,我們建議在可以使用其他模式的情況下,不要使用這種模式

7. 穩妥建構函式模式

所謂穩妥物件,指的是沒有公共屬性,而且其方法也不引用this的物件。穩妥物件最適合在一些安全的環境中(這些環境中會禁止使用this和new),或者在防止資料被其他應用程式改動時使用。穩妥建構函式遵循與寄生建構函式類似的模式,但有兩點不同:一是新建立物件的例項方法不引用this;二是不使用new操作符呼叫建構函式。按照穩妥建構函式的要求,可以將前面的Person建構函式重寫如下。

function Person(name, age, job){

    //建立要返回的物件
    var o = new Object();

    //可以在這裡定義私有變數和函式

    //新增方法
    o.sayName = function(){
        alert(name);
    };    

    //返回物件
    return o;
}
複製程式碼

注意,在以這種模式建立的物件中,除了使用sayName()方法之外,沒有其他辦法訪問name的值。可以像下面使用穩妥的Person建構函式。

var person = Person("Nicholas", 29, "Software Engineer");
person.sayName();  //"Nicholas"
複製程式碼

這樣,變數person中儲存的是一個穩妥物件,而除了呼叫sayName()方法外,沒有別的方式可以訪問其資料成員。即使有其他程式碼會給這個物件新增方法或資料成員,但也不可能有別的辦法訪問傳入到建構函式中的原始資料。穩妥建構函式模式提供的這種安全性,使得它非常適合在某些安全執行環境下使用。

參考:《javascript高階程式設計》

相關文章