用愚公移山說明Javascript建立物件的各種姿勢

志如發表於2018-06-23

  都退後,我要繼續講故事了。

  北山愚公者,年且九十,面山而居。

var person = {
    name : '愚公',
    age: 90,
    address: '北山腳下',
    whereToLive: function () {
        alert(this.address)
    }
};

  ......北山愚公曰:“雖我之死,有子存焉;子又生孫,孫又生子;子又有子,子又有孫;子子孫孫無窮匱也”。

  看到這兒,問題來了,愚公的子子孫孫那麼多,顯然使用物件字面量去建立是不合理的。我們介紹第一種建立方式。

工廠模式

function createPerson (name, age, address){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.address = address;
    o.whereToLive = function () {
        alert(this.address)
    };
    return o;
}

var son = createPerson('愚小公', 30, '北山');
var grandSon = createPerson('愚小小公', 5, '北山');

  工廠模式比較明顯的一個缺點就是由於生成並返回了一箇中間物件,所以不能判斷物件的型別。

建構函式模式

function Person(name, age, address) {
        this.name = name;
        this.age = age;
        this.address = address;
        this.whereToLive = function(){
            alert(this.address);
        }; 
}
var son = new Person('愚小公', 30, '北山');
var grandSon = new Person('愚小小公', 5, '北山');

  建構函式與普通函式沒有異處,沒有語法上的任何差別,只是在呼叫的時候使用了new關鍵字。所以我們有必要說一下new到底幹了什麼:

  1. 建立一個新的中間物件
  2. 將建構函式的作用於賦給這個中間物件
  3. 執行建構函式中的程式碼
  4. 返回中間物件

  以這裡的程式碼為例,實際上第二步和第三步的操作可以總結為Person.apply(newObject,arguments),這裡順便說一句bind與call/apply的一個區別,bind返回的是一個函式,call/apply是順帶把這個函式給執行了,返回的是執行後的結果。

  那麼,建構函式模式有什麼問題呢,其實也是顯而易見的,如果愚公有一千個子子孫孫,那麼每個子孫都會自帶一個whereToLive的方法,顯然這種做法不文藝範兒

原型模式

function Person () {
    
}

Person.prototype.name = '愚公';
Person.prototype.age = 90;
Person.prototype.address = '北山';
Person.prototype.whereToLive = function () {
    alert(this.address); 
};

var son = new Person();
var grandSon = new Person();
son.name = '愚小公';
son.address = '山的那邊';


son.whereToLive();   //  '山的那邊'
grandSon.whereToLive();   //  '北山'

  我們在son物件上試圖修改address屬性,並且似乎看起來也修改成功了,但是沒有影響到grandSon的屬性。所以其實這兩個address其實並不一樣。為什麼呢?我們在做如下操作:

delete son.address;
son.whereToLive();   //  '北山'

  我們刪掉了son的address屬性,這時候son的address又成了原型中定義的值。所以我們在修改address屬性的時候並沒有動到原型中的值,而是在這個物件上新建了一個屬性。並且在試圖獲取這個屬性的時候會優先返回物件上的屬性值。我們管這個現象叫屬性遮蔽。

  另外多提一點,就是在讀取物件屬性的時候,首先會檢視該物件本身有沒有,沒有的話會順著原型鏈一直向上查詢,如果達到原型鏈頂層都沒有找到,則返回undefined。這裡再穿插一個知識點。很多剛入門的開發者會犯這樣的錯誤:

var a = {};
console.log(a.b.c)

  在沒有校驗b屬性是否存在便去試圖獲取c屬性。如果到了原型鏈的頂端都沒有找到b,a.b的值則為undefined,所以獲取undefined的c屬性一定會報錯。正確的做法是在不確定是否存在對應屬性的時候,應當先做判斷。

  但是在寫入基本型別屬性的時候有所不同,在當前物件沒有找到要寫入的屬性時,不會向上查詢,而是在當前物件裡新建一個屬性,這麼做的原因是防止汙染其他物件的屬性值。細心的你可能發現了我在開頭的時候強調了基本型別屬性。如果是引用型別會怎麼樣呢?

function Person () {
    
}

Person.prototype.name = '愚公';
Person.prototype.age = 90;
Person.prototype.address = ['北山'];
Person.prototype.whereToLive = function () {
    alert(this.address); 
};

var son = new Person();
var grandSon = new Person();
son.address.push('山的那邊');

grandSon.whereToLive();   //  '北山','山的那邊'

  這裡又有一個小知識點,引用型別是存在堆記憶體中的,不同地方的應用其實指向的是同一塊堆記憶體。所以如果試圖修改原型物件中的應用型別,會造成全域性汙染,這也就是原型模式的一個致命缺點。

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

  坐穩,我又要穿插新的知識點了。我們可以採用簡寫的方式避免原型模式賦予原型物件方法時囉嗦的問題。

function Person(name, age, address) {
        this.name = name;
        this.age = age;
        this.address = address;
}
Person.prototype = {
    constructor : Person,  // 手動修改建構函式指向
    whereToLive : function () {
        alert(this.address); 
    },
    howOld : function () {
        alert(this.age); 
    }
}

  組合使用建構函式模式和原型模式的寫法是不是同時規避掉了建構函式模式和原型模式的問題呢?既可以共享公用的函式,又可以讓每個物件獨享自己的屬性。

  需要注意的是,我們在重寫Person.prototype的時候,實際上使得constructor指向了Object,所以我這裡進行了手動修正。

寄生建構函式模式

function PersonList (name, age, address){
    var o = new Array();
    o.push.apply(o, arguments);
    o.consoleString = function () {
       return this.join(",");
    };
    return o;
}

var list = new PersonList('愚小公', '愚小小公');
alert(list.consoleString());

  是不是很眼熟,跟工廠模式一模一樣,只不過是在呼叫的時候使用了new關鍵字。利用這種模式,我們可以為物件新增額外的能力。本例中,就是給陣列新增一個自定義的方法,使其可以擁有我們賦予的新能力。

結語

  實際開發中還是得根據實際場景靈活運用,總有適合你的那一款。今天就聊到這,歡迎大家補充和指正。

相關文章