跳转到内容

深拷贝

本文将从最基础的深拷贝方法到更复杂的方法,深入讲解深拷贝的原理,以及各方法之间的差异。

JSON.parse(JSON.stringify())

在不使用第三方库的情况下,想要深拷贝一个对象,一般来讲最简单的用的最多的就是 JSON.parse(JSON.stringify(obj)),其过程说白了就是利用 JSON.stringify 将 JS 对象序列化(JSON字符串),再使用 JSON.parse 来反序列化(还原) JS 对象。

js
JSON.parse(JSON.stringify(obj));

这种写法非常简单,而且可以应对大部分的应用场景,但注意 JSON 只能用来序列化对象、数组、数值、字符串、布尔值和 null,依靠 JSON 深拷贝时存在很大缺陷,原因在于 JSON.stringify() 在序列化时会有以下问题:

1、时间对象序列化后会变成字符串;

js
const target = {
    name: 'Jack',
    date: [new Date(1536627600000), new Date(1540047600000)]
};
JSON.parse(JSON.stringify(target));

image

Date 日期调用了 toJSON() 将其转换为了 string 字符串(同 Date.toISOString()),因此会被当做字符串处理。

js
JSON.stringify(new Date(1536627600000));
// '"2018-09-11T01:00:00.000Z"'

2、RegExp、Error 对象序列化后将只得到空对象;

js
const target = {
    re: new RegExp("\\w+"),
    err: new Error('"x" is not defined')
};
JSON.stringify(target);
// '{"re":{},"err":{}}'

image

3、任意的函数、undefined 以及 symbol 值,在序列化过程中会被忽略;

js
const target = {
    func: function () {
        console.log(1)
    },
    val: undefined,
    sym: Symbol('foo')
};
JSON.stringify(target);
// '{}'

image

4、NaN 和 Infinity 格式的数值都会被当做 null;

  • 1.7976931348623157E+10308 是浮点数的最大上限 显示为 Infinity
  • -1.7976931348623157E+10308 是浮点数的最小下限 显示为 -Infinity

image

const target = {
    nan: NaN,
    infinityMax: 1.7976931348623157E+10308,
    infinityMin: -1.7976931348623157E+10308,
};
JSON.stringify(target);
// '{"nan":null,"infinityMax":null,"infinityMin":null}'

image

5、对包含循环引用的对象(对象之间相互引用,形成无限循环)序列化,会抛出错误。

js
var circularReference = { otherData: 123 };
circularReference.myself = circularReference;
JSON.stringify(circularReference);
// TypeError: cyclic object value(Firefox) 或 Uncaught TypeError: Converting circular structure to JSON(Chrome and Opera)

image

在 JSON 中出现循环引用时,JavaScript 会抛出 "cyclic object value" 的异常。JSON.stringify() 并不会尝试解决这个问题,因此导致运行失败。

  • 提示信息:
  • TypeError: cyclic object value (Firefox)
  • TypeError: Converting circular structure to JSON (Chrome and Opera)
  • TypeError: Circular reference in value argument not supported (Edge)

递归

通过递归实现深拷贝:

js
function deepClone(obj) { // 递归拷贝
    if (typeof obj !== 'object' || obj === null) return obj; // 如果不是复杂数据类型 或者为null,直接返回
    if (obj instanceof RegExp) return new RegExp(obj);
    if (obj instanceof Date) return new Date(obj);
    let cloneObj = Array.isArray(obj) ? [] : {};
    for (let key in obj) {
        // 判断是否是对象自身的属性,筛掉对象原型链上继承的属性
        if (obj.hasOwnProperty(key)) {
            // 如果 obj[key] 是复杂数据类型,递归
            cloneObj[key] = deepClone(obj[key]);
        }
    }
    return cloneObj;
}

我们应该拷贝要拷贝对象自身的属性,对象原型上的属性我们不应该拷贝,这里我们用到 hasOwnProperty() 方法来解决。

hasOwnProperty() 方法会返回一个布尔值,这个方法可以用来检测一个对象是否含有特定的自身属性;该方法会忽略掉那些从原型链上继承到的属性。

