最近在 vue 框架下写业务代码,不可避免地涉及到对象深浅拷贝的问题,趁机会总结记录一下。
内存的堆区与栈区
首先要讲一下大家耳熟能详的「堆栈」,要区分一下数据结构和内存中的「堆栈」定义。
数据结构中的堆和栈是两种不同的、数据项按序排列的数据结构。
而我们重点要讲的是内存中的堆区与栈区。
在 C 语言中,栈区分配局部变量空间,而堆区是地址向上增长的用于分配程序猿申请的内存空间,另外还有静态区是分配静态变量、全局变量空间的;只读区是分配常量和程序代码空间的。以下举个简单的例子:
而 JavaScript 是高级语言,底层依旧依靠 C/C++ 来编译实现,其变量划分为基本数据类型和引用类型。 基本数据类型包括:
undefined
null
boolean
number
string
这些类型在内存中分别占有固定大小的空间,他们的值保存在栈空间,通过按值访问、拷贝和比较。
引用类型包括:
object
array
function
error
date
这些类型的值大小不固定,栈内存中存放地址指向堆内存中的对象,是按引用访问的,说白了就跟 C 语言的指针一样的道理。
对于引用类型变量,栈内存中存放的知识该对象的访问地址,在堆内存中为该值分配空间,由于这种值的大小不固定,因此不能把他们保存到栈内存中;但内存地址大小是固定的,因此可以将堆内存地址保存到栈内存中。这样,当查询引用类型的变量时,就先从栈中读取堆内存地址,然后再根据该地址取出对应的值。
很显而易见的一点就是,JavaScript 中所有引用类型创建实例时,都是显式或隐式地 new 出对应类型的实例,实际上就是对应 C 语言的 分配内存函数。
JavaScript 中变量的赋值
js 中变量的赋值分为「传值」与「传址」。
给变量赋基本数据类型的值,就是「传值」;而给变量赋引用数据类型的值,实际上是「传址」。
基本数据类型变量的赋值、比较,只是值的赋值和比较,也即栈内存中的数据的拷贝和比较,参见如下直观的代码:
引用数据类型变量的赋值、比较,只是存于栈内存中的堆内存地址的拷贝、比较,参加如下直观的代码:
再提及一个要点,js 中所有引用数据类型的顶级原型,都是 ,也就都是对象。
JavaScript 中变量的拷贝
js 中的拷贝区分为「浅拷贝」与「深拷贝」。
浅拷贝
浅拷贝只会将对象的各个属性进行依次复制,并不会进行递归复制,也就是说只会赋值目标对象的第一层属性。
对于目标对象第一层为基本数据类型的数据,就是直接赋值,即「传值」; 而对于目标对象第一层为引用数据类型的数据,就是直接赋存于栈内存中的堆内存地址,即「传值」。
深拷贝
深拷贝不同于浅拷贝,它不只拷贝目标对象的第一层属性,而是递归拷贝目标对象的所有属性。
一般来说,在JavaScript中考虑复合类型的深层复制的时候,往往就是指对于 、 与 这三个复合类型的处理。我们能想到的最常用的方法就是先创建一个空的新对象,然后递归遍历旧对象,直到发现基础类型的子节点才赋予到新对象对应的位置。
不过这种方法会存在一个问题,就是 JavaScript 中存在着神奇的原型机制,并且这个原型会在遍历的时候出现,然后需要考虑原型应不应该被赋予给新对象。那么在遍历的过程中,我们可以考虑使用 方法来判断是否过滤掉那些继承自原型链上的属性。
动手实现一份浅拷贝加扩展的函数
测试用例:
jQuery.extend 实现深浅拷贝加扩展功能
贴下 jQuery@3.3.1 中 的实现:
该方法的作用是用一个或多个其他对象来扩展一个对象,返回被扩展的对象。
如果不指定target,则给jQuery命名空间本身进行扩展。这有助于插件作者为jQuery增加新方法。
如果第一个参数设置为true,则jQuery返回一个深层次的副本,递归地复制找到的任何对象;否则的话,副本会与原对象共享结构。 未定义的属性将不会被复制,然而从对象的原型继承的属性将会被复制。
ES6 实现深浅拷贝
Object.assign
方法可以把任意多个的源对象所拥有的自身可枚举属性拷贝给目标对象,然后返回目标对象。
注意:
对于访问器属性,该方法会执行那个访问器属性的 函数,然后把得到的值拷贝给目标对象,如果你想拷贝访问器属性本身,请使用 和 方法;
字符串类型和 symbol 类型的属性都会被拷贝;
在属性拷贝过程中可能会产生异常,比如目标对象的某个只读属性和源对象的某个属性同名,这时该方法会抛出一个 异常,拷贝过程中断,已经拷贝成功的属性不会受到影响,还未拷贝的属性将不会再被拷贝;
该方法会跳过那些值为 或 的源对象;
利用 JSON 进行忽略原型链的深拷贝
同样的它也有缺点: 该方法会忽略掉值为 的属性以及函数表达式,但不会忽略值为 的属性。
再谈原型链属性
在项目实践中,发现有起码有以下两种方式可以来规避原型链属性上的拷贝。
方式1
最常用的方式:
缺点:遍历了原型链上的所有属性,效率不高;
方式2
以下是 ES6 的方式:
注意:只会返回参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名所组成的数组。
方式3
另辟蹊径:
领取专属 10元无门槛券
私享最新 技术干货