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

重點總結

  1. 每個物件都有一個原型(除了 Object.prototype,它的原型是 null
  2. 函式的 prototype 屬性會成為用 new 建立之物件的原型
  3. 屬性查找會沿著原型鏈往上找
  4. 使用 Object.create() 可以指定新物件的原型
  5. hasOwnProperty() 可以區分自有屬性和繼承屬性