循环引用

循环引用会使递归进入死循环导致栈内存溢出。

我们拷贝一下前面循环引用的例子:

js
var circularReference = { otherData: 123 };
circularReference.myself = circularReference;
deepClone(circularReference);
// Uncaught RangeError: Maximum call stack size exceeded 超出最大调用堆栈大小

解决循环引用问题,可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

这个存储空间,需要可以存储 key-value 形式的数据,且 key 可以是一个引用类型,我们可以选择 Map 这种数据结构:

  • 检查 map 中有无克隆过的对象
  • 有 - 直接返回
  • 没有 - 将当前对象作为 key,克隆对象作为 value 进行存储
  • 继续克隆
js
function deepClone(obj, map = new Map()) { // 递归拷贝
    if (typeof obj !== 'object' || obj === null) return obj; // 如果不是复杂数据类型 或者为null,直接返回
    if (obj instanceof RegExp) return new RegExp(obj);
    if (obj instanceof Date) return new Date(obj);
    if (map.has(obj)) return map.get(obj);
    let cloneObj = Array.isArray(obj) ? [] : {};
    map.set(obj, cloneObj);
    for (let key in obj) {
        // 判断是否是对象自身的属性,筛掉对象原型链上继承的属性
        if (obj.hasOwnProperty(key)) {
            // 如果 obj[key] 是复杂数据类型,递归
            cloneObj[key] = deepClone(obj[key], map);
        }
    }
    return cloneObj;
}

再次执行前面的用例可以发现没有报错,循环引用的问题解决了。

使用 WeakMap 优化

下面我们用 WeakMap 替代 Map 来优化深拷贝的实现。

如下:

js
function deepClone(obj, map = new WeakMap()) {
    // ...
};

为什么要这样做呢?先来看看 WeakMap 的作用:

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

什么是弱引用呢?

在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。

我们默认创建一个对象:const obj = {},就默认创建了一个强引用的对象,我们只有手动将 obj = null,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。

举个例子:

如果我们使用 Map 的话,那么对象间是存在强引用关系的:

js
let obj = { name : 'Jack'}
const target = new Map();
target.set(obj,'person');
obj = null;

虽然我们手动将 obj,进行释放,然是 target 依然对 obj 存在强引用关系,所以这部分内存依然无法被释放。

再来看 WeakMap

js
let obj = { name : 'Jack'}
const target = new WeakMap();
target.set(obj,'person');
obj = null;

如果是 WeakMap 的话,targetobj 存在的就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。

设想一下,如果我们要拷贝的对象非常庞大时,使用 Map 会对内存造成非常大的额外消耗,而且我们需要手动清除 Map 的属性才能释放这块内存,而 WeakMap 会帮我们巧妙化解这个问题。

我也经常在某些代码中看到有人使用 WeakMap 来解决循环引用问题,但是解释都是模棱两可的,当你不太了解 WeakMap 的真正作用时。我建议你也不要在面试中写这样的代码,结果只能是给自己挖坑,即使是准备面试,你写的每一行代码也都是需要经过深思熟虑并且非常明白的。

能考虑到循环引用的问题,你已经向面试官展示了你考虑问题的全面性,如果还能用 WeakMap 解决问题,并很明确的向面试官解释这样做的目的,那么你的代码在面试官眼里应该算是合格了。

structuredClone()

Window接口的 structuredClone() 方法使用结构化克隆算法将给定的值进行深拷贝。

该方法还支持把原值中的可转移对象转移(而不是拷贝)到新对象上。可转移对象与原始对象分离并附加到新对象;它们将无法在原始对象中被访问。

结构化克隆算法

结构化克隆算法用于复制复杂 JavaScript 对象的算法。通过来自 Worker的 postMessage() 或使用 IndexedDB 存储对象时在内部使用。它通过递归输入对象来构建克隆,同时保持先前访问过的引用的映射,以避免无限遍历循环。

