跳至主要内容

[JS] DefineProperty

TL;DR

物件的屬性還有許多特徵可以調整,這個章節介紹透過Object.defineProperty來調整物件的屬性特徵。

參考資料

相關連結


defineProperty

一個物件屬性的特徵會包含:

  1. 值(value)
  2. 可否被寫入(writable)
  3. 可否被刪除(configurable)
  4. 可否被列舉(enumerable)

我們可以透過Object.defineProperty來調整屬性的特徵。

語法

Object.defineProperty(obj, prop, descriptor)
  • obj:要定義屬性的物件。

  • prop:要被定義或修改的屬性名字。

  • descriptor:要定義或修改物件敘述內容。

getOwnPropertyDescriptor()

在開始介紹屬性特徵之前...
我們要先知道一個取出屬性特徵參數Object原型方法- Object.getOwnPropertyDescriptor()

語法如下:

Object.getOwnPropertyDescriptor(obj, prop)

會返回指定對象(obj)內的指定屬性(prop)的屬性特徵。

範例:

const foo={
a:100,
}

console.log(Object.getOwnPropertyDescriptor(foo,'a'));
// {value: 100, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor(foo.__proto__,"constructor");
// {writable: true, enumerable: false, configurable: true, value: ƒ}
info

我們自己定義的物件屬性,預設特徵writableconfigurableenumerable都會是true。代表,可寫入,可調整,可列舉。

也會發現原型內的方法特徵主要差異在於不可被列舉

範例

透過一個基本的範例,來分別介紹descriptor不同設定時的影響

writable

接著我們建立物件,首先先調整a屬性的writable

const foo={
a:1,
b:2,
c:3
}

console.log(foo); // {a: 1, b: 2, c: 3}

Object.defineProperty(foo,'a',{
value:4,
writable:false,
configurable:true,
enumerable:true
})
console.log(foo) // {a: 4, b: 2, c: 3}

foo.a=5;
console.log(foo); // {a: 4, b: 2, c: 3}

foo['a']=8;
console.log(foo); // {a: 4, b: 2, c: 3}

Object.defineProperty(foo,'a',{
value:100,
writable:false,
configurable:true,
enumerable:true
})
console.log(foo); // {a: 100, b: 2, c: 3}

設定為不可寫入,只會影響透過點記法或括號法來寫入值的方法。

雖然設定為不可寫入(writable:false),但是仍然可以再次透過defineProperty來調整參數的值。

tip

禁止寫入屬性如果值是一個物件,仍然可以對該物件深層進行修改(i.e:只能做到淺層保護)

const foo={
a:3
}

Object.defineProperty(foo,'bar',{
value:{},
writable:false
})

foo.bar.test='仍然可被寫入'

console.log(foo) // {a: 3, bar: {test: "仍然可被寫入"}};
caution
  • 非嚴格模式靜默錯誤:

在一般模式下,試著調整已被設定禁止寫入的屬性時,並不會有任何錯誤產生,後方程式碼會繼續執行。


  • 嚴格模式錯誤

在嚴格模式下如果試著調整已被設定禁止寫入的屬性時,則會出現錯誤,並且後方程式碼會因為錯誤而無法執行:

const foo={
a:3
}
Object.defineProperty(foo,'a',{
writable:false
})
a=5; // 這行會執行,但是不會寫入,也不會出現錯誤

(function(){
'use strict'
foo.a=10;
})(); // TypeError: Cannot assign to read only property 'a' of object '#<Object>'
console.log('這行會因為前方的錯誤無法執行');

執行結果圖示

範例程式碼執行結果

configurable

const foo={
a:1,
b:2,
c:3
}

console.log(Object.getOwnPropertyDescriptor(foo,'b'));
// {value: 2, writable: true, enumerable: true, configurable: true}

Object.defineProperty(foo,'b',{
configurable:false
})

console.log(Object.getOwnPropertyDescriptor(foo,'b'));
// {value: 2, writable: true, enumerable: true, configurable: false}

delete foo.b; // false

foo.b=5;

console.log(foo); // {a: 1, b: 5, c: 3}

Object.defineProperty(foo,'b',{
configurable:true
})
// Uncaught TypeError: Cannot redefine property: b
// 這邊因為嘗試重新改回configurable:true(可修改屬性),所以產生錯誤
// 在舊值為false的狀態下,唯一能調整的只有writable
// 並且writable只能是由true改為false。(即writable由false改回true也會噴出錯誤)

configurable設定為false之後,我們就無法透過delete運算子刪除該屬性(會回傳false代表刪除失敗~),但是仍然可以直接修改值,(包含使用defineProperty修改value也可以)。

caution

修改屬性:

如果該屬性已經存在, Object.defineProperty() 將會根據描述符內的值和物件當前的 configuration 來修改屬性。

如果舊的描述符之 configurable 的特徵為 false (屬性為 「non-configurable」), 那除了 writable 之外的特徵都將無法修改。 在這個情況,也不可能在 data 和 accessor 屬性類型中來回切換。

