谨慎使⽤delete
这⾥⼤家可能要笑了,这不就⼀个操作符吗,还⽤单独来讲。
有这时间,还不如去看看react源码,vue源码。
我说:react源码会去看的,但是这个也很重要。
这⾥提⼏个问题
1. delete的返回值是什么
2. delete删除不存在的属性返回值是什么
3. 能不能删除原型上的属性
4. 能否删除变量
5. 删除数组某个数据,数组长度会不会变
6. 哪些属性不能被删除
我们就挨个来验证⼀下
1. delete的返回值是什么
var a = { p1: 1}console.log(delete a.p1); // trueconsole.log(delete a.p2); // trueconsole.log(delete window); // fal
从上⾯可以看出delete返回的是布尔值,如果删除成功返回真,这⾥包括删除⼀个不存在的属性。删除失败返回fal。
2. delete删除不存在的属性返回值是什么
从第⼀个demo看出,删除不存在的属性返回值也是true
3. 能不能删除原型上的属性
var a = { p1: 10}
a.__proto__ = { p2: 20}console.log("a.p2:before", a.p2); // 20console.log(delete a.p2); // trueconsole.log("a.p2:after", a.p2); // 20
我上⾯的代码是为了省事,你最好不要直接使⽤__proto__。
好吧,我还是写⼀个正经⼀点的例⼦。
function Foo(){ this.name = "name";
}
Foo.prototype.age = 20;var foo = new Foo();console.log("foo.p2:before", foo.age); // 20console.log(delete foo.age); // trueconsole.log("foo.p2:after", foo.age); // 20
我都说了,不要在乎哪个写法,结果你就是不信,结果还是⼀样的。
你没法删除原型上的属性。
4. 能否删除变量
var a = 10;console.log(delete a); // falconsole.log("a", a); // 10
显然,是删除不掉,你换成函数,结果也是⼀样的。
5. 删除数组某个数据,数组长度会不会变
var arr = [10,2,16];console.log("length:before", arr.length); // 3console.log("delete", delete arr[1]); // trueconsole.log("length:after",arr.length); // 3console.log("arr", arr); // [10, empty, 16]
delete删除数据的某个数据,并不会导致数组的长度变短。
对应的数据的值会变成empty, 不是undefined或者null。
是未初始化的意思,你⽤new Array(2)会得到[empty × 2]。
都是⼀个意思。
这⾥我们接着对empty扩展⼀下。
var arr = [0];
arr[10] = 10;
arr.forEach(v=>console.log(v)); // 0 ,10for(let p in arr){ console.log(arr[p]); // 0, 10}for(let p of arr){ console.log(arr[p]); // 0 ,undefined x 9, 10}
forEach和in并不会对未初始化的值进⾏任何操作。
具体可以参见
No operation for uninitialized values (spar arrays)
照片日记6. 哪些属性不能被删除
1. var const let等变量 , 全局变量。
delete window // falvar a;delete a; // fal// 有意思的delete thisfunction a (){
this.a = 333; console.log("delete this:" , delete this); // true
console.log("a", this.a, this); // 333, {a:333}}
a.call({});
1. 数据属性configurable为fal的属性
ES5严格模式中delete configuable为fal的对象时会直接抛异常
// 内置document, location等OwnPropertyDescriptor(window, "document");// { configurable:
fal }console.log("delete", delete window.document); // falconsole.log("delete", delete window.location); // fal// 数组长度var arr = [];Ob
1. 原型上的属性
2. 函数参数
function delP(){ console.log("delete", delete arguments); // fal
console.log("arguments", arguments); // 0: 1}
delP(1);
1. ⼀些常量(NaN、Infinity、undefined)
delete NaN; // faldelete Infinity; // faldelete undefined; // fal
1. 函数声明
function fn() {}delete fn;console.String()); // function fn() {}
更多细节
ECMA-262_5th_edition_december_2009.pdf
ECMA-262_3rd_edition_december_1999.pdf 58页
JavaScript中delete操作符不能删除的对象
我们可以看⼀下ES3的定义, ES5的定义是有变动的。
The delete OperatorThe production UnaryExpression : delete UnaryExpression is evaluated as follows:1. Evaluate UnaryExpression.2. If Type(Result(1)) is not Reference, return true.3. Call GetBa(Result(1)).4. Call GetPropertyN
我简单翻译⼀下,可能不太正确哈:
1. 执⾏⼀元表达式
2. 如果第⼀步返回的未被引⽤,返回真
console.log(delete xxxxxxxxxx) //true
console.log(delete "a") // true
拍手唱歌笑呵呵
console.log(delete {a:1}) // true
console.log(delete 1) // true
1. 取到对象
2. 取属性名
3. ⽤第四步获得的属性名,在第三步返回的结果上进⾏删除操作
4. 返回第五步返回的结果。
这⾥的Resuslt(1)本⾝应该不是数据本⾝,类似⼀个引⽤地址吧。
1. delete 返回fal, ⼀定是没删除成功
清风徐来王菲
2. delete 返回true,不⼀定删除成功
所以,delete返回true,最好⾃⼰再动⼿检查⼀下。万⽆⼀失。
额,我是不是跑题了,今天的主题,不是告诉你如何使⽤delete,⽽是谨慎⽤delete。
我们先创建1万个对象,每个对象都有p0到p24 ⼀共25个属性。
然后我们按照⼀定的规则删除属性和设置属性为undefined。
function createObjects(counts = 10000) { var arr = []; for (let i = 0; i < counts; i++) { const obj = {}; // for (let j = 0; j < pcounts; j++) {
// obj[`p${j}`] = `value-${i}-${j}`;
// }
arr.push({ "p0": `value-${i}-0`, "p1": `value-${i}-1`, "p2": `value-${i}-2`, "p3": `value-${i}-3`, "p4": `value-${i}-4`, "p5": `value-${i}-5`, "p6": `value-${i} });
} return arr;
} const arr = createObjects(); const arr2 = createObjects(); console.time("del"); for (let i = 0; i < arr.length; i++) { const rd = i % 25; delete arr[i][`p${rd}`]
} console.timeEnd("del"); console.time("t"); for (let i = 0; i < arr2.length; i++) { const rd = i % 25;
arr2[i][`p${rd}`] = undefined;
} console.timeEnd("t");// del: 31.68994140625 ms// t: 6.875 ms// del: 24.43310546875 ms // t: 3.7861328125 ms// del: 79.622802734375 ms// t: 3.876953125 ms// del: 53.015869140625 ms// t: 3.242919921875 ms// de
我们记录了⼤约五次执⾏事件对⽐。
可以看出来delete 时间不稳定,⽽且性能低不少。
到这⾥,我们还不要惊讶。看我稍微改动⼀下代码:
function createObjects(counts = 10000) { var arr = []; for (let i = 0; i < counts; i++) { const obj = {}; // for (let j = 0; j < pcounts; j++) {
// obj[`p${j}`] = `value-${i}-${j}`;设备租赁合同范本
// }
arr.push({ 0: `value-${i}-0`, 1: `value-${i}-1`, 2: `value-${i}-2`, 3: `value-${i}-3`, 4: `value-${i}-4`, 5: `value-${i}-5`, 6: `value-${i}-6`, 7: `value-${i}-7`, });
} return arr;
} const arr = createObjects(); const arr2 = createObjects(); console.time("del"); for (let i = 0; i < arr.length; i++) { const rd = i % 25; delete arr[i][rd]
} console.timeEnd("del"); console.time("t"); for (let i = 0; i < arr2.length; i++) { const rd = i % 25;
arr2[i][rd] = undefined;
} console.timeEnd("t");// del: 1.44189453125 ms// t: 2.43212890625 ms// del: 1.737060546875 ms // t: 3.10400390625 ms// del: 1.281005859375 ms// t: 2.85107421875 ms// del: 1.338134765625 ms// t: 1.877197265625 m
到这⾥,画风⼀转。 del居然⽐t还快了。。。。。。
⽽t的速度实际基本没有什么变化。
这⾥就要提出⼏个概念:
常规属性 (properties) 和排序属性 (element)。
上⾯的代码变化不多,就是属性名称从p0格式修改为了0格式。
p0正式常规属性,0是排序属性。
对象中的数字属性称为排序属性,在 V8 中被称为 elements。
字符串属性就被称为常规属性,在 V8 中被称为 properties。
在 ECMAScript 规范中定义了数字属性应该按照索引值⼤⼩升序排列,字符串属性根据创建时的顺序升序排列。
function Foo() { this[3] = '3'
this["B"] = 'B'
this[2] = '2'
this[1] = '1'
this["A"] = 'A'
this["C"] = 'C'
} var foo = new Foo() for (key in foo) { console.log(`key:${key} value:${foo[key]}`)
}// key:1 value:1// key:2 value:2// key:3 value:3// key:B value:B// key:A value:A// key:C value:C
我们的数字属性设置的顺序为 3 -> 2 -> 1, 实际遍历输出的时候为 1->2->3;
我们的字符串属性设置顺序为 B->A->C, 实际输出 B->A->C。
到这⾥为⽌,我们知道我们的两个栗⼦,⼀个使⽤的是数字属性(排序属性),⼀个使⽤的是字符串属性(常规属性)。
暂停⼀下:
有⼀种说法,逆向删除属性,不会导致map被改变。要不要试试。
说到这⾥,⼤家还会说,就算是这样。和速度有⽑关系?
现在是还看不来,我们还要提出⼀个新的概念,隐藏类。
图解 Google V8 ⾥⾯是这样描述的:
V8 在运⾏ JavaScript 的过程中,会假设 JavaScript 中的对象是静态的,具体地讲,V8 对每个对象做如下两点假设:
对象创建好了之后就不会添加新的属性;
对象创建好了之后也不会删除属性。
具体地讲,V8 会为每个对象创建⼀个隐藏类,对象的隐藏类中记录了该对象⼀些基础的布局信息,包括以下两点:
对象中所包含的所有的属性;
每个属性相对于对象的偏移量。
亮叶桦
有了隐藏类之后,那么当 V8 访问某个对象中的某个属性时,就会先去隐藏类中查找该属性相对于它的对象的偏移量,有了偏移量和属性类型,V8 就可以直接去内存中取出对于的属性值,⽽不需要经历⼀系列的查找多个对象共⽤⼀个隐藏类
那么,什么情况下两个对象的形状是相同的,要满⾜以下两点:
相同的属性名称;
相等的属性个数
在执⾏过程中,对象的形状是可以被改变的,如果某个对象的形状改变了,隐藏类也会随着改变,这意味着 V8 要为新改变的对象重新构建新的隐藏类,这对于 V8 的执⾏效率来说,是⼀笔⼤的开销。
看到红⾊部分,你就应该差不多得到答案了。
那如何查看隐藏类呢?
使⽤chrome的开发者⼯具,Memory模块的 Heap snapshot功能:
。
然后再搜索对应的构造函数,⽐如Foo
这⾥为了⽅便查找,我们简单包装⼀下代码:
先看常规属性:
验证流程很简单:
1. 先创建好两个Foo实例, take snapshot
2. 执⾏删除操作,再takge snapshot
function Foo() { ate = (counts = 10000, prefix = "") => { this.arr = createObjects(counts, prefix);
}
} function createObjects(counts = 10000, prefix = "") { var arr = []; for (let i = 0; i < counts; i++) {
arr.push({ "p0": `${prefix}-value-${i}-0`, "p1": `${prefix}-value-${i}-1`, "p2": `${prefix}-value-${i}-2`
});
} return arr;
} var counts = 2; var foo1 = new Foo(); var foo2 = new Foo();
foo2.arr[i][`p${rd}`] = undefined;
} console.timeEnd("t");
})
看看执⾏前后的截图:
执⾏删除前:
执⾏删除后:
可以看出使⽤delete删除属性的对象的map发⽣了变化。
我们调整⼀下
function createObjects(counts = 10000, prefix = "") { var arr = []; for (let i = 0; i < counts; i++) {
苹果7配置arr.push({ 0: `${prefix}-value-${i}-0`, 1: `${prefix}-value-${i}-1`, 2: `${prefix}-value-${i}-2`
});
} return arr;
}
就只看删除操作后的截图吧:
方向盘怎么打执⾏删除后:
map没有变化。
借⽤
图解 Google V8 总结的⼀句话。
尽量避免使⽤ delete ⽅法。delete ⽅法会破坏对象的形状,同样会导致 V8 为该对象重新⽣成新的隐藏类。
我们接下来再测试⼀下属性多少对性能的影响:
1. ⼀万条数据3个常规属性
del: 7.614990234375 mst: 3.297119140625 msdel: 8.5048828125 mst: 3.344970703125 msdel: 7.107177734375 mst: 2.950927734375 ms
1. ⼀万条数据10个常规属性
del: 9.324951171875 mst: 3.31201171875 msdel: 9.4580078125 mst: 3.0908203125 msdel: 9.501953125 mst: 3.119873046875 ms
del: 9.324951171875 mst: 3.31201171875 msdel: 9.4580078125 mst: 3.0908203125 msdel: 9.501953125 mst: 3.119873046875 ms
1. ⼀万条数据25个常规属性
del: 15.0390625 mst: 5.799072265625 msdel: 16.137939453125 mst: 5.30615234375 msdel: 15.543701171875 mst: 5.489990234375 msdel: 20.700927734375 mst: 3.203125 ms
1. ⼀万条数据40个常规属性
del: 30.131103515625 mst: 4.299072265625 msdel: 26.7041015625 mst: 3.68701171875 msdel: 24.31005859375 mst: 4.10888671875 ms
可以看到属性越多,delete的消耗越⼤。
总结
从我们测试来看,使⽤排序属性执⾏delete,并未导致对象的隐藏类被改变。
⽽常规属性就没那么幸运了。所以使⽤delete来删除常规属性的代价是相对⽐较⼤的。
我们简单回顾⼀下:
1. delete 很多时候删不掉。
2. delete 返回true的时候,也不代表⼀定删除成功。⽐如原型上的属性。
3. delete 某些场景下会导致隐藏类改变,可能导致性能问题。
这⼏条,就⾜以让我们谨慎使⽤delete。
额外的
排序属性的结构也是会变化的。
我们⾸先贴⼀段代码出来:
1. 在Foo的实例上⾯有序的数字属性
2. ⼀顿猛如虎的瞎操作
3. 观察变化
function Foo() { ate = (counts = 10, prefix = "") => {
createPropertes.call(this, counts);
}
}function createPropertes(counts = 10) { for (let i = 0; i < counts; i++) { this[i] = `${i}-${Math.random()}`;
取精用宏
}
}var foo = new Foo();
actions(); console.log("actions", " done");
})function actions() {
foo[100000] = `${100000}-${Math.random()}`;
foo[100] = `${100}-${Math.random()}`; delete foo[9];
foo[2] = `2-${Math.random()}`;
}
还是看图,⽐较给⼒:
是不是惊喜的发现结构变化啦,那是不是for in的时候,顺序也会变化呢。
答案不是的,那他是怎么做到的呢?
答案:elements默认应该采⽤连续的存储结构,直接下标访问,提升访问速度。但当elements的序号⼗分不连续时或者操作过猛,会优化成为hash表。
参考引⽤
No operation for uninitialized values (spar arrays)
ECMA-262_5th_edition_december_2009.pdf
ECMA-262_3rd_edition_december_1999.pdf 58页
JavaScript中delete操作符不能删除的对象