资讯专栏INFORMATION COLUMN

javascript系列--Object.assign实现浅拷贝的原理以及实现

sunsmell / 2028人阅读

摘要:传送门本文会介绍浅拷贝的实现原理,然后咱们试着实现一个浅拷贝。返回的对象就是目标对象。使用转成对象,并保存为,最后返回这个对象。

一、前言

之前在前面一篇学习了赋值,浅拷贝和深拷贝。介绍了这三者的相关知识和区别。

传送门:https://www.mwcxs.top/page/59...

本文会介绍浅拷贝Object.assign()的实现原理,然后咱们试着实现一个浅拷贝。

二、浅拷贝Object.assign()

什么是浅拷贝?浅拷贝就是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。

浅拷贝Object.assign()是什么?主要将所有可枚举属性的值从一个或者多个数据源对象复制到目标对象,同时返回目标对象。

语法规则:

Object.assign(target,...sources)

其中target是目标对象,source是源对象,可以是多个,修改返回的是目标对象target。

1、如果目标对象中的属性具有相同的属性键,则属性将被源对象中的属性覆盖;

2、源对象的属相将类似覆盖早先的属性。

强调两点:

1、可枚举的属性(自有属性)

2、string或者symbol类型是可以被直接分配的

2.1栗子1

浅拷贝就是拷贝第一层的基本类型值,以及第一层的引用类型地址。

// saucxs
// 第一步
let a = {
    name: "advanced",
    age: 18
}
let b = {
    name: "saucxs",
    book: {
        title: "You Don"t Know JS",
        price: "45"
    }
}
let c = Object.assign(a, b);
console.log(c);
// {
//     name: "saucxs",
//  age: 18,
//     book: {title: "You Don"t Know JS", price: "45"}
// }
console.log(a === c);
// true

// 第二步
b.name = "change";
b.book.price = "55";
console.log(b);
// {
//     name: "change",
//     book: {title: "You Don"t Know JS", price: "55"}
// }

// 第三步
console.log(a);
// {
//     name: "saucxs",
//  age: 18,
//     book: {title: "You Don"t Know JS", price: "55"}
// }

分析:

1、第一步中,使用Object.assign把源对象b的值复制到目标对象a中,这里把返回值定义为对象c,可以看出b会替换掉a中具有相同键的值,即如果目标对象a中的属性具有相同的键,则属相将被源对象b中的属性覆盖。返回的对象c就是目标对象a。

2、第二步中,修改源对象b的基本类型值(name)和引用类型值(book)。

3、第三步中,浅拷贝之后目标对象a的基本类型值没有改变,但是引用类型值发生了改变,因为Object.assign()拷贝的是属性值。加入源对象的属性值是一个指向对象的引用,只拷贝那个引用地址。

2.2栗子2

string类型和symbol类型的属性都会被拷贝,而且不会跳过那些值为null或undefined的源对象。

// saucxs
// 第一步
let a = {
    name: "saucxs",
    age: 18
}
let b = {
    b1: Symbol("saucxs"),
    b2: null,
    b3: undefined
}
let c = Object.assign(a, b);
console.log(c);
// {
//     name: "saucxs",
//  age: 18,
//     b1: Symbol(saucxs),
//     b2: null,
//     b3: undefined
// }
console.log(a === c);
// true
三、Object.assign模拟实现

实现Object.assign模拟实现大致思路:

1、判断原生的Object是否支持assign这个函数,如果不存在的话就会创建一个assign函数,并使用Object.defineProperty将函数绑定到Object上。

2、判断参数是否正确(目标参数不能为空,可以直接设置{}传递进去,但是必须有值)。

3、使用Object()转成对象,并保存为to,最后返回这个对象to。

4、使用for in 循环遍历出所有的可枚举的自有属性,并复制给新的目标对象(使用hasOwnProperty获取自有属性,即非原型链上的属性)

参考原生,实现代码如下,使用assign2代替assign。此处的模拟不支持symbol属性,因为es5中没有symbol。

