一、拷贝对象或将其作为函数参数传递时,所拷贝的是引用,而不是对象本身。
1、拷贝一个对象变量会又创建一个对相同对象的引用。
2、所有通过被拷贝的引用的操作(如添加、删除属性)都作用在同一个对象上。
二、这时需要创建“真正的拷贝”(一个克隆),包括浅拷贝/深拷贝。

拷贝的定义

浅拷贝

一、浅拷贝只会将对象的各个属性进行依次复制,并不会进行递归复制。

  • 也就是说只会赋值目标对象的第一层属性。 对于目标对象第一层为基本数据类型的数据,就是直接赋值,即「传值」;
  • 而对于目标对象第一层为引用数据类型的数据,就是直接赋存于栈内存中的堆内存地址,即「传值」。

    • 新建一个指针指向原内存地址。

      深拷贝

      一、深拷贝不同于浅拷贝,它不只拷贝目标对象的第一层属性,而是递归拷贝目标对象的所有属性。
  • 把对象重新开辟一个内存地址拷贝过来

二、一般来说,在JavaScript中考虑复合类型的深层复制的时候,往往就是指对于 Date 、Object 与 Array 这三个复合类型的处理。

js及库实现拷贝

js:手动实现一份浅拷贝加扩展的函数

一、实现代码

  1. function _isPlainObject(target) {
  2. return (typeof target === 'object' && !!target && !Array.isArray(target));
  3. }
  4. function shallowExtend() {
  5. var args = Array.prototype.slice.call(arguments);
  6. // 第一个参数作为target
  7. var target = args[0];
  8. var src;
  9. target = _isPlainObject(target) ? target : {};
  10. for (var i=1; i<args.length; i++) {
  11. src = args[i];
  12. if (!_isPlainObject(src)) {
  13. continue;
  14. }
  15. for(var key in src) {
  16. if (src.hasOwnProperty(key)) {
  17. if (src[key] != undefined) {
  18. target[key] = src[key];
  19. }
  20. }
  21. }
  22. }
  23. return target;
  24. }

二、测试用例

  1. // 初始化引用数据类型变量
  2. var target = {
  3. key: 'value',
  4. num: 1,
  5. bool: false,
  6. arr: [1, 2, 3],
  7. obj: {
  8. objKey: 'objValue'
  9. },
  10. };
  11. // 拷贝+扩展
  12. var result = shallowExtend({}, target, {
  13. key: 'valueChanged',
  14. num: 2,
  15. bool: true,
  16. });
  17. // 对原引用类型数据做修改
  18. target.arr.push(4);
  19. target.obj['objKey2'] = 'objValue2';
  20. // 比较基本数据类型的属性值
  21. result === target; // false
  22. result.key === target.key; // false
  23. result.num === target.num; // false
  24. result.bool === target.bool;// false
  25. // 比较引用数据类型的属性值
  26. result.arr === target.arr; // true
  27. result.obj === target.obj; // true

jQuery:jQuery.extend 实现深浅拷贝加扩展功能

一、贴下 jQuery@3.3.1 中 jQuery.extend 的实现:

  1. jQuery.extend = jQuery.fn.extend = function() {
  2. var options,
  3. name,
  4. src,
  5. copy,
  6. copyIsArray,
  7. clone,
  8. target = arguments[0] || {},
  9. i = 1,
  10. length = arguments.length,
  11. deep = false;
  12. // 如果第一个参数是布尔值,则为判断是否深拷贝的标志变量
  13. if (typeof target === "boolean") {
  14. deep = target;
  15. // 跳过 deep 标志变量,留意上面 i 的初始值为1
  16. target = arguments[i] || {};
  17. // i 自增1
  18. i++;
  19. }
  20. // 判断 target 是否为 object / array / function 以外的类型变量
  21. if (typeof target !== "object" && !isFunction(target)) {
  22. // 如果是其它类型变量,则强制重新赋值为新的空对象
  23. target = {};
  24. }
  25. // 如果只传入1个参数;或者是传入2个参数,第一个参数为 deep 变量,第二个为 target
  26. // 所以 length 的值可能为 1 或 2,但无论是 1 或 2,下段 for 循环只会运行一次
  27. if (i === length) {
  28. // 将 jQuery 本身赋值给 target
  29. target = this;
  30. // i 自减1,可能的值为 0 或 1
  31. i--;
  32. }
  33. for (; i < length; i++) {
  34. // 以下拷贝操作,只针对非 null 或 undefined 的 arguments[i] 进行
  35. if ((options = arguments[i]) != null) {
  36. // Extend the base object
  37. for (name in options) {
  38. src = target[name];
  39. copy = options[name];
  40. // 避免死循环的情况
  41. if (target === copy) {
  42. continue;
  43. }
  44. // Recurse if we're merging plain objects or arrays
  45. // 如果是深拷贝,且copy值有效,且copy值为纯object或纯array
  46. if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = Array.isArray(copy)))) {
  47. if (copyIsArray) {
  48. // 数组情况
  49. copyIsArray = false;
  50. clone = src && Array.isArray(src) ? src : [];
  51. } else {
  52. // 对象情况
  53. clone = src && jQuery.isPlainObject(src) ? src : {};
  54. }
  55. // 克隆copy对象到原对象并赋值回原属性,而不是重新赋值
  56. // 递归调用
  57. target[name] = jQuery.extend(deep, clone, copy);
  58. // Don't bring in undefined values
  59. } else if (copy !== undefined) {
  60. target[name] = copy;
  61. }
  62. }
  63. }
  64. }
  65. // Return the modified object
  66. return target;
  67. };

