javascript之Object.assign()的痛点分析


Posted in Javascript onMarch 03, 2022

最近也一直会用javascript,然后中间使用的一些组件,如Echarts 会有非常复杂的配置文件,而大部分配置可能都是一样的,所以想着写一份通用配置,然后,其他地方需要使用的时候,用这份配置深拷贝一份配置,然后在上面继续改。

就如下:

const defaultOpt = {
    key1: xxx,
    key2: {
        dd: ee
    },
    .....
};
// deepCopy为某个实现深拷贝的方法
const opt1 = deepCopy(defaultOpt);
opt1.....
const opt2 = deepCopy(defaultOpt);
opt2.....

深拷贝和浅拷贝

这里也涉及到一个深拷贝和浅拷贝的概念。javascript中存储对象都是存地址的,所以浅拷贝是都指向同一块内存区块,而深拷贝则是另外开辟了一块区域。

下面实例也可以看出这一点:

// 浅拷贝
const a = {t: 1, p: 'gg'};
const b = a;
b.t = 3;
console.log(a); // {t: 3, p: 'gg'}
console.log(b); // {t: 3, p: 'gg'}
//深拷贝
const c = {t: 1, p: 'gg'};
const d = deepCopy(c);
d.t = 3;
console.log(c); // {t: 1, p: 'gg'}
console.log(d); // {t: 3, p: 'gg'}

可以明显看出,浅拷贝在改变其中一个值时,会导致其他也一起改变,而深拷贝不会。

Object.assign()

我需要的是深拷贝的方法,然后发现原来es6 中有Object.assign() 这个方法,感觉可以拿来用了。

贴一下两个官方例子:

// Cloning an object
var obj = { a: 1 };
var copy = Object.assign({}, obj);
console.log(copy); // { a: 1 }
// Merging objects
var o1 = { a: 1 };
var o2 = { b: 2 };
var o3 = { c: 3 };
var obj = Object.assign(o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
console.log(o1);  // { a: 1, b: 2, c: 3 }, target object itself is changed.

是不是很完美,又可以clone又可以merge。在我这种情况下,我觉得我的代码量又可以减少了,比如:

const defaultOpt = {
    title: 'hello', 
    name: 'oo', 
    type: 'line'
};
// 原来可能需要这样
const opt1 = deepCopy(a);
opt1.title = 'opt1';
opt1.type = 'bar';
opt1.extra = 'extra'; // 额外增加配置
// 现在只要这样
const opt2 = Object.assign({}, a, {
    title: 'opt2', 
    type: 'bar', 
    extra: 'extra'
});

不过,很快,问题出现了,那就是

merge和我想象的不一样

且看例子:

const defaultOpt = {
    title: {
        text: 'hello world',
        subtext: 'It\'s my world.'
    }
};
const opt = Object.assign({}, defaultOpt, {
    title: {
        subtext: 'Yes, your world.'
    }
});
console.log(opt);
// 预期结果
{
    title: {
        text: 'hello world',
        subtext: 'Yes, your world.'
    }
}
// 实际结果
{
    title: {
        subtext: 'Yes, your world.'
    }
}

原本想的是它只会覆盖subtext ,然而其实它直接覆盖了整个title ,这个让我比较郁闷,相当于它只merge根属性,下面的就不做处理了。

代码只能重构成相对麻烦一点的:

const defaultOpt = {
    title: {
        text: 'hello world',
        subtext: 'It\'s my world.'
    }
};
const opt = Object.assign({}, defaultOpt);
opt.title.subtext = 'Yes, your world.';
console.log(opt);
// 结果正常
{
    title: {
        text: 'hello world',
        subtext: 'Yes, your world.'
    }
}

这样用虽然麻烦一点,但是也还好,可以用了。不过。。。很快,又出现问题了,如下:

const defaultOpt = {
    title: {
        text: 'hello world',
        subtext: 'It\'s my world.'
    } 
};
const opt1 = Object.assign({}, defaultOpt);
const opt2 = Object.assign({}, defaultOpt);
opt2.title.subtext = 'Yes, your world.';
console.log('opt1:');
console.log(opt1);
console.log('opt2:');
console.log(opt2);
// 结果
opt1:
{
    title: {
        text: 'hello world',
        subtext: 'Yes, your world.'
    }
}
opt2:
{
    title: {
        text: 'hello world',
        subtext: 'Yes, your world.'
    }
}

上面结果发现两个配置变得一模一样,而其实我们并没有去更改opt1 的subtext ,只是改了opt2 的。

这说明一点:在title 这一层只是简单的浅拷贝 ,而没有继续深入的深拷贝。

这里不经让我怀疑这个接口到底是怎么实现的,它到底是不是和我所想的一样。

翻了一下官方文档,发现它写得一个Polyfill ,代码我加了点注释如下:

if (!Object.assign) {
    // 定义assign方法
  Object.defineProperty(Object, 'assign', {
    enumerable: false,
    configurable: true,
    writable: true,
    value: function(target) { // assign方法的第一个参数
      'use strict';
      // 第一个参数为空,则抛错
      if (target === undefined || target === null) {
        throw new TypeError('Cannot convert first argument to object');
      }
      var to = Object(target);
      // 遍历剩余所有参数
      for (var i = 1; i < arguments.length; i++) {
        var nextSource = arguments[i];
        // 参数为空,则跳过,继续下一个
        if (nextSource === undefined || nextSource === null) {
          continue;
        }
        nextSource = Object(nextSource);
        // 获取改参数的所有key值,并遍历
        var keysArray = Object.keys(nextSource);
        for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
          var nextKey = keysArray[nextIndex];
          var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
          // 如果不为空且可枚举,则直接浅拷贝赋值
          if (desc !== undefined && desc.enumerable) {
            to[nextKey] = nextSource[nextKey];
          }
        }
      }
      return to;
    }
  });
}

