js 深淺拷貝知多少

最近在 vue 框架下寫業務代碼,不可避免地涉及到對象深淺拷貝的問題,趁機會總結記錄一下。vue

因爲微信文章平臺只能再從新編輯一次,之後文章有更新的話,會更新到我本身的我的博客,有興趣的能夠圍觀下: 我的博客地址:blog.ironmaxi.com數組

內存的堆區與棧區

首先要講一下你們耳熟能詳的「堆棧」,要區分一下數據結構和內存中的「堆棧」定義。bash

數據結構中的堆和棧是兩種不一樣的、數據項按序排列的數據結構。微信

而咱們重點要講的是內存中的堆區與棧區。數據結構

在 C 語言中,棧區分配局部變量空間,而堆區是地址向上增加的用於分配程序猿申請的內存空間,另外還有靜態區是分配靜態變量、全局變量空間的;只讀區是分配常量和程序代碼空間的。如下舉個簡單的例子:框架

int a = 0; // 全局初始化區
char *p1; // 全局未初始化區
main()
{
  int b; // 棧
  char s[] = "abc"; // 棧
  char *p2; // 棧
  char *p3 = "123456"; // 在常量區,p3在棧上。
  static int c =0; // 全局(靜態)初始化區
  p1 = (char *)malloc(10); // 堆
  p2 = (char *)malloc(20); // 堆
}
複製代碼

而 JavaScript 是高級語言,底層依舊依靠 C/C++ 來編譯實現,其變量劃分爲基本數據類型和引用類型。 基本數據類型包括:函數

  • undefined
  • null
  • boolean
  • number
  • string

這些類型在內存中分別佔有固定大小的空間,他們的值保存在棧空間,經過按值訪問、拷貝和比較。測試

引用類型包括:ui

  • object
  • array
  • function
  • error
  • date

這些類型的值大小不固定,棧內存中存放地址指向堆內存中的對象,是按引用訪問的,說白了就跟 C 語言的指針同樣的道理。this

對於引用類型變量,棧內存中存放的知識該對象的訪問地址,在堆內存中爲該值分配空間,因爲這種值的大小不固定,所以不能把他們保存到棧內存中;但內存地址大小是固定的,所以能夠將堆內存地址保存到棧內存中。這樣,當查詢引用類型的變量時,就先從棧中讀取堆內存地址,而後再根據該地址取出對應的值。

很顯而易見的一點就是,JavaScript 中全部引用類型建立實例時,都是顯式或隱式地 new 出對應類型的實例,實際上就是對應 C 語言的 malloc 分配內存函數。

JavaScript 中變量的賦值

js 中變量的賦值分爲「傳值」與「傳址」。

給變量賦基本數據類型的值,就是「傳值」;而給變量賦引用數據類型的值,其實是「傳址」。

基本數據類型變量的賦值、比較,只是值的賦值和比較,也即棧內存中的數據的拷貝和比較,參見以下直觀的代碼:

var num1 = 123;
var num2 = 123;
var num3 = num1;
num1 === num2; // true
num1 === num3; // true
num1 = 456;
num1 === num2; // false
num1 === num3; // false
複製代碼

引用數據類型變量的賦值、比較,只是存於棧內存中的堆內存地址的拷貝、比較,參加以下直觀的代碼:

var arr1 = [1, 2, 3];
var arr2 = [1, 2, 3];
var arr3 = arr1;
arr1 === arr2; // false
arr1 === arr3; // true
arr1 = [1, 2, 3];
arr1 === arr2; // false
arr1 === arr3; // false
複製代碼

再說起一個要點,js 中全部引用數據類型的頂級原型,都是 Object,也就都是對象。

JavaScript 中變量的拷貝

js 中的拷貝區分爲「淺拷貝」與「深拷貝」。

淺拷貝

淺拷貝只會將對象的各個屬性進行依次複製,並不會進行遞歸複製,也就是說只會賦值目標對象的第一層屬性。

對於目標對象第一層爲基本數據類型的數據,就是直接賦值,即「傳值」; 而對於目標對象第一層爲引用數據類型的數據,就是直接賦存於棧內存中的堆內存地址,即「傳址」。

深拷貝

深拷貝不一樣於淺拷貝,它不僅拷貝目標對象的第一層屬性,而是遞歸拷貝目標對象的全部屬性。

通常來講,在JavaScript中考慮複合類型的深層複製的時候,每每就是指對於 DateObjectArray 這三個複合類型的處理。咱們能想到的最經常使用的方法就是先建立一個空的新對象,而後遞歸遍歷舊對象,直到發現基礎類型的子節點才賦予到新對象對應的位置。

不過這種方法會存在一個問題,就是 JavaScript 中存在着神奇的原型機制,而且這個原型會在遍歷的時候出現,而後須要考慮原型應不該該被賦予給新對象。那麼在遍歷的過程當中,咱們能夠考慮使用 hasOwnProperty 方法來判斷是否過濾掉那些繼承自原型鏈上的屬性。

動手實現一份淺拷貝加擴展的函數

function _isPlainObject(target) {
  return (typeof target === 'object' && !!target && !Array.isArray(target));
}
function shallowExtend() {
  var args = Array.prototype.slice.call(arguments);
  // 第一個參數做爲target
  var target = args[0];
  var src;

  target = _isPlainObject(target) ? target : {};
  for (var i=1;i<args.length;i++) {
    src = args[i];
    if (!_isPlainObject(src)) {
      continue;
    }
    for(var key in src) {
      if (src.hasOwnProperty(key)) {
        if (src[key] != undefined) {
          target[key] = src[key];
        }
      }
    }
  }

  return target;
}
複製代碼

測試用例:

// 初始化引用數據類型變量
var target = {
  key: 'value',
  num: 1,
  bool: false,
  arr: [1, 2, 3],
  obj: {
    objKey: 'objValue'
  },
};
// 拷貝+擴展
var result = shallowExtend({}, target, {
  key: 'valueChanged',
  num: 2,
  bool: true,
});
// 對原引用類型數據作修改
target.arr.push(4);
target.obj['objKey2'] = 'objValue2';
// 比較基本數據類型的屬性值
result === target; // false
result.key === target.key;  // false
result.num === target.num;  // false
result.bool === target.bool;// false
// 比較引用數據類型的屬性值
result.arr === target.arr;  // true
result.obj === target.obj;  // true
複製代碼

jQuery.extend 實現深淺拷貝加擴展功能

貼下 jQuery@3.3.1 中 jQuery.extend 的實現:

jQuery.extend = jQuery.fn.extend = function() {
  var options,
    name,
    src,
    copy,
    copyIsArray,
    clone,
    target = arguments[0] || {},
    i = 1,
    length = arguments.length,
    deep = false;

  // 若是第一個參數是布爾值,則爲判斷是否深拷貝的標誌變量
  if (typeof target === "boolean") {
    deep = target;
    // 跳過 deep 標誌變量,留意上面 i 的初始值爲1
    target = arguments[i] || {};
    // i 自增1
    i++;
  }

  // 判斷 target 是否爲 object / array / function 之外的類型變量
  if (typeof target !== "object" && !isFunction(target)) {
    // 若是是其它類型變量,則強制從新賦值爲新的空對象
    target = {};
  }

  // 若是隻傳入1個參數;或者是傳入2個參數,第一個參數爲 deep 變量,第二個爲 target
  // 因此 length 的值可能爲 1 或 2,但不管是 1 或 2,下段 for 循環只會運行一次
  if (i === length) {
    // 將 jQuery 自己賦值給 target
    target = this;
    // i 自減1,可能的值爲 0 或 1
    i--;
  }

  for (; i < length; i++) {
    // 如下拷貝操做,只針對非 null 或 undefined 的 arguments[i] 進行
    if ((options = arguments[i]) != null) {
      // Extend the base object
      for (name in options) {
        src = target[name];
        copy = options[name];
        // 避免死循環的狀況
        if (target === copy) {
          continue;
        }
        // Recurse if we're merging plain objects or arrays
        // 若是是深拷貝,且copy值有效,且copy值爲純object或純array
        if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = Array.isArray(copy)))) {
          if (copyIsArray) {
            // 數組狀況
            copyIsArray = false;
            clone = src && Array.isArray(src)
              ? src
              : [];
          } else {
            // 對象狀況
            clone = src && jQuery.isPlainObject(src)
              ? src
              : {};
          }
          // 克隆copy對象到原對象並賦值回原屬性,而不是從新賦值
          // 遞歸調用
          target[name] = jQuery.extend(deep, clone, copy);

          // Don't bring in undefined values
        } else if (copy !== undefined) {
          target[name] = copy;
        }
      }
    }
  }
  // Return the modified object
  return target;
};
複製代碼

該方法的做用是用一個或多個其餘對象來擴展一個對象,返回被擴展的對象。

若是不指定target,則給jQuery命名空間自己進行擴展。這有助於插件做者爲jQuery增長新方法。

若是第一個參數設置爲true,則jQuery返回一個深層次的副本,遞歸地複製找到的任何對象;不然的話,副本會與原對象共享結構。 未定義的屬性將不會被複制,然而從對象的原型繼承的屬性將會被複制

ES6 實現深淺拷貝

Object.assign

Object.assign 方法能夠把 任意多個的源對象所擁有的自身可枚舉屬性 拷貝給目標對象,而後返回目標對象。

注意:

  1. 對於訪問器屬性,該方法會執行那個訪問器屬性的 getter 函數,而後把獲得的值拷貝給目標對象,若是你想拷貝訪問器屬性自己,請使用 Object.getOwnPropertyDescriptor()Object.defineProperties() 方法;
  2. 字符串類型和 symbol 類型的屬性都會被拷貝;
  3. 在屬性拷貝過程當中可能會產生異常,好比目標對象的某個只讀屬性和源對象的某個屬性同名,這時該方法會拋出一個 TypeError 異常,拷貝過程當中斷,已經拷貝成功的屬性不會受到影響,還未拷貝的屬性將不會再被拷貝;
  4. 該方法會跳過那些值爲 nullundefined 的源對象;

利用 JSON 進行忽略原型鏈的深拷貝

var dest = JSON.parse(JSON.stringify(target));
複製代碼

一樣的它也有缺點: 該方法會忽略掉值爲 undefined 的屬性以及函數表達式,但不會忽略值爲 null 的屬性。

再談原型鏈屬性

在項目實踐中,發現有起碼有如下兩種方式能夠來規避原型鏈屬性上的拷貝。

方式1

最經常使用的方式:

for (let key in targetObj) {
  if (targetObj.hasOwnProperty(key)) {
    // 相關操做
  }
}
複製代碼

缺點:遍歷了原型鏈上的全部屬性,效率不高;

方式2

如下都是 ES6 的方式:

const keys = Object.keys(targetObj);
keys.map((key)=>{
  // 相關操做
});
複製代碼

注意:只會返回參數對象自身的(不含繼承的)全部可遍歷(enumerable)屬性的鍵名所組成的數組。

方式3

另闢蹊徑:

const obj = Object.create(null);
target.__proto__ = Object.create(null);
for (let key in target) {
  // 相關操做
}
複製代碼

微信公衆號

相關文章
相關標籤/搜索