一。問題背景
場景是這樣:
'use strict';
var F = function() {
this.arr = [1, 2, 3, 4, 5, 6, 7];
var self = this;
Object.defineProperty(self, 'value', {
get: function() {
if (!self._value) {
self._value = self.doStuff();
}
return self._value;
},
set: function(value) {
return self._value = value;
}
})
}
F.prototype.doStuff = function() {
return this.arr.reduce(function(acc, value) {return acc + value}, 0);
};
F 的實例擁有一個 value 屬性,但不希望在 new 的時候就初始化屬性值(因為這個值不一定用得到,而且計算成本比較高,或者 new 的時候還不一定能算出來),那麼自然想到通過定義 getter 來實現「按需計算」:
var f = new F();
// 此時 f 身上有 value 屬性,但值是什麼還不知道
// 第一次訪問該屬性時才去計算初始值(通過 doStuff)
f.value
var tmpF = new F()
// 如果不訪問 value 屬性,就永遠不用計算其初始值
這樣可以避免預先做不必要的昂貴操作,比如:
-
DOM 查詢
-
layout(如getComputedStyle()) -
深度遍歷
當然,直接添一個 getValue() 也能達到想要的效果,但 getter 對使用方更友好,外部完全不知道值是提前算好的還是現算的
delete 的奇怪行為 分為 2 部分:
// 1.delete 用 defineProperty 定義的屬性報錯
// Uncaught TypeError: Cannot delete property 'value' of #<F>
delete f.value
// 2. 添上佔位初始值後,能正常 delete 掉了
// 把 F 的 value 定義部分改為
var self = this;
self.value = null; // 佔位,避免 delete 報錯
Object.defineProperty(self, 'value', {/*...*/});
二。原因分析
delete 報錯
記得 delete 操作符的規則是:成功 delete 返回 true,否則返回 false
無論成功刪除了沒,應該不會報錯才對。其實報錯是因為開了嚴格模式:
Third, strict mode makes attempts to delete undeletable properties throw (where before the attempt would simply have no effect):
(引自 Strict mode - JavaScript | MDN)
嚴格模式下,刪不掉就報錯。但已經通過 defineProperty() 添了 value 屬性,為什麼刪不掉呢?是 configurable 作祟:
configurable
true if and only if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object.
Defaults to false.
這個東西竟然預設是 false,查了一下發現其它幾個預設也是 false:
configurable Defaults to false.
enumerable Defaults to false.
writable Defaults to false.
value, get, set Defaults to undefined.
因為定義 descriptor 改變了屬性的讀寫方式,!writable 還算合理,!enumerable 有點強勢,而 !configurable 就有點過分了。但規則是這樣,所以奇怪行為 1 是合理的
佔位初始值
猜測如果屬性已經存在了,defineProperty() 會收斂一些,考慮一下原 descriptor 的感受:
var obj = {};
obj.value = null;
var _des = Object.getOwnPropertyDescriptor(obj, 'value');
Object.defineProperty(obj, 'value', {
get: function() {},
set: function() {}
});
var des = Object.getOwnPropertyDescriptor(obj, 'value');
console.log(_des);
console.log(des);
結果如下:
// _des
{
configurable: true,
enumerable: true,
value: null,
writable: true
}
// des
{
configurable: true,
enumerable: true,
get: (),
set: ()
}
發現 defineProperty() 後,configurable 和 enumerable 原樣沒變,所以添上佔位值後能刪掉了。另外 writable 沒了,因為定義 getter/setter 後是否可寫取決於 gettter/setter 的具體實現,一眼看不出來了(比如 setter 丟棄新值,或者 getter 返回不變的值,效果都是不可寫)
三。delete 的規則
既然遇到了 delete 的問題,乾脆再多看一點
delete var
一般都認為 delete 刪不掉 var 聲明的變量,可以刪掉屬性。實際上不全對,例如:
var x = 1;
delete x === false
// 能刪掉 var 聲明的變量
eval('var evalX = 1');
delete evalX === true
// 屬性不一定能刪掉
var arr = [];
delete arr.length === false
var F = function() {};
delete F.prototype === false
// DOM,BOM 對象不聽話的就更多了
至少從形式上來看,delete 不掉 var 聲明的變量 是不對的。至於 evalX 能被刪掉的原因,就比較有意思了,需要了解幾個東西:執行環境、變量對象/活動對象、eval 環境的特殊性
執行環境
執行環境分為 3 種:Global 環境(比如 script 標籤圈起來的環境)、Function 環境(比如 onclick 屬性值的執行環境,函數調用創建的執行環境)和 eval 環境(eval 傳入代碼的執行環境)
變量對象/活動對象
每個執行環境都對應一個變量對象,源碼裡聲明的變量和函數都作為變量對象的屬性存在,所以在全域作用域聲明的東西會成為 global 的屬性,例如:
var p = 'value';
function f() {}
window.p === p
window.f === f
如果是 Function 執行環境,變量對象一般不是 global,叫做活動對象,每次進入 Function 執行環境,都創建一個活動對象,除了函數體裡聲明的變量和函數外,各個形參以及 arguments 對象也作為活動對象的屬性存在,雖然沒有辦法直接驗證
注意:變量對象和活動對象都是抽象的內部機制,用來維護變量作用域,隔離環境等等,無法直接訪問,即便 Global 環境中變量對象看起來好像就是 global,這個 global 也不全是內部的變量對象(只是屬性訪問上有交集)
P.S. 變量對象與活動對象這種「玄幻」的東西沒必要太較真,各是什麼有什麼關係都不重要,理解其作用就好
eval 環境的特殊性
eval 執行環境中聲明的屬性和函數將作為調用環境(也就是上一層執行環境)的變量對象的屬性存在,這是與其它兩種環境不同的地方,當然,也沒有辦法直接驗證(無法直接訪問變量對象)
變量對象身上的屬性都有一些內部特徵,比如看得見的 configurable, enumerable, writable(當然內部劃分可能更細緻一些,能不能刪可能只是 configurable 的一部分)
遵循的規則 是:通過聲明創建的變量和函數帶有一個不能刪的天賦,而通過顯式或者隱式屬性賦值創建的變量和函數沒有這個天賦
內置的一些對象屬性也帶有不能刪的天賦,例如:
var arr = [];
delete arr.length === false
void function(arg) {console.log(delete arg === false);}(1);
因為屬性賦值創建的變量和函數沒有不能刪天賦,所以通過賦值創建的變量和函數可以刪,例如:
x = 1;
delete x === true
window.a = 1
delete window.a === true
而同樣會被添加到 global 身上的全域變量聲明創建的東西就不能刪:
var y = 2;
delete window.y === false
就因為創建方式不同,而創建時天賦就給定了
此外,還有一個有意思的嘗試,既然 eval 直接拿外層的變量對象,而且 eval 環境聲明的東西沒有不能刪天賦,那麼二者起來,是不是能夠覆蓋強刪?
var x = 1;
/* Can't delete, `x` has DontDelete */
delete x; // false
typeof x; // "number"
eval('function x(){}');
/* `x` property now references function, and should have no DontDelete */
typeof x; // "function"
delete x; // should be `true`
typeof x; // should be "undefined"
結果是覆蓋之後還是刪不掉,變量對象身上通過聲明方式由內部添加的屬性,貌似禁止修改 descriptor,上面的 x 值雖然被覆蓋了,但不能刪天賦還在
四。總結
通過 defineProperty() 定義的新屬性,其 descriptor 預設幾個屬性都是 false,即不可枚舉,不可修改 descriptor、不可刪除,例如:
var obj = {};
Object.defineProperty(obj, 'a', {configurable: true, value: 10});
Object.defineProperty(obj, 'a', {configurable: true, value: 100});
delete obj.a === true
Object.defineProperty(obj, 'b', {value: 11});
delete obj.b === false
// 報錯,不讓改 descriptor
// Uncaught TypeError: Cannot redefine property: b
Object.defineProperty(obj, 'b', {value: 110});
另外,delete 操作符的簡單規則如下:
-
如果操作數不是個引用,直接 return true
-
如果變量對象/活動對象身上沒有這個屬性,return true
-
如果屬性存在,但有不能刪天賦,return false
-
否則,刪除屬性,return true
所以:
delete 1 === true
基本值第一步就 true 了,反正刪沒刪也不知道
暫無評論,快來發表你的看法吧