// saucxs
if (typeof Object.assign2 != "function") {
  // 注意 1
  Object.defineProperty(Object, "assign2", {
    value: function (target) {
      "use strict";
      if (target == null) { // 注意 2
        throw new TypeError("Cannot convert undefined or null to object");
      }

      // 注意 3
      var to = Object(target);
        
      for (var index = 1; index < arguments.length; index++) {
        var nextSource = arguments[index];

        if (nextSource != null) {  // 注意 2
          // 注意 4
          for (var nextKey in nextSource) {
            if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
              to[nextKey] = nextSource[nextKey];
            }
          }
        }
      }
      return to;
    },
    writable: true,
    configurable: true
  });
}

测试一下:

// saucxs
// 测试用例
let a = {
    name: "advanced",
    age: 18
}
let b = {
    name: "saucxs",
    book: {
        title: "You Don"t Know JS",
        price: "45"
    }
}
let c = Object.assign2(a, b);
console.log(c);
// {
//     name: "saucxs",
//  age: 18,
//     book: {title: "You Don"t Know JS", price: "45"}
// } 
console.log(a === c);
// true
3.1 注意1:可枚举性

原生情况下挂载在Object上的属性时不可枚举的,但是直接在Object上挂载属性a之后就可以枚举的,所以必须使用Object.defineProperty,并设置enumerable: false 以及 writable: trueconfigurable: true

// saucxs
for(var i in Object) {
    console.log(Object[i]);
}
// 无输出

Object.keys( Object );
// []

上面说明,原生的Object上的属性不可枚举。

我们可以使用2种方法查看Object.assign是否可枚举,使用Object.getOwnPropertyDescriptor或者Object.propertyIsEnumberable都可以,其中propertyIsEnumerable(..)会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足enumerable:true。具体用法如下:

// saucxs
Object.getOwnPropertyDescriptor(Object, "assign");
// {
//     value: ƒ, 
//  writable: true,     // 可写
//  enumerable: false,  // 不可枚举,注意这里是 false
//  configurable: true    // 可配置
// }
// saucxs
Object.propertyIsEnumerable("assign");
// false

说明Object.assign是不可枚举的。

直接在Object上挂载属性a之后是可以枚举的。我们来看一下代码:

// saucxs
Object.a = function () {
    console.log("log a");
}

Object.getOwnPropertyDescriptor(Object, "a");
// {
//     value: ƒ, 
//  writable: true, 
//  enumerable: true,  // 注意这里是 true
//  configurable: true
// }

Object.propertyIsEnumerable("a");
// true

所以要实现 Object.assign 必须使用 Object.defineProperty,并设置 writable: true, enumerable: false, configurable: true,当然默认情况下不设置就是 false

// saucxs
Object.defineProperty(Object, "b", {
    value: function() {
        console.log("log b");
    }
});

Object.getOwnPropertyDescriptor(Object, "b");
// {
//     value: ƒ, 
//  writable: false,     // 注意这里是 false
//  enumerable: false,  // 注意这里是 false
//  configurable: false    // 注意这里是 false
// }

模拟实现涉及到代码

// saucxs
// 判断原生 Object 中是否存在函数 assign2
if (typeof Object.assign2 != "function") {
  // 使用属性描述符定义新属性 assign2
  Object.defineProperty(Object, "assign2", {
    value: function (target) { 
      ...
    },
    // 默认值是 false,即 enumerable: false
    writable: true,
    configurable: true
  });
}
3.2 注意2:判断参数是否正确

有些文章判断参数是否正确是这样的。

// saucxs
if (target === undefined || target === null) {
    throw new TypeError("Cannot convert undefined or null to object");
}

这样肯定没问题,但是这样写没有必要,因为 undefinednull 是相等的(高程 3 P52 ),即 undefined == null 返回 true,只需要按照如下方式判断就好了。

// saucxs
if (target == null) { // TypeError if undefined or null
    throw new TypeError("Cannot convert undefined or null to object");
}
3.3 注意3:原始类型被包装为对象
// saucxs
var v1 = "abc";
var v2 = true;
var v3 = 10;
var v4 = Symbol("foo");

var obj = Object.assign({}, v1, null, v2, undefined, v3, v4); 

// 原始类型会被包装,null 和 undefined 会被忽略。
// 注意,只有字符串的包装对象才可能有自身可枚举属性。
console.log(obj); 
// { "0": "a", "1": "b", "2": "c" }

上面代码中的源对象 v2、v3、v4 实际上被忽略了,原因在于他们自身没有可枚举属性

