爲何 JavaScript 要設計原型模式

雖然 Object 構造函數或對象的字面量能夠用來建立單個對象,可是這些方式有個明顯的缺點,建立相同結構的對象,會產生大量的重複代碼。javascript

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 具備相同的屬性和方法,但它們之間沒有複用。爲了解決這個問題,有人開始使用工廠模式的一種變體。java

工廠模式

工廠模式抽象了建立具體對象的過程。由於在 JavaScript 中沒有類(ES6 中的類也是函數),開發人員就發明一種函數,用函數來封裝以特定接口建立對象的細節,以下面的示例:編程

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');
複製代碼

函數 createPerson() 可以根據接受的參數構建一個包含全部必要信息的 Person 對象。能夠無數次的調用這個函數,每次都會返回全新的 Person 對象。瀏覽器

然而,工廠模式雖然解決了建立多個類似對象的問題,可是沒有解決對象的識別問題,即沒法知道一個對象的類型。函數

隨着 JavaScript 的發展,又出現了一種新的模式。測試

構造函數模式

ECMAScript 中的構造函數能夠用來建立特定類型的對象。像 Object 和 Array 這樣的原生構造函數,在運行時會自動在執行環境中調用。ui

所以,咱們也能夠爲自定義對象設計構造函數。使用構造函數重寫前面的例子。this

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() 函數取代了 createPerson() 函數。而且它們的代碼有幾個不一樣之處:spa

  • 沒有顯式地建立對象;
  • 直接將屬性和方法賦值給 this 對象;
  • 沒有 return 語句。

要建立 Person 的新實例,必須使用 new 操做符。以這種方式調用構造函數會經歷 4 個步驟:prototype

  1. 建立一個新對象;
  2. 將構造函數的做用域賦值給新對象(指向 this);
  3. 執行構造函數中的代碼;
  4. 返回新對象。

使用 Person 構造函數建立對象時,對象會被添加一個 constructor 屬性,該屬性指向 Person,也就是構造函數的指針地址。

console.log(person1.constructor === Person); // true
console.log(person2.constructor === Person); // true
複製代碼

對象的 constructor 屬性能夠用來標識對象的類型,這也是將 JavaScript 用於面向對象編程必不可少的特性。

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

咱們來驗證一下:

console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
複製代碼

若是測試 createPerson() 建立的對象是不是 Person 的實例,返回的會是 false。

1. 構造函數與普通函數的區別

構造函數與普通函數惟一的區別在於調用它們的方式不一樣。不過,構造函數畢竟也是函數,不存在定義構造函數的特殊語法。

任何函數,只要經過 new 操做符來調用,那麼就能夠做爲構造函數;而任何函數,若是不經過 new 操做符來調用,那它跟普通的函數也沒有什麼兩樣。

例如,前面例子定義的 Person() 函數能夠經過下列任何一種方式調用。

// 做爲構造函數使用
const person = new Person('Zhang san', 18, 'Engineer');
person.sayName(); // Zhang san

// 做爲普通函數調用
Person('Li si', 18, 'Doctor');
global.sayName(); // Li si
複製代碼

使用 new 操做符來建立新對象時,Person() 做爲構造函數。而不使用 new 操做符直接調用,屬性和方法會被添加給 global 對象。

當在全局做用域中調用一個函數時,this 對象老是指向 global 對象(瀏覽器中是 window 對象)。所以,在調用完函數以後,能夠經過 window/global 對象來調用 sayName() 方法,而且返回正確的值。

2. 構造函數的問題

構造函數雖然好用但也有缺點。使用構造函數的主要問題,就是每一個方法都要在每一個實例上從新建立一遍。

咱們來看一下 Person 構造函數的定義:

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');
複製代碼

用這個函數建立 person1 和 person2 都有一個名爲 sayName() 的方法,但這兩個方法不是同一個 Function 實例。

以這種方式建立函數,會導出現不一樣的做用域鏈和標識符解析,雖然它們作的事情是同樣的,但不一樣的實例沒有獲得共享。

console.log(person1.sayName == person2.sayName); // false
複製代碼

建立兩個完成相同任務的 Function 實例,實屬浪費內存。有 this 對象在,其實咱們並不須要在構造函數的時候就將函數綁定到特定對象上。所以,大可像下面這樣,經過函數定義轉移到構造函數外部來解決這個問題。

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

function sayName() {
    console.log(this.name);
}
複製代碼

咱們把 sayName() 函數的定義轉移到構造函數外部。而在構造函數內部,咱們將 Person 的 sayName 屬性設置成等於全局的 sayName 函數。這樣一來,因爲 sayName 包含的是一個指向函數的指針,所以 person1 和 person2 對象就共享了在全局做用域定義的同一個 sayName() 函數。

這樣作確實解決了兩個函數作同一件事的問題,可是又引入了兩個新的問題:

  1. 全局做用域中定義的函數實際上只能被某個對象調用,這讓全局做用域變得名存實亡。
  2. 若是對象須要定義不少方法,那麼就要定義不少個全局函數,因而咱們這個自定義的對象類型就毫無封裝性可言了。

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

原型模式

咱們建立的每個函數其實都有一個 prototype 屬性,這個屬性是一個指針,指向一個對象,這個對象的屬性和方法被由這個函數建立的全部實例共享。prototype 對象被稱爲這些實例的原型對象。

這樣咱們就能夠把構造函數中定義對象的方法,直接添加到原型對象上,以下實例所示。

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 做用域內上生效。

小結

本文涉及到的內容:

  • 檢查一個對象的類型;
  • 如何使用 new 關鍵字定義函數(不是調用);
  • 如何正確地建立自定義構造函數。
相關文章
相關標籤/搜索