上面的代码可以直接说明它只对顶层属性做了赋值,完全没有继续做递归之类的把所有下一层的属性做深拷贝。

小结一下

Object.assign() 只是一级属性复制,比浅拷贝多深拷贝了一层而已。用的时候,还是要注意这个问题的。

附:发现一个可以简单实现深拷贝的方法,当然,有一定限制,如下:

const obj1 = JSON.parse(JSON.stringify(obj));

思路就是将一个对象转成json字符串,然后又将字符串转回对象。 

细说一下Object.assign()

Object.assign()

  • Object.assign() 第一个参数是目标对象,后面的都是源对象。 Object.assign (target, source1,source2, source3, …);
  • 如果源对像与目标对象有相同的属性名,或源对象中有相同的属性名,后面的会覆盖前边的值 。
  • 如果参数传入的不是Object,会转成Object
  • null和undefined 不能作为参数传入,因为null和undefined 不能转成Object
  • 如果发生的值是一个对象,Object.assign的处理方法是直接替换,而不是添加。 如下面的 a 和 b
  • 可以为类添加方法
const obj1  = {name:'小明', age:'18',education:'undergraduate'}
    const obj2 = {height:'180cm',hobby:'painting'}
    let  obj = Object.assign({},obj1, obj2)
    console.log('合并后的类:');
    console.log(JSON.stringify(obj));
    Object.assign(obj, obj, {height:'170cm'});
    console.log('修改过height后的类:');
    console.log(JSON.stringify(obj));
    Object.assign(obj, {arr:{index:1, name:'类'}}, {name:'加了一个类进去'})
    console.log(JSON.stringify(obj));
    console.log("加一个类进去后:"+obj.arr.index);
    // a. 这种修改方式,只会修改index 的值
    Object.assign(obj, Object.assign(obj.arr, {index:2}))
    console.log(JSON.stringify(obj));
    console.log("修改类index后:"+obj.arr.index);
    // b. 这种修改方式,arr只剩下index属性
    // Object.assign(obj, {arr:{index:2}}, {name:'修改类的index为:2'})
    // console.log(JSON.stringify(obj));
    // console.log("修改类index后:"+obj.arr.index);
    // Object.assign()做的是浅拷贝, 如果一个属性是新合并进来的对象,改变源对象的值,会影响合并后的值 。
    let newObj  = {type:{aa:'蔬菜'}};
    Object.assign(obj, newObj);
    console.log("合并一个含属性type的类后:"+JSON.stringify(obj));
    // c. 这种不会影响obj中的type.aa
    // Object.assign(newObj, {type:{aa:'水果'}});
    // d. 这种会影响obj中的type.aa
    newObj.type.aa = '水果';
    console.log("修改newObj中的type.aa后:"+JSON.stringify(newObj));
    console.log("修改newObj中的type.aa后:"+JSON.stringify(obj));
    // e. 用Object.assign合并一个数组的时候,会把数组当成一个属性名为index的类
    const arr1  = [1, 2, 3, 4, 5] ;  // 在Object的眼里是这样的: arr1={0:1, 1:2, 2:3,3:4, 4:5}
    const arr2 =  [8, 9, 10];        // 在Object的眼里是这样的: arr2={0:8, 1:9, 2:10}
    console.log("合并后的数组为:"+Object.assign(arr1, arr2)); // 得到的结果是:8, 9, 10, 4, 5
    // f. Object.assign() 为类添加方法
    Object.assign(UserInfo.prototype, {
      getUserName (){
        return this.name;
      },
      getUserGender (){
        return this.gender ;
      }
    })
    let user  = new UserInfo("笑笑", '女');
    console.log("userinfo中的信息为: "+ user.getUserName() +", "+user.getUserGender()); // 输出的结果为:笑笑,女

输出的结果:

