JS 面试合集(11 ~ 20)

引言

嘿! 欢迎来到我整理的 JS 面试题集!! 无论你是正在寻找前端开发的机会, 还是想巩固自己的 JS 知识, 相信这些问题会给你带来些帮助或启发!!!

考虑到 JS 相关面试内容会比较多, 所以这里会将其拆分多个篇幅进行讲解!!! 没篇固定十道题目!!!

现在, 让我们一起开始吧!!

一、防抖与节流

1.1 防抖

当触发高频函数事件, n 秒内又再次触发事件, 则取消上一次事件的执行; 也就是说一段时间内如果高频次触发事件只会执行最后一个, 之前触发的事件都认为是 "抖动" 引起的、是误操作、是做不得数的

  1. 闭包: 对事件进行包裹
  2. 内部使用定时器, 事件函数延迟 n 秒执行
  3. 当新的事件触发则取消上一次计算器, 重新一轮定时器
js 复制代码
// 防抖
function debounce (func, wait) {
  let timeout;
  return functio  ( ){
    if (timeout) {
      clearTimeout(timeout);
    }

    timeout = setTimeout(() => {
      func.apply(this, arguments);
    }, wait)
  }
}

1.2 节流

当触发高频函数事件, n 秒内又再次触发事件, 则不做任何处理, 节流会 稀释 函数的 执行频率; 也就是说假设我们的 n 设置为 1s 那么在高频操作下 10s 内最多只会触发 10 次事件

  1. 闭包: 对事件进行包裹
  2. 内部使用定时器, 事件函数延迟 n 秒执行
  3. 当新的事件触发, 如果上一个事件还未执行(定时器还在), 则不执行新的事件, 否则开始新的定时器
js 复制代码
// 节流
function throttle (func, wait) {
  let timeout;
  return function () {
    if (!timeout) {
      timeout = setTimeout(() => {
        timeout = null;
        func.apply(this, arguments);
      }, wait);
    }
  }
}

二、本地存储

主要介绍下面四种: cookie、localStorage、sessionStorage、indexedDB

  1. 存储类型: 小型文本字符串

  2. 作用: 指某些网站为了辨别用户身份而储存在用户本地终端上的数据!! 是为了解决 HTTP无状态导致的问题!!!

  3. 大小: 4KB (跟浏览器厂商有关系)

  4. 补充说明: 首先要知道标准的 http 协议是无状态的, 也就是指服务端对于客户端的每次请求都认为它是一个新的请求, 上一次会话和下一次会话之间是没有任何联系, 这时当用户访问服务端并进行登录后, 客户端之后的请求服务端依然无法对客户端身份进行识别, 如果将客户端与服务器之间的多次交互当做一个整体来看, 那么服务端若想识别客户端的身份那么就需要将多次交互所涉及的数据(状态)保存下来。 cookie 的作用就是对请求和响应的状态进行一个管理, 服务端通过在响应体中设置 cookie (状态), 客户端会将 cookie (状态) 存储起来, 之后客户端的每个请求都将 cookie (状态)带上, 这样服务端就能够对客户端的身份进行识别;在早期 cookie 还未出现之前有个最简单的办法就是在请求的页面中插入一个 token, 然后在下次请求时将这个 token 返回至服务器。这需要在页面的 form 表单中插入一个包含 token 的隐藏域, 或者将 token 放在 URL 的 query 字符串中来传递。这两种方法都需要手动操作, 而且极易出错, 而 cookie 则不同一般情况下客户端会自动存储服务器发来的 cookie, 并在之后的每次请求中自动带上 cookie 而无需客户端进行手动处理。需要注意的是标准的 http 协议指的是不包括 cookies 他是不属于标准协议, 虽然各种网络应用提供商, 实现语言、web 容器等, 都默认支持它。

详细介绍参考: cookie 简介

localStorage & sessionStorage

  1. 相同点
  • 大小: 5M (跟浏览器厂商有关系)
  • 本质上是对字符串的读取, 如果存储内容多的话会消耗内存空间, 会导致页面变卡
  • 当本页操作(新增、修改、删除) 了 Storage 的时候, 本页面不会触发 storage 事件, 但是别的页面会触发 storage 事件
  1. 不同点:
  • 生命周期: localStorage 持久化的本地存储, 除非主动删除数据, 否则数据是永远不会过期的; sessionStorage 一旦页面(会话)关闭, 就会删除数据
  • 限制: localStorage 受同浏览器限制、同源限制; sessionStorage 受到同浏览器限制、同源限制、同标签页限制

