给企业建设网站的流程图,南昌网站建设模板网络公司,青岛 网站备案,有限公司网站建设 中企动力重庆目录 目录 一、 防抖/节流/闭包/定时器 编写一个组件#xff0c;在input中输入文本#xff0c;在给定的数据中查找相关的项目#xff0c;并渲染搜索结果列表 1.新增InputSearch.vue组件 key的作用 2.新增 InputView.vue 3.添加路由 4.效果演示 follow up加上防抖怎么处理 1.… 目录 目录 一、 防抖/节流/闭包/定时器 编写一个组件在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时可以准确地追踪每个元素的变化。有了keyVue能够更高效地识别出哪些节点是新增、删除或更新的从而减少不必要的DOM操作提高性能。如果不加keyvue在dom更新时会尽可能地复用已存在的DOM元素Vue可能会出现混乱导致不必要的重新渲染或错误的DOM更新。 在vue3ts这种语法要定义每项的数据类型使用interface定义一个Item类型定义相关变量时将类型带上。
templatedivinputv-modelsearchTextinputsearchItemsplaceholder请输入搜索文本/ul v-ifsearchResults.lengthli v-forresult in searchResults :keyresult.id{{ result.name }}/li/ul/div
/templatescript setup langts
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 refArrayItem([]);function searchItems() {if (searchText.value) {searchResults.value items.filter((item) item.name.toLocaleLowerCase().includes(searchText.value.toLocaleLowerCase()));} else {searchResults.value [];}
}
/scriptstyle/style通过数组的filter过滤方法简单使用字符串的includes从原始字符串中匹配子串。为了查询通用性将所有字母转成小写进行查询。 2.新增 InputView.vue 由于测试组件在组件内部定义了数组常量这里就不通过属性传值了。 templateInputSearch/InputSearch
/template
script setup langts
import InputSearch from ../components/InputSearch.vue;
/script
style/style3.添加路由 为了显示效果将单个功能通过路由隔开用单个页面呈现 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方法 inputv-modelsearchTextinputdebounceInputplaceholder请输入搜索文本/ 定义并初始化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是个对象我们可以认为timernull的时候定时器结束。那么!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)清空定时器呀而是用timernull。 因为节流场景下不需要真正取消定时器因为同一时刻不会出现多个定时器。只需要控制timer变量的状态达到节流的效果。timernull时节流停止。 思考防抖为什么用clearTimeout 因为防抖情况下每次点击都会创建一个定时器需要将之前的定时器取消手动清理防止内存泄漏。 思考那节流没有显式调用clearTimeout清除定时器会不会造成内存泄漏呢 节流不会造成内存泄漏。定时器执行完毕后会自动被系统回收不会一直存在于内存中。在节流函数中即使没有使用clearTimeout来清除定时器只要定时器执行完毕后将timer设为null就不会造成内存泄漏。定时器执行完毕后会被系统回收不会一直占用内存。 以上是我手写节流时候的思考可能你们也有这种疑惑有没有(●▼●) 2.效果演示 完整代码
templatedivinputv-modelsearchTextinputthrottleInputplaceholder请输入搜索文本/ul v-ifsearchResults.lengthli v-forresult in searchResults :keyresult.id{{ result.name }}/li/ul/div
/templatescript setup langts
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 refArrayItem([]);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);}};
}
/scriptstyle/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 对接口访问的效果的方法。 XMLHttpRequestXHR是js原生的异步操作它可以在不刷新页面的情况下向服务器发送请求并获取数据实现异步通信。但是在使用 XHR 时我们常常需要写大量的回调函数来处理请求和响应的结果代码量急剧增加逻辑变得混乱难以维护。这时Promise 就可以起到很好的封装作用。
Promise的作用 通过使用 Promise 封装 XMLHttpRequest可以实现以下类似 Axios 的效果 更优雅的 APIPromise 封装可以提供更清晰、简洁的接口使得发起请求和处理响应更加直观。 链式调用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 langenheadmeta charsetUTF-8 /meta nameviewport contentwidthdevice-width, initial-scale1.0 /titleDocument/title/headbodybutton idmyButton发送请求/button/body
/html
scriptfunction 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);});}
/script3.效果演示 三、深拷贝/浅拷贝 浅拷贝和深拷贝的概念 浅拷贝复制对象的引用而不是对象本身新对象和原对象共享内存空间。深拷贝复制对象本身而不是对象的引用新对象和原对象不共享内存空间。区别浅拷贝只复制对象的引用修改新对象可能会影响原对象深拷贝会复制对象本身新对象和原对象互不影响。 应用场景 浅拷贝当对象比较简单且不包含引用类型数据时可以使用浅拷贝。深拷贝当对象包含引用类型数据或者需要完全独立的副本时应使用深拷贝。 请编写一个浅拷贝函数 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) ? [] : {};//递归的将数组或对象的数组复制给copyObjfor (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;}//进行深拷贝设置mapmap.set(obj, true);//根据数组还是对象创建新的变量let copyObj Array.isArray(obj) ? [] : {};//递归的将数组或对象的数组复制给copyObjfor (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;}//进行深拷贝设置setset.add(obj);//根据数组还是对象创建新的变量let copyObj Array.isArray(obj) ? [] : {};//递归的将数组或对象的数组复制给copyObjfor (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;}//进行深拷贝设置mapmap.set(obj, true);//根据数组还是对象创建新的变量let copyObj Array.isArray(obj) ? [] : {};//递归的将数组或对象的数组复制给copyObjfor (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;}//进行深拷贝设置setset.add(obj);//根据数组还是对象创建新的变量let copyObj Array.isArray(obj) ? [] : {};//递归的将数组或对象的数组复制给copyObjfor (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);