变量声明与复制拷贝是我们在写代码中必不可少的一部分,这篇文章就来主要讲一下JS的变量存储以及深浅拷贝,还有,我们常用的对象拷贝Object.assign()到底是深拷贝还是浅拷贝。
var 与 let、const 的区别
要说JS的堆与栈,先从最基本的var、let、const区别说起。
我们使用var定义一个变量时,通常是跳过检查是否已经定义了此变量,尤其在编写局部代码时,这就更加容易导致命名引起的重复定义,导致一些核心变量被覆盖,造成系统重大破坏。let和const的出现,也可以说成是对JS声明变量的一个限制,而不像var那样太过于“随意”。我们先来看下let、const特(限)性(制):
-
块级声明
/* 块级声明 */
if (true) { var a = 3; }
console.log(a); // 3
if (true) { let b = 4; }
console.log(b); // Uncaught ReferenceError: b is not defined
- 不存在变量提升
/* 变量提升 */
console.log(a); // undefined
var a = 1;
console.log(b); // Uncaught ReferenceError: b is not defined
let b = 2;
-
不允许重复声明
/* 重复定义 */
var a = 1;
var a = 2;
console.log(a); // 2
let b = 3;
let b = 4;
console.log(b); // Uncaught SyntaxError: Identifier 'b' has already been declared
let与const的区别是:let声明的变量值可更改,而const声明的变量值不可更改。
但是(问题来了),const声明的变量真的不可以更改吗???
我们可以先看下下面的一个实验:
/* 值的可更改性 */
const obj = { a: 1, b: 2 };
console.log(obj); // {a: 1, b: 2}
obj.b = 3;
console.log(obj); // {a: 1, b: 3}
答案是:部分能改,部分不能改。若const声明的变量是基本类型,则值不可以更改;若为复杂数据类型(对象),则是可以通过修改对象属性等方法来改变的。
这就要说一下JS的堆内存与栈内存了:
JS的堆内存与栈内存:
在JS引擎中对变量的存储主要有两种位置,堆内存和栈内存。
-
栈内存主要用于存储各种基本类型的变量,包括Boolean、Number、String、Undefined、Null,以及对象变量的指针;
优势:存取速度比堆要快,存放在一级缓存中,仅次于直接位于CPU中的寄存器;
缺点:存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
-
堆内存主要负责像对象Object这种引用类型的存储。查询引用类型的变量时, 先从栈中读取内存地址, 然后再通过地址找到堆中的值。
优势:可以动态地分配内存大小,存在二级缓存中,生存期也不必事先告诉编译器,垃圾收集器会自动地收走这些不再使用的数据,比如对象和数组是可以无限拓展的,正好放在可以动态分配大小的堆中;
缺点:由于在运行时动态分配内存,所以存取速度较慢。
定义一个const对象的时候,我们说的常量其实是指针。const对象对应的堆内存指向是不变的,但是堆内存中的数据本身的大小或者属性是可变的。而对于const定义的基础变量而言,这个值就相当于const对象的指针,是不可变。
说到堆内存与栈内存怎么能不提一下JS的深拷贝与浅拷贝~~~
JS的深拷贝与浅拷贝
浅拷贝和深拷贝都只针对于引用数据类型。
-
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存;
-
深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
浅拷贝实例:
/*
* 浅拷贝实例
* 只复制第一层
*/
function shallowCopy (obj1) {
let obj2 = Array.isArray(obj1) ? [] : {};
for (let i in obj1) {
obj2[i] = obj1[i];
}
return obj2;
}
let obj1 = {
a: 1,
b: 2,
c: {
d: 3
},
};
let obj2 = shallowCopy(obj1);
obj2.b = 4;
obj2.c.d = 5;
console.log(obj1.b); // 2
console.log(obj2.b); // 4
console.log(obj1.c); // {d: 5}
console.log(obj2.c); // {d: 5}
深拷贝实例:
/*
* 深拷贝实例
*/
function deepCopy (obj1) {
let obj2 = Array.isArray(obj1) ? [] : {};
if (obj1 && typeof obj1 === 'object') {
for (let i in obj1) {
if (obj1.hasOwnProperty(i)) {
if (obj1[i] && typeof obj1[i] === 'object') {
// 如果自属性为引用数据类型,递归复制
obj2[i] = deepCopy(obj1[i]);
} else {
// 基本数据类型,则直接拷贝
obj2[i] = obj1[i];
}
}
}
}
return obj2;
}
let obj1 = {
a: 1,
b: 2,
c: {
d: 3
},
};
let obj2 = deepCopy(obj1);
obj2.b = 4;
obj2.c.d = 5;
console.log(obj1.b); // 2
console.log(obj2.b); // 4
console.log(obj1.c); // {d: 3}
console.log(obj2.c); // {d: 5}
Object.assign()
那么,我们常用的Object.assign()是深拷贝还是浅拷贝呢?
Object.assign()方法的第一个参数是目标对象,后面的参数都是源对象,Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
我们先来看一段用Object.assign()拷贝对象的代码:
/*
* Object.assign()
*/
const obj1 = {a: 1, b: 2, c: 3};
const obj2 = {};
Object.assign(obj2, obj1);
obj2.a = 4;
console.log(obj2); // {a: 4, b: 2, c: 3}
const obj1 = {a: 1, b: 2, c: {d: 3}};
const obj2 = {};
Object.assign(obj2, obj1);
obj2.a = 4;
obj2.c.d = 5;
console.log(obj1); // {a: 1, b: 2, c: {d: 5}}
console.log(obj2); // {a: 4, b: 2, c: {d: 5}}
经过对比可以发现,如果对象的属性值为基本类型(string,number),通过Object.assign()得到的新对象为深拷贝;如果属性值为对象或其他引用类型,那对于这个对象而言其实是浅拷贝的,这是Object.assign()特别需要注意的地方。
那么我们再看看深拷贝和浅拷贝的一些方法:
深拷贝的方法
1. 使用JSON.stringify()和JSON.parse()实现深拷贝:JSON.stringify()把对象转成字符串,再用JSON.parse()把字符串转成新的对象。
注:它会抛弃对象的constructor,深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object;这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,也就是说,只有可以转成JSON格式的对象才可以这样用,像function没办法转成JSON。
2. 递归拷贝(前面已说);
3. 手动实现深拷贝(一个一个赋值);
4. Object.create();// 方法创建一个新对象,使用现有的对象来提供新创建的对象的proto
5.函数库lodash,有提供_.cloneDeep()用来做深拷贝;
6. jquery 提供的$.extend()。
浅拷贝的方法
1. 直接赋值对象指针;
2. Object.assign():// 将所有可枚举属性的值从一个或多个源对象复制到目标对象;
3. Array.slice(); // 从已有的数组中返回选定的元素;
4. Array.concat(); // 用于连接两个或多个数组。
注:Object.assign()、 Array.slice()、Array.concat()非引用类型的值属于深拷贝;引入类型的值属于浅拷贝。即只进行第一层的深拷贝。
其实说到JS的内存,还必须提到的一点就是JS的垃圾回收机制,下个文章再讲~~