1、工廠模式
new Object()
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");
這裡,函式 createPerson() 接收3個引數,根據這幾個引數構建了一個包含 Person 資訊的物件。可以用不同的引數多次呼叫這個函式,每次都會返回包含3個屬性和1個方法的物件。這種工廠模式雖然可以解決建立多個類似物件的問題,但沒有解決物件標識問題(即新建立的物件是什麼型別)。2、建構函式模式function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function () { console.log(this.name); };}let person1 = new Person("Nicholas", 29, "Software Engineer");let person2 = new Person("Greg", 27, "Doctor");person1.sayName(); // Nicholas person2.sayName(); // Greg
這種替代了工廠函式,實現效果一樣的,區別在於:沒有顯式地建立物件。屬性和方法直接賦值給了 this 。沒有 return 。Person 的首字母要大寫 這是慣例,建構函式名稱的首字母都是要大寫的,非建構函式則以小寫字母開頭let person1 = new Person("Nicholas", 29, "Software Engineer");執行過程:
(1) 在記憶體中建立一個新物件。 (2) 這個新物件內部的 [[Prototype]] 特性被賦值為建構函式的 prototype 屬性。即為person1.__proto__===Person.prototypeperson1.constructor == Person (3) 建構函式內部的 this 被賦值為這個新物件(即 this 指向新物件)。 (4) 執行建構函式內部的程式碼(給新物件新增屬性)。 (5) 如果建構函式返回非空物件,則返回該物件;否則,返回剛建立的新物件。
constructor 本來是用於標識物件型別的。不過,一般認為 instanceof 運算子是確定物件型別更可靠的方式。
console.log(person1 instanceof Object); // true console.log(person1 instanceof Person); // trueconsole.log(person2 instanceof Object); // true console.log(person2 instanceof Person); // true
建構函式不一定要寫成函式宣告的形式。賦值給變數的函式表示式也可以表示建構函式:
let person1 = new Person("Nicholas", 29, "Software Engineer");let person2 = new Person("Greg", 27, "Doctor");person1.sayName(); // Nicholas person2.sayName(); // Greg console.log(person1 instanceof Object); // true console.log(person1 instanceof Person); // trueconsole.log(person2 instanceof Object); // true console.log(person2 instanceof Person); // true
(1)建構函式也是函式建構函式與普通函式唯一的區別就是呼叫方式不同,任何函式只要使用 new 運算子呼叫就是建構函式,而不使用 new 運算子呼叫的函式就是普通函式。 比如,前面的例子中定義的 Person() 可以像下面這樣呼叫:
// 作為建構函式let person = new Person("Nicholas", 29, "Software Engineer");person.sayName(); // "Nicholas" // 作為函式呼叫Person("Greg", 27, "Doctor"); // 新增到window物件// 結果會將屬性和方法新增到 window 物件。window.sayName(); // "Greg" // 在另一個物件的作用域中呼叫let o = new Object();Person.call(o, "Kristen", 25, "Nurse");o.sayName(); // "Kristen"
(2)建構函式的問題
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = sayName;}function sayName() { console.log(this.name);}let person1 = new Person("Nicholas", 29, "Software Engineer");let person2 = new Person("Greg", 27, "Doctor");person1.sayName(); // Nicholas person2.sayName(); // Greg
sayName() 被定義在了建構函式外部,sayName 屬性等於全域性 sayName() 函式。因為這一次sayName 屬性中包含的只是一個指向外部函式的指標,所以person1 和 person2 共享了定義在全域性作用域上的sayName() 函式。這樣雖然解決了相同邏輯的函式重複定義的問題,但全域性作用域也因此被搞亂了,因為那個函式實際上只能在一個物件上呼叫。如果這個物件需要多個方法,那麼就要在全域性作用域中定義多個函式。這會導致自定義型別引用的程式碼不能很好地聚集一起。這個新問題可以透過原型模式來解決。
3、原型模式每個函式都會建立一個 prototype 屬性,這個屬性是一個物件,包含應該由特定引用型別的例項共享的屬性和方法。實際上,這個物件就是透過呼叫建構函式建立的物件的原型。使用原型物件的好處是,在它上面定義的屬性和方法可以被物件例項共享。
function Person() {}// let Person = function() {}; 使用函式表示式也可以Person.prototype.name = "Nicholas";Person.prototype.age = 29;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function () { console.log(this.name);};let person1 = new Person();person1.sayName(); // "Nicholas"let person2 = new Person();person2.sayName(); // "Nicholas"console.log(person1.sayName == person2.sayName); // true
這裡,所有屬性和 sayName() 方法都直接新增到了 Person 的 prototype 屬性上,建構函式體中什麼也沒有。但這樣定義之後,呼叫建構函式建立的新物件仍然擁有相應的屬性和方法。與建構函式模式不同,使用這種原型模式定義的屬性和方法是由所有例項共享的。因此 person1 和 person2 訪問的都是相同的屬性和相同的sayName() 函式。要理解這個過程,就必須理解ECMAScript中原型的本質。
理解原型無論何時,只要建立一個函式,就會按照特定的規則為這個函式建立一個 prototype 屬性(指向原型物件)。預設情況下,所有原型物件自動獲得一個名為 constructor 的屬性,指回與之關聯的建構函式。對前面的例子而言,Person.prototype.constructor 指向 Person 。然後,因建構函式而異,可能會給原型物件新增其他屬性和方法。在自定義建構函式時,原型物件預設只會獲得 constructor 屬性,其他的所有方法都繼承自 Object 。每次呼叫建構函式建立一個新例項,這個例項的內部 [[Prototype]] 指標就會被賦值為建構函式的原型物件。指令碼中沒有訪問這個[[Prototype]] 特性的標準方式,但Firefox、Safari和Chrome會在每個物件上暴露 proto 屬性,透過這個屬性可以訪問物件的原型。在其他實現中,這個特性完全被隱藏了。關鍵在於理解這一點:例項與建構函式原型之間有直接的聯絡,但例項與建構函式之間沒有。這種關係不好視覺化,但可以透過下面的程式碼來理解原型的行為:
/** * * 建構函式可以是函式表示式 * 也可以是函式宣告,因此以下兩種形式都可以: * function Person {} * let Person = function() {} */function Person() {}/** * 宣告之後,建構函式就有了一個 * 與之關聯的原型物件: */ console.log(typeof Person.prototype);console.log(Person.prototype); // { // constructor: f Person(),// __proto__: Object // }/** * 如前所述,建構函式有一個prototype屬性 * 引用其原型物件,而這個原型物件也有一個 * constructor屬性,引用這個建構函式 * 換句話說,兩者迴圈引用: */console.log(Person.prototype.constructor === Person); // true/** * 正常的原型鏈都會終止於Object的原型物件 * Object原型的原型是null */console.log(Person.prototype.__proto__ === Object.prototype); // trueconsole.log(Person.prototype.__proto__.constructor === Object); // trueconsole.log(Person.prototype.__proto__.__proto__ === null); // trueconsole.log(Person.prototype.__proto__);// {// constructor: f Object(),// toString: ...// hasOwnProperty: ...// isPrototypeOf: ...// ...// }let person1 = new Person(), person2 = new Person();/*** 建構函式、原型物件和例項*是3個完全不同的物件:*/ console.log(person1 !== Person); // trueconsole.log(person1 !== Person.prototype); // trueconsole.log(Person.prototype !== Person); // true/*** 例項透過__proto__連結到原型物件, * 它實際上指向藏特性[[Prototype]] ** 建構函式透過prototype屬性連結到原型物件 ** 例項與建構函式沒有直接聯絡,與原型物件有直接聯絡 */console.log(person1.__proto__ === Person.prototype); // trueconsole.log(person1.__proto__.constructor === Person); // true/** * 同一個建構函式建立的兩個例項 * 共享同一個原型物件: */console.log(person1.__proto__ === person2.__proto__); // true/** * instanceof檢查例項的原型鏈中 * 是否包含指定建構函式的原型: */ console.log(person1 instanceof Person); // trueconsole.log(person1 instanceof Object);// trueconsole.log(Person.prototype instanceof Object); // true
更多見《JavaScript高階程式設計》3版第六章 或第四版第8章