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 放大镜 移动镜片效果代码
May 09 Javascript
JavaScript实现统计文本框Textarea字数增强用户体验
Dec 21 Javascript
如何让easyui gridview 宽度自适应窗口改变及fitColumns应用
Jan 25 Javascript
jquery 滚动条事件简单实例
Jul 12 Javascript
jquery 追加tr和删除tr示例代码
Sep 12 Javascript
js中传递特殊字符(+,&amp;)的方法
Jan 16 Javascript
基于js与flash实现的网站flv视频播放插件代码
Oct 14 Javascript
微信小程序开发之实现选项卡(窗口顶部TabBar)页面切换
Nov 25 Javascript
Websocket协议详解及简单实例代码
Dec 12 Javascript
jQuery Easyui datagrid连续发送两次请求问题
Dec 13 Javascript
vue父组件触发事件改变子组件的值的方法实例详解
May 07 Javascript
Vue实现点击按钮复制文本内容的例子
Nov 09 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
php绘制一条直线的方法
2015/01/24 PHP
PHP实现删除字符串中任何字符的函数
2015/08/11 PHP
PHP Yaf框架的简单安装使用教程(推荐)
2016/06/08 PHP
php  PATH_SEPARATOR判断当前服务器系统类型实例
2016/10/28 PHP
PHP实现二维数组按照指定的字段进行排序算法示例
2019/04/23 PHP
超级兔子让浮动层消失的前因后果
2007/03/09 Javascript
jQuery 添加/移除CSS类实现代码
2010/02/11 Javascript
基于jQuery的固定表格头部的代码(IE6,7,8测试通过)
2010/05/18 Javascript
JavaScript 验证码的实例代码(附效果图)
2013/03/22 Javascript
浅析基于WEB前端页面的页面内容搜索的实现思路
2014/06/10 Javascript
javascript实现博客园页面右下角返回顶部按钮
2015/02/22 Javascript
JS运动框架之分享侧边栏动画实例
2015/03/03 Javascript
js改变Iframe中Src的方法
2015/05/05 Javascript
JS判断页面是否出现滚动条的方法
2015/07/17 Javascript
静态页面实现 include 引入公用代码的示例
2017/09/25 Javascript
Vue插件打包与发布的方法示例
2018/08/20 Javascript
基于JavaScript实现简单的轮播图
2021/03/03 Javascript
[47:31]完美世界DOTA2联赛PWL S3 INK ICE vs DLG 第一场 12.12
2020/12/16 DOTA
使用python Django做网页
2013/11/04 Python
python实现的二叉树定义与遍历算法实例
2017/06/30 Python
Python定时任务sched模块用法示例
2018/07/16 Python
Python3.5装饰器典型案例分析
2019/04/30 Python
利用pyinstaller打包exe文件的基本教程
2019/05/02 Python
Python3 执行Linux Bash命令的方法
2019/07/12 Python
python使用paramiko模块通过ssh2协议对交换机进行配置的方法
2019/07/25 Python
Python Web框架之Django框架Form组件用法详解
2019/08/16 Python
python3常用的数据清洗方法(小结)
2019/10/31 Python
C++和python实现阿姆斯特朗数字查找实例代码
2020/12/07 Python
企划主管岗位职责
2013/12/12 职场文书
大班亲子运动会方案
2014/06/10 职场文书
2014年班组长工作总结
2014/11/20 职场文书
2015年上半年党建工作总结
2015/03/30 职场文书
英语演讲开场白
2015/05/29 职场文书
Python中json.dumps()函数的使用解析
2021/05/17 Python
Tomcat项目启动失败的原因和解决办法
2022/04/20 Servers
Java获取字符串编码格式实现思路
2022/09/23 Java/Android