结构化克隆所不能做到的

  • Function对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出 DATA_CLONE_ERR 的异常。
  • 企图去克隆 DOM 节点同样会抛出 DATA_CLONE_ERR 异常。
  • 对象的某些特定参数也不会被保留
    • RegExp 对象的 lastIndex 字段不会被保留
    • 属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write,因为这是默认的情况下。
    • 原形链上的属性也不会被追踪以及复制。

支持的类型

JavaScript 类型
  • Array
  • ArrayBuffer
  • DataView
  • Date
  • Error类型(仅限部分 Error 类型))。
  • Map
  • Object对象:仅限简单对象(如使用对象字面量创建的)。
  • symbol 以外的基本类型。
  • RegExplastIndex 字段不会被保留。
  • Set
  • String
  • TypedArray

不支持的类型

函数
  • 函数包含执行环境(闭包上下文),这些环境无法序列化。
  • 克隆后无法恢复原始作用域链。
  • 函数可能引用外部变量,而这些引用在新环境中无法还原。
DOM元素
  • DOM 元素依赖浏览器的渲染上下文。
  • 元素包含事件监听器、样式、状态、节点树等复杂结构,不能用普通 JS 表达。
  • DOM 是浏览器内部的宿主对象,不是纯 JS 对象。
Proxy
  • Proxy 本质是动态行为的代理,它不是数据,而是一种 行为包装器
  • 内部的 handler(代理对象)包含不可见的行为逻辑,structuredClone 无法复原。
  • 克隆后即使强行复制,行为可能完全不同。
Error 子类
  • Error 对象包含 stack trace(堆栈信息)、message 等与运行时紧密绑定的内容。
  • 虽然有的浏览器支持部分 Error 克隆,但并不是规范强制要求。

实际上,所有深拷贝都不能完美解决以上类型。

Lodash.cloneDeep()

Lodash 实现了函数、正则、Date、Buffer、Map、Set、原型链等情况下的深拷贝。

入口

入口文件是 cloneDeep.js,直接调用核心文件 baseClone.js 的方法。

baseClone 关联了多种 clone 方法,如 clonecloneWith, cloneDeepcloneDeepWith

js
const CLONE_DEEP_FLAG = 1
const CLONE_SYMBOLS_FLAG = 4

function cloneDeep(value) {
    return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG)
}

第一个参数是需要拷贝的对象,第二个是位掩码(Bitwise),关于位掩码的详细介绍请看下面拓展部分。

baseClone 方法

然后我们进入 baseClone.js 查看具体方法,主要实现逻辑都在这个方法里。

先介绍下该方法的参数 baseClone(value, bitmask, customizer, key, object, stack)

  • 初始化需要的数据:

    value:字符串,数字,对象,数组,函数

    bitmask:位掩码,其中 1 是深拷贝,2 拷贝原型链上的属性,4 是拷贝 Symbols 属性

    customizer:定制的 clone 函数 (这个是给 cloneWith 函数用的,与本文无关)

  • 递归中需要的数据:

    value:字符串,数字,对象,数组,函数

    key:字符串,数字或 Symbol

    object :父对象

    stack:Stack 栈,用来处理循环引用

    customizer:定制的 clone 函数

我将分成以下几部分进行讲解,可以选择自己感兴趣的部分阅读。

  • 位掩码
  • 定制 clone 函数
  • 基本数据类型
  • 数组 & 正则
  • 对象 & 函数
  • 循环引用
  • Map & Set
  • Symbol & 原型链

位掩码

上面简单介绍了位掩码,参数定义如下。

js
// 主线代码
const CLONE_DEEP_FLAG = 1		// 1 即 0001,深拷贝标志位
const CLONE_FLAT_FLAG = 2		// 2 即 0010,拷贝原型链标志位,
const CLONE_SYMBOLS_FLAG = 4	// 4 即 0100,拷贝 Symbols 标志位

位掩码用于处理同时存在多个布尔选项的情况,其中掩码中的每个选项的值都等于 2 的幂。相比直接使用变量来说,优点是可以节省内存

人话:原本是用布尔值(true/false)存储状态的,现在用二进制来代替了

js
// 位掩码方式
flags = 0b0101; // 表示第0位和第2位为true

