javaScript手写专题——防抖/节流/闭包/Promise/深浅拷贝

目录

目录

一、 防抖/节流/闭包/定时器

编写一个组件,在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中编写一个组件的步骤:

  1. 在components文件夹中新增.vue组件
  2. 在views页面中引入你创建的组件
  3. 在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><inputv-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方法

    <inputv-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><inputv-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 发送网络请求时,需要经过一系列步骤来完成整个请求过程。

  1. xhr.open(method, url):这个方法用于初始化一个请求。其中,method 参数表示请求的方法(比如 GET 、POST 等),url 参数表示请求的 URL。调用 open 方法后,请求还没有真正发送出去,只是初始化了请求。
  2. xhr.onload:这是一个事件处理函数,当请求成功完成时被触发。在这个事件处理函数中,你可以对请求成功后的响应进行处理,比如获取响应内容并将其传递给 Promise 的 resolve 方法。
  3. xhr.onerror:与 xhr.onload 对应的是 xhr.onerror 事件处理函数。当请求发生错误时(比如网络错误),会触发这个事件处理函数。在这个事件处理函数中,你可以对请求失败的情况进行处理,比如将错误信息传递给 Promise 的 reject 方法。
  4. 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 的效果:

  1. 更优雅的 API:Promise 封装可以提供更清晰、简洁的接口,使得发起请求和处理响应更加直观。

  2. 链式调用:Promise 的特性使得可以链式调用多个异步操作,更容易处理复杂的请求逻辑。

  3. 错误处理:Promise 可以很方便地处理请求失败的情况,并进行错误处理。

  4. 更好的可读性:通过 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) ? [] : {};//递归的将数组或对象的数组复制给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 对象。

当我们需要复制或克隆一个MapSet对象时,需要保留它们内部的所有元素。我们需要使用一个新的集合对象来存储复制的元素,而不是直接修改原始对象。这是因为MapSet对象是可变的,如果直接修改原始对象,可能会导致与其他引用的对象产生不可预知的影响。

  // 处理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);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/800209.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

「51媒体网」邀请媒体采访报道对企业宣传有何意义?

传媒如春雨&#xff0c;润物细无声的&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 邀请媒体采访报道对企业宣传具有多重意义&#xff1a; 提升品牌知名度和曝光度&#xff1a;媒体是信息传播的重要渠道&#xff0c;通过媒体的报道&#xff0c;企业及其活动、产品能够迅…

软考信息处理技术员2024年5月报名流程及注意事项

2024年5月软考信息处理技术员报名入口&#xff1a; 中国计算机技术职业资格网&#xff08;http://www.ruankao.org.cn/&#xff09; 2024年软考报名时间暂未公布&#xff0c;考试时间上半年为5月25日到28日&#xff0c;下半年考试时间为11月9日到12日。不想错过考试最新消息的…

Sketch是免费软件吗?这款软件支持导入!

Sketch 是一款针对网页、图标、插图等设计的矢量绘图软件。Sketch 的操作界面非常简单易懂&#xff0c;帮助全世界的设计师创作出许多不可思议的作品。但是同时&#xff0c;Sketch 也有一些痛点&#xff1a;使用 Sketch 需要安装 InVision、Abstract 、Zeplin 等插件&#xff0…

配置 施耐德 modbusTCP 分布式IO子站 PRA0100

模块官方介绍&#xff1a;https://www.schneider-electric.cn/zh/product/BMXPRA0100 1. 总体步骤 2. 软件组态&#xff1a;在 Unity Pro 软件中创建编辑 PRA 模块工程 2.1 新建项目 模块箱硬件型号如下 点击 Unity Pro 软件左上方【新建】按钮&#xff0c;选择正确的 DIO …

Filter Listener Interceptor

文章目录 第一章 Filter1. 目标2. 内容讲解2.1 Filter的概念2.2 Filter的作用2.3 Filter的入门案例2.3.1 案例目标2.3.2 代码实现2.3.2.1 创建ServletDemo012.3.2.2 创建EncodingFilter 2.4 Filter的生命周期2.4.1 回顾Servlet生命周期2.4.1.1 Servlet的创建时机2.4.1.2 Servle…

git提交代码时报错,提不了

问题 今天在换了新电脑&#xff0c;提交代码时报错 ✖ eslint --fix found some errors. Please fix them and try committing again. ✖ 21 problems (20 errors, 1 warning) husky > pre-commit hook failed (add --no-verify to bypass) 解决 通过 --no-verify 解决&…

程序员如何搞副业

#程序员如何搞副业&#xff1f;# 在快速发展的IT行业中&#xff0c;程序员作为技术骨干&#xff0c;通常拥有扎实的编程能力和丰富的项目经验。然而&#xff0c;随着职业生涯的深入&#xff0c;许多程序员开始思考如何进一步提升自我价值&#xff0c;实现更多的经济收益。副业成…

RobotFramework测试框架(2)-测试用例

