JavaScript 核心觀念(56)- 物件屬性延伸章節:屬性的特徵 - 屬性列舉與原型的關係

前言

接下來回到原型跟前面一直在講的枚舉,因此這邊會說明枚舉與原型兩者之間的關係。

屬性列舉與原型的關係

首先我們先講講原型,一開始我們會透過以下程式碼建立原型

1
2
3
4
function myName() {}
myName.prototype.name = 'QQ';

var ray = new myName();

接下來當我們 new 一個實體出來之後,其實它也是一個物件,因此可以針對該實體賦予,這個行為是不會出現錯誤的

1
2
3
4
5
6
7
function myName() {}
myName.prototype.name = 'QQ';

var ray = new myName();

ray.a = 'Hello';
console.log(ray); // { a: 'Hello' };

如果你將上面程式碼貼到瀏覽器中之後,也可以嘗試展開 __proto__ 看到 name: "QQ",這個 name 就是原型中的屬性:

__proto__

這邊有一個額外好玩的實驗,Object 中有一個方法叫做 hasOwnProperty()hasOwnProperty 可以用於檢測物件中有沒有這個屬性的存在

1
2
3
4
var obj = {
a: 'Ray',
}
obj.hasOwnProperty('a'); // true

這邊要注意 hasOwnProperty 是找這個物件中是否存在這個屬性,與它的值並沒有任何關係的,因此我們知道若該屬性不存在是會回傳 false

1
2
3
4
5
var obj = {
a: 'Ray',
}
obj.hasOwnProperty('a'); // true
obj.hasOwnProperty('b'); // false

因此就算你的值是賦予 undefined 依然是會顯示存在的,因為這個屬性是存在於這個物件中

1
2
3
4
5
6
7
var obj = {
a: 'Ray',
c: undefined
}
obj.hasOwnProperty('a'); // true
obj.hasOwnProperty('b'); // false
obj.hasOwnProperty('c'); // true

接下來拉回剛剛的範例:

1
2
3
4
5
6
7
function myName() {}
myName.prototype.name = 'QQ';

var ray = new myName();

ray.a = 'Hello';
console.log(ray); // { a: 'Hello' };

我們知道可以使用 hasOwnProperty 去找值,但是這邊要注意,雖然原型有向上尋找的特性,但是 hasOwnProperty 是只針對當前物件作為尋找,因此是無法找到掛在 prototype 上的 name 屬性的:

1
2
3
4
5
6
7
8
function myName() {}
myName.prototype.name = 'QQ';

var ray = new myName();

ray.a = 'Hello';
console.log(ray); // { a: 'Hello' };
ray.hasOwnProperty('name'); // false

那麼列舉是什麼呢?在前面章節有講過,簡單來講就是可以不可以被讀取出來,因此建立在 prototype 上的屬性是否可以被取得?

1
2
3
4
5
6
7
8
9
10
11
function myName() {}
myName.prototype.name = 'QQ';

var ray = new myName();

ray.a = 'Hello';

for(var key in ray) {
console.log(key);
}
console.log(Object.getOwnPropertyDescriptor(ray.__proto__.name));

你如果將上述程式碼貼到瀏覽器應該會發現,for in 竟然會將掛在原型上的屬性給列舉出來,基本上這邊會覺得很奇怪,如果原型是可以被列舉的狀況下被列舉出來是滿合理的,但是原型內的屬性當然不只有這些,為什麼卻沒有被列舉出來呢?主要原因出在 Object.defineProperty 上,在這邊的 name 屬性原型特徵與原生的原型特徵是有不同的,好奇的話可以試著使用 Object.getOwnPropertyDescriptor 來試著查看原型特徵:

1
console.log(Object.getOwnPropertyDescriptor(ray.__proto__, 'name')); // configurable: true, enumerable: true, value: "QQ". writable: true

基本上是可以看到與一般的物件屬性特徵是相同的,好奇的話自己也可以嘗試看一下原生原型與我們自訂的原型有何差異:

1
console.log(Object.getOwnPropertyDescriptor(ray.__proto__.__proto__, 'toString'));

那麼該如何解決這個問題呢?其實很簡單,函式本身也是屬於物件的一種,因此可以針對函式去調整屬性特徵:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function myName() {}
myName.prototype.name = 'QQ';

Object.defineProperty(myName.prototype, 'name', {
enumerable: false,
});

var ray = new myName();

ray.a = 'Hello';

for(var key in ray) {
console.log(key);
}

這樣子你在執行時就不會出現原型中的 name 屬性哩~

(你可以嘗試看一下 myName__proto__.name 屬性呈現的顏色變成如何唷)

如果沒有設定 Object.defineProperty 也可以透過以下程式碼來避免 for in 跑出不應該列舉出來的原型屬性:

1
2
3
4
5
for(var key in ray) {
if(ray.hasOwnProperty(key)) {
...
}
}

參考文獻