搞不懂JS中赋值·浅拷贝·深拷贝的请看这里

koala 2019-7-14 Javascript

# 前言

为什么写拷贝这篇文章?同事有一天提到了拷贝,他说赋值就是一种浅拷贝方式,另一个同事说赋值和浅拷贝并不相同。 我也有些疑惑,于是我去 MDN 搜一下拷贝相关内容,发现并没有关于拷贝的实质概念,没有办法只能通过实践了,同时去看一些前辈们的文章总结了这篇关于拷贝的内容,本文也属于公众号【程序员成长指北】学习路线中【JS 必知必会】内容。

# 数据类型与堆栈的关系

# 基本类型与引用类型

  • 基本类型:undefined,null,Boolean,String,Number,Symbol

  • 引用类型:Object,Array,Date,Function,RegExp 等

# 存储方式

  • 基本类型:基本类型值在内存中占据固定大小,保存在栈内存中(不包含闭包中的变量)

  • 引用类型:引用类型的值是对象,保存在堆内存中。而栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址(引用),引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

注意:

  1. 闭包中的变量并不保存在栈内存中,而是保存在堆内存中。这一点比较好想,如果闭包中的变量保存在了栈内存中,随着外层中的函数从调用栈中销毁,变量肯定也会被销毁,但是如果保存在了堆内存中,内存函数仍能访问外层已销毁函数中的变量。看一段对应代码理解下:
function A() {
  let a = 'koala'
  function B() {
    console.log(a)
  }
  return B
}
1
2
3
4
5
6
7
  1. 本篇所讲的浅拷贝和深拷贝都是对于引用类型的,对于基础类型不会有这种操作。

# 赋值操作

# 基本数据类型复制

看一段代码

let a ='koala';
let b = a;
b='程序员成长指北';
console.log(a); // koala
1
2
3
4

基本数据类型复制配图:

结论:在栈内存中的数据发生数据变化的时候,系统会自动为新的变量分配一个新的之值在栈内存中,两个变量相互独立,互不影响的。

# 引用数据类型复制

看一段代码

let a = { x: 'kaola', y: 'kaola1' }
let b = a
b.x = '程序员成长指北'
console.log(a.x) // 程序员成长指北
1
2
3
4

引用数据类型复制配图:

结论:引用类型的复制,同样为新的变量 b 分配一个新的值,保存在栈内存中,不同的是这个变量对应的具体值不在栈中,栈中只是一个地址指针。两个变量地址指针相同,指向堆内存中的对象,因此 b.x 发生改变的时候,a.x 也发生了改变。

# 浅拷贝

# 浅拷贝定义:

不知道的 api 我一般比较喜欢看 MDN,浅拷贝的概念 MDN 官方并没有给出明确定义,但是搜到了一个函数 Array.prototype.slice,官方说它可以实现原数组的浅拷贝。 对于官方给的结论,我们通过两段代码验证一下,并总结出浅拷贝的定义。

  • 第一段代码:
var a = [1, 3, 5, { x: 1 }]
var b = Array.prototype.slice.call(a)
b[0] = 2
console.log(a) // [ 1, 3, 5, { x: 1 } ];
console.log(b) // [ 2, 3, 5, { x: 1 } ];
1
2
3
4
5

从输出结果可以看出,浅拷贝后,数组 a[0]并不会随着 b[0]改变而改变,说明 a 和 b 在栈内存中引用地址并不相同。

  • 第二段代码
var a = [1, 3, 5, { x: 1 }]
var b = Array.prototype.slice.call(a)
b[3].x = 2
console.log(a) // [ 1, 3, 5, { x: 2 } ];
console.log(b) // [ 1, 3, 5, { x: 2 } ];
1
2
3
4
5

从输出结果可以看出,浅拷贝后,数组中对象的属性会根据修改而改变,说明浅拷贝的时候拷贝的已存在对象的对象的属性引用。

  • 浅拷贝定义

通过这个官方的slice浅拷贝函数分析浅拷贝定义

新的对象复制已有对象中非对象属性的值和对象属性的引用。如果这种说法不理解换一种一个新的对象直接拷贝已存在的对象的对象属性的引用,即浅拷贝。

# 浅拷贝实例

# Object.assign

  • 语法:

    语法:Object.assign(target, ...sources)

ES6 中拷贝对象的方法,接受的第一个参数是拷贝的目标target,剩下的参数是拷贝的源对象sources(可以是多个)

  • 举例说明:
let target = {}
let source = { a: 'koala', b: { name: '程序员成长指北' } }
Object.assign(target, source)
console.log(target) // { a: 'koala', b: { name: '程序员成长指北' } }
source.a = 'smallKoala'
source.b.name = '程序员成长指北哦'
console.log(source) // { a: 'smallKoala', b: { name: '程序员成长指北哦' } }
console.log(target) // { a: 'koala', b: { name: '程序员成长指北哦' } }
1
2
3
4
5
6
7
8

从打印结果可以看出,Object.assign是一个浅拷贝,它只是在根属性(对象的第一层级)创建了一个新的对象,但是对于属性的值是对象的话只会拷贝一份相同的内存地址。

  • Object.assign 注意事项
  1. 只拷贝源对象的自身属性(不拷贝继承属性)
  2. 它不会拷贝对象不可枚举的属性
  3. undefinednull无法转成对象,它们不能作为Object.assign参数,但是可以作为源对象
Object.assign(undefined) // 报错
Object.assign(null) // 报错

let obj = { a: 1 }
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true
1
2
3
4
5
6
  1. 属性名为Symbol 值的属性,可以被 Object.assign 拷贝。

# Array.prototype.slice

这个函数在浅拷贝概念定义的时候已经进行了分析,看上文。

