JavaScript 对象复制完全指南(速查手册)

lishihuan大约 8 分钟

JavaScript 对象复制完全指南(速查手册)

📚 目录

  1. 浅拷贝 vs 深拷贝
  2. 对象浅拷贝
  3. 对象深拷贝
  4. 数组浅拷贝
  5. 数组深拷贝
  6. Vue 中的特殊情况
  7. 实战场景

浅拷贝 vs 深拷贝

概念对比

// 原始对象
const original = {
  name: '张三',
  age: 25,
  address: {
    city: '北京',
    district: '朝阳区'
  }
};

// 浅拷贝:只复制第一层
const shallow = { ...original };
shallow.name = '李四';           // ✅ 不影响原对象
shallow.address.city = '上海';   // ❌ 会影响原对象!

console.log(original.name);           // '张三' ✅
console.log(original.address.city);   // '上海' ❌ 被修改了!

// 深拷贝:递归复制所有层级
const deep = JSON.parse(JSON.stringify(original));
deep.name = '王五';              // ✅ 不影响原对象
deep.address.city = '广州';      // ✅ 不影响原对象

console.log(original.name);           // '张三' ✅
console.log(original.address.city);   // '北京' ✅ 没有被修改

可视化对比

原始对象:
┌─────────────┐
│ name: '张三' │
│ age: 25     │
│ address: ───┼──→ ┌──────────────┐
└─────────────┘    │ city: '北京'  │
                   │ district: ... │
                   └──────────────┘

浅拷贝:
┌─────────────┐
│ name: '李四' │  (新的)
│ age: 25     │  (新的)
│ address: ───┼──→ ┌──────────────┐
└─────────────┘    │ city: '北京'  │  (共享!)
                   │ district: ... │
                   └──────────────┘

深拷贝:
┌─────────────┐
│ name: '王五' │  (新的)
│ age: 25     │  (新的)
│ address: ───┼──→ ┌──────────────┐
└─────────────┘    │ city: '广州'  │  (新的!)
                   │ district: ... │
                   └──────────────┘

对象浅拷贝

方法1:展开运算符 ... (推荐⭐⭐⭐⭐⭐)

// 基本用法
const original = { name: '张三', age: 25 };
const copy = { ...original };

// 同时修改属性
const copy2 = { ...original, age: 30 };

// 合并多个对象
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const merged = { ...obj1, ...obj2 };
// 结果: { a: 1, b: 2, c: 3, d: 4 }

// 覆盖属性
const base = { name: '张三', age: 25, city: '北京' };
const updated = { ...base, age: 30 };
// 结果: { name: '张三', age: 30, city: '北京' }

方法2:Object.assign()

// 基本用法
const original = { name: '张三', age: 25 };
const copy = Object.assign({}, original);

// 合并多个对象
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const obj3 = { c: 3 };
const merged = Object.assign({}, obj1, obj2, obj3);
// 结果: { a: 1, b: 2, c: 3 }

// 修改目标对象(不推荐)
const target = { a: 1 };
Object.assign(target, { b: 2 }); // target 被修改了

方法3:手动复制

// 简单对象
const original = { name: '张三', age: 25 };
const copy = {};
for (let key in original) {
  copy[key] = original[key];
}

对象深拷贝

方法1:JSON 序列化 (最简单⭐⭐⭐⭐⭐)

// 基本用法
const original = {
  name: '张三',
  age: 25,
  address: {
    city: '北京',
    district: '朝阳区'
  },
  hobbies: ['读书', '旅游']
};

const deepCopy = JSON.parse(JSON.stringify(original));

// 修改深层属性
deepCopy.address.city = '上海';
console.log(original.address.city); // '北京' ✅ 不受影响

⚠️ 注意:JSON 方法的局限性

const obj = {
  date: new Date(),           // ❌ 会变成字符串
  func: () => {},             // ❌ 会丢失
  undefined: undefined,       // ❌ 会丢失
  symbol: Symbol('test'),     // ❌ 会丢失
  regex: /test/,              // ❌ 会变成空对象
  nan: NaN,                   // ❌ 会变成 null
  infinity: Infinity          // ❌ 会变成 null
};