// 布尔方式
flag1 = true;
flag2 = false;
flag3 = true;

这就是为什么没有3,因为3是011,有两位1,再后续每位也是 2 的幂,如8为1000

js
// cloneDeep.js 添加标志位,1 | 4 即 0001 | 0100 即 0101 即 5
CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG

// 主线代码
// baseClone.js 取出标志位
let result // 初始化返回结果,后续代码需要,和位掩码无关
const isDeep = bitmask & CLONE_DEEP_FLAG 	// 5 & 1 即 1 即 true
const isFlat = bitmask & CLONE_FLAT_FLAG	// 5 & 2 即 0 即 false
const isFull = bitmask & CLONE_SYMBOLS_FLAG // 5 & 4 即 4 即 true

定制 clone 函数

用于 cloneWith 函数

js
// 主线代码
// 判断是否传入customizer函数
if (customizer) {
    result = object ? customizer(value, key, object, stack) : customizer(value);
}
if (result !== undefined) {
    return result;
}

基本数据类型

非对象的就是基本数据类型,判断要拷贝的值是否是对象,非对象直接返回本来的值

function 也是对象

js
// 主线代码
if (!isObject(value)) {
    return value;
}

function isObject(value) {
    const type = typeof value;
    return value != null && (type == 'object' || type ='function');// 非空 函数 对象
}

判断完基础数据类型后就只剩下数组和对象了

数组 & 正则

js
// 主线代码
const isArr = Array.isArray(value)
const hasOwnProperty = Object.prototype.hasOwnProperty

// 判断数组
if (isArr) {
    result = initCloneArray(value)
    if (!isDeep) {
		return copyArray(value, result)
	}
} else {
    // 非数组,后面解析
}

// 初始化一个数组
function initCloneArray(array) {
  	const { length } = array
    // 构造相同长度的新数组
  	const result = new array.constructor(length)

  	// 正则 `RegExp#exec` 返回的数组
  	if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {
   	 	result.index = array.index
    	result.input = array.input
  	}
  	return result
}

// ... 未完待续,最后部分有数组遍历赋值

传入的对象是数组时,构造一个相同长度的数组 new array.constructor(length),这里相当于 new Array(length),因为 array.constructor === Array

js
var a = [];
a.constructor === Array; // true

var a = new Array;
a.constructor === Array // true

如果存在正则 RegExp#exec 返回的数组,拷贝属性 indexinput。判断逻辑是 1、数组长度大于 0,2、数组第一个元素是字符串类型,3、数组存在 index 属性。

js
if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {
    result.index = array.index
    result.input = array.input
}

其中正则表达式 regexObj.exec(str) 匹配成功时,返回一个数组,并更新正则表达式对象的属性。返回的数组将完全匹配成功的文本作为第一项,将正则括号里匹配成功的作为数组填充到后面。匹配失败时返回 null

js
var re = /quick\s(brown).+?(jumps)/ig;
var result = re.exec('The Quick Brown Fox Jumps Over The Lazy Dog');
console.log(result);
// [
//	0: "Quick Brown Fox Jumps" 	// 匹配的全部字符串
//	1: "Brown"					// 括号中的分组捕获
//	2: "Jumps"
//	groups: undefined
//	index: 4					// 匹配到的字符位于原始字符串的基于0的索引值
//	input: "The Quick Brown Fox Jumps Over The Lazy Dog" // 原始字符串
//	length: 3
// ]

如果不是深拷贝,传入valueresult,直接返回浅拷贝后的数组。这里的浅拷贝方式就是循环然后复制。

js
if (!isDeep) {
	return copyArray(value, result)
}

// 浅拷贝数组
function copyArray(source, array) {
  let index = -1
  const length = source.length
  array || (array = new Array(length))
  while (++index < length) {
    array[index] = source[index]
  }
  return array
}

对象&函数

