一.问题背景
场景是这样:
'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
了,反正删没删也不知道