JavaScript 复制对象与Object.assign方法无法实现深复制


Posted in Javascript onNovember 02, 2018

在JavaScript这门语言中,数据类型分为两大类:基本数据类型和复杂数据类型。基本数据类型包括Number、Boolean、String、Null、String、Symbol(ES6 新增),而复杂数据类型包括Object,而所有其他引用类型(Array、Date、RegExp、Function、基本包装类型(Boolean、String、Number)、Math等)都是Object类型的实例对象,因此都可以继承Object原型对象的一些属性和方法。

而对于基本数据类型来说,复制一个变量值,本质上就是copy了这个变量。一个变量值的修改,不会影响到另外一个变量。看一个简单的例子。

let val = 123;
let copy = val;
console.log(copy); //123
val = 456;     //修改val的值对copy的值不产生影响
console.log(copy); //123

而对于复杂数据类型来说,同基本数据类型实现的不太相同。对于复杂数据类型的复制,要注意的是,变量名只是指向这个对象的指针。当我们将保存对象的一个变量赋值给另一个变量时,实际上复制的是这个指针,而两个变量都指向都一个对象。因此,一个对象的修改,会影响到另外一个对象。

// obj只是指向对象的指针
let obj = {
  character: 'peaceful'
};
//copy变量复制了这个指针,指向同一个对象
let copy = obj;
console.log(copy);     //{character: 'peaceful'}
obj.character = 'lovely';
console.log(copy);     //{character: 'lovely'}

有一副很形象的图描述了复杂数据类型复制的原理

JavaScript 复制对象与Object.assign方法无法实现深复制

同理,在复制一个数组时,变量名只是指向这个数组对象的指针;在复制一个函数时,函数名只是指向这个函数对象的指针

let arr = [1, 2, 3];
let copy = arr;
console.log(copy); // [1, 2, 3]
arr[0] = 'keith';
console.log(copy); // 数组对象被改变: ['keith', 2, 3]
arr = null;
console.log(copy); // ['keith', 2, 3] 即使arr=null,也不会影响copy。因此此时的arr变量只是一个指向数组对象的指针

function foo () {
  return 'hello world';
};
let bar = foo;
console.log(foo());
foo = null;   //foo只是指向函数对象的指针
console.log(bar());

因此,我们应该如何实现对象的深浅复制?

复制对象

在JavaScript中,复制对象分为两种方式,浅复制和深复制。

浅复制没有办法去真正的去复制一个对象,而只是保存了对该对象的引用;而深复制可以实现真正的复制一个对象。

浅复制

在ES6中,Object对象新增了一个assign方法,可以实现对象的浅复制。这里谈谈Object.assign方法的具体用法,因为稍后会分析jQuery的extend方法,实现的原理同Object.assign方法差不多

Object.assign的第一个参数是目标对象,可以跟一或多个源对象作为参数,将源对象的所有可枚举([[emuerable]] === true)复制到目标对象。这种复制属于浅复制,复制对象时只是包含对该对象的引用。Object.assign(target, [source1, source2, ...])

  • 如果目标对象与源对象有同名属性,则后面的属性会覆盖前面的属性
  • 如果只有一个参数,则直接返回该参数。即Object.assign(obj) === obj
  • 如果第一个参数不是对象,而是基本数据类型(Null、Undefined除外),则会调用对应的基本包装类型
  • 如果第一个参数是Null和Undefined,则会报错;如果Null和Undefined不是位于第一个参数,则会略过该参数的复制

要实现对象的浅复制,可以使用Object.assign方法

let target = {a: 123};
let source1 = {b: 456};
let source2 = {c: 789};
let obj = Object.assign(target, source1, source2);
console.log(obj);

不过对于深复制来说,Object.assign方法无法实现

let target = {a: 123};
let source1 = {b: 456};
let source2 = {c: 789, d: {e: 'lovely'}};
let obj = Object.assign(target, source1, source2);
source2.d.e = 'peaceful';
console.log(obj);  // {a: 123, b: 456, c: 789, d: {e: 'peaceful'}}

