javascript的数据是如何存储的
function foo(){
let a = {title:"hello word"};
let b = a;
a.title= "hi" ;
console.log(a);//{title: "hi"}
console.log(b);//{title: "hi"}
}
foo()
由上面的例子可以看出,把a赋给b,改变b的title值,同时a的title值也会随之改变,为啥?
要解答这个问题就要先了解,JS中数据是如何存储的:
JS内存空间分为:代码空间、栈空间、堆空间
- 代码空间:代码空间主要是存储可执行代码的。
- 栈空间:栈(call stack)指的就是调用栈,用来存储执行上下文的。(每个执行上下文包括了:变量环境、词法环境)
- 堆空间:堆(Heap)空间,一般用来存储对象的。
JS的数据类型有:Number、BigInt、String、Boolean、Symble、Null、Undefined、Object
更详细的讲解,请看:阮一峰的《JavaScript教程-数据类型》
前7种称为原始类型,最后一种Object称为引用类型,之所以把它们区分成两种类型,是因为它们在内存中存放的位置不同;
原始类型存放在栈空间中,具体点到执行上下文来说就是:用var定义的变量会存放在变量环境中,而用let、const定义的变量会存放在词法环境中。并且对原始类型来说存放的是值,而引用类型存放的是指针,指针指向堆内存中存放的真正内容
深拷贝和浅拷贝的区别
浅拷贝
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型(Number、String、Boolean、Null、Undefined、Symbol(ES6 引入了一种新的原始数据类型,表示独一无二的值,最大的用法是用来定义对象的唯一属性名)),拷贝的就是基本类型的值,如果属性是引用类型(比如:Object和Array),拷贝的就是内存地址(复制的是指针,最终指针指向的内存地址都是一样的) ,所以修改新拷贝的对象会影响原对象。
深拷贝
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
常用的拷贝方式:
1. Object.assign()
Object.assign()方法可以把任意多个源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象,Object.assign 只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝。
注意:如果obj只有一层的时候,是深拷贝
2. 运算符…
let obj={
name:"张三",
age:"18",
school:{
name:"北京大学"
}
}
let copeObj={
...obj
}
copeObj.age=19;
copeObj.school.name="清华大学";
console.log(obj)//{name:"张三", age:"18", school:{ name:"清华大学"}}
3. Array的concat()
连接两个或多个数组,并返回已连接数组的副本
let arr = [1, 2, true, {
name: '李四'
}];
let arr2 = arr.concat()
arr2[0] = 99;
arr2[3].name = "王二"
console.log(arr) // [1,2,true,{name:"王二"}]
修改对象会修改原对象的,但是第一层上的基本数据类型修改不会影响
4. Array的slice()
选择数组的一部分,并返回新数组
let arr = [1, 2, true, {
name: '李四'
}];
let arr2 = arr.slice();
arr2[0] = 99;
arr2[3].name = "王二"
console.log(arr) // [1,2,true,{name:"王二"}]
5. JSON对象的parse和stringify
let arr = [1, 2, true, [
{
name: '李四',
age:19,
hobby:["乒乓球","羽毛球"],
school:{
name:"北京大学",
address:"北京",
faculty:{
name:"计算机学院",
number:888,
}
}
},
{
name: '张三',
age:18,
hobby:["足球","篮球"],
school:{
name:"上海交通大学",
address:"上海",
faculty:{
name:"文学院",
number:1111,
}
}
}
]
];
let arr2 = JSON.parse(JSON.stringify(arr));
arr2[0] = 99;
arr2[3][0].name = "王二"
arr2[3][0].hobby[0]= "排球"
arr2[3][0].school.name= "深圳大学"
arr2[3][0].school.address= "深圳"
console.log(arr)
用JSON.stringify()将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象就产生了,而且对象会开辟新的栈,实现深拷贝.
但是该方法也是有局限性的:
- 会忽略undefined
- 会忽略symbol
- 不能序列化函数(因为JSON.stringify()方法是将一个JavaScript值(对象或者数组)转换为一个JSON字符串,不能接受函数)
- 不能解决循环引用的对象
let arr = {
name:"测试数据",
time:"2021-11-11",
person:{
name: '李四',
age:19,
hobby:undefined,
sex: Symbol('male'),
school:{
name:"北京大学",
address:"北京",
faculty:{
name:"计算机学院",
number:888,
}
},
getSchoolName: function(){
console.log("学校名字");
}
}
}
//会忽略undefined及symbol
let arr2 = JSON.parse(JSON.stringify(arr));
//不能解决循环引用
//arr.personCope = arr.person;
//arr.person.school = arr.personCope;//会报错
//arr.person.school.name= arr.personCope.name;
//let arr2 = JSON.parse(JSON.stringify(arr));
console.log(arr2)
//不能序列化函数:诸如 Map, Set, RegExp, Date, ArrayBuffer 和其他内置类型在进行序列化时会丢失
5. $.extend()
将两个或更多对象的内容合并到第一个对象
定义:
在默认情况下,通过$.extend()合并操作不是递归的(浅拷贝);如果第一个对象的属性本身是一个对象或数组,那么它将完全用第二个对象相同的key重写一个属性。这些值不会被合并。然而,如果将 true 作为该函数的第一个参数,那么会在对象上进行递归的合并(深拷贝)
浅拷贝(false 默认):如果第二个参数对象有的属性第一个参数对象也有,那么不会进行相同参数内部的比较,直接将第一个对象的相同参数覆盖。
深拷贝(true):如果第二个参数对象有的属性第一个参数对象也有,还要继续在这个相同的参数向下一层找,比较相同参数的对象中是否还有不一样的属性,如果有,将其继承到第一个对象,如果没有,则覆盖。
可以拷贝函数,会忽略undefined,不会忽略Symbol
let person={
name: '李四',
age:19,
hobby:["乒乓球","羽毛球"],
school:{
name:"北京大学",
address:"北京",
phone:"123456789",
faculty:{
name:"计算机学院",
number:888,
}
}
};
let person02={
name: '张三',
age:19,
hobby:undefined,
sex: Symbol('male'),
phone:"987654321",
school:{
name:"上海交通大学",
address:"上海",
faculty:{
name:"计算机学院",
number:888,
}
},
getSchoolName: function(){
console.log("学校名字");
}
}
//$.extend(person, person02);
$.extend(true,person, person02);
console.log(person);
7. 函数库 lodash 中的lodash.cloneDeep()方法
Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库
Lodash 中文文档
为什么选择 Lodash ?
Lodash 通过降低 array、number、objects、string 等等的使用难度从而让 JavaScript 变得更简单。 Lodash 的模块化方法 非常适用于:
- 遍历 array、object 和 string
- 对值进行操作和检测
- 创建符合功能的函数
8. 自己递归实现
原理:递归方法实现深度克隆原理:遍历对象,数组直到里面都是基本数据类型,然后再去复制,就是深度拷贝
我自己在网上随便找了几个深度拷贝的方法
/* ------------------------- 方法一 ------------------------- */
//深度拷贝一个数组或者对象
deepCope(obj) {
let type = Object.prototype.toString.call(obj);
if (type == "[object Array]") {
let backObj = [];
for (let val of obj) {
backObj.push(this.deepCope(val));
}
return backObj;
}
if (type == "[object Object]") {
let backObj = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
backObj[key] = this.deepCope(obj[key]);
}
}
return backObj;
}
return obj;
},
/* ------------------------- 方法二 ------------------------- */
deepCopy(obj2) {
//递归深层copy
/**
* 把一个对象递归拷贝给另外一个对象
* 源对象与拷贝后的对象没有引用关系
*/
var obj = this.isArray(obj2) ? [] : {};
for (var property in obj2) {
// 如果当前拷贝的数据还是一个对象的话,那么继续调用
// deepCopy 进行二次拷贝
// 递归
if (this.isObject(obj2[property])) {
obj[property] = this.deepCopy(obj2[property]);
} else {
obj[property] = obj2[property];
}
}
return obj;
},
isArray(val) {
//检测数组
//通过Object.prototype.toString.call来精准检测类型
return Object.prototype.toString.call(val) === "[object Array]";
},
isObject(val) {
//检测对象
return typeof val === "object" && val !== null;
},
/* ------------------------- 方法三 ------------------------- */
deepCopy(data) {
if (data.constructor.name === "Array") {
// 判断为数组类型
var arrCopy = [];
for (let i = 0, len = data.length; i < len; i++) {
//遍历数组
if (data[i] instanceof Object) {
arrCopy.push(this.deepCopy(data[i]));
} else {
// 基本类型
arrCopy.push(data[i]);
}
}
return arrCopy;
} else {
// 为对象
var objCopy = {};
for (let x in data) {
if (data[x] instanceof Object) {
objCopy[x] = this.deepCopy(data[x]);
} else {
// 基本类型
objCopy[x] = data[x];
}
}
return objCopy;
}
},
深拷贝思路是:
- 处理原始类型 如: Number String Boolean Symbol Null Undefined
- 处理不可遍历类型 如: Date RegExp Function
- 处理循环引用情况 使用: WeakMap
- 处理可遍历类型 如: Set Map Array Object
详细的深度拷贝实现思路:https://juejin.cn/post/6881889117437689864