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