為什么 JavaScript 要設計原型模式

雖然 Object 構造函數(shù)或?qū)ο蟮淖置媪靠梢杂脕韯?chuàng)建單個對象,但是這些方式有個明顯的缺點,創(chuàng)建相同結構的對象,會產(chǎn)生大量的重復代碼。

const person1 = {
    name: 'Zhang san',
    age: 18,
    job: 'Engineer',
    sayName: function() {
        alert(this.name);
    }
};

const person2 = {
    name: 'Li si',
    age: 18,
    job: 'Engineer',
    sayName: function() {
        alert(this.name);
    }
};

person1 和 person2 具有相同的屬性和方法,但它們之間沒有復用。為了解決這個問題,有人開始使用工廠模式的一種變體。

工廠模式

工廠模式抽象了創(chuàng)建具體對象的過程。因為在 JavaScript 中沒有類(ES 6 中的類也是函數(shù)),開發(fā)人員就發(fā)明一種函數(shù),用函數(shù)來封裝以特定接口創(chuàng)建對象的細節(jié),如下面的示例:

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;
}

const person1 = createPerson('Zhang san', 18, 'Engineer');
const person2 = createPerson('Li si', 18, 'Doctor');

函數(shù) createPerson() 能夠根據(jù)接受的參數(shù)構建一個包含所有必要信息的 Person 對象??梢詿o數(shù)次的調(diào)用這個函數(shù),每次都會返回全新的 Person 對象。

然而,工廠模式雖然解決了創(chuàng)建多個相似對象的問題,但是沒有解決對象的識別問題,即無法知道一個對象的類型。

隨著 JavaScript 的發(fā)展,又出現(xiàn)了一種新的模式。

構造函數(shù)模式

ECMAScript 中的構造函數(shù)可以用來創(chuàng)建特定類型的對象。像 Object 和 Array 這樣的原生構造函數(shù),在運行時會自動在執(zhí)行環(huán)境中調(diào)用。

因此,我們也可以為自定義對象設計構造函數(shù)。使用構造函數(shù)重寫前面的例子。

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        console.log(this.name);
    }
}

const person1 = new Person('Zhang san', 18, 'Engineer');
const person2 = new Person('Li si', 18, 'Doctor');

Person() 函數(shù)取代了 createPerson() 函數(shù)。并且它們的代碼有幾個不同之處:

  • 沒有顯式地創(chuàng)建對象;
  • 直接將屬性和方法賦值給 this 對象;
  • 沒有 return 語句。

要創(chuàng)建 Person 的新實例,必須使用 new 操作符。以這種方式調(diào)用構造函數(shù)會經(jīng)歷 4 個步驟:

  1. 創(chuàng)建一個新對象;
  2. 將構造函數(shù)的作用域賦值給新對象(指向 this);
  3. 執(zhí)行構造函數(shù)中的代碼;
  4. 返回新對象。

使用 Person 構造函數(shù)創(chuàng)建對象時,對象會被添加一個 constructor 屬性,該屬性指向 Person,也就是構造函數(shù)的指針地址。

console.log(person1.constructor === Person); // true
console.log(person2.constructor === Person); // true

對象的 constructor 屬性可以用來標識對象的類型,這也是將 JavaScript 用于面向?qū)ο缶幊瘫夭豢缮俚奶匦浴?/p>

但是在檢測類型時,使用 instanceof 操作符會更可靠一些, 因為 constructor 屬性有時可能會被修改。

我們來驗證一下:

console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true

如果測試 createPerson() 創(chuàng)建的對象是否是 Person 的實例,返回的會是 false。

1. 構造函數(shù)與普通函數(shù)的區(qū)別

構造函數(shù)與普通函數(shù)唯一的區(qū)別在于調(diào)用它們的方式不同。不過,構造函數(shù)畢竟也是函數(shù),不存在定義構造函數(shù)的特殊語法。

任何函數(shù),只要通過 new 操作符來調(diào)用,那么就可以作為構造函數(shù);而任何函數(shù),如果不通過 new 操作符來調(diào)用,那它跟普通的函數(shù)也沒有什么兩樣。