const copy = JSON.parse(JSON.stringify(obj));
console.log(copy);
// { date: "2024-01-01T00:00:00.000Z", regex: {}, nan: null, infinity: null }

方法2:递归深拷贝 (通用⭐⭐⭐⭐⭐)

/**
 * 深拷贝函数(支持对象、数组、Date、RegExp等)
 * @param {*} obj - 要拷贝的对象
 * @returns {*} 拷贝后的对象
 */
function deepClone(obj) {
  // 处理 null 和基本类型
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  
  // 处理 Date
  if (obj instanceof Date) {
    return new Date(obj.getTime());
  }
  
  // 处理 RegExp
  if (obj instanceof RegExp) {
    return new RegExp(obj);
  }
  
  // 处理数组
  if (Array.isArray(obj)) {
    return obj.map(item => deepClone(item));
  }
  
  // 处理对象
  const clonedObj = {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      clonedObj[key] = deepClone(obj[key]);
    }
  }
  
  return clonedObj;
}

// 使用示例
const original = {
  name: '张三',
  date: new Date(),
  regex: /test/g,
  nested: {
    value: 100,
    array: [1, 2, 3]
  }
};

const copy = deepClone(original);
copy.nested.value = 200;
console.log(original.nested.value); // 100 ✅

方法3:lodash 库 (生产环境推荐⭐⭐⭐⭐⭐)

// 安装: npm install lodash

import _ from 'lodash';

const original = {
  name: '张三',
  address: {
    city: '北京'
  }
};

const deepCopy = _.cloneDeep(original);

方法4:structuredClone (现代浏览器⭐⭐⭐⭐)

// 浏览器原生方法(Chrome 98+, Firefox 94+)
const original = {
  name: '张三',
  date: new Date(),
  nested: {
    value: 100
  }
};

const deepCopy = structuredClone(original);

数组浅拷贝

方法1:展开运算符 ... (推荐⭐⭐⭐⭐⭐)

// 基本用法
const original = [1, 2, 3, 4, 5];
const copy = [...original];

// 合并数组
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const merged = [...arr1, ...arr2];
// 结果: [1, 2, 3, 4, 5, 6]

// 在开头添加元素
const arr = [2, 3, 4];
const newArr = [1, ...arr];
// 结果: [1, 2, 3, 4]

// 在末尾添加元素
const arr = [1, 2, 3];
const newArr = [...arr, 4];
// 结果: [1, 2, 3, 4]

// 在中间插入元素
const arr = [1, 2, 5];
const newArr = [...arr.slice(0, 2), 3, 4, ...arr.slice(2)];
// 结果: [1, 2, 3, 4, 5]

方法2:slice()

// 基本用法
const original = [1, 2, 3, 4, 5];
const copy = original.slice();

// 复制部分数组
const partial = original.slice(1, 3);
// 结果: [2, 3]

方法3:concat()

// 基本用法
const original = [1, 2, 3];
const copy = [].concat(original);

// 合并数组
const arr1 = [1, 2];
const arr2 = [3, 4];
const merged = arr1.concat(arr2);
// 结果: [1, 2, 3, 4]

方法4:Array.from()

// 基本用法
const original = [1, 2, 3];
const copy = Array.from(original);

// 转换类数组对象
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
const arr = Array.from(arrayLike);
// 结果: ['a', 'b', 'c']

数组深拷贝

方法1:JSON 序列化

// 基本用法
const original = [
  { id: 1, name: '张三' },
  { id: 2, name: '李四' }
];

const deepCopy = JSON.parse(JSON.stringify(original));

// 修改深层属性
deepCopy[0].name = '王五';
console.log(original[0].name); // '张三' ✅

方法2:递归深拷贝

// 使用前面定义的 deepClone 函数
const original = [
  { id: 1, name: '张三', tags: ['tag1', 'tag2'] },
  { id: 2, name: '李四', tags: ['tag3', 'tag4'] }
];

const deepCopy = deepClone(original);

方法3:map + 展开运算符(一层嵌套)