创建测试数据 测试数据语法 这里的测试数据就是指的测试用例。 测试文件组织 测试用例的组织层次结构如下&#xff1a; 在测试用例文件&#xff08; test case file &#xff09;中建立测试用例 一个测试文件自动的建成一个包含了这些测试用例的测试集&#xff08; test s…

python中for与while的区别是什么

Python中for循环和while循环本质上是没有区别的&#xff0c;但是在实际应用上&#xff0c;针对性不太一样。 for主要应用在遍历中&#xff0c;比如&#xff1a; example1&#xff1a; for i in range(10):print(i) 打印结果为&#xff1a; 0 1 2 3 4 5 6 7 8 9 注&#xff1a;…

RuoYi-Vue若依框架-在框架内用颜色选择器,页面显示色块

在用若依框架进行二次开发的时候写到自己的一个模块&#xff0c;其中涉及到颜色&#xff0c;我就想着是手动输入还是采用颜色选择器呢&#xff0c;考虑到后续涉及到另一个字段编码于时就采用了颜色选择器&#xff0c;选择完的颜色显示的是十六进制的颜色选择器&#xff0c;这时…

Excel 文件底部sheet 如何恢复

偶然打开一个excel文件&#xff0c;惊奇地发现&#xff1a;原来excel文件底部的若干个sheet居然全都看不到了。好神奇啊。 用其它的电脑打开同样的excel文件&#xff0c;发现&#xff1a;其实能看到的。说明这个excel文件并没有被损坏。只要将修改相关设置。就可以再次看…

JS与Python函数在语法的区别

区别 标题语法&#xff1a;Python使用缩进来表示代码块&#xff0c;而JavaScript使用大括号{}。 Python函数定义&#xff1a; def my_function():# 函数体JavaScript函数定义&#xff1a; function myFunction() {// 函数体 }标题参数传递&#xff1a;Python支持位置参数、…

flask接口返回文本、json、图片格式

&#x1f4da;博客主页&#xff1a;knighthood2001 ✨公众号&#xff1a;认知up吧 &#xff08;目前正在带领大家一起提升认知&#xff0c;感兴趣可以来围观一下&#xff09; &#x1f383;知识星球&#xff1a;【认知up吧|成长|副业】介绍 ❤️感谢大家点赞&#x1f44d;&…

【随笔】Git 高级篇 -- 提交的技巧(上) rebase commit --amend(十八)

&#x1f48c; 所属专栏&#xff1a;【Git】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &#x1f496; 欢迎大…

【分治算法】大整数乘法Python实现

文章目录 [toc]问题描述基础算法时间复杂性 优化算法时间复杂性 Python实现 个人主页&#xff1a;丷从心. 系列专栏&#xff1a;Python基础 学习指南&#xff1a;Python学习指南 问题描述 设 X X X和 Y Y Y都是 n n n位二进制整数&#xff0c;计算它们的乘积 X Y XY XY 基础…

ChatGPT 之联盟营销

原文&#xff1a;ChatGPT for Affiliate Marketing 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 第二章 制定转化对话 制定转化对话是每个营销人员和企业所有者都应该掌握的关键技能。它涉及创建和传递引人入胜的信息&#xff0c;吸引您的受众并激励他们采取行动。…

Pytorch张量的数学运算:矩阵运算

文章目录 一、基础运算二、矩阵的特殊运算1、矩阵的转置1.1、语法1.2、示例1.2.1、二维矩阵转置1.2.2、更高维度的张量转置 2、方阵的行列式2.1、计算行列式2.2、示例&#xff1a;使用PyTorch计算行列式 3、方阵的迹4、方阵的逆4.1、计算矩阵的逆4.2、使用PyTorch计算逆矩阵 二…

若依 ruoyi-vue 接口挂载获取Resources静态资源文件权限校验

解决小程序图片打包过大&#xff0c;放置后端&#xff0c;不引用ngnix、minio等组件&#xff0c;还能进行权限校验 package com.huida.web.controller.common.app;import com.huida.common.core.controller.BaseController; import com.huida.common.utils.file.FileUtils; imp…

vulhub之fastjson篇-1.2.27-rce

一、启动环境 虚拟机:kali靶机:192.168.125.130/172.19.0.1(docker地址:172.19.0.2) 虚拟机:kali攻击机:192.168.125.130/172.19.0.1 本地MAC:172.XX.XX.XX 启动 fastjson 反序列化导致任意命令执行漏洞 环境 1.进入 vulhub 的 Fastjson 1.2.47 路径 cd /../../vulhub/fa…

蓝桥杯刷题-12-公因数匹配-数论(分解质因数)不是很理解❓❓

蓝桥杯2023年第十四届省赛真题-公因数匹配 给定 n 个正整数 Ai&#xff0c;请找出两个数 i, j 使得 i < j 且 Ai 和 Aj 存在大于 1 的公因数。 如果存在多组 i, j&#xff0c;请输出 i 最小的那组。如果仍然存在多组 i, j&#xff0c;请输出 i 最小的所有方案中 j 最小的那…