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

news2024/11/24 20:49:00

目录

目录

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

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

  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) ? [] : {};
    //递归的将数组或对象的数组复制给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 对象。

当我们需要复制或克隆一个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.coloradmin.cn/o/1580719.html

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

相关文章

如何从头开始编写LoRA代码,这有一份教程

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 新建了免费的人工智能中文站https://ai.weoknow.com 新建了收费的人工智能中文站https://ai.hzytsoft.cn/ 更多资源欢迎关注 作者表示&#xff1a;在各种有效的 LLM 微调方法中&#xff0c;LoRA 仍然是他的首选。 LoRA&…

机器学习-随机森林算法预测温度

文章目录 算法简介解决问题获取数据集探索性数据分析查看数据集字段信息查看数据集综合统计结果查看特征值随时间变化趋势 数据预处理处理缺失数据字符列编码数据集分割训练集、验证集、测试集数据集分割 构建模型并训练结果分析与评估进一步优化实际使用经验总结 算法简介 随…

基于遗传优化的SVD水印嵌入提取算法matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 5.完整程序 1.程序功能描述 基于遗传优化的的SVD水印嵌入提取算法。对比遗传优化前后SVD水印提取性能&#xff0c;并分析不同干扰情况下水印提取效果。 2.测试软件版本以及运行结果展示 MA…

深度学习实践(一)基于Transformer英译汉模型

本文目录 前述一、环境依赖二、数据准备1. 数据加载2. 构建单词表程序解析&#xff08;1&#xff09;将列表里每个子列表的所有单词合并到一个新列表&#xff08;没有子列表&#xff09;中。&#xff08;2&#xff09;Counter&#xff08;&#xff09;-- 统计迭代对象各元素出现…

【Spring AOP】@Aspect结合案例详解(一): @Pointcut使用@annotation + 五种通知Advice注解(已附源码)

文章目录 前言AOP与Spring AOPAspect简单案例快速入门 一、Pointcutannotation 二、五种通知Advice1. Before前置通知2. After后置通知3. AfterRunning返回通知4. AfterThrowing异常通知5. Around环绕通知 总结 前言 在微服务流行的当下&#xff0c;在使用SpringCloud/Springb…

Mogdb双网卡同步最佳实践

大家都知道Oracle数据库无论是单机还是RAC集群在进行生产部署实施时&#xff0c;我们都会对网卡做冗余考虑&#xff0c;比如使用双网卡&#xff0c;比如public、心跳网络。这样的目的主要是为了安全&#xff0c;避免淡点故障。当然也网卡Bond不仅是可以做主备还可以支持负载均衡…

redis分布式锁+redisson框架

目录 &#x1f9c2;1.锁的类型 &#x1f32d;2.基于redis实现分布式 &#x1f953;3. 基于redisson实现分布式锁 1.锁的类型 1.本地锁&#xff1a;synchronize、lock等&#xff0c;锁在当前进程内&#xff0c;集群部署下依旧存在问题2.分布式锁&#xff1a;redis、zookeeper等…

OLAP介绍

OLAP OLAP介绍 Rollup OLAP&#xff08;在线分析处理&#xff09;的上下文中&#xff0c;"Rollup"是一个重要的概念&#xff0c;它指的是在多维数据集中自动地聚合数据到更高的层次或维度的过程。这种操作通常用于快速计算和展示汇总数据&#xff0c;以便于用户进…

包和final.Java

1&#xff0c;包 包就是文件夹。用来管理不同功能的Java类&#xff0c;方便后期代码的维护。 &#xff08;1&#xff09;包名的规则是什么&#xff1f; 公司域名反写报的作用&#xff0c;需要全部英文小写&#xff0c;见名知意。com.itheima.domain &#xff08;2&#xff…

15.队列集

1.简介 在使用队列进行任务之间的“沟通交流”时&#xff0c;一个队列只允许任务间传递的消息为同一种数据类型&#xff0c;如果需要在任务间传递不同数据类型的消息时&#xff0c;那么就可以使用队列集。FreeRTOS提供的队列集功能可以对多个队列进行“监听”&#xff0c;只要…