详细介绍参考: localStorage 和 sessionStorage 简介

2.3 indexedDB

优点:

  • 储存量理论上没有上限
  • 所有操作都是异步的, 相比 localStorage 那几个同步操作, 性能会更高, 尤其是数据量较大时
  • 原生支持储存 JS 的对象
  • 是个正经的数据库, 意味着数据库能干的事它都能干

缺点:

  • 操作非常繁琐
  • 使用上有一定门槛

相同点

  1. 跨域: 都存在跨域问题
  2. 存储数据类型: 存储的值都是字符串, 如果要存储对象则需要通过 JSON.stringify 进行格式化

不同点

  1. 生命周期: cookie 可自定义时长(默认会话结束即失效)、sessionStorage 会话结束则失效、localStorage 永久存在, 除非手动清除(通过 api 清除或直接清理浏览器缓存)
  2. 大小: cookie 4KB、sessionStorage 5MB、localStorage 5MB
  3. 语法上: cookie 通过直接修改 window.document.cookie 来进行增删改, localStorage 和 sessionStorage 则通过对应 api(setItem getItem clear ) 来修改数据
  4. 参与通信: cookie 会参与通信, 在请求接口时会自动在请求头中添加 cookie、localStorage 和 sessionStorage 则不参与通信

三、模块化

3.1 CommonJS 模块化规范

CommonJS

规范 是 NodeJS 中实现的一种模块化规范

  1. 使用 require 导入模块, require 是一个可执行函数
  2. 使用 module.exports 导出模块
  3. 特性:
  • 同步: require 加载文件是同步进行的
  • 缓存机制: require 第一次加载模块会对该模块进行缓存, 再次加载模块时会先比较异同, 若模块没有改变, 就不会执行 require
  • 每次加载都是对值的拷贝
  • 运行时加载

3.2 ES6 模块化规范

E6 正式将模块定义为一种规范

  1. 使用 import 导入模块
  2. 使用 export 导出模块
  3. 特性:
  • 异步: import 加载文件是异步加载的
  • 每次加载都是模块的一个引用地址
  • 编译时加载

3.3 ES6 和 CommonJS 区别

  1. 本质区别:
  • CommonJS 是输出的是值的拷贝, 模块内部的改变, 不会影响到其他地方
  • ES6 模块输出的是值的引用, 模块内部的改变, 会影响到其他地方
  1. 运行时机:(由本质决定)
  • commentJS 是运行时才加载模块(因为导出的是值的拷贝, 所以每次都要重新加载保证最新), 所以它就有了缓存机制
  • ES6 则是编译时进行加载(引起模块导出的是引用)
  1. 语法上不同
  2. require 是同步加载、import 是异步加载
  3. this 关键词:
  • 在 ES6 模块顶层, this 指向 undefined
  • CommonJS 模块的顶层的 this 指向当前模块
  1. 在 ES6 模块中可以直接加载 CommonJS 模块, 但是只能整体加载, 不能加载单一的输出项
  2. 循环引用:
  • CommonJS 模块遇到循环加载时, 输出的是当前已经执行那部分的值, 而不是代码全部执行后的值
  • ES6 模块是动态引用, 如果使用 import 加载一个变量, 变量不会被缓存, 真正取值的时候是能够取到最终的值

3.4 为什么不在浏览器也是用 CommonJS

  • 回答这个问题之前, 我们首先要清楚一个事实, CommonJS 的 require 语法是同步的, 当我们使用 require 加载一个模块的时候, 必须要等这个模块加载完后, 才会执行后面的代码

  • 那我们的问题也就很容易回答了, NodeJS 是服务端, 使用 require 语法加载模块, 一般是一个文件, 只需要从本地硬盘中读取文件, 它的速度是比较快的

  • 但是在浏览器端就不一样了, 文件一般存放在服务器或者 CDN 上, 如果使用同步的方式加载一个模块还需要由网络来决定快慢, 可能时间会很长, 这样浏览器很容易进入 假死状态

  • 所以异步加载, 是比较适合在浏览器端使用