从上面代码中可以看出,source2对象中e属性的改变,仍然会影响到obj对象

深复制

在实际的开发项目中,前后端进行数据传输,主要是通过JSON实现的。JSON全称:JavaScript Object Notation,JavaScript对象表示法。

JSON对象下有两个方法,一是将JS对象转换成字符串对象的JSON.stringify方法;一个是将字符串对象转换成JS对象的JSON.parse方法。

这两个方法结合使用可以实现对象的深复制。也就是说,当我们需要复制一个obj对象时,可以先调用JSON.stringify(obj),将其转换为字符串对象,然后再调用JSON.parse方法,将其转换为JS对象。就可以轻松的实现对象的深复制

let obj = {
  a: 123,
  b: {
    c: 456,
    d: {
      e: 789
    }
  }
};
let copy = JSON.parse(JSON.stringify(obj));
// 对obj对象无论怎么修改,都不会影响到copy对象
obj.b.c = 'hello';
obj.b.d.e = 'world';
console.log(copy); // {a: 123, b: {c: 456, d: {e: 789}}}

当然,使用这种方式实现深复制有一个缺点就是必须给JSON.parse方法传入的字符串必须是合法的JSON,否则会抛出错误

jQuery.extend || jQuery.fn.extend

jQuery.extend对象,对使用jQuery超过一定时间的朋友来说并不默认。这个$.extend方法可以用来扩展jQuery的全局对象,而$.fn.extend方法可以用来扩展实例对象。fn实际上是prototype对象的别名,所以,扩展实例对象的方法实际上就是在jQuery原型对象上添加一些方法。

$.extend方法不仅可以用来写jQuery插件,同样的,它可以用来实现对象的深浅复制。(使用$.extend与$.fn.extend实现深浅复制都可以,唯一的差别就是this的指向性不同)

在具体分析源代码之前,我在源码中看到的$.extend方法的一些特点

  • 当不接受任何参数时,直接返回一个空对象
  • 当只有一个参数时(这个参数可以任何数据类型(Null、Undefined、Boolean、String、Number、Object)),会返回this对象,这里会分为两种情况。如果用$.extend,会返回jQuery对象;如果用$.fn.extend,会返回jQuery的原型对象。
  • 当接收两个参数时,并且第一个参数是Boolean值时,也会返回一个空对象。如果第一个参数不是Boolean值,那么会将源对象复制到目标对象
  • 当接收三个参数以上时,可以分为两种情况。如果第一个参数是Boolean值表示深浅复制,那么目标对象会移动到第二个参数,源对象会移动到第三个参数。(目标对象、源对象和Object.assign方法中的相同)。如果第一个参数不是Boolean值,那么用法与Object.assign方法常规的复制相同。
  • 在循环源对象的过程中,任何数据类型为Null、Undefined或者源对象是一个空对象时,在复制的过程中都会被忽略。
  • 如果源对象和目标对象具有同名的属性,则源对象的属性会覆盖掉目标对象中的属性。如果同名属性是一个对象的话,则会在deep=true等其他条件下向目标对象的该同名对象添加属性

下面贴出jQuery-2.1.4中jQuery.extend实现方式的源代码

jQuery.extend = jQuery.fn.extend = function() {
  var options, name, src, copy, copyIsArray, clone,
    target = arguments[0] || {},
    // 使用||运算符,排除隐式强制类型转换为false的数据类型
    // 如'', 0, undefined, null, false等
    // 如果target为以上的值,则设置target = {}
    i = 1,
    length = arguments.length,
    deep = false;

  // 当typeof target === 'boolean'时
  // 则将deep设置为target的值
  // 然后将target移动到第二个参数,
  if (typeof target === "boolean") {
    deep = target;
    // 使用||运算符,排除隐式强制类型转换为false的数据类型
    // 如'', 0, undefined, null, false等
    // 如果target为以上的值,则设置target = {}
    target = arguments[i] || {};
    i++;
  }

  // 如果target不是一个对象或数组或函数,
  // 则设置target = {}
  // 这里与Object.assign的处理方法不同,
  // assign方法会将Boolean、String、Number方法转换为对应的基本包装类型
  // 然后再返回,
  // 而extend方法直接将typeof不为object或function的数据类型
  // 全部转换为一个空对象
  if (typeof target !== "object" && !jQuery.isFunction(target)) {
    target = {};
  }

  // 如果arguments.length === 1 或
  // typeof arguments[0] === 'boolean', 且存在arguments[1],
  // 这时候目标对象会指向this
  // this的指向哪个对象需要看是使用$.fn.extend还是$.extend
  if (i === length) {
    target = this;
    // i-- 表示不进入for循环
    i--;
  }

  // 循环arguments类数组对象,从源对象开始
  for (; i < length; i++) {
    // 针对下面if判断
    // 有一点需要注意的是
    // 这里有一个隐式强制类型转换 undefined == null 为 true
    // 而undefined === null 为 false
    // 所以如果源对象中数据类型为Undefined或Null
    // 那么就会跳过本次循环,接着循环下一个源对象
    if ((options = arguments[i]) != null) {
      // 遍历所有[[emuerable]] === true的源对象
      // 包括Object, Array, String
      // 如果遇到源对象的数据类型为Boolean, Number
      // for in循环会被跳过,不执行for in循环
      for (name in options) {
        // src用于判断target对象是否存在name属性
        src = target[name];

        // 需要复制的属性
        // 当前源对象的name属性
        copy = options[name];

        // 这种情况暂时未遇到..
        // 按照我的理解,
        // 即使copy是同target是一样的对象
        // 两个对象也不可能相等的..
        if (target === copy) {
          continue;
        }

        // if判断主要用途:
        // 如果是深复制且copy是一个对象或数组
        // 则需要递归jQuery.extend(),
        // 直到copy成为一个基本数据类型为止
        if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
          // 深复制
          if (copyIsArray) {
            // 如果是copy是一个数组
            // 将copyIsArray重置为默认值
            copyIsArray = false;
            // 如果目标对象存在name属性且是一个数组
            // 则使用目标对象的name属性,否则重新创建一个数组,用于复制
            clone = src && jQuery.isArray(src) ? src : [];

          } else {
            // 如果目标对象存在name属性且是一个对象
            // 则使用目标对象的name属性,否则重新创建一个对象,用于复制
            clone = src && jQuery.isPlainObject(src) ? src : {};
          }

          // 因为深复制,所以递归调用jQuery.extend方法
          // 返回值为target对象,即clone对象
          // copy是一个源对象
          target[name] = jQuery.extend(deep, clone, copy);

        } else if (copy !== undefined) {
          // 浅复制
          // 如果copy不是一个对象或数组
          // 那么执行elseif分支
          // 在elseif判断中如果copy是一个对象或数组,
          // 但是都为空的话,排除这种情况
          // 因为获取空对象的属性会返回undefined
          target[name] = copy;
        }
      }
    }
  }

  // 当源对象全部循环完毕之后,返回目标对象
  return target;
};

因此,可以针对分析过后的源码,给出一些例子

let obj1 = $.extend();
console.log(obj1); // 返回一个空对象 {}

let obj2 = $.extend(undefined);
console.log(obj2); //返回jQuery对象,Object.assign传入undefined会报错

let obj3 = $.extend('123');
console.log(obj3); // 返回jQuery对象,Object.assign传入'123'会返回字符串的String对象

let target = {
  a: 123,
  b: 234
};

let source1 = {
  b: 456,
  d: ['keith', 'peaceful', 'lovely']
};

let source2 = {c: 789};
let source3 = {};

let obj4 = $.extend(target, source1, source2);
// let obj4 = $.extend(false, target, source1, source2);
console.log(obj4); // {a: 123, b: 456, d: Array(3), c: 789}
// 默认情况下,复制方式都是浅复制
// 如果只需要浅复制,不传入deep参数也可以
// 浅复制时,obj4对象中的d属性只是指向数组对象的指针

let obj5 = $.extend(target, undefined, source2);
let obj6 = $.extend(target, source3, source2);
console.log(obj5, obj6);
// {a: 123, b: 234, c: 789}, {a: 123, b: 234, c: 789}
// 会略过空对象或Undefined、Null值

let obj7 = $.extend(true, target, source1, source2);
console.log(obj7); // {a: 123, b: 456, d: Array(3), c: 789}
// 这里target对象有b属性,源对象source1也有b属性
// 此时源对象的b属性会覆盖目标对象的b属性
// 这里deep=true,属于深复制
// 当name=d时,会递归调用$.extend, 直到它的属性对应的属性值全部为基本数据类型
// 源对象的改变不会影响到obj7对象

JavaScript 复制对象

因此,可以根据$.extend方法,写出一个通用的实现对象深浅复制的函数,copyObject函数唯一的不同就是当i === arguments.length属性时,copyObject函数直接返回了target对象

function copyObject () {
  let i = 1,
    target = arguments[0] || {},
    deep = false,
    length = arguments.length,
    name, options, src, copy,
    copyIsArray, clone;

  // 如果第一个参数的数据类型是Boolean类型
  // target往后取第二个参数
  if (typeof target === 'boolean') {
    deep = target;
    // 使用||运算符,排除隐式强制类型转换为false的数据类型
    // 如'', 0, undefined, null, false等
    // 如果target为以上的值,则设置target = {}
    target = arguments[1] || {};
    i++;
  }

  // 如果target不是一个对象或数组或函数
  if (typeof target !== 'object' && !(typeof target === 'function')) {
    target = {};
  }

  // 如果arguments.length === 1 或
  // typeof arguments[0] === 'boolean',
  // 且存在arguments[1],则直接返回target对象
  if (i === length) {
    return target;
  }

  // 循环每个源对象
  for (; i < length; i++) {
    // 如果传入的源对象是null或undefined
    // 则循环下一个源对象
    if (typeof (options = arguments[i]) != null) {
      // 遍历所有[[emuerable]] === true的源对象
      // 包括Object, Array, String
      // 如果遇到源对象的数据类型为Boolean, Number
      // for in循环会被跳过,不执行for in循环
      for (name in options) {
        // src用于判断target对象是否存在name属性
        src = target[name];
        // copy用于复制
        copy = options[name];
        // 判断copy是否是数组
        copyIsArray = Array.isArray(copy);
        if (deep && copy && (typeof copy === 'object' || copyIsArray)) {
          if (copyIsArray) {
            copyIsArray = false;
            // 如果目标对象存在name属性且是一个数组
            // 则使用目标对象的name属性,否则重新创建一个数组,用于复制
            clone = src && Array.isArray(src) ? src : [];
          } else {
            // 如果目标对象存在name属性且是一个对象
            // 则使用目标对象的name属性,否则重新创建一个对象,用于复制
            clone = src && typeof src === 'object' ? src : {};
          }
          // 深复制,所以递归调用copyObject函数
          // 返回值为target对象,即clone对象
          // copy是一个源对象
          target[name] = copyObject(deep, clone, copy);
        } else if (copy !== undefined){
          // 浅复制,直接复制到target对象上
          target[name] = copy;
        }
      }
    }
  }
  // 返回目标对象
  return target;   
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript去掉数组中的重复元素
Jan 13 Javascript
js传参数受特殊字符影响错误的解决方法
Oct 21 Javascript
JS控制表格实现一条光线流动分割行的方法
Mar 09 Javascript
JavaScript用select实现日期控件
Jul 17 Javascript
Jquery效果大全之制作电脑健康体检得分特效附源码下载
Nov 02 Javascript
jQuery对html元素的取值与赋值实例详解
Dec 18 Javascript
js中使用使用原型(prototype)定义方法的好处详解
Jul 04 Javascript
jQuery实现立体式数字滚动条增加效果
Dec 21 Javascript
jQuery 表单序列化实例代码
Jun 11 jQuery
Angular指令之restict匹配模式的详解
Jul 27 Javascript
详解vue.js之props传递参数
Dec 12 Javascript
解决vue 绑定对象内点击事件失效问题
Sep 05 Javascript
vue头部导航动态点击处理方法
Nov 02 #Javascript
angular6 利用 ngContentOutlet 实现组件位置交换(重排)
Nov 02 #Javascript
vue单页面实现当前页面刷新或跳转时提示保存
Nov 02 #Javascript
BootStrap中的模态框(modal,弹出层)功能示例代码
Nov 02 #Javascript
Bootstrap的aria-label和aria-labelledby属性实例详解
Nov 02 #Javascript
axios使用拦截器统一处理所有的http请求的方法
Nov 02 #Javascript
vue实现与安卓、IOS交互的方法
Nov 02 #Javascript
You might like
7个超级实用的PHP代码片段
2011/07/11 PHP
基于PHP5魔术常量与魔术方法的详解
2013/06/13 PHP
利用Fix Rss Feeds插件修复WordPress的Feed显示错误
2015/12/19 PHP
firefox事件处理之自动查找event的函数(用于onclick=foo())
2010/08/05 Javascript
使用js正则控制input标签只允许输入的值
2013/07/29 Javascript
JS实现点击文字对应DIV层不停闪动效果的方法
2015/03/02 Javascript
12个超实用的JQuery代码片段
2015/11/02 Javascript
js简单获取表单中单选按钮值的方法
2016/08/23 Javascript
解决Angular.Js与Django标签冲突的方案
2016/12/20 Javascript
node.js环境搭建图文详解
2018/09/19 Javascript
小程序云开发之用户注册登录
2019/05/18 Javascript
基于JavaScript 实现拖放功能
2019/09/12 Javascript
jQuery/JS监听input输入框值变化实例
2019/10/17 jQuery
原生js实现密码强度验证功能
2020/03/18 Javascript
[31:29]完美世界DOTA2联赛PWL S3 INK ICE vs Magma 第一场 12.20
2020/12/23 DOTA
sqlalchemy对象转dict的示例
2014/04/22 Python
python回调函数用法实例分析
2015/05/09 Python
Python中super()函数简介及用法分享
2016/07/11 Python
浅谈python中的__init__、__new__和__call__方法
2017/07/18 Python
利用Python模拟登录pastebin.com的实现方法
2019/07/12 Python
对python中UDP,socket的使用详解
2019/08/22 Python
Python使用__new__()方法为对象分配内存及返回对象的引用示例
2019/09/20 Python
构建高效的python requests长连接池详解
2020/05/02 Python
Python命令行参数定义及需要注意的地方
2020/11/30 Python
解决selenium+Headless Chrome实现不弹出浏览器自动化登录的问题
2021/01/09 Python
Nike德国官网:Nike.com (DE)
2018/11/13 全球购物
俄罗斯珠宝市场的领导者之一:Бронницкий ювелир
2019/10/02 全球购物
地球鞋加拿大官网:Earth Shoes Canada
2020/11/17 全球购物
2014年化妆品销售工作总结
2014/12/01 职场文书
岁月神偷观后感
2015/06/11 职场文书
2016大学生毕业实习心得体会
2016/01/23 职场文书
python识别围棋定位棋盘位置
2021/07/26 Python
一篇文章带你深入了解Mysql触发器
2021/08/02 MySQL
关于MybatisPlus配置双数据库驱动连接数据库问题
2022/01/22 Java/Android
高性能跳频抗干扰宽带自组网电台
2022/02/18 无线电
vue3引入highlight.js进行代码高亮的方法实例
2022/04/08 Vue.js