如果有一個屬性是 non-configurable, 那它的 writable 特徵只能被改變為 false.
若嘗試改變 non-configurable property attributes,將會丟出一個 TypeError,除非當前之值與新值相同。

i.e不管enumerable本來是什麼,只要configurablefalse之後也都無法再更改了。

enumerable

最後來調整可否被列舉(enumerable)特徵:

const foo={
a:1,
b:2,
c:3
}

Object.defineProperty(foo,'c',{
enumerable:false
})

for (const key in foo) {
console.log(`列舉${key}`);
}

// 執行結果:
// 列舉a
// 列舉b
info

for...in...語法除了物件本身以外,也會列舉出原型內所有可列舉的屬性

const foo={
a:1,
b:2,
c:3
}

for (const key in foo) {
console.log(key);
}
// a
// b
// c

Object.getOwnPropertyDescriptor(Object.getPrototypeOf(foo),'toString');
// {writable: true, enumerable: false, configurable: true, value: ƒ}

Object.defineProperty(Object.getPrototypeOf(foo),'toString',{
enumerable:true
})

Object.getOwnPropertyDescriptor(Object.getPrototypeOf(foo),'toString');
// {writable: true, enumerable: true, configurable: true, value: ƒ}

for (const key in foo) {
console.log(key);
}
// a
// b
// c
// toString

雖然原始定義的原型,都已經預設是不可列舉了。但是如果今天是自己定義的原型,就有可能會列舉出不在物件本身,而是存在於原型中的屬性。

為了避免這個問題,我們可以使用VSCode自動補全程式碼,在輸入forin之後<Tab>產生程式碼片段如下

for (const key in object) {
if (Object.hasOwnProperty.call(object, key)) {
const element = object[key];

}
}

透過Object.hasOwnProperty.call傳入object當作thisArg,並且將key值自動帶入執行後,會回傳一個布林值。如果是true代表object真的具有key的這個屬性,才會執行block內的程式碼

defineProperties

使用defineProperties可以一次改變多個屬性的特徵,範例如下:

const foo={}
Object.defineProperties(foo,{
a:{
value:'a'
},
b:{
value:'b',
writable:false
},
c:{
value:{},
writable:false,
configurable:false,
enumerable:false
}
})

console.log(foo) // {a: 'a', b: 'b', c: {}}

原型的列舉屬性特徵

在開始說明原型的屬性特徵時,我們要先知道hasOwnProperty的運作模式

hasOwnProperty

MDN文件的說明:hasOwnProperty() 回傳物件是否有該屬性的布林值。

我們現在知道hasOwnProperty的用途,那麼如果在原型鏈上,存在該屬性,hasOwnProperty會不會回傳true呢?

info

先說結論:

hasOwnProperty只會在當下物件真的存在該屬性時,才會返回ture。如果物件本身不存在該屬性而原型鏈上存在,也會回傳false。

透過以下範例來驗證:

function Person() {} // 定義一個簡單的建構函式

Person.prototype.myName='人類'; // 新增原型的屬性myName,值為'人類'

const foo=new Person(); // 建立instance
console.log(foo.__proto__.myName) // '人類'
console.log(foo.hasOwnProperty('myName')) // false

雖然Person原型裡面確實有我們新增的myName原型屬性,但是hasOwnProperty回傳的結果仍然是false

note

手動賦予的undefined仍然會被識別為具有該屬性:

// 延續上一段程式碼範例
foo.a=undefined;
foo.b=null;

console.log(foo.a); // undefined
console.log(foo.b); // null
console.log(foo.c) // undefined

console.log(foo.hasOwnProperty('a')); // true
console.log(foo.hasOwnProperty('b')); // true
console.log(foo.hasOwnProperty('c')); // false

我們把注意力放在foo.a以及foo.c上,可以發現雖然同樣都是列印出undefined,但是因為在屬性a上我們是手動賦予undefined這個值的(當然實務上我們並不會賦予變數undefined,最多只會賦予null而已),是實際在記憶體上存在這個記憶體空間的,所以在hasOwnProperty顯示出來的結果也會不同。

原生原型特徵

原始型別的原型內本身會有許多原型方法,而這些原型方法跟我們自己定義的原型的原型方法之間,最大的差異就在於:是否可被列舉

先前也有提到,for...in語法會沿著原型鏈將所有enumerable屬性為true的屬性放入迴圈中,所以以上一段範例微粒,我們自己定義的Person.prototype.myName因為並沒有另外設定defineProperty,所以預設是可列舉的

for (const key in foo) {
console.log(`${key}:${foo[key]}`);
}
// myName:人類

也可以發現我們自己定義的原型在開發者工具中的顏色會跟原生方法不同(以Arc瀏覽器為例):

enumerable的特徵不同時,所顯示的顏色也會不同

enumerable的特徵不同時,所顯示的顏色也會不同

如果不想取到原型鏈上的屬性,也可以使用hasOwnProperty過濾:

for (const key in object) {
if (Object.hasOwnProperty.call(object, key)) {
const element = object[key];

}
}