3.5 其他模块方案

  • AMD 模块化规范: 定义异步模块
  • CMD 模块化规范: 定义通用模块定义

3.6 参考

3.7 TODO

  • ES 模块如何被 webpack 编译的

四、Promise

4.1 promise a+ 规范

  1. promise 有三个状态: promise 只会从等待变为成功, 或者从等待变为失败
  • 等待(PENDING)
  • 已解决(RESOLVED), 返回的结果, 被 .then 捕获
  • 已拒绝(REJECTED), 返回的原因, 被 .catch 捕获
  1. promise 接受一个函数, 该函数会被立即执行(宏任务)
  2. promise 中 .then 有两个函数参数, 第一个函数参数作为已解决时的回调函数被调用(微任务), 第二参数则被作为已拒绝时的回调函数被调用(微任务)
  3. promise 中 .catch 方法用于捕获失败状态
  4. .then 和 .catch 会返回一个新的 Promise(所以才支持链式调用), 如果返回的是一个普通值则会使用 promise.resolve 进行包裹
  5. promise 错误永远会被最近的一个捕获函数捕获到, 可以是某个最近 .then 的第二参数函数, 也可能是最近的一个 .catch

4.2 promise 常用方法

  • Promise.resolve(): 静态方法返回一个已解决(RESOLVED)的 Promise 对象, .then 参数为函数传入的参数

  • Promise.reject(): 静态方法返回一个已拒绝(REJECTED)的 Promise 对象, .catch 拒绝原因为给定的参数

  • Promise.all(): 所以都已解决才是已解决, 否则出现一个已拒绝, 则是已拒绝;

    • 参数: 由多个 Promise 组成的可迭代对象
    • 功能: 所有 Promise 并行执行, 当所有输入的 Promise 都被兑现时, 返回的 Promise 也将被兑现(即使传入的是一个空的可迭代对象), 并返回一个包含所有兑现值的数组; 如果输入的任何 Promise 被拒绝, 则返回的 Promise 将被拒绝, 并带有第一个被拒绝的原因
  • Promise.allSettled(): 等待所有 Promise 执行完成(不会像 all() 一样只要有一个已拒绝, 就结束)

    • 参数: 由多个 Promise 组成的可迭代对象
    • 功能: 和 all() 方法不同, 当所有输入的 Promise 状态都已结束(包括传入空的可迭代对象时), 返回的 Promise 将被兑现, 并带有描述每个 Promise 结果的一个对象数组
  • Promise.any(): 一个成功则成功, 全部失败则失败, 并返回所有拒绝原因

    • 参数: 由多个 Promise 组成的可迭代对象
    • 功能: 和 all() 相反, 这里只要有一个状态改为已解决, 则结束, 并返回该值; 如果所有状态都是已拒绝, 则结束, 并返回所有拒绝的原因
  • Promise.race(): 只要一个状态被修改, 则结束;

    • 参数: 由多个 Promise 组成的可迭代对象
    • 功能: (竞速)等待第一个状态改变, 所有 promise 中只要一个状态改变即结束, 如果先成功了那就成功了, 如果先失败了那就失败了
  • Promise.prototype.then():

    • 参数: 两个函数, 第一个是已解决状态的回调函数, 第二个是已拒绝的回调函数
    • 功能: 设置 Promise 状态修改的一个回调函数, 需要注意的是该方法返回一个新的 Promise, 从而实现链式调用
  • Promise.prototype.catch():

    • 参数: 回调函数
    • 功能: 用于注册一个在 promise 被拒绝时要调用的函数, 需要注意的是该方法返回一个新的 Promise, 从而实现链式调用
  • Promise.prototype.finally():

    • 参数: 回调函数
    • 功能: 用于注册一个在 promise 状态被修改时要调用的函数, 需要注意的是该方法返回一个新的 Promise, 从而实现链式调用

4.3 中断 promise

  1. 在执行器中则, 使用 resolve reject 中断
