題外話
這里大家可能要笑了,這不就一個操作符嗎,還用單獨來講。
有這時間,還不如去看看react源碼,vue源碼。
我說:react源碼會去看的,但是這個也很重要。
delete你了解多少
這里提幾個問題
- delete的返回值是什么
- delete刪除不存在的屬性返回值是什么
- 能不能刪除原型上的屬性
- 能否刪除變量
- 刪除數組某個數據,數組長度會不會變
- 哪些屬性不能被刪除
我們就挨個來驗證一下
1. delete的返回值是什么
var a = {
p1: 1
}
console.log(delete a.p1); // true
console.log(delete a.p2); // true
console.log(delete window); // false
從上面可以看出delete返回的是布爾值,如果刪除成功返回真,這里包括刪除一個不存在的屬性。 刪除失敗返回false。
2. delete刪除不存在的屬性返回值是什么
從第一個demo看出,刪除不存在的屬性返回值也是true
3. 能不能刪除原型上的屬性
var a = {
p1: 10
}
a.__proto__ = {
p2: 20
}
console.log("a.p2:before", a.p2); // 20
console.log(delete a.p2); // true
console.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); // 20
console.log(delete foo.age); // true
console.log("foo.p2:after", foo.age); // 20
我都說了,不要在乎哪個寫法,結果你就是不信,結果還是一樣的。
你沒法刪除原型上的屬性。
4. 能否刪除變量
var a = 10;
console.log(delete a); // false
console.log("a", a); // 10
顯然,是刪除不掉,你換成函數,結果也是一樣的。
5. 刪除數組某個數據,數組長度會不會變
var arr = [10,2,16];
console.log("length:before", arr.length); // 3
console.log("delete", delete arr[1]); // true
console.log("length:after",arr.length); // 3
console.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 ,10
for(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
並不會對未初始化的值進行任何操作。
具體可以參見
6. 哪些屬性不能被刪除
- var const let等變量 , 全局變量。
delete window // false
var a;
delete a; // false
// 有意思的delete this
function a (){
this.a = 333;
console.log("delete this:" , delete this); // true
console.log("a", this.a, this); // 333, {a:333}
}
a.call({});
- 數據屬性configurable為false的屬性
ES5嚴格模式中delete configuable為false的對象時會直接拋異常
// 內置document, location等
Object.getOwnPropertyDescriptor(window, "document");// { configurable: false }
console.log("delete", delete window.document); // false
console.log("delete", delete window.location); // false
// 數組長度
var arr = [];
Object.getOwnPropertyDescriptor(arr, "length");// { configurable: false }
console.log("delete", delete arr.length); // false
// 函數長度
function fn(){};
Object.getOwnPropertyDescriptor(fn, "length");// { configurable: false }
console.log("delete", delete fn.length); // false
// 各種內置原型
Object.getOwnPropertyDescriptor(Object, "prototype") // { configurable: false }
console.log("delete", delete Object.prototype); // false
// 內置Math的函數
Object.getOwnPropertyDescriptor(Math, "PI") // { configurable: false }
console.log("delete", delete Math.PI); // false
// https://www.cnblogs.com/snandy/archive/2013/03/06/2944815.html
// 有提到正則對象的屬性(source、global、ignoreCase、multiline、lastIndex)delete 返回 false
// 實際測試結果,刪除返回true,但是沒刪除掉
var reg = /.*/;
Object.getOwnPropertyDescriptor(reg, "source") // undefined
console.log("delete", delete reg.source); // true
console.log("reg.source", reg.source); // .*
console.log("reg prototype source", reg.__proto__source); // "(?:)
delete reg.lastIndex // false
delete reg.global // true
delete reg.ignoreCase // true
delete reg.multiline // true
- 原型上的屬性
- 函數參數
function delP(){
console.log("delete", delete arguments); // false
console.log("arguments", arguments); // 0: 1
}
delP(1);
- 一些常量(NaN、Infinity、undefined)
delete NaN; // false
delete Infinity; // false
delete undefined; // false
- 函數聲明
function fn() {}
delete fn;
console.log(fn.toString()); // function fn() {}
更多細節
ECMA-262_5th_edition_december_2009.pdf
ECMA-262_3rd_edition_december_1999.pdf 58頁
JavaScript中delete操作符不能刪除的對象
我們可以看一下ES3的定義, ES5的定義是有變動的。
The delete Operator
The production UnaryExpression : delete UnaryExpression is evaluated as follows:
1. Evaluate UnaryExpression.
2. If Type(Result(1)) is not Reference, return true.
3. Call GetBase(Result(1)).
4. Call GetPropertyName(Result(1)).
5. Call the [[Delete]] method on Result(3), providing Result(4) as the property name to delete.
6. Return Result(5).
我簡單翻譯一下,可能不太正確哈:
- 執行一元表達式
- 如果第一步返回的未被引用,返回真
console.log(delete xxxxxxxxxx) //true
console.log(delete "a") // true
console.log(delete {a:1}) // true
console.log(delete 1) // true
- 取到對象
- 取屬性名
- 用第四步獲得的屬性名,在第三步返回的結果上進行刪除操作
- 返回第五步返回的結果。
這里的Resuslt(1)本身應該不是數據本身,類似一個引用地址吧。
小結一下
- delete 返回false, 一定是沒刪除成功
- 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}-6`,
"p7": `value-${i}-7`,
"p8": `value-${i}-8`,
"p9": `value-${i}-9`,
"p10": `value-${i}-10`,
"p11": `value-${i}-10`,
"p12": `value-${i}-10`,
"p13": `value-${i}-10`,
"p14": `value-${i}-10`,
"p15": `value-${i}-10`,
"p16": `value-${i}-10`,
"p17": `value-${i}-10`,
"p18": `value-${i}-10`,
"p19": `value-${i}-10`,
"p20": `value-${i}-10`,
"p21": `value-${i}-10`,
"p22": `value-${i}-10`,
"p23": `value-${i}-10`,
"p24": `value-${i}-10`
});
}
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("set");
for (let i = 0; i < arr2.length; i++) {
const rd = i % 25;
arr2[i][`p${rd}`] = undefined;
}
console.timeEnd("set");
// del: 31.68994140625 ms
// set: 6.875 ms
// del: 24.43310546875 ms
// set: 3.7861328125 ms
// del: 79.622802734375 ms
// set: 3.876953125 ms
// del: 53.015869140625 ms
// set: 3.242919921875 ms
// del: 18.84619140625 ms
// set: 3.645751953125 ms
我們記錄了大約五次執行事件對比。
可以看出來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`,
8: `value-${i}-8`,
9: `value-${i}-9`,
10: `value-${i}-10`,
11: `value-${i}-10`,
12: `value-${i}-10`,
13: `value-${i}-10`,
14: `value-${i}-10`,
15: `value-${i}-10`,
16: `value-${i}-10`,
17: `value-${i}-10`,
18: `value-${i}-10`,
19: `value-${i}-10`,
20: `value-${i}-10`,
21: `value-${i}-10`,
22: `value-${i}-10`,
23: `value-${i}-10`,
24: `value-${i}-10`
});
}
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("set");
for (let i = 0; i < arr2.length; i++) {
const rd = i % 25;
arr2[i][rd] = undefined;
}
console.timeEnd("set");
// del: 1.44189453125 ms
// set: 2.43212890625 ms
// del: 1.737060546875 ms
// set: 3.10400390625 ms
// del: 1.281005859375 ms
// set: 2.85107421875 ms
// del: 1.338134765625 ms
// set: 1.877197265625 ms
// del: 1.3203125 ms
// set: 2.09912109375 ms
到這里,畫風一轉。 del居然比set還快了。。。。。。
而set的速度實際基本沒有什么變化。
常規屬性 (properties) 和排序屬性 (element)
這里就要提出幾個概念:
常規屬性 (properties) 和排序屬性 (element)。
上面的代碼變化不多,就是屬性名稱從p0
格式修改為了0
格式。
p0
正式常規屬性,0
是排序屬性。
在 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 要為新改變的對象重新構建新的隱藏類,這對於 V8 的執行效率來說,是一筆大的開銷。
看到紅色部分,你就應該差不多得到答案了。
那如何查看隱藏類呢?
使用chrome的開發者工具,Memory模塊的 Heap snapshot功能:
。
然后再搜索對應的構造函數,比如Foo
這里為了方便查找,我們簡單包裝一下代碼:
先看常規屬性:
驗證流程很簡單:
- 先創建好兩個Foo實例, take snapshot
- 執行刪除操作,再takge snapshot
function Foo() {
this.create = (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();
foo1.create(counts, "del");
foo2.create(counts, "set");
var propertiesCount = 3;
document.getElementById("btnDelete").addEventListener("click", () => {
console.time("del");
for (let i = 0; i < foo1.arr.length; i++) {
const rd = i % propertiesCount;
delete foo1.arr[i][`p${rd}`];
}
console.timeEnd("del");
console.time("set");
for (let i = 0; i < foo2.arr.length; i++) {
const rd = i % propertiesCount;
foo2.arr[i][`p${rd}`] = undefined;
}
console.timeEnd("set");
})
看看執行前后的截圖:
執行刪除前:
執行刪除后:
可以看出使用delete刪除屬性的對象的map發生了變化。
我們調整一下
function createObjects(counts = 10000, prefix = "") {
var arr = [];
for (let i = 0; i < counts; i++) {
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 為該對象重新生成新的隱藏類。
我們接下來再測試一下屬性多少對性能的影響:
- 一萬條數據3個常規屬性
del: 7.614990234375 ms
set: 3.297119140625 ms
del: 8.5048828125 ms
set: 3.344970703125 ms
del: 7.107177734375 ms
set: 2.950927734375 ms
- 一萬條數據10個常規屬性
del: 9.324951171875 ms
set: 3.31201171875 ms
del: 9.4580078125 ms
set: 3.0908203125 ms
del: 9.501953125 ms
set: 3.119873046875 ms
- 一萬條數據25個常規屬性
del: 15.0390625 ms
set: 5.799072265625 ms
del: 16.137939453125 ms
set: 5.30615234375 ms
del: 15.543701171875 ms
set: 5.489990234375 ms
del: 20.700927734375 ms
set: 3.203125 ms
- 一萬條數據40個常規屬性
del: 30.131103515625 ms
set: 4.299072265625 ms
del: 26.7041015625 ms
set: 3.68701171875 ms
del: 24.31005859375 ms
set: 4.10888671875 ms
可以看到屬性越多,delete的消耗越大。
總結
從我們測試來看,使用排序屬性執行delete,並未導致對象的隱藏類被改變。
而常規屬性就沒那么幸運了。 所以使用delete來刪除常規屬性的代價是相對比較大的。
我們簡單回顧一下:
- delete 很多時候刪不掉。
- delete 返回true的時候,也不代表一定刪除成功。 比如原型上的屬性。
- delete 某些場景下會導致隱藏類改變,可能導致性能問題。
這幾條,就足以讓我們謹慎使用delete。
額外的
排序屬性的結構也是會變化的。
我們首先貼一段代碼出來:
- 在Foo的實例上面有序的數字屬性
- 一頓猛如虎的瞎操作
- 觀察變化
function Foo() {
this.create = (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();
foo.create();
document.getElementById("btnDelete").addEventListener("click", () => {
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 (sparse arrays)
ECMA-262_5th_edition_december_2009.pdf
ECMA-262_3rd_edition_december_1999.pdf 58頁
JavaScript中delete操作符不能刪除的對象