ECMAScript 仍然是 prototype-based,而非 class-based

儘管沒用到 繼承,ECMAScript 有一個很特別的 Prototype 概念,負責存放 object 的 method 與共用 property,雖然 ECMAScript 2015 支援了 class 語法,但事實上底層仍然使用 Prototype 實作,了解 Prototype 能讓我們更進一步掌握 ECMAScript。

Version


ECMAScript 5
ECMAScript 2015

Factory Function


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function createPerson(firstName, lastName) {
return {
firstName,
lastName,
};
}

const person = createPerson('Sam', 'Xiao');

const prototype = {
fullName: function() {
return this.firstName + ' ' + this.lastName;
},
};
Object.setPrototypeOf(person, prototype);

console.log(person.__proto__);
console.log(person.fullName());

我們知道若將 method 放在 object,當建立多個 object 時,會不斷建立新的 method,較浪費記憶體,比較好的方式是將 property 建立在 object 內,而將 method 建立在 prototype。

第 1 行

1
2
3
4
5
6
7
8
function createPerson(firstName, lastName) {
return {
firstName,
lastName,
};
}

const person = createPerson('Sam', 'Xiao');

建立 createPerson() Factory Function,只負責建立 object 的 property 即可。

第 10 行

1
2
3
4
5
6
const prototype = {
fullName: function() {
return this.firstName + ' ' + this.lastName;
},
};
Object.setPrototypeOf(person, prototype);

使用 Object.setPrototypeOf() 動態將 prototype 成為 person 的 prototype。

Object.setPropertyOf() 為 ECMAScript 2015 所新增

17 行

1
console.log(person.__proto__);

使用 person.__proto__ 觀察其 prototype。

__proto__ 在 ECMAScript 2015 已經成為標準

prototype000

  1. 觀察 person.__proto__
  2. person 的 prototype 為含有 fullName() 的 object。

Constructor Function


1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

Person.prototype.fullName = function() {
return this.firstName + ' ' + this.lastName;
}

const person = new Person('Sam', 'Xiao');

console.log(person.__proto__);
console.log(Person.prototype);
console.log(person.fullName());

自己使用 Factory Function 實踐 prototype 當然可行,但程式碼稍嫌冗長,ECMAScript 另外提供了 Constructor Function,讓我們以更精簡的方式實現 prototype。

第 1 行

1
2
3
4
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

類似 Factory Function 傳入 firstNamelastName,只負責建立 object 的 property 即可。

其中 this 為稍後 new 所建立的 object,因此可以直接使用 this.firstName 新增 property。

第 6 行

1
2
3
Person.prototype.fullName = function() {
return this.firstName + ' ' + this.lastName;
}

Person.prototype 預設為 {},直接對 {} 動態加上 fullName() method。

prototype 為 function 專屬的 property,在 object 沒有 prototype,只有 __proto__

第 10 行

1
const person = new Person('Sam', 'Xiao');

當 function 使用 new 時,就搖身一變成為 Constructor Function,this 為所建立的 object。

12 行

1
2
console.log(person.__proto__);
console.log(Person.prototype);

無論使用 person.__proto__Person.prototype,都得到相同結果,prototype 都是指向相同的 object。

我們可以發現 Constructor Function 寫法,遠比土法煉鋼的 Factory Function 精簡,這也是為什麼在 ES5 時代,都是使用 Constructor Function,而很少使用 Factory Function

prototype001

  1. 分別使用 person.__proto__Person.prototype 觀察
  2. person.__proto__Person.prototype 的結果都一樣
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

Person.prototype = {
constructor: Person,
fullName: function() {
return this.firstName + ' ' + this.lastName;
},
};

const person = new Person('Sam', 'Xiao');

console.log(person.__proto__);
console.log(Person.prototype);
console.log(person.fullName());

第 6 行

1
2
3
4
5
6
Person.prototype = {
constructor: Person,
fullName: function() {
return this.firstName + ' ' + this.lastName;
},
};

若 method 多時,其實有另外一種寫法,就是直接使用 Object Literal 方式定義 object,然後指定給 prototype

不過這種寫法有個地方要小心,之前 Person.prototype.fullName 是對 {} 動態新增 method,原本 {} 的 property 都還留著,其中一個最重要的 property 就是 constructor 指向 Person Constructor Function。

但若將整個 Object Literal 指定給 Person.prototype 時,別忘了要自行補上 constructor: Person,這才符合原本 Person.prototype.constructor 要指向 Constructor Function 的要求。

prototype002

  1. 分別使用 person.__proto__Person.prototype 觀察
  2. person.__proto__Person.prototype 的結果都一樣

我在學習 Prototype 時,最大的關卡是觀念上明明是要對 object 指定 prototype,為什麼到最後是對 Constructor Function 指定 prototype 呢 ?

其實剛剛有發現一個有趣的現象:

person.__proto__Person.prototype 指向同一個 object 。

Factory Function 是直接建立 prototype object,再透過 Object.setPropertyOf()__proto__ 指向 prototype object。

Constructor Function 也是直接建立 prototype object,讓 __proto__ 指向 Constructor Function 的 prototype property。

也就是說,指定給 Constructor Function 的 prototype 的 object,最後相當於指定給 object 的 __proto__,這也是為什麼我們明明希望將 prototype object 指定給 __proto__,卻最後卻指定給 Constructor Function 的 prototype,因為意義一樣,這就是 Constructor Function 的黑魔法。

Class


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

fullName() {
return this.firstName + ' ' + this.lastName;
}
}

const person = new Person('Sam', 'Xiao');

console.log(person.__proto__);
console.log(Person.prototype);
console.log(person.fullName());

ECMAScript 2015 支援 class 後,語法又更簡單了,連 prototype 字眼都完全沒看見。

prototype003

  1. 分別使用 person.__proto__Person.prototype 觀察
  2. person.__proto__Person.prototype 的結果都一樣

這再次證明了 ECMAScript 2015 的 class,其本質仍然是 prototype-based,而非 class-based,method 該放在 prototype 的觀念也完全一樣,只是 prototype 這些瑣事,底層都幫你做掉了

Conclusion


  • 隨著時代的進步,寫法不斷的精簡,但 ECMAScript 的 prototype-based 本質是不變的
  • 雖然名義上是針對 Constructor Function 的 prototype 指定 prototype object,但也相當於是對 __proto__ 指定 prototype object
  • ECMAScript 2015 的 class 寫法雖然完全看不到 prototype,但本質仍是將 method 放在 prototype,是道地的 Syntatic Sugar

Reference


MDN, Object.prototype.constructor
MDN, Object prototypes
John Resig, Secret of the JavaScript Ninja, First Edition

2018-11-05