JavaScript从根源了解深浅拷贝问题

变量

JavaScript变量是松散类型的,而且变量不过就是特定时间点一个特定值的名称而已。

原始值&引用值

ECMAScript变量有原始值和引用值两种类型。原始值(primitive value)就是最简单的数据(如Number、String、Undefined等6种),引用值(reference value)则是由多个值构成的对象(如:Object、Array等)。

保存原始值的变量是按值访问的,我们所操作的就是存储在变量中实际的值。

let a = 10;
let b = a;
console.log(a,b);//10 10
b=30;
console.log(a,b);//10 30
复制代码

保存引用值的变量是按引用访问的。引用值是保存在内存中的对象。实际上操作的是对该对象的引用,而非实际的对象本身。我们可以看下面的例子,假设我们认为操作对象是操作其本身,那么我们可以理解为:系统复制一份一样的a的变量赋值给b,两个互不影响,这不难理解。但其实不是,a和b共享这个变量,它们引用同一块内存,可以理解为C语言中的指针。

let a = {};
let b = null;
b=a;
console.log(b===a)//true
复制代码

动态属性

对于引用值而言,我们可以动态的添加、修改或删除其属性,而原始值不能有属性,但是也有例外

原始值不能有属性,若为其添加属性也不会报错,但是无效。

但是,原始类型的变量初始化有两种形式:字面量和new关键字,若采用new关键字,则会创建一个Object类型的实例,但其行为类似原始值,也就是说:功能上是原始值,本质是一个对象

//原始值字面量
let name = 'zs';
name.age = 10;
console.log(name.age);//undefined

//原始值new关键字
let name = new String("zs");
console.log(name, typeof name);//[String: 'zs'] object
name.age = 18;
console.log(name);//[String: 'zs'] { age: 18 }

//引用值
let person = {};
person.name = 'wn';
person.age = 18;
console.log(person.name,person.age);//wn18
复制代码

复制值

对于原始值赋值到另一个变量时,原始值会被复制一份,这两个变量可以独立使用,互不干扰

对于引用值,复制的实际上是一个指针,即两个变量指向同一个对象

//原始值
let a = 10;
let b = a;
console.log(a,b);//10 10
b=30;
console.log(a,b);//10 30

//引用值
let zs = {name: 'zs'};
let person = zs;
console.log(person.name);//zs
zs.name='zss';
console.log(person.name);//zss
console.log(zs.name);//zss
复制代码

传递参数

ECMAScript中所有的函数参数都是按值传递的。函数外的值会被复制到函数内部的参数中。若是原始值,就跟原始值变量的复制一样,若是引用值,那么就跟引用值变量的复制一样,那这也意味着在函数中对对象的修改会反映到函数外部。

function setNum(num) {
  num=20;
}
function setName(person) {
  person.name='zss';
}
let num=10;
let zs = {name: 'zs'};
setName(zs);
setNum(num);
console.log(num);//10
console.log(zs);//{ name: 'zss' }
复制代码

引用传递参数时,在函数内部对对象属性的修改会反映到函数外部,因为它们指向的是同一个对象。但是函数参数是按值传递的,对像的传递可以理解为把指向当作值传入函数内部,我们不能改变引用

注意:函数中参数就是局部变量,意味着函数执行结束便会被回收。

var person = {name: 'zs'};

function setName(person) {
  person.name = 'zss';
  console.log(person === window.person)//true
  person = {name: 'ls'};
  console.log(person === window.person)//false
  console.log(person,window.person);//{name: "ls"} {name: "zss"}
}

setName(person);
console.log(person);//{name: "zss"}
复制代码

这个例子利用var关键字在全局声明的属性会挂载到window对象上这一特性,我们来比较函数参数和原始的对象有什么不同。由上例person===window.personfalse可以看出,外部原始对象的引用并未改变,只是函数参数的指向变了。因为参数在函数内部也是局部变量,在函数执行完毕后销毁,所以函数内部的person最后也就被回收了。下图反应了变量的指向改变过程一目了然。

微信图片_20210805225118.jpg

确定类型

typeof用来判断一个变量是否为undefinedstringnumberboolean的最好方式。尤其注意typeofnull值的判断为object

typeof原始值很有用,但是对引用值用处不大,我们不关心它的类型是否为object,而更想知道是什么类型的对象。instanceof操作符的出现,实现了这个需求。

//typeof对原始值的判断
let n = 10, str = 'a', bool = false, undef;
console.log(typeof n);//number
console.log(typeof str);//string
console.log(typeof bool);//boolean
console.log(typeof undef);//undefined
console.log(typeof null);//object

//instanceof对引用值的判断
class Person {}
let zs=new Person();
let obj={};

console.log(zs instanceof Person);//true
console.log(Person[Symbol.hasInstance](zs));//true 这个写法与使用instanceof操作符效果一致
console.log(obj instanceof Person);//false
复制代码

深浅拷贝

深浅拷贝主要针对对象和数组。通俗来说,浅拷贝进行复制时,只遍历了一层,若这一层内有对象,则还会存在引用关系。深拷贝则递归一直到原始值,对原始值的复制,则是两个独立的、互不影响的值。我们常见的浅拷贝有:解构赋值(...展开运算符)、Object.assign()等。

//解构赋值的浅拷贝
let obj1 = {a: 1, b: 2, arr: [1, 2, 3]};
let obj2 = {...obj1};
let obj3 = Object.assign(obj1, obj2);
obj2.a = 10;
obj2.arr[0] = 10;
console.log(obj1);//{ a: 1, b: 2, arr: [ 10, 2, 3 ] }
console.log(obj2);//{ a: 10, b: 2, arr: [ 10, 2, 3 ] }
console.log(obj3);//{ a: 1, b: 2, arr: [ 10, 2, 3 ] }
复制代码

