3

JavaScript 物件複製方法比較

 2 years ago
source link: https://blog.darkthread.net/blog/js-object-clone/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

JavaScript 物件複製方法比較

2022-05-06 09:00 PM 0 1,269

複製物件是 JavaScript 的實用技巧之一,十幾前我就學會用 jQuery.extend() 搞定,常見應用是結合參數預設值及函式傳入的設定值,例如:

const defaults = {
    fontSize: '10pt',
    color: 'white',
    backgroundColor: 'red'
}

funtion showSomething(options) {
    const finalOptions = $.extended({}, defaults, options);
    //...略...
    something.style.fontSize = finalOptions.fontSize;
    something.style.color = finalOptions.color;
    something.style.backgroundColor = finalOptions.backgroundColor;
    //...略...
}

靠 jQuery.extend() 再戰十年也是沒問題的,不過最近剛解封,我對 JavaScript 好奇心正盛,花了點時間研究複製物件這件事。

開始前先說物件複製常要考慮的事:

  1. 除了複製屬性 (Property) 外,函式 (Function/Method) 也要複製嗎?
  2. Shallow Copy (淺層複製) 還是 Deep Copy (深層複製) ?
    當屬性型別為物件(Non-Primitive Type,可想成 C# 的 Reference Type)時,Shallow Copy 複製屬性會指向原物件、Deep Copy 則是為該物件產生副本,事後修改原物件不會對 Deep Copy 物件產生影響。

參考網路文章,我整理了幾種 JavaScript 複製物件方法:

  1. Spread 法 (Shallow Copy,會複製函式)
    寫成 { ...src },相當於將 src 屬性展開傳入,等於 { src.prop1, src.prop2, src.prop3,... },套用前篇文章提到的 Shorthand Property Name,巧妙地讓新物件具有跟原物件一樣的屬性與方法。
    Spread in object literals 語法,Safari 要 11.3 才支援,Deno 不支援,需留意瀏覽器支援性
  2. Object.assign 法 (Shallow Copy,會複製函式)
    寫成 Object.assign({}, src)
  3. JSON 大絕 (Deep Copy,不複製函式)
    JSON.parse(JSON.stringify(src)),要留意還原失真問題(例如:Date() 序列化再還原會變成字串 JSON.parse(JSON.stringify(new Date())) )
  4. jQuery.extend (Shallow Copy,會複製函式)
    寫成 $.extend({}, src),還可一次合併多個 $.extend({}, src1, src2)
  5. Lodash .clone() & .cloneDeep() (有 Shallow Copy 及 Deep Copy 兩種,會複製函式)
    寫成 _.clone(src)_.cloneDeep()

最後,用彙整範例結束這回合:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <style>
        body { display: flex; font-size: 9pt; }
        .tab { width: 320px;}
        table { border-collapse: collapse; border-spacing: 0; }
        td { padding: 3px 6px; border: 1px solid gray; }
        thead td, tbody td:first-child { text-align: left; background-color: #eee; }
        tbody td { text-align: center; }
    </style>
</head>

<body>
    <div class="tab">
        <table>
            <thead>
                <tr>
                    <td>Method</td>
                    <td>Deep/<br />Shallow</td>
                    <td>Functions<br />Included?</td>
                    <td>Date Type<br /> Lost?</td>
                </tr>
            </thead>
            <tbody></tbody>
        </table>
    </div>
    <pre></pre>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script>
        const instance = {
            status: "before"
        };
        const src = {
            i: 123,
            a: [1, 2, 3],
            s: 'String',
            d: new Date(2012, 11, 21),
            hi() { console.log('Hi'); },
            obj: { p: 'test' },
            ins: instance
        };
        const bySpread = { ...src };
        const byAssign = Object.assign({}, src);
        const byJson = JSON.parse(JSON.stringify(src));
        const byJQuery = $.extend({}, src);
        const byLodashShallow = _.clone(src);
        const byLodashDeep = _.cloneDeep(src);
        instance.status = 'after';
        src.hi();
        const res = [];
        const logs = [];
        const check = (o) => {
            const varName = Object.keys(o)[0];
            const obj = o[varName];
            res.push([
                varName,
                obj.ins.status == 'before' ? 'Deep' : 'Shallow',
                typeof (obj.hi) == 'function' ? 'Y' : 'N',
                obj.d instanceof Date ? 'N' : 'Y'
            ]);
            logs.push(`=== ${varName} ===`);
            Object.keys(obj).forEach(propName =>
                logs.push(` * ${propName}[${typeof (obj[propName])}] = ${JSON.stringify(obj[propName])}`));
        }
        check({ bySpread });
        check({ byAssign });
        check({ byJson });
        check({ byLodashShallow });
        check({ byLodashDeep });
        check({ byJQuery });
        $('pre').text(logs.join('\n'));
        $('tbody').html(
            res.map(a => `<tr>${a.map(e => `<td>${e}</td>`).join()}</tr>`).join('\n')
        );
    </script>
</body>

</html>

其中的 check() 有點意思,特別說一下。我用了一個小技巧去抓變數名稱,JavaScript 不像 C# 有 nameof(varName) 可將變數名稱轉成字串,但再次借用前幾天學到的 Shorthand Property Name 宣告物件 ,透過 Object.keys(o) 取屬性名稱便能得到 "varName",還蠻有趣的。而 check() 函式會進行以下檢查:

  1. 複製物件後,instance.status 從 'before' 被改為 'after',若為 Deep Copy,ins.status 應為 before;若為 after 即為 Shallow Copy。
  2. 用 typeof 檢查複製物件的 hi() 是否為 function 檢查函式有無被複製。
  3. 用 .d instanceof Date 檢查 d 屬性型別是否失真。

六種做法的處理特性如下表:

MethodDeep/ShallowFunctions?Date Type Lost?
bySpreadShallowYN
byAssignShallowYN
byJsonDeepNY
byLodashShallowShallowYN
byLodashDeepDeepYN
byJQueryShallowYN

附上完整測試結果:

【參考資料】

[2022-05-07 補充] PO 文後再獲新知(感謝莊志弘與張清忠兩位先進不約而同分享),Chrome 98+ 新增 structuredClone() API,主要用於資料傳輸物件之複製,具有一些特異功能如:能處理循環參照、複製後讓原物件無法使用... 等。但其能複製的型別有限,若包含不支援型別(例如函式)會出錯,例如:Failed to execute 'structuredClone' on 'Window': hi() { console.log('Hi'); } could not be cloned.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK