Wang's blog Wang's blog
首页
  • 前端文章

    • HTML教程
    • CSS
    • JavaScript
  • 前端框架

    • Vue
    • React
    • VuePress
    • Electron
  • 后端技术

    • Npm
    • Node
    • TypeScript
  • 编程规范

    • 规范
  • 我的笔记
  • Git
  • GitHub
  • VSCode
  • Mac工具
  • 数据库
  • Google
  • 服务器
  • Python爬虫
  • 前端教程
更多
收藏
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Wang Mings

跟随大神,成为大神!
首页
  • 前端文章

    • HTML教程
    • CSS
    • JavaScript
  • 前端框架

    • Vue
    • React
    • VuePress
    • Electron
  • 后端技术

    • Npm
    • Node
    • TypeScript
  • 编程规范

    • 规范
  • 我的笔记
  • Git
  • GitHub
  • VSCode
  • Mac工具
  • 数据库
  • Google
  • 服务器
  • Python爬虫
  • 前端教程
更多
收藏
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • CSS

  • Npm

  • Vue

  • HTML

  • Node

  • Yaml

  • React

  • 框架

  • 规范

  • Electron

  • JS演示

  • VuePress

  • JavaScript

    • DOM
    • 事件
    • 实用API
    • 作用域
    • 基础篇
    • 定时器
    • 内存管理
    • 异步操作
    • 日期处理
    • 点击约束
    • 防抖节流
    • this的学习
    • ES面向对象
    • JS事件详解
    • new命令原理
    • 内置对象篇
    • 函数柯里化
    • 正则表达式
    • 浏览器模型
    • 事件订阅发布
    • 获取页面高度
    • 面向对象编程
    • JS随机打乱数组
    • js各种开发技巧
    • 函数的深入浅出
    • 防抖与节流函数
    • 模块化的使用总结
    • JS获取和修改url参数
    • JS设计模式总结笔记
    • fetch拦截和封装使用
    • 浏览器渲染原理流程
    • JavaScript对象从浅入深
    • JavaScript高级程序设计
    • Promise使用和实现方法
    • JS的种打断点调试方式
    • js深浅拷贝详解与实现
      • JS中数据类型
      • 深浅拷贝
      • 什么是浅拷贝?
      • 浅拷贝实现方式
        • 1.= 赋值。
        • 2. 数组的concat() 或者 slice()
        • 3.对象的Object.assign()
        • 4.封装个浅拷贝函数(支持数组和对象)
        • 5. 使用ES6扩展运算符 ...
      • 真正深拷贝的实现
        • 1. JSON实现深拷贝
        • 封装个递归深拷贝
    • 九种跨域方式实现原理
    • 多种数组去重性能对比
    • 闭包原理以及使用场景
    • 判断是否为移动端浏览器
    • 比typeof运算符更准确的类型判断
    • 搞清事件循环、宏任务、微任务
    • 将一维数组按指定长度转为二维数组
  • TypeScript

  • 微信小程序

  • TypeScript-axios

  • 前端
  • JavaScript
wangmings
2022-07-19
目录
JS中数据类型
深浅拷贝
什么是浅拷贝?
浅拷贝实现方式
1.= 赋值。
2. 数组的concat() 或者 slice()
3.对象的Object.assign()
4.封装个浅拷贝函数(支持数组和对象)
5. 使用ES6扩展运算符 ...
真正深拷贝的实现
1. JSON实现深拷贝
封装个递归深拷贝

js深浅拷贝详解与实现

# js深浅拷贝详解与实现

# JS中数据类型

  • 基本数据类型: Number、String、undefined、null、Boolean、和Symbol(ES6)
  • 引用数据类型: Object(Array, Function, Object, Date, RegExp, Math)

# 深浅拷贝

深浅拷贝只是针对引用类型的,因为引用类型是存放在堆内存中,在栈地址有一个或者多个地址来指向推内存的某一数据

对数据类型的内存存储还不明白的,可以看看我另外一篇文章 内存管理

# 什么是浅拷贝?

顾名思义,浅拷贝就是流于表面的拷贝方式;当属性值为对象类型时,只拷贝了对象数据的引用,导致新旧数据没有完全分离,还会互相影响

# 浅拷贝实现方式

# 1.= 赋值。

不多说,最基础的赋值方式,只是将对象的引用赋值。