// saucxs
var v1 = "abc";
var v2 = true;
var v3 = 10;
var v4 = Symbol("foo");
var v5 = null;

// Object.keys(..) 返回一个数组,包含所有可枚举属性
// 只会查找对象直接包含的属性,不查找[[Prototype]]链
Object.keys( v1 ); // [ "0", "1", "2" ]
Object.keys( v2 ); // []
Object.keys( v3 ); // []
Object.keys( v4 ); // []
Object.keys( v5 ); // TypeError: Cannot convert undefined or null to object

上面代码说明:Object.keys(..)返回一个数组,包含所有可枚举的属性,只会查找对象直接包含的属性,而不会查找[[prototype]]链。

// Object.getOwnPropertyNames(..) 返回一个数组,包含所有属性,无论它们是否可枚举
// 只会查找对象直接包含的属性,不查找[[Prototype]]链
Object.getOwnPropertyNames( v1 ); // [ "0", "1", "2", "length" ]
Object.getOwnPropertyNames( v2 ); // []
Object.getOwnPropertyNames( v3 ); // []
Object.getOwnPropertyNames( v4 ); // []
Object.getOwnPropertyNames( v5 ); 
// TypeError: Cannot convert undefined or null to object

上面代码说明:Object.getOwnPropertyNames(..)返回一个数组,保护焊所有属性,无论他们是否可以枚举,只会查找对象直接包含的属性,不查找[[prototype]]链。

但是这样是可以执行的:

// saucxs
var a = "abc";
var b = {
    v1: "def",
    v2: true,
    v3: 10,
    v4: Symbol("foo"),
    v5: null,
    v6: undefined
}

var obj = Object.assign(a, b); 
console.log(obj);
// { 
//   [String: "abc"]
//   v1: "def",
//   v2: true,
//   v3: 10,
//   v4: Symbol(foo),
//   v5: null,
//   v6: undefined 
// }

为什么?因为undefined,true等不适作为对象,而是作为对象b的属性值,对象b是可枚举的。

// saucxs
// 接上面的代码
Object.keys( b ); // [ "v1", "v2", "v3", "v4", "v5", "v6" ]

这里其实又可以看出一个问题来,那就是目标对象是原始类型,会包装成对象,对应上面的代码就是目标对象 a 会被包装成 [String: "abc"],那模拟实现时应该如何处理呢?很简单,使用 Object(..) 就可以了。

// saucxs
var a = "abc";
console.log( Object(a) );
// {0: "a", 1: "b", 2: "c"}

我们再来看看下面代码能不能执行:

// saucxs
var a = "abc";
var b = "def";
Object.assign(a, b); // TypeError: Cannot assign to read only property "0" of object "[object String]"

还是会报错的,原因在于:Object("abc")时候,其属性描述符writable为不可写,即writeable: false。

// saucxs
var myObject = Object( "abc" );

Object.getOwnPropertyNames( myObject );
// [ "0", "1", "2", "length" ]

Object.getOwnPropertyDescriptor(myObject, "0");
// { 
//   value: "a",
//   writable: false, // 注意这里
//   enumerable: true,
//   configurable: false 
// }
3.4 注意4:存在性

如何在不访问属性值的情况下判断对象中是否存在某个属性,看下面代码:

// saucxs
var anotherObject = {
    a: 1
};

// 创建一个关联到 anotherObject 的对象
var myObject = Object.create( anotherObject );
myObject.b = 2;

("a" in myObject); // true
("b" in myObject); // true

myObject.hasOwnProperty( "a" ); // false
myObject.hasOwnProperty( "b" ); // true

使用in和hasOwnProperty方法,区别如下:

1、in 操作符会检查属性是否在对象及其[[propertype]]原型链中;

2、hasOwnProperty(..)只会检查是否在myObject对象中,不会检查[[prototype]]原型链中。

Object.assign方法肯定是不会拷贝原型链上的属性,所以模拟实现时需要用hasOwnProperty(..)判断处理下,但是直接使用myObject.hasOwnProperty(..)是有问题的,因为有的对象可能没有连接到Object.prototype上(通过Object.create(null)来创建),这种情况下,使用myObject.hasOwnProperty(..)就会失败。