js
// 主线代码
const isArr = Array.isArray(value)
const tag = getTag(value)
if (isArr) {
    ... // 数组情况,详见上面解析
} else {
    // 函数
    const isFunc = typeof value == 'function'

    // 如果是 Buffer 对象,拷贝并返回
    if (isBuffer(value)) {
        return cloneBuffer(value, isDeep)
    }
    
    // Object 对象、类数组、或者是函数但没有父对象
    if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
        // 拷贝原型链或者 value 是函数时,返回 {},不然初始化对象
        result = (isFlat || isFunc) ? {} : initCloneObject(value)
        if (!isDeep) {
            return isFlat
                ? copySymbolsIn(value, copyObject(value, keysIn(value), result))
            	: copySymbols(value, Object.assign(result, value))
        }
    } else {
        // 在 cloneableTags 中,只有 error 和 weakmap 返回 false
        // 函数或者 error 或者 weakmap 时,
        if (isFunc || !cloneableTags[tag]) {
            // 存在父对象返回value,不然返回空对象 {}
            return object ? value : {}
        }
        // 初始化非常规类型
        result = initCloneByTag(value, tag, isDeep)
    }
}

通过上面代码可以发现,函数、errorweakmap 时返回空对象 {},并不会真正拷贝函数。

value 类型是 Object 对象和类数组时,调用 initCloneObject 初始化对象,最终调用 Object.create 生成新对象。

js
function initCloneObject(object) {
    // 构造函数并且自己不在自己的原型链上
    return (typeof object.constructor == 'function' && !isPrototype(object))
        ? Object.create(Object.getPrototypeOf(object))
    	: {}
}

// 本质上实现了一个instanceof,用来测试自己是否在自己的原型链上
function isPrototype(value) {
    const Ctor = value && value.constructor
    // 寻找对应原型
    const proto = (typeof Ctor == 'function' && Ctor.prototype) || Object.prototype
    return value === proto
}

其中 Object 的构造函数是一个函数对象。

js
var obj = new Object();
typeof obj.constructor; 
// 'function'

var obj2 = {};
typeof obj2.constructor;
// 'function'

对于非常规类型对象,通过各自类型分别进行初始化。

js
function initCloneByTag(object, tag, isDeep) {
    const Ctor = object.constructor
    switch (tag) {
        case arrayBufferTag:
            return cloneArrayBuffer(object)

        case boolTag: // 布尔与时间类型
        case dateTag:
            return new Ctor(+object) // + 转换为数字

        case dataViewTag:
            return cloneDataView(object, isDeep)

        case float32Tag: case float64Tag:
        case int8Tag: case int16Tag: case int32Tag:
        case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag:
            return cloneTypedArray(object, isDeep)

        case mapTag: // Map 类型
            return new Ctor

        case numberTag: // 数字和字符串类型
        case stringTag:
            return new Ctor(object)

        case regexpTag: // 正则
            return cloneRegExp(object)

        case setTag: // Set 类型
            return new Ctor

        case symbolTag: // Symbol 类型
            return cloneSymbol(object)
    }
}

拷贝正则类型

js
// \w 用于匹配字母,数字或下划线字符,相当于[A-Za-z0-9_]
const reFlags = /\w*$/
function cloneRegExp(regexp) {
    // 返回当前匹配的文本
    const result = new regexp.constructor(regexp.source, reFlags.exec(regexp))
    // 下一次匹配的起始索引
    result.lastIndex = regexp.lastIndex
    return result
}

初始化 Symbol 类型

js
const symbolValueOf = Symbol.prototype.valueOf
function cloneSymbol(symbol) {
    return Object(symbolValueOf.call(symbol))
}

循环引用

构造了一个栈用来解决循环引用的问题。

js
// 主线代码
stack || (stack = new Stack)
const stacked = stack.get(value)
// 已存在
if (stacked) {
    return stacked
}
stack.set(value, result)

如果当前需要拷贝的值已存在于栈中,说明有环,直接返回即可。栈中没有该值时保存到栈中,传入 valueresult。这里的 result 是一个对象引用,后续对 result 的修改也会反应到栈中。

Map & Set

value 值是 Map 类型时,遍历 value 并递归其 subValue,遍历完成返回 result 结果。

js
// 主线代码
if (tag == mapTag) {
    value.forEach((subValue, key) => {
        result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
    })
    return result
}