Redis高级-分布式缓存

分布式缓存 – 基于Redis集群解决单机Redis存在的问题 单机的Redis存在四大问题&#xff1a; 0.目标 1.Redis持久化 Redis有两种持久化方案&#xff1a; RDB持久化AOF持久化 1.1.RDB持久化 RDB全称Redis Database Backup file&#xff08;Redis数据备份文件&#xff09;…

QT drawPixmap和drawImage处理图片模糊问题

drawPixmap和drawImage显示图片时&#xff0c;如果图片存在缩放时&#xff0c;会出现模糊现象&#xff0c;例如将一个100x100 的图片显示到30x30的区域&#xff0c;这个时候就会出现模糊。如下&#xff1a; 实际图片&#xff1a; 这个问题就是大图显示成小图造成的像素失真。 当…

FPGA(Verilog)实现按键消抖

实现按键消抖功能&#xff1a; 1.滤除按键按下时的噪声和松开时的噪声信号。 2.获取已消抖的按键按下的标志信号。 3.实现已消抖的按键的连续功能。 Verilog实现 模块端口 key_filter(input wire clk ,input wire rst_n ,input wire key_in , //按下按键时为0output …

[NKCTF2024]-PWN:leak解析(中国剩余定理泄露libc地址,汇编覆盖返回地址)

查看保护 查看ida 先放exp 完整exp&#xff1a; from pwn import* from sympy.ntheory.modular import crt context(log_leveldebug,archamd64)while True:pprocess(./leak)ps[101,103,107,109,113,127]p.sendafter(bsecret\n,bytes(ps))cs[0]*6for i in range(6):cs[i]u32(p…

6.模板初阶(函数模板、类模板、类模板声明与定义分离)

1. 泛型编程 如何实现一个通用的交换函数呢&#xff1f; 使用函数重载虽然可以实现&#xff0c;但是有一下几个不好的地方&#xff1a; 重载的函数仅仅是类型不同&#xff0c;代码复用率比较低&#xff0c;只要有新类型出现时&#xff0c;就需要用户自己增加对应的函数代码的…

线性、逻辑回归算法学习

1、什么是一元线性回归 线性&#xff1a;两个变量之间的关系是一次函数&#xff0c;也是数据与数据之间的关系。 回归&#xff1a;人们在测试事物的时候因为客观条件所限&#xff0c;求的都是测试值&#xff0c;而不是真实值&#xff0c;为了无限接近真实值&#xff0c;无限次的…

HarmonyOS开发实例:【状态管理】

状态管理 ArkUI开发框架提供了多维度的状态管理机制&#xff0c;和UI相关联的数据&#xff0c;不仅可以在组件内使用&#xff0c;还可以在不同组件层级间传递&#xff0c;比如父子组件之间&#xff0c;爷孙组件之间等&#xff0c;也可以是全局范围内的传递&#xff0c;还可以是…

【考研数学】1800还是660还是880?

关于这几本习题册如何选择&#xff0c;肯定是根据他们的不同特点以及我们的需求结合选择&#xff0c;给大家的建议如下&#xff1a; 1800适合初期&#xff0c;可以帮助你熟悉数学公式和基础定义&#xff0c;迅速上手用。刚开始觉得难很正常&#xff0c;存在一个上手的过程&…

VRRP虚拟路由实验(思科)

一&#xff0c;技术简介 VRRP&#xff08;Virtual Router Redundancy Protocol&#xff09;是一种网络协议&#xff0c;用于实现路由器冗余&#xff0c;提高网络可靠性和容错能力。VRRP允许多台路由器共享一个虚拟IP地址&#xff0c;其中一台路由器被选为Master&#xff0c;负…

【Erlang】【RabbitMQ】Linux(CentOS7)安装Erlang和RabbitMQ

一、系统环境 查版本对应&#xff0c;CentOS-7&#xff0c;选择Erlang 23.3.4&#xff0c;RabbitMQ 3.9.16 二、操作步骤 安装 Erlang repository curl -s https://packagecloud.io/install/repositories/rabbitmq/erlang/script.rpm.sh | sudo bash安装 Erlang package s…