最近在 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) {
// 相关操做
}
复制代码