// 适用于数组元素是对象的情况
const original = [
  { id: 1, name: '张三' },
  { id: 2, name: '李四' }
];

const deepCopy = original.map(item => ({ ...item }));

// 修改属性
deepCopy[0].name = '王五';
console.log(original[0].name); // '张三' ✅

Vue 中的特殊情况

问题:Vue 的响应式对象

// Vue 2
const original = {
  name: '张三',
  age: 25,
  __ob__: Observer  // Vue 添加的响应式标记
};

// ❌ 错误:会复制 __ob__
const copy1 = { ...original };

// ✅ 正确:使用 JSON(会去除 __ob__)
const copy2 = JSON.parse(JSON.stringify(original));

// ✅ 正确:手动过滤
const copy3 = Object.keys(original).reduce((acc, key) => {
  if (key !== '__ob__') {
    acc[key] = original[key];
  }
  return acc;
}, {});

解决方案:Vue 专用复制函数

/**
 * 复制 Vue 响应式对象(去除 __ob__)
 * @param {Object} obj - Vue 响应式对象
 * @returns {Object} 纯净的对象
 */
function cloneVueObject(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  
  if (Array.isArray(obj)) {
    return obj.map(item => cloneVueObject(item));
  }
  
  const cloned = {};
  for (let key in obj) {
    // 跳过 Vue 的内部属性
    if (key === '__ob__' || key === '__v_skip') {
      continue;
    }
    if (obj.hasOwnProperty(key)) {
      cloned[key] = cloneVueObject(obj[key]);
    }
  }
  
  return cloned;
}

// 使用示例
export default {
  data() {
    return {
      form: {
        name: '张三',
        age: 25
      }
    }
  },
  methods: {
    copyForm() {
      // ✅ 正确
      const copy = cloneVueObject(this.form);
      
      // 或者使用 JSON
      const copy2 = JSON.parse(JSON.stringify(this.form));
    }
  }
}

实战场景

场景1:表单重置

export default {
  data() {
    return {
      // 原始表单数据
      defaultForm: {
        name: '',
        age: null,
        address: {
          city: '',
          district: ''
        }
      },
      // 当前表单数据
      form: {}
    }
  },
  created() {
    // 初始化时深拷贝
    this.resetForm();
  },
  methods: {
    resetForm() {
      // ✅ 深拷贝,避免修改 defaultForm
      this.form = JSON.parse(JSON.stringify(this.defaultForm));
    },
    
    submitForm() {
      // 提交表单...
      
      // 提交成功后重置
      this.resetForm();
    }
  }
}

场景2:编辑对话框

export default {
  data() {
    return {
      dialogVisible: false,
      editForm: {},
      originalData: null
    }
  },
  methods: {
    // 打开编辑对话框
    handleEdit(row) {
      // ✅ 深拷贝,避免直接修改表格数据
      this.originalData = row;
      this.editForm = JSON.parse(JSON.stringify(row));
      this.dialogVisible = true;
    },
    
    // 取消编辑
    handleCancel() {
      this.dialogVisible = false;
      this.editForm = {};
      this.originalData = null;
    },
    
    // 保存编辑
    handleSave() {
      // 更新原始数据
      Object.assign(this.originalData, this.editForm);
      this.dialogVisible = false;
    }
  }
}

场景3:数组操作(不修改原数组)

export default {
  data() {
    return {
      list: [
        { id: 1, name: '张三', selected: false },
        { id: 2, name: '李四', selected: false },
        { id: 3, name: '王五', selected: false }
      ]
    }
  },
  methods: {
    // ❌ 错误:直接修改原数组
    toggleSelectWrong(id) {
      const item = this.list.find(item => item.id === id);
      item.selected = !item.selected;
    },
    
    // ✅ 正确:创建新数组
    toggleSelectRight(id) {
      this.list = this.list.map(item => {
        if (item.id === id) {
          return { ...item, selected: !item.selected };
        }
        return item;
      });
    },
    
    // 添加项目
    addItem(newItem) {
      // ✅ 不修改原数组
      this.list = [...this.list, newItem];
    },
    
    // 删除项目
    removeItem(id) {
      // ✅ 不修改原数组
      this.list = this.list.filter(item => item.id !== id);
    },
    
    // 更新项目
    updateItem(id, updates) {
      // ✅ 不修改原数组
      this.list = this.list.map(item => {
        if (item.id === id) {
          return { ...item, ...updates };
        }
        return item;
      });
    }
  }
}

