1. 为什么深拷贝这件事,最后都会绕到递归上
深拷贝这个需求,前端开发里几乎人人都遇到过。不管是状态管理里要隔离数据、提交表单前深拷贝一份配置,还是做组件库时避免副作用,你都会面临同一个问题:怎么把一个对象完整地复制一份,让它和原来的对象彻底没关系。
网上搜“深拷贝”能出来一堆文章,但大部分要么直接甩一行 JSON.parse(JSON.stringify(obj)),要么推荐 lodash.cloneDeep。真正愿意把深拷贝背后的递归逻辑讲清楚的很少。我刚工作那会儿也是这样,遇到拷贝直接JSON一把梭,直到有一次日期被转成了字符串、undefined 直接消失、循环引用直接报错,我才开始认真研究递归和深拷贝之间的底层关系。
先说结论:深拷贝本质上是在遍历一棵“由引用关系组成的树”,而递归是遍历树形结构最自然、最优雅的方式。你不需要背代码,把递归的逻辑想明白,深拷贝的每个分支都能自己推出来。
这篇文章适合谁看?正在学 JavaScript 基础、被深浅拷贝和递归绕晕的前端新手,以及“会用 cloneDeep 但一直没搞懂到底发生了什么”的进阶开发者。我会从最底层的数据结构讲起,把递归的思路拆开,最后给出一套能直接放进项目的生产级深拷贝实现。
1.1 浅拷贝和深拷贝的本质区别
很多人对深浅拷贝的理解停留在“一层”和“多层”这个说法上,其实这是不对的。浅拷贝不是“只拷贝一层”,而是“只处理当前层,遇到引用类型就原封不动地共享地址”。
看一个最简单的例子:
javascript复制const original = {
name: 'Tom',
address: {
city: 'Shanghai',
zip: 200000
}
};
// 浅拷贝
const shadowCopy = { ...original };
shadowCopy.name = 'Jerry'; // 不影响 original
shadowCopy.address.city = 'Beijing'; // original.address.city 也变成了 Beijing
为什么第二层会互相影响?因为 ... 展开运算符在做拷贝时,只会把 original 的属性逐个复制到新对象里。对于基本类型(string、number、boolean),复制的是值,所以改 name 不影响原对象;但对于 address 这种引用类型,复制的是“指向内存地址的引用”,新对象和原对象的 address 属性指向同一块内存区域。所以不管你从新对象还是原对象去改 address.city,改的都是同一块内存。
这就是浅拷贝的核心:它只走了一层,更深层的引用关系没有断开。
深拷贝要做的事情,就是沿着这棵引用树继续往下走,走到每一个叶子节点都是基本类型,然后一层层把“引用”替换成“真正新建的对象”。这个“沿着引用关系往下走”的过程,天然就是递归的形态。
1.2 递归天生就是为树形结构设计的
递归的经典理解方式有两个关键词:递推和回归。你定义一个函数,在函数体里调用自己,每次调用时输入更小、更简单的子问题,直到子问题简单到可以直接返回结果,然后再一层层把结果组合回去。
把对象想象成一棵树:根节点是那个 obj,它的每个属性值是树的分支节点。如果一个属性值还是一个对象,就又是一个子树。基本类型的属性是叶子节点,没有子分支了。
递归深拷贝的逻辑其实就两句:
- 当前节点的值不是对象吗?对,那先造一个空对象/数组,然后遍历它的每个属性,对每个属性值“重复同样的拷贝逻辑”。
- 当前节点的值是基本类型吗?对,那直接返回这个值。
这个“对每个属性值重复同样的拷贝逻辑”,就是函数调用它自己。所以当你写下第一版递归深拷贝时,会发现代码几乎是在“翻译”那句话。
javascript复制function deepClone(source) {
// 叶子节点:基本类型,直接返回
if (source === null || typeof source !== 'object') {
return source;
}
// 分支节点:递归处理每个属性
const target = Array.isArray(source) ? [] : {};
for (let key in source) {
target[key] = deepClone(source[key]);
}
return target;
}
这段代码虽然短,但已经触及了递归深拷贝的骨架:先判断当前层的情况,再决定是直接返回还是深入递归。 后面所有复杂版本,都是在“分支节点”的处理上增加更多细分的类型判断,但骨架不变。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 亲手写一版基础版深拷贝,先跑通核心逻辑
在堆复杂类型之前,我建议你先亲手写一版尽可能简单的深拷贝,跑通基本场景,再一步步加边界处理。直接看完整版代码很容易被一堆 instanceof 分支吓到,忘了核心递归逻辑其实就二十行。
2.1 怎么处理对象和数组的分支
上面那段基础代码,有人可能会觉得 Array.isArray(source) 这个判断很多余:反正数组也是对象,直接 {} 不也一样吗?
不一样。数组和对象在 JavaScript 里的表现虽然相似,但在赋值属性时,数组有特殊的 length 属性和索引语义。如果拷贝时传入一个数组,你用 {} 来承接,拷贝出来的结果虽然属性都有,但它失去了数组的方法和 length 的自动维护能力,比如 push 就没了。
javascript复制const arr = [1, 2, 3];
const copy = deepClone(arr);
// 如果是 {} 承接,copy 是 {0: 1, 1: 2, 2: 3},没有 push、map、filter
所以在递归分支里,第一步要先判断这个节点是数组还是普通对象,用不同的容器来承接。这一步是所有递归深拷贝的共性:容器类型决定递归结果的结构。
2.2 为什么 for...in 会带出原型链上的属性
基础版本的 for (let key in source) 有一个隐藏问题:for...in 遍历的是“可枚举属性”,包括继承自原型链的属性。虽然常见的普通对象自身属性都能被遍历到,但如果你要拷贝的对象是用构造函数创建的、带着原型上的方法或属性,for...in 会把它们一并拷贝进去。
javascript复制function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() { console.log('hi'); };
const p = new Person('Tom');
const copy = deepClone(p);
// copy 上会多一个 sayHi,因为 for...in 遍历了原型链
解决办法有两种。一种是改用 Object.keys(source) 来拿“自身可枚举属性”,另一种是判断 Object.prototype.hasOwnProperty.call(source, key),只处理自身属性。生产环境中,我更推荐配合 Object.keys 加 for (const key of keys) 的写法,语义更明确,也不容易被原型链污染。
javascript复制function deepClone(source) {
if (source === null || typeof source !== 'object') {
return source;
}
const target = Array.isArray(source) ? [] : {};
const keys = Object.keys(source);
for (const key of keys) {
target[key] = deepClone(source[key]);
}
return target;
}
这段代码就是基础版的最终形态,跑普通嵌套对象、嵌套数组都没问题。但它距离“生产可用”还差得远,接下来要处理真正的硬骨头。
2.3 基础版踩坑实录:JSON.parse 和它解决不了的问题
如果你一直用 JSON.parse(JSON.stringify(obj)) 做深拷贝,我强烈建议你亲手测一下下面几个场景,看看它到底会丢掉什么:
javascript复制const obj = {
date: new Date(),
regex: /test/gi,
func: function() { console.log('hello'); },
key: undefined,
symbolKey: Symbol('id'),
nested: { map: new Map([['a', 1]]), set: new Set([1, 2]) }
};
console.log(JSON.parse(JSON.stringify(obj)));
// 输出:
// {
// date: "2025-...(字符串)",
// regex: {},
// nested: { map: {}, set: {} },
// // func 不见了,key 不见了,symbolKey 不见了
// }
这就是 JSON 方式的致命伤:它只认识 JSON 支持的数据类型。函数、undefined、Symbol 会被直接丢弃;Date 会被转成字符串再转回来,变成了纯字符串;正则、Map、Set 会退化成一个空对象。如果你拷贝的是简单的纯数据对象,JSON 方式够用,但只要数据结构里混入这些特殊类型,你就会得到一份“残缺”的拷贝。
这也就是为什么真正写深拷贝,不能依赖 JSON 序列化,而要用递归加类型判断逐层重建。
3. 生产级递归深拷贝完整实现:循环引用、特殊类型一个不落
当你把基础版本跑通,开始处理复杂数据结构时,会遇到三座大山:循环引用、特殊对象类型、丢失原型和 Symbol 属性。这一章我直接给出完整解法,并且把每一步的逻辑讲清楚。
3.1 循环引用:用 WeakMap 打破无限递归
先看一个最常见的崩溃场景:
javascript复制const obj = {};
obj.self = obj;
如果用基础版去拷贝这个对象,会发生什么?递归进入 obj.self,发现它还是 obj,于是又创建一个新对象,然后继续遍历,又遇到 obj.self……无限循环下去,直到爆栈,控制台报 Maximum call stack size exceeded。
解决思路并不复杂:我们只需要记住“某个对象已经被拷贝过了”,如果再次遇到同一个对象,直接返回之前拷贝出来的那份引用,而不是重新递归。
怎么记?用一个 Map 或者 WeakMap,把“原对象”和“拷贝出来的新对象”关联起来。这里我推荐 WeakMap,因为它对键的引用是弱引用,不会阻碍原对象的垃圾回收。在深拷贝这种场景里,原对象用完就可能被丢弃,WeakMap 更安全。
javascript复制function deepClone(source, map = new WeakMap()) {
if (source === null || typeof source !== 'object') {
return source;
}
// 如果当前对象已经被拷贝过,直接返回之前的拷贝结果
if (map.has(source)) {
return map.get(source);
}
const target = Array.isArray(source) ? [] : {};
map.set(source, target);
const keys = Object.keys(source);
for (const key of keys) {
target[key] = deepClone(source[key], map);
}
return target;
}
注意 map.set(source, target) 必须放在递归子属性之前。如果先递归再 set,循环引用还是会在第一次进入子属性时再次遇到原来的对象,依然死循环。先登记,再递归,这个顺序是循环引用的破局点。
我用一个循环对象实际测试了一下,这个版本能正确输出一个同样带 self 引用、且互相引用关系保持一致的拷贝对象,而且不会爆栈。
3.2 特殊类型逐个击破:Date、RegExp、Map、Set 各有各的坑
现在版递归只处理了普通对象和数组,遇到 Date、RegExp、Map、Set 这些“真的对象”时,Object.keys 拿出来通常是空数组,拷贝结果会退化成空对象。比如 new Date() 拷贝过去就变成了 {},毫无意义。
这些类型需要单独处理。我的做法是在递归开头增加类型判断的分支,每个分支返回正确的拷贝结果:
javascript复制function deepClone(source, map = new WeakMap()) {
if (source === null || typeof source !== 'object') {
return source;
}
if (map.has(source)) {
return map.get(source);
}
// 处理 Date:基于时间戳新建一个 Date
if (source instanceof Date) {
const copy = new Date(source.getTime());
map.set(source, copy);
return copy;
}
// 处理 RegExp:保留 source 和 flags
if (source instanceof RegExp) {
const copy = new RegExp(source.source, source.flags);
copy.lastIndex = source.lastIndex;
map.set(source, copy);
return copy;
}
// 处理 Map:递归拷贝键和值
if (source instanceof Map) {
const copy = new Map();
map.set(source, copy);
source.forEach((value, key) => {
copy.set(deepClone(key, map), deepClone(value, map));
});
return copy;
}
// 处理 Set:递归拷贝每个值
if (source instanceof Set) {
const copy = new Set();
map.set(source, copy);
source.forEach((value) => {
copy.add(deepClone(value, map));
});
return copy;
}
// 普通对象或数组
const target = Array.isArray(source) ? [] : {};
map.set(source, target);
const keys = Object.keys(source);
for (const key of keys) {
target[key] = deepClone(source[key], map);
}
return target;
}
这里有几个细节值得单独说一下。
Date 的拷贝为什么不能直接 new Date(source)?因为有些 Date 对象是无效日期,直接复制引用无法区分“这是同一个对象”还是“值相同的新对象”,写深拷贝就要严格遵守“新建一个对象”的原则。用 getTime() 拿时间戳再新建,是最稳妥的做法。
RegExp 为什么还要手动处理 lastIndex?因为正则对象如果设置了全局匹配(g 或 y 标志),它是有状态的,lastIndex 会随着 exec 调用而移动。不拷贝 lastIndex 的话,克隆出来的正则会停留在原有状态,可能影响后续匹配结果。
Map 和 Set 有个共同点:它们的值也可能嵌套引用类型的对象,甚至可能直接循环引用自己。所以内部的每个 key 和 value 都要继续走递归,而不是简单地把引用赋值过去。
3.3 别忘了 Symbol 和原型链
前面的代码用 Object.keys 拿到的只有字符串 key,Symbol 类型的 key 会被忽略。如果对象里有 Symbol 属性(很多组件库和内部标识都爱用 Symbol),拷贝出来就丢了。
要处理 Symbol 属性,用 Object.getOwnPropertySymbols(source) 拿到所有 Symbol 键,然后递归拷贝对应的值,相加到目标对象上。这一步要不要做,看你的场景。如果确认拷贝的数据只是普通配置对象,可以跳过;但如果做通用工具函数,我建议加上,成本很低:
javascript复制// 在普通对象的递归处理分支里,加上 Symbol 键的遍历
const symbols = Object.getOwnPropertySymbols(source);
for (const sym of symbols) {
const desc = Object.getOwnPropertyDescriptor(source, sym);
if (desc.enumerable) {
target[sym] = deepClone(source[sym], map);
}
}
还有原型链的问题。如果你拷贝一个自定义类实例,直接 {} 会产生一个普通对象,丢失构造函数和原型上的方法。更稳的做法是保留原来的原型:
javascript复制// 在递归创建容器时,保留原型
const target = Array.isArray(source)
? []
: Object.create(Object.getPrototypeOf(source));
Object.create(Object.getPrototypeOf(source)) 会创建一个“原型和 source 相同”的新对象。这样,深拷贝一个 Person 实例时,拷贝结果仍然是 Person 的实例,原型上的方法不会丢。这是很多简化版本深拷贝忽略的重型细节。
3.4 完整版代码汇总,可直接复制到项目里
把前面所有的处理逻辑汇总起来,就是下面这个完整版本。这段代码我已经在真实项目里跑过,常规的配置对象、类实例、日期、正则、Map、Set、循环引用都能正确复制:
javascript复制function deepClone(source, map = new WeakMap()) {
// 基本类型直接返回
if (source === null || typeof source !== 'object') {
return source;
}
// 循环引用:返回之前的拷贝结果
if (map.has(source)) {
return map.get(source);
}
// Date
if (source instanceof Date) {
const copy = new Date(source.getTime());
map.set(source, copy);
return copy;
}
// RegExp
if (source instanceof RegExp) {
const copy = new RegExp(source.source, source.flags);
copy.lastIndex = source.lastIndex;
map.set(source, copy);
return copy;
}
// Map
if (source instanceof Map) {
const copy = new Map();
map.set(source, copy);
source.forEach((value, key) => {
copy.set(deepClone(key, map), deepClone(value, map));
});
return copy;
}
// Set
if (source instanceof Set) {
const copy = new Set();
map.set(source, copy);
source.forEach((value) => {
copy.add(deepClone(value, map));
});
return copy;
}
// 普通对象 / 数组:保留原型
const target = Array.isArray(source)
? []
: Object.create(Object.getPrototypeOf(source));
map.set(source, target);
// 处理字符串键
const keys = Object.keys(source);
for (const key of keys) {
target[key] = deepClone(source[key], map);
}
// 处理 Symbol 键
const symbols = Object.getOwnPropertySymbols(source);
for (const sym of symbols) {
const desc = Object.getOwnPropertyDescriptor(source, sym);
if (desc.enumerable) {
target[sym] = deepClone(source[sym], map);
}
}
return target;
}
你可以写一段测试代码验证一下这个版本和多层嵌套加循环引用的场景:
javascript复制const obj = {
name: 'Tom',
date: new Date(),
reg: /hello/g,
map: new Map([['a', { x: 1 }]]),
set: new Set([{ y: 2 }]),
arr: [1, [2, [3]]],
sym: Symbol('id')
};
obj.self = obj;
const copy = deepClone(obj);
console.log(copy.self === copy); // true,循环引用保留且指向拷贝对象
console.log(copy === obj); // false,不是同一个对象
console.log(copy.arr[1][1][0]); // 3,深层数组正常
console.log(copy.map.get('a').x); // 1,Map 中的嵌套对象也是新引用
4. 深拷贝实战避坑:这些年我踩过的类型陷阱
写深拷贝这件事,看起来是“写一个函数”,实际上真正花时间的全在边界处理上。这一章把我过去项目中遇到过的典型问题整理成速查表,每一条都是真实踩坑记录。
4.1 Date 被拷成空对象的经典乌龙
有段时间我在做数据报表,需要把后端返回的筛选条件对象深拷贝一份,给“重置筛选”功能用。筛选条件里有一个 dateRange 是 Date 类型,我当时用的是简化版深拷贝,只处理普通对象和数组。
结果用户设置好日期范围,点“重置筛选”之后,日期没消失,反而变成了一堆 {}。当时我还没意识到是 Date 拷贝出了问题,排查了很久才发现:拷贝后的 dateRange 已经是一个没有时间信息的空对象,渲染层拿它和“空值”做比较时永远不相等,所以重置逻辑失效了。
后来我把 Date 分支加上去,问题立刻消失。这个教训让我意识到:深拷贝的“完整”不是主观感受,而是每个可枚举属性都变成了正确且独立的引用。 Date 看起来是个对象,但它没有可枚举属性,必须特判。
4.2 循环引用导致页面直接卡死
还有一次是在做复杂表单的动态字段配置,数据模型由后端动态返回,节点之间存在循环引用(父节点引用子节点,子节点引用父节点)。我在操作配置数据时用了自己写的深拷贝,没想到循环引用一进来,递归直接无限循环,页面卡死,控制台报错 Maximum call stack size exceeded。
当时第一反应是“数据有问题”,后来一想,循环引用的对象在 JavaScript 里是合法的,真正的深层原因是我的拷贝函数没有做“已拷贝记录”的登记。从那以后,我给引用类型统一建了一个 WeakMap 登记表,遇到相同的对象直接返回之前的拷贝结果,既解决循环引用,又顺便优化了性能——同一个对象被多处引用时,不会重复拷贝出两份不一致的新对象。
4.3 Map 和 Set 内部嵌套的对象引用没有断开
另一个容易忽略的陷阱是,Map 和 Set 的“深层”也是引用类型。比如:
javascript复制const original = { data: new Map([['user', { name: 'Tom', address: { city: 'SH' } }]]) };
如果你只在 Map 分支里 copy.set(key, value),而 value 本身又是一个对象,那拷贝出来的 Map 里的 user,和原对象的 user 仍然共享同一个引用。这时候修改拷贝结果的 address.city,原对象也会跟着变。
这就是为什么 Map 分支里,value 必须继续走 deepClone(value, map)。不只是 Map,Set 分支里每个值也要继续递归。深拷贝的本质就是“所有引用都要断干净”,任何依赖类型的值只要内部还藏着对象,就必须继续递归下去。
4.4 函数到底要不要拷贝
关于函数,有一个很实际的争论:深拷贝到底要不要管 function 类型的属性?
我的处理原则是:函数直接共享引用,不拷贝。原因很简单:函数的本质是“一段可执行的代码 + 闭包环境”,你当然可以用 new Function 重新构造一个函数,但闭包无法被序列化,强行拷贝大概率会得到一段无法正确执行的代码。
在项目中,我通常在进入“引用类型分支”前加一个判断:
javascript复制// 函数直接返回原引用
if (typeof source === 'function') {
return source;
}
这样,当深拷贝遇到函数属性时,会原样返回函数引用,不进入递归逻辑。绝大多数业务场景下这是正确的行为:深拷贝关心的是“数据”的独立性,而不是“行为”的独立性。
5. 再深一层:递归的边界与性能思考
走到这一步,你已经有一版很能打的深拷贝函数了。但如果想继续往深了挖,递归本身还有一些值得琢磨的问题——性能、调用栈、尾递归优化,这些直接关系到深拷贝在大数据量场景下能不能用。
5.1 递归深度和调用栈溢出
递归实现依赖函数调用栈,每次递归调用都会在调用栈上增加一层帧。如果一个对象的嵌套深度非常深(比如 10000 层,虽然业务数据里很少见),递归版深拷贝就会在到达 JavaScript 默认栈上限时崩溃。
栈溢出的本质是“递归深度超过引擎限制”。这跟“死循环”不一样,死循环是逻辑错误,而栈溢出是有意识写出来的递归,因为嵌套深度确实太深了。实际业务里,10 层的嵌套已经很少见,100 层的配置数据更是稀有动物,所以递归深拷贝的栈风险在绝大多数场景下可以忽略。但如果你处理的是某些自动生成的数据结构(比如 AST 树、语法分析器输出),嵌套深度可能上千,那就得考虑改用显式栈迭代来实现深拷贝了。
5.2 JavaScript 的尾递归为什么靠不住
很多人一提到递归就说“可以用尾递归优化解决栈溢出”。但 JavaScript 的情况比较尴尬:ES6 规范里虽然定义了尾调用优化(TCO),但长期以来主流浏览器都没有完全实现它(苹果 Safari 有些版本实现了,V8 一直没做)。
这意味着,即使你写成尾递归形式,在 Chrome 里照样会爆栈。所以我不建议深拷贝依赖尾递归优化来缓解栈溢出问题。如果真的担心深度,老老实实用显式栈:
javascript复制function deepCloneIterative(root) {
if (root === null || typeof root !== 'object') return root;
const map = new WeakMap();
const rootCopy = Array.isArray(root) ? [] : {};
map.set(root, rootCopy);
const stack = [{ source: root, target: rootCopy }];
while (stack.length) {
const { source, target } = stack.pop();
const keys = Object.keys(source);
for (const key of keys) {
const value = source[key];
if (value === null || typeof value !== 'object') {
target[key] = value;
} else if (map.has(value)) {
target[key] = map.get(value);
} else {
const valueCopy = Array.isArray(value) ? [] : {};
map.set(value, valueCopy);
target[key] = valueCopy;
stack.push({ source: value, target: valueCopy });
}
}
}
return rootCopy;
}
显式栈版本的思路是把“递归函数调用”改成了“手动维护一个待处理列表”。它不依赖调用栈,深度再大也只会受内存限制,不会爆栈。缺点是代码可读性比递归差一些,处理特殊类型也更繁琐。我的建议是:业务里用递归版,性能和可读性平衡得最好;只有在明确处理超深嵌套数据时才切换成迭代版。
5.3 什么时候该自己写,什么时候直接用 lodash
深拷贝在 JavaScript 生态里实在太常见,以至于每个工具库都有现成实现。新手往往纠结:我该自己写一个,还是直接用 lodash.cloneDeep?
我的观点很明确:做业务开发,直接用成熟库;做技术学习和通用工具封装,自己写一遍非常有价值。cloneDeep 经过了无数项目的验证,对 Date、RegExp、Map、Set、ArrayBuffer、typed arrays 等类型都有完备处理,连 BigInt 也考虑到了。自己写的版本,如果遇到 ArrayBuffer、DataView 之类的二进制类型,很可能漏掉。但如果你只是拷贝业务数据里的 JSON 安全对象,自己写的那版 30 行代码完全够用,还能省掉引入整个 lodash 的成本。
关于 WeakMap,我再多讲一句。上面代码里用了 WeakMap 作为登记表,除了弱引用的特性,它还有另一个好处:它不干扰对象的可枚举性判断。如果你用普通 Map,在遍历原对象时不会遍历到它,所以实际差异只在内存回收上体现。但在递归往返过程中,如果某个对象后来不再被引用,WeakMap 里对应的键值对会被自动回收,不会造成内存泄漏。这就是为什么从“生产级”角度看,WeakMap 是更优的选择。
6. 写在最后的经验分享
递归实现深拷贝这件事,技术点上看起来就一个函数,但要真正写“对”,需要你对 JavaScript 的类型系统、引用语义、原型链、迭代协议都有扎实的理解。这也是为什么很多面试官爱问深拷贝——它像一个漏斗,能快速筛出对语言底层理解深浅的人。
我自己写深拷贝的经历,从最早 JSON.parse(JSON.stringify()) 一把梭,到踩了 Date 和循环引用的坑,再到逐步补全特殊类型分支,最后形成一套完整的实现方案,整个过程其实就是对 JavaScript 数据类型认知的完善过程。如果你现在还处于“会用工具库但不知道内部逻辑”的阶段,我强烈建议你花一个下午,亲手把深拷贝从简易版写到完整版,把你遇到过的每个坑都体现在代码里。写完了你会发现,看很多其他问题的视角都会不一样。
最后再分享一个小技巧:写深拷贝之前,先想清楚一个原则——深拷贝克隆的是“值的结构”,而不是“代码的行为”。 这句话能帮你解决 80% 的边界纠结。遇到 Date,克隆它的时间值;遇到 RegExp,克隆它的源码和 flags;遇到函数,直接拿引用——因为它本来就不属于“数据”。想清楚这几个字,你的深拷贝函数就不会在设计上跑偏。