value 值是 Set 类型时,遍历 value 并递归其 subValue,遍历完成返回 result 结果。

js
// 主线代码
if (tag == setTag) {
    value.forEach((subValue) => {
        result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
    })
    return result
}

上面的区别在于添加元素的 API 不同,即 Map.setSet.add

Symbol & 原型链

这里我们介绍下 Symbol 和 原型链属性的拷贝,通过标志位 isFullisFlat 来控制是否拷贝。

js
// 主线代码
// 类型化数组对象
if (isTypedArray(value)) {
    return result
}

const keysFunc = isFull // 拷贝 Symbol 标志位
	? (isFlat 			// 拷贝原型链属性标志位
       ? getAllKeysIn 	// 包含自身和原型链上可枚举属性名以及 Symbol
       : getAllKeys)	// 仅包含自身可枚举属性名以及 Symbol
	: (isFlat 
       ? keysIn 		// 包含自身和原型链上可枚举属性名的数组
       : keys)			// 仅包含自身可枚举属性名的数组

const props = isArr ? undefined : keysFunc(value)
arrayEach(props || value, (subValue, key) => {
    if (props) {
        key = subValue
        subValue = value[key]
    }
    // 递归拷贝(易受调用堆栈限制)
    assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
})
return result

我们先来看下怎么获取自身、原型链、Symbol 这几种属性名组成的数组 keys

js
// 创建一个包含自身和原型链上可枚举属性名以及 Symbol 的数组
// 使用 for...in 遍历
function getAllKeysIn(object) {
    const result = keysIn(object)
    if (!Array.isArray(object)) {
        result.push(...getSymbolsIn(object))
    }
    return result
}

// 创建一个仅包含自身可枚举属性名以及 Symbol 的数组
// 非 ArrayLike 数组使用 Object.keys
function getAllKeys(object) {
    const result = keys(object)
    if (!Array.isArray(object)) {
        result.push(...getSymbols(object))
    }
    return result
}

上面通过 keysInkeys 获取常规可枚举属性,通过 getSymbolsIngetSymbols 获取 Symbol 可枚举属性。

js
// 创建一个包含自身和原型链上可枚举属性名的数组
// 使用 for...in 遍历
function keysIn(object) {
    const result = []
    for (const key in object) {
        result.push(key)
    }
    return result
}

// 创建一个仅包含自身可枚举属性名的数组
// 非 ArrayLike 数组使用 Object.keys
function keys(object) {
    return isArrayLike(object)
        ? arrayLikeKeys(object)
    	: Object.keys(Object(object))
}

// 测试代码
function Foo() {
  this.a = 1
  this.b = 2
}
Foo.prototype.c = 3

keysIn(new Foo)
// ['a', 'b', 'c'] (迭代顺序无法保证)
     
keys(new Foo)
// ['a', 'b'] (迭代顺序无法保证)

常规属性遍历原型链用的是 for.. in,那么 Symbol 是如何遍历原型链的呢,这里通过循环以及使用 Object.getPrototypeOf 获取原型链上的 Symbol

js
// 创建一个包含自身和原型链上可枚举 Symbol 的数组
// 通过循环和使用 Object.getPrototypeOf 获取原型链上的 Symbol
function getSymbolsIn (object) {
    const result = []
    while (object) { // 循环
        result.push(...getSymbols(object))
        object = Object.getPrototypeOf(Object(object))
    }
    return result
}

// 创建一个仅包含自身可枚举 Symbol 的数组
// 通过 Object.getOwnPropertySymbols 获取 Symbol 属性
const nativeGetSymbols = Object.getOwnPropertySymbols
const propertyIsEnumerable = Object.prototype.propertyIsEnumerable

function getSymbols (object) {
    if (object == null) { // 判空
        return []
    }
    object = Object(object)
    return nativeGetSymbols(object)
        .filter((symbol) => propertyIsEnumerable.call(object, symbol))
}

我们回到主线代码,获取到 keys 组成的 props 数组之后,遍历并递归。

