当前位置:   article > 正文

JavaScript系列—Object.assign()介绍以及原理实现_js object.assign()

js object.assign()

 

Object.assign()主要是将所有可枚举属性的值从一个或多个源对象复制到目标对象,同时返回目标对象

语法如下所示:

Object.assign(target, ...sources)

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

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

示例1

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

  1. // 木易杨
  2. // 第一步
  3. let a = {
  4.     name: "advanced",
  5.     age: 18
  6. }
  7. let b = {
  8.     name: "muyiy",
  9.     book: {
  10.         title: "You Don't Know JS",
  11.         price: "45"
  12.     }
  13. }
  14. let c = Object.assign(a, b);
  15. console.log(c);
  16. // {
  17. //     name: "muyiy",
  18. //  age: 18,
  19. //     book: {title: "You Don't Know JS", price: "45"}
  20. // } 
  21. console.log(a === c);
  22. // true
  23. // 第二步
  24. b.name = "change";
  25. b.book.price = "55";
  26. console.log(b);
  27. // {
  28. //     name: "change",
  29. //     book: {title: "You Don't Know JS", price: "55"}
  30. // } 
  31. // 第三步
  32. console.log(a);
  33. // {
  34. //     name: "muyiy",
  35. //  age: 18,
  36. //     book: {title: "You Don't Know JS", price: "55"}
  37. // } 

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

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

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

示例2

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

  1. // 木易杨
  2. // 第一步
  3. let a = {
  4.     name: "muyiy",
  5.     age: 18
  6. }
  7. let b = {
  8.     b1: Symbol("muyiy"),
  9.     b2null,
  10.     b3: undefined
  11. }
  12. let c = Object.assign(a, b);
  13. console.log(c);
  14. // {
  15. //     name: "muyiy",
  16. //  age: 18,
  17. //     b1: Symbol(muyiy),
  18. //     b2null,
  19. //     b3: undefined
  20. // } 
  21. console.log(a === c);
  22. // 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 。

  1. // 木易杨
  2. if (typeof Object.assign2 != 'function') {
  3.   // Attention 1
  4.   Object.defineProperty(Object"assign2", {
  5.     valuefunction (target) {
  6.       'use strict';
  7.       if (target == null) { // Attention 2
  8.         throw new TypeError('Cannot convert undefined or null to object');
  9.       }
  10.       // Attention 3
  11.       var to = Object(target);
  12.       for (var index = 1index < arguments.lengthindex++) {
  13.         var nextSource = arguments[index];
  14.         if (nextSource != null) {  // Attention 2
  15.           // Attention 4
  16.           for (var nextKey in nextSource) {
  17.             if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
  18.               to[nextKey] = nextSource[nextKey];
  19.             }
  20.           }
  21.         }
  22.       }
  23.       return to;
  24.     },
  25.     writable: true,
  26.     configurable: true
  27.   });
  28. }

测试一下

  1. // 木易杨
  2. // 测试用例
  3. let a = {
  4.     name: "advanced",
  5.     age: 18
  6. }
  7. let b = {
  8.     name: "muyiy",
  9.     book: {
  10.         title: "You Don't Know JS",
  11.         price: "45"
  12.     }
  13. }
  14. let c = Object.assign2(a, b);
  15. console.log(c);
  16. // {
  17. //     name: "muyiy",
  18. //  age: 18,
  19. //     book: {title: "You Don't Know JS", price: "45"}
  20. // } 
  21. console.log(a === c);
  22. // true

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

注意1:可枚举性

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

  1. // 木易杨
  2. for(var i in Object) {
  3.     console.log(Object[i]);
  4. }
  5. // 无输出
  6. Object.keys( Object );
  7. // []

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

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

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

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

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

  1. // 木易杨
  2. Object.a = function () {
  3.     console.log("log a");
  4. }
  5. Object.getOwnPropertyDescriptor(Object"a");
  6. // {
  7. //     value: ƒ, 
  8. //  writable: true
  9. //  enumerable: true,  // 注意这里是 true
  10. //  configurable: true
  11. // }
  12. Object.propertyIsEnumerable("a");
  13. // true

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

  1. // 木易杨
  2. Object.defineProperty(Object"b", {
  3.     valuefunction() {
  4.         console.log("log b");
  5.     }
  6. });
  7. Object.getOwnPropertyDescriptor(Object"b");
  8. // {
  9. //     value: ƒ, 
  10. //  writable: false,     // 注意这里是 false
  11. //  enumerable: false,  // 注意这里是 false
  12. //  configurable: false    // 注意这里是 false
  13. // }

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

  1. // 木易杨
  2. // 判断原生 Object 中是否存在函数 assign2
  3. if (typeof Object.assign2 != 'function') {
  4.   // 使用属性描述符定义新属性 assign2
  5.   Object.defineProperty(Object"assign2", {
  6.     valuefunction (target) { 
  7.       ...
  8.     },
  9.     // 默认值是 false,即 enumerable: false
  10.     writable: true,
  11.     configurable: true
  12.   });
  13. }

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

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

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

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

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

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

  1. // 木易杨
  2. var v1 = "abc";
  3. var v2 = true;
  4. var v3 = 10;
  5. var v4 = Symbol("foo");
  6. var obj = Object.assign({}, v1null, v2, undefined, v3, v4); 
  7. // 原始类型会被包装,null 和 undefined 会被忽略。
  8. // 注意,只有字符串的包装对象才可能有自身可枚举属性。
  9. console.log(obj); 
  10. // { "0""a""1""b""2""c" }

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

  1. // 木易杨
  2. var v1 = "abc";
  3. var v2 = true;
  4. var v3 = 10;
  5. var v4 = Symbol("foo");
  6. var v5 = null;
  7. // Object.keys(..) 返回一个数组,包含所有可枚举属性
  8. // 只会查找对象直接包含的属性,不查找[[Prototype]]链
  9. Object.keys( v1 ); // [ '0''1''2' ]
  10. Object.keys( v2 ); // []
  11. Object.keys( v3 ); // []
  12. Object.keys( v4 ); // []
  13. Object.keys( v5 ); 
  14. // TypeError: Cannot convert undefined or null to object
  15. // Object.getOwnPropertyNames(..) 返回一个数组,包含所有属性,无论它们是否可枚举
  16. // 只会查找对象直接包含的属性,不查找[[Prototype]]链
  17. Object.getOwnPropertyNames( v1 ); // [ '0''1''2''length' ]
  18. Object.getOwnPropertyNames( v2 ); // []
  19. Object.getOwnPropertyNames( v3 ); // []
  20. Object.getOwnPropertyNames( v4 ); // []
  21. Object.getOwnPropertyNames( v5 ); 
  22. // TypeError: Cannot convert undefined or null to object

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

  1. // 木易杨
  2. var a = "abc";
  3. var b = {
  4.     v1"def",
  5.     v2true,
  6.     v310,
  7.     v4: Symbol("foo"),
  8.     v5null,
  9.     v6: undefined
  10. }
  11. var obj = Object.assign(a, b); 
  12. console.log(obj);
  13. // { 
  14. //   [String'abc']
  15. //   v1'def',
  16. //   v2true,
  17. //   v310,
  18. //   v4: Symbol(foo),
  19. //   v5null,
  20. //   v6: undefined 
  21. // }

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

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

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

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

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

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

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

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

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

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

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

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