js 复制代码
new Promise((resolve, reject) => {
  // ......
  // 直接调用 resolve 或 reject
  resolve();
});
  1. 中断调用链: 如果在某个 then/catch 执行之后, 不想让后续的链式调用继续执行了, 则可以返回一个永不结束的 Promise
js 复制代码
new Promise((resolve, reject) => {
  // ....
})
.then(() => {
  // ....
  // 关键代码: 返回一个永不结束的 Promise, 该 promise 状态永远是 PENDING 中, 导致后面的链式调用(.then/.catch/finally)无法被执行
  return Promise.reject(() => {});
})
.then(() => console.log('then'))
.catch(() => console.log('catch'))
.finally(() => console.log('finally'));
  1. 使用 Promise.race 竞速, 只要一个状态改变就结束
js 复制代码
const p1 = new Promise(() => {});
const p2 = new Promise(() => {});
const p3 = new Promise(() => {});
Promise.race([p1, p2, p3]).then(() => {});

4.4 手写 promise

下面是一个最简易版本的

js 复制代码
const PENDING = "PENDING";
const RESOLVED = "RESOLVED";
const REJECTED = "REJECTED";

class MyPromise {
  constructor(executor) {
    this.status = PENDING; // 宏变量, 默认是等待态
    this.onfulfilled = null; // .then 中成功的回调函数
    this.onrejected = null; // .then 中失败的回调函数

    // 定义 resolve
    const resolve = (value) => {
      if (this.status === PENDING) {
        // 保证只有状态是等待态的时候才能更改状态
        this.status = RESOLVED;
        this.onfulfilled && this.onfulfilled(value);
      }
    };

    // 定义 reject
    const reject = (reason) => {
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.onrejected && this.onrejected(reason);
      }
    };

    // 执行 executor 传入我们定义的成功和失败函数
    try {
      executor(resolve, reject);
    } catch (e) {
      reject(e); // 报错
    }
  }

  // .then 方法
  then(onfulfilled, onrejected) {
    this.onfulfilled = onfulfilled;
    this.onrejected = onrejected;
  }
}

4.5 参考

五、柯里化

参考: 深入理解 JavaScript 柯里化: 提升函数的复用性和灵活性

5.1 柯里化

柯里化 (Currying), 又称 部分求值 (Partial Evaluation), 是函数编程的一种高级技巧, 通常只需要传递给函数 一部分参数 来调用它, 让它返回一个新的函数去 处理剩下的参数

  • 最简单的一个 DEMO
js 复制代码
// 使用「柯里化」思想编写的一个求和函数
const sumCurry = (a) => (b) => {
  console.log(a + b)
}

sumCurry(1)(2) // 3
  • 无限参数
js 复制代码
// 使用「柯里化」思想编写的一个求和函数
const sumCurry =  (...preArgs) => {
  // 1. 计算当前结果
  const preTotal = preArgs.reduce((total, ele) => (total + ele), 0)

  // 2. 使用 bind, 基于原函数创建新函数, 并将上一次结果作为第一个参数
  const _sumCurry = sumCurry.bind(this, preTotal)

  // 3. 修改 toString 返回值, 用于值值
  _sumCurry.toString = () => preTotal

  return _sumCurry
}

// 1. 参数固定
  • 普通函数转柯里化
js 复制代码
/**
 * 中转函数
 * @param fun           待柯里化的原函数
 * @param allArgs       已接收的参数列表
 * @returns {Function}  返回一个接收剩余参数的函数  
 */
const _curry = (fun, ...allArgs) => {
  // 1. 返回一个接收剩余参数的函数    
  return (...currentArgs) => {
    // 2. 当前接收到的所有参数
    const _args = [...allArgs, ...currentArgs] 

    // 3. 接收到的参数大于或等于函数本身的参数时, 执行原函数
    if (_args.length >= fun.length) {
      return fun.call(this, ..._args)
    }

    // 4. 继续执行 _curry 返回一个接收剩余参数的函数 
    return _curry.call(this, fun, ..._args)
  }
}

/**
 * 将函数柯里化
 * @param fun  待柯里化的原函数
 * @returns {Function} 返回「柯里化」函数
 */
const curry = (fun) => _curry.call(this, fun)


