专栏首页黯羽轻扬delete的奇怪行为

delete的奇怪行为

一.问题背景

场景是这样:

'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

本文分享自微信公众号 - 前端向后(backward-fe),作者:ayqy

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-03-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • new一个Vue

    感谢支持ayqy个人订阅号,每周义务推送1篇(only unique one)原创精品博文,话题包括但不限于前端、Node、Android、数学...

    ayqy贾杰
  • ES2018

    但如果数据源是异步的,for...of循环就只能拿到一堆Promise,而不是想要的值:

    ayqy贾杰
  • 如何理解 Scalability?

    关注「前端向后」微信公众号,你将收获一系列「用心原创」的高质量技术文章,主题包括但不限于前端、Node.js以及服务端技术

    ayqy贾杰
  • ECMAScript 6 新特性总结

    个人感觉ECMAScript 6总体上来说:添加了块级作用域,增加了一些语法糖,增强了字符串的处理,引入Generator函数控制函数的内部状态的变化,原生提供...

    IMWeb前端团队
  • JSP总结三(JSTL核心标签库的使用)

    爱撒谎的男孩
  • 微信小程序 UI界面

    组件通用属性: id class style hidden data-:用法,<view data-test="test" />,获取:e.curre...

    用户5760343
  • 0689-1.4.0-CDSW目录迁移变更技术手册

    CDSW使用一段时间以后,Master节点的/var/lib/cdsw目录挂载的是单块磁盘没有做raid等数据备份而且已经运行时间长达两年,因此为规避磁盘低概率...

    Fayson
  • 高分辨率256*256人脸生成效果介绍及代码

    众所周知,训练GAN非常困难. In order to train at 256 x 256 we utilize:

    用户1908973
  • 0817-6.3.3-Impala执行DDL慢问题分析报告

    随着集群使用时间的增长,在Impala中执行DDL语句消耗的时间越来越长,排查该问题时进行测试,create一张表的耗时达到4-5s,drop一张表的时间5-1...

    Fayson
  • JavaScript组件设计思想(二)

    开发中,我们经常会遇到在用户登录成功后我们需要初始化“header”、“toolbar”、“menu”等情况。通常的做法是在登录成功回调中去调用对应模块的初始化...

    奋飛

扫码关注云+社区

领取腾讯云代金券