let arr = [22, 44, 66, 88];
let newArr = arr;
newArr[0] = 11;
console.log(arr, newArr);
1
2
3
4

结果如图:

k

我们本来想把 arr 赋值给 newArr ,当我们修改newArr数组中第一个元素时,却发现了原始数组arr发生了改变 很显然,这就是一个浅拷贝的例子

原理:

  • 对于引用类型,赋值操作符只是把存放在栈内容中的指针赋值给另外一个变量。
  • 所以在赋值完成后,在栈内存就有两个指针指向堆内存同一个数据。
  • 也就可以说两个变量在共用着同一个数据,这就是浅拷贝。

# 2. 数组的concat() 或者 slice()

let arr1 = [1, [2, 2], 3];
let arr2 = arr1.concat();
arr2[0] = '哈';
arr2[1][1] = '变';
console.log('arr1 =>', arr1);
console.log('arr2 =>', arr2);
1
2
3
4
5
6
let arr1 = [1, [2, 2], 3];
let arr2 = arr1.slice(0);
arr2[0] = '哈';
arr2[1][1] = '变';
console.log('arr1 =>', arr1);
console.log('arr2 =>', arr2);
1
2
3
4
5
6

结果如图:

k2
  • 通过结果可以看出,concat()是可以复制一份新数组出来, 可当元素为对象类型时,只拷贝了对象数据的引用,导致新旧数据没有完全分离,
  • arr2[0]的元素是一个简单类型的值,改变它就不会影响原理的数组对应的值,
  • arr[1]的元素则是引用类型的值,所以改变arr[1][1]则也影响了原来数组的值

# 3.对象的Object.assign()

Object.assign是ES6的新函数。Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign() 进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。

Object.assign(target, ...sources)

  • 参数:
  • target:目标对象。
  • sources:任意多个源对象。
  • 返回值:目标对象会被返回。
let obj = { a: {a: "hello", b: 21} };
let initalObj = Object.assign({}, obj);

initalObj.a.a = "changed";
console.log(obj.a.a); // "changed"
1
2
3
4
5

上面例子可以看到obj原对象的a.a的值也改变了

需要注意的是:

Object.assign()可以处理一层的深度拷贝,如下:

let obj1 = { a: 10, b: 20, c: 30 };
let obj2 = Object.assign({}, obj1);
obj2.b = 100;
console.log(obj1);
// { a: 10, b: 20, c: 30 } <-- 沒被改到
console.log(obj2);
// { a: 10, b: 100, c: 30 }
1
2
3
4
5
6
7

# 4.封装个浅拷贝函数(支持数组和对象)

function shallowCop(target){
    //判断参数是不是引用类型, 不是就原样返回
    if(typeof target !== 'object') return target;
    //判断目标类型,是数组就用[], 其他就用 {}
    var  newObj = target instanceof Array ? [] : {};

    for(var item in target){
        //只复制元素自身的属性,不复制原生链上的
        if(target.hasOwnProperty(item)){
            newObj[item] = target[item]
        }
    }
    return newObj;
}    

let test = [1, { name: 'yu' }];
let copyarr = shallowCop(test);
copyarr[1].name = 'YY';
console.log(test);
console.log(copyarr);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

结果如图:

k3

# 5. 使用ES6扩展运算符 ...

let arr = [1 , [2, 3]]
let arr1 = [...arr];
arr[1][0] = 222;
console.log(arr); // [1, [222, 3]]
console.log(arr1);// [1, [222, 3]]
1
2
3
4
5

ES6扩展运算符,也只是深拷贝了一层

# 真正深拷贝的实现

从浅拷贝解释基本可以明白,深拷贝就是 ‘完全’拷贝,拷贝之后新旧数据完全分离,不再共用对象类型的属性值,不会互相影响。

# 1. JSON实现深拷贝

let obj1 = { body: { a: 10 } };
let obj2 = JSON.parse(JSON.stringify(obj1));
obj2.body.a = 20;
console.log(obj1);
// { body: { a: 10 } } <-- 沒被改到
console.log(obj2);
// { body: { a: 20 } }
console.log(obj1 === obj2);
// false
console.log(obj1.body === obj2.body);
// false
1
2
3
4
5
6
7
8
9
10
11