// 测试
const sum = (a, b, c) => (a + b + c) // 原函数
const currySum = curry(sum) // 柯里化 函数

currySum(1)(2)(3) // 6
currySum(1)(2, 3) // 6
currySum(1, 2, 3) // 6

5.2 柯里化作用

参数复用: 因为是闭包的一种实现, 所以能复用上一个函数的参数 提前计算: 提前计算好, 返回新的函数; 如兼容性处理, 提前对浏览器进行兼容性处理, 并返回一个通用函数 延迟运行: 返回的是新函数, 而不是值, 当所以参数收集完毕, 最后统一处理

5.3 柯里化和闭包的关系

闭包 和 柯里化 是两个不同的概念, 但它们之间有一定的关联:

  1. 在编程中, 闭包允许函数捕获并访问其定义时的上下文中的变量, 即使在其定义环境之外被调用时也可以使用这些变量, 闭包可以通过函数返回函数的方式创建, 从而使得内部函数可以访问外部函数的变量

  2. 柯里化是指高阶函数使用技巧, 柯里化函数的特点是, 允许被连续调用 f(a)(b)(c) 每次调用传递若干参数, 同时返回一个 接受剩余参数 的 新函数, 直到所有参数都被传递完毕, 才会执行主体逻辑

  3. 闭包和柯里化之间的关系在于, 柯里化函数 通常会 使用闭包 来 实现, 当我们将一个函数进行柯里化时, 每次返回一个新的函数, 这个新的函数会捕获前一次调用时的参数和上下文, 这个上下文就形成了闭包, 使得新函数可以在后续调用中继续访问之前传递的参数和上下文

六、类型判断

参考: JavaScript 中四种常见的数据类型判断方法 🔥

6.1 typeof

  1. 基本数据 类型除了 null 外都能够返回正确的数据类型(null 会返回 object)
  2. 函数返回 function
  3. typeof 可以判断基本数据类型, 但是难以判断除了函数以外的复杂数据类型
js 复制代码
typeof null         // 'object'
typeof 1            // 'number"'
typeof 'a'          // 'string'
typeof true         // 'boolean'
typeof undefined    // 'undefined'
typeof Symbol()     // 'symbol'
typeof 42n          // 'bigint'

typeof function(){} // 'function'
typeof NaN          // 'number'
typeof ({a:1})      // 'object'
typeof [1,3]        // 'object'
typeof (new Date)   // 'object'

6.2 instanceof & isPrototypeOf()

  1. 在 JS 中我们有 两种 方式可以判断 原型 是否存在于某个 实例 的 原型链 上, 通过这种判断就可以帮助我们, 确定 引用数据 的具体类型
  • instanceof 代码演示
js 复制代码
function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}

const auto = new Car('Honda', 'Accord', 1998);

auto instanceof Car // Car.prototype 是否在 auto 原型链上, true
auto instanceof Object // Object.prototype 是否在 auto 原型链上, true
  • isPrototypeOf 代码演示
js 复制代码
function Car() {}

const auto = new Car();

Car.prototype.isPrototypeOf(auto) // Car.prototype 是否在 auto 原型链上, true
Object.prototype.isPrototypeOf(auto) // Object.prototype 是否在 auto 原型链上, true

6.3 constructor

constructor 判断方法跟 instanceof 相似, 已知在 实例对象 的原型中存在 constructor 指向 构造函数, 那么借用这个特性我们可以用于判断 数据 类型

js 复制代码
function Car() {}
const auto = new Car();
auto.constructor === Car // true

不同于 instanceof, 通过该方式既可以处理 引用数据、又能够处理 基本数据

js 复制代码
(123).constructor === Number // true
(true).constructor === Boolean // true
('bar').constructor === String // true
  • 同于 instanceof, 不能判断 对象父类

6.4 终结办法: Object.prototype.toString()

Object.prototype.toString.call() 方法返回一个表示该对象的字符串, 该字符串格式为 "[object Type]", 这里的 Type 就是对象的类型

js 复制代码
const toString = Object.prototype.toString;

toString.call(111); // [object Number]
toString.call(null); // [object Null]
toString.call(undefined); // [object Undefined]

