浅谈JavaScript数据类型与深浅拷贝的实现

一.基本数据类型与引用数据类型

     JavaScript的数据类型分为基本数据类型和引用数据类型。基本数据类型有六种,分别是Number、String、Boolean、Null、 Undefined以及es6新增的Symbol。基本数据类型是直接存放在栈中的简单数据段,单独分配内存空间,可以按值访问。引用数据类型(Object)的值由于大小不固定且由于堆结构的存储空间比较灵活,因此存放在堆内存(相当于一棵完全二叉树)中,按引用访问。所谓按引用访问就是我们不能直接操作堆内存中的值,在操作对象时,实际操作的是对象的引用而不是存在堆中的值。所谓引用,可以理解为一个指针。该指针存储了与堆中的每个值相对应的一个地址,通过该地址我们可以找到这个值。

二.变量的复制

    基本类型的复制,系统会为新声明的变量分配内存,这意味着赋值完成后,复制与被复制的变量除了值一样外,毫无关系。 而引用类型则不同,其复制只是引用的复制,即新的值也是一个指针,它同样指向堆内存中的值。两个指针尽管相互独立,但他们指向的值却是一样的。因此复制与被复制的对象会产生关联,即当通过其中一个指针改变堆内存中的值,另一个指针的值也会发生变化。下面通过一个例子印证一下。

let a = 2
let b = a
b = 3
console.log(a,b) //2,3
let obj1 = {
    name:'小明',
    age:18
}
let obj2 = obj1
obj2.name = '小红'
console.log(obj1) // { name: '小红', age: 18 } */

可以看到,对引用类型直接进行复制,复制与被复制的对象会产生关联。那如何能让他们彼此独立呢?可以通过拷贝来完成。

三.浅拷贝与深拷贝

1 浅拷贝
1.1 什么是浅拷贝

      创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。什么意思呢?可以概括为两点:
1.浅拷贝会在内存中创建一个新对象,这就区别于对象的直接赋值。直接赋值只是引用的赋值,不会创建新对象
2.如果被浅拷贝的对象的属性全是基本类型,那么拷贝与被拷贝的对象之间不会产生关联。如果属性中有引用类型,则会产生关联。即浅拷贝只会对对象的第一层级进行拷贝。后续层级则还是引用的复制

1.2 浅拷贝的实现方法

常见的方法有

  1. Object.assign()
  2. 展开运算符
  3. Array.slice()
  4. Array.concat()
    下面以Object.assign()为例说明浅拷贝
const obj = {
  a:1,
  b:{
    c:2
  }
}
let target = {}
Object.assign(target,obj)
console.log(target) // { a: 1, b: { c: 2 } }
obj.a = 11
obj.b.c = 3
console.log(target) // { a: 1, b: { c: 3 } }

      需要注意的是Object.assign()不会拷贝对象继承的属性,不会拷贝不可枚举的属性(Object.defineProperty()中设置enumerable为false),可以拷贝Symbol类型。

1.3 手动实现浅拷贝

   思路:1.判断传入的对象类型 2.创建对象 3.循环赋值

// 判断是不是object类型
function isObject(obj) {
  // null、对象、数组返回的都是object类型
  return typeof obj === "object" && obj !== null;
}
function shallowClone (obj){
  if (!isObject(obj)) { // 该方法判断是不是引用类型
    throw new Error('obj 不是一个对象!')
  }
  const _obj = Array.isArray(obj)? []:{}
  // 使用Reflect.ownKeys可以访问symbol类型
  Reflect.ownKeys(obj).forEach(key => {
    _obj[key] = obj[key] 
  })
  return _obj
}

let obj1 = {
  a: 1,
  b: {
    c: 2,
  },
  f: function () {
    console.log("hello");
  },
};
let sym = Symbol('Symbol')
obj1[sym] = 111

let obj2 = shallowClone(obj1);
obj2.a = 3
obj2.b.c = 4
console.log(obj2[sym]) // 111
console.log(obj2.a,obj1.a) // 3,1
obj2.f() // hello
console.log(obj1.b.c) // 4 修改了obj2的该属性,obj1该属性也一起变化

可以看到,经过浅拷贝得到的对象的第一层级与原对象不会产生关联。
且Symbol类型,函数类型的属性都可以拷贝。但无法拷贝引用类型

2 深拷贝
2.1 什么是深拷贝

     顾名思义,无论对象的属性是基本类型还是引用类型,他都会将这个对象从堆内存中完整的拷贝出来。并在堆内存中开辟一个新的区域来存放拷贝出来的对象。

2.2 深拷贝的实现方法

2.2.1 序列化+反序列化法。
   丐版深拷贝,使用JSON对象的parse和stringify方法来实现深拷贝。这也是开发中经常使用的拷贝方法。思路和实现过程比较简单。

function DeepClone(obj) {
  return JSON.parse(JSON.stringify(obj))
}
let obj1 = {
  a:1,
  b:{
    c:2
  }
}
let obj2 = DeepClone(obj1)

obj1.b.c = 4
console.log(obj2) // { a: 1, b: { c: 2 } }