通过上述代码我们不难理解浅拷贝的概念,第一层的属性则是独立的,而obj.arr则是引用了同一个对象。

深拷贝的基本思路就是递归至原始值,再进行复制。JavaScript原生没有给我们提供深拷贝的功能函数,我们可以自己实现一个:

function deepClone(obj) {

  let keys = null, newObj = null;
  //对object和arr分别处理
  if (Array.isArray(obj)) {
    //生成数组的keys,即下标
    keys = [...new Array(obj.length)].map((_, i) => i);
    newObj = [];
  } else {
    //获取对象的key
    keys = Object.getOwnPropertyNames(obj);
    newObj = {};
  }
  for (let i = 0, len = keys.length; i < len; i++) {
    let key = keys[i];
    let val = obj[key];
    //若不是原始值则递归
    if (typeof val === "object")
      newObj[key] = deepClone(val);
    else newObj[key] = val;
  }
  return newObj;
}

let obj1 = {
  a: 1,
  b: 2,
  c: {
    d: 3,
    e: 4,
    arr: [1, 2, [3, 4]]
  }
}
let obj2 = deepClone(obj1);
obj2.a = 10;
obj2.c.d = 30;
obj2.c.arr[0] = 10;
obj2.c.arr[2][0] = 30;
console.log(obj1);
//{ a: 1, b: 2, c: { d: 3, e: 4, arr: [ 1, 2, [ 3, 4 ] ] } }
console.log(obj2);
//{ a: 10, b: 2, c: { d: 30, e: 4, arr: [ 10, 2, [ 30, 4 ] ] } }
复制代码

我们可以看出,deepClone()函数已经能够满足我们的需求了,但是我们会遇到一个新的问题,假如我们的对象是自定义对象呢?如下代码所示:

class Person {
  constructor(name = '') {
    this.name = name;
  }

  setName(name) {
    this.name = name;
  }
}

let obj1 = {
  a: 1,
  b: 2,
  person: new Person('zs')
}
let obj2 = deepClone(obj1);
console.log(obj1);//{ a: 1, b: 2, person: Person { name: 'zs' } }
console.log(obj1.person.setName)//[Function: setName]
console.log(obj2);//{ a: 1, b: 2, person: { name: 'zs' } }
console.log(obj2.person.setName)//undefined
复制代码

从结果看出,我们的自定义Person类包含的方法并没有被复制,原因是Object.getOwnPropertyNames(obj)并没有获取到类的方法,我们可以这样解决:

  //对object和arr分别处理
  if (Array.isArray(obj)) {
    //生成数组的keys,即下标
    keys = [...new Array(obj.length)].map((_, i) => i);
    newObj = [];
  } else {
    //获取对象的key
    keys = Object.getOwnPropertyNames(obj);
    //维持原有原型链
    newObj = new obj.constructor();//通过实例化一个新的对象,来保留对象的原型链
  }
复制代码

下面是完整的函数以及测试数据:

function deepClone(obj) {
  //特殊处理
  if (obj === null) return null
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  if (typeof obj !== "object") return obj;
  //对object和arr分别处理
  let keys = null, newObj = null;
  if (Array.isArray(obj)) {
    //生成数组的keys,即下标
    keys = [...new Array(obj.length)].map((_, i) => i);
    newObj = [];
  } else {
    //获取对象的key
    keys = Object.getOwnPropertyNames(obj);
    //维持原有原型链
    newObj = new obj.constructor();
  }
  for (let i = 0, len = keys.length; i < len; i++) {
    let key = keys[i];
    let val = obj[key];
    //若不是原始值则递归
    if (typeof val === "object")
      newObj[key] = deepClone(val);
    else newObj[key] = val;
  }
  return newObj;
}
class A {
  constructor(val = null) {
    this.val = val;
  }

  setVal(val) {
    this.val = val;
  }
}

let obj1 = {
  a: 1,
  c: {
    d: 4
  },
  person: new A('obj1'),
  arr: [1, [2, 3, 4]]
};
let obj2 = deepClone(obj1);
obj1.arr[0] = 10;
obj1.arr[1][0] = 20;
obj2.person.val = 'obj2'
obj1.person.setVal(666);
obj1.person.addtionFunc = function () {}

console.log(obj1)
console.log(obj2)
/**
obj1
{
  a: 1,
  c: { d: 4 },
  person: A { val: 666, addtionFunc: [Function (anonymous)] },
  arr: [ 10, [ 20, 3, 4 ] ]
}
obj2
{
  a: 1,
  c: { d: 4 },
  person: A { val: 'obj2' },
  arr: [ 1, [ 2, 3, 4 ] ]
}
*/
复制代码

至此一个能实现基本需求的深拷贝函数已经完成了。还有一种简便的方法,通过JSON序列化来实现深拷贝。本质上也是原始值的复制,因为经过序列化后,对象称为JSON字符串,为原始值,所以相当于原始值的复制。但是有个缺陷,经过JSON序列化的对象,会失去其所有的方法和原型链:

class Person {
  constructor(name = '') {
    this.name = name;
  }
}

let obj1 = {
  a: 1, b: 2, func() {
  }, person: new Person('zs')
};

console.log(obj1);
//{ a: 1, b: 2, func: [Function: func], person: Person { name: 'zs' } }
console.log(typeof JSON.stringify(obj1));
//string
console.log(obj1.person instanceof Person);
//true
let obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj2);
//{ a: 1, b: 2, person: { name: 'zs' } }
console.log(obj2.person instanceof Person);
//false
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享