《JavaScript物件導向精要》之四:建構函式和原型物件

明易發表於2018-12-16

由於 JavaScript(ES5) 缺乏類,但可用建構函式和原型物件給物件帶來與類相似的功能。

4.1 建構函式

建構函式的函式名首字母應大寫,以此區分其他函式。

當沒有需要給建構函式傳遞引數,可忽略小括號:

var Person = {
  // 故意留空
}
var person = new Person;
複製程式碼

儘管 Person 建構函式沒有顯式返回任何東西,但 new 操作符會自動建立給定型別的物件並返回它們。

每個物件在建立時都自動擁有一個建構函式屬性(constructor,其實是它們的原型物件上的屬性),其中包含了一個指向其建構函式的引用。

通過物件字面量形式({})或 Object 建構函式建立出來的泛用物件,其建構函式屬性(constructor)指向 Object;而那些通過自定義建構函式建立出來的物件,其建構函式屬性指向建立它的建構函式。

console.log(person.constructor === Person); // true
console.log(({}).constructor === Object); // true
console.log(([1,2,3]).constructor === Object); // false

// 證明 constructor 是在原型物件上
console.log(person.hasOwnProperty("constructor")); // false
console.log(person.constructor.prototype.hasOwnProperty("constructor")); // true
複製程式碼

儘管物件例項及其建構函式之間存在這樣的關係,但還是建議使用 instanceof 來檢查物件型別。這是因為建構函式屬性可以被覆蓋。(person.constructor = "")。

當你呼叫建構函式時,new 會自動自動建立 this 物件,且其型別就是建構函式的型別(建構函式就像類,相當於一種資料型別)。

你也可以在建構函式中顯式呼叫 return。如果返回值是一個物件,它會代替新建立的物件例項而返回,如果返回值是一個原始型別,它會被忽略,新建立的物件例項會被返回。

始終確保要用 new 呼叫建構函式;否則,你就是在冒著改變全域性物件的風險,而不是建立一個新的物件。

var person = Person("Nicholas"); // 缺少 new

console.log(person instanceof Person); // false
console.log(person); // undefined,因為沒用 new,就相當於一個普通函式,預設返回 undefined
console.log(name); // "Nicholas"
複製程式碼

當 Person 不是被 new 呼叫時,建構函式中的 this 物件等於全域性 this 物件。

在嚴格模式下,會報錯。因為嚴格模式下,並沒有為全域性物件設定 this,this 保持為 undefined。

以下程式碼,通過 new 例項化 100 個物件,則會有 100 個函式做相同的事。因此可用 prototype 共享同一個方法會更高效。

var person = {
  name: "Nicholas",
  sayName: function(){
    console.log(this.name);
  }
}
複製程式碼

4.2 原型物件

可以把原型物件看作是物件的基類。幾乎所有的函式(除了一些內建函式)都有一個名為 prototype 的屬性,該屬性是一個原型物件用來建立新的物件例項。

所有建立的物件例項(同一建構函式,當然,可能訪問上層的原型物件)共享該原型物件,且這些物件例項可以訪問原型物件的屬性。例如,hasOwnProperty() 定義在 Object 的原型物件中,但卻可被任何物件當作自己的屬性訪問。

var book = {
  title : "book_name"
}

"hasOwnProperty" in book; // true
book.hasOwnProperty("hasOwnProperty"); // false
Object.property.hasOwnProperty("hasOwnProperty"); // true
複製程式碼

鑑別一個原型屬性

function hasPrototypeProperty(object, name){
  return name in object && !object.hasOwnProperty(name);
}
複製程式碼

4.2.1 [[Prototype]] 屬性

一個物件例項通過內部屬性 [[Prototype]] 跟蹤其原型物件。

該屬性是一個指向該例項使用的原型物件的指標。當你用 new 建立一個新的物件時,建構函式的原型物件就會被賦給該物件的 [[Prototype]] 屬性。

Object.getPrototypeOf() 方法可讀取 [[Prototype]] 屬性的值。

var obj = {};
var prototype = Object.getPrototypeOf(obj);

console.log(prototype === Object.prototype); // true
複製程式碼

大部分 JavaScript 引擎在所有物件上都支援一個名為 __proto__ 的屬性。該屬性使你可以直接讀寫 [[Prototype]] 屬性。

isPrototypeOf() 方法會檢查某個物件是否是另一個物件的原型物件,該方法包含在所有物件中。

var obj = {}
console.log(Object.prototype.isPrototypeOf(obj)); // true
複製程式碼

當讀取一個物件的屬性時,JavaScript 引擎首先在該物件的自有屬性查詢屬性名。如果找到則返回。否則會搜尋 [[Prototype]] 中的物件,找到則返回,找不到則返回 undefined。

var obj = new Object();
console.log(obj.toString()); // "[object Object]"

obj.toString = function(){
  return "[object Custom]";
}
console.log(obj.toString()); // "[object Custom]"

delete obj.toString; // true
console.log(obj.toString()); // "[object Object]"

delete obj.toString; // 無效,delete不能刪除一個物件從原型繼承而來的屬性
cconsole.log(obj.toString()); // // "[object Object]"
複製程式碼

delete 操作符不能刪除的屬性有:①顯式宣告的全域性變數不能被刪除,該屬性不可配置(not configurable); ②內建物件的內建屬性不能被刪除; ③不能刪除一個物件從原型繼承而來的屬性(不過你可以從原型上直接刪掉它)。

