for...in 遇到 class 時之靈異現象

Object.keys()for...in 的差異在於 Object.keys() 只能顯示目前 Object 的 Property Key,而 for...in 會連同 Prototype 的 Property Key 一併顯示。

for...in 搭配 Constructor Function 或 Object.create() 時一切正常,但搭配 class 時,就無法顯示 Prototype 的 Property Key 了,為什麼會這樣呢 ?

Version


ECMAScript 2015

Constructor Function


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.fullName = function() {
return this.firstName + ' ' + this.lastName;
};

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

for(let key in person)
console.log(key);

// firstName
// lastName
// fullName

由 Constructor Function 建立 object,並將 method 定義在 Prototype,這是 ECMAScript OOP 標準寫法。

當對 object 使用 for...in ,會顯示所有的 Property Key,連 Prototype 也會顯示。

forin000

  1. 使用 Prototype 定義 fullName() method
  2. 使用 for...in 列出 object 所有的 Property Key
  3. 連 Prototype 的 fullName 也會列出

Object.create


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const prototype = {
fullName: function() {
return this.firstName + ' ' + this.lastName;
},
};

const person = Object.create(prototype);
person.firstName = 'Sam';
person.lastName = 'Xiao';

for(let key in person)
console.log(key);

// firstName
// lastName
// fullName

Prototype 除了事後在 Constructor Function 動態指定外,也可以事先建立 Prototype object,然後傳入 Object.create()

一樣使用 for...in,也如預期列出 Prototype 的 Property Key。

forin001

  1. 先定義 Prototype object
  2. 使用 Object.create() 建立 object,並將 Prototype object 傳入
  3. 一樣使用 for...in 列出 object 所有的 Property Key
  4. 連 Prototype 的 fullName 也會列出

Class


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

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

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

for(let key in person)
console.log(key);

// firstName
// lastName

ECMAScript 2015 導入 class 語法後,method 可以直接定義在 class 內,會自己幫我們定義在 Prototype。

但使用 for..in,卻發現 Prototype 的 fullName 無法顯示,退化成與 Object.keys() 功能一樣,為什麼會這樣呢 ?

forin002

  1. 改用 class 寫法
  2. 將 method 定義在 class 內
  3. 一樣使用 for...in 列出 object 所有的 Property Key
  4. 沒有列出 Prototype 的 fullName

Why ?


for...inObject.keys() 都僅能列出 Enumerable Property,所以很有可能 class 的寫法,造成 Prototype 的 property 為 Non Enumerable,使得 for...in 無法顯示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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(Object.getOwnPropertyDescriptor(person, 'firstName'));
console.log(Object.getOwnPropertyDescriptor(person, 'lastName'));
console.log(Object.getOwnPropertyDescriptor(person.__proto__, 'fullName'));

for(let key in person)
console.log(key);

Object.getOwnPropertyDescriptor() 會傳回每個 property 的屬性,藉此觀察是否為 Non Enumerable。

forin003

  1. 特別加上 Object.getOwnPropertyDescriptor() 觀察之
  2. 果然如假設所言,Prototype 的 fullNameenumerablefalse,因此 for...in 無法顯示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
'use strict';

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Person = function () {
function Person(firstName, lastName) {
_classCallCheck(this, Person);

this.firstName = firstName;
this.lastName = lastName;
}

_createClass(Person, [{
key: 'fullName',
value: function fullName() {
return this.firstName + ' ' + this.lastName;
}
}]);

return Person;
}();

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

for (var key in person) {
console.log(key);
}
//# sourceMappingURL=es6class.js.map

從另外一個角度,我們由 Babel 所 transpile 的 ES5 觀察。

1
descriptor.enumerable = descriptor.enumerable || false;

對於 Prototype 的 property,Babel 也是將 enumerable 設定為 false

由這兩點都可以證明,當使用 class 寫法時,根據 ECMAScript 2015 規格,都會將 Prototype 的 property 設定為 Non Enumerable Property,也就是 for...in 功能將等同 Object.keys()

Conclusion


  • for...in 為 ECMAScript 3 所定義,而 Object.keys() 為 ECMAScript 5.1 所加入,理論上兩者的差異就在於 Prototype 部分,但 ECMAScript 2015 支援 class 後,又使得 for..inObject.keys() 功能趨於一至,個人是不太喜歡這種改變,這使得 for...in 與過去的觀念不同,算 breaking change,但既然 ECMAScript 2015 規格就是這樣定義,也只能自己注意這個微小的差異了
2018-11-04