深拷贝与浅拷贝都是Javascript中实现拷贝的一个方式,或许说拷贝有些同学可能不是很理解,其实我们也可以叫他们深复制与浅复制。那么接下来,让我们先了解一下什么是浅拷贝,什么是深拷贝。
浅拷贝指的是创建了一个指向原始对象的新对象,但是在该新对象中仍然与原始对象共享一些内部对象和内存地址。
举个例子:
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = Object.assign({}, obj1);
obj2.a = 3;
console.log(obj1.a); // 1
obj2.b.c = 4;
console.log(obj1.b.c); // 4
在刚刚这个例子里,我们就进行了一次浅拷贝。在拷贝过程中,对于基本数据类型a来说,两者之间可以说是毫无关联的,对obj2.a进行的更改,对obj1.a没有影响,但是对于引用数据类型b来说,两者的更改却是同步的。至于为什么会发生这种情况,会在后面讲到,此处我们只做概念的介绍。
1.Object.assign()
Object.assign()是一个 JavaScript 方法,它用于将一个或多个源对象的属性值复制到目标对象中。这种属性值的合并是浅拷贝的,也就是说只有属性值的引用被复制,而不会进行深拷贝。
Object.assign()方法的语法如下:
Object.assign(target, ...sourcrs)
2.Array.prototype.concat()
Array.prototype.concat()是 JavaScript 中 Array 对象的原型方法之一,用于合并两个或多个数组。这个方法可以创建一个新的数组,其中包含所有被合并的数组中的元素。它不会改变原来的数组,而是返回一个新的数组。
需要注意的是,如果被合并的数组中包含对象或数组等引用类型数据,在新数组中只会复制它们的引用地址。
3.扩展运算符
扩展运算符 (...) 在复制对象或数组时会进行浅拷贝。这意味着只有对象或数组的第一层属性被复制,而更深层次的嵌套对象或数组则保持引用关系。
深拷贝是指创建一个新的独立对象,完全复制原始对象及其嵌套所有数据,并将其存储在新对象中。与浅拷贝不同的是,如果所复制的对象或数组包含任何嵌套引用类型数据,则这些嵌套数据也必须被递归地复制以达到完全独立的效果。
我们可以使用 JSON.parse(JSON.stringify(object)) 模式来执行深度复制,其中object是要拷贝的对象。这种方法与递归功能类似,因为它也是将整个对象转换为 JSON 字符串,然后将其解析回新对象。该方法的优点是传递正确格式的对象即可工作,无需递归功能。
这种方法也存在不足之处。首先,它只适用于能够序列化为 JSON 的对象。原型属性、函数属性和 undefined 属性会被忽略,并且任何循环引用都会导致 TypeError。其次,对于大型对象或多次执行此操作的情况,可能会影响性能。
我们先来了解一些JS的知识。
当 JavaScript 代码被执行时,首先会进行词法分析和语法分析。在这个过程中,解析器会按照一定的规则将代码解析成抽象语法树(AST)。
接下来,驱动引擎会根据生成的 AST 来生成字节码,通过解释器对字节码进行逐一执行。如果需要,解释器还可以将字节码编译成机器代码,以提高执行效率。
在执行过程中,解释器会根据标识符的作用域链来查找变量所在的作用域,以确定变量的值。当执行到函数调用时,解释器会创建一个新的运行上下文,并将该上下文添加到调用栈中。当函数执行完毕后,解释器会将该运行上下文从调用栈中弹出,并返回函数的执行结果。
在处理 JavaScript 代码时,解析器会遵循一些复杂的规则和算法,如变量提升、作用域和闭包等。这些规则和算法的实现都是基于语言规范和实际执行环境的特性,不同浏览器和 JavaScript 引擎的实现也会有所差异。
总的来说,JavaScript 解析器的运行原理是将代码解析成抽象语法树,然后生成相应的字节码或机器代码,并在解释器中逐一执行,同时维护变量作用域和运行上下文等状态信息,以便在需要时查找变量并维护函数调用栈。
在运行时将JavaScript代码转换成低级语言的程序。
将JavaScript代码通过解析器解析成抽象语法树AST。
接着将AST通过解释器转变为字节码(字节码是跨平台的中间表示,与机器代码无关,可以在多个平台上运行)。
字节码最后通过编译器,生成机器代码。由于不同平台使用的机器代码有差异,因此编译器会根据当前平台来生成机器代码。
当 JavaScript 代码被执行时,解释器会创建一个新的运行上下文(Execution Context,EC),也称为执行环境或上下文。运行上下文是一个对象,它包含了当前代码的执行状态、变量、函数和作用域等信息。每个运行上下文都是相互独立的,它们之间通过作用域链(Scope Chain)来联系。
在 JavaScript 中,运行上下文可以分为三种类型:全局上下文、函数上下文和 Eval 上下文。其中全局上下文是最外层的上下文,它在脚本加载后被创建,并一直存在于整个页面的生命周期中。函数上下文和 Eval 上下文则是在代码执行到函数调用和 Eval() 函数时才会动态创建。
每当 JavaScript 运行到一个可执行代码块(如全局代码、函数内部或 Eval 代码)时,就会创建一个新的运行上下文。该上下文包含了当前代码块的执行状态和作用域链等信息,并将其添加到调用栈(Call Stack)的顶部,表示当前正在执行的代码块。当代码块执行完毕后,该运行上下文会从调用栈中弹出,控制权回到上一个运行上下文。
每个运行上下文都包含了以下几个重要的组成部分:
变量对象(Variable Object):用于存储变量、函数和参数等信息的对象。在全局上下文中,变量对象就是全局对象 window。在函数上下文中,变量对象包含了所有函数参数和内部声明的变量和函数,并按照一定的规则进行排序。
作用域链(Scope Chain):用于解析变量和函数标识符的链式结构。作用域链由当前上下文的变量对象和所有父级上下文的变量对象组成。当访问一个变量或函数时,解释器会沿着作用域链逐级查找,直到找到匹配的标识符或遍历完整个链。
this 值(This Binding):用于表示函数执行的上下文对象。this 值在运行时动态绑定,取决于函数被调用的方式和上下文环境。
闭包状态(Closure):用于保存函数和变量的引用,使得该函数能够访问外部的变量和函数。
总的来说,JavaScript 运行上下文是代码执行的关键组成部分,它记录了代码的执行状态、作用域和上下文环境等信息,使得 JavaScript 能够实现诸如作用域、闭包等高级特性。
想要明白为何会发生这种情况,我们首先需要了解JS数据都存储在什么地方。可以看看下面这篇文章。
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
在明白了基础数据类型存放在栈中,而引用数据类型存放在堆中后,结合前面的JS解析器运行原理中的运行上下文的概念,我们可以知道,在JS解析过程中,对于基本数据类型是直接将数据存放在了运行上下文中,而引用数据类型则是只存了一个地址值,这个地址值就是真正的引用数据类型在堆中的地址。
此时我们应该差不多能明白了,并不是浅拷贝本身故意不去拷贝对象的,浅拷贝已经将运行上下文中的所有数据都进行拷贝了,只是说引用数据类型存放在运行上下文中的本来就不是真正的他,只是一个地址值,所以才导致了使用浅拷贝的时候,拷贝过后的对象被修改了,会影响到原来的对象。真正不符合常理的并不是浅拷贝,而是深拷贝。深拷贝在遍历的时候一旦判断类型为对象,就会对这个对象再进行一次拷贝,相当于给原先只是一个拷贝的过程改成了一个递归的过程。
function deepClone(obj) {
if(typeof obj !== 'object') return;
var newObj = obj instanceof Array ? [] : {};
for(var key in obj) {
if(obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
}
}
return newObj;
}