这种方法缺陷比较明显,具体如下
1.只能拷贝对象和数组。拷贝Date引用类型会变成字符串,拷贝RegExp引用类型会变成空对象。
2.拷贝的对象的值中如果有函数,undefined,symbol则经过序列化后的JSON字符串中这个键值对会消失。
3.无法拷贝对象的循环引用。

2.2.2.循环对象+递归拷贝
       前面说到,浅拷贝会在内存中创建一个新对象,并拷贝对象的第一层级(基本类型)。那么深拷贝无非就是在遇到引用类型时进行递归拷贝即可,同时要在函数开头进行判断,基本类型直接返回,引用类型则进行递归拷贝。

function DeepClone(obj) {
  if (!isObject(obj)) {
    // 非引用类型 直接返回
    return obj;
  }
  const _obj = Array.isArray(obj) ? [] : {};
  Reflect.ownKeys(obj).forEach((key) => {
    // 引用类型,递归拷贝
    _obj[key] = DeepClone(obj[key]);
  });
  return _obj;
}
let obj1 = {
  a: 1,
  b: {
    c: 2,
  },
  f: function () {
    console.log("hello");
  },
};
let obj2 = DeepClone(obj1);
obj2.a = 3;
obj2.b.c = 4;
console.log(obj2.a, obj1.a); // 3,1 
console.log(obj1.b.c); // 2  

   可以看到通过这种方法拷贝得到的对象,与原对象无论是基本类型还是引用类型,都不会产生关联。但此时的拷贝方法还不够完善,它仍然无法解决循环引用的问题。举个例子:

let obj1 = {
  a: 1,
  b:[1,2],
  c:{
    d:3
  }
};

obj1.b.push(obj1.c)
obj1.c.e = obj1.b
let obj2 = DeepClone(obj1);
console.log(obj2)

上面例子中,obj1的属性b和c相互引用。我们对其进行深拷贝,来看一下控制台输出。

栈溢出.jpg

栈溢出,显然这是由于无限递归造成的。仔细分析一下上述深拷贝代码不难找出原因。每遇到一个引用类型就会递归执行函数,而两个引用类型又相互引用,因此递归会在两个引用类型之间无限执行。
清楚了原因,问题也就迎刃而解。我们只需记住已经拷贝过的属性,当再次遇到该属性时,直接返回该属性而不进行递归。这种思路类似于去重,因此我们可以用字典解决该问题。

// 默认传入一个空字典
function DeepClone(obj, map = new Map()) {
  if (!isObject(obj)) {
    return obj;
  }
  const _obj = Array.isArray(obj) ? [] : {};
  // 之前已经拷贝过该属性 直接返回 避免循环递归
  if (map.has(obj)) return map.get(obj);
  // 未拷贝过 添加到字典中
  map.set(obj, _obj);
  Reflect.ownKeys(obj).forEach((key) => {
   // 每次递归调用时传入该字典
    _obj[key] = DeepClone(obj[key], map);
  });
  return _obj;
}

解决循环引用.jpg

   通过控制台打印结果看到,成功对该对象进行了拷贝且实现了引用类型属性的循环引用。
   此时的深拷贝仍然有待完善,我们无法拷贝原型链上的属性,这是因为Reflect.ownKeys方法不能获取对象原型链上的属性,因此也就无法对其拷贝。我们知道for..in循环能获取原型链的属性。但又不能拷贝Symbol。这里我们可以使用Object.getOwnPropertySymbols()方法来获取对象的Symbol类型的属性。

接下来继续完善。

function DeepClone(obj, map = new Map()) {
  if (!isObject(obj)) {
    return obj;
  }
  let _obj = Array.isArray(obj) ? [] : {};
  if (map.has(obj)) return map.get(obj);
  map.set(obj, _obj);
  // 获取源对象所有的 Symbol 类型键
  let symKeys = Object.getOwnPropertySymbols(obj);
  // 拷贝 Symbol 类型键对应的属性
  if (symKeys.length) {
    symKeys.forEach((symKey) => {
      _obj[symKey] = DeepClone(obj[symKey], map);
    });
  }
  // 拷贝可枚举属性(包括原型链上的)
  for (let key in obj) {
    _obj[key] = DeepClone(obj[key], map);
  }
  return _obj;
}

let sym = Symbol("Symbol");
let obj1 = {
  name: "xiaom",
};
let obj2 = {
  a: 1,
};
obj2[sym] = "aaa";
// 将obj1接入obj2的原型链
Object.setPrototypeOf(obj2, obj1);



let cloneObj = DeepClone(obj2);
console.log(cloneObj)// { a: 1, name: 'xiaom', [Symbol(Symbol)]: 'aaa' }
console.log(cloneObj.name); // xiaom

   可以看到,无论是Symbol类型还是原型链上的属性均可以实现拷贝。但仍然要说明,这种方法也只能实现对象和数组的深拷贝。对于其他引用类型如RegExp等,由于他们的构造函数比较特殊,该方法无法拷贝。

2.2.3 Lodash
    Lodash是一个强大的JS工具函数库,其cloneDeep深拷贝方法支持多种引用类型的拷贝。

源码地址,欢迎star😀:
https://github.com/eyzqdm/Javascript-basics/blob/master/deepClone.js
参考:
https://www.bilibili.com/video/BV1qE411K7tS?p=5

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容