一 前置知识-数据类型 && 赋值传参
Q1 JS中的数据类型有哪些
A:
S1 基础类型: Boolean/ Number/ String/ Null/ Undefined/ Symbol
S2 引用类型: Object
- Object
- Function
- Array
- Date
- Regexp
Q2 基础类型(原始类型) 和 引用类型的区别
A:
S1 存储方式: 基础类型的值存储在内存栈中; 引用类型的实际值存储在内存堆中,栈只是存储 堆地址
S2 值是否可变: 基础类型的值是不可变的; 引用类型的值是可变的
- String类型的值 有些方法看上去返回了一个修改后的字符串,实际上返回的是一个新的字符串值
js
let str = 'ygm';
str.slice(1);
str.toUpperCase(0);
str[0] = 'A';
// 基础类型的值是不可变的
console.log(str); // ygm
// 看起来值是变化了,其实是在内存中开辟了一个新的栈空间,然后让变量指向了这个新值
str += '666'
console.log(str); // ygm666
S3 变量赋值: 基础类型直接复制变量值,变量间互不影响; 引用类型复制的是堆地址,变量间互相影响
js
// 基础类型: 直接复制变量值; 互不影响
let name = 'ygm';
let name2 = name;
name2 = '新name2';
console.log(name); // ygm;
console.log(name2); // 新name2;
// 引用类型: 复制的是堆地址; 会互相影响
let obj = {name:'ygm'};
let obj2 = obj;
obj2.name = '新name2';
console.log(obj.name); // 新name2
console.log(obj2.name); // 新name2
S4 变量相等比较: 基础类型 比较变量值是否相同; 引用类型 比较堆内存地址是否相同
js
const name = 'ygm';
const name2 = 'ygm';
const obj = {name:'ygm'};
const obj2 = {name:'ygm'};
// 基础类型 直接比较变量值
console.log(name === name2); // true
// 引用类型 比较堆地址
console.log(obj === obj2); // false
Q3 变量赋值、函数传参、浅拷贝、深拷贝的区别
A:
S1 变量赋值: 基础类型是传参,引用类型是传址
S2 函数传参: 在JS中都是按值传递: 传递的都是变量在内存栈中的值 (即 变量值/堆内存地址)
js
let name = 'ygm';
function changeValue(name2){
name2 = '函数name';
}
// 基础类型: 按值穿递- name和name2互不影响
changeValue(name);
console.log(name); // ygm
// 引用类型: 同样按值传递- 只不过传递的值是堆内存地址
let obj = {};
function changeValue(obj2) {
obj2.name = 'ygm';
// 这里obj2指向了新的堆地址,所以不会再影响到obj指向的值
obj2 = { name:'指向新name' };
return obj2
}
const obj2 = changeValue(obj);
console.log(obj.name); // ygm
console.log(obj2.name); // 指向新name
S3 赋值 VS 浅拷贝 VS 深拷贝
下列表格中,× 表示互不影响; √ 表示互相影响
操作类型 | 基础数据类型a | 第一层数据对象b | b中包含子对象c |
---|---|---|---|
赋值 | × | √ | √ |
浅拷贝 | × | × | √ |
深拷贝 | × | × | x |
具体代码示例为:
js
const obj1 = { name: 'ygm', age: 10, friends: ['a', 'b'] }
const fObj = obj1
const shallowObj = {...obj1}
const deepObj = JSON.parse(JSON.stringify(obj1))
// 赋值: fObj对象内的 基础类型和引用类型都会影响 原对象obj1
fObj.name='新name'
fObj.friends.push('c')
// 浅拷贝: shallowObj对象内的 基础类型不影响原对象obj1; 引用类型会影响obj1
shallowObj.age = 20
shallowObj.friends.push('d')
// 深拷贝: deepObj对象内的 基础类型和引用类型 都不影响原对象obj1
deepObj.friends.push('e')
console.log(obj1) // { name: '新name', age: 10, friends: [ 'a', 'b', 'c', 'd' ] }
二 实现 浅拷贝
Q1 如何实现对象类型 的浅拷贝
A:
- 方法1: Object.create(obj)
js
// 新对象的原型链指向obj + 只能克隆原始对象的属性,不能克隆原始对象的方法
let obj = {a: 1, b: {c: 2}};
let newObj = Object.create(obj);
console.log(newObj); // {}
console.log(newObj.a); // 1
console.log(newObj.b); // {c: 2}
- 方法2: Object.assign({}, obj)
js
// 只能克隆原始对象的自身属性,不能克隆原始对象的 继承属性和 方法
let a = { 1: 'aa', 2: 'bb' }
let b = { 1: 'cc', 4: 'dd' }
let c = Object.assign({}, a, b) // { 1: "cc", 2: "bb", 4: "dd"}
- 方法3: 扩展运算符...
js
let obj = { a: 1, b: {c: 2} };
let newObj = {...obj}
console.log(newObj); // { a:1, b: {c:2} }
console.log(newObj.b === obj.b); // true
Q2 如何实现数组类型 的浅拷贝
A:
- 方法1: [].concat(arr)
js
// 只能克隆一维数组 + 只会复制源对象的自身属性,不会复制继承属性
let arr = [1, 2, {a: 3}];
let newArr = [].concat(arr);
console.log(newArr); // [ 1, 2, { a: 3 } ]
console.log(newArr[2] === arr[2]); // true
- 方法2: Object.assign([], arr1, arr2)
js
// a和c指向同一个内存地址,会互相影响
let a = [1,2]
let b = [3,4]
let c = Object.assign(a, b) // [3, 4]
- 方法3: 扩展运算符...
js
let a = [1,2]
let b = [3,4]
let c = [...a, ...b] // [1, 2, 3, 4]
Q3 Object.assign和扩展运算法 两者区别是什么
A:
- 返回值不同:
- 扩展运算符 总是返回一个 拷贝后的新对象
- Object.assign()函数是 直接修改第一个传入对象obj 并返回
- 即 Object.assign()可能会影响 原对象的基础类型值,而 扩展运算符必然不会影响拷贝的 基础类型值
js
// 例1.1 此时,key相同的属性 后面元素值覆盖前面 + a/b/c指向的是不同 内存地址,互不影响
let a = { 1: 'aa', 2: 'bb' }
let b = { 1: 'cc', 4: 'dd' }
let c = Object.assign({}, a, b) // { 1: "cc", 2: "bb", 4: "dd" }
// 例1.2 此时,key相同的属性 后面元素值覆盖前面 + a和c指向的是相同 内存地址,互相影响
let a = { 1: 'aa', 2: 'bb' }
let b = { 1: 'cc', 4: 'dd' }
let c = Object.assign(a, b) // { 1: "cc", 2: "bb", 4: "dd" }
// 例1.3 此时,key相同的属性 后面元素值覆盖前面 + a/b/c指向的是不同 内存地址,互不影响
let a = { 1: 'aa', 2: 'bb' }
let b = { 1: 'cc', 4: 'dd' }
let c = {...a, ...b} // { 1: "cc", 2: "bb", 4: "dd" }
// 例1.4 此时,key相同的属性 后面元素值覆盖前面 + a/b/c指向的是不同 内存地址,互不影响
let a = { 1: 'aa', 2: 'bb' }
let b = { 1: 'cc', 4: 'dd' }
let c = {...{}, ...a, ...b} // { 1: "cc", 2: "bb", 4: "dd" }
- 实现本质上不同:
-
Object.assign是使用 for..in循环遍历出 所有可枚举的+自有属性
- 所以 Object.assign会触发ES6的setter拦截,扩展运算符不会
-
扩展运算符是调用 该数据结构的 Interator遍历器接口(对象没有Interator,所以是特殊处理的)
- 所以 扩展操作符可以复制ES6的symbols属性,Object.assign()不能
三 实现 深拷贝
Q1 如何实现 深拷贝
A:
- 方法1: JSON.parse(JSON.stringify)
js
// 问题: 由于是依赖于JSON,所以无法支持 JSON不支持的类型/特性,具体表现为:
// 缺点1: 当对象内有 Fucntion类型/Undefined类型值: undefined/function值 会直接丢失
const obj = {
undef: undefined,
fun: () => { console.log("我是函数") },
};
const objCopy = JSON.parse(JSON.stringify(obj));
console.log("obj", obj); // obj { undef: undefined, fun: [Function: fun] }
console.log("objCopy", objCopy); // objCopy {}
// 缺点2: 当对象中有 RegExp正则类型值: 它的值会变成 {}空对象
const obj = {
re: /hello/ig
}
const objCopy = JSON.parse(JSON.stringify(obj));
console.log("obj", obj); // obj { re: /hello/gi }
console.log("objCopy", objCopy); // objCopy { re: {} }
console.log("objCopy-typeof", typeof objCopy.re); // 'object'
// 缺点3: 当对象中有NaN/ Infinity/ -Infinity这3种值: 它的值会变成 null
const obj = {
nan: NaN,
infinityMax: Number.POSITIVE_INFINITY,
infinityMin: Number.NEGATIVE_INFINITY,
}
const objCopy = JSON.parse(JSON.stringify(obj));
// obj { nan: NaN, infinityMax: Infinity, infinityMin: -Infinity }
console.log("obj", obj);
// objCopy { nan: null, infinityMax: null, infinityMin: null }
console.log("objCopy", objCopy);
// 缺点4: 当对象内的 Date类型值: Date类型会被变成 字符串类型
const obj = {
date: new Date()
}
const objCopy = JSON.parse(JSON.stringify(obj));
console.log('obj', typeof obj.date === 'object') // true
console.log('objCopy', typeof objCopy.date === 'string') // true
// 缺点5: 当对象 存在循环引用时: 会报错
const obj = {
self: null
}
obj.self = obj;
const objCopy = JSON.parse(JSON.stringify(obj));
// TypeError: Converting circular structure to JSON
console.log("objCopy", objCopy);
- 方法2: 递归实现
2.1 版本1: 递归支持 基本数据类型+ 简单对象类型(不支持 Array/Func/Date/RegExp等)
js
function isObject(target) {
const type = typeof target;
return target && (type === 'object' || type === 'function');
}
function deepClone(target) {
// 递归中止条件: 类型值为基本数据类型
if (!isObject(target)) {
return target;
}
// 对于引用类型,递归进行深拷贝即可
const cloned = {};
for (let key in target) {
cloned[key] = deepClone(target[key]);
}
return cloned;
}
const obj1 = {
name: "hello",
child: {
name: "我是child",
},
};
const obj2 = deepClone(obj1);
console.log('obj2', obj2.child !== obj1.child) // true
2.2 版本2: 支持基本数据类型+ 简单引用类型+ Array/RegExp/Date/Func
js
function isObject(target) {
const type = typeof target;
return target && (type === 'object' || type === 'function');
}
function deepClone(target) {
// 递归中止条件: 类型值为基本数据类型
if (!isObject(target)) {
return target;
}
// 对于引用类型,递归进行深拷贝
// const cloned = {};
// 根据不同引用类型,初始化对应类型值
let cloned;
if (target instanceof Array) {
cloned = [];
} else if (target instanceof RegExp) {
// source属性能 获取到正则的模式,flags属性 能获取到 正则的参数
cloned = new RegExp(target.source, target.flags);
} else if (target instanceof Date) {
cloned = new Date(target);
} else if (target instanceof Function) {
cloned = function() {
// 在函数中去执行原来的函数,确保 返回值相同
return target.call(this, ...arguments)
}
} else {
cloned = {};
}
// 拷贝属性值
for (let key in target) {
cloned[key] = deepClone(target[key]);
}
return cloned;
}
const obj1 = {
name: "hello",
time: new Date()
};
const obj2 = deepClone(obj1);
console.log('obj2', obj2.time.getTime) // obj2 [Function: getTime]
2.3 版本3: 优化实现
- 优化点1: 不克隆target原型上的 属性
- 优化点2: 解决 循环对象导致的 递归爆栈问题==> weakMap(src, clone)
- 注意,该版本在对象嵌套层级过深时,还是会出现递归爆栈的情况
js
function isObject(target) {
const type = typeof target;
return target && (type === 'object' || type === 'function');
}
// weakMap不会对target进行强引用,从而避免无法释放内存
function deepClone(target, cache = new weakMap()) {
// 递归中止条件1: 类型值为基本数据类型
if (!isObject(target)) {
return target;
}
// 递归中止条件2: 循环遇到 已克隆的缓存对象,直接返回缓存的克隆值即可
if (cache.get(target)) {
return cache.get(target);
}
// 对于引用类型,递归进行深拷贝
// 根据不同引用类型,初始化对应类型值
let cloned;
if (target instanceof Array) {
cloned = [];
} else if (target instanceof RegExp) {
// source属性能 获取到正则的模式,flags属性 能获取到 正则的参数
cloned = new RegExp(target.source, target.flags);
} else if (target instanceof Date) {
cloned = new Date(target);
} else if (target instanceof Function) {
cloned = function () {
// 在函数中去执行原来的函数,确保 返回值相同
return target.call(this, ...arguments);
};
} else {
cloned = {};
}
// 把 属性值-拷贝后的属性值 存入缓存map里
cache.set(target, cloned);
// 拷贝属性值
// for...in 会遍历包括原型上的 所有可迭代属性
for (let key in target) {
// 优化点1: 过滤掉原型身上的属性, 即只遍历本身的属性
if (target.hasOwnProperty(key)) {
cloned[key] = deepClone(target[key], cache);
}
}
return cloned;
}
const obj1 = {
name: "hello",
friends: ["a", "b"],
self: null,
};
obj1.self = obj1;
const obj2 = deepClone(obj1);
// { name: 'hello', friends: [ 'a', 'b' ], self: [Circular *1] }
console.log("obj2", obj2);
- 方法3: 递归实现2- 代码优化版
这版本的实现思路是和上文的 方法2是一致的,只是优化了具体实现的代码语法, 所以不再分步骤赘述 每一步的实现思路,直接给出完整代码。
js
function isObject(obj) {
return obj !== null && (typeof obj === "object" || typeof obj === "function");
}
function getType(target) {
const typeStr = Object.prototype.toString.call(target);
return typeStr.match(/\[object (.*)\]/)[1];
}
function deepClone(target, cache = new WeakMap()) {
// S1.1 递归中止条件1: 类型值为基本数据类型
if (!isObject(target)) {
return target;
}
// S1.2 递归中止条件2: 特殊处理无法继续进行递归知道类型,如Date等
const targetType = getType(target);
const deepTypes = ["Object", "Array", "Map", "Set", "Arguments"];
if (!deepTypes.includes(targetType)) {
// 此处处理的是 基本类型的包装对象、日期、正则等特殊对象类型
if (["Date", "Error", "Number", "Boolean", "String"].includes(targetType)) {
return new target.constructor(target);
} else if (targetType === "RegExp") {
return new target.constructor(target.source, target.flags);
} else if (targetType === "Symbol") {
return Object(Symbol.prototype.valueOf.call(target));
} else if (targetType === "Function") {
return target;
} else {
return null;
}
}
// S1.3 递归中止条件3: 循环遇到 已克隆的缓存对象,直接返回缓存的克隆值即可
const cacheVal = cache.get(target);
if (cacheVal) {
return cacheVal;
}
// S2 支持各种数据类型
let cloned = new target.constructor();
// S2.2 特殊处理Set和Map类型
if (targetType === "Set") {
target.forEach((v) => {
cloned.add(deepClone(v));
});
return cloned;
}
if (targetType === "Map") {
target.forEach((v, k) => {
cloned.set(deepClone(k), deepClone(v));
});
return cloned;
}
// S3 把 属性值-拷贝后的属性值 存入缓存map里
cache.set(target, cloned);
// S4 递归实现深拷贝
for (const key in target) {
// S4.2 性能优化:for...in 会遍历包括原型上的 所有可迭代属性
// 这里可以 过滤掉对象原型身上的属性, 即只遍历对象本身的 属性
if (target.hasOwnProperty(key)) {
cloned[key] = deepClone(target[key], cache);
}
}
// S5 返回结果
return cloned;
}