1、该方法的作用是用一个或多个其他对象来扩展一个对象,返回被扩展的对象。
2、如果不指定target,则给jQuery命名空间本身进行扩展。这有助于插件作者为jQuery增加新方法。
3、如果第一个参数设置为true,则jQuery返回一个深层次的副本,递归地复制找到的任何对象;否则的话,副本会与原对象共享结构。 未定义的属性将不会被复制,然而从对象的原型继承的属性将会被复制。

拷贝的方法

浅拷贝 / 简单克隆的方法

一、简单克隆的2种方法:
1、创建新对象、遍历现有属性,复制新对象
2、Object.assign()

遍历 for…in

一、创建一个新对象,并通过遍历现有属性的结构,在原始类型值的层面,将其复制到新对象,以复制已有对象的结构。
【示例1】

  1. let user = {
  2. name: "John",
  3. age: 30
  4. };
  5. let clone = {}; // 新的空对象
  6. // 将 user 中所有的属性拷贝到其中
  7. for (let key in user) {
  8. clone[key] = user[key];
  9. }
  10. // 现在 clone 是带有相同内容的完全独立的对象
  11. clone.name = "Pete"; // 改变了其中的数据
  12. alert( user.name ); // 原来的对象中的 name 属性依然是 John

es6 的Object.assign()方法

一、Object.assign 方法可以把 任意多个的源对象所拥有的自身可枚举属性 拷贝给目标对象,然后返回目标对象。
1、对于访问器属性,该方法会执行那个访问器属性的 getter 函数,然后把得到的值拷贝给目标对象,如果你想拷贝访问器属性本身,请使用 Object.getOwnPropertyDescriptor() 和 Object.defineProperties() 方法;
2、字符串类型和 symbol 类型的属性都会被拷贝;
3、在属性拷贝过程中可能会产生异常,比如目标对象的某个只读属性和源对象的某个属性同名,这时该方法会抛出一个 TypeError 异常,拷贝过程中断,已经拷贝成功的属性不会受到影响,还未拷贝的属性将不会再被拷贝;
4、该方法会跳过那些值为 null 或 undefined 的源对象;
二、语法是:

  1. Object.assign(dest, [src1, src2, src3...])
  • 第一个参数dest是指目标对象。
  • 更后面的参数src1, …, srcN(可按需传递多个参数)是源对象。
  • 该方法将所有源对象的属性拷贝到目标对象dest中。换句话说,从第二个开始的所有参数的属性都被拷贝到第一个参数的对象中。
  • 调用结果返回dest。

【示例1】我们可以用它来合并多个对象:

  1. let user = { name: "John" };
  2. let permissions1 = { canView: true };
  3. let permissions2 = { canEdit: true };
  4. // 将 permissions1 和 permissions2 中的所有属性都拷贝到 user 中
  5. Object.assign(user, permissions1, permissions2);
  6. // 现在 user = { name: "John", canView: true, canEdit: true }

1、如果被拷贝的属性的属性名已经存在,那么它会被覆盖:

  1. let user = { name: "John" };
  2. Object.assign(user, { name: "Pete" });
  3. alert(user.name); // 现在 user = { name: "Pete" }

2、我们也可以用Object.assign代替for..in循环来进行简单克隆:

  1. let user = {
  2. name: "John",
  3. age: 30
  4. };
  5. let clone = Object.assign({}, user);

(1)它将user中的所有属性拷贝到了一个空对象中,并返回这个新的对象。
三、20210511 aSuncat:有说法是说“当对象中只有一级属性,没有二级属性的时候,此方法为深拷贝,但是对象中有对象的时候,此方法,在二级属性以后就是浅拷贝”,但我觉得,有能力进行递归复制的这个方法才叫深拷贝,即能实现二级属性的拷贝才叫深拷贝,所以Object.assign()是浅拷贝。

对象展开运算符…(ES2018)

一、将对象的所有可枚举属性拷贝到新构造的对象中,类似于Object.assign()

