zoukankan      html  css  js  c++  java
  • 【JS】307- 复习 Object.assign 原理及其实现

    640?wx_fmt=png点击上方“前端自习课”关注,学习起来~

    640?wx_fmt=jpeg


    引言

    上篇文章介绍了赋值、浅拷贝和深拷贝,其中介绍了很多赋值和浅拷贝的相关知识以及两者区别,限于篇幅只介绍了一种常用深拷贝方案。

    本篇文章会先介绍浅拷贝 Object.assign 的实现原理,然后带你手动实现一个浅拷贝,并在文末留下一道面试题,期待你的评论。

    浅拷贝 Object.assign

    上篇文章介绍了其定义和使用,主要是将所有可枚举属性的值从一个或多个源对象复制到目标对象,同时返回目标对象。(来自 MDN)

    语法如下所示:

    Object.assign(target, ...sources)

    其中 target 是目标对象,sources 是源对象,可以有多个,返回修改后的目标对象 target

    如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后来的源对象的属性将类似地覆盖早先的属性。

    示例1

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

    // 木易杨// 第一步let a = {    name: "advanced",    age: 18}let b = {    name: "muyiy",    book: {        title: "You Don't Know JS",        price: "45"    }}let c = Object.assign(a, b);console.log(c);// {//     name: "muyiy",//  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: "muyiy",//  age: 18,//     book: {title: "You Don't Know JS", price: "55"}// } 
    // 第一步
    let a = {
        name"advanced",
        age18
    }
    let b = {
        name"muyiy",
        book: {
            title"You Don't Know JS",
            price"45"
        }
    }
    let c = Object.assign(a, b);
    console.log(c);
    // {
    //     name: "muyiy",
    //  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: "muyiy",
    //  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

    String 类型和 Symbol 类型的属性都会被拷贝,而且不会跳过那些值为 nullundefined 的源对象。

    // 木易杨// 第一步let a = {    name: "muyiy",    age: 18}let b = {    b1: Symbol("muyiy"),    b2: null,    b3: undefined}let c = Object.assign(a, b);console.log(c);// {//     name: "muyiy",//  age: 18,//     b1: Symbol(muyiy),//     b2: null,//     b3: undefined// } console.log(a === c);// true
    // 第一步
    let a = {
        name"muyiy",
        age18
    }
    let b = {
        b1Symbol("muyiy"),
        b2null,
        b3undefined
    }
    let c = Object.assign(a, b);
    console.log(c);
    // {
    //     name: "muyiy",
    //  age: 18,
    //     b1: Symbol(muyiy),
    //     b2: null,
    //     b3: undefined
    // } 
    console.log(a === c);
    // true

    Object.assign 模拟实现

    实现一个 Object.assign 大致思路如下:

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

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

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

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

    实现代码如下,这里为了验证方便,使用 assign2 代替 assign。注意此模拟实现不支持 symbol 属性,因为ES5 中根本没有 symbol

    // 木易杨if (typeof Object.assign2 != 'function') {  // Attention 1  Object.defineProperty(Object, "assign2", {    value: function (target) {      'use strict';      if (target == null) { // Attention 2        throw new TypeError('Cannot convert undefined or null to object');      }      // Attention 3      var to = Object(target);      for (var index = 1; index < arguments.length; index++) {        var nextSource = arguments[index];        if (nextSource != null) {  // Attention 2          // Attention 4          for (var nextKey in nextSource) {            if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {              to[nextKey] = nextSource[nextKey];            }          }        }      }      return to;    },    writable: true,    configurable: true  });}
    if (typeof Object.assign2 != 'function') {
      // Attention 1
      Object.defineProperty(Object"assign2", {
        valuefunction (target{
          'use strict';
          if (target == null) { // Attention 2
            throw new TypeError('Cannot convert undefined or null to object');
          }

          // Attention 3
          var to = Object(target);

          for (var index = 1; index < arguments.length; index++) {
            var nextSource = arguments[index];

            if (nextSource != null) {  // Attention 2
              // Attention 4
              for (var nextKey in nextSource) {
                if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
                  to[nextKey] = nextSource[nextKey];
                }
              }
            }
          }
          return to;
        },
        writabletrue,
        configurabletrue
      });
    }

    测试一下

    // 木易杨// 测试用例let a = {    name: "advanced",    age: 18}let b = {    name: "muyiy",    book: {        title: "You Don't Know JS",        price: "45"    }}let c = Object.assign2(a, b);console.log(c);// {//     name: "muyiy",//  age: 18,//     book: {title: "You Don't Know JS", price: "45"}// } console.log(a === c);// true
    // 测试用例
    let a = {
        name"advanced",
        age18
    }
    let b = {
        name"muyiy",
        book: {
            title"You Don't Know JS",
            price"45"
        }
    }
    let c = Object.assign2(a, b);
    console.log(c);
    // {
    //     name: "muyiy",
    //  age: 18,
    //     book: {title: "You Don't Know JS", price: "45"}
    // } 
    console.log(a === c);
    // true

    针对上面的代码做如下扩展。

    注意1:可枚举性

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

    // 木易杨for(var i in Object) {    console.log(Object[i]);}// 无输出Object.keys( Object );// []
    for(var i in Object) {
        console.log(Object[i]);
    }
    // 无输出

    Object.keys( Object );
    // []

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

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

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

    // 方法2
    Object.propertyIsEnumerable("assign");
    // false

    上面代码说明 Object.assign 是不可枚举的。

    介绍这么多是因为直接在 Object 上挂载属性 a 之后是可枚举的,我们来看如下代码。

    // 木易杨Object.a = function () {    console.log("log a");}Object.getOwnPropertyDescriptor(Object, "a");// {//     value: ƒ, //  writable: true, //  enumerable: true,  // 注意这里是 true//  configurable: true// }Object.propertyIsEnumerable("a");// true
    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

    // 木易杨Object.defineProperty(Object, "b", {    value: function() {        console.log("log b");    }});Object.getOwnPropertyDescriptor(Object, "b");// {//     value: ƒ, //  writable: false,     // 注意这里是 false//  enumerable: false,  // 注意这里是 false//  configurable: false    // 注意这里是 false// }
    Object.defineProperty(Object"b", {
        valuefunction() {
            console.log("log b");
        }
    });

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

    所以具体到本次模拟实现中,相关代码如下。

    // 木易杨// 判断原生 Object 中是否存在函数 assign2if (typeof Object.assign2 != 'function') {  // 使用属性描述符定义新属性 assign2  Object.defineProperty(Object, "assign2", {    value: function (target) {       ...    },    // 默认值是 false,即 enumerable: false    writable: true,    configurable: true  });}
    // 判断原生 Object 中是否存在函数 assign2
    if (typeof Object.assign2 != 'function') {
      // 使用属性描述符定义新属性 assign2
      Object.defineProperty(Object"assign2", {
        valuefunction (target
          ...
        },
        // 默认值是 false,即 enumerable: false
        writable: true,
        configurabletrue
      });
    }

    注意2:判断参数是否正确

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

    // 木易杨if (target === undefined || target === null) {    throw new TypeError('Cannot convert undefined or null to object');}
    if (target === undefined || target === null) {
        throw new TypeError('Cannot convert undefined or null to object');
    }

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

    // 木易杨if (target == null) { // TypeError if undefined or null    throw new TypeError('Cannot convert undefined or null to object');}
    if (target == null) { // TypeError if undefined or null
        throw new TypeError('Cannot convert undefined or null to object');
    }

    注意3:原始类型被包装为对象

    // 木易杨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" }
    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 实际上被忽略了,原因在于他们自身没有可枚举属性

    // 木易杨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.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
    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.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

    但是下面的代码是可以执行的。

    // 木易杨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 // }
    var a = "abc";
    var b = {
        v1"def",
        v2true,
        v310,
        v4Symbol("foo"),
        v5null,
        v6undefined
    }

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

    原因很简单,因为此时 undefinedtrue 等不是作为对象,而是作为对象 b 的属性值,对象 b 是可枚举的。

    // 木易杨// 接上面的代码Object.keys( b ); // [ 'v1', 'v2', 'v3', 'v4', 'v5', 'v6' ]
    // 接上面的代码
    Object.keys( b ); // [ 'v1', 'v2', 'v3', 'v4', 'v5', 'v6' ]

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

    // 木易杨var a = "abc";console.log( Object(a) );// [String: 'abc']
    var a = "abc";
    console.log( Object(a) );
    // [String: 'abc']

    到这里已经介绍很多知识了,让我们再来延伸一下,看看下面的代码能不能执行。

    // 木易杨var a = "abc";var b = "def";Object.assign(a, b); 
    var a = "abc";
    var b = "def";
    Object.assign(a, b); 

    答案是否定的,会提示以下错误。

    // 木易杨TypeError: Cannot assign to read only property '0' of object '[object String]'
    TypeError: Cannot assign to read only property '0' of object '[object String]'

    原因在于 Object("abc") 时,其属性描述符为不可写,即  writable: false

    // 木易杨var myObject = Object( "abc" );Object.getOwnPropertyNames( myObject );// [ '0', '1', '2', 'length' ]Object.getOwnPropertyDescriptor(myObject, "0");// { //   value: 'a',//   writable: false, // 注意这里//   enumerable: true,//   configurable: false // }
    var myObject = Object"abc" );

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

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

    同理,下面的代码也会报错。

    // 木易杨var a = "abc";var b = {  0: "d"};Object.assign(a, b); // TypeError: Cannot assign to read only property '0' of object '[object String]'
    var a = "abc";
    var b = {
      0"d"
    };
    Object.assign(a, b); 
    // TypeError: Cannot assign to read only property '0' of object '[object String]'

    注意4:存在性

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

    // 木易杨var anotherObject = {    a: 1};// 创建一个关联到 anotherObject 的对象var myObject = Object.create( anotherObject );myObject.b = 2;("a" in myObject); // true("b" in myObject); // truemyObject.hasOwnProperty( "a" ); // falsemyObject.hasOwnProperty( "b" ); // true
    var anotherObject = {
        a1
    };

    // 创建一个关联到 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 方法,区别如下(你不知道的JS上卷 P119):

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

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

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

    // 木易杨var myObject = Object.create( null );myObject.b = 2;("b" in myObject); // truemyObject.hasOwnProperty( "b" );// TypeError: myObject.hasOwnProperty is not a function
    var myObject = Object.create( null );
    myObject.b = 2;

    ("b" in myObject); 
    // true

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

    解决方法也很简单,使用我们在【进阶3-3期】中介绍的 call 就可以了,使用如下。

    // 木易杨var myObject = Object.create( null );myObject.b = 2;Object.prototype.hasOwnProperty.call(myObject, "b");// true
    var myObject = Object.create( null );
    myObject.b = 2;

    Object.prototype.hasOwnProperty.call(myObject, "b");
    // true

    所以具体到本次模拟实现中,相关代码如下。

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

    本期思考题

    如何实现一个深拷贝?

    参考

    MDN 之 Object.assign()

    ES2015系列(二) 理解 Object.assign

    ▼原创系列推荐▼1.JavaScript 重温系列(22篇全)
    2.ECMAScript 重温系列(10篇全)
    3.JavaScript设计模式 重温系列(9篇全)
    4.正则 / 框架 / 算法等 重温系列(16篇全)

    640?wx_fmt=png

    640?wx_fmt=png你点的每个赞,我都认真当成了喜欢
    个人博客:http://www.pingan8787.com 微信公众号【前端自习课】和千万网友一起,每日清晨,享受一篇前端优秀文章。 目前已连续推送文章 600+ 天,愿每个人的初心都能一直坚持下去!
  • 相关阅读:
    C#中的委托和事件的使用
    C#中Attribute/特性的使用
    Eclipase + CDT
    设计模式总结
    Nginx源码编译
    Nginx自定义扩展模块
    电池的寿命
    大盗阿福
    河中跳房子
    An Easy Problem
  • 原文地址:https://www.cnblogs.com/pingan8787/p/11838139.html
Copyright © 2011-2022 走看看