淺拷貝與深拷貝詳解

淺拷貝(shallow clone)

淺拷貝是

  • 對 Object 只有一層深度的 複製
  • 假如對應的值是reference,原本的跟複製的會共用,會互相影響

常見的做法有

Spread Syntax

const original = { name: 'Alice', age: 30 }
const shallowCopy = { ...original }

// Modifying the copy doesn't change the original
shallowCopy.name = 'Bob'
console.log(original) // { name: "Alice", age: 30 }
console.log(shallowCopy) // { name: "Bob", age: 30 }

Object.assign

const original = { name: 'Alice', age: 30, address: { city: 'New York' } }
const shallowCopy = Object.assign({}, original)

// 有共用reference的值,修改後,原本的跟複製的會互相影響
shallowCopy.address.city = 'San Francisco'
console.log(original) // { name: "Alice", age: 30, address: { city: "San Francisco" } }
console.log(shallowCopy) // { name: "Alice", age: 30, address: { city: "San Francisco" } }

深拷貝(deep clone)

一般深拷貝有三種解法

  • JSON.parse(JSON.stringify(obj))
  • structuredClone(obj)
  • _.cloneDeep(obj) (第三方套件)

JSON.parse(JSON.stringify(obj))

這個其實就是先對 javascript 序列化(serialize),然後再用 JSON反序列化(deserialize)

JSON.stringify() 會把 value 轉成JSON notation

轉換行為可以參考MDN

value 有改變的,以下做個統整

  • undefinedfunction跟symbol
    • 會被忽略,變成空的
    • 但是在array中會變成null
  • InfinityNaNnull
    • 都被轉成 null
  • MapSetWeakMapWeakSetErrorregex
    • 都被轉成 {}
  • new Date
    • 轉成string
    • .toJSON()方法的,JSON.stringify()會執行.toJSON()
  • TypedArray類型
    • Int8Array、Int16Array、Int32Array、Uint8Array、Uint16Array、Uint32Array 等等
    • JSON.stringify(new Int8Array([1])) 都會變成'{"0":1}'

clone

還蠻多雷的其實

尤其是常見的functionundefined,直接消失

不熟的 debug,de 半天

structuredClone(obj)

structuredClone 前身是Structured cloning演算法

詳情看這篇文章

瀏覽器有三種 api 可以使用這個演算法

只是很不方便

  • MessageChannel
  • History API
  • Notification API

還好現在實現了原生的structuredClone

比較值得注意不支援的有四種

  • function
  • dom node
  • getter/setter
  • object prototype

詳細可以看MDN

structuredClone

getter/setter 在 在 vue 的情境下還蠻常碰到的

function 一樣是沒支援

Structured cloning 演算法可以在 html spec 中找到

_.cloneDeep(obj)

以下是 lodash 的 cloneDeep 的代碼

基本上就是在 runtime 時

逐個判斷型別,做相對應的複製

然後再 recursive 下去

有時間再回來補這邊源碼的講解

或是另開一篇,哈哈

// src/deepClone.ts

const CLONE_DEEP_FLAG = 1
const CLONE_SYMBOLS_FLAG = 4

function cloneDeep(value) {
    return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG)
}
// src/.internal/baseClone.ts

function baseClone(value, bitmask, customizer, key, object, stack) {
    let result
    const isDeep = bitmask & CLONE_DEEP_FLAG
    const isFlat = bitmask & CLONE_FLAT_FLAG
    const isFull = bitmask & CLONE_SYMBOLS_FLAG

    if (customizer) {
        result = object ? customizer(value, key, object, stack) : customizer(value)
    }
    if (result !== undefined) {
        return result
    }
    if (!isObject(value)) {
        return value
    }
    const isArr = Array.isArray(value)
    const tag = getTag(value)
    if (isArr) {
        result = initCloneArray(value)
        if (!isDeep) {
            return copyArray(value, result)
        }
    } else {
        const isFunc = typeof value === 'function'

        if (isBuffer(value)) {
            return cloneBuffer(value, isDeep)
        }
        if (tag === objectTag || tag === argsTag || (isFunc && !object)) {
            result = isFlat || isFunc ? {} : initCloneObject(value)
            if (!isDeep) {
                return isFlat
                    ? copySymbolsIn(value, copyObject(value, keysIn(value), result))
                    : copySymbols(value, Object.assign(result, value))
            }
        } else {
            if (isFunc || !cloneableTags[tag]) {
                return object ? value : {}
            }
            result = initCloneByTag(value, tag, isDeep)
        }
    }
    // Check for circular references and return its corresponding clone.
    stack || (stack = new Stack())
    const stacked = stack.get(value)
    if (stacked) {
        return stacked
    }
    stack.set(value, result)

    if (tag === mapTag) {
        value.forEach((subValue, key) => {
            result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
        })
        return result
    }

    if (tag === setTag) {
        value.forEach((subValue) => {
            result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
        })
        return result
    }

    if (isTypedArray(value)) {
        return result
    }

    const keysFunc = isFull ? (isFlat ? getAllKeysIn : getAllKeys) : isFlat ? keysIn : keys

    const props = isArr ? undefined : keysFunc(value)
    arrayEach(props || value, (subValue, key) => {
        if (props) {
            key = subValue
            subValue = value[key]
        }
        // Recursively populate clone (susceptible to call stack limits).
        assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
    })
    return result
}