最近在 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++ 來編譯實現,其變量劃分爲基本數據類型和引用類型。 基本數據類型包括:函數
這些類型在內存中分別佔有固定大小的空間,他們的值保存在棧空間,經過按值訪問、拷貝和比較。測試
引用類型包括:ui
這些類型的值大小不固定,棧內存中存放地址指向堆內存中的對象,是按引用訪問的,說白了就跟 C 語言的指針同樣的道理。this
對於引用類型變量,棧內存中存放的知識該對象的訪問地址,在堆內存中爲該值分配空間,因爲這種值的大小不固定,所以不能把他們保存到棧內存中;但內存地址大小是固定的,所以能夠將堆內存地址保存到棧內存中。這樣,當查詢引用類型的變量時,就先從棧中讀取堆內存地址,而後再根據該地址取出對應的值。
很顯而易見的一點就是,JavaScript 中全部引用類型建立實例時,都是顯式或隱式地 new 出對應類型的實例,實際上就是對應 C 語言的 malloc
分配內存函數。
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
,也就都是對象。
js 中的拷貝區分爲「淺拷貝」與「深拷貝」。
淺拷貝只會將對象的各個屬性進行依次複製,並不會進行遞歸複製,也就是說只會賦值目標對象的第一層屬性。
對於目標對象第一層爲基本數據類型的數據,就是直接賦值,即「傳值」; 而對於目標對象第一層爲引用數據類型的數據,就是直接賦存於棧內存中的堆內存地址,即「傳址」。
深拷貝不一樣於淺拷貝,它不僅拷貝目標對象的第一層屬性,而是遞歸拷貝目標對象的全部屬性。
通常來講,在JavaScript中考慮複合類型的深層複製的時候,每每就是指對於 Date
、Object
與 Array
這三個複合類型的處理。咱們能想到的最經常使用的方法就是先建立一個空的新對象,而後遞歸遍歷舊對象,直到發現基礎類型的子節點才賦予到新對象對應的位置。
不過這種方法會存在一個問題,就是 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@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返回一個深層次的副本,遞歸地複製找到的任何對象;不然的話,副本會與原對象共享結構。 未定義的屬性將不會被複制,然而從對象的原型繼承的屬性將會被複制。
Object.assign
方法能夠把 任意多個的源對象所擁有的自身可枚舉屬性 拷貝給目標對象,而後返回目標對象。
注意:
getter
函數,而後把獲得的值拷貝給目標對象,若是你想拷貝訪問器屬性自己,請使用 Object.getOwnPropertyDescriptor()
和 Object.defineProperties()
方法;TypeError
異常,拷貝過程當中斷,已經拷貝成功的屬性不會受到影響,還未拷貝的屬性將不會再被拷貝;null
或 undefined
的源對象;var dest = JSON.parse(JSON.stringify(target));
複製代碼
一樣的它也有缺點: 該方法會忽略掉值爲 undefined
的屬性以及函數表達式,但不會忽略值爲 null
的屬性。
在項目實踐中,發現有起碼有如下兩種方式能夠來規避原型鏈屬性上的拷貝。
最經常使用的方式:
for (let key in targetObj) {
if (targetObj.hasOwnProperty(key)) {
// 相關操做
}
}
複製代碼
缺點:遍歷了原型鏈上的全部屬性,效率不高;
如下都是 ES6 的方式:
const keys = Object.keys(targetObj);
keys.map((key)=>{
// 相關操做
});
複製代碼
注意:只會返回參數對象自身的(不含繼承的)全部可遍歷(enumerable)屬性的鍵名所組成的數組。
另闢蹊徑:
const obj = Object.create(null);
target.__proto__ = Object.create(null);
for (let key in target) {
// 相關操做
}
複製代碼