例如,前面例子定義的 Person() 函數(shù)可以通過下列任何一種方式調(diào)用。

// 作為構造函數(shù)使用
const person = new Person('Zhang san', 18, 'Engineer');
person.sayName(); // Zhang san

// 作為普通函數(shù)調(diào)用
Person('Li si', 18, 'Doctor');
global.sayName(); // Li si

使用 new 操作符來創(chuàng)建新對象時,Person() 作為構造函數(shù)。而不使用 new 操作符直接調(diào)用,屬性和方法會被添加給 global 對象。

當在全局作用域中調(diào)用一個函數(shù)時,this 對象總是指向 global 對象(瀏覽器中是 window 對象)。因此,在調(diào)用完函數(shù)之后,可以通過 window/global 對象來調(diào)用 sayName() 方法,并且返回正確的值。

2. 構造函數(shù)的問題

構造函數(shù)雖然好用但也有缺點。使用構造函數(shù)的主要問題,就是每個方法都要在每個實例上重新創(chuàng)建一遍。

我們來看一下 Person 構造函數(shù)的定義:

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function('console.log(this.name)');
}

const person1 = new Person('Zhang san', 18, 'Engineer');
const person2 = new Person('Li si', 18, 'Doctor');

用這個函數(shù)創(chuàng)建 person1 和 person2 都有一個名為 sayName() 的方法,但這兩個方法不是同一個 Function 實例。

以這種方式創(chuàng)建函數(shù),會導出現(xiàn)不同的作用域鏈和標識符解析,雖然它們做的事情是一樣的,但不同的實例沒有得到共享。

console.log(person1.sayName == person2.sayName); // false

創(chuàng)建兩個完成相同任務的 Function 實例,實屬浪費內(nèi)存。有 this 對象在,其實我們并不需要在構造函數(shù)的時候就將函數(shù)綁定到特定對象上。因此,大可像下面這樣,通過函數(shù)定義轉(zhuǎn)移到構造函數(shù)外部來解決這個問題。

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
      this.sayName = sayName;
};

function sayName() {
    console.log(this.name);
}

我們把 sayName() 函數(shù)的定義轉(zhuǎn)移到構造函數(shù)外部。而在構造函數(shù)內(nèi)部,我們將 Person 的 sayName 屬性設置成等于全局的 sayName 函數(shù)。這樣一來,由于 sayName 包含的是一個指向函數(shù)的指針,因此 person1 和 person2 對象就共享了在全局作用域定義的同一個 sayName() 函數(shù)。

這樣做確實解決了兩個函數(shù)做同一件事的問題,但是又引入了兩個新的問題:

  1. 全局作用域中定義的函數(shù)實際上只能被某個對象調(diào)用,這讓全局作用域變得名不副實。
  2. 如果對象需要定義很多方法,那么就要定義很多個全局函數(shù),于是我們這個自定義的對象類型就毫無封裝性可言了。

原型模式的出場很好地解決了這個問題。

原型模式

我們創(chuàng)建的每一個函數(shù)其實都有一個 prototype 屬性,這個屬性是一個指針,指向一個對象,這個對象的屬性和方法被由這個函數(shù)創(chuàng)建的所有實例共享。prototype 對象被稱為這些實例的原型對象。

這樣我們就可以把構造函數(shù)中定義對象的方法,直接添加到原型對象上,如下實例所示。

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
};

Person.prototype.sayName = function() {
    console.log(this.name);
}

我們將 sayName() 方法和所有屬性直接添加到 Person 的 prototype 屬性中,Person 的所有實例就共用了同一個方法,同時又保證該方法只在 Person 作用域內(nèi)上生效。

小結

本文涉及到的內(nèi)容:

  • 檢查一個對象的類型;
  • 如何使用 new 關鍵字定義函數(shù)(不是調(diào)用);
  • 如何正確地創(chuàng)建自定義構造函數(shù)。

本文內(nèi)容來自《JavaScript 高級程序設計》閱讀筆記。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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