toString.call(Math); // [object Math]
toString.call(new Date()); // [object Date]
toString.call(new String()); // [object String]

注意: 对于自定义构造函数实例化出来的对象, 返回的是 [object Object]

js 复制代码
const toString = Object.prototype.toString;
function Bar(){}

toString.call(new Bar()); // [object Object]

默认, 如果一个对象有 Symbol.toStringTag 属性并且该属性值是个字符串, 那么这个属性值, 会被用作 Object.prototype.toString() 返回内容的 Type 值进行展示

js 复制代码
const toString = Object.prototype.toString;

const obj = {
  [Symbol.toStringTag]: 'Bar'
}

toString.call(obj) // [object Bar]

补充: 一个通用方法, 一行代码获取 数据的类型

js 复制代码
const getType = (data) => {
  return Object.prototype.toString.call(someType)
    .slice(8, -1)
    .toLocaleLowerCase()
}

七、作用域、作用域链、作用域链、闭包

7.1 作用域

  1. 作用域是在运行时代码中的变量、函数的可访问性
  2. 作用域决定了代码区块中变量和其他资源的可见性, 作用域最大的用处就是隔离变量, 不同作用域下同名变量不会有冲突
  3. ES6 之前 JavaScript 没有块级作用域, 只有全局作用域和函数作用域

7.2 全局作用域和函数作用域

  1. 全局作用域中的变量、函数在任何地方都能被访问到

  2. 函数作用域内的变量只有在函数内部才能被访问到(包括其子函数)

  3. 在 js 最外面定义的变量、函数属于全局作用域

  4. 在函数中定义的变量、函数属于函数作用域

  5. 对末定义的变量进行赋值, 将会被作为全局变量进行声明

  6. window 对象的属性拥有全局作用域

7.3 块级作用域

  1. var 声明的变量只在函数中声明才具有块级作用域特性
  2. let const 声明的变量在任意 {} 中都具有块级作用域

7.4 作用域链

作用域链是指查找变量的过程, 假如在函数中调用了某个变量 A, 首先会查询当前作用域看是否存在该变量, 有则直接使用, 没有则寻找上一层作用域, 如果还没有则继续往上一层就行选择, 如此一层层的寻找过程被称为作用域链

7.5 作用域和执行上下文

  1. 首先作用域和执行上下文并不是一个相同的概念
  2. js 属于解释型语言, 执行过程中分两个阶段: 解释和执行
  3. 在解析阶段作用域就被确定了, 但是执行上下文则是在执行阶段才被创建, 并且通过 call apply bind 都可修改执行上下文
  4. 执行上下文在运行时确定, 随时可能改变; 作用域在定义时就确定, 并且不会改变
  5. 补充: 执行上下文其实就是 this 指向, 只有在执行阶段才会被创建

补充:

解释阶段: 词法分析、语法分析、作用域规则确定 执行阶段: 创建执行上下文、执行函数代码、垃圾回收

7.6 闭包

闭包: 指一个函数有权限访问另一个函数中的变量的变量, 一般发生在嵌套函数中; 在嵌套函数, 返回一个新函数, 新函数引用了外层函数的变量, 导致外层函数的执行上下文无法销毁, 从而形成了闭包

应用场: 从现实出发, 就和容易想象带它的使用场景, 在需要缓存数据、在内存中维持变量的情况下使用, 如: 缓存数据(解决循环问题)、防抖、节流、柯里化等等

js 复制代码
// 解决循环问题
var list = document.getElementsByTagName('li');
for (var i = 0; i < list.length; i++) {
  list[i].addEventListener('click', function(){
    alert(i + 1)
  }, true)
}

var list = document.getElementsByTagName('li');
for (var i = 0; i < list.length; i++) {
  list[i].addEventListener('click', function(i){
    return function(){
      alert(i + 1)
    }
  }(i), true)
}
js 复制代码
// 柯里化
var add = function (m) {
  var temp = function (n) {
    return add(m + n);
  }

  // 为了取值, 隐性转换等
  temp.toString = function (){
    console.log('[ m ]', m);
    return m;
  }

  return temp;
}
js 复制代码
// 可在内存中维持住变量 a
const fun = a => () => {};
const f = fun({ name: 'qy' });