ObjectAssignDemo.js:13 合并后的类:
ObjectAssignDemo.js:14 {"name":"小明","age":"18","education":"undergraduate","height":"180cm","hobby":"painting"}
ObjectAssignDemo.js:16 修改过height后的类:
ObjectAssignDemo.js:17 {"name":"小明","age":"18","education":"undergraduate","height":"170cm","hobby":"painting"}
ObjectAssignDemo.js:19 {"name":"加了一个类进去","age":"18","education":"undergraduate","height":"170cm","hobby":"painting","arr":{"index":1,"name":"类"}}
ObjectAssignDemo.js:20 加一个类进去后:1
ObjectAssignDemo.js:24 {"name":"类","age":"18","education":"undergraduate","height":"170cm","hobby":"painting","arr":{"index":2,"name":"类"},"index":2}
ObjectAssignDemo.js:25 修改类index后:2
ObjectAssignDemo.js:35 合并一个含属性type的类后:{"name":"类","age":"18","education":"undergraduate","height":"170cm","hobby":"painting","arr":{"index":2,"name":"类"},"index":2,"type":{"aa":"蔬菜"}}
ObjectAssignDemo.js:40 修改newObj中的type.aa后:{"type":{"aa":"水果"}}
ObjectAssignDemo.js:41 修改newObj中的type.aa后:{"name":"类","age":"18","education":"undergraduate","height":"170cm","hobby":"painting","arr":{"index":2,"name":"类"},"index":2,"type":{"aa":"水果"}}
ObjectAssignDemo.js:46 合并后的数组为:8,9,10,4,5
ObjectAssignDemo.js:58 userinfo中的信息为: 笑笑, 女

以上为个人经验,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JQuery设置时间段下拉选择实例
Dec 30 Javascript
对JavaScript的全文搜索实现相关度评分的功能的方法
Jun 24 Javascript
JavaScript中字符串(string)转json的2种方法
Jun 25 Javascript
JS模拟实现Select效果代码
Sep 24 Javascript
JS代码防止SQL注入的方法(超简单)
Apr 12 Javascript
浅析Bootstrap组件之面板组件
May 04 Javascript
聊一聊Vue.js过渡效果
Sep 07 Javascript
JS实现选定指定HTML元素对象中指定文本内容功能示例
Feb 13 Javascript
jquery.validate表单验证插件使用详解
Jun 21 jQuery
webstorm和.vue中es6语法报错的解决方法
May 08 Javascript
解决layui中onchange失效以及form动态渲染失效的问题
Sep 27 Javascript
Webpack设置环境变量的一些误区详解
Dec 19 Javascript
vue实现移动端div拖动效果
Mar 03 #Vue.js
vue实现滑动解锁功能
JavaScript最完整的深浅拷贝实现方式详解
Feb 28 #Javascript
Vue全局事件总线你了解吗
Feb 24 #Vue.js
Vue的生命周期一起来看看
Vue的过滤器你真了解吗
Feb 24 #Vue.js
Vue监视数据的原理详解
Feb 24 #Vue.js
You might like
php读取二进制流(C语言结构体struct数据文件)的深入解析
2013/06/13 PHP
详谈PHP程序Laravel 5框架的优化技巧
2016/07/18 PHP
PHP序列化操作方法分析
2016/09/28 PHP
php实现通过stomp协议连接ActiveMQ操作示例
2020/02/23 PHP
Nodejs极简入门教程(三):进程
2014/10/27 NodeJs
Lua表达式和控制结构学习笔记
2014/12/15 Javascript
纯javascript实现简单下拉刷新功能
2015/03/13 Javascript
jQuery寻找n以内完全数的方法
2015/06/24 Javascript
js基础知识(公有方法、私有方法、特权方法)
2015/11/06 Javascript
Javascript计算二维数组重复值示例代码
2016/12/18 Javascript
yii form 表单提交之前JS在提交按钮的验证方法
2017/03/15 Javascript
将angular-ui的分页组件封装成指令的方法详解
2017/05/10 Javascript
JS立即执行函数功能与用法分析
2019/01/15 Javascript
vue通信方式EventBus的实现代码详解
2019/06/10 Javascript
浅析Angular 实现一个repeat指令的方法
2019/07/21 Javascript
js实现滑动进度条效果
2020/08/21 Javascript
JavaScript仿京东轮播图效果
2021/02/25 Javascript
[01:04:02]DOTA2-DPC中国联赛 正赛 Elephant vs IG BO3 第二场 1月24日
2021/03/11 DOTA
在Python中使用Neo4j数据库的教程
2015/04/16 Python
Python 装饰器深入理解
2017/03/16 Python
Python及PyCharm下载与安装教程
2017/11/18 Python
Python读取Json字典写入Excel表格的方法
2018/01/03 Python
python实现微信发送邮件关闭电脑功能
2018/02/22 Python
关于python的缩进规则的知识点详解
2020/06/22 Python
python的setattr函数实例用法
2020/12/16 Python
matplotlib绘制鼠标的十字光标的实现(内置方式)
2021/01/06 Python
特步官方商城:Xtep
2017/03/21 全球购物
英国快时尚女装购物网站:PrettyLittleThing
2018/08/15 全球购物
毕业自荐书
2013/12/09 职场文书
运动会广播稿300字
2014/01/10 职场文书
药店促销活动总结
2014/07/10 职场文书
医院深入开展党的群众路线教育实践活动实施方案
2014/08/27 职场文书
2014年国庆节演讲稿
2014/09/19 职场文书
2014年医德医风工作总结
2014/11/13 职场文书
检讨书大全
2015/01/27 职场文书
2015年房地产销售工作总结
2015/04/20 职场文书