原理:

  • JSON.parse() 方法用于将一个 JSON 字符串转换为对象。
  • JSON.stringify() 方法用于将 JavaScript 值(通常为对象或数组)转换为 JSON 字符串。
  • 在JSON.stringify()完成后,对象就转为了字符串(简单类型),也就可以说实实在在的复制了一个值,不存在引用之说。
  • 再利用JSON.parse()转为对象,这样达到深拷贝的目的

这样做是真正的Deep Copy,这种方法简单易用。

但是这种方法也有不少坏处,譬如它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。

这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,即那些能够被 json 直接表示的数据结构。RegExp对象或者Function对象是无法通过这种方式深拷贝。

也就是说,只有可以转成JSON格式的对象才可以这样用,像function没办法转成JSON。

let obj = {
    nul: null,
    und: undefined,
    sym: Symbol('sym'),
    str: 'str',
    bol: true,
    num: 45,
    arr: [1, 4],
    reg: /[0-9]/,
    dat: new Date(),
    fun: function() {},  
}
console.log(JSON.parse(JSON.stringify(obj)))
1
2
3
4
5
6
7
8
9
10
11
12
13

结果如图:

k4

通过我们可以发现有些属性被忽略或者改变了:

  • dat对象改成字符串了,reg也改变了,
  • undefined, symbol,function 忽略了

可以看得出来JSON实现深拷贝也有不足之处

# 封装个递归深拷贝

function deepCopy(target) {
    // 判断不是引用类型则原样返回
    if (typeof target !== 'object') return target;
    //判断目标类型,是数组用 [], 其他用 {}
    var newObj = target instanceof Array ? [] : {};

    for (var item in target) {
        //只复制元素自身的属性,不复制原生链上的
        if (target.hasOwnProperty(item)) {
            //判断属性值类型,是引用类型则继续调用自身函数,其他直接赋值
            newObj[item] = typeof target[item] === 'object' ? deepCopy(target[item]) : target[item];
        }
    }
    return newObj;
}

let obj = {
    nul: null,
    und: undefined,
    sym: Symbol('sym'),
    str: 'str',
    bol: true,
    num: 45,
    arr: [1, 4],
    reg: /[0-9]/,
    dat: new Date(),
    fun: function () { },
}
let newObj = deepCopy(obj);
console.log(obj);
console.log(newObj);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

结果如图:

k5

通过结果看出,拷贝虽然是拷贝了,可是Null类型,Date类型,RegExp类型的值都改变了,都改成了{},

所以我们得再优化下代码判断,最终代码如下:

function deepCopy(target) {
    // 判断不是引用类型则原样返回
    if (typeof target !== 'object') return target;
    //判断目标类型,是数组用 [], 其他用 {}
    var newObj = target instanceof Array ? [] : {};

    for (var item in target) {
        //只复制元素自身的属性,不复制原生链上的
        if (target.hasOwnProperty(item)) {
            //判断属性值类型,是引用类型(并且不是Null、Date、RegExp)则继续调用自身函数,其他直接赋值
            newObj[item] = 
                        typeof target[item] === 'object' && 
                        Object.prototype.toString.call(target[item]) !== '[object Date]' && 
                        Object.prototype.toString.call(target[item]) !== '[object Null]' && 
                        Object.prototype.toString.call(target[item]) !== '[object RegExp]' ? 
                        deepCopy(target[item]) : target[item];
            // newObj[item] = typeof target[item] === 'object' ? deepCopy(target[item]) : target[item];
        }
    }
    return newObj;
}

let obj = {
    nul: null,
    und: undefined,
    sym: Symbol('sym'),
    str: 'str',
    bol: true,
    num: 45,
    arr: [1, 4],
    reg: /[0-9]/,
    dat: new Date(),
    fun: function () { },
}
let newObj = deepCopy(obj);
console.log(obj);
console.log(newObj);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

结果如图:

k6

哦耶,完美深拷贝,所有数据类型都覆盖到

参考文献

js浅拷贝与深拷贝方法 (opens new window)

深入理解JS深浅拷贝 (opens new window)

感谢大家耐心看完,以上纯属个人见解,各位小伙伴有什么想法,更好的办法,欢迎留言交流

编辑 (opens new window)
JS的种打断点调试方式
九种跨域方式实现原理

← JS的种打断点调试方式 九种跨域方式实现原理→

最近更新
01
theme-vdoing-blog博客静态编译问题
09-16
02
搜索引擎
07-19
03
友情链接
07-19
更多文章>
Theme by Vdoing | Copyright © 2019-2025 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
  • 主题模式