八、「for in」和「for of」的区别

本质区别:

  • for...in 用于迭代 对象 的可枚举属性(数组本质上也是对象, 所以也可用于数组)
  • for...of 用于跌到 可迭代对象, 因为数组是可迭代对象所以可以被使用, 普通对象不是可迭代对象所以不可被使用

九、map 和 forEach 的区别

9.1 相同点

  1. 参数一致:
  • callbackFn: (ele, index, arr) => {}
  • thisArg
  1. 作用: 都可用于循环
  2. 可变性: 都能在 callback 中改变原数组, 但是 map 并不推荐这么做

9.2 不同点

  1. 返回值不同
  • map 返回一个新数组, 新数组是基于源数组中每个元素生成的, 长度和源数组保持一致
  • forEach 返回 Undefined
  1. 是否可以链式调用
  • map 因为返回的是数组, 所以可以链式调用其他数组方法
  • forEach 因为返回的是 undefined, 所以不可以链式调用

9.3 性能比较

  1. 性能比较: for > forEach > map

性能的比较实际上与环境使用的 V8 版本相关, 这也是为什么 map 方法在 chrome 里比在 Node 中慢 10 倍, 有人测试过(在 chrome 62 和 Node.js v9.1.0 环境下): for 循环比 forEach 快 1 倍, forEach 比 map 快 20% 左右

  1. 原因分析:
  • for: for 循环没有额外的函数调用栈和上下文, 所以它的实现最为简单, 所以也最快

  • forEach: 对于 forEach 来说, 它的函数签名是这样的 array.forEach(function(currentValue, index, arr), thisValue) 其中包含了参数和上下文, 这会影响它的性能

  • map: map 最慢的原因是因为 map 和 forEach 相比还多了 返回一个新的数组 这一环节, 数组的创建和赋值会导致分配内存空间, 因此会带来较大的性能开销

9.2 参考

十、浅拷贝、深拷贝

参考: 八股文: 讲讲什么是浅拷贝、深拷贝?

10.1 前置知识

  1. 数据类型分类

基本数据类型: null undefined number string boolean symbol bigInt 引用数据类型: Object 细分有 Object Array Date Function ...

  1. 数据存储方式

基本数据: 基本数据类型保存在 栈内存, 栈内存 中分别存储着变量的标识符以及变量的值, 值是直接存储在变量访问的位置 引用数据: 数据存储在 堆内存 中, 栈内存 存储着变量标识以及变量的值(该值是一个引用地址)

  1. 栈内存 & 堆内存
  • 栈内存: 它是一种计算机内存中划分出来的一块 连续 的 存储区域, 它的主要特点是 先进后出
  • 堆内存: 它是一种计算机内存中划分出来的一块 非连续 的 存储区域, 它的特点是可以动态分配和释放内存空间, 但它需要手动管理内存空间
  1. 赋值操作, 不同类型数据, 内存变化
js 复制代码
let a = 1;
let b = a; // 栈内存会新开辟一个内存, 存储变量标识和值
js 复制代码
let a = { name: 'A',age: 10 };
let b = a;   // 栈内存会新开辟一个内存, 存储变量标识和值(堆内存中数据的引用地址)
a.age = 20;  // 修改的是堆内存中数据, a b 都指向了同一个引用地址(堆内存中的数据)

10.2 深拷贝

创建一个 新对象, 拷贝对象的所有属性, 如果属性是 基本数据, 拷贝的就是 基本数据 的值; 如果是 引用数据, 则需要重新分配一块内存, 拷贝该 引用数据 的所有属性, 然后将 引用地址 赋值给对应的属性, 如果该 引用数据 中某个属性也是 引用数据 则需要继续一层层递归拷贝......

  1. JSON.parse(JSON.stringify()): 利用 JSON.stringify 将对象转成 JSON 字符串, 再用 JSON.parse 把字符串解析成对象, 如此一来一去就能够实现 引用数据 的一个深拷贝, 但是该方法有缺陷如下:
  • NaN Infinity -Infinity 会被序列化为 null
  • Symbol undefined function 会被忽略(对应属性会丢失)
  • Date 将得到的是一个字符串
  • RegExp Error 对象, 得到的是空对象 {}
  • 如果被拷贝的对象中, 有多个属性复用同一个对象, 拷贝后复用的情况将不复存在
  • 如果存在 循环引用 对象则会报错
  1. structuredClone: 新的 API 可用于对数据进行 深拷贝, 同时还支持循环引用, 但是不支持 函数 类型的属性值

  2. 使用第三方库: lodash 中的 cloneDeep 方法

  3. 手写:

