Why系列:謹慎使用delete


題外話

這里大家可能要笑了,這不就一個操作符嗎,還用單獨來講。
有這時間,還不如去看看react源碼,vue源碼。
我說:react源碼會去看的,但是這個也很重要。

delete你了解多少

這里提幾個問題

  1. delete的返回值是什么
  2. delete刪除不存在的屬性返回值是什么
  3. 能不能刪除原型上的屬性
  4. 能否刪除變量
  5. 刪除數組某個數據,數組長度會不會變
  6. 哪些屬性不能被刪除

我們就挨個來驗證一下

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
}

forEachin 並不會對未初始化的值進行任何操作。
具體可以參見

No operation for uninitialized values (sparse arrays)

6. 哪些屬性不能被刪除

  1. 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({});

  1. 數據屬性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

  1. 原型上的屬性
  2. 函數參數
function delP(){
    console.log("delete", delete arguments);  // false
    console.log("arguments", arguments);  //  0: 1
}

delP(1);

  1. 一些常量(NaN、Infinity、undefined)
delete NaN; // false
delete Infinity; // false
delete undefined; // false
  1. 函數聲明
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).

我簡單翻譯一下,可能不太正確哈:

  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 返回false, 一定是沒刪除成功
  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}-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是排序屬性。

對象中的數字屬性稱為排序屬性,在 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 要為新改變的對象重新構建新的隱藏類,這對於 V8 的執行效率來說,是一筆大的開銷。

看到紅色部分,你就應該差不多得到答案了。

那如何查看隱藏類呢?
使用chrome的開發者工具,Memory模塊的 Heap snapshot功能:

然后再搜索對應的構造函數,比如Foo

這里為了方便查找,我們簡單包裝一下代碼:
先看常規屬性:

驗證流程很簡單:

  1. 先創建好兩個Foo實例, take snapshot
  2. 執行刪除操作,再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 為該對象重新生成新的隱藏類。

我們接下來再測試一下屬性多少對性能的影響:

  1. 一萬條數據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
  1. 一萬條數據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
  1. 一萬條數據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
  1. 一萬條數據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來刪除常規屬性的代價是相對比較大的。

我們簡單回顧一下:

  1. delete 很多時候刪不掉。
  2. delete 返回true的時候,也不代表一定刪除成功。 比如原型上的屬性。
  3. delete 某些場景下會導致隱藏類改變,可能導致性能問題。

這幾條,就足以讓我們謹慎使用delete。

額外的

排序屬性的結構也是會變化的。
我們首先貼一段代碼出來:

  1. 在Foo的實例上面有序的數字屬性
  2. 一頓猛如虎的瞎操作
  3. 觀察變化

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操作符不能刪除的對象


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM