最近在 vue 框架下写业务代码, 不可避免地涉及到对象深浅拷贝的问题, 趁机会总结记录一下.
由于微信文章平台只能再重新编辑一次, 以后文章有更新的话, 会更新到我自己的个人博客, 有兴趣的可以围观下: 个人博客地址: http://blog.ironmaxi.com
内存的堆区与栈区
首先要讲一下大家耳熟能详的堆栈, 要区分一下数据结构和内存中的堆栈定义.
数据结构中的堆和栈是两种不同的, 数据项按序排列的数据结构.
而我们重点要讲的是内存中的堆区与栈区.
在 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
这些类型在内存中分别占有固定大小的空间, 他们的值保存在栈空间, 通过按值访问, 拷贝和比较.
引用类型包括:
- object
- array
- function
- error
- date
这些类型的值大小不固定, 栈内存中存放地址指向堆内存中的对象, 是按引用访问的, 说白了就跟 C 语言的指针一样的道理.
对于引用类型变量, 栈内存中存放的知识该对象的访问地址, 在堆内存中为该值分配空间, 由于这种值的大小不固定, 因此不能把他们保存到栈内存中; 但内存地址大小是固定的, 因此可以将堆内存地址保存到栈内存中. 这样, 当查询引用类型的变量时, 就先从栈中读取堆内存地址, 然后再根据该地址取出对应的值.
很显而易见的一点就是, 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 中考虑复合类型的深层复制的时候, 往往就是指对于 Date ,Object 与 Array 这三个复合类型的处理. 我们能想到的最常用的方法就是先创建一个空的新对象, 然后递归遍历旧对象, 直到发现基础类型的子节点才赋予到新对象对应的位置.
不过这种方法会存在一个问题, 就是 JavaScript 中存在着神奇的原型机制, 并且这个原型会在遍历的时候出现, 然后需要考虑原型应不应该被赋予给新对象. 那么在遍历的过程中, 我们可以考虑使用 hasOenProperty 方法来判断是否过滤掉那些继承自原型链上的属性.
动手实现一份浅拷贝加扩展的函数
- 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 方法可以把 任意多个的源对象所拥有的自身可枚举属性 拷贝给目标对象, 然后返回目标对象.
注意:
对于访问器属性, 该方法会执行那个访问器属性的 getter 函数, 然后把得到的值拷贝给目标对象, 如果你想拷贝访问器属性本身, 请使用
Object.getOwnPropertyDescriptor()
和
Object.defineProperties()
方法;
字符串类型和 symbol 类型的属性都会被拷贝;
在属性拷贝过程中可能会产生异常, 比如目标对象的某个只读属性和源对象的某个属性同名, 这时该方法会抛出一个 TypeError 异常, 拷贝过程中断, 已经拷贝成功的属性不会受到影响, 还未拷贝的属性将不会再被拷贝;
该方法会跳过那些值为 null 或 undefined 的源对象;
利用 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) {
- // 相关操作
- }
来源: https://juejin.im/post/5acc7e606fb9a028c67609f7