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 相关文章推荐
JS类定义原型方法的两种实现的区别评论很多
Sep 12 Javascript
基于jQuery的弹出框插件
Mar 18 Javascript
js中for in的用法示例解析
Dec 25 Javascript
Jquery操作Ajax方法小结
Nov 29 Javascript
JavaScript中的编码和解码函数
Feb 15 Javascript
JS检测是否可以访问公网服务器功能代码
Jun 19 Javascript
React根据宽度自适应高度的示例代码
Oct 11 Javascript
vue2.0组件之间传值、通信的多种方式(干货)
Feb 10 Javascript
Vue源码分析之Vue实例初始化详解
Aug 25 Javascript
原生javascript如何实现共享onload事件
Jul 03 Javascript
解决vue字符串换行问题(绝对管用)
Aug 06 Javascript
vue+element ui实现锚点定位
Jun 29 Vue.js
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 simple_html_dom.php+正则 采集文章代码
2009/12/24 PHP
4种PHP异步执行的常用方式
2015/12/24 PHP
PHP基于redis计数器类定义与用法示例
2018/02/08 PHP
捕获关闭窗口的脚本
2009/01/10 Javascript
Prototype 学习 工具函数学习($A方法)
2009/07/12 Javascript
用document.documentElement取代document.body的原因分析
2009/11/12 Javascript
Javascript 通过json自动生成Dom的代码
2010/04/01 Javascript
Fixie.js 自动填充内容的插件
2012/06/28 Javascript
中国地区三级联动下拉菜单效果分析
2012/11/15 Javascript
动态加载JS文件的三种方法
2013/11/08 Javascript
2014年50个程序员最适用的免费JQuery插件
2014/12/15 Javascript
12种JavaScript常用的MVC框架比较分析
2015/11/16 Javascript
JavaScript事件类型中焦点、鼠标和滚轮事件详解
2016/01/25 Javascript
jQuery实现简单滚动动画效果
2016/04/07 Javascript
jQuery双向列表选择器select版
2016/11/01 Javascript
浅谈vue的iview列表table render函数设置DOM属性值的方法
2017/09/30 Javascript
Angularjs实现数组随机排序的方法
2018/10/02 Javascript
Element-ui中元素滚动时el-option超出元素区域的问题
2019/05/30 Javascript
vue 实现tab切换保持数据状态
2020/07/21 Javascript
python3中bytes和string之间的互相转换
2017/02/09 Python
Python基于opencv的图像压缩算法实例分析
2018/05/03 Python
Python实现读取机器硬件信息的方法示例
2018/06/09 Python
对Python 获取类的成员变量及临时变量的方法详解
2019/01/22 Python
python实现扫描局域网指定网段ip的方法
2019/04/16 Python
pandas将多个dataframe以多个sheet的形式保存到一个excel文件中
2019/10/10 Python
解决numpy矩阵相减出现的负值自动转正值的问题
2020/06/03 Python
python破解同事的压缩包密码
2020/10/14 Python
澳大利亚时尚前卫设计师珠宝在线:Amber Sceats
2017/10/04 全球购物
Bench加拿大官方网站:英国城市服装品牌
2017/11/03 全球购物
运动会解说词100字
2014/01/31 职场文书
电工工作职责范本
2014/02/22 职场文书
2015年元旦促销方案书
2014/12/09 职场文书
乡镇干部学习心得体会
2016/01/23 职场文书
《打电话》教学反思
2016/02/22 职场文书
OpenCV全景图像拼接的实现示例
2021/06/05 Python
MySQL连表查询分组去重的实现示例
2021/07/01 MySQL