JavaScript Prototype (原型)
JavaScript 是一個基於原型 (prototype-based) 的語言。每個物件都有一個內部連結指向另一個物件,稱為它的「原型」。這個原型物件也有自己的原型,一直連結下去,形成所謂的「原型鏈」(prototype chain)。
理解 Prototype
在 JavaScript 中,當你存取物件的屬性或方法時,如果物件本身沒有這個屬性,JavaScript 會沿著原型鏈往上找,直到找到為止或到達原型鏈的頂端(null)。
var animal = {
speak: function() {
console.log('動物發出聲音');
}
};
var dog = Object.create(animal); // dog 的原型是 animal
dog.bark = function() {
console.log('汪汪!');
};
dog.bark(); // '汪汪!' - dog 自己的方法
dog.speak(); // '動物發出聲音' - 從原型繼承
prototype 屬性
每個函式都有一個 prototype 屬性,這個屬性是一個物件。當你用 new 關鍵字呼叫函式(建構函式)時,新建立的物件會將這個 prototype 物件設為自己的原型。
function Person(name) {
this.name = name;
}
// 在 Person.prototype 上定義方法
Person.prototype.greet = function() {
console.log('Hello, I am ' + this.name);
};
var mike = new Person('Mike');
var john = new Person('John');
mike.greet(); // 'Hello, I am Mike'
john.greet(); // 'Hello, I am John'
// mike 和 john 共享同一個 greet 方法
console.log(mike.greet === john.greet); // true
將方法定義在 prototype 上,而不是在建構函式內,可以讓所有實例共享同一個方法,節省記憶體。
__proto__ 與 [[Prototype]]
每個物件都有一個內部屬性 [[Prototype]],指向它的原型。雖然 [[Prototype]] 無法直接存取,但大多數瀏覽器提供了 __proto__ 屬性來存取它:
function Person(name) {
this.name = name;
}
var mike = new Person('Mike');
// __proto__ 指向建構函式的 prototype
console.log(mike.__proto__ === Person.prototype); // true
// Person.prototype 的原型是 Object.prototype
console.log(Person.prototype.__proto__ === Object.prototype); // true
// Object.prototype 的原型是 null(原型鏈的頂端)
console.log(Object.prototype.__proto__); // null
建議使用
Object.getPrototypeOf() 來取得物件的原型,而不是 proto,因為 proto 不是標準的一部分。原型鏈
原型鏈是物件繼承的基礎:
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(this.name + ' 發出聲音');
};
function Dog(name, breed) {
Animal.call(this, name); // 呼叫父建構函式
this.breed = breed;
}
// 設定 Dog.prototype 的原型為 Animal.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log(this.name + ' 汪汪叫');
};
var myDog = new Dog('小黑', '柴犬');
myDog.bark(); // '小黑 汪汪叫'
myDog.speak(); // '小黑 發出聲音' - 從 Animal 繼承
原型鏈結構:
myDog -> Dog.prototype -> Animal.prototype -> Object.prototype -> null
檢查原型關係
instanceof
檢查物件的原型鏈中是否包含特定建構函式的 prototype:
function Person(name) {
this.name = name;
}
var mike = new Person('Mike');
console.log(mike instanceof Person); // true
console.log(mike instanceof Object); // true
console.log(mike instanceof Array); // false
isPrototypeOf()
檢查一個物件是否在另一個物件的原型鏈中:
console.log(Person.prototype.isPrototypeOf(mike)); // true
console.log(Object.prototype.isPrototypeOf(mike)); // true
hasOwnProperty()
檢查屬性是物件自己的還是從原型繼承的:
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {};
var mike = new Person('Mike');
console.log(mike.hasOwnProperty('name')); // true(自己的屬性)
console.log(mike.hasOwnProperty('greet')); // false(原型的屬性)
Object.create()
Object.create() 可以用指定的原型來建立新物件:
// 建立一個以 animal 為原型的物件
var animal = {
speak: function() {
console.log('動物發出聲音');
}
};
var dog = Object.create(animal);
dog.bark = function() {
console.log('汪汪!');
};
// 建立一個沒有原型的「純淨」物件
var pureObject = Object.create(null);
console.log(pureObject.toString); // undefined(沒有從 Object.prototype 繼承)
Object.getPrototypeOf() / Object.setPrototypeOf()
function Person() {}
var mike = new Person();
// 取得原型
var proto = Object.getPrototypeOf(mike);
console.log(proto === Person.prototype); // true
// 設定原型(不建議在執行時使用,會影響效能)
var newProto = { greet: function() { console.log('Hi'); } };
Object.setPrototypeOf(mike, newProto);
mike.greet(); // 'Hi'
在原型上擴充內建物件
你可以在內建物件的原型上新增方法(但通常不建議這樣做):
// 為所有陣列新增一個 first 方法
Array.prototype.first = function() {
return this[0];
};
var arr = [1, 2, 3];
console.log(arr.first()); // 1
擴充內建物件的原型可能會與未來的 JavaScript 標準或其他程式庫衝突,除非你很清楚自己在做什麼,否則應該避免。
ES6 Class 與 Prototype
ES6 的 class 語法本質上還是基於原型的,只是語法糖:
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log('Hello, ' + this.name);
}
}
// 等同於
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log('Hello, ' + this.name);
};
重點總結
- 每個物件都有一個原型(除了
Object.prototype,它的原型是null) - 函式的
prototype屬性會成為用new建立之物件的原型 - 屬性查找會沿著原型鏈往上找
- 使用
Object.create()可以指定新物件的原型 hasOwnProperty()可以區分自有屬性和繼承屬性