JS 屬性特徵

屬性特徵是甚麼

我們可以使用Object.defineProperty調整屬性特徵,但defineProperty不能對子屬性造成限制,屬於淺層保護

可以調整特徵為:

  1. 可否寫入
  2. 可否被刪除
  3. 可否被列舉

此種用法比較常在大型專案使用到,例如Vue

列舉以下範例說明,可調整物件屬性的特徵,如果writablefalse,對此屬性進行賦值,將會失敗,會有靜默的錯誤,可以在嚴格模式下查看,如想一次定義大量屬性特徵,可以使用defineProperties

var example = {
a: 1,
b: 2,
c: 3,
};

// 可設定物件屬性特徵
Object.defineProperty(example, "a", {
value: 4, // 設值
writable: false, // 可否寫入
configurable: true, // 可否被刪除
enumerable: true, // 可否被列舉
});

// 賦值
example.a = 10;

// 刪除
delete example.a;

// 列舉
for (var key in example) {
console.log(`列舉${key}`);
}

(function () {
"use strict";
example.a = 10; // 報錯: 不可對於不可寫入的值進行賦值
})();

// 定義大量屬性範例
Object.definePropreties(example, {
a: {
configurable: false, // 可否被刪除
},
b: {
writable: false, // 可否寫入
},
});

屬性延伸方法

防止擴充preventExtensions

Object.preventExtensions() 可用來避免物件被新增新的屬性,物件如果可以被增加新的屬性,我們稱它可以被擴充(extensible)
Object.preventExtensions() 標註物件使它無法被擴充,所以在它被標註為無法擴充當下,它將無法再增加新的屬性。

var example = {
a: 1,
b: 2,
c: 3,
};

Object.preventExtensions(example);
console.log(`是否可被擴充${Object.isExtensible(example)}`); // 是否可被擴充false
// Object.defineProperty throws 當為無法擴充的物件增加屬性
Object.defineProperty(example, "new", { value: 8675309 }); // throws a TypeError

封裝seal

MDN: Object.seal()方法封閉一個對象,阻止添加新屬性並將所有現有屬性標記為不可配置。當前屬性的值只要原來是可寫的就可以改變。

簡單來說就是無法新增刪除擴充,也無法配置特性,但如果原屬性值可寫入就可調整該屬性值

var example = {
a: 1,
b: 2,
c: 3,
};

Object.seal(example);
console.log(`是否可被擴充${Object.isExtensible(example)}`); // 是否可被擴充false

Object.defineProperty(example, "new", { value: 8675309 }); // 錯誤不可擴充

// 可改變屬性值
example.a = 111;
console.log(example.a); // 輸出: 111

凍結freeze

MDN: Object.freeze() 方法可以凍結一個對象。一個被凍結的對象再也不能被修改;凍結了一個對象則不能向這個對象添加新的屬性,不能刪除已有屬性,不能修改該對像已有屬性的可枚舉性、可配置性、可寫性,以及不能修改已有屬性的值。此外,凍結一個對像後該對象的原型也不能被修改。 freeze() 返回和傳入的參數相同的對象。

簡單來說就是,物件加上seal,並且無法更改值,原型也不能被修改

var example = {
a: 1,
b: 2,
c: 3,
};

Object.freeze(example);
console.log(`是否可被擴充${Object.isExtensible(example)}`); // 是否可被擴充false

Object.defineProperty(example, "new", { value: 8675309 }); // Cannot define property new, object is not extensible

example.a = 111; // 嚴格模式下會拋出錯誤
console.log(example.a); // 輸出: 1

屬性列舉與原型關係

如果我們使用建構函式建立一個物件,使用for將物件屬性輸出出來看的時候,會發現原型的屬性也一併輸出出來,
為什麼會如此可以查看他們原型的屬性,joe的 name 屬性特徵,可以發現它是可以被列舉的,因此才會輸出

function Person() {}
Person.prototype.name = "人類";

// 可將name設為不可列舉
Object.defineProperty(Person.prototype, "name", {
enumerable: false, // 可否被列舉
});

var joe = new Person();
joe.a = 1;

// 會輸出a name
for (var key in joe) {
console.log(`key:${key}`);
}

console.log(Object.getOwnPropertyDescriptor(joe.__proto__, "name")); // 查看name屬性特徵,可以發現可列舉
console.log(
Object.getOwnPropertyDescriptor(joe.__proto__.__proto__, "toString")
); // 查看toString屬性特徵,可發現不可列舉

// 如果只需要當前物件屬性,可以在迴圈使用hasOwnProperty確認,
// 或是將屬性enumerable設為false
for (var key in joe) {
if (joe.hasOwnProperty(key)) {
console.log(`key:${key}`);
}
}

Getter 和 Setter

set 語法會在物件屬性被嘗試定義時,將其屬性綁定到要呼叫的函式內。

get 語法會將物件屬性,綁定到屬性被檢索時,所呼叫的函式。

從以下範例可以看到可以透過setget賦值運算不使用函式

var wallet = {
total: 100,
set save(price) {
this.total = this.total + price - 5;
},
get save() {
return this.total / 2;
},
};

wallet.save = 500;
console.log(wallet);

也可以使用Object.defineProperty的方式賦予

var wallet = {
total: 100,
};

Object.defineProperty(wallet, "save", {
set: function (price) {
this.total = this.total + price - 5;
},
get: function () {
return this.total / 2;
},
});

wallet.save = 500;

console.log(wallet);
console.log(Object.getOwnPropertyDescriptor(wallet, "save"));

輸出如下可以看到屬性特徵被設為不可刪除及不可列舉,如有需要也可在defineProperty將他更改為true

也可以在陣列原型上使用

var lists = [1, 2, 3];

Object.defineProperty(Array.prototype, "latest", {
get: function () {
return this[this.length - 1];
},
});

console.log(lists.latest); // 3