| 【示例】```javascript var obj1 = { foo: { a: 1, b: 2, }, x: 42 };

var clonedObj = { …obj1 };

clonedObj.foo.a = ‘testt’ console.log(obj1); / { foo: { a: ‘testt’, b: 2, }, x: 42 } /

  1. |
  2. | --- |
  3. <a name="zkJvV"></a>
  4. ### 数组展开运算符...(ES6)
  5. 一、展开语法和Object.assign()行为一致,执行的都是浅拷贝(即只遍历一层)
  6. | 【示例】a是多层数组,b只拷贝了第一层,对于第二层依旧和a持有同一个地址,所以对b的修改会影响到a```javascript
  7. var a = [[1], [2], [3]]
  8. var b = [...a];
  9. b.shift().shift(); // 1
  10. console.log(a)
  11. // [[], [2], [3]];

| | —- |

拷贝访问器属性本身:Object.defineProperties,Object.getOwnPropertyDescriptors

见属性标志和属性描述符#克隆对象https://www.yuque.com/tqpuuk/yrrefz/xg43a2

拷贝对象及原型链上的所有属性:Object.create

见Object方法#Object.create-克隆对象:https://www.yuque.com/tqpuuk/yrrefz/tx2i78#lZLUs

深拷贝 / 深层克隆

一、到现在为止,我们都假设user的所有属性均为原始类型。但属性可以是对其他对象的引用。那应该怎样处理它们呢?
【示例1】

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};
alert( user.sizes.height ); // 182

1、现在这样拷贝clone.sizes = user.sizes已经不足够了,因为user.sizes是个对象,它会以引用形式被拷贝。因此clone和user会共用一个 sizes:
【示例1】就像这样:

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

let clone = Object.assign({}, user);

alert( user.sizes === clone.sizes ); // true,同一个对象

// user 和 clone 分享同一个 sizes
user.sizes.width++;       // 通过其中一个改变属性值
alert(clone.sizes.width); // 51,能从另外一个看到变更的结果

二、为了解决此问题,我们应该使用会检查每个user[key]的值的克隆循环,如果值是一个对象,那么也要复制它的结构。这就叫“深拷贝”。

遍历 for…in

一、我们能想到的最常用的方法就是先创建一个空的新对象,然后递归遍历旧对象,直到发现基础类型的子节点才赋予到新对象对应的位置。
1、不过这种方法会存在一个问题,就是 JavaScript 中存在着神奇的原型机制,并且这个原型会在遍历的时候出现,然后需要考虑原型应不应该被赋予给新对象。那么在遍历的过程中,我们可以考虑使用 hasOwnProperty 方法来判断是否过滤掉那些继承自原型链上的属性。
二、实现代码

function cloneDeep(arr){
  var obj=arr.constructor == Array ? [] : {};
  //第二种方法 var obj=arr instanceof Array?[]:{}
  // 第三种方法 var obj = Array.isArray(source) ? [] : {}
  for(var item in arr){
    if(typeof arr[item]==="object"){
      obj[item]=cloneDeep(arr[item]);
    }else{
      obj[item]=arr[item];
    }
  }
  return obj;
}

1、用例

const obj1 = {
    name: 'John',
  sizes: {
      width: 100,
    height: 200,
  }
}

const obj2 = cloneDeep(obj1);
console.log(obj2);

(1)结果
image.png

jQuery的extend方法

let $ = require('jquery');
let obj1 = {
   a: 1,
   b: {
     f: {
       g: 1
     }
   },
   c: [1, 2, 3]
};
let obj2 = $.extend(true, {}, obj1);

JSON.stringify和JSON.parse

一、可以利用 JSON 进行忽略原型链的深拷贝
1、该方法会忽略掉值为 undefined 的属性以及函数表达式,但不会忽略值为 null 的属性。

var dest = JSON.parse(JSON.stringify(target));

lodash.cloneDeep()

let _ = require('lodash');
let obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
let obj2 = _.cloneDeep(obj1);
const obj1 = {
    name: 'johe',
  sizes: {
      width: 100,
    height: 200,
  }
}

const obj2 = cloneDeep(obj1);

https://www.jsdelivr.com/package/npm/lodash
【示例】_.cloneDeep()源码分析

规避原型链属性上的拷贝

obj.hasOwnProperty(key)

一、最常用的方式:

for (let key in targetObj) {
  if (targetObj.hasOwnProperty(key)) {
    // 相关操作
  }
}

二、缺点
1、遍历了原型链上的所有属性,效率不高;

Object.keys(obj)

二、只会返回参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名所组成的数组。

const keys = Object.keys(targetObj);
keys.map((key)=>{
  // 相关操作
});

Object.create

const obj = Object.create(null);
target.__proto__ = Object.create(null);
for (let key in target) {
  // 相关操作
}