具体可见https://github.com/febobo/web-interview
4.浅拷贝与深拷贝
①栈内存与堆内存
栈内存(Stack Memory)
- 栈内存用于存储基本类型的变量和引用类型的变量引用(即指向堆内存中实际数据的指针)。
- 当一个函数被调用时,会创建一个称为“执行上下文”的栈帧,其中包含函数的参数、局部变量和返回地址。
- 栈内存是一个后进先出(LIFO)的数据结构,它在程序执行期间动态地增加和减少内存空间。
- 变量在栈内存中的分配是自动的,当变量超出作用域时会被自动销毁。
//1.基本数据
整数(例如:let num = 10;)
浮点数(例如:let floatNum = 3.14;)
布尔值(例如:let isTrue = true;)
字符串(例如:let str = "Hello";)
//2.函数调用
function add(a, b) {
return a + b;
}
let result = add(5, 3); // 函数调用会在栈内存中创建执行上下文
基本数据类型和函数调用的数据存储在栈内存中。
【具体例子】
let a = 10;
let b = a; // 赋值操作
b = 20;
console.log(a); // 10值
堆内存(Heap Memory)
- 堆内存用于存储引用类型的数据,如对象和数组。
- 在堆内存中,数据的大小不固定,可以动态地分配和释放内存。
- 当在栈内存中创建一个引用类型的变量时,实际的数据存储在堆内存中,而栈内存存储的是对数据的引用。
- 堆内存的数据需要手动的分配和释放,js引擎通过垃圾回收机制来自动管理堆内存中不在使用的数据,以防止内存泄露。
//1.对象
let person = {
name: "Alice",
age: 30
};
//2.数组
let numbers = [1, 2, 3, 4, 5];
//3.动态分配的对象
let user = null; // 声明一个变量
user = {
name: "Bob",
age: 25
};
//4.使用构造函数创建对象
function Person(name, age) {
this.name = name;
this.age = age;
}
let person1 = new Person("Charlie", 35);
对象、数组和动态分配的对象存储在堆内存中。
【具体例子】
var obj1 = {}
var obj2 = obj1;
obj2.name = "wow";
console.log(obj1.name); // wow
【总结】
- 栈内存用于存储基本类型的变量和引用类型的变量的引用,具有自动分配和释放内存的特性。
- 堆内存用于存储引用类型的数据,具有动态分配和手动释放内存的特性。
②浅拷贝
浅拷贝指的是创建新的数据,这个数据是对原数据属性值的精准拷贝。当属性是基本类型,则拷贝的是基本类型的值;如果是引用类型,则拷贝的是内存地址。即浅拷贝是拷贝一层,深层次的引用类型则是共享地址。
//例1
let obj1 = { name: "Alice", age: [30,21] };
let shallowCopy = Object.assign({}, obj1);
shallowCopy.age[0] = 40;
console.log(obj1.age); // 输出 40,原始对象改变
shallowCopy.name = "Bob";
console.log(obj1.name); // 输出 "Alice",原始对象不会受到影响
在例1中,由于属性name是基本类型,所以shallowCopy直接拷贝的数据,对基本数据值修改不会影响原始数据;而age是数组,则拷贝的是数组的地址,所以对age进行修改,则会影响到原始数据。
在JavaScript
中,存在浅拷贝的现象有:
-
Object.assign
-
Array.prototype.slice()
,Array.prototype.concat()
-
使用拓展运算符实现的复制
Object.assign
Object.assign()静态方法将一个或者多个源对象中所有可枚举的自有属性复制到目标对象,并返回修改后的目标对象。
//Object.assign
var obj = {
age: 18,
nature: ['smart', 'good'],
names: {
name1: 'fx',
name2: 'xka'
},
love: function () {
console.log('fx is a great girl')
}
}
var newObj = Object.assign({}, obj); //浅拷贝
Array.prototype.concat()
concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
//Array.concat
const Arr = ["One", "Two", "Three"]
const fxArrs1 = Arr.concat()
fxArrs1[1] = "hello";
console.log(Arr) // ["One", "Two", "Three"]
console.log(fxArrs1) // ["One", "hello", "Three"]
拓展运算符
扩展运算符(spread) 是三个点(…),它如同 rest 运算,将一个数组转为用逗号分割的参数序列。
const fxArr = ["One", "Two", "Three"]
const fxArrs = [...fxArr]
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]
③深拷贝
深拷贝开辟一个新的栈,两个对象属性完全相同,但是对应两个不同的地址;对一个对象进行修改,不会影响到另一个对象。
// 使用 Lodash 库中的深拷贝方法
//例1
const _ = require('lodash');
let obj1 = { name: "Alice", nestedObj: { key: "value" } };
let deepCopy = _.cloneDeep(obj1);
deepCopy.nestedObj.key = "new value";
console.log(obj1.nestedObj.key); // 输出 "value",原始对象不受影响
JS中,常见的深拷贝方式有:
-
_.cloneDeep()
-
jQuery.extend()
-
JSON.stringify()
-
手写循环递归
_.cloneDeep()
_.cloneDeep是lodash
库中一个用于实现深拷贝方法,它会递归地赋值一个对象或数组及其所有嵌套对象和数组,从而创建一个完全独立的副本。
//见例1
jQuery.extend()
在 jQuery 中,可以使用 jQuery.extend()
方法来实现对象的深拷贝。这个方法可以用于将一个或多个对象的内容合并到目标对象中,如果目标对象中已经存在相同的属性,则会被覆盖;在某种程度上也可以实现深拷贝。当你将一个空对象作为目标对象,并将需要复制的对象作为参数传递给 jQuery.extend()
方法时,它会创建目标对象的一个深层副本。
let obj1 = {
name: "Alice",
nestedObj: {
key: "value"
}
};
let deepCopy = $.extend(true, {}, obj1);
deepCopy.nestedObj.key = "new value";
console.log(obj1.nestedObj.key); // 输出 "value",原始对象不受影响
JSON.stringify()
JSON.stringify() 方法将一个 JavaScript 对象或值转换为 JSON 字符串,如果指定了一个 replacer 函数,则可以选择性地替换值,或者指定的 replacer 是数组,则可选择性地仅包含数组指定的属性。
const obj = {
name:{
key : "a"
},
name1: undefined,
name3: function () { },
name4: Symbol('A')
}
const obj2 = JSON.parse(JSON.stringify(obj));
obj.name.key = 'b'
console.log(obj2); // 输出a
但是这种方式存在弊端,会忽略undefined
、symbol
和函数
循环递归
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
const obj = {
name: {
key: "a"
},
name1: undefined,
name3: function () { },
name4: Symbol('A')
}
const obj2 = deepClone(obj)
obj2.name.key = "hello"
console.log(obj); //输出a
总结
前提为拷贝类型为引用类型的情况下:
- 浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址
- 深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址