// saucxs
var myObject = Object.create( null );
myObject.b = 2;

("b" in myObject); 
// true

myObject.hasOwnProperty( "b" );
// TypeError: myObject.hasOwnProperty is not a function

解决办法,使用call就可以了,如下:

// saucxs
var myObject = Object.create( null );
myObject.b = 2;

Object.prototype.hasOwnProperty.call(myObject, "b");
// true
所以具体到本次模拟实现中,相关代码如下。

// saucxs
// 使用 for..in 遍历对象 nextSource 获取属性值
// 此处会同时检查其原型链上的属性
for (var nextKey in nextSource) {
    // 使用 hasOwnProperty 判断对象 nextSource 中是否存在属性 nextKey
    // 过滤其原型链上的属性
    if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
        // 赋值给对象 to,并在遍历结束后返回对象 to
        to[nextKey] = nextSource[nextKey];
    }
}
四、参考

1、MDN的Object.assign()

2、理解Object.assign()

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/109620.html

相关文章

  • 「前端面试题系列9」拷贝与深拷贝含义、区别及实现(文末有岗位内推哦~)

    摘要:深拷贝与浅拷贝的出现,就与这两个数据类型有关。这时,就需要用浅拷贝来实现了。数据一但过多,就会有递归爆栈的风险。这个方法是在解决递归爆栈问题的基础上,加以改进解决循环引用的问题。但如果你并不想保持引用,那就改用用于解决递归爆栈即可。 前言 这是前端面试题系列的第 9 篇,你可能错过了前面的篇章,可以在这里找到: 数组去重(10 种浓缩版) JavaScript 中的事件机制(从原生到...

    caige 评论0 收藏0
  • 【进阶4-2期】Object.assign 原理及其实现

    摘要:木易杨注意原始类型被包装为对象木易杨原始类型会被包装,和会被忽略。木易杨原因在于时,其属性描述符为不可写,即。木易杨解决方法也很简单,使用我们在进阶期中介绍的就可以了,使用如下。 引言 上篇文章介绍了赋值、浅拷贝和深拷贝,其中介绍了很多赋值和浅拷贝的相关知识以及两者区别,限于篇幅只介绍了一种常用深拷贝方案。 本篇文章会先介绍浅拷贝 Object.assign 的实现原理,然后带你手动实...

    layman 评论0 收藏0
  • JavaScript系列--JavaScript解析赋值、拷贝和深拷贝区别

    摘要:它将返回目标对象。有些文章说是深拷贝,其实这是不正确的。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。使用深拷贝的场景完全改变变量之后对没有任何影响,这就是深拷贝的魔力。 一、赋值(Copy) 赋值是将某一数值或对象赋给某个变量的过程,分为: 1、基本数据类型:赋值,赋值之后两个变量互不影响 2、引用数据类型:赋址,两个变量具有相同的引用,指向同一个对象,相互之间有...

    laznrbfe 评论0 收藏0
  • 【进阶4-1期】详细解析赋值、拷贝和深拷贝区别

    摘要:展开语法木易杨通过代码可以看出实际效果和是一样的。木易杨可以看出,改变之后的值并没有发生变化,但改变之后,相应的的值也发生变化。深拷贝使用场景木易杨完全改变变量之后对没有任何影响,这就是深拷贝的魔力。木易杨情况下,转换结果不正确。 一、赋值(Copy) 赋值是将某一数值或对象赋给某个变量的过程,分为下面 2 部分 基本数据类型:赋值,赋值之后两个变量互不影响 引用数据类型:赋址,两个...

    silvertheo 评论0 收藏0
  • JavaScript·随记 深拷贝 vs. 拷贝

    摘要:而在这个运算符的相关用例中,往往会涉及到其他知识点,深拷贝和浅拷贝就是其中之一。即对象的浅拷贝会对主对象的值进行拷贝,而该值有可能是一个指针,指向内存中的同一个对象。,可以看到深拷贝和浅拷贝是对复制引用类型变量而言的。 在ES6的系列文章中,基本都会提到Spread——扩展运算符(...)。而在这个运算符的相关用例中,往往会涉及到其他知识点,深拷贝和浅拷贝就是其中之一。 背景知识 在讨...

    RyanQ 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<