# Array.prototype.concat

  • 语法

    var new_array = old_array.concat(value1[, value2[, ...[, valueN]]]) 参数:将数组和/或值连接成新数组

  • 举例说明

let array = [{ a: 1 }, { b: 2 }]
let array1 = [{ c: 3 }, { d: 4 }]
let array2 = array.concat(array1)
array1[0].c = 123
console.log(array2) // [ { a: 1 }, { b: 2 }, { c: 123 }, { d: 4 } ]
console.log(array1) // [ { c: 123 }, { d: 4 } ]
1
2
3
4
5
6

Array.prototype.concat 也是一个浅拷贝,只是在根属性(对象的第一层级)创建了一个新的对象,但是对于属性的值是对象的话只会拷贝一份相同的内存地址。

# ...扩展运算符

  • 语法

    var cloneObj = { ...obj };

  • 举例说明

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

obj.b.c = 2
console.log(obj) //{a:2,b:{c:2}}
console.log(obj2) //{a:1,b:{c:2}}
1
2
3
4
5
6
7
8
9

扩展运算符也是浅拷贝,对于值是对象的属性无法完全拷贝成 2 个不同对象,但是如果属性都是基本类型的值的话,使用扩展运算符也是优势方便的地方。

补充说明:以上 4 中浅拷贝方式都不会改变原数组,只会返回一个浅拷贝了原数组中的元素的一个新数组。

# 自己实现一个浅拷贝

实现原理:新的对象复制已有对象中非对象属性的值和对象属性的引用,也就是说对象属性并不复制到内存。

  • 实现代码:
function cloneShallow(source) {
  var target = {}
  for (var key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      target[key] = source[key]
    }
  }
  return target
}
1
2
3
4
5
6
7
8
9
  • for in 与 hasOwnProperty 函数说明,怕有些人小伙伴可能不清楚具体内容

for in

for...in 语句以任意顺序遍历一个对象自有的、继承的、可枚举的、非 Symbol 的属性。对于每个不同的属性,语句都会被执行。

hasOwnProperty

语法:obj.hasOwnProperty(prop)
prop 是要检测的属性字符串名称或者Symbol

该函数返回值为布尔值,所有继承了 Object 的对象都会继承到 hasOwnProperty 方法,和 in 运算符不同,该函数会忽略掉那些从原型链上继承到的属性和自身属性。

# 深拷贝操作

说了赋值操作和浅拷贝操作,大家是不是已经能想到什么是深拷贝了,下面直接说深拷贝的定义。

# 深拷贝定义

深拷贝会另外拷贝一份一个一模一样的对象,从堆内存中开辟一个新的区域存放新对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

# 深拷贝实例

# JSON.parse(JSON.stringify())

JSON.stringify()是前端开发过程中比较常用的深拷贝方式。原理是把一个对象序列化成为一个 JSON 字符串,将对象的内容转换成字符串的形式再保存在磁盘上,再用 JSON.parse()反序列化将 JSON 字符串变成一个新的对象

  • 举例说明:
let arr = [
  1,
  3,
  {
    username: ' koala',
  },
]
let arr4 = JSON.parse(JSON.stringify(arr))
arr4[2].username = 'smallKoala'
console.log(arr4) // [ 1, 3, { username: 'smallKoala' } ]
console.log(arr) // [ 1, 3, { username: ' koala' } ]
1
2
3
4
5
6
7
8
9
10
11

实现了深拷贝,当改变数组中对象的值时候,原数组中的内容并没有发生改变。JSON.stringify()虽然可以实现深拷贝,但是还有一些弊端比如不能处理函数等。

  • JSON.stringify()实现深拷贝注意点
  1. 拷贝的对象的值中如果有函数,undefined,symbol 则经过 JSON.stringify()序列化后的 JSON 字符串中这个键值对会消失
  2. 无法拷贝不可枚举的属性,无法拷贝对象的原型链
  3. 拷贝 Date 引用类型会变成字符串
  4. 拷贝 RegExp 引用类型会变成空对象
  5. 对象中含有 NaN、Infinity 和-Infinity,则序列化的结果会变成 null
  6. 无法拷贝对象的循环应用(即 obj[key] = obj)

# 自己实现一个简单深拷贝

深拷贝,主要用到的思想是递归,遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。 实现代码:

//定义检测数据类型的功能函数
function isObject(obj) {
  return typeof obj === 'object' && obj != null
}
function cloneDeep(source) {
  if (!isObject(source)) return source // 非对象返回自身

  var target = Array.isArray(source) ? [] : {}
  for (var key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      if (isObject(source[key])) {
        target[key] = cloneDeep(source[key]) // 注意这里
      } else {
        target[key] = source[key]
      }
    }
  }
  return target
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

该简单深拷贝未考虑内容: 遇到循环引用,会陷入一个循环的递归过程,从而导致爆栈

// RangeError: Maximum call stack size exceeded
1

小伙伴们有没有什么好办法呢,可以写下代码在评论区一起讨论哦!

# 第三方深拷贝库

该函数库也有提供_.cloneDeep 用来做 Deep Copy(lodash 是一个不错的第三方开源库,有好多不错的函数,也可以看具体的实现源码)

var _ = require('lodash')
var obj1 = {
  a: 1,
  b: { f: { g: 1 } },
  c: [1, 2, 3],
}
var obj2 = _.cloneDeep(obj1)
console.log(obj1.b.f === obj2.b.f)
// false
1
2
3
4
5
6
7
8
9

# 拷贝内容总结

用一张图总结

今天就分享这么多,如果对分享的内容感兴趣,可以关注公众号「程序员成长指北」,或者加入技术交流群,大家一起讨论。

进阶技术路线

加入我们一起学习吧!

# 给我留言

关注作者公众

和万千小伙伴一起学习

加入技术交流群

扫描二维码 备注 加群