前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >delete的奇怪行为

delete的奇怪行为

作者头像
ayqy贾杰
发布2019-06-12 12:22:28
2.3K0
发布2019-06-12 12:22:28
举报
文章被收录于专栏:黯羽轻扬黯羽轻扬

一.问题背景

场景是这样:

'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.valuevar 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()后,configurableenumerable原样没变,所以添上占位值后能删掉了。另外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 === trueObject.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了,反正删没删也不知道

参考资料

  • Understanding delete
  • Object.defineProperty() – JavaScript | MDN
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-03-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一.问题背景
  • 二.原因分析
    • delete报错
      • 占位初始值
      • 三.delete的规则
        • delete var
          • 执行环境
          • 变量对象/活动对象
          • eval环境的特殊性
      • 四.总结
        • 参考资料
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档