目录
目录
一、 防抖/节流/闭包/定时器
编写一个组件,在input中输入文本,在给定的数据中查找相关的项目,并渲染搜索结果列表
1.新增InputSearch.vue组件
key的作用
2.新增 InputView.vue
3.添加路由
4.效果演示
follow up加上防抖怎么处理
1.怎么实现防抖?
2.手写debounce
3.如何调用debounce
4.效果演示
follow up 加上节流怎么处理
1.手写throttle
2.效果演示
二、Promise
请使用Promise封装XMLHttpRequest,并发出真实请求获取数据
1. 为什么要用Promise封装XMLHttpRequest?
Promise的作用
2.封装request方法
3.效果演示
三、深拷贝/浅拷贝
请编写一个浅拷贝函数 shallowCopy(obj),实现对一个对象的浅拷贝。
1.使用Object.assign
请编写一个深拷贝函数 deepCopy(obj),实现对一个对象的深拷贝。
1.使用JSON.stringify和JSON.parse
2.使用递归
3.解决递归循环引用——使用Map/Set
4.解决强引用——使用WeakMap/WeakSet
follow up:在递归+weakMap的基础上考虑以下场景的深拷贝:函数、正则表达式、日期对象、error对象、Map和Set对象、Symbol类型、原型链。
处理正则、日期、Error
处理函数Function
处理symbol类型
处理Map和Set对象
处理对象或数组,复制原型链
完整代码
测试
一、 防抖/节流/闭包/定时器
编写一个组件,在input中输入文本,在给定的数据中查找相关的项目,并渲染搜索结果列表
考察vue组件的封装,v-model的使用,事件和方法的使用
在vue中编写一个组件的步骤:
- 在components文件夹中新增.vue组件
- 在views页面中引入你创建的组件
- 在router添加views页面的路由
1.新增InputSearch.vue组件
分析题目:编写一个输入框,输入框在输入信息的时候自动检索。首先:是不是需要一个变量接收用户的输入啊,因此需要使用v-model双向绑定一个变量,接收用户输入的searchText。
如何自动检索,是不是需要事件触发?谁会触发事件,input的@input获取输入时的事件。将过滤结果的方法写在searchItems中。
在渲染结果的时候,使用for循环,这里用列表li接收每一项信息吧。因为结果是数组,所以输出时要用for循环,在写for循环的时候,一定要加key
key的作用
为什么要加key?
key是什么,是一个元素的唯一标识,这样Vue在更新DOM时可以准确地追踪每个元素的变化。有了
key
,Vue能够更高效地识别出哪些节点是新增、删除或更新的,从而减少不必要的DOM操作,提高性能。如果不加key,vue在dom更新时会尽可能地复用已存在的DOM元素,Vue可能会出现混乱,导致不必要的重新渲染或错误的DOM更新。
在vue3+ts这种语法要定义每项的数据类型,使用interface定义一个Item类型,定义相关变量时将类型带上。
<template>
<div>
<input
v-model="searchText"
@input="searchItems"
placeholder="请输入搜索文本"
/>
<ul v-if="searchResults.length">
<li v-for="result in searchResults" :key="result.id">
{{ result.name }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
interface Item {
id: number;
name: string;
}
const searchText = ref("");
const items = [
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Orange" },
{ id: 4, name: "Pear" },
] as Item[];
const searchResults = ref<Array<Item>>([]);
function searchItems() {
if (searchText.value) {
searchResults.value = items.filter((item) =>
item.name
.toLocaleLowerCase()
.includes(searchText.value.toLocaleLowerCase())
);
} else {
searchResults.value = [];
}
}
</script>
<style></style>
通过数组的filter过滤方法,简单使用字符串的includes从原始字符串中匹配子串。为了查询通用性,将所有字母转成小写进行查询。
2.新增 InputView.vue
由于测试组件,在组件内部定义了数组常量,这里就不通过属性传值了。
<template>
<InputSearch></InputSearch>
</template>
<script setup lang="ts">
import InputSearch from "../components/InputSearch.vue";
</script>
<style></style>
3.添加路由
为了显示效果,将单个功能通过路由隔开,用单个页面呈现
import { createRouter, createWebHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "home",
component: HomeView,
},
//添加路由
{
path: "/input",
name: "input",
component: () => import("../views/InputView.vue"),
},
],
});
export default router;
4.效果演示
follow up加上防抖怎么处理
考察防抖、闭包、setTimeout用法。要区分防抖和节流,防抖是疯狂点,最后停下来了间隔gap一段时间,触发事件。节流是,固定的一段时间gap,不管你点多少下,都在gap后触发一次。两者都是延迟执行函数。
防抖的应用场景: 用于输入框输入验证、搜索框实时搜索等场景,可以避免频繁触发事件导致的性能问题。
1.怎么实现防抖?
- 首先:要某个东西延迟执行是不是可以使用定时器setTimeout,方法调用放在setTimeout里
可是setTimeout执行后如果不管它,那么当前请求肯定会在某个时间后执行(注意事件循环,不是定时器定了5秒就会在5秒后执行),防抖是不是按最后一次点击的setTimeout为准啊,前面点击的事件不触发。但是你怎么知道当前就是最后一次点击了呢?换个思路,我们是不是可以处理当前点击的时候,将之前的定时器取消就好了。
- 其次:设置一个timer,每次新的setTimeout的时候给之前的timer清除掉,再生成新的timer。
这里还需要考虑一个问题,要封装一个防抖函数,那么timer是不是不能放在全局作用域里,你想啊,你这个防抖可以作为方法单独使用,你还能依赖全局作用域这不乱套了。那么作为方法单独封装,怎么能访问到之前的timer呢?你想的了什么,是不是闭包啊。
- 在哪定义timer:将timer定义在外层,内层通过return一个函数返回,在函数内部使用timer
闭包是不是描述了一种状态,有两个函数,函数内部引用了函数外部的变量,即使外部的函数已经执行过一次,但是由于内部的函数还在调用,引用的变量不会销毁
2.手写debounce
function debounce(func: Function, delay: number) {
let timer: any = null;
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
func();
}, delay);
};
}
防抖定义好了,怎么使用呢,观察debounce,内部初始化了一个timer,返回了一个定时器执行的函数。我们在使用的时候希望timer类似一个全局变量,初始化一次,但是可以被多次修改,在执行setTimeout的时候销毁之前的timer。所以timer只会被初始化一次。也就是说外层的debounce只会被执行一次。这点很重要!看下调用debounce方式
3.如何调用debounce
这里将input事件改成debounceInput方法
<input
v-model="searchText"
@input="debounceInput"
placeholder="请输入搜索文本"
/>
定义并初始化debounceInput方法=》通过赋值语句将debounce函数调用的结果复制给debounceInput
const debounceInput = debounce(searchItems, 3000);
为什么debounceInput是一个函数,但可以通过赋值语句拿到?
因为debounce返回的不是别的常量、数组啥的,是一个函数。debounceInput等价于debounce内部的那个函数。 你多次点击实际使用的是不是里面的return的那个function啊,不会在去初始化timer。
为什么timer不会被二次初始化,因为在赋值语句的时候debounce从上到下执行一遍已经初始化了timer并返回了。而debounceInput已经拿到返回值不会反复执行debounce,就不会反复初始化timer。
4.效果演示
在程序中打个debuuger看下执行过程
timer只在页面重新加载时,赋值给debounceInput的时候被初始化;多次点击只有最后一次执行了方法,之前的定时器都被clear了
follow up 加上节流怎么处理
节流:节流技术确保在一定时间间隔内只执行一次函数,无论事件触发频率多高。节流在这个题中显示是不合适的,这里只是捎带着写节流。
节流的应用场景:适用于滚动事件、resize事件等频繁触发的事件,可以控制函数的执行频率,减少不必要的计算和渲染。
1.手写throttle
在一定时间间隔内只执行一次函数。是不是还是用定时器将方法包裹住。
什么时候创建定时器?
是不是需要一个标识,标记定时器执行完成了。这里你可以在闭包的外层函数里再创建一个标识符,标记定时器是否完成。也可以直接用timer这个对象标记定时器是否结束。你只需要true和false就行了,谁来标记,无所谓嘛。
什么时候结束定时器?
是不是方法执行的时候清空啊。所以标识跟方法都写在定时器里面。这里我们就使用timer作为定时器是否完成的标识,由于timer是个对象,我们可以认为timer=null的时候定时器结束。那么!timer的时候是不是有定时器啊,被节流住了
throttle方法如下:
function throttle(func: Function, delay: number) {
let timer: any = null;
return function () {
if (!timer) {
//如果没有节流
timer = setTimeout(() => {
timer = null;
func();
}, delay);
}
};
}
外层黄色的timer是先执行,经过delay时间后(考虑事件循环实际要大于delay时间),内部的绿色timer才会被清空。
思考:诶?这里为什么没有使用clearTimeout(timer)清空定时器呀,而是用timer=null。
因为节流场景下,不需要真正取消定时器,因为同一时刻不会出现多个定时器。只需要控制timer变量的状态达到节流的效果。timer=null时节流停止。
思考:防抖为什么用clearTimeout?
因为防抖情况下,每次点击都会创建一个定时器,需要将之前的定时器取消(手动清理防止内存泄漏)。
思考:那节流没有显式调用clearTimeout
清除定时器,会不会造成内存泄漏呢?
节流不会造成内存泄漏。定时器执行完毕后会自动被系统回收,不会一直存在于内存中。在节流函数中,即使没有使用
clearTimeout
来清除定时器,只要定时器执行完毕后将timer
设为null
,就不会造成内存泄漏。定时器执行完毕后会被系统回收,不会一直占用内存。
以上是我手写节流时候的思考,可能你们也有这种疑惑,有没有(●▼●)
2.效果演示
完整代码
<template>
<div>
<input
v-model="searchText"
@input="throttleInput"
placeholder="请输入搜索文本"
/>
<ul v-if="searchResults.length">
<li v-for="result in searchResults" :key="result.id">
{{ result.name }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
interface Item {
id: number;
name: string;
}
const searchText = ref("");
const items = [
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Orange" },
{ id: 4, name: "Pear" },
] as Item[];
const searchResults = ref<Array<Item>>([]);
function searchItems() {
console.log("执行方法searchText:", searchText.value);
if (searchText.value) {
searchResults.value = items.filter((item) =>
item.name
.toLocaleLowerCase()
.includes(searchText.value.toLocaleLowerCase())
);
} else {
searchResults.value = [];
}
}
// const debounceInput = debounce(searchItems, 3000);
// function debounce(func: Function, delay: number) {
// let timer: any = null;
// return function () {
// clearTimeout(timer);
// console.log("打印timer", timer);
// timer = setTimeout(() => {
// func();
// }, delay);
// };
// }
const throttleInput = throttle(searchItems, 3000);
function throttle(func: Function, delay: number) {
let timer: any = null;
return function () {
if (!timer) {
timer = setTimeout(() => {
timer = null;
func();
}, delay);
}
};
}
</script>
<style></style>
二、Promise
请使用Promise封装XMLHttpRequest,并发出真实请求获取数据
前置知识:在使用 XMLHttpRequest 发送网络请求时,需要经过一系列步骤来完成整个请求过程。
xhr.open(method, url)
:这个方法用于初始化一个请求。其中,method
参数表示请求的方法(比如 GET 、POST 等),url
参数表示请求的 URL。调用open
方法后,请求还没有真正发送出去,只是初始化了请求。xhr.onload
:这是一个事件处理函数,当请求成功完成时被触发。在这个事件处理函数中,你可以对请求成功后的响应进行处理,比如获取响应内容并将其传递给 Promise 的resolve
方法。xhr.onerror
:与xhr.onload
对应的是xhr.onerror
事件处理函数。当请求发生错误时(比如网络错误),会触发这个事件处理函数。在这个事件处理函数中,你可以对请求失败的情况进行处理,比如将错误信息传递给 Promise 的reject
方法。xhr.send()
:这个方法用于实际发送请求。在调用send
方法后,浏览器会根据之前设置的请求方法、URL等信息,向服务器发送网络请求。
1. 为什么要用Promise封装XMLHttpRequest?
先看一下vue里异步请求操作axios的用法,get请求就调get方法,post请求就调post方法。通过then拿到成功的,通过catch捕捉失败的。是不是很方便。
// 引入 axios 库
const axios = require('axios');
// 发起 GET 请求
axios.get('https://api.example.com/data')
.then(response => {
// 请求成功后的处理逻辑
console.log(response.data);
})
.catch(error => {
// 请求失败后的处理逻辑
console.error(error);
});
// 发起 POST 请求
axios.post('https://api.example.com/data', { name: 'John', age: 30 })
.then(response => {
// 请求成功后的处理逻辑
console.log(response.data);
})
.catch(error => {
// 请求失败后的处理逻辑
console.error(error);
});
使用 Promise 封装 XMLHttpRequest 其实是一种实现类似 Axios 对接口访问的效果的方法。
XMLHttpRequest(XHR)是js原生的异步操作,它可以在不刷新页面的情况下,向服务器发送请求并获取数据,实现异步通信。但是,在使用 XHR 时,我们常常需要写大量的回调函数来处理请求和响应的结果,代码量急剧增加,逻辑变得混乱难以维护。这时,Promise 就可以起到很好的封装作用。
Promise的作用
通过使用 Promise 封装 XMLHttpRequest,可以实现以下类似 Axios 的效果:
更优雅的 API:Promise 封装可以提供更清晰、简洁的接口,使得发起请求和处理响应更加直观。
链式调用:Promise 的特性使得可以链式调用多个异步操作,更容易处理复杂的请求逻辑。
错误处理:Promise 可以很方便地处理请求失败的情况,并进行错误处理。
更好的可读性:通过 Promise 封装,可以使代码更具可读性和可维护性。
2.封装request方法
封装后request方法传递方法类型和url地址,返回一个Promise对象,可以使用.then方法和.catch方法处理成功或失败的请求
function request(method, url) {
// 整体返回一个 Promise 对象
return new Promise((resolve, reject) => {
// 创建一个xhr对象
let xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = function () {
// 请求成功的处理逻辑
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response); //使用resolve函数标记Promise成功,并且resolve中的内容传递后续的then方法
} else {
reject(xhr.statusText); // 使用reject函数标记Promise失败,并将reject中失败信息传递给后面的catch方法
}
};
xhr.onerror = function () {
reject(xhr.statusText); // 发生错误时的处理逻辑,同上
};
xhr.send(); // 发送请求
});
}
加上HTML及方法调用的完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="myButton">发送请求</button>
</body>
</html>
<script>
function request(method, url) {
// 整体返回一个 Promise 对象
return new Promise((resolve, reject) => {
// 创建一个xhr对象
let xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = function () {
// 请求成功的处理逻辑
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response); //使用resolve函数标记Promise成功,并且resolve中的内容传递后续的then方法
} else {
reject(xhr.statusText); // 使用reject函数标记Promise失败,并将reject中失败信息传递给后面的catch方法
}
};
xhr.onerror = function () {
reject(xhr.statusText); // 发生错误时的处理逻辑,同上
};
xhr.send(); // 发送请求
});
}
let button = document.getElementById("myButton");
button.addEventListener("click", () => {
handleClick();
});
function handleClick() {
//调用封装的request方法
request("GET", "https://jsonplaceholder.typicode.com/posts/1")
.then((response) => console.log("成功获取数据", response))
.catch((err) => {
console.log("获取列表失败", err);
});
}
</script>
3.效果演示
三、深拷贝/浅拷贝
浅拷贝和深拷贝的概念:
- 浅拷贝:复制对象的引用而不是对象本身,新对象和原对象共享内存空间。
- 深拷贝:复制对象本身,而不是对象的引用,新对象和原对象不共享内存空间。
- 区别:浅拷贝只复制对象的引用,修改新对象可能会影响原对象;深拷贝会复制对象本身,新对象和原对象互不影响。
应用场景:
- 浅拷贝:当对象比较简单,且不包含引用类型数据时,可以使用浅拷贝。
- 深拷贝:当对象包含引用类型数据,或者需要完全独立的副本时,应使用深拷贝。
请编写一个浅拷贝函数 shallowCopy(obj)
,实现对一个对象的浅拷贝。
1.使用Object.assign
//浅拷贝函数
function shallowCopy(obj) {
return Object.assign({}, obj);
}
浅拷贝测试
请编写一个深拷贝函数 deepCopy(obj)
,实现对一个对象的深拷贝。
1.使用JSON.stringify和JSON.parse
//深拷贝函数
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
2.使用递归
类型判断:首先要区分传入的是基本类型数据还是引用类型数据,因为深拷贝就是处理引用类型的。
基本类型的判断,一般使用typeof类型判断。对于数组、日期、正则表达式等特殊对象类型,以及null类型都会被判断为 "object"。基本类型和null都原封不动返回。因此基本类型的条件typeof obj !="object"||obj ===null
对于引用类型:区分数组还是对象,首先要创建变量接收拷贝的值吧,要初始化时数组还是对象。
如何区分数组?
- 可以使用Array.isArray(arr) 推荐
- 使用 arr instanceof Array 基于原型链
- 使用Object.prototype.toString.call(arr).includes("Array");
//使用递归方式创建深拷贝
function deepClone(obj) {
//基本类型原封不动返回
if (typeof obj != "object" || obj === null) {
return obj;
}
//根据数组还是对象,创建新的变量
let copyObj = Array.isArray(obj) ? [] : {};
//递归的将数组或对象的数组复制给copyObj
for (let key in obj) {
//for in可以遍历数组或对象
if (obj.hasOwnProperty(key)) {
//遍历数组时,for in会遍历原型链上的,要用hasOwnProperty区分
copyObj[key] = deepClone(obj[key]);
}
}
return copyObj;
}
使用递归有这个问题
let obj = {val : 100};
obj.target = obj;
deepClone(obj);//报错: RangeError: Maximum call stack size exceeded
这就是循环引用。我们怎么来解决这个问题呢?
3.解决递归循环引用——使用Map/Set
循环引用:对象之间可能存在循环引用,例如
a
对象中有一个属性引用b
对象,而b
对象中也有一个属性引用a
对象,这种情况下需要使用一种数据结构来记录已经处理过的对象,避免重复处理。使用 Map 和 Set 来记录已经处理过的对象,同时也需要在递归调用 deepClone 函数时传入这个记录对象,避免重复创建。
使用Map
//使用递归方式创建深拷贝
function deepClone(obj, map = new Map()) {
if (map.get(obj)) {
return obj;
}
//基本类型原封不动返回
if (typeof obj != "object" || obj === null) {
return obj;
}
//进行深拷贝,设置map
map.set(obj, true);
//根据数组还是对象,创建新的变量
let copyObj = Array.isArray(obj) ? [] : {};
//递归的将数组或对象的数组复制给copyObj
for (let key in obj) {
//for in可以遍历数组或对象
if (obj.hasOwnProperty(key)) {
//遍历数组时,for in会遍历原型链上的,要用hasOwnProperty区分
copyObj[key] = deepClone(obj[key], map); //deepClone递归调用的时候传入map
}
}
return copyObj;
}
//测试递归
const obj = { val: 2 };
obj.target = obj;
let newObj = deepClone(obj);//程序不会报递归的错误
console.log(newObj);
当第二次进入递归调用时,
map
参数仍然能够拿到是因为JavaScript中的函数参数是按值传递的,而Map
对象是引用类型。这意味着在函数内部对map
对象的修改会影响到函数外部传入的map
对象,因为它们引用的是同一个对象。
使用Set
//使用递归方式创建深拷贝+set
function deepClone(obj, set = new Set()) {
if (set.has(obj)) {
return obj;
}
//基本类型原封不动返回
if (typeof obj != "object" || obj === null) {
return obj;
}
//进行深拷贝,设置set
set.add(obj);
//根据数组还是对象,创建新的变量
let copyObj = Array.isArray(obj) ? [] : {};
//递归的将数组或对象的数组复制给copyObj
for (let key in obj) {
//for in可以遍历数组或对象
if (obj.hasOwnProperty(key)) {
//遍历数组时,for in会遍历原型链上的,要用hasOwnProperty区分
copyObj[key] = deepClone(obj[key], set); //deepClone递归调用的时候传入set
}
}
return copyObj;
}
好像是没有问题了, 拷贝也完成了。但还是有一个潜在的坑, 就是Map或Set 上的 key 和 value 构成了强引用关系,这是相当危险的。当key使用完,Map和Set仍未释放key和value的引用。被弱引用的对象可以在任何时候被回收,而对于强引用来说,只要这个强引用还在,那么对象无法被回收。
怎么解决这个问题?
ES6给我们提供了这样的数据结构,它的名字叫WeakMap和WeakSet,其中的键是弱引用的。这意味着如果没有其他强引用指向键,键值对会被自动从
WeakMap
和WeakSet中删除,从而避免内存泄漏问题。WeakMap的键都是对象。
4.解决强引用——使用WeakMap/WeakSet
//使用递归方式创建深拷贝+WeakMap实现
function deepClone(obj, map = new WeakMap()) {
if (map.get(obj)) {
return obj;
}
//基本类型原封不动返回
if (typeof obj != "object" || obj === null) {
return obj;
}
//进行深拷贝,设置map
map.set(obj, true);
//根据数组还是对象,创建新的变量
let copyObj = Array.isArray(obj) ? [] : {};
//递归的将数组或对象的数组复制给copyObj
for (let key in obj) {
//for in可以遍历数组或对象
if (obj.hasOwnProperty(key)) {
//遍历数组时,for in会遍历原型链上的,要用hasOwnProperty区分
copyObj[key] = deepClone(obj[key], map); //deepClone递归调用的时候传入map
}
}
return copyObj;
}
const obj = { val: 2 };
obj.target = obj;
let newObj = deepClone(obj);
console.log(newObj);
//使用递归方式创建深拷贝+set
function deepClone(obj, set = new WeakSet()) {
if (set.has(obj)) {
return obj;
}
//基本类型原封不动返回
if (typeof obj != "object" || obj === null) {
return obj;
}
//进行深拷贝,设置set
set.add(obj);
//根据数组还是对象,创建新的变量
let copyObj = Array.isArray(obj) ? [] : {};
//递归的将数组或对象的数组复制给copyObj
for (let key in obj) {
//for in可以遍历数组或对象
if (obj.hasOwnProperty(key)) {
//遍历数组时,for in会遍历原型链上的,要用hasOwnProperty区分
copyObj[key] = deepClone(obj[key], set); //deepClone递归调用的时候传入set
}
}
return copyObj;
}
const obj = { val: 2 };
obj.target = obj;
let newObj = deepClone(obj);
console.log(newObj);
follow up:在递归+weakMap的基础上考虑以下场景的深拷贝:函数、正则表达式、日期对象、error对象、Map和Set对象、Symbol类型、原型链。
手写深拷贝需要考虑很多细节,需要根据具体情况来进行处理。在前面的实现中,我们已经考虑了一些常见的情况,但是还有很多其他的情况需要考虑。在手写深拷贝时,需要考虑以下几种场景:
特殊的对象:正则表达式、日期对象、Error 对象,使用构造函数创建新的对象
Fuction对象:对象可能会有函数属性,例如构造函数或方法,这种情况下需要将函数原样复制过来,而不是执行函数。
特殊类型:symbol
迭代复制:Map对象、Set对象。遍历原对象的键值对,然后在目标对象中创建新的 Map 和 Set 对象。
原型链:对象可能有原型链,例如
a
对象的原型是b
对象,b
对象的原型是c
对象,这种情况下需要递归地复制原型链。
处理正则、日期、Error
正则表达式:正则表达式也是一种特殊的对象,它们也可能存在于对象中。正则表达式也是不可枚举的,因此也不需要特殊处理。但是如果需要将正则表达式也复制过来,可以使用 RegExp() 构造函数来创建一个新的正则表达式。
日期对象:日期对象也是一种特殊的对象,它们也可能存在于对象中。如果需要将日期对象也复制过来,可以使用 Date() 构造函数来创建一个新的日期对象。
Error 对象:Error 对象也是一种特殊的对象,它们也可能存在于对象中。如果需要将 Error 对象也复制过来,可以使用 Error() 构造函数来创建一个新的 Error 对象。
// 处理正则表达式
if (obj instanceof RegExp) {
return new RegExp(obj);
}
// 处理日期对象
if (obj instanceof Date) {
return new Date(obj.getTime());
}
// 处理Error对象
if (obj instanceof Error) {
return new Error(obj.message);
}
既然都是通过构造函数创建的,我直接用对象本身的构造器方法constructor不就好了。
简化后的代码如下
if ([Date, RegExp, Error].includes(obj.constructor)) {
return new obj.constructor(obj);
}
处理函数Function
Fuction对象:对象可能会有函数属性,例如构造函数或方法,这种情况下需要将函数原样复制过来,而不是执行函数。在前面的实现中,我们并没有对函数进行特殊处理,因为函数是不可枚举的,for in 循环不会遍历函数。
JavaScript 函数是一种特殊的可调用对象,可以使用
new Function(arg1, arg2, ..., argN, functionBody)
创建一个新函数
// 处理函数
if (typeof obj === "function") {
return new Function("return " + obj.toString())();
}
处理symbol类型
Symbol 类型ES6 中新增的 Symbol 类型也可能存在于对象中。如果需要将 Symbol 类型也复制过来,可以使用 Symbol() 函数来创建一个新的 Symbol 类型。
Symbol
是一种基本数据类型,不是对象,因此不能直接使用new obj.constructor(obj)
的方式进行复制。使用for...in
循环和hasOwnProperty
方法也是无法直接访问到 Symbol 类型的属性的。这是因为for...in
循环会枚举对象的所有可枚举属性,包括原型链上的可枚举属性,而hasOwnProperty
方法只能检查对象自身拥有的属性。
使用
Reflect.ownKeys(obj)
方法可以获取对象所有自身的属性,包括可枚举和不可枚举的属性,而不会获取原型链上的属性。这个方法包括所有的 Symbol 类型的属性,因此使用这种方法可以获取 Symbol 属性。
for (let key of Reflect.ownKeys(obj)) {//这里可以枚举出symbol
//只处理对象本身属性
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key], map);
}
}
处理Map和Set对象
Map 和 Set 对象:ES6 中新增的 Map 和 Set 对象也可能存在于对象中。如果需要将 Map 和 Set 对象也复制过来,可以使用 Map.prototype.forEach() 和 Set.prototype.forEach() 方法来遍历原对象的键值对,然后在目标对象中创建新的 Map 和 Set 对象。
当我们需要复制或克隆一个
Map
或Set
对象时,需要保留它们内部的所有元素。我们需要使用一个新的集合对象来存储复制的元素,而不是直接修改原始对象。这是因为Map
和Set
对象是可变的,如果直接修改原始对象,可能会导致与其他引用的对象产生不可预知的影响。
// 处理Map对象
if (obj instanceof Map) {
const clonedMap = new Map();
map.set(obj, clonedMap);
obj.forEach((value, key) => {
clonedMap.set(key, deepClone(value, map));
});
return clonedMap;
}
// 处理Set对象
if (obj instanceof Set) {
const clonedSet = new Set();
map.set(obj, clonedSet);
obj.forEach((value) => {
clonedSet.add(deepClone(value, map));
});
return clonedSet;
}
- 对于
Map
对象,我们可以使用forEach()
方法遍历它的所有键值对,并将它们逐一添加到一个新的Map
对象中。在添加键值对时,我们需要递归调用deepClone()
函数,以确保复制的同时也能正确处理嵌套的Map
对象。- 对于
Set
对象,我们可以使用forEach()
方法遍历它的所有元素,并将它们逐一添加到一个新的Set
对象中。同样,在添加元素时,我们需要递归调用deepClone()
函数,以确保复制的同时也能正确处理嵌套的Set
对象。
处理对象或数组,复制原型链
原型链:对象可能有原型链,例如
a
对象的原型是b
对象,b
对象的原型是c
对象,这种情况下需要递归地复制原型链。在前面的实现中,我们并没有对原型链进行特殊处理,因为对象的原型链不会被 for in 循环遍历到。但是如果需要将原型链也复制过来,可以使用 Object.getPrototypeOf() 方法来获取原型对象
// 处理对象、原型链/symbol/数组
const proto = Object.getPrototypeOf(obj);
const clonedObj = Object.create(proto);
map.set(obj, clonedObj);
for (let key of Reflect.ownKeys(obj)) {//这里可以枚举出symbol
//只处理对象本身属性
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key], map);
}
}
return clonedObj;
使用
Object.getPrototypeOf(obj)
获取对象的原型,并使用Object.create
方法创建一个新的对象,并将其原型设置为该原型。这个新对象可以保证和原对象具有相同的原型,从而继承了原对象的原型上的属性和方法。
完整代码
function isObjcet(obj) {
if (typeof obj == "object" && obj != null) {
return true;
}
if (typeof obj == "symbol" || typeof obj == "function") {
return true;
}
return false;
}
function deepClone(obj, map = new WeakMap()) {
//非object类型,直接返回obj本身
if (!isObjcet(obj)) return obj;
// 处理循环引用
if (map.has(obj)) {
return map.get(obj);
}
//处理日期,正则、错误
if ([Date, RegExp, Error].includes(obj.constructor)) {
return new obj.constructor(obj);
}
// 处理函数
if (typeof obj === "function") {
return new Function("return " + obj.toString())();
}
// 处理Symbol类型
if (typeof obj === "symbol") {
}
// 处理Map对象
if (obj instanceof Map) {
const clonedMap = new Map();
map.set(obj, clonedMap);
obj.forEach((value, key) => {
clonedMap.set(key, deepClone(value, map));
});
return clonedMap;
}
// 处理Set对象
if (obj instanceof Set) {
const clonedSet = new Set();
map.set(obj, clonedSet);
obj.forEach((value) => {
clonedSet.add(deepClone(value, map));
});
return clonedSet;
}
// 处理对象、原型链/symbol/数组
const proto = Object.getPrototypeOf(obj);
const clonedObj = Object.create(proto);
map.set(obj, clonedObj);
for (let key of Reflect.ownKeys(obj)) {//这里可以枚举出symbol
//只处理对象本身属性
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key], map);
}
}
return clonedObj;
}
测试
const symbolKey = Symbol("key");
const obj = {
// 循环引用
self: null,
// 函数
func: function () {
console.log("Hello, World!");
},
// 正则表达式
regex: /hello/g,
// 日期对象
date: new Date(),
// Error对象
error: new Error("This is an error message"),
// Map对象
map: new Map([
[1, "one"],
[2, "two"],
]),
// Set对象
set: new Set([1, 2, 3]),
// Symbol类型
[symbolKey]: "symbol value",
};
// 设置循环引用
obj.self = obj;
// 测试原型链
function Parent() {
this.name = "Parent";
}
function Child() {
this.name = "Child";
}
Child.prototype = new Parent();
obj.proto = new Child();
const newObj = deepClone(obj);
newObj.date = new Date("2033");
newObj.map.set(1, "altert");
newObj.error = new Error("改变error");
// 输出obj对象
console.log(obj);
console.log(newObj);