◆

JavaScript 浅拷贝与深拷贝详解及实现

2023-02-02 01:370

在 JavaScript 中,我们将数据分为 基本数据类型(原始值) 与 引用类型

  • 基本数据类型的值是按值访问的,基本类型的值是不可变的
  • 引用类型的值是按引用访问的,引用类型的值是动态可变的
var fx = 100;
var fx1 = 100;
console.log(fx === fx1); // true
var fx2 = { a: 1, b: 2 };
var fx3 = { a: 1, b: 2 };
console.log(fx2 === fx3); // false 两个不同的对象
  • 基本数据类型的比较是值得比较
  • 引用类型的比较是引用地址的比较

鉴于以上数据类型的特点,我们可以初步想到:所谓 浅拷贝 与 深拷贝 可能就是对于值的拷贝和引用的拷贝(基本数据类型都是对值的拷贝,不进行区分)。一般来说,我们所涉及的拷贝对象,也都是针对引用类型的。

  • 浅拷贝是拷贝一层,深层次的对象级别的就拷贝引用;深拷贝是拷贝多层,每一级别的数据都会拷贝出来;
  • 浅拷贝和深拷贝都只针对于引用数据类型,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存;但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象;
  • 区别:浅拷贝只复制对象的第一层属性、深拷贝可以对对象的属性进行递归复制;
浅拷贝和深拷贝
var fxArr = ['One', 'Two', 'Three'];
var fxArrs = fxArr;
fxArrs[1] = 'love';
// 由于是赋值所以fxArr的值也发生改变
console.log(fxArr); // ["One", "love", "Three"]
console.log(fxArrs); // ["One", "love", "Three"]

浅拷贝:

对对象进行浅层次的复制,只复制一层对象的属性,并不包括对象里面的引用类型数据。

数组的浅拷贝:

解决方法一:数组的 slice 方法

var fxArr = ['One', 'Two', 'Three'];
var fxArrs = fxArr.slice(0);
fxArrs[1] = 'love';
console.log(fxArr); // ["One", "Two", "Three"]
console.log(fxArrs); // ["One", "love", "Three"]

解决方法二:数组的 concat 方法

var fxArr = ['One', 'Two', 'Three'];
var fxArrs = fxArr.concat();
fxArrs[1] = 'love';
console.log(fxArr); // ["One", "Two", "Three"]
console.log(fxArrs); // ["One", "love", "Three"]

解决方法三:ES6 的扩展运算符...

var fxArr = ['One', 'Two', 'Three'];
var fxArrs = [...fxArr];
fxArrs[1] = 'love';
console.log(fxArr); // ["One", "Two", "Three"]
console.log(fxArrs); // ["One", "love", "Three"]

对象的浅拷贝

// 只复制第一层的浅拷贝
function simpleCopy(obj1) {
  var obj2 = Array.isArray(obj1) ? [] : {};
  for (let i in obj1) {
    obj2[i] = obj1[i];
  }
  return obj2;
}

var fxObj = {
  age: 18,
  nature: ['smart', 'good'],
  names: {
    name1: 'fx',
    name2: 'xka',
  },
  love: function () {
    console.log('fx is a great girl');
  },
};
var newFxObj = simpleCopy(fxObj);
newFxObj.age = 8;
newFxObj.nature.push('why');
newFxObj.names.name1 = 'why fx';
newFxObj.love = function () {
  console.log('fx is 18 years old');
};
console.log(fxObj.age); // 18 基本数据类型不会改变
console.log(fxObj.nature); //  ["smart", "good", "why"] 引用类型会改变
console.log(fxObj['names']); // {name1: "why fx", name2: "xka"} 引用类型会改变
console.log(fxObj['love']); // ƒ () {console.log('fx is a great girl')}

第二种方法:Object.assign 方法(只能处理深度只有一层的对象)

var fxObj = {
  age: 18,
  nature: ['smart', 'good'],
  names: {
    name1: 'fx',
    name2: 'xka',
  },
  love: function () {
    console.log('fx is a great girl');
  },
};
var newFxObj = Object.assign({}, fxObj);
newFxObj.age = 8;
newFxObj.nature.push('why');
newFxObj.names.name1 = 'why fx';
newFxObj.love = function () {
  console.log('fx is 18 years old');
};
console.log(fxObj.age); // 18
console.log(fxObj.nature); //  ["smart", "good", "why"]
console.log(fxObj['names']); // {name1: "why fx", name2: "xka"}
console.log(fxObj['love']); // ƒ () {console.log('fx is a great girl')}

方法三:ES6 的对象扩展方法 var newFxxObj = {...fxObj}

var fxObj = {
  age: 18,
  nature: ['smart', 'good'],
  names: {
    name1: 'fx',
    name2: 'xka',
  },
  love: function () {
    console.log('fx is a great girl');
  },
};
var newFxObj = { ...fxObj };

newFxObj.age = 8;
newFxObj.nature.push('why');
newFxObj.names.name1 = 'why fx';
newFxObj.love = function () {
  console.log('fx is 18 years old');
};
console.log(fxObj.age); // 18
console.log(fxObj.nature); //  ["smart", "good", "why"]
console.log(fxObj['names']); // {name1: "why fx", name2: "xka"}
console.log(fxObj['love']); // ƒ () {console.log('fx is a great girl')}

例子:

var person = {
  name: 'tt',
  age: 18,
  friends: ['oo', 'cc', 'yy'],
};