注意4:存在性

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

  1. // 木易杨
  2. var anotherObject = {
  3.     a: 1
  4. };
  5. // 创建一个关联到 anotherObject 的对象
  6. var myObject = Object.create( anotherObject );
  7. myObject.b = 2;
  8. ("a" in myObject); // true
  9. ("b" in myObject); // true
  10. myObject.hasOwnProperty( "a" ); // false
  11. 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(..) 就会失败。

  1. // 木易杨
  2. var myObject = Object.create( null );
  3. myObject.b = 2;
  4. ("b" in myObject); 
  5. // true
  6. myObject.hasOwnProperty( "b" );
  7. // TypeError: myObject.hasOwnProperty is not a function

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

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

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

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

华丽分割线---------------------------------------------------------------------------------------------------------------------------------------------------------

前置知识(own)

关于call

首先了解一下call()怎么用,看了以下例子可以粗暴的理解为里面的对象可以拥有外面对象的所有的属性和方法

  1. function sum(num1, num2){
  2. return num1 + num2;
  3. }
  4. function callSum(num1, num2){
  5. return sum.call(this, num1, num2);
  6. }
  7. alert(callSum(10,10)); //20
  1. window.color = "red";
  2. var o = { color: "blue" };
  3. alert(this.color);
  4. function sayColor(){ }
  5. sayColor(); //red
  6. sayColor.call(this); //red
  7. sayColor.call(window); //red
  8. sayColor.call(o); //blue
  1. function Person(name,age) {
  2. this.name=name;
  3. this.age=age;
  4. }
  5. var Student=function(name,age,gender) {
  6. Person.call(this,name,age);//this继承了person的属性和方法
  7. this.gender=gender;
  8. }
  9. var student=new Student("陈安东", 20, "男");
  10. alert("姓名:"+student.name+"\n"+"年龄:"+student.age+"\n"+"性别:"+student.gender);

 所以

Object.prototype.hasOwnProperty.call(myObject, "b");也可以理解为myObject.hasOwnProperty(b)

关于 Object.defineProperty()

  1. var person = {};
  2. Object.defineProperty(person, "name", {
  3. writable: false,
  4. value: "Nicholas"
  5. });
  6. alert(person.name); //"Nicholas"
  7. person.name = "Greg";
  8. alert(person.name); //"Nicholas"

Configurable 表示能否通过 delete 删除属性从而重新定义属性或者直接在对象上定义的属性

Enumerable  表示能否通过 for-in 循环返回属性(是否可枚举)

Writable  表示能否修改属性的值

Value   包含这个属性的数据值。

注意: assign作为object上面的属性是不可枚举的,所以使用(模拟实现的assign用)Object.defineProperty来定义

enumerable: false 以及 writable: true, configurable: true。(当然默认情况下不设置就是  false。)

原始类型被包装为对象

这里整理一下容易出错的地方

第一个

  1. // 木易杨
  2. var a = "abc";
  3. var b = {
  4. v1: "def",
  5. v2: true,
  6. v3: 10,
  7. v4: Symbol("foo"),
  8. v5: null,
  9. v6: undefined
  10. }
  11. var obj = Object.assign(a, b);
  12. console.log(obj);
  13. // {
  14. // [String: 'abc']
  15. // v1: 'def',
  16. // v2: true,
  17. // v3: 10,
  18. // v4: Symbol(foo),
  19. // v5: null,
  20. // v6: undefined
  21. // }

第二个 

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

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

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

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

第三个

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

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

但是把上面的var b={0: "d"}改成var b ={v1: "d"}就可以了。也就是他的key值不能是boolean

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/68084
推荐阅读
相关标签
  

闽ICP备14008679号