场景4:保存历史记录(撤销/重做)

export default {
  data() {
    return {
      form: { name: '', age: null },
      history: [],
      historyIndex: -1
    }
  },
  methods: {
    // 保存当前状态到历史
    saveHistory() {
      // 删除当前位置之后的历史
      this.history = this.history.slice(0, this.historyIndex + 1);
      
      // 添加新的历史记录(深拷贝)
      this.history.push(JSON.parse(JSON.stringify(this.form)));
      this.historyIndex++;
      
      // 限制历史记录数量
      if (this.history.length > 50) {
        this.history.shift();
        this.historyIndex--;
      }
    },
    
    // 撤销
    undo() {
      if (this.historyIndex > 0) {
        this.historyIndex--;
        this.form = JSON.parse(JSON.stringify(this.history[this.historyIndex]));
      }
    },
    
    // 重做
    redo() {
      if (this.historyIndex < this.history.length - 1) {
        this.historyIndex++;
        this.form = JSON.parse(JSON.stringify(this.history[this.historyIndex]));
      }
    }
  }
}

场景5:对比数据变化

export default {
  data() {
    return {
      originalData: {},
      currentData: {}
    }
  },
  methods: {
    // 加载数据
    loadData(data) {
      // 保存原始数据
      this.originalData = JSON.parse(JSON.stringify(data));
      // 当前数据
      this.currentData = JSON.parse(JSON.stringify(data));
    },
    
    // 检查是否有修改
    hasChanges() {
      return JSON.stringify(this.originalData) !== JSON.stringify(this.currentData);
    },
    
    // 获取变化的字段
    getChangedFields() {
      const changes = {};
      for (let key in this.currentData) {
        if (JSON.stringify(this.originalData[key]) !== JSON.stringify(this.currentData[key])) {
          changes[key] = {
            old: this.originalData[key],
            new: this.currentData[key]
          };
        }
      }
      return changes;
    },
    
    // 重置修改
    resetChanges() {
      this.currentData = JSON.parse(JSON.stringify(this.originalData));
    }
  }
}

快速参考表

场景推荐方法代码
对象浅拷贝展开运算符{ ...obj }
对象深拷贝JSONJSON.parse(JSON.stringify(obj))
数组浅拷贝展开运算符[...arr]
数组深拷贝JSONJSON.parse(JSON.stringify(arr))
合并对象展开运算符{ ...obj1, ...obj2 }
合并数组展开运算符[...arr1, ...arr2]
更新对象属性展开运算符{ ...obj, key: newValue }
更新数组元素maparr.map(item => item.id === id ? { ...item, ...updates } : item)
Vue 表单重置JSONthis.form = JSON.parse(JSON.stringify(this.defaultForm))
复杂对象深拷贝lodash_.cloneDeep(obj)

总结

记忆口诀

浅拷贝:展开运算符 {...obj} [...arr]
深拷贝:JSON.parse(JSON.stringify())
Vue 对象:去除 __ob__
不修改:总是创建新对象/数组

最佳实践

  1. 默认使用浅拷贝(性能更好)
  2. 需要深层修改时使用深拷贝
  3. Vue 中避免直接修改 props 和 data
  4. 数组操作使用 map/filter/concat 等不可变方法
  5. 表单重置使用深拷贝

常见错误

// ❌ 错误1:直接赋值(不是拷贝)
const copy = original;

// ❌ 错误2:浅拷贝嵌套对象
const copy = { ...original };
copy.nested.value = 100; // 会影响原对象!

// ❌ 错误3:直接修改数组
this.list.push(item); // 可能导致 Vue 响应式问题

// ✅ 正确
const copy = JSON.parse(JSON.stringify(original));
this.list = [...this.list, item];