function shallowCopy(source) {
  if (!source || typeof source !== 'object') {
    throw new Error('error');
  }
  var targetObj = source.constructor === Array ? [] : {};
  for (var keys in source) {
    if (source.hasOwnProperty(keys)) {
      targetObj[keys] = source[keys];
    }
  }
  return targetObj;
}

var p1 = shallowCopy(person);

console.log(p1);
  • 在上面的代码中,我们创建了一个 shallowCopy 函数,它接收一个参数也就是被拷贝的对象。
  • 首先创建了一个对象
  • 然后 for...in 循环传进去的对象,为了避免循环到原型上面会被遍历到的属性,使用 hasOwnProperty 限制循环只在对象自身,将被拷贝对象的每一个属性和值添加到创建的对象当中
  • 最后返回这个对象

通过测试,我们拿到了和 person 对象几乎一致的对象 p1。看到这里,你是不是会想那这个结果和 var p1 = person 这样的赋值操作又有什么区别呢?

var p2 = person;

// 这个时候我们修改person对象的数据
person.name = 'tadpole';
person.age = 19;
person.friends.push('tt');

p2.name; // tadpole
p2.age; // 19
p2.friends; // ["oo", "cc", "yy", "tt"]

p1.name; // tt
p1.age; // 18
p1.friends; // ["oo", "cc", "yy", "tt"]

上面我们创建了一个新的变量 p2 ,将 person 赋值给 p2 ,然后比较两个变量 浅拷贝和深拷贝

深拷贝

浅拷贝由于只是复制一层对象的属性,当遇到有子对象的情况时,子对象就会互相影响。所以,深拷贝是对对象以及对象的所有子对象进行拷贝

  • 方法一:用 JSON.stringify 把对象转成字符串,再用 JSON.parse 把字符串转成新的对象。 缺点:
    1. 会忽略 undefined
    1. 会忽略 symbol
    1. 不能序列化函数,,无法拷贝函数
    1. 不能解决循环引用的对象 const a = {val:2}; a.target = a; 拷贝 a 会出现系统栈溢出,因为出现了无限递归的情况
    1. 不能正确处理 RegExp, Date, Set, Map 等
    1. 不能处理正则
    1. 会抛弃对象的 constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成 Object。
var fxObj = {
  age: 18,
  why: undefined,
  why1: Symbol('why1'),
  nature: ['smart', 'good'],
  names: {
    name1: 'fx',
    name2: 'xka',
  },
  love: function () {
    console.log('fx is a great girl');
  },
};
var newFxObj = JSON.parse(JSON.stringify(fxObj));
newFxObj.age = 8;
newFxObj.nature.push('why');
newFxObj.names.name1 = 'why fx';

newFxObj.love = function () {
  console.log('fx is 18 years old');
};
console.log(fxObj.age); // 18
console.log(fxObj.nature); // ["smart", "good"]
console.log(fxObj['names']); // {name1: "fx", name2: "xka"}
console.log(fxObj['love']); // ƒ () {console.log('fx is a great girl')}
console.log(newFxObj['love']); // undefined function没办法转成JSON。
console.log(newFxObj); // {age: 8, nature: Array(3), names: Object, love: function} why why1 love 都会被忽略

循环引用情况下,会报错。

let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  },
};
obj.a = obj.b;
obj.b.c = obj.a;

let b = JSON.parse(JSON.stringify(obj));
// Uncaught TypeError: Converting circular structure to JSON
  • 方法二:循环递归
function deepClone(initalObj, finalObj) {
  var obj = finalObj || {};
  for (var i in initalObj) {
    var prop = initalObj[i]; // 避免相互引用对象导致死循环
    if (prop === obj) {
      continue;
    }
    if (typeof prop === 'object') {
      obj[i] = prop.constructor === Array ? [] : {};
      arguments.callee(prop, obj[i]);
    } else {
      obj[i] = prop;
    }
  }
  return obj;
}

var fxObj = {
  age: 18,
  nature: ['smart', 'good'],
  names: {
    name1: 'fx',
    name2: 'xka',
  },
  love: function () {
    console.log('fx is a great girl');
  },
};
var newFxObj = deepClone(fxObj);
newFxObj.age = 8;
newFxObj.names.name1 = 'newfx';
console.log(fxObj);
console.log(newFxObj);

输出 浅拷贝和深拷贝

  • 方法三:jquery 和 zepto 里的 $.extend 方法可以用作深拷贝
var $ = require('jquery');
var newObj = $.extend(true, {}, obj);

例子:

function deepCopy(source) {
  if (!source || typeof source !== 'object') {
    throw new Error('error');
  }
  var targetObj = source.constructor === Array ? [] : {};
  for (var keys in source) {
    if (source.hasOwnProperty(keys)) {
      if (source[keys] && typeof source[keys] === 'object') {
        targetObj[keys] = source[keys].constructor === Array ? [] : {};
        targetObj[keys] = deepCopy(source[keys]);
      } else {
        targetObj[keys] = source[keys];
      }
    }
  }
  return targetObj;
}

var obj1 = {
  arr: [1, 2, 3],
  key: {
    id: 22,
  },
  func: function () {
    console.log(123);
  },
};

var obj2 = deepCopy(obj1);

obj1.arr.push(4);

obj1.arr; // [1, 2, 3, 4]
obj2.arr; // [1, 2, 3]
obj1.key === obj2.key; // false
obj1.func === obj2.func; // true

对于深拷贝的对象,改变源对象不会对得到的对象有影响。只是在拷贝的过程中源对象的方法丢失了,这是因为在序列化 JavaScript 对象时,所有函数和原型成员会被有意忽略

💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVEJavaScript 浅拷贝与深拷贝详解及实现加载中