js
// 主线代码
const props = isArr ? undefined : keysFunc(value)
arrayEach(props || value, (subValue, key) => {
    // props 时替换 key 和 subValue,因为 props 里面的 subValue 只是 value 的 key
    if (props) { 
        key = subValue
        subValue = value[key]
    }
    // 递归拷贝(易受调用堆栈限制)
    assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
})

// 返回结果,主线结束
return result

我们看下 arrayEach 的实现,主要实现了一个遍历,并在 iteratee 返回为 false 时退出。

js
// 迭代数组
// iteratee 是每次迭代调用的函数
function arrayEach(array, iteratee) {
    let index = -1
    const length = array.length

    while (++index < length) {
        if (iteratee(array[index], index, array) === false) {
            break
        }
    }
    return array
}

我们看下 assignValue 的实现,在值不相等情况下,将 value 分配给 object[key]

js
const hasOwnProperty = Object.prototype.hasOwnProperty

// 如果现有值不相等,则将 value 分配给 object[key]。
function assignValue(object, key, value) {
    const objValue = object[key]

    // 不相等
    if (! (hasOwnProperty.call(object, key) && eq(objValue, value)) ) {
        // 值可用
        if (value !== 0 || (1 / value) == (1 / objValue)) {
            baseAssignValue(object, key, value)
        }
    // 值未定义而且键 key 不在对象中    
    } else if (value === undefined && !(key in object)) {
        baseAssignValue(object, key, value)
    }
}

// 赋值基本实现,其中没有值检查。
function baseAssignValue(object, key, value) {
    if (key == '__proto__') {
        Object.defineProperty(object, key, {
            'configurable': true,
            'enumerable': true,
            'value': value,
            'writable': true
        })
    } else {
        object[key] = value
    }
}

// 比较两个值是否相等
// (value !== value && other !== other) 是为了判断 NaN
function eq(value, other) {
  return value === other || (value !== value && other !== other)
}

baseClone完整代码

这部分就是核心代码了,各功能分割如下,详细功能实现部分将对各个功能详细解读。

js
/**
* The base implementation of `_.clone` and `_.cloneDeep` which tracks
* traversed objects.
*
* @private
* @param {*} value The value to clone.
* @param {boolean} bitmask The bitmask flags.
*  1 - Deep clone
*  2 - Flatten inherited properties
*  4 - Clone symbols
* @param {Function} [customizer] The function to customize cloning.
* @param {string} [key] The key of `value`.
* @param {Object} [object] The parent object of `value`.
* @param {Object} [stack] Tracks traversed objects and their clone counterparts.
* @returns {*} Returns the cloned value.
*/

function baseClone(value, bitmask, customizer, key, object, stack) {
    let result

    // 标志位
    const isDeep = bitmask & CLONE_DEEP_FLAG		// 深拷贝,true
    const isFlat = bitmask & CLONE_FLAT_FLAG		// 拷贝原型链,false
    const isFull = bitmask & CLONE_SYMBOLS_FLAG	// 拷贝 Symbol,true

    // 为 cloneWith 方法提供自定义 clone 函数
    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)
        }
    }
    // 循环引用
    stack || (stack = new Stack)
    const stacked = stack.get(value)
    if (stacked) {
        return stacked
    }
    stack.set(value, result)

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

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

    // TypedArray
    if (isTypedArray(value)) {
        return result
    }

    // Symbol & 原型链
    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]
        }
        assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
    })
    
    // 返回结果
    return result
}

比较

方法性能兼容性支持复杂结构(循环、函数、Symbol 等)适用场景
JSON.parse(JSON.stringify(obj))🚀 快✅ 高❌ 不支持循环、函数、undefinedDateMap简单对象
lodash.cloneDeep(obj)🐢 慢✅ 高✅ 支持大部分情况,支持循环引用大多数项目通用
structuredClone(obj)🚀🚀 非常快❌ 新浏览器才支持✅ 内建支持 DateMapSet、循环引用等现代浏览器或 Node 17+
手写递归(深拷贝)🐢~🚀 性能因实现而异✅ 自定义❌ 默认不支持复杂结构,需手动处理学习用途或高度定制

参考文献

木易杨前端进阶