文章目录
1 数据类型
在 JavaScript 中,数据被分为基本数据类型和引用数据类型。
⏳ 基本数据类型有:
- String
- Number
- Boolean
- Undefined
- Null
- Symbol(ES 6 新增的数据类型)
⏳ 引用数据类型有:
- Object
- Array
- Function
🚨 它们有一个重要区别就是:基本数据类型存储在栈内存中,引用数据类型存储在堆内存中。
图解:
// 声明一个基本数据类型
var name = 'cez';
// 声明一个引用数据类型
var obj = { name: 'zlz' };
声明变量name
时,会在栈内存中开辟一块内存空间,用来存储该变量的值'cez'
;
声明变量obj
时,先在堆内存中开辟一块内存空间(假设这块内存空间的地址为0x123
)来存储{name:'zlz'}
,然后在栈内存中开辟一块内存空间来存储地址0x123
。
2 浅拷贝与深拷贝
浅拷贝和深拷贝都是对于引用数据类型来说的。
1️⃣ 浅拷贝:
浅拷贝只是复制某个对象的指针(地址),导致它们都指向了堆内存中同一个数据,互相影响。
示例:
var originObj, cloneObj;
originObj = { name: 'cez', age: 18 };
cloneObj = originObj;
originObj.name = 'zlz';
console.log(JSON.stringify(cloneObj)); // {"name":"zlz","age":18}
cloneObj.age = 180;
console.log(JSON.stringify(originObj)); // {"name":"zlz","age":180}
结果:经过赋值操作,两个对象都指向了堆内存中的同一个数据,所以其中一个发生变化时,另一个也会随着变化。
分析:在进行cloneObj = originObj
赋值操作时,因为originObj
存储的值是一个地址(这个地址指向了堆内存中的数据{name:'cez', age:18}
),所以这一步是把该地址赋给cloneObj
,导致两者指向了同一个数据。如图:
2️⃣ 深拷贝:
深拷贝是在堆内存中创建一个一模一样的数据,然后把新数据的内存地址赋给新变量,这样旧变量和新变量就指向了不同的数据,也就不会互相影响。
示例:
var originObj, cloneObj;
originObj = { name: 'cez', age: 18 };
cloneObj = deepClone(originObj);
// 深拷贝函数
function deepClone() {
// some code...
};
originObj.name = 'zlz';
console.log(JSON.stringify(cloneObj)); // {"name":"cez","age":18}
cloneObj.age = 180;
console.log(JSON.stringify(originObj)); // {"name":"zlz","age":18}
结果: 经过深拷贝操作,originObj
和cloneObj
指向了不同的数据,所以其中一个发生变化时,另一个并不会跟着变化。
分析: 在进行深拷贝操作时,会在堆内存中创建一个一模一样的数据,然后把新数据的内存地址赋给cloneObj
,这样originObj
和cloneObj
就指向了不同的数据,也就不会互相影响。如图:
3 实现深拷贝方法
实现深拷贝的方式有2种:
JSON.stringify()
结合JSON.parse()
- 递归
3.1 JSON.string() 结合 JSON.parse()
JSON.stringify:将一个 JS 值转为 JSON 字符串。
JSON.parse:将 JSON 字符串转成 JS 值或对象。
示例:
var originObj, cloneObj;
originObj = { name: "cez", age: 18 };
cloneObj = JSON.parse(JSON.stringify(originObj));
originObj.name = "zlz";
cloneObj.age = 180;
console.log(originObj === cloneObj); // false
console.log(originObj); // {name: 'zlz', age: 18}
console.log(cloneObj); // {name: 'cez', age: 180}
这种方法虽然可以实现数组或对象深拷贝,但不能处理函数:
var originObj, cloneObj;
originObj = {
name: "cez",
age: 18,
getAge: function () {
console.log("Hello World");
},
};
cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(originObj); // {name: 'cez', age: 18, getAge: ƒ}
console.log(cloneObj); // {name: 'cez', age: 18}
发现在cloneObj
中缺失了getAge
属性。这是为什么呢?
在 MDN 上找到了原因:
所以,当对象中含有函数时,就不能使用这个方法进行深拷贝。
3.2 递归
写法一:
var originArr, cloneArr;
function deepClone(source) {
const targetObj = source.constructor === Array ? [] : {};
for (let key in source) {
if (source.hasOwnProperty(key)) {
// 只拷贝自身的属性,不拷贝原型的属性
if (source[key] && typeof source[key] === "object") {
// 元素为引用数据类型
targetObj[key] = source[key].constructor === Array ? [] : {};
targetObj[key] = deepClone(source[key]);
} else {
// 元素为基本数据类型
targetObj[key] = source[key];
}
}
}
return targetObj;
}
originArr = [1, 2, 3, function () {}];
cloneArr = deepClone(originArr);
originArr[2] = "cez";
console.log(originArr); // [1, 2, 'cez', f]
console.log(cloneArr); // [1, 2, 3, f]
写法二:
var originObj, cloneObj;
// 检测数据类型
function checkedType(target) {
return Object.prototype.toString.call(target).slice(8, -1);
}
function deepClone(target) {
let result,
targetType = checkedType(target);
if (targetType !== "Object" && targetType !== "Array") return target;
result = targetType === "Object" ? {} : [];
// 遍历目标数据
for (let key in target) {
let value = target[key];
let valueType = checkedType(value);
if (valueType === "object" || valueType === "Array") {
// 元素为引用数据类型,接着递归
result[key] = deepClone(value);
} else {
// 元素为基本数据类型
result[key] = value;
}
}
return result;
}
originObj = { name: "cez", getName: function () {} };
cloneObj = deepClone(originObj);
originObj.name = "zlz";
console.log(originObj); // {name: 'zlz', getName: ƒ}
console.log(cloneObj); // {name: 'cez', getName: ƒ}
4 JS 中的拷贝方法
不管是数组的concat
和slice
方法,还是 ES6 新增的Object.assgn
和...
扩展运算符,都能实现对对象的拷贝,它们都是返回新数组的,并不会修改原数组。
那么它们是浅拷贝还是深拷贝呢?
4.1 concat()
concat()
方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
用法:
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = arr1.concat(arr2);
console.log(arr3); // [1, 2, 3, 4, 5, 6]
是否为深拷贝呢?
// 第一种情况:数组为一维数组
let originArr = [1, 2, 3, 4];
let cloneArr = originArr.concat();
console.log(originArr === cloneArr); // false
console.log(cloneArr); // [1, 2, 3, 4]
originArr.push(5);
console.log(cloneArr); // [1, 2, 3, 4]
// 第二种情况:数组为多维数组或包含对象
let originArr = [1, 2, 3, 4, [5, 6, 7], { name: "cez" }];
let cloneArr = originArr.concat();
console.log(originArr === cloneArr); // false
console.log(JSON.stringify(cloneArr)); // [1,2,3,4,[5,6,7],{"name":"cez"}]
originArr.push(5);
originArr[4][0] = "a";
originArr[5].name = "zlz";
console.log(JSON.stringify(cloneArr)); // [1,2,3,4,["a",6,7],{"name":"zlz"}]
可以看到,当修改originArr
的第一层数组时,两者互不影响,说明是深拷贝。
当修改originArr
的第二层数组或对象时,互相影响,说明是浅拷贝。
结论:concat()
只是对数组的第一层进行深拷贝。
4.2 slice()
MDN介绍:
用法:
const animals = ['ant', 'bison', 'camel', 'duck'];
console.log(animals.slice(0)); // ['ant', 'bison', 'camel', 'duck']
console.log(animals.slice(1, 3)); // ['bison', 'camel']
是否为深拷贝?
// 第一种情况:数组为一维数组
let originArr = [1, 2, 3, 4];
let cloneArr = originArr.slice(0);
console.log(originArr === cloneArr); // false
console.log(cloneArr); // [1, 2, 3, 4]
originArr.push(5);
console.log(cloneArr); // [1, 2, 3, 4]
// 第二种情况:数组为多维数组或包含对象
let originArr = [1, [2, 3], { name: "cez" }];
let cloneArr = originArr.slice(0);
console.log(originArr === cloneArr); // false
console.log(JSON.stringify(cloneArr)); // [1,[2,3],{"name":"cez"}]
originArr.push(4);
originArr[1][0] = "a";
originArr[2].name = "zlz";
console.log(JSON.stringify(cloneArr)); // [1,["a",3],{"name":"zlz"}]
结论: slice()
只是对数组的第一层进行深拷贝。
4.3 展开运算符(...
)
展开运算符允许一个表达式在某处展开。展开运算符在多个参数(用于函数调用)或多个元素(用于数组字面量)或者多个变量(用于解构赋值)的地方可以使用。
用法:
// 在函数调用中使用
var arr = [0, 1, 2];
function test(a, b, c){};
test(...arr);
// 在数组字面量中使用
var arr1 = [1, 2, 3];
var arr2 = [...arr1, 4, 5]; // [1, 2, 3, 4, 5]
// 用于解构赋值
var [arg1, arg2, ...arg3] = [1, 2, 3, 4, 5]
arg1 // 1
arg2 // 2
arg3 // [3, 4, 5]
是否为深拷贝?
// 拷贝数组
let originArr = [1, [2, 3]];
let cloneArr = [...originArr];
console.log(JSON.stringify(cloneArr)); // [1,[2,3]]
originArr[0] = 'a';
originArr[1][0] = 'a'
console.log(JSON.stringify(cloneArr)); // [1,["a",3]]
// 拷贝对象
let originObj = { a: 1, b: { c: 2 } };
let cloneObj = {...originObj};
console.log(JSON.stringify(cloneObj)); // {"a":1,"b":{"c":2}}
originObj.a = 'a';
originObj.b.c = 'c';
console.log(JSON.stringify(cloneObj)); // {"a":1,"b":{"c":"c"}}
结论: ...
只是对数组和对象的第一层进行深拷贝。
4.4 Object.assign()
Object.assign()
方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
用法:
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const returnedTarget = Object.assign(target, source);
console.log(target); // { a: 1, b: 4, c: 5 }
console.log(returnedTarget); // { a: 1, b: 4, c: 5 }
是否为深拷贝?
let originObj, cloneObj;
originObj = { a: 1, b: { c: 2 } };
cloneObj = Object.assign({}, originObj);
console.log(JSON.stringify(cloneObj)); // {"a":1,"b":{"c":2}}
originObj.a = "a";
originObj.b.c = "c";
console.log(JSON.stringify(cloneObj)); // {"a":1,"b":{"c":"c"}}
结论: Object.assign()
只是对对象的第一层进行深拷贝。