一個重要概念:無法給一個物件的原型屬性賦值。但我們可以通過 obj.constructor.prototype.sayHi = function(){console.log("Hi!")} 向原型物件新增屬性。

《JavaScript物件導向精要》之四:建構函式和原型物件
(圖片中間可以看出,為物件 obj 新增的 toString 屬性代替了原型屬性)

4.2.2 在建構函式中使用原型物件

原型物件的共享機制使得它們成為一次性為所有物件定義所有方法的理想手段,因為一個方法對所有的物件例項做相同的事,沒理由每個例項都要有一份自己的方法。

將方法放在原型物件中並使用this方法當前例項是更有效的做法。

function Person(name) {this.name = name}
Person.prototype.sayName = function() {console.log(this.name)};
var person1 = new Person("Nicholas")
console.log(person1.name)                        // Nicholas
person1.sayName()                                // Nicholas
複製程式碼

也可以在原型物件上儲存其他型別的資料,但是在儲存引用值時要注意,因為這些引用值會被多個例項共享,可能大家不希望一個例項能夠改變另一個例項的值。

function Person(name) {this.name = name}
Person.prototype.favorites = []
var person1 = new Person("Nicholas")
var person2 = new Person("Greg")
person1.favorites.push("pizza")
person2.favorites.push("quinoa")

console.log(person1.favorites)                // ["pizza", "quinoa"]
console.log(person2.favorites)                // ["pizza", "quinoa"]
複製程式碼

favorites屬性被定義到原型物件上,意味著person1.favorites和person2.favorites指向同一個陣列,你對任意Person物件的favorites插入的值都將成為原型物件上陣列的元素。也可以使用字面量的形式替換原型物件:

function Person(name) {this.name=name}
Person.prototype= {
    sayName: function() {console.log(this.name)},
    toString: function(){return `[Person ${this.name} ]`}
}
複製程式碼

雖然用這種字面量的形式定義原型非常簡潔,但是有個副作用需要注意。

var person1 = new Person('Nicholas')
console.log(person1 instanceof Person)                // true
console.log(person1.constructor === Person)                // false
console.log(person1.constructor === Object)                // true
複製程式碼

使用字面量形式改寫原型物件改寫了建構函式的屬性,因此現在指向Object而不是Person,這是因為原型物件具有個constructor屬性,這是其他物件例項所沒有的。當一個函式被建立時,其prototype屬性也被建立,且該原型物件的constructor屬性指向該函式自己,當使用字面量形式改寫原型物件Person.prototype時,其constructor屬性將被複寫為泛用物件Object。為了避免這一點,需要在改寫原型物件時手動重置其constructor屬性:

function Person(name) {this.name = name}
Person.prototype = {
    constructor: Person,             // 為了不忘記賦值,最好在第一個屬性就把constructor重置為自己
    sayName() {console.log(this.name)},
    toString() {return `[Person ${this.name} ]`}
}

var person1 = new Person('Nicholas')
console.log(person1 instanceof Person)                    // true
console.log(person1.constructor === Person)                // true
console.log(person1.constructor === Object)                // false
複製程式碼

建構函式、原型物件、物件例項之間:物件例項和建構函式之間沒有直接聯絡。不過物件例項和原型物件之間以及原型物件和建構函式之間都有直接聯絡。

這樣的連線關係也意味著,如果打斷物件例項和原型物件之間的聯絡,那麼也將打斷物件例項及其建構函式之間的關係。

4.2.3 改變原型物件

因為每個物件的 [[Prototype]] 只是一個指向原型物件的指標,所以原型物件的改動會立刻反映到所有引用它的物件。

當對一個物件使用封印 Object.seal() 或凍結 Object.freeze() 時,完全是在操作物件的自有屬性,但任然可以通過在原型物件上新增屬性來擴充套件這些物件例項。

4.2.4 內建物件(如Array、String)的原型物件

所有內建物件都有建構函式,因此也都有原型物件可以去改變,例如要在陣列上新增一個新的方法只需要改變Array.prototype即可

Array.prototype.sum = function() {
    return this.reduce((privious, current) => privious + current)
}
var numbers = [1, 2, 3, 4, 5, 6]
var result = numbers.sum()
console.log(result)                    // 21
複製程式碼

sum()函式內部,在呼叫時this指向陣列的物件例項numbers,因此this也可以呼叫該陣列的其他方法,比如reduce()。 改變原始封裝型別的原型物件,就可以給這些原始值新增更多功能,比如:

String.prototype.capitalize = function() {
    return this.charAt(0).toUpperCase() + this.substring(1)
}
var message = 'hello world!'
console.log(message.capitalize())            // Hello world!
複製程式碼

總結

  • 建構函式就是用 new 操作符呼叫的普通函式。可用過 instanceof 操作符或直接訪問 constructor(實際上是原型物件的屬性) 來鑑別物件是被哪個建構函式所建立的。
  • 每個函式都有一個 prototype 物件,它定義了該建構函式建立的所有物件共享的屬性。而 constructor 屬性實際上是定義在原型物件裡,供所有物件例項共享。
  • 每個物件例項都有 [[Prototype]] 屬性,它是指向原型物件的指標。當訪問物件的某個屬性時,先從物件自身查詢,找不到的話就到原型物件上找。
  • 內建物件的原型物件也可被修改

相關文章