【javascript系列】Object.assign實現淺拷貝的原理以及實現

1、前言

以前在前面一篇學習了賦值,淺拷貝和深拷貝。介紹了這三者的相關知識和區別。html

傳送門:www.mwcxs.top/page/592.ht…node

本文會介紹淺拷貝Object.assign()的實現原理,而後我們試着實現一個淺拷貝。數組

2、淺拷貝Object.assign()

什麼是淺拷貝?淺拷貝就是建立一個新對象,這個對象有着原始對象屬性值的一份精確拷貝。bash

淺拷貝Object.assign()是什麼?主要將全部可枚舉屬性的值從一個或者多個數據源對象複製到目標對象,同時返回目標對象。函數

語法規則:學習

Object.assign(target,...sources)
複製代碼

其中target是目標對象,source是源對象,能夠是多個,修改返回的是目標對象target。測試

一、若是目標對象中的屬性具備相同的屬性鍵,則屬性將被源對象中的屬性覆蓋;ui

二、源對象的屬相將相似覆蓋早先的屬性。es5

強調兩點:spa

一、可枚舉的屬性(自有屬性)

二、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"}
// }
複製代碼

分析:

一、第一步中,使用Object.assign把源對象b的值複製到目標對象a中,這裏把返回值定義爲對象c,能夠看出b會替換掉a中具備相同鍵的值,即若是目標對象a中的屬性具備相同的鍵,則屬相將被源對象b中的屬性覆蓋。返回的對象c就是目標對象a。

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

三、第三步中,淺拷貝以後目標對象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
複製代碼

3、Object.assign模擬實現

實現Object.assign模擬實現大體思路:

一、判斷原生的Object是否支持assign這個函數,若是不存在的話就會建立一個assign函數,並使用Object.defineProperty將函數綁定到Object上。

二、判斷參數是否正確(目標參數不能爲空,能夠直接設置{}傳遞進去,可是必須有值)。

三、使用Object()轉成對象,並保存爲to,最後返回這個對象to。

四、使用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" }
複製代碼

上面代碼中的源對象 v二、v三、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方法,區別以下:

一、in 操做符會檢查屬性是否在對象及其[[propertype]]原型鏈中;

二、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];
    }
}
複製代碼

4、參考

一、MDN的Object.assign()

二、理解Object.assign()

相關文章
相關標籤/搜索