js 复制代码
// map 用于记录出现过的对象, 解决循环引用
const deepClone = (target, map = new WeakMap()) => {
  // 1. 对于基本数据类型(string、number、boolean......), 直接返回
  if (typeof target !== 'object' || target === null) {
    return target
  }

  // 2. 函数 正则 日期 MAP Set: 执行对应构造题, 返回新的对象
  const constructor = target.constructor
  if (/^(Function|RegExp|Date|Map|Set)$/i.test(constructor.name)) {
    return new constructor(target)
  }

  // 3. 解决 共同引用 循环引用等问题
  // 借用 `WeakMap` 来记录每次复制过的对象, 在递归过程中, 如果遇到已经复制过的对象, 则直接使用上次拷贝的对象, 不重新拷贝
  if (map.get(target)) {
    return map.get(target)
  }

  // 4. 创建新对象
  const cloneTarget = Array.isArray(target) ? [] : {}
  map.set(target, cloneTarget)

  // 5. 循环 + 递归处理
  Object.keys(target).forEach(key => {
    cloneTarget[key] = deepClone(target[key], map);
  })

  // 6. 返回最终结果
  return cloneTarget
}

10.3 浅拷贝

会新建一个对象, 拷贝对象的所有属性值, 对于 基本数据 来说就是拷贝一份对应的值, 但是对于 引用数据 则是拷贝一份 引用数据 的引用地址

  1. Object.assign()
  2. 展开运算符 ...
  3. 对于数组, 可以使用一些数组方法, 比如 Array.prototype.concat() Array.prototype.slice() Array.from 等方法, 它们的特点都是不改变原数组、同时返回一个新的数组
  4. 第三方库: lodash 中的 clone 方法
  5. 手写:
js 复制代码
const clone = (target) => {
  // 1. 对于基本数据类型(string、number、boolean......), 直接返回
  if (typeof target !== 'object' || target === null) {
    return target
  }

  // 2. 创建新对象
  const cloneTarget = Array.isArray(target) ? [] : {}

  // 3. 循环 + 递归处理
  Object.keys(target).forEach(key => {
    cloneTarget[key] = target[key];
  })

  return cloneTarget
}
const res= clone({ name: 1, user: { age: 18 } }) 

补充: 个人理解, 要判断是 浅拷贝 还是 深拷贝, 只需要区分拷贝前后两个数据是否是完全独立、隔离的, 是则是深拷贝, 否则则是浅拷贝
未完待续, 敬请期待!!!!!

相关推荐
Dream-Y.ocean2 小时前
鸿蒙PC平台宅匠 3D 室内设计适配实战:基于 Electron 壳方案的 Three.js 双渲染引擎与参数化户型编辑器开发
javascript·3d·harmonyos
Csvn3 小时前
性能监控
前端
Highcharts.js3 小时前
Highcharts Gantt使用符号的自定义数据标签开发甘特图示例
javascript·甘特图·highcharts·图表开发·实战代码·响应式图表
Csvn3 小时前
构建工具
前端
FRing034 小时前
vue2干货总结3(computed计算属性、watch监听器)
开发语言·前端·vue.js·vue2
kyriewen6 小时前
Skills 榜前 10 我全装了:只有 3 个能直接用
前端·javascript·程序员
IT_陈寒6 小时前
当Java的hashCode遇上Lombok,我的缓存被全清了
前端·人工智能·后端
工控丫丫7 小时前
帆软看板中经常使用的公式函数及经常搭的组合
前端·javascript·数据库
jerrywus7 小时前
我用 GPT-6.1-sol 做了台会讲解的 3D 售货机:60 罐饮料,一口没喝上
前端·agent·claude
IT规划师8 小时前
编程术语中的同步与异步:从"等不等"到工程取舍
后端·面试