普通视图

发现新文章,点击刷新页面。
今天 — 2025年10月18日技术

AI给我的建议

作者 menu
2025年10月18日 10:00

在掌握 Vue2、Vue3、核心生态(路由、状态管理等)和大屏可视化的基础上,想要进一步提升,可以从「技术深度」「技术广度」「工程能力」「业务理解」四个维度突破,形成从 “会用” 到 “精通” 再到 “能解决复杂问题” 的进阶。以下是具体方向和实践建议:

一、深化前端核心技术(打牢底层)

前端框架只是工具,底层基础决定了解决复杂问题的能力。重点突破 JavaScript 深入原理浏览器机制 和 前端性能优化

1. JavaScript 深入

  • 核心概念进阶

    • 深入理解异步编程(宏任务 / 微任务、Promise 源码、async/await 原理、Generator);
    • 原型链与继承(ES6 class 底层实现、寄生组合式继承);
    • 闭包与作用域(词法作用域、作用域链、内存泄漏场景);
    • 模块化(CommonJS/ES Module 差异、循环依赖处理、运行时加载机制)。
  • 设计模式实践:在 Vue 项目中落地常用设计模式,比如:

    • 用「观察者模式」理解 Vue 响应式原理;
    • 用「工厂模式」封装图表组件(统一初始化 ECharts/D3 的逻辑);
    • 用「策略模式」处理大屏中不同数据格式的解析逻辑。
  • TypeScript 精通:从 “能写类型” 到 “写好类型”:泛型高级用法(条件类型、映射类型)、类型声明文件(.d.ts)编写、Vue3 + TS 最佳实践(组件 props 类型约束、Composition API 类型推导)。

2. 浏览器与网络

  • 浏览器渲染机制:理解回流(Reflow)、重绘(Repaint)、合成层(Compositing),掌握如何在大屏可视化中减少渲染阻塞(比如大屏中大量图表动画的性能优化)。
  • 网络与请求优化:HTTP/HTTPS 协议细节(缓存策略、状态码、HTTPS 握手流程)、WebSocket 原理(大屏实时数据推送的底层逻辑)、HTTP/2 多路复用特性。
  • 前端安全:XSS、CSRF 攻击原理与防御(在大屏后台管理系统中尤为重要)、CSP 策略配置。

二、Vue 生态深度攻坚(从 “用” 到 “懂原理”)

不满足于 API 调用,深入 Vue 底层原理和高级特性,能解决复杂场景问题。

1. Vue3 底层原理

  • 响应式系统:手写简化版响应式(基于 Proxy),理解依赖收集(track)和触发更新(trigger)的流程,以及为什么 Vue3 比 Vue2 响应式更高效(解决数组索引、对象新增属性的监听问题)。
  • 虚拟 DOM 与 Diff 算法:理解 Vue3 的 PatchFlags(静态标记)和 hoistStatic(静态提升)优化,为什么能减少 Diff 开销(尤其大屏中大量静态元素的场景)。
  • Composition API 设计思想:对比 Options API 优劣,掌握自定义 Hooks 的设计原则(单一职责、可复用、可组合),比如封装 useChart(图表初始化 / 更新 / 销毁)、useResize(大屏自适应监听)等 Hooks。

2. 状态管理与路由高级用法

  • Pinia 深入:不仅会定义 store,还要掌握:

    • 状态持久化(结合 localStorage/IndexedDB,解决大屏刷新数据丢失问题);
    • 模块拆分与跨模块通信(大型大屏的多模块数据共享);
    • 开发工具与调试(Pinia Devtools 的时间旅行功能)。
  • Vue Router 高级场景:动态路由(基于权限的大屏菜单生成)、路由守卫的异步逻辑(权限校验)、路由过渡动画(大屏页面切换的流畅性优化)。

3. 组件设计与性能优化

  • 大型组件库设计:封装通用组件(如大屏中的数据卡片、图表容器、筛选器),考虑:

    • 可扩展性(通过 slots 或 props 允许自定义内容);
    • 可访问性(ARIA 标签、键盘导航,尤其大屏可能有触控 / 遥控操作场景);
    • 按需加载(结合 Vite 的 import () 实现组件懒加载)。
  • Vue 性能优化实战

    • 渲染优化:v-memo 缓存组件、v-slot 避免不必要的重渲染、defineAsyncComponent 异步组件;
    • 计算优化:computed 缓存、shallowRef/shallowReactive 减少响应式开销(大屏中纯展示的大数据无需深度响应式);
    • 打包优化:Vite 配置(splitChunks 拆分公共库、CDN 引入第三方包如 ECharts)。

三、扩展技术广度(从 “前端” 到 “全栈视角”)

大屏可视化往往需要与后端、数据、部署环境深度交互,扩展技术栈能提升解决问题的全局视野。

1. 数据可视化深化

  • 从 “用库” 到 “自定义” :不局限于 ECharts,学习 D3.js(自定义复杂可视化,如大屏中的拓扑图、流向图)、Three.js(3D 可视化,如大屏 3D 地球、3D 柱状图)、WebGL 基础(理解 3D 渲染原理)。
  • 可视化设计原则:学习数据可视化的配色(大屏深色 / 浅色模式适配)、布局(信息层级划分)、交互(缩放、钻取、tooltip 优化),参考《可视化之美》《鲜活的数据》等书籍。

2. 后端与数据库基础

  • Node.js 与 API 开发:用 Express/Koa 写简单的后端接口,理解前后端数据交互的全流程(比如大屏实时数据的后端推送逻辑)。
  • 数据库基础:了解 MySQL(关系型数据查询)、MongoDB(非结构化数据存储),能看懂大屏数据源的表结构和查询语句,优化前端数据请求(比如减少冗余字段)。

3. 跨端与部署

  • 跨端开发:用 Vue3 + Electron 开发大屏控制客户端(比如本地数据导入工具),或用 uni-app 开发移动端辅助控制页面(与大屏联动)。
  • 部署与运维:掌握 Docker 部署 Vue 项目(大屏通常部署在服务器 / 工控机上)、Nginx 配置(静态资源缓存、反向代理解决跨域)、CI/CD 流程(GitHub Actions 自动打包部署)。

四、工程化与协作能力(从 “个人开发” 到 “团队协作”)

大型大屏项目往往是团队协作的结果,工程化能力决定了项目的可维护性和效率。

1. 工程化体系搭建

  • 构建工具深入:从 “会用 Vite” 到 “优化 Vite 配置”:自定义插件(比如大屏项目中自动注入环境变量)、预构建优化(optimizeDeps 配置)、构建产物分析(rollup-plugin-visualizer)。
  • 代码质量与规范:搭建团队规范:ESLint + Prettier 强制代码风格、Husky + lint-staged 提交校验、Commitlint 规范提交信息(Angular 规范)。
  • 测试体系:为核心组件写单元测试(Jest + Vue Test Utils),为大屏关键功能写 E2E 测试(Cypress,比如数据刷新是否正常)。

2. 项目架构设计

  • 大型项目目录结构:设计可扩展的目录(按业务模块划分 vs 按功能分层),比如大屏项目可分为:

    plaintext

    src/
      ├── modules/          # 业务模块(首页大屏、详情页、设置页)
      ├── components/       # 通用组件
      │   ├── common/       # 基础组件(按钮、输入框)
      │   ├── chart/        # 图表组件(折线图、柱状图)
      ├── hooks/            # 自定义Hooks
      ├── utils/            # 工具函数(数据处理、日期格式化)
      ├── services/         # API请求
      ├── store/            # 状态管理
      ├── styles/           # 全局样式(主题、变量)
    
  • 状态管理设计:区分全局状态(用户信息、全局配置)、模块状态(某大屏的筛选条件),避免状态臃肿。

  • 权限系统设计:基于角色的权限控制(RBAC),在大屏管理后台中实现按钮级、数据级权限。

五、实战与总结(从 “知识” 到 “经验”)

技术提升离不开实战,通过复杂项目沉淀经验,并形成自己的方法论。

1. 做有挑战性的项目

  • 尝试开发一个 “企业级大屏平台”:支持多模板切换、数据动态配置、拖拽布局、权限管理,覆盖你学过的所有技术点。
  • 参与开源项目:比如给 Vue 生态库(如 Pinia、Vue Router)提交 PR,或贡献一个大屏可视化相关的工具库(如自定义 ECharts 主题、大屏适配 Hooks)。

2. 输出与总结

  • 写技术博客:总结大屏适配方案的踩坑经验、Vue3 性能优化实践、数据可视化设计思路等。
  • 做技术分享:在团队内分享你的大屏项目架构,或在社区(如掘金、B 站)讲解某个技术点(比如 “从 0 实现大屏 3D 地球”)。

总结

提升的核心路径是:底层原理强化 → 生态深度攻坚 → 技术广度扩展 → 工程化能力落地 → 实战经验沉淀。从 “实现功能” 到 “做好功能”,再到 “设计系统”,最终形成对复杂问题的拆解能力和解决方案的设计能力。大屏可视化作为你的优势,可以结合 3D 可视化、实时数据处理、跨端交互等方向深入,形成差异化竞争力。

手写 Promise.resolve:从使用场景到实现的完整推导

作者 CoderZz
2025年10月18日 09:57

Promise 是前端面试常考点,不管概念还是手写题都绕不开。

不知道你是不是跟我一样 —— 每次手写完,过阵子不碰,下次又卡壳,妥妥的 “失忆式编程”。

我总在想:能不能像做数学题那样,记几个 “公式” 就顺着推?毕竟编程也有规律,抓住关键点就能推导实现原理。所以才有了这篇文。

:::warning 大致推理逻辑:从使用场景 → 总结 “数学公式”→ 按公式推实现逻辑

:::

文章大纲主要如下:

  • Promise.resolve 的概念
  • Promise.resolve 的使用场景
  • 从场景总结出 “数学公式”
  • 根据 “公式” 推导出实现逻辑
  • “公式”概括总结

Promise.resolve 的概念

先搞懂最基础的:Promise.resolve 是 Promise 对象自带的静态方法—— 不用先 new 一个 Promise 实例,直接写 Promise.resolve(...) 就能用。

它的核心作用很简单:快速返回一个 “已成功(fulfilled)” 的 Promise 对象。不过有个小细节:如果传进去的参数本身就是 Promise 实例,它会直接把这个实例返回,不会多包一层 —— 这点后面推导实现时会用到,先记个小重点就好。

简单说就是:你给它点 “原料”,它要么给你包成成功的 Promise,要么直接把现成的 Promise 还给你,省了手动写 new Promise 的麻烦。

Promise.resolve 的使用场景

下面先来看下 Promise.resolve 使用的大多数具体场景,结合代码例子:

场景 1:处理基本数据类型 & 处理空值 & 处理无参数 & 处理引用数据类型

如果要把字符串、数字、布尔值、null、undefined 这些基本类型,快速变成 “已完成” 的 Promise,直接传进去就行,后续用 .then 就能拿到值:

// 数字
Promise.resolve(123).then((v) => console.log(v)); // 输出: 123

// 字符串
Promise.resolve("hello").then((v) => console.log(v)); // 输出: "hello"

// 布尔值
Promise.resolve(false).then((v) => console.log(v)); // 输出: false

// null
Promise.resolve(null).then((v) => console.log(v)); // 输出: null

// undefined
Promise.resolve(undefined).then((v) => console.log(v)); // 输出: undefined

当没传参数,它也能正常返回 Promise,.then 里能拿到就是 undefined

// 无参数
Promise.resolve().then((v) => console.log(v)); // 输出: undefined

遇到不含 then 方法的普通对象或者数组时,用 Promise.resolve 包一下,通过 .then 里就能直接拿到完整对象,不用额外处理:

// 对象
const obj = { a: 1, b: 2 };
Promise.resolve(obj).then((v) => console.log(v)); // 输出: { a: 1, b: 2 }

// 数组
const arr = [1, 2];
Promise.resolve(arr).then((v) => console.log(v)); // 输出: [1, 2]

场景 2:处理 Promise 实例

如果传进去的本身就是个 Promise 实例,它不会多包一层,而是直接返回这个实例 —— 成功的就走.then,失败的就走 .catch

// 成功的 Promise
const successP = Promise.resolve("ok");
Promise.resolve(successP).then((v) => console.log(v)); // 输出: "ok"

// 失败的 Promise
const failP = Promise.reject("error");
Promise.resolve(failP).catch((v) => console.log(v)); // 输出: "error"

// 失败的 Promise
let p1 = new Promise((resolve, reject) => {
  reject(-200);
});
Promise.resolve(p1).then(
  (data) => {
    console.log(data);
  },
  (reason) => {
    console.log(reason); // 输出:-200
  }
);

场景 3:处理 thenable 对象

thenable 对象:是一种 “长得像 Promise,但不是正经 Promise” 的对象 —— 它唯一的特点就是带一个 then 方法。

Promise.resolve 遇到这种对象时,会专门处理它的 then 方法,这里有两个关键逻辑要讲清楚:

  1. then 方法只认一个参数:thenable 的 then 方法只需要接收一个参数,这个参数是 resolve 回调(可以理解成 “成功时要执行的函数”)。

:::warning 当 Promise.resolve 处理它时,会主动调用这个 then 方法,并且把自己的 resolve 传进去 —— 只要在 then 里调用了这个 resolve,后续 .then 就能拿到值。

:::

  1. 失败怎么处理?靠抛错:因为 thenable 没有正经 Promise 的 reject 回调,所以如果想让它 “失败”,只能在 then 方法内部主动抛错(用 throw)。一旦抛错,Promise.resolve 就会把这个错误传给后续的 .catch

看两个例子就懂了:

// 例子1:thenable 成功(调用 resolve)
const basicThenable = {
  // then 只接收 resolve 一个参数
  then: (resolve) => {
    // 调用 resolve,把结果传出去
    resolve("done");
  },
};
// Promise.resolve 内部会执行 basicThenable.then 方法,所以后续 .then 能拿到 "done"
Promise.resolve(basicThenable).then((v) => console.log(v)); // 输出: "done"

// 例子2:thenable 失败(主动抛错)
const errorThenable = {
  then: (resolve) => {
    // 没调用 resolve,反而抛了个错
    throw "boom";
    // 这里就算不写 resolve,只要抛错,就会触发后续 .catch
  },
};
// Promise.resolve 捕获到抛错,所以后续 .catch 能拿到 "boom"
Promise.resolve(errorThenable).catch((v) => console.log(v)); // 输出: "boom"

简单总结,处理 thenable 时,Promise.resolve 就干两件事:

  1. 调用 thenable 的 then 方法,并传递自身的 resolve 回调
  2. 捕获 thenable 的 then 方法内部抛错,交给自身的 reject 处理

从场景总结的"数学公式"

前面梳理了 Promise.resolve 的各种使用场景,其实这些场景背后藏着统一的处理逻辑 —— 就像数学题有固定公式一样,我们把这些逻辑提炼成 “公式”,后面推导实现就有了明确方向。

先明确 Promise.resolve 的核心是 “接收一个参数(记为 x),返回一个 Promise 实例(记为 P)”,所有场景的处理规则都围绕 “x 是什么类型” 展开,最终总结出 3 条核心 “公式”:

:::warning

  1. 若 x 是基本类型 / 普通引用数据类型 / 空值 / 无参数 → Promise.resolve(x) = 成功态 Promise(结果为 x,无参数时 x 为 undefined)
  2. 若 x 是 Promise 实例 → Promise.resolve(x) = x(直接返回 x,不额外包装)
  3. 若 x 是 thenable 对象(带 then 方法) → 新建 Promise 实例 P,执行 x.then(P 的 resolve),抛错则调用 P 的 reject,最终返回 P

:::

根据“公式”推导出实现逻辑

要实现 Promise.resolve,核心是把前面总结的 4 条“公式”翻译成代码逻辑——先判断参数 x 的类型,再按对应规则处理,最终返回符合要求的 Promise 实例。下面按公式顺序逐步推导实现思路:

第一步:搭建基础结构

Promise.resolve 是静态方法,不需要通过实例调用,所以先确定它的基础形式:
直接在 Promise 构造函数上挂载 resolve 方法,方法接收参数 x,内部返回一个 Promise 实例(或 x 本身,按公式 2 处理)。

Promise.resolve = function (x) {
  // 后续逻辑按公式判断 x 类型,处理后返回对应结果
};

第二步:实现公式 1:x 是基本类型/普通引用类型/空值/无参数

x 不是 Promise 实例,也不是 thenable 对象时(即基本类型、普通对象、nullundefined,或没传 x),按公式 1 要求,返回一个“成功态”的 Promise,结果为 x(无参数时 xundefined)。

判断逻辑:先排除公式 2 和公式 3 的情况(后续处理),剩下的都走公式 1,直接用 new Promise 新建实例并调用 resolve(x)

Promise.resolve = function (x) {
  // 先处理公式2和公式3,剩下的走公式1
  // ...(公式2、3逻辑后续加)

  // 公式1:返回成功态 Promise,结果为 x
  return new Promise((resolve) => {
    // 此处(内部):直接调用 resolve x,外部就能通过 .then 并拿到 x 值
    // 无参数时 x 是 undefined
    resolve(x);
  });
};

第三步:实现公式 2:x 是 Promise 实例

按公式 2 要求,若 x 本身就是 Promise 实例,直接返回 x,不额外包装。

判断逻辑:用 x instanceof Promise 检查 x 是否为 Promise 实例,若是则直接 return x,跳过后续逻辑。

Promise.resolve = function (x) {
  // 公式2:x 是 Promise 实例 → 返回 x 本身
  if (x instanceof Promise) {
    return x;
  }

  // 公式1逻辑(后续加公式3后,公式1放在最后)
  // ...
};

第四步:实现公式 3:x 是 thenable 对象(带 then 方法)

按公式 3 要求,x 是带 then 方法的对象时,需新建 Promise 实例 P,执行 x.then(P的resolve),若 x.then 内部抛错,则调用 P的reject,最终返回 P

关键逻辑拆解:

  1. 判断 x 是否为 thenable:先确保 x 是对象或函数(避免非对象调用 then 报错),且 x.then 是函数;
  2. 新建 Promise 实例 P,在 P 的执行器里调用 x.then
  3. Presolve 传给 x.then(让 x.then 内部能触发 P 的成功态);
  4. x.thencatch 捕获抛错,若出错则调用 Preject(触发 P 的失败态);
  5. 返回新建的 P

代码实现:

Promise.resolve = function (x) {
  // 公式2:x 是 Promise 实例 → 返回 x
  if (x instanceof Promise) {
    return x;
  }

  // 公式3:x 是 thenable 对象(带 then 方法的对象)
  // 注意:typeof null 也等于 'object'
  if (typeof x === "object" && x !== null) {
    // 检查 x 是否有 then 方法,且 then 是函数
    if (typeof x.then === "function") {
      return new Promise((resolve, reject) => {
        // 执行 x.then,把 P 的 resolve 传进去
        x.then(resolve)
          // 捕获 x.then 内部的抛错,调用 P 的 reject
          .catch(reject);
      });
    }
  }

  // 公式1:剩下的情况 → 返回成功态 Promise
  return new Promise((resolve) => {
    resolve(x);
  });
};

最终完整代码实现

将上述逻辑整合,得到 Promise.resolve 的完整实现,能覆盖所有场景:

// 仅实现 Promise.resolve 静态方法
Promise.resolve = function (x) {
  // 情况1:若参数x是Promise实例,直接返回x(避免重复包装)
  if (x instanceof Promise) {
    return x;
  }

  // 情况2:若参数x是thenable对象(带then方法的对象)
  if ((typeof x === "object" && x !== null) || typeof x === "function") {
    // 确认x的then属性是函数,才按thenable处理
    if (typeof x.then === "function") {
      // 新建Promise实例,执行thenable的then方法
      return new Promise((resolve, reject) => {
        try {
          // 调用thenable的then,传递resolve和reject
          x.then(resolve, reject);
        } catch (error) {
          // 捕获then方法执行时的抛错,触发reject
          reject(error);
        }
      });
    }
  }

  // 情况3:其他类型参数(基础类型、null、undefined等)
  // 直接返回成功态Promise,结果为x
  return new Promise((resolve) => {
    resolve(x);
  });
};

“公式”概括总结

Promise.resolve 的实现本质,就是对我们之前提炼的 3 条核心“公式”的代码化翻译,每一段逻辑都对应一条规则:

:::warning

  1. 若 x 是基本类型 / 普通引用数据类型 / 空值 / 无参数 → Promise.resolve(x) = 成功态 Promise(结果为 x,无参数时 x 为 undefined)
  2. 若 x 是 Promise 实例 → Promise.resolve(x) = x(直接返回 x,不额外包装)
  3. 若 x 是 thenable 对象(带 then 方法) → 新建 Promise 实例 P,执行 x.then(P 的 resolve),抛错则调用 P 的 reject,最终返回 P

:::

记住这 3 条“分类处理”的公式,即使长时间不写代码,也能通过“判断类型 → 按规则处理”的步骤一步步推导出实现逻辑。这种将复杂逻辑提炼为简洁公式的方法,同样适用于其他前端手写题(如 Promise.rejectPromise.all),是提升代码记忆效率的有效手段。

从0死磕全栈之Next.js 自定义 Server 指南:何时使用及如何实现

2025年10月18日 09:21

Next.js 默认自带一个高性能的内置服务器,通过 next start 命令即可启动生产环境服务。但在某些特殊场景下,你可能需要对请求处理流程进行更精细的控制,这时就可以考虑使用 自定义 Server。本文将详细介绍 Next.js 自定义 Server 的使用场景、实现方式、注意事项以及最佳实践。


一、什么是自定义 Server?

自定义 Server 是指通过 Node.js 原生 HTTP 模块(或其他框架如 Express、Koa)手动启动一个服务器,并将 Next.js 应用“挂载”到该服务器上,从而实现对请求路径、中间件、代理等更灵活的控制。

⚠️ 重要提示:官方文档明确指出,绝大多数情况下你并不需要自定义 Server。因为这样做会失去 Next.js 的一些关键性能优化,例如:

  • 自动静态优化(Automatic Static Optimization)
  • 静态资源预渲染(ISR/SSG)的部分能力受限
  • 与 Standalone 输出模式不兼容

因此,仅在 Next.js 内置路由系统无法满足需求时才考虑使用。


二、适用场景

以下是一些可能需要自定义 Server 的典型场景:

  • 需要在 Next.js 应用前添加自定义中间件(如身份验证、日志记录、请求改写)
  • 需要将 Next.js 与其他后端服务(如 REST API、WebSocket 服务)集成在同一端口
  • 需要对特定路径进行代理或重定向(且无法通过 next.config.jsrewrites/redirects 实现)
  • 需要完全控制 HTTP 服务器生命周期(如优雅关闭、自定义错误处理)

三、实现步骤

1. 创建 server.js 文件

在项目根目录下创建 server.js(或 server.ts,但需注意语法兼容性):

// server.js
import { createServer } from 'http';
import { parse } from 'url';
import next from 'next';

const port = parseInt(process.env.PORT || '3000', 10);
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  createServer((req, res) => {
    const parsedUrl = parse(req.url!, true);
    handle(req, res, parsedUrl);
  }).listen(port);

  console.log(
    `> Server listening at http://localhost:${port} as ${
      dev ? 'development' : process.env.NODE_ENV
    }`
  );
});

📌 注意:server.js 不会经过 Next.js 的编译或打包流程,因此必须使用当前 Node.js 版本原生支持的语法(如不支持 JSX、TypeScript 等,除非自行编译)。

2. 修改 package.json 脚本

更新启动命令,不再使用 next devnext start,而是直接运行 server.js

{
  "scripts": {
    "dev": "node server.js",
    "build": "next build",
    "start": "NODE_ENV=production node server.js"
  }
}

💡 提示:开发时仍需先运行 npm run build(除非 dev: true),因为自定义 Server 也需要加载构建产物。

3. (可选)使用 Express 增强功能

如果你需要更强大的路由或中间件能力,可以结合 Express:

// server.js (with Express)
import next from 'next';
import express from 'express';

const port = parseInt(process.env.PORT || '3000', 10);
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  const server = express();

  // 自定义中间件
  server.use((req, res, next) => {
    console.log('Request URL:', req.url);
    next();
  });

  // 自定义 API 路由
  server.get('/api/hello', (req, res) => {
    res.json({ message: 'Hello from custom server!' });
  });

  // 将所有其他请求交给 Next.js 处理
  server.all('*', (req, res) => {
    return handle(req, res);
  });

  server.listen(port, (err) => {
    if (err) throw err;
    console.log(`> Ready on http://localhost:${port}`);
  });
});

记得安装依赖:

npm install express

四、配置选项说明

next() 函数接受一个配置对象,常用选项如下:

选项 类型 说明
dev boolean 是否启用开发模式,默认 false
dir string Next.js 项目目录,默认 '.'
conf object 等同于 next.config.js 的配置对象
quiet boolean 是否隐藏服务器日志,默认 false
hostname / port string / number 服务器运行的主机和端口(用于内部识别)

示例:

const app = next({
  dev: false,
  dir: './my-next-app',
  conf: { experimental: { appDir: true } }
});

五、注意事项与限制

  1. 性能损失:自定义 Server 会绕过 Next.js 的自动静态优化,所有页面将被视为 SSR 页面,即使它们可以静态生成。
  2. 不支持 Standalone 模式:如果你在 next.config.js 中启用了 output: 'standalone',则无法使用自定义 Server,因为该模式会生成独立的 server.js,与你的自定义逻辑冲突。
  3. 部署复杂度增加:你需要自行处理进程管理、错误监控、日志收集等,而 next start 已内置这些能力。
  4. TypeScript 支持有限server.ts 需要额外配置编译(如使用 ts-node),否则 Node.js 无法直接运行。

总结

Next.js 的自定义 Server 是一把“双刃剑”——它提供了极致的灵活性,但代价是牺牲性能与简化部署的优势。除非你有明确且无法通过其他方式解决的需求,否则应避免使用

当你确实需要它时,请确保:

  • 充分测试性能影响

  • 明确部署和维护成本

  • 保持 server.js 逻辑简洁、兼容当前 Node.js 版本

20251018-JavaScript八股文整理版(上篇)

作者 张可爱
2025年10月18日 01:11

🌸 第 1 页:JS 数据类型总览

🧠 一句话记忆:

JS 的数据类型分两大类: 基本类型(值类型) vs 引用类型(对象类型)

分类 子类型 存储位置
基本类型 Number, String, Boolean, Null, Undefined, Symbol, BigInt 栈内存(Stack)
引用类型 Object, Array, Function 堆内存(Heap)

📦 生活类比: 想象你的电脑桌上放两个地方:

  • “📏小抽屉”(栈内存)放简单小物件(值类型)
  • “📦大纸箱”(堆内存)放复杂的大玩意儿(对象)

小抽屉存的是实际的数值; 大纸箱存的是“标签”(引用),告诉你东西在哪个角落。


🌸 第 2 页:Number 类型

🧮 概念:

Number 表示数字,可以是整数、小数、正无穷 Infinity、负无穷 -Infinity、或者不是数的 NaN(Not a Number)。

💻 示例代码:

let a = 123;
let b = 1.23;
let c = Infinity;
let d = NaN;

💡 小笨蛋专属记忆法:

想象你在奶茶店当收银员:

  • 123 元:正常顾客付款;
  • 1.23 元:客人只买一小杯;
  • Infinity:有人要“无穷续杯”,你无语;
  • NaN:客人拿树叶付款,系统:???

🌸 第 3 页:Undefined、Null、String

🌀 Undefined:

表示“变量声明了但没赋值”。

let a;
console.log(a); // undefined

🍵 类比:你有一个空奶茶杯子,但里面还没倒奶茶。


🚫 Null:

表示“空对象,没有内容”。

let cup = null;
console.log(cup); // null

🍶 类比:杯子倒了奶茶但又被喝光了,里面是空的(杯子存在但内容没了)。


✨ String:

表示文本字符串,用 ' '" " 或 `` 包起来。

let name = '小可爱';
let desc = `今天学习 JavaScript`;

🍰 类比:你在奶茶杯上贴上标签写 “草莓味”,这个标签就是字符串。


🌸 第 4 页:Boolean、Symbol

✅ Boolean:

只有两种取值:truefalse

let isCute = true;
let isTall = false;

🐰 类比:像开关一样,开灯/关灯,真/假。


🧩 Symbol:

表示唯一值,主要用于区分对象属性,防止命名冲突。

let id1 = Symbol('id');
let id2 = Symbol('id');
console.log(id1 === id2); // false

🎀 类比:你和别人都叫“小可爱”,但身份证号不同——Symbol 就是那个身份证号。


🌸 第 5 页:引用类型(Object, Array, Function)

🧱 Object(对象):

一组键值对(key-value)

let person = {
  name: '小可爱',
  age: 18
};

🧋 类比:就像点奶茶的单子,写着「名字:小可爱」「口味:草莓味」。


🧮 Array(数组):

有序的值列表,用 [] 包住。

let drinks = ['奶茶', '果汁', '可乐'];

🍹 类比:一排奶茶杯排队站好,编号从 0 开始。


🧠 Function(函数):

封装可复用的逻辑代码。

function sayHi(name) {
  console.log('Hi ' + name);
}
sayHi('小可爱');

🎤 类比:这是一个“喊人函数”,输入名字,它就帮你喊出来。


🍬 总结口诀

“七值三引记心间,栈装小值堆装函。 数字布尔加字符串,空空未定各分担。 对象数组函数连,Symbol 唯一独自仙。”

🌼 第 6 页:函数与存储区别(值类型 vs 引用类型)

🧩 1.3.4 其他引用类型

代码示例:

function sum(a, b) {
  return a + b;
}
let result = sum(1, 2);

💬 解释: 这里定义了一个函数 sum,它像一个“小计算员”一样,你给他两个数 ab,他会算出和并告诉你结果。

📦 函数也是一种“引用类型”,存在堆内存里。 👉 想象它是一个“奶茶机”,机器说明书(函数体)放在堆里,调用时(执行)才会真的“打奶茶”。


🌸 第 7 页:存储区别

JavaScript 变量在底层的存储有两种:

  1. 基本类型存在“栈内存”(stack)
  2. 引用类型存在“堆内存”(heap)

🧠 1.4.1 基本类型

let a = 10;
let b = a;
b = 20;
console.log(a, b); // 10, 20

📖 解释:

  • 变量 a 存了一个数值 10
  • b = a 其实是复制了一份数值;
  • b 不影响 a

🍬 类比:

像你复制了一杯奶茶配方卡,自己改成多糖版,原来的不变。


🧱 1.4.2 引用类型

let obj1 = { name: '小可爱' };
let obj2 = obj1;
obj2.name = '小笨蛋';
console.log(obj1.name); // 小笨蛋

💬 解释:

  • obj1 是个对象,存在堆中;
  • obj2 = obj1 其实复制的是“地址标签”;
  • obj2 的内容,其实两人共用一杯奶茶!💥

🍵 类比:

你和朋友共用一杯奶茶(对象),谁加糖,整杯都变甜。


🌸 第 8 页:堆栈结构图

这页有一张大图,展示了:

  • 栈内存(stack):存放变量名与引用地址;
  • 堆内存(heap):存放对象真实数据;
  • 箭头代表“引用关系”。

🧋 生活类比:

栈像你桌上的“备忘贴”,写着「obj1 👉 柜子3号」; 堆像“奶茶仓库”,柜子3号里放着真正的奶茶。 当你复制 obj2 = obj1,就是在贴一张新的备忘贴,但还是指向同一个柜子。


🌼 第 9 页:小结

重点回顾:

  1. 基本类型(栈内存):

    • 存值;
    • 拷贝的是值;
    • 互不影响。
  2. 引用类型(堆内存):

    • 存地址;
    • 拷贝的是引用;
    • 改一个,全体变化。

📦 口诀记忆:

“栈装值,堆装址,拷贝值不动,拷贝址同命。”


🌷 第 10 页:数据结构入门

🧭 2.1 什么是数据结构?

就是“存放数据的方式”。

举例:

  • 书架的摆法不同(分类 vs 随意堆);
  • 程序中的“数组、栈、队列”也各自有存放规则。

📚 2.2 数组(Array)

let arr = [1, 2, 3];

📦 特点:

  • 有序;
  • 可通过索引(编号)访问。 🍵 类比:像奶茶杯一排排放好,每个都有编号。

🧱 2.3 栈(Stack)

一种 后进先出(LIFO) 的数据结构。

📘 图中: 栈顶进 → 栈顶出。

🧋 类比:

奶茶杯子堆叠起来,你最后放上去的那杯(最上面的),会最先被取走。

let stack = [];
stack.push('奶茶1');
stack.push('奶茶2');
stack.pop(); // 拿走奶茶2

🪜 2.4 队列(Queue)

一种 先进先出(FIFO) 的结构。

📘 图中: 队首出 → 队尾进。

🍹 类比:

排队买奶茶,先来的顾客先拿走,后来的慢慢排。

let queue = [];
queue.push('客人A');
queue.push('客人B');
queue.shift(); // 客人A先走

🎯 超萌总结(第6~10页)

概念 记忆口诀 生活类比
基本类型 拷贝值,不连心 两张奶茶配方卡
引用类型 拷贝址,共命运 共喝一杯奶茶
后进先出 奶茶杯堆叠
队列 先进先出 顾客排队买奶茶

🍓 第 11 页:队列结构延伸(Queue ➜ Linked List)

🪜 队列回顾

图上显示「先进先出 (FIFO)」队列流程: 排队买奶茶 → 先来的顾客先拿走 → 新顾客从队尾入队。


🧵 2.5 链表(Linked List)

链表是一种“节点(Node)串起来”的结构。 每个节点保存:

  1. 当前的数据;
  2. 下一个节点的引用(next 指针)。

💻 示例理解:

let node1 = { value: 1, next: null };
let node2 = { value: 2, next: null };
node1.next = node2;

💬 解释:

  • node1 里放了数字 1;
  • 它的 next 指向 node2;
  • 这样就变成 “1 → 2” 的链式结构。

🍬 类比: 想象一串奶茶杯,每个杯子底部绑着一根绳子,绳子另一头连着下一个杯子。 要拿第 3 杯奶茶,得先摸第 1 杯 → 找到第 2 杯 → 再摸到第 3 杯。


🏷️ 2.6 字典(Map / Object)

字典是一种 “键值对存储” 结构。

let dict = {
  name: '小可爱',
  drink: '草莓奶茶'
};

📘 解释:

  • “name” 是 key;
  • “小可爱” 是 value。

🍵 类比: 就像点单系统的“清单表”:

名称 奶茶类型
小可爱 草莓奶茶

你通过“名字(key)”找到对应的“奶茶(value)”。


🍪 2.7 集合(Set)

Set 是一种“不重复的值集合”。

let drinks = new Set(['奶茶', '果汁', '奶茶']);
console.log(drinks); // Set(2) { '奶茶', '果汁' }

💬 解释: Set 自动去重!同样的奶茶只保留一杯。

🍰 类比:

你在收银台点单系统里重复点“奶茶”,系统会说:“重复啦,不用再加一杯!”


🌸 第 12 页:DOM 操作

🏗️ 3. DOM 是什么?

DOM(Document Object Model)是网页的树状结构。 每个标签(如 <div><p><img>)都对应一个节点。

<div>
  <p>Hello 小可爱</p>
</div>

📖 这其实是:

父节点: div
子节点: p
文本: Hello 小可爱

🍓 类比: 网页就像一棵“奶茶店组织树”:

  • 店长(div) ↳ 店员(p) ↳ 负责说“欢迎光临!”(文本内容)

🪄 第 13 页:DOM 操作常见方法

3.2 操作节点四大类:

  1. 创建(Create)
  2. 查找(Select)
  3. 修改(Update)
  4. 删除(Delete)

是不是很像奶茶店 CRUD 流程:

  • Create:开新奶茶;
  • Read:查看奶茶;
  • Update:改口味;
  • Delete:下架。

🧋 第 14 页:创建节点

🧱 3.2.1 创建节点

let p = document.createElement('p'); // 创建 <p>
p.innerText = '草莓奶茶';
document.body.appendChild(p);

📖 解释:

  1. createElement —— 造一个新标签;
  2. innerText —— 填入文字;
  3. appendChild —— 把奶茶(元素)放到货架(页面)上。

🍹 类比:

你新做了一杯奶茶(创建节点), 在杯上写上口味(设置内容), 最后把它放到展示台(插入网页)。


✨ 第 15 页:修改、删除节点

🧾 修改属性

let img = document.querySelector('img');
img.setAttribute('src', 'milk-tea.jpg');

→ 改图片地址(换奶茶照片 📸)


🚮 删除节点

let p = document.querySelector('p');
p.remove(); // 删除 <p>

→ 把某段介绍(旧宣传语)删掉。


🌷 小结口诀(第 11~15 页)

概念 一句话记忆 生活类比
链表 节点串成链 奶茶杯用绳子连起来
字典 键值对查找 通过顾客名字查点单
集合 去重集合 重复点的奶茶只保留一杯
DOM 网页结构树 奶茶店组织架构图
CRUD 增删查改 奶茶上新、下架、改口味

🎯 一句总结送给你:

“链表串串连,字典查得全; 集合不重样,DOM搭舞台。”

🧋 第 16 页:DOM 的层级与节点关系

💡 DOM 节点层次图

图中展示了 “父节点(Parent)—子节点(Child)—兄弟节点(Sibling)” 的关系。

🍵 类比成奶茶店:

  • 店长(父节点)

    • 店员 A(子节点)
    • 店员 B(子节点)
    • 店员 A 和 B 是“兄弟节点”

🧱 获取父子关系的代码

let box = document.querySelector('.box');
let parent = box.parentNode;     // 父节点
let first = box.firstChild;      // 第一个子节点
let last = box.lastChild;        // 最后一个子节点
let next = box.nextSibling;      // 下一个兄弟节点

📖 解释:

  • parentNode:找上级;
  • firstChild:找第一个孩子;
  • lastChild:找最后一个孩子;
  • nextSibling:找下一个同级。

🍬 类比:

就像你在员工名单上查“谁是我的上司 / 第一个同事 / 下一个排班同事”。


🌷 第 17 页:修改节点内容

3.2.2 修改节点内容

JS 中修改标签里的内容常见 3 种写法:

div.innerText = '草莓奶茶';
div.textContent = '波霸奶茶';
div.innerHTML = '<p>新品:🍈哈密瓜奶绿</p>';

📖 区别:

  • innerText:只改文字;
  • textContent:更快,也改文字;
  • innerHTML:能直接插入 HTML 标签。

🍹 类比:

innerText:换菜单文字 textContent:换更快的菜单 innerHTML:整张新菜单贴上去(含图文排版)


🌸 第 18 页:插入节点 / 移除节点

🧩 插入节点

let p = document.createElement('p');
p.innerText = '新品上线啦!';
document.body.appendChild(p);

💬 就像你新做一杯奶茶 ➜ 放上货架展示。


🧺 插入指定位置

let parent = document.querySelector('.menu');
let item = document.createElement('li');
item.innerText = '抹茶拿铁';
parent.insertBefore(item, parent.firstChild);

📘 解释: insertBefore 表示插在某个节点前面。 🍵 类比:

你要把新品“抹茶拿铁”插到菜单第一个位置。


🧹 删除节点

let node = document.querySelector('p');
node.remove();

🍋 类比:

不好卖的奶茶,直接下架扔掉。


🍑 第 19 页:节点属性与样式

🧾 获取属性

let img = document.querySelector('img');
console.log(img.getAttribute('src'));

💬 获取 <img> 的“src”属性值。


🧱 修改属性

img.setAttribute('src', 'new-tea.jpg');

💬 换新图啦~(把“草莓奶茶”照片换成“抹茶奶茶”)。


🧍‍♀️ 修改样式

let box = document.querySelector('.box');
box.style.color = 'pink';
box.style.background = 'white';

🍓 类比:

店员换新制服:粉色字体 + 白色背景。 (也就是通过 JS 改 CSS)


☀️ 第 20 页:BOM(浏览器对象模型)

🌎 什么是 BOM?

BOM(Browser Object Model)是浏览器提供的“窗口级对象”,让你能操作整个浏览器,而不只是网页。

🌐 包括这些核心对象:

对象 功能
window 整个浏览器窗口本身
location 当前网页的 URL 地址
navigator 浏览器信息(比如 Chrome、Edge)
screen 屏幕大小、分辨率
history 浏览记录(前进、后退)

🪟 4.1 window 对象

所有 JS 全局变量其实都是挂在 window 上的。

window.alert('欢迎光临小可爱奶茶店!');

🍵 类比:

“window” 就像整家奶茶店的大玻璃窗—— 你能通过它“观察整个店”,还能在上面贴公告牌。


🎀 小笨蛋也能秒懂总结(第16~20页)

知识点 记忆口诀 奶茶店类比
父子兄弟节点 DOM家谱图 店长和店员层级
修改节点内容 三种方式改菜单 文字、HTML、整页菜单
插入/删除节点 append / remove 上架新品 / 下架旧款
属性与样式 get/set/style 看/改奶茶照片和外观
BOM 操作浏览器 整个奶茶店的“外部系统”

🎯 一句话助记:

“DOM管店内布局,BOM管整家店; 节点有家谱,样式换新颜。”

🌸 第 21 页:BOM 继续讲解(window)

🪟 4.2 window 对象

var a = 10;
console.log(window.a); // 10

💬 解释: 所有的全局变量,默认都挂在 window 这个“老大对象”上。 → 你定义的 a 实际上等价于 window.a

🍵 类比:

整个奶茶店的“总控台”是 window, 所有设备(冰箱、奶茶机、灯光)都登记在 window 上。 你说 a = 10,就像往总控台上新增了一个“新开关”。


🧾 window 常见方法

window.alert('新品上架啦!');
window.confirm('确定要删除这杯奶茶吗?');
window.prompt('请输入您最爱的奶茶口味:');
方法 功能 奶茶店类比
alert() 弹出提示 公告板贴出“新品上架”通知
confirm() 确认操作 问顾客“确定要退单吗?”
prompt() 获取输入 让顾客填写“口味偏好”

🌬️ 窗口操作

window.open('https://milk-tea.com');
window.close();

🍰 类比:

你点开一个新的“分店网页”(open), 再关掉(close)就像拉下卷帘门。


🍓 第 22 页:location 对象

负责记录、读取和修改“当前网址信息”。

console.log(location.href); // 当前网址
location.href = 'https://www.taobao.com'; // 跳转页面

🍬 类比:

“location” 是你奶茶店的“地址牌”。

  • 看地址牌(href) = 查看现在在哪个店;
  • 换地址牌 = 搬去新店(跳转页面)。

🧭 location 属性总结:

属性 含义 举例
href 完整地址 https://milk.com/page.html
hostname 主机名 milk.com
pathname 路径 /page.html
protocol 协议 https:
search 查询参数 ?id=123

🍵 小笨蛋记忆口诀:

“协议头、主机名、路径尾,问号后面参数配。”


🌼 第 23 页:navigator 对象

记录浏览器本身的信息,比如用的是 Chrome、Safari 还是 Edge。

console.log(navigator.userAgent);

输出示例:

Mozilla/5.0 (Windows NT 10.0; Win64; x64)
AppleWebKit/537.36 Chrome/122.0 Safari/537.36

🍹 类比:

navigator 就像奶茶店的“前台员工”, 你问他:“你是哪家店的?” 他回答:“Chrome 奶茶旗舰店,版本 122。”


💻 navigator 常用属性

属性 含义
appName 浏览器名称
appVersion 版本号
language 浏览器语言
platform 操作系统平台
userAgent 浏览器完整信息

🍓 类比:

这些属性就像员工证件:姓名、编号、语言、工号。


🧋 第 24 页:screen 和 history

4.5 screen(屏幕)

存储的是屏幕尺寸、可视宽高等信息。

console.log(screen.width, screen.height);

🍵 类比:

“screen” 就像你奶茶店的橱窗大小: 橱窗越大(屏幕宽),能展示的奶茶越多。


4.6 history(浏览记录)

记录你访问过的网页,可以前进或后退。

history.back(); // 后退一页
history.forward(); // 前进一页

🍰 类比:

history 就是“奶茶回忆录”📖。

  • back() 回到上一杯喝过的奶茶;
  • forward() 去尝下一杯。

🍑 第 25 页:== 与 === 的区别

💡 区别一览表:

运算符 含义 特点
== 相等(值相等) 自动类型转换
=== 全等(值与类型都相等) 不会转换类型

🧠 举例讲透:

1 == '1'   // true(字符串自动转数字)
1 === '1'  // false(类型不同)
null == undefined // true
null === undefined // false

🍬 类比:

== 就像“模糊匹配”——你说“草莓”,我也当成“草莓奶茶”; === 是“严格匹配”——只有“草莓奶茶”才算对,不含糊。


☕ 记忆口诀:

“双等宽松聊恋爱,三等谨慎签合同。”

(意思是: == 可能因为类型转换“将就”一下, === 必须类型、数值都一模一样,像签合同一样严谨。)


🎀 小笨蛋也能懂的总结(第21~25页)

模块 核心功能 奶茶店类比
window 浏览器全局对象 总控台
location 地址栏 门牌号
navigator 浏览器信息 店员身份证
screen 屏幕信息 橱窗尺寸
history 浏览记录 奶茶回忆录
== vs === 比较规则 模糊恋爱 vs 严谨婚姻

🌟 一句话记忆:

“window 全局掌控,location 换店导航; navigator 报身份,screen 看橱窗,history 查奶茶过往。 双等将就爱,三等要真心。” ❤️

🧋第 26 页:== 和 === 再复习

🍰 图里的核心区分

  • ==宽松比较,会自动转换类型。
  • ===严格比较,类型和值都必须相同。
  • != / !==:对应“不相等”和“非全等”。

📖 举个例子:

1 == '1'    // true  字符串自动转数字
1 === '1'   // false 类型不同

🍬 类比:

== 就像店员说:“草莓味?草莓奶茶也行~” === 就像严格审查:“必须草莓奶茶加波霸,否则不算!”


🌸第 27 页:5.1 关于类型转换

🧮 JS 在比较时会自动“帮你转换类型”

console.log(1 == true);  // true
console.log(0 == false); // true
console.log('' == false); // true

💡 JS 的脑回路是这样的:

  • true → 转成数字 1
  • false → 转成数字 0
  • 空字符串 → 也是 0

所以这些结果都是真的。

🍹 类比:

顾客点单时,服务员会自动脑补: “他说‘一杯’,那肯定是数量 1;他说‘不要’,就是 0;啥都不说就是空(0)。”


💥 常见坑点示例

console.log([] == false); // true
console.log([] == ![]);   // true

解释:

  • [] 转换成数字 → 0;
  • ![] 是 false;
  • 0 == 0 → true。

🍵 类比:

空杯子和没杯子,在店员眼里都“算空”~ 结果就被认成一样。


🍑第 28 页:5.2 空字符串转换 & 5.3 对象转换

空字符串的比较

console.log('' == 0); // true
console.log('' === 0); // false

💬 '' 会自动变成数字 0 → 所以 == 觉得一样,=== 说类型不同。

🍰 类比:

“0 元优惠券”和“空白优惠券”,服务员模糊地认为一样(==); 但经理查账会说:不一样的单据(===)。


对象比较

let a = {};
let b = {};
console.log(a == b); // false
console.log(a === b); // false

💬 解释: 对象比较时,看的是引用地址,不是内容。 a、b 两个对象在不同的内存位置。

🍵 类比:

你做了两杯一模一样的奶茶,但放在不同桌上, 看起来一样,其实“杯号”不同。


🌷第 29 页:5.4 小结 & 6.1 typeof

💡 小结口诀:

“值类型比值,引用比址; == 会转换,=== 不客气。”


🧭 6.1 typeof 操作符

用来判断 数据类型

typeof 123           // 'number'
typeof '奶茶'        // 'string'
typeof true          // 'boolean'
typeof undefined     // 'undefined'
typeof Symbol()      // 'symbol'
typeof null          // ❗ 'object'(历史遗留 bug)
typeof {}            // 'object'
typeof []            // 'object'
typeof function(){}  // 'function'

🍬 总结成一句话:

typeof 可以看出“是什么口味”的奶茶, 但遇到空杯(null)会认错成“奶茶杯(object)”😂


📦 小笨蛋速记表:

typeof 结果 小故事
123 number 数字奶茶编号
'奶茶' string 奶茶标签
true boolean 点单开关
undefined undefined 还没做好的奶茶
null object ❗ 空杯(被误认成对象)
[] object 奶茶组合盒
function function 奶茶制作机

🌼第 30 页:6.2 instanceof

判断一个对象是否是某个构造函数的实例。

let arr = [];
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true

💬 原理: instanceof 是沿着原型链往上查,看是否能找到对应的构造函数。

🍰 类比:

店员小明是“饮品部(Array)”的员工, 但饮品部又属于“总店(Object)”。 所以:

  • 是 Array 的实例 ✅
  • 也是 Object 的一员 ✅

☕ typeof vs instanceof 区别总结:

比较方式 用于 返回 特点
typeof 基本类型 字符串 判断简单类型(除了 null)
instanceof 引用类型 布尔值 判断对象属于哪种“家族”

🍹 口诀记忆:

“typeof 查口味,instanceof 查家族。”


🎀 小可爱总结(第26~30页)

知识点 一句话记忆 奶茶店例子
== / === 宽松恋爱 vs 严谨婚姻 “差不多”也算 vs 必须完全一致
类型转换 JS 自动脑补 空杯 ≈ 没杯
typeof 看口味 判断奶茶类型
instanceof 查家族 判断是饮品部还是甜品部

💡 超级总结口诀

“双等将就爱,三等要真心; typeof 看奶茶口味,instanceof 查员工部门; 值比值,址比址,空杯容易出事。”

🧋 第 31 页:instanceof 实战讲解

function Car() {}
let myCar = new Car();

console.log(myCar instanceof Car);     // true
console.log(myCar instanceof Object);  // true

💡解释:

  • myCar 是用 Car 这个构造函数造出来的。
  • 所以 myCarCar 的实例;
  • 而所有对象最终都继承自 Object,因此它也是 Object 的实例。

🍰类比:

“Car” 是【奶茶制作机的蓝图】; myCar 是【做好的那台机器】; 所有机器都属于【设备部(Object)】。


🪜 instanceof 的工作原理图

它会从对象的原型链一路往上查:

对象的 __proto__ → 构造函数的 prototype → 再往上找直到 Object.prototype。

📦类比:

就像从“店员”往上查找: 店员 → 店长 → 区经理 → 总公司。 如果在任一层找到了“身份证模板”,那就返回 true。


🌸 第 32 页:6.3 复制(拷贝)

有时候我们想“复制一个对象”,但是对象存在“引用传递”问题:

let obj1 = { name: '小可爱' };
let obj2 = obj1;
obj2.name = '小笨蛋';
console.log(obj1.name); // 小笨蛋

💬解释: obj2 并不是新的对象,只是复制了引用地址。 改一个,另一个也变。

🍹类比:

你把“奶茶配方表”的链接发给别人,他改了糖量,你的配方也跟着变~😱


✨浅拷贝与深拷贝

拷贝方式 含义 常见方法
浅拷贝 只复制第一层 Object.assign()...展开符
深拷贝 连内部对象也复制 JSON.parse(JSON.stringify(obj))

🍵类比:

浅拷贝就像“复制菜单目录”; 深拷贝是“连奶茶配方都重新抄一份”。


🧠 第 33 页:7. JavaScript 原型与原型链

🧩 原型(Prototype)是啥?

每个函数在创建时,JS 都会自动给它加一个属性 prototype。 这个属性指向一个对象,这个对象里保存着“共享的属性和方法”。

举例:

function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  console.log('Hi,我是 ' + this.name);
};

let p1 = new Person('小可爱');
p1.sayHi(); // 输出:Hi,我是 小可爱

🍬解释:

  • Person 是“模板”(构造函数);
  • prototype 是模板的“说明书”;
  • p1 是根据模板造出的“员工”;
  • 员工会自动带上说明书里的技能。

🍓类比:

你是“奶茶培训学校校长(Person)”; 每个毕业生(p1、p2)都学会相同技能 sayHi(), 因为都读了一样的“培训手册(prototype)”。


🧋 第 34 页:原型链(Prototype Chain)

console.log(p1.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

💡解释:

JS 会一层层往上查找属性,直到最顶层 Object.prototype

🍵类比:

小可爱(p1) → 奶茶学院(Person) → JS 总部(Object) → 没上级了(null)。


🧭 原型链查找机制

当你访问 p1.sayHi 时,JS 会:

  1. 先查 p1 自己有没有;
  2. 没有就查它的原型;
  3. 再没有就一直往上找,直到顶层。

🍹类比:

员工不会调奶茶机?那去问店长; 店长不会?问区域经理; 再不会?问总部; 总部都没教?那就“undefined”了 😆。


🌷 第 35 页:可视化理解原型链

代码示例:

function Person() {}
let person1 = new Person();

console.log(person1.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true

🧩解释:

  • 构造函数的 prototype 有个 constructor 属性指回自己;
  • 实例的 __proto__ 指向构造函数的 prototype。

🍰类比:

奶茶培训学院(Person)有一本教材(prototype), 教材上注明“作者:Person”; 学生(person1)的说明书(proto)引用了这本教材。


💡 小笨蛋超记口诀(第31~35页)

概念 一句话总结 奶茶店类比
instanceof 判断家族关系 员工属于哪个部门
浅拷贝 复制目录 只抄菜单标题
深拷贝 完整复制 连奶茶配方都重写
prototype 模板说明书 培训手册
原型链 一层层往上找 员工 → 店长 → 总公司

🎀 超萌终极口诀

“instanceof 查家谱, 浅深拷贝别混淆; prototype 是培训书, 原型链上层层找。”

🧋第 36 页:原型链的全图理解

图上是一张“大型 JS 家族树”,主要说明:

  • 所有函数(包括构造函数)都是 Function 的实例。
  • 所有对象的最终祖宗都是 Object.prototype
  • 顶端是 null —— 没有再往上的人了。

🧱 你可以这样理解层级:

角色 说明 类比
Person 构造函数(奶茶学院) 教你做奶茶
person 实例对象(学员) 毕业的奶茶师
Person.prototype 原型(教材) 学员共享技能
Object.prototype JS 的终极祖宗 总部培训手册

📘 示例代码:

function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  console.log('你好,我是 ' + this.name);
};

let p1 = new Person('小可爱');
p1.sayHi(); // 输出:你好,我是 小可爱

📖 解释:

  • p1 自己没有 sayHi
  • JS 会顺着原型链去找 → 找到 Person.prototype
  • 如果还没有,就再去 Object.prototype
  • 一直到顶端才停止。

🍓 类比:

奶茶师(p1)不知道做某种新口味 → 去查学院教材(prototype); 教材里没写?再问总公司(Object)!


🌸第 37 页:原型链的关系验证

console.log(p1.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

📚 结论:

  • 原型链是一个“逐级上溯”的结构;
  • null 表示没有再往上的祖先。

🍵 类比:

小可爱(p1) → 奶茶学院(Person) → 总部(Object) → 天花板(null)。


🪜 小笨蛋记忆口诀:

“对象有原型,原型也有原型, 原型尽头是 null,总公司没人管。”


🍰第 38 页:作用域链的理解

JS 中作用域是“变量能被访问到的范围”。

📖 分三类:

  1. 全局作用域:所有地方都能访问。
  2. 函数作用域:只在函数内部有效。
  3. 块级作用域letconst{} 内有效。

🍹 类比:

  • 全局作用域:奶茶总部公告,所有分店都能看;
  • 函数作用域:某家分店的员工守则;
  • 块级作用域:当天的临时活动通知。

📘 示例

let name = '总部'; // 全局

function branch() {
  let name = '奶茶店A'; // 局部
  console.log(name);
}
branch();  // 奶茶店A
console.log(name); // 总部

🍬 解释: 内部作用域可以访问外部,但外部不能反过来访问内部。


☕第 39 页:作用域链机制

当 JS 访问变量时,会:

  1. 先在当前作用域找;
  2. 找不到就往上一级;
  3. 一直查到全局;
  4. 如果都没有,就报错。
let a = '总部';
function shop() {
  let a = '奶茶店';
  function employee() {
    console.log(a);
  }
  employee();
}
shop(); // 奶茶店

🍓 类比:

员工问“今天喝什么口味?” 自己分店有菜单 → 就用分店的; 没有才去问总部。


⚙️ 块级作用域

if (true) {
  let x = 10;
}
console.log(x); // ❌ 报错

💬 解释: let 定义的变量只在 {} 内可用。

🍵 类比:

活动期间的“买一送一券”, 离开当天活动就作废。


🧠第 40 页:作用域综合演示

var x = 10;
function foo() {
  console.log(x);
  var x = 20;
}
foo(); // undefined

💬 解释:

  • JS 在执行前会“提升变量声明(hoisting)”,
  • 所以 var x 在函数里被提前声明但未赋值;
  • 输出时它是 undefined

🍰 类比:

店员问:“今天有草莓吗?” 仓库(函数)先登记了草莓这件事(声明)但还没送货(赋值), 所以此时“库存为空”。


🎀 小笨蛋终极总结(第36~40页)

概念 一句话记忆 奶茶店比喻
原型链 层层继承 员工 → 店长 → 总部
prototype 共用技能本 培训教材
作用域 变量可见范围 告示适用范围
作用域链 一层层查找变量 从店员问到总部
变量提升 先登记后发货 草莓先记账还没送到

🍓 超萌口诀收尾!

“原型链上查家谱,作用域里找口味; 局部外部层层叠,未赋先提是空杯。”

🧋第 41 页:作用域链回顾

来看这段代码👇

var a = 2;
function foo() {
  var b = 3;
  function bar() {
    var c = 4;
    console.log(a + b + c);
  }
  bar();
}
foo(); // 输出 9

💬 解释: JS 查找变量时是“从内往外”找的。 bar() → 先看自己有没有 abc。 找不到 a → 去 foo() 外层 → 再找全局。

🍵 类比:

店员(bar)要做奶茶:

  • 先看自己柜台有没有材料;
  • 没有就去门店仓库(foo);
  • 再不行就打电话总部(全局)。

🌱 图示说明

图上展示:

  • 每个函数都有自己的“作用域气泡”;
  • 外层函数就是“上一级仓库”;
  • 它们连接起来形成 作用域链(Scope Chain)

🍬 口诀:

“函数套函数,作用域像洋葱; 从内往外剥,总能找到葱。”


🌸第 42 页:作用域链图解(办公楼模型)

图里是一个“高楼层结构”:

  • 每层代表一个作用域;
  • 顶楼是全局作用域;
  • 内层函数越深 → 楼层越高。

📖 小笨蛋记法:

想象变量是一杯奶茶 🍹 你在 7 楼想找奶茶,先看自己桌上有没有; 没有就去 6 楼 → 5 楼 → 一直找到 1 楼总部。


📘 代码解释

var x = 10;
function outer() {
  var y = 20;
  function inner() {
    var z = 30;
    console.log(x + y + z);
  }
  inner();
}
outer(); // 输出 60

📍执行顺序:

  1. inner 自己的作用域(z = 30)
  2. outer 的作用域(y = 20)
  3. 全局作用域(x = 10)

🍰 类比:

店员(inner)要做“特调奶茶”, 自己柜台有糖浆(z), 从楼下仓库拿茶叶(y), 最后总部提供牛奶(x)。


☀️第 43 页:作用域链小结

级别 名称 范围
1 全局作用域 所有函数外部
2 函数作用域 每个函数独立
3 块级作用域 {} 内生效(let/const)

💡 JS 在“定义函数时”就决定好作用域,而不是运行时才决定。 👉 这就是 “词法作用域(Lexical Scope)”

🍹 类比:

店员入职那天,公司就决定了他能进哪些仓库。 不是等他去拿材料时才临时决定的。


🧠第 44 页:this 对象(灵魂考点登场)

this 就是当前执行环境下的对象, 取决于“函数被谁调用”,而不是定义时。

💻 基础例子:

var name = '总部';
function sayHi() {
  console.log(this.name);
}

const shop = {
  name: '奶茶店A',
  sayHi: sayHi
};

sayHi();       // 输出:总部
shop.sayHi();  // 输出:奶茶店A

📖 解释:

  • 第一行 sayHi() 是由全局环境调用 → this 指向 window
  • 第二行 shop.sayHi() 是由对象调用 → this 指向 shop

🍓 类比:

同一句话 “欢迎光临~”

  • 在总部喊:代表总部欢迎;
  • 在分店喊:代表分店欢迎。 所以 this 取决于“谁喊的”!

🌷第 45 页:this 指向的 4 条核心规则

💡 1. 默认绑定(普通函数调用)

function show() {
  console.log(this);
}
show(); // 浏览器中输出 window

👉 没有指定对象,this 默认是全局对象(浏览器里是 window)。

🍹 类比:

没写发件人名字,系统默认认为是“总部”发的。


💡 2. 隐式绑定(对象调用)

const tea = {
  name: '奶茶A',
  getName() {
    console.log(this.name);
  }
};
tea.getName(); // 奶茶A

👉 调用者是 tea 对象 → this = tea。

🍰 类比:

店员(this)属于哪个分店,就说那家店的口味。


💡 3. 显式绑定(call/apply/bind)

function greet() {
  console.log('Hi, ' + this.name);
}

const shop = { name: '奶茶店B' };
greet.call(shop); // Hi, 奶茶店B

💬 .call() 手动指定 this。 🍬 类比:

像你强制让总部的广播系统播放“奶茶店B”的口号。


💡 4. new 绑定(构造函数)

function Person(name) {
  this.name = name;
}
const p = new Person('小可爱');
console.log(p.name); // 小可爱

👉 使用 new 时,this 会绑定到“新建的对象”上。

🍵 类比:

“new” 就像开了一个新分店,新店就成了 this!


💫 小笨蛋专属总结(第41~45页)

概念 一句话总结 奶茶店类比
作用域 变量能访问的区域 奶茶仓库范围
作用域链 多层作用域查找 从分店查到总部
词法作用域 定义时确定范围 入职时发的权限卡
this 谁调用指向谁 谁喊“欢迎光临”就代表谁
call/apply/bind 手动指定 this 强制让总部喊出分店的口号
new 绑定 构造函数绑定 开新分店,新 this 诞生!

🎀 终极口诀

“作用域链像仓库,层层往上找; this 看谁喊话,谁喊谁代表。 new 开新店,call 改台词, JS 虽抽象,其实奶茶味十足~🍹”

🧋第 46 页:this 的四种绑定方式复习

这几页主要讲的就是—— this 到底是谁?取决于“谁喊话”!

你可以想成:this 是函数执行时的“身份牌” 。 谁在调用函数,this 就代表谁!


🍰 9.2.2 隐式丢失(this 丢了)

function show() {
  console.log(this.name);
}

var name = '总部';
const obj = { name: '分店A', show };

obj.show();      // 分店A
const f = obj.show;
f();             // 总部(this 丢失)

💬解释:

  • obj.show() → 调用者是 obj,所以 this = obj;
  • f() → 函数单独被调用,就回到默认绑定 → this = window。

🍹类比:

奶茶店员工(show)平时在分店上班(this = 分店A)。 但有一天他辞职在家做兼职(脱离 obj),那就成了自由人(this = window)。


🌸第 47 页:隐式丢失的陷阱

var name = '总部';
const shop = {
  name: '奶茶店B',
  getName() {
    console.log(this.name);
  }
};

setTimeout(shop.getName, 1000); // 输出:总部

💡原因: setTimeout 里的回调函数是“普通调用”, 不是由 shop 直接调用的,this 丢了!

🍬类比:

总部让你“1秒后喊出你的名字”, 结果你忘了自己在哪家分店工作,就喊“总部!”😂

✅ 解决办法:

setTimeout(() => shop.getName(), 1000);

👉 箭头函数不改变 this(后面讲)


☕第 48 页:new 绑定(构造函数)

function Tea(name) {
  this.name = name;
}
let cup = new Tea('草莓奶茶');
console.log(cup.name); // 草莓奶茶

💬解释:

  • 使用 new 时,JS 会自动创建一个新对象;
  • 并把 this 绑定到这个新对象上;
  • 最后自动返回它。

🍵类比:

“new” 就像开了一家新分店。 新分店会挂着自己的门牌(this),独立存在。


🧠第 49 页:箭头函数中的 this

const shop = {
  name: '分店C',
  show: () => {
    console.log(this.name);
  }
};
shop.show(); // undefined(不是分店C!)

💬解释: 箭头函数不会创建自己的 this, 它的 this 是“外层作用域”的 this。

🍬类比:

箭头函数是“实习生”——没有自己的工牌, 它用的是外层经理的工牌。 如果外层是总部环境 → 它拿的是总部的工牌。


📘 正确写法

const shop = {
  name: '分店C',
  show() {
    console.log(this.name);
  }
};
shop.show(); // 分店C ✅

🍓第 50 页:事件绑定中的 this

document.querySelector('button').addEventListener('click', function() {
  console.log(this); // <button> 元素本身
});

💡解释:

  • 普通函数绑定事件 → this 指向触发事件的 DOM 元素。

🍵类比:

顾客(click)点了哪一杯奶茶(button), this 就代表那杯奶茶本身。


⚡箭头函数事件绑定

document.querySelector('button').addEventListener('click', () => {
  console.log(this); // window
});

💬箭头函数没有自己的 this,所以 this 来自外层(通常是 window)。

🍹类比:

实习生负责点单(箭头函数), 但他没有“杯子编号”概念, 所以直接指到总部去了(window)。

🧋第 51 页:this 四种绑定规则复习

上一章(第 45~50 页)讲了基础,现在这页讲的是:

✅ 默认绑定 ✅ 隐式绑定 ✅ 显式绑定(call/apply/bind) ✅ new 绑定


🧱 1️⃣ 默认绑定(普通函数)

function foo() {
  console.log(this.a);
}
var a = 2;
foo(); // 输出 2(浏览器中是 window.a)

💬 没有任何修饰的函数调用 → this 默认指向全局对象。

🍵 类比:

如果没人明确点名是谁喊话,就默认认为是“总部”发的通知。


🧱 2️⃣ 隐式绑定(对象调用)

function foo() {
  console.log(this.a);
}
var obj = { a: 2, foo: foo };
obj.foo(); // 输出 2

💬 谁点的 .foo(),this 就指向谁。 🍰 类比:

“奶茶店 A” 员工喊“我最棒” → 那代表奶茶店 A,不是总部。


🧱 3️⃣ 显式绑定(call/apply/bind)

function foo() {
  console.log(this.a);
}
var obj = { a: 2 };
foo.call(obj); // 输出 2

💬 call() / apply() / bind() 可以强制修改 this。 🍓 类比:

总部(window)拿着麦克风说:“请奶茶店 A 来喊这句话!” → 强行切换发言人。


🧱 4️⃣ new 绑定

function Foo(a) {
  this.a = a;
}
var bar = new Foo(2);
console.log(bar.a); // 2

💬 使用 new 创建新对象时:

  1. 创建一个新空对象;
  2. 把 this 绑定到这个新对象;
  3. 自动返回它。

🍹 类比:

“new” 就像开了一家新的奶茶分店 🍵, 新店(this)有自己独立的库存和菜单。


🌸第 52 页:箭头函数中的 this

function outer() {
  return () => {
    console.log(this.a);
  };
}
var obj = { a: 2 };
var foo = outer.call(obj);
foo(); // 输出 2

💬 箭头函数不会自己绑定 this,它会继承外层的 this。

🍰 类比:

箭头函数像“学徒”, 没有自己的工作证(this),只能用师傅的。


📘 坑点对比

var a = 10;
var obj = {
  a: 20,
  fn: () => {
    console.log(this.a);
  }
};
obj.fn(); // 输出 10

💬 因为箭头函数的 this 是定义时绑定,不是运行时! 所以这里 this 是 window,而不是 obj。

🍵 类比:

学徒(箭头函数)不是听当前店长的,而是永远听“总部”的命令。😂


☀️第 53 页:new 与普通函数的区别

function Foo(name) {
  this.name = name;
}
let a = Foo('奶茶A');      // 普通调用
let b = new Foo('奶茶B');  // 构造调用

💬 区别:

调用方式 this 指向 返回值
普通函数调用 全局对象(window) undefined
new 调用 新建对象 这个对象本身

🍓 类比:

普通调用:总部代工,产品归总部。 new 调用:开新分店,产品归新店。


🧠第 54 页:new 操作符做了什么

function Person(name, age) {
  this.name = name;
  this.age = age;
}
const person1 = new Person('Tom', 20);

💡 new 操作符做的 4 件事:

1️⃣ 创建一个新空对象 {}; 2️⃣ 把新对象的原型指向构造函数的 prototype; 3️⃣ 把构造函数的 this 绑定到新对象上; 4️⃣ 如果函数没有返回其他对象,则自动返回这个新对象。

🍵 类比:

“new” = 新开奶茶店:

  1. 建新店(空对象)
  2. 店长培训(绑定 prototype)
  3. 店内设置菜单、装修(赋值 this)
  4. 新店开始营业(返回实例)

📘 代码演示

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.sayHi = function() {
  console.log('Hi, 我是 ' + this.name);
};

const p1 = new Person('小可爱', 18);
p1.sayHi(); // Hi, 我是 小可爱

🍰 类比:

新分店(p1)开业 → 会使用总部的“招呼语模板(sayHi)”。


🌷第 55 页:可视化理解 new 流程

图中展示 new 的四个阶段(总结成奶茶故事版)👇:

阶段 JS 行为 奶茶类比
创建新对象 新奶茶店开张
连接原型 给店铺挂上总部培训手册
绑定 this 店长接管店铺
返回对象 店铺正式营业 🎉

🎀 小可爱总结(第51~55页)

概念 一句话记忆 奶茶店类比
this 默认绑定 谁都没喊就是总部 默认 window
this 隐式绑定 谁点的谁说话 obj.fn()
this 显式绑定 强制切换发言人 call/apply
箭头函数 没自己 this,继承外层 学徒听师傅的
new 操作符 新建对象并绑定 新开奶茶店

💡 终极口诀

“谁喊谁代表,call 改发言人; new 开新店铺,箭头跟师傅干; 四步造分店,prototype 挂菜单。”

🧋第 56 页:手写 new 操作符(模拟实现)

💻 new 是什么?

当我们执行:

function Person(name, age) {
  this.name = name;
  this.age = age;
}
let p = new Person('小可爱', 18);

JS 内部其实做了 4 步事👇:

1️⃣ 创建一个空对象 {} 2️⃣ 把这个对象的原型指向 Person.prototype 3️⃣ 绑定 this 到这个新对象上,并执行构造函数 4️⃣ 如果构造函数没有返回对象,则返回这个新建对象

🍵 类比:

“new” 就像 新开一家奶茶分店: ① 先建店面(空对象) ② 给它挂上总部的培训手册(prototype) ③ 店长(this)负责装修和配置菜单 ④ 没意外就开门营业(返回对象)


🧱 手写 new 代码实现

function myNew(fn, ...args) {
  const obj = {};                     // ① 创建空对象
  obj.__proto__ = fn.prototype;       // ② 链接原型
  const result = fn.apply(obj, args); // ③ 执行构造函数并绑定 this
  return result instanceof Object ? result : obj; // ④ 返回对象
}

🍰 类比解释:

  • obj 是“新开的奶茶店”;
  • obj.__proto__ = fn.prototype 让它能用总部的配方书;
  • fn.apply(obj, args) 店长装修菜单;
  • 最后返回新开的奶茶店实例。

🌸第 57 页:call / apply / bind 的作用与区别

方法 是否立即执行 参数传递方式 作用
call ✅ 立即执行 参数一个个传 改变 this
apply ✅ 立即执行 参数数组传 改变 this
bind ❌ 不立即执行 参数一个个传 返回新函数

🍬 类比:

  • call:立刻喊总部广播宣传。📢
  • apply:让总部一次性播放多个分店的公告。📜
  • bind:先录好音,等需要时再播放。🎙️

🧱 例子演示(第57页)

function greet(gift) {
  console.log(`${this.name} 收到 ${gift}`);
}

const person = { name: '小可爱' };

greet.call(person, '草莓奶茶'); // 小可爱 收到 草莓奶茶
greet.apply(person, ['波霸奶茶']); // 小可爱 收到 波霸奶茶

const newFn = greet.bind(person, '珍珠奶茶');
newFn(); // 小可爱 收到 珍珠奶茶

🍵 记忆口诀:

call 是“单点直呼”;apply 是“套餐传递”;bind 是“预约执行”。


☀️第 58 页:手写 call、apply、bind 实现

✨ 手写 call

Function.prototype.myCall = function (context, ...args) {
  context = context || window;
  context.fn = this;       // 暂时把函数放进 context 里
  const result = context.fn(...args);
  delete context.fn;       // 清除函数
  return result;
};

🍹 类比:

你(this)临时借用了别的奶茶店的广播系统(context), 播完广告(函数执行)后再拔掉插头(删除 fn)。


✨ 手写 apply

Function.prototype.myApply = function (context, args) {
  context = context || window;
  context.fn = this;
  const result = args ? context.fn(...args) : context.fn();
  delete context.fn;
  return result;
};

📘 区别:apply 的参数是数组形式传入。


✨ 手写 bind

Function.prototype.myBind = function (context, ...args) {
  const fn = this;
  return function (...innerArgs) {
    return fn.apply(context, [...args, ...innerArgs]);
  };
};

🍰 类比:

你把总部的“广告词”录成语音(返回新函数), 以后店长随时播放(延迟执行)。


🧠第 59 页:call / apply / bind 的区别表(总结页)

方法 是否立即执行 参数格式 返回结果
call ✅ 是 单个参数 结果
apply ✅ 是 数组 结果
bind ❌ 否 单个参数 新函数

📍小笨蛋口诀:

“call 立刻喊,apply 队形喊,bind 等会儿喊。”


🌷第 60 页:总结 + 延伸思考

💡 面试高频问:

“请你实现一个简单的 bind() 函数。”

你只要记得:

  1. bind 不会立刻执行;
  2. 它返回一个新函数;
  3. 新函数执行时 this 指向固定对象;
  4. 还能预置参数。

📘 面试示例:

Function.prototype.simpleBind = function(context, ...args) {
  const fn = this;
  return function(...innerArgs) {
    return fn.apply(context, args.concat(innerArgs));
  };
};

🌈 一句话总结(第56~60页)

知识点 含义 奶茶店比喻
new 操作符 创建对象的过程 新开奶茶分店
call 改变 this,立即执行 总部广播切换发言人
apply 改变 this,数组参数 一次性播放多条公告
bind 返回新函数,延迟执行 录音留待下次播放

🎀 小笨蛋终极口诀

“new 开分店,四步搞定; call、apply、bind 三兄弟, 一个立刻喊,一个成群喊,一个改天喊。🎤”

🧋第 61 页:代码执行机制 & 执行上下文

💡 什么是执行上下文?

JS 在运行代码时,每当进入一个环境(比如全局 / 函数) , 它都会创建一个“执行上下文(Execution Context)”。

执行上下文包含三部分: 1️⃣ 变量对象(Variable Object) — 存储变量、函数声明等; 2️⃣ 作用域链(Scope Chain) — 用于查找变量; 3️⃣ this 指向 — 谁在调用。

🍵 类比:

“执行上下文”就像一个奶茶工作台

  • 变量对象:桌上摆的原料;
  • 作用域链:能借到的外部仓库;
  • this:当班的奶茶师是谁。

📘 示例代码

var a = 10;
function foo() {
  var b = 20;
  function bar() {
    var c = 30;
    console.log(a + b + c);
  }
  bar();
}
foo(); // 输出 60

🍰 解释:

  1. JS 先进入全局上下文(厨房总控台);
  2. 执行到 foo() 时,创建 foo 的上下文(新工作台);
  3. 执行到 bar(),又开一张小桌(bar 的上下文);
  4. 打印完结果后,bar 桌子收掉,回到 foo 桌,再回到全局。

📦 图示:每个函数运行时会“压入执行栈”(像奶茶杯叠起来), 执行完毕就“弹出栈顶”(喝完收掉)。


🌸第 62 页:执行栈(Execution Stack)

所有执行上下文是按照 “先进后出(LIFO)” 的方式存放在执行栈中。

💻 举例:

function first() {
  console.log('🥤 first');
  second();
}
function second() {
  console.log('🧋 second');
}
first();
console.log('🏁 end');

执行顺序:

顺序 事件 栈状态
1 进入全局 [Global]
2 调用 first [Global, first]
3 调用 second [Global, first, second]
4 执行完 second [Global, first]
5 执行完 first [Global]
6 执行完毕 栈空

🍹 类比:

奶茶师做单的顺序—— 第一单(first)点了要调用第二单(second), 做完第二单再回来继续第一单,最后才清空。


☀️第 63 页:执行上下文的生命周期

执行上下文一共经历三个阶段:

1️⃣ 创建阶段(Creation Phase)

  • 创建变量对象(VO)
  • 建立作用域链
  • 确定 this 指向

2️⃣ 执行阶段(Execution Phase)

  • 逐行执行代码,给变量赋值、调用函数等

🍓 类比:

  • 创建阶段:奶茶店“准备原料、工具、菜单”;
  • 执行阶段:开始实际“做奶茶、出单”。

💻 举例

console.log(a); // undefined
var a = 10;
console.log(a); // 10

📖 执行顺序:

  1. 创建阶段:a 被声明但未赋值(默认 undefined);
  2. 执行阶段:给 a 赋值为 10。

🍵 类比:

点单还没开始做的时候,奶茶杯已准备好(声明), 但杯子里还没倒奶茶(赋值) → 所以一开始是空的 undefined。


🧠第 64 页:变量提升与函数提升

JS 在创建阶段会把所有变量声明函数声明提前到顶部。

📘 示例

console.log(a); // undefined
var a = 10;

foo(); // OK ✅
function foo() {
  console.log('函数提升成功');
}

💡解释:

  • 变量声明被提升,但赋值不会;
  • 函数声明整体提升(所以能提前调用)。

🍰 类比:

店长先在白板上写好菜单(声明), 但奶茶还没做(赋值)。 函数像“预制好的奶茶”,随时可以拿来用。


🧱 函数表达式不会被提升

sayHi(); // ❌ 报错
var sayHi = function() {
  console.log('hi~');
};

💬 因为这是“变量声明 + 函数赋值”, 声明会提升,但赋值在执行阶段才发生。

🍹 类比:

你先说“我要雇个员工”,但真正员工还没上班。 所以执行时会找不到人。


🌷第 65 页:执行上下文小结图

💡 三层概念:

名称 含义 奶茶类比
执行上下文 代码运行环境 奶茶工作台
执行栈 上下文的堆叠顺序 点单堆叠系统
生命周期 从准备到完成的过程 从备料到出单

📜 总结口诀:

“先建工作台,再堆栈出单; 声明先到场,赋值慢半拍; 函数提前泡好茶,表达式还在请人。”


💎 小笨蛋专属总结(第61~65页)

概念 一句话记忆 奶茶店比喻
执行上下文 每个函数的“运行环境” 奶茶工作台
执行栈 上下文的执行顺序 奶茶订单堆叠
生命周期 创建→执行→销毁 备料→做单→送出
变量提升 声明提前,赋值靠后 先写菜单,后做奶茶
函数提升 可提前调用 预制奶茶随时取用

🌈 终极口诀

“上下文是厨房台,执行栈是订单排; 提升只是登记名,真正赋值要后来。”

🧋第 66 页:执行上下文更深入理解

🧠 执行上下文栈(Execution Context Stack)

我们之前知道每个函数执行时都会创建一个“执行上下文”。 所有的执行上下文,会按顺序压入一个栈(stack)中,栈底是全局上下文。

function first() {
  console.log('🍓 first');
  second();
}

function second() {
  console.log('🍵 second');
}

first();
console.log('🏁 end');

🧩 执行顺序:

  1. 进入全局上下文(创建 Global Context)
  2. 调用 first() → 入栈
  3. 执行到 second() → 入栈
  4. second() 执行完 → 出栈
  5. first() 执行完 → 出栈
  6. 程序结束 → 栈空

🍵 类比奶茶店:

点单系统像“订单栈”:

  • 先做的奶茶压在下面;
  • 后下单的排在上面;
  • 必须先完成上面的订单才能继续下面的。

💡 图示理解:

📊 图中绿色方框表示不同的执行上下文:

  • Global(全局)永远在最底部;
  • 当前执行的函数上下文在最上面。 当函数结束后,JS 会“弹出”这层上下文。

🍓 小口诀:

“先进后出,上桌先收。” (奶茶要先完成上面的新单,才能轮到旧单。)


🌸第 67 页:执行顺序的可视化理解

💻 示例

console.log('A');
setTimeout(() => console.log('B'), 0);
console.log('C');

输出结果:

A
C
B

💬 为什么 B 最后? 因为:

  1. JS 是单线程执行
  2. setTimeout 的回调会进入任务队列(Event Queue)
  3. 主线程执行完当前任务(A、C)后,再执行队列任务。

🍰 奶茶店类比:

前台点单员(主线程)只能一次接待一位顾客; 叫号机(任务队列)会记下还没轮到的顾客(setTimeout), 前面客人都服务完后,再喊“下一位~!”


☀️第 68 页:事件模型(Event Model)

💡 概念:

事件模型决定了浏览器中事件(比如点击、输入、滚动)是如何被捕获与触发的。

分三种阶段: 1️⃣ 捕获阶段(从外到内) 2️⃣ 目标阶段(事件触发的元素本身) 3️⃣ 冒泡阶段(从内往外)

🍵 类比:

奶茶店有三个层级:

  • 店长(捕获阶段)→ 看全场;
  • 柜台(目标阶段)→ 处理点单;
  • 店员(冒泡阶段)→ 把消息传回管理层。

📘 示例

<div id="outer">
  <button id="inner">点我!</button>
</div>

<script>
  document.getElementById('outer').addEventListener('click', () => {
    console.log('外层 div 被点击');
  }, true); // 捕获阶段

  document.getElementById('inner').addEventListener('click', () => {
    console.log('按钮被点击');
  }, false); // 冒泡阶段
</script>

输出顺序:

外层 div 被点击
按钮被点击

💡 因为 true 表示“捕获阶段”先执行!

🍹 类比:

店长(outer)先看到顾客进门 → 再由店员(inner)服务点单。


🧠第 69 页:事件捕获与冒泡机制

📘 事件流示意图

图上箭头展示三阶段:

🔽 捕获阶段:从 window → document → body → div → button 🔼 冒泡阶段:从 button → div → body → document → window

🍬 类比:

顾客从门口(window)走到收银台(button)点单, 然后信息再层层传回总部(window)。


📘 示例 2

document.body.addEventListener('click', () => console.log('Body 捕获'), true);
document.body.addEventListener('click', () => console.log('Body 冒泡'), false);
document.getElementById('btn').addEventListener('click', () => console.log('Button 被点击'));

点击按钮:

Body 捕获
Button 被点击
Body 冒泡

💡 “捕获先走,冒泡后到”。


🌷第 70 页:事件对象(Event Object)

每个事件都有一个 event 对象,里面记录了各种信息:

属性 作用
target 触发事件的元素
currentTarget 正在处理事件的元素
type 事件类型(如 click)
stopPropagation() 阻止事件冒泡
preventDefault() 阻止默认行为(比如表单提交)

🍵 类比:

event 是“点单表单”:

  • target:哪位顾客点的;
  • currentTarget:哪个店员在处理;
  • stopPropagation:别让消息传上去;
  • preventDefault:比如顾客点了饮品但不想付款按钮生效。

📘 示例

document.getElementById('btn').addEventListener('click', (e) => {
  e.preventDefault();
  e.stopPropagation();
  console.log('按钮点击但阻止冒泡与默认行为');
});

💬 点击按钮后:

  • 不会触发父级 div 的点击事件;
  • 不会执行表单默认提交。

🍰 类比:

顾客点了奶茶但告诉店员:“别通知经理,也别立刻出单。” — 这就是 stopPropagation + preventDefault 的效果!


🎀 小可爱总结(第66~70页)

概念 含义 奶茶店比喻
执行栈 函数调用顺序 奶茶订单堆叠
任务队列 异步任务排队 顾客叫号系统
捕获阶段 从外到内传递事件 店长观察顾客
冒泡阶段 从内到外回传事件 店员报告经理
event 对象 事件详情表 顾客点单信息表

🌈 终极口诀

“执行栈像订单叠,事件流像客人走; 捕获先下楼,冒泡再上楼; 阻止冒泡别传递,防止默认先暂停。”

🧋第 71 页:事件绑定与管理

💡 什么是事件绑定?

事件绑定就是“当某个元素被触发时执行一个函数”。 比如点击按钮、鼠标移入、键盘按下等。


📘 示例

<button id="btn">点我喝奶茶</button>
<script>
  const btn = document.getElementById('btn');
  btn.addEventListener('click', function() {
    alert('奶茶已下单!');
  });
</script>

🍵 通俗解释:

就像顾客按下点单按钮,系统自动执行“下单函数”。 addEventListener 就是告诉系统:“这个按钮被点时,请执行这个操作。”


✨ addEventListener 的三个参数

element.addEventListener(type, listener, useCapture)
参数 含义 举例
type 事件类型 'click', 'keyup', 'mouseover'
listener 触发时执行的函数 function() {...}
useCapture 是否在捕获阶段执行 true / false

🍰 类比:

这就像奶茶店给按钮“绑定服务员”:

  • type:哪种操作触发;
  • listener:服务员做的事;
  • useCapture:是“进门时”服务(捕获)还是“出门时”处理(冒泡)。

🌸第 72 页:事件解绑与管理优化

💡 解绑事件 — removeEventListener

function sayHi() {
  console.log('欢迎光临小可爱的奶茶店~');
}
btn.addEventListener('click', sayHi);
btn.removeEventListener('click', sayHi);

🍬 注意:

  • 必须传入同一个函数引用
  • 匿名函数是无法解绑的。

🍹 类比:

如果服务员是“实名登记”的(命名函数),可以取消排班; 如果是“临时兼职匿名的”,那就找不到人取消啦~


💻 常见写法陷阱

btn.addEventListener('click', function() {
  console.log('hi');
});
btn.removeEventListener('click', function() {
  console.log('hi');
}); // ❌ 无效!

💬 因为这两个函数虽然长得一样,但在内存中是不同地址


☀️第 73 页:事件管理最佳实践

1️⃣ 使用命名函数 方便解绑与维护。

2️⃣ 用事件委托(Event Delegation) 当有很多元素时,不要每个都绑定事件,而是把监听交给它们的父级。

🍓 类比:

一百位顾客排队点单,你不可能每人配一个店员, 所以让前台一个人监听所有顾客的“点击事件” → 这就是事件委托!


🧠第 74 页:事件委托(Event Delegation)

💡 概念

事件委托是指:

利用事件冒泡机制,把事件绑定在父级元素上, 通过判断事件目标(event.target)来处理子元素的行为。


📘 示例

<ul id="menu">
  <li>珍珠奶茶</li>
  <li>波霸奶茶</li>
  <li>草莓奶茶</li>
</ul>

<script>
  const menu = document.getElementById('menu');
  menu.addEventListener('click', function(e) {
    if (e.target.tagName === 'LI') {
      alert('你点了:' + e.target.innerText);
    }
  });
</script>

🍵 输出:

点击“波霸奶茶” → 弹出 “你点了:波霸奶茶”

💬 解释:

  • 事件监听绑在 ul 上;
  • 当点击 li 时,事件会冒泡到 ul
  • 通过 e.target 判断具体点击了哪个 li

🍰 类比:

顾客在不同的收银台点单, 店长(ul)只需监听“有人点单”这件事, 然后看看是哪种奶茶(e.target.innerText)。


🌷第 75 页:事件委托的优点与应用场景

✅ 优点

1️⃣ 提高性能(减少事件绑定数量); 2️⃣ 动态元素也能自动响应(新增的 li 无需重新绑定); 3️⃣ 管理方便(只需处理父级事件)。

🍹 类比:

店长只需要管理“点单区”整体, 不管今天多了几个新饮品窗口,都能自动响应。


💻 实际应用场景

例如电商商品列表、评论区点赞、表格点击行等:

<table id="orderTable">
  <tr><td>珍珠奶茶</td></tr>
  <tr><td>波霸奶茶</td></tr>
  <tr><td>草莓奶茶</td></tr>
</table>

<script>
  const table = document.getElementById('orderTable');
  table.addEventListener('click', function(e) {
    if (e.target.tagName === 'TD') {
      console.log('点击了:' + e.target.innerText);
    }
  });
</script>

🍓 这样无论表格多少行、动态加载多少商品, 都不需要重新绑事件~✨


🎀 小可爱总结(第71~75页)

知识点 一句话记忆 奶茶店类比
addEventListener 给元素“登记动作” 按按钮下单
removeEventListener 取消登记 取消员工排班
匿名函数解绑失败 不同函数地址 找不到临时工
事件委托 父级代理处理事件 店长统一管理点单
e.target 事件真正触发的元素 点单的那位顾客

💎 超萌口诀:

“绑定事件要实名,解绑要同名; 委托省心又高效,店长统一管点击~🍹”

🧋第 76 页:事件委托进阶 + 动态绑定

前半页在讲事件委托的实战延伸,我们快速复习一下:

📘 示例:动态添加的元素依然能响应事件

<ul id="menu">
  <li>奶茶A</li>
  <li>奶茶B</li>
</ul>

<script>
  const menu = document.getElementById('menu');
  menu.addEventListener('click', function(e) {
    if (e.target.tagName === 'LI') {
      console.log('你点了 ' + e.target.innerText);
    }
  });

  // 动态添加
  const newItem = document.createElement('li');
  newItem.innerText = '奶茶C';
  menu.appendChild(newItem);
</script>

💬 结果:

即使 “奶茶C” 是后来添加的,也能被点击响应!

🍵 原因: 事件绑定在 ul 上,而不是每个 li。 事件冒泡时,ul 能捕捉到点击,并判断是哪杯奶茶被点。

🍰 类比:

店长只管“有人点单”这件事,不管是哪杯奶茶。 新品奶茶上架(新增 li)也能自动被处理。


🌸第 77 页:闭包的概念登场!

💡 什么是闭包(Closure)?

闭包就是:

函数可以“记住”并访问它被创建时的作用域中的变量。

即使这个函数在外部执行,它依然能访问那个作用域的变量!


📘 示例 1

function makeTea() {
  let flavor = '草莓奶茶';
  return function() {
    console.log('顾客点的口味是:' + flavor);
  };
}
const order = makeTea();
order(); // 顾客点的口味是:草莓奶茶

🍬 解释:

  • makeTea 执行完后,本该销毁内部变量;
  • 但返回的函数“引用”了 flavor
  • JS 为了让它还能用 → 把 flavor 留下;
  • 这就叫 闭包

🍹 类比:

顾客点单后离开(函数执行完), 但奶茶师(内部函数)仍记得顾客的口味。 这张“点单纸条”就是闭包!📒


☀️第 78 页:闭包的实际应用场景

闭包看似神秘,其实常用于👇:

✅ 1. 数据持久化(记忆功能)

function counter() {
  let count = 0;
  return function() {
    count++;
    console.log('当前奶茶订单号:' + count);
  };
}
const orderCounter = counter();
orderCounter(); // 当前奶茶订单号:1
orderCounter(); // 当前奶茶订单号:2

💬 闭包帮我们“记住”了上一次的值。

🍰 类比:

每接一单,点单机会自动加一号, 虽然顾客不同,但机器能记住上次的编号。


✅ 2. 模拟私有变量(隐藏信息)

function createTeaMachine() {
  let secret = '配方:波霸 + 草莓酱';
  return {
    show: function() {
      console.log(secret);
    }
  };
}
const machine = createTeaMachine();
machine.show(); // 输出:配方:波霸 + 草莓酱

🍬 类比:

顾客只能通过按钮(show 方法)获取配方信息, 却不能直接打开厨房偷看“secret”。

这就是 闭包实现封装 的典型应用。


🧠第 79 页:闭包与循环(经典坑题)

面试高频问题:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1000);
}

结果:

3
3
3

💬 为什么?

  • var 没有块级作用域;
  • 当定时器执行时,循环已结束;
  • i 变成了 3。

🍵 类比:

你排了 3 个顾客的单,但奶茶师只留了一张总单, 等要做时发现全都写成“第3号顾客”😅。


✅ 解决方法 1:用 let

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1000);
}

结果:

0
1
2

🍓 因为 let 为每次循环都创建了一个独立作用域!


✅ 解决方法 2:用闭包手动保存 i

for (var i = 0; i < 3; i++) {
  (function(n) {
    setTimeout(() => console.log(n), 1000);
  })(i);
}

🍹 每次循环都传入一个新参数 n, 闭包帮我们“记住”当前的 i。

🍰 类比:

每位顾客都留一张独立点单纸, 奶茶师根据那张纸做单,就不会混乱。


🌷第 80 页:闭包的注意事项与总结

⚠️ 闭包的风险:内存泄漏

闭包会让变量“长期存在”,如果滥用可能导致内存不释放。

function leak() {
  const bigData = new Array(100000).fill('🍵');
  return function() {
    console.log(bigData.length);
  };
}
const fn = leak();

💬 因为 fn 引用了 bigData,JS 不会释放这块内存。 除非手动清理引用(fn = null)。

🍬 类比:

奶茶师一直留着过期点单纸不丢,厨房就被塞满啦!


💎 小可爱总结(第76~80页)

知识点 含义 奶茶店比喻
闭包 函数记住外部变量 点单纸条 📒
应用场景 计数器、私有变量 点单机记号、配方封装
循环问题 延迟执行变量被共享 所有顾客都写成同一个号
解决方式 用 let 或立即执行函数 每人一张点单纸
风险 内存泄漏 纸条太多堆爆仓库

🎀 终极口诀:

“闭包记心头,变量不放手; 点单要留名,循环别共用; 内存要勤扫,纸条要烧掉~🧋”

🧋第 81 页:闭包的综合应用(模块封装)

这一页在讲:如何用闭包封装数据,创建“模块化结构”。


📘 示例:

var Customer = (function() {
  var name = '小可爱';
  var orders = [];

  function addOrder(item) {
    orders.push(item);
  }

  function getOrders() {
    return orders;
  }

  return {
    name: name,
    order: addOrder,
    show: getOrders
  };
})();

💬 调用:

Customer.order('珍珠奶茶');
Customer.order('草莓冰沙');
console.log(Customer.show());

🧠 输出:

['珍珠奶茶', '草莓冰沙']

🪄 通俗解释:

这里我们用闭包把变量 nameorders“藏”起来, 外部访问不到,只能通过暴露的函数(ordershow)来操作。

🍹 奶茶店比喻:

Customer 是一个“点单管理系统”:

  • 点单列表(orders)是后台数据库;
  • 外部顾客不能直接改,只能通过按钮(order / show)操作;
  • 这样就防止“乱改订单”啦 ✅。

📎 小记忆法:

闭包 + 返回对象 = 简易模块系统。 就像“店长后台 + 前台操作台”组合。


🌸第 82 页:闭包的延伸——封装与构造函数结合

继续升级版,讲到闭包 + 构造函数结合


📘 示例:

function Factory() {
  var secretRecipe = '配方保密 🧋';
  this.make = function() {
    console.log('开始制作奶茶:' + secretRecipe);
  };
}
var shop = new Factory();
shop.make(); // 输出:开始制作奶茶:配方保密 🧋

🍬 含义:

  • secretRecipe 是“私有变量”;
  • make() 是“公开方法”;
  • 但外部无法直接访问 secretRecipe

🍰 类比:

顾客能点奶茶(调用 make), 但不知道厨房配方(secretRecipe)~ 闭包帮我们实现了“私有属性”


☀️第 83 页:类型转换机制概览

终于到了 JS 面试最爱问的陷阱系列之一: 👉 JavaScript 的类型转换机制


💡 JS 中的类型转换分三类:

类型 描述 举例
显示转换 你手动调用函数转换 Number('123'), String(10)
隐式转换 JS 自动帮你转 '5' * 2 → 10
强制转换 一些奇怪运算符强制触发 '3' - 1, '3' + 1

🍹 奶茶店类比:

显式转换:你主动换奶茶口味; 隐式转换:店员自动帮你加冰(没告诉你); 强制转换:顾客搞混点单,系统硬帮你纠正 😆。


🌷第 84 页:显示转换 (Explicit Conversion)

JS 提供了三个常见的手动转换函数:

方法 作用 示例
Number() 转数字 Number('3') → 3
String() 转字符串 String(123) → '123'
Boolean() 转布尔值 Boolean('') → false

📘 示例:Number()

console.log(Number('123')); // 123
console.log(Number('')); // 0
console.log(Number(true)); // 1
console.log(Number(false)); // 0
console.log(Number('小可爱')); // NaN

🍬 记忆口诀:

“空变零,真变一,假变零,文字懵(NaN)。”


📘 parseInt() / parseFloat()

parseInt('12.3')  // 12
parseFloat('12.3') // 12.3
parseInt('abc') // NaN

🍵 类比:

这俩函数像“扫单机”:

  • parseInt:看到数字就截下来;
  • parseFloat:连小数点都识别;
  • 如果一开头就看不懂,就说“我懵了 NaN”。

🧠第 85 页:String()Boolean() 转换

📘 String()

String(123); // "123"
String(true); // "true"
String(null); // "null"

🍰 类比:

“把所有奶茶都装进同一个标签罐里”——统一变成文字。


📘 Boolean()

Boolean('小可爱'); // true
Boolean(''); // false
Boolean(0); // false
Boolean([]); // true

📦 规则:

转布尔结果
0, NaN, '', null, undefined, false false
其他 true

🍹 奶茶店类比:

“有内容的杯子就算真,有空杯就是假。” 即便是空数组 [],杯子还在,所以 true


🎀 小可爱总结(第81~85页)

概念 含义 奶茶店比喻
闭包模块化 封装数据与操作 后台点单系统
构造函数+闭包 私有属性实现 店长配方保密
显式转换 手动换类型 主动改口味
隐式转换 JS 自动帮转 店员帮你加冰
Number() 变数字 空=0,真=1,假=0
Boolean() 真假判断 空杯是假,装奶茶是真

🌈 终极口诀:

“闭包藏秘密,模块保隐私; 类型别混乱,真空要牢记; 手动转最稳,隐式要小心~🍵”

🧋第 86 页:类型转换进阶(隐式转换)

💡 隐式类型转换(Type Coercion)

就是 JavaScript 自动帮你“悄悄换类型” 的机制。

🧠 举个例子:

console.log(1 + '2'); // "12"
console.log('3' * 2); // 6
console.log(true + 1); // 2

🍬 解释:

  • '+' 有“拼接字符串”的优先级,所以 '1' + 2"12"
  • 乘号 * 只懂数字 → '3' * 2 自动把 '3' 变成 3
  • true 自动变为 1

🍰 类比:

这就像收银员在算账:

  • “字符串”就是文字票据;
  • “数字”是金额;
  • JS 会自动帮你把票据转成金额去加减(有时很聪明,有时瞎搞 😆)。

🌸第 87 页:隐式转换的规则

JS 自动转换时,会根据运算符种类判断目标类型:

运算符 转换方向
+ 如果任一操作数是字符串 → 转字符串
-, *, / 全部转数字
比较运算(==) 尝试先转数字再比较

📘 示例:

console.log('5' - 2); // 3
console.log('5' + 2); // "52"
console.log('5' * '2'); // 10
console.log('5' == 5); // true
console.log('5' === 5); // false

💬 一句话总结:

“== 会自动帮你转换类型,=== 不会。”

🍹 奶茶店比喻:

==:不挑顾客,身份证号或昵称都行(只要内容相同) ===:严格核验身份证号、名字、出生日期都得一样。


☀️第 88 页:自动类型转换的细节陷阱

JS 的隐式转换有几条“神奇规则”🤯:

转数字 Number 转字符串 String 转布尔 Boolean
undefined NaN "undefined" false
null 0 "null" false
true / false 1 / 0 "true"/"false" true / false
[] 0 "" true
{} NaN "[object Object]" true

📘 示例:

console.log([] == 0); // true
console.log([] == ![]); // true (面试陷阱题)

💬 解释: 1️⃣ [] == 0[] 转成 0,结果相等 2️⃣ ![] 先转布尔(false),再转数字(0) 所以变成 0 == 0true

🍰 类比:

空杯子看起来没内容(被当成 0), “没奶茶”(false)也当作 0 → 所以两边居然相等😅。


🧃第 89 页:浅拷贝 vs 深拷贝(对象复制)

这一页进入对象复制的考点。


💡 定义

类型 说明 类比
浅拷贝 只复制第一层引用 拷贝了奶茶包装,但里面的珍珠还是同一碗
深拷贝 连内部内容都复制 整杯奶茶和配料都重做一份

📘 示例:浅拷贝

const obj = { name: '奶茶', info: { taste: '草莓' } };
const copy = Object.assign({}, obj);
copy.info.taste = '波霸';

console.log(obj.info.taste); // "波霸"

💬 为什么? Object.assign() 只复制第一层, 内部的 info 仍是同一个引用地址。

🍹 类比:

拷贝了奶茶标签,但配料桶共用 → 改一边另一边也变!


🌷第 90 页:实现深拷贝的方法

✅ 1. JSON 方法(简易版)

const obj = { name: '奶茶', info: { taste: '草莓' } };
const deepCopy = JSON.parse(JSON.stringify(obj));

deepCopy.info.taste = '波霸';
console.log(obj.info.taste); // "草莓"

🍰 原理: JSON.stringify() 把对象转成字符串; JSON.parse() 再生成一个全新对象。

⚠️ 缺点:

  • 无法复制函数;
  • 无法处理循环引用;
  • 丢失 undefined

✅ 2. 递归实现(进阶)

function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') return obj;
  const result = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    result[key] = deepClone(obj[key]);
  }
  return result;
}

💬 每次遇到对象就递归复制一份,直到最深层都分开。

🍹 奶茶店比喻:

深拷贝 = 从奶茶到珍珠,从珍珠到糖浆都重新制作一份, 两杯完全独立,互不影响。


🎀 小可爱总结(第86~90页)

知识点 含义 奶茶店比喻
隐式转换 JS 自动帮你换类型 店员偷偷帮你加料
== vs === 前者宽松,后者严格 一个看脸,一个查身份证
空数组转数字 [] -> 0 空杯子当 0
浅拷贝 复制标签不复制配料 改一边另一边变
深拷贝 整杯重做 互不影响

🌈 可爱口诀:

“加减要小心,隐式变类型; == 不可靠,=== 才安心; 拷贝要彻底,奶茶要分离~🧋”

🧋第 91 页:数组拷贝与合并方法

我们先来看最简单的——数组的复制与合并


📘 slice()

const arr = ['奶茶', '可可', '绿茶'];
const copy = arr.slice();
copy[0] = '草莓奶茶';

console.log(arr);  // ['奶茶', '可可', '绿茶']
console.log(copy); // ['草莓奶茶', '可可', '绿茶']

💬 含义: slice() 会返回一个新数组,是 浅拷贝。 它不会影响原数组的第一层内容。

🍹 类比:

你复制了奶茶菜单 📋,自己在副本上改成草莓奶茶, 原店菜单没动。


📘 concat()

const arr = ['奶茶'];
const arr2 = ['抹茶'];
const merged = arr.concat(arr2);
console.log(merged); // ['奶茶', '抹茶']

💬 含义: concat() 把多个数组拼接起来,同样返回一个新数组(浅拷贝)。

🍰 类比:

这就像把“奶茶店菜单 + 咖啡厅菜单”合并成一本新菜单📖。


🌸第 92 页:多层结构的陷阱(浅拷贝问题)

📘 示例:

const arr = [{ name: '奶茶' }, { name: '绿茶' }];
const copy = arr.slice();
copy[0].name = '草莓奶茶';

console.log(arr[0].name); // '草莓奶茶'

💬 为什么改动了原数组? 因为 浅拷贝只复制第一层引用, 对象 { name: '奶茶' } 在原数组和新数组中是“同一个地址”!

🍵 奶茶店类比:

你复制了菜单,但里面的“原料仓”还是同一个, 改掉一种配方,两个菜单都显示变了~😂


☀️第 93 页:深拷贝(Deep Clone)实现方法

现在来看看让“菜单完全独立”的几种深拷贝方法👇


✅ 1. Lodash 的 _.cloneDeep()

const _ = require('lodash');
const obj = { name: '奶茶', detail: { taste: '草莓' } };
const newObj = _.cloneDeep(obj);
newObj.detail.taste = '波霸';

console.log(obj.detail.taste); // 草莓

💡 说明: _.cloneDeep() 会递归复制所有层级内容,是真正的深拷贝。

🍓 类比:

这就像请了一个“奶茶复制机机器人”🤖 每一层原料、每个配方都复制新的,不混用。


✅ 2. jQuery 的 $.extend(true, target, obj)

const obj = { a: 1, b: { c: 2 } };
const copy = $.extend(true, {}, obj);
copy.b.c = 99;
console.log(obj.b.c); // 2

💬 true 参数表示“深度合并(递归复制)”。

🍹 类比:

extend(true, …) 就像说:“连珍珠桶、糖浆罐都复制一份。”


✅ 3. JSON 方法(简单但有局限)

const obj = { name: '奶茶', taste: { flavor: '草莓' } };
const copy = JSON.parse(JSON.stringify(obj));
copy.taste.flavor = '波霸';

console.log(obj.taste.flavor); // 草莓

💬 优点:简单好用。 ⚠️ 缺点:

  • 会丢失函数、undefined;
  • 不能处理循环引用。

🍰 类比:

这就像把菜单拍照打印再扫描回来📸, 文本能复制,图片里的隐藏信息(函数)没了。


🧃第 94 页:浅拷贝 vs 深拷贝图示

这页有一张超重要的对比图,我帮你总结一下:


层级 浅拷贝 深拷贝
一级属性 ✅ 独立 ✅ 独立
多级对象 ❌ 共用引用 ✅ 各自独立

📊 图形理解:

浅拷贝:

原数组 ──→ 对象A
拷贝数组 ──→ 同一个 对象A

深拷贝:

原数组 ──→ 对象A1
拷贝数组 ──→ 对象A2(新建的!)

🍵 奶茶店比喻:

浅拷贝是两个菜单共用一个配料仓。 深拷贝是各自有独立厨房 🔥。


📘 对比示例

const a = { drink: '奶茶', ingredients: { sugar: '三分糖' } };
const b = a; // 引用
const c = JSON.parse(JSON.stringify(a)); // 深拷贝

b.ingredients.sugar = '全糖';
console.log(a.ingredients.sugar); // 全糖(b 改了共用的)
c.ingredients.sugar = '无糖';
console.log(a.ingredients.sugar); // 仍是全糖(c 独立)

🌷第 95 页:闭包缓存机制(函数记忆)

这一页开始讲一个非常有趣的高级话题: 👉 如何用函数 + 闭包实现缓存(Memoization)


💡 核心思想:

当一个函数执行结果“可以被重复利用”时,我们用缓存存下来, 下次调用就不用重新计算。


📘 示例:

function memoizedAdd() {
  const cache = {};
  return function(x, y) {
    const key = x + ',' + y;
    if (cache[key]) {
      console.log('取缓存~');
      return cache[key];
    }
    const result = x + y;
    cache[key] = result;
    console.log('计算并存入缓存~');
    return result;
  };
}

const add = memoizedAdd();
add(1, 2); // 计算并存入缓存~
add(1, 2); // 取缓存~

🍹 奶茶店比喻:

顾客点“草莓奶茶”→厨房先做一杯(存缓存); 下次再有人点同样口味 → 店长直接从冰箱取出成品 🧊!


✅ 优点:

  • 节省性能;
  • 避免重复计算;
  • 减少请求次数。

🍰 实战应用:

  • Vue、React 的“计算属性缓存”;
  • 接口数据缓存;
  • 图像处理或复杂计算缓存。

🎀 小可爱总结(第91~95页)

知识点 含义 奶茶店比喻
slice / concat 浅拷贝数组 复制菜单但共用原料
浅拷贝问题 多层结构共用地址 改配料两边都变
深拷贝方法 全面复制新对象 各自独立厨房
cloneDeep / JSON 两种深拷贝常用法 机器人复制 / 拍照扫描
函数缓存 用闭包保存结果 奶茶配方复用

🌈 可爱口诀:

“slice 拷壳不拷芯, concat 拼接也浅心; cloneDeep 真香机, 缓存闭包省电力~💡”

🧋第 96 页:函数缓存(Function Memoization)

💡 一、什么是函数缓存?

函数缓存就是——把函数的计算结果“记下来” , 下次遇到相同输入时直接取结果,不再重新算。

🍹 类比:

就像奶茶店老板有“老顾客卡”, 你上次点“波霸奶茶三分糖”,系统记住了,下次直接调出那杯配方 ✅。


📘 示例:

function add(x, y) {
  return x + y;
}
console.log(add(2, 3)); // 每次都算一次

如果要加缓存👇:

function memoAdd() {
  const cache = {};
  return function(x, y) {
    const key = `${x},${y}`;
    if (cache[key]) {
      console.log('从缓存拿结果');
      return cache[key];
    }
    console.log('计算并存入缓存');
    const result = x + y;
    cache[key] = result;
    return result;
  };
}
const add = memoAdd();
add(2, 3); // 计算并存入缓存
add(2, 3); // 从缓存拿结果

🧠 小总结:

  • 第一次计算 → 存缓存;
  • 第二次相同输入 → 直接取缓存;
  • 减少性能浪费 💪。

🍰 奶茶店版:

顾客第一次点“波霸三分糖”, 厨房调一杯 → 存冰箱; 第二次同样的单 → 直接从冰箱拿 🧊。


🌸第 97 页:函数缓存的多种写法

✅ 1️⃣ 闭包写法

利用闭包保存 cache

function memo(fn) {
  const cache = {};
  return function(...args) {
    const key = args.join(',');
    if (cache[key]) {
      console.log('取缓存');
      return cache[key];
    }
    console.log('重新计算');
    const result = fn(...args);
    cache[key] = result;
    return result;
  };
}

function add(a, b) { return a + b; }
const cachedAdd = memo(add);
cachedAdd(1, 2); // 重新计算
cachedAdd(1, 2); // 取缓存

✅ 2️⃣ Map结构缓存(性能更好)

function memoMap(fn) {
  const cache = new Map();
  return function(...args) {
    const key = args.toString();
    if (cache.has(key)) {
      return cache.get(key);
    }
    const res = fn(...args);
    cache.set(key, res);
    return res;
  };
}

💡 Map 更快、更高效,适合缓存大量数据。

🍵 奶茶店类比:

Map 就像“带条码的冰箱”📦, 拿饮品更快更精确,不会重复查找。


☀️第 98 页:函数缓存的实际应用

函数缓存常用于:

  1. 重复运算(比如斐波那契数列);
  2. 接口请求缓存;
  3. 搜索结果缓存。

📘 示例:斐波那契(递归优化)

function fib(n, cache = {}) {
  if (n <= 1) return n;
  if (cache[n]) return cache[n];
  cache[n] = fib(n - 1, cache) + fib(n - 2, cache);
  return cache[n];
}
console.log(fib(10)); // 超快!

💬 解释:

递归本来会重复算很多次。 加缓存后,计算过的结果直接用,速度起飞 🚀!

🍰 奶茶店比喻:

你要做“波霸 + 芋圆 + 三分糖”的奶茶, 第一次调出来存档。下次再点,就直接复制 ✅。


🌷第 99 页:字符串操作方法大全(开胃小甜点 🍬)

💡 字符串的三类操作:

类型 举例
操作方法 拼接、截取、替换
结构方法 查找、判断
转换方法 大小写、数组互转

📘 1️⃣ 操作方法 - concat()

let str1 = '奶茶';
let str2 = '三分糖';
console.log(str1.concat(' - ', str2)); // 奶茶 - 三分糖

🍹 类比:

concat 就像“拼接标签机”—— 把两个标签贴成一个新标题。


📘 2️⃣ slice()

let str = '草莓波霸奶茶';
console.log(str.slice(0, 2)); // 草莓

🍓 说明:

截取从 0 到 2 的字符(不包含 2)。

🍰 奶茶店版:

这就像把“草莓波霸奶茶”只拿出“草莓”部分尝尝味道。


📘 3️⃣ substring() vs substr()

let str = '珍珠奶茶';
console.log(str.substring(0, 2)); // 珍珠
console.log(str.substr(0, 2)); // 珍珠

⚠️ 区别:

  • substring(start, end):截到 end 前一位
  • substr(start, length):截 length 个字符

🍬 类比:

substring:说“从第几层到第几层” substr:说“从这层开始取几层” 都能把奶茶“截成小份”😆。


☕️第 100 页:字符串更多操作!

📘 4️⃣ indexOf() / includes()

let str = '波霸奶茶';
console.log(str.indexOf('奶')); // 2
console.log(str.includes('茶')); // true

💬 含义:

  • indexOf 找位置;
  • includes 判断是否存在。

🍹 类比:

“菜单上第几行是奶?” “这杯奶茶里有没有加茶?”


📘 5️⃣ replace()

let str = '三分糖奶茶';
console.log(str.replace('三分', '半糖')); // 半糖奶茶

💡 替换匹配到的内容。

🍰 奶茶店比喻:

把“订单上的三分糖”换成“半糖”~ 就是 replace 在干的事。


📘 6️⃣ split()

let str = '奶茶,咖啡,果汁';
console.log(str.split(',')); // ['奶茶', '咖啡', '果汁']

💡 按分隔符拆分成数组。

🍓 奶茶店版:

这就像“多杯订单”自动拆分成三杯单独制作的单子。


🎀 小可爱总结(第96~100页)

知识点 含义 奶茶店比喻
函数缓存 保存计算结果 老顾客卡 / 冰箱取成品
闭包缓存 用函数内部变量记住历史 店长小本本记菜单
Map 缓存 高效查找 条码冰箱系统
字符串 concat 拼接文字 拼标签机
slice / substr 截取文字 分杯、半份奶茶
replace 替换文字 改订单口味
split 拆分文字 拆多杯订单

🌈 可爱口诀:

“缓存记结果,字符串真灵巧; 拼接要 concat,截取分杯妙; 替换换口味,拆单 split 到~🧋”

🧋第 101 页:字符串的进阶方法(继续甜品时间)

1️⃣ trim() / trimStart() / trimEnd()

去掉字符串两端或一端的空格。

let name = '  奶茶  ';
console.log(name.trim());      // '奶茶'
console.log(name.trimStart()); // '奶茶  '
console.log(name.trimEnd());   // '  奶茶'

🍬 记忆法: 就像擦桌子,“trim” 就是擦干净两边的奶茶渍trimStart() 擦左边,trimEnd() 擦右边。✨


2️⃣ repeat()

重复字符串。

let word = '波霸';
console.log(word.repeat(3)); // 波霸波霸波霸

🍹 类比: 顾客喊“再来三杯!”——服务员复制三份相同订单。


3️⃣ padStart() / padEnd()

在字符串的开头或结尾补全。

let order = '7';
console.log(order.padStart(3, '0')); // '007'
console.log(order.padEnd(5, '*'));   // '7****'

🍰 比喻: 就像订单号补零或打星号的系统~让它更整齐漂亮。


4️⃣ toLowerCase() / toUpperCase()

全部变小写 / 全部变大写。

let name = 'MilkTea';
console.log(name.toLowerCase()); // milktea
console.log(name.toUpperCase()); // MILKTEA

☕ 比喻: 就像配料表统一格式:

  • 小写 = 店员语气柔和
  • 大写 = 店长喊话模式(“全糖加珍珠!”🤣)

🌸第 102 页:查找类方法

1️⃣ indexOf() / lastIndexOf()

找字符在字符串中的位置。

let drink = '草莓奶茶';
console.log(drink.indexOf('茶')); // 3
console.log(drink.lastIndexOf('莓')); // 1

💡 就像菜单上“第几个字写了茶?” lastIndexOf从右边开始找


2️⃣ includes()

判断是否包含。

let str = '奶茶波霸';
console.log(str.includes('奶')); // true

🍓 类比: 就像问“这杯奶茶里有加奶吗?” → 有就返回 true


3️⃣ startsWith() / endsWith()

检查字符串是否以某部分开头/结尾。

let name = '珍珠奶茶';
console.log(name.startsWith('珍珠')); // true
console.log(name.endsWith('奶茶')); // true

🍵 类比:

“这杯奶茶是不是以珍珠打底?” “结尾是不是奶茶味?”


☀️第 103 页:转换类方法

1️⃣ split()

按分隔符拆成数组。

let drinks = '奶茶,咖啡,果汁';
console.log(drinks.split(',')); // ['奶茶','咖啡','果汁']

💡 比喻:

一张总订单分成三张单子,各自制作。


2️⃣ search()

查找匹配的位置(可配合正则)。

let text = '波霸奶茶';
console.log(text.search('奶茶')); // 2

📦 类比:

就像系统里搜索“奶茶”关键字,告诉你从第几个字开始。


3️⃣ replace()(加强版)

let order = '三分糖奶茶';
console.log(order.replace('三分', '半糖')); // 半糖奶茶

🍬 类比:

顾客改口味:“三分糖换成半糖!”


4️⃣ match()

用正则匹配多个。

let msg = '奶茶123波霸456';
console.log(msg.match(/\d+/g)); // ['123','456']

💬 类比:

就像在菜单里找出所有数字配料编号。


🌷第 104 页:数组的常用方法(重头戏来啦~)

这页开始讲数组(Array), 也就是“多杯奶茶订单集合”。


一、操作类(增删改)

push() / pop()

  • push():尾部加一个;
  • pop():尾部删一个。
let drinks = ['奶茶'];
drinks.push('绿茶');  // ['奶茶','绿茶']
drinks.pop();         // ['奶茶']

🍵 类比:

push = 新订单加入队列 pop = 最后一杯做完送走 🚗


shift() / unshift()

  • shift():从头删;
  • unshift():从头加。
let drinks = ['奶茶','咖啡'];
drinks.shift();     // ['咖啡']
drinks.unshift('果汁'); // ['果汁','咖啡']

🍓 比喻:

shift = 把最早的订单交给顾客 unshift = 新订单插队到最前面!


☕第 105 页:数组遍历方法(逻辑思维区)

1️⃣ forEach()

遍历数组,每个元素都执行一次。

['奶茶','抹茶','咖啡'].forEach(item => console.log(item));

🍰 类比:

店员一杯一杯确认:“奶茶好了~”、“抹茶好了~”……


2️⃣ map()

生成一个新数组

let prices = [10, 15, 20];
let newPrices = prices.map(p => p + 5);
console.log(newPrices); // [15, 20, 25]

💡 类比:

把每杯奶茶都加价 5 元,生成新的菜单副本。


3️⃣ filter()

筛选符合条件的。

let drinks = [5, 15, 25];
let result = drinks.filter(p => p > 10);
console.log(result); // [15, 25]

🍵 类比:

从全部订单中筛出“单价大于10元”的奶茶。


4️⃣ reduce()

从左到右累计运算。

let prices = [10, 15, 25];
let total = prices.reduce((sum, p) => sum + p, 0);
console.log(total); // 50

💬 类比:

结账计算总价 💰


🎀 小可爱总结(第101~105页)

分类 方法 奶茶店记忆
去空格 trim 擦掉两边奶渍
重复字符串 repeat 多杯复制
补位 padStart / padEnd 补零打星号
查找 indexOf / includes 看菜单有没有
拆分 split 拆多杯订单
替换 replace 改糖度口味
添加 push / unshift 新订单加入队列
删除 pop / shift 订单完成送出
遍历 forEach 逐杯确认
映射 map 新菜单价表
筛选 filter 选贵奶茶
汇总 reduce 结账统计

🌈 可爱口诀总结:

“trim 去渍,split 拆杯; push 加单,pop 送杯; map 改价,reduce 结尾; filter 挑单最美味~🧋✨”

从奶茶店悟透 JavaScript:递归、继承、浮点数精度、尾递归全解析(通俗易懂版)

作者 张可爱
2025年10月18日 01:05
🍰 一、背景:为什么要学这些“烧脑”的东西? 因为—— 从 继承 到 递归,从 浮点数精度 到 尾递归优化, 每一个小知识点都在考验你对「执行机制」的理解。 那我们就从生活出发—— 在奶茶店里,轻松掌

[Python3/Java/C++/Go/TypeScript] 一题一解:贪心 + 排序(清晰题解)

作者 lcbin
2025年10月18日 07:24

方法一:贪心 + 排序

我们不妨对数组 $\textit{nums}$ 排序,然后从左到右考虑每个元素 $x$。

对于第一个元素,我们可以贪心地将其变为 $x - k$,这样可以使得 $x$ 尽可能小,给后续的元素留下更多的空间。我们用变量 $\textit{pre}$ 当前使用到的元素的最大值,初始化为负无穷大。

对于后续的元素 $x$,我们可以贪心地将其变为 $\min(x + k, \max(x - k, \textit{pre} + 1))$。这里的 $\max(x - k, \textit{pre} + 1)$ 表示我们尽可能地将 $x$ 变得更小,但不能小于 $\textit{pre} + 1$,如果存在该值,且小于 $x + k$,我们就可以将 $x$ 变为该值,不重复元素数加一,然后我们更新 $\textit{pre}$ 为该值。

遍历结束,我们就得到了不重复元素的最大数量。

###python

class Solution:
    def maxDistinctElements(self, nums: List[int], k: int) -> int:
        nums.sort()
        ans = 0
        pre = -inf
        for x in nums:
            cur = min(x + k, max(x - k, pre + 1))
            if cur > pre:
                ans += 1
                pre = cur
        return ans

###java

class Solution {
    public int maxDistinctElements(int[] nums, int k) {
        Arrays.sort(nums);
        int n = nums.length;
        int ans = 0, pre = Integer.MIN_VALUE;
        for (int x : nums) {
            int cur = Math.min(x + k, Math.max(x - k, pre + 1));
            if (cur > pre) {
                ++ans;
                pre = cur;
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int maxDistinctElements(vector<int>& nums, int k) {
        ranges::sort(nums);
        int ans = 0, pre = INT_MIN;
        for (int x : nums) {
            int cur = min(x + k, max(x - k, pre + 1));
            if (cur > pre) {
                ++ans;
                pre = cur;
            }
        }
        return ans;
    }
};

###go

func maxDistinctElements(nums []int, k int) (ans int) {
sort.Ints(nums)
pre := math.MinInt32
for _, x := range nums {
cur := min(x+k, max(x-k, pre+1))
if cur > pre {
ans++
pre = cur
}
}
return
}

###ts

function maxDistinctElements(nums: number[], k: number): number {
    nums.sort((a, b) => a - b);
    let [ans, pre] = [0, -Infinity];
    for (const x of nums) {
        const cur = Math.min(x + k, Math.max(x - k, pre + 1));
        if (cur > pre) {
            ++ans;
            pre = cur;
        }
    }
    return ans;
}

时间复杂度 $O(n \times \log n)$,空间复杂度 $O(\log n)$。其中 $n$ 为数组 $\textit{nums}$ 的长度。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈 😄~

每日一题-执行操作后不同元素的最大数量🟡

2025年10月18日 00:00

给你一个整数数组 nums 和一个整数 k

你可以对数组中的每个元素 最多 执行 一次 以下操作:

  • 将一个在范围 [-k, k] 内的整数加到该元素上。

返回执行这些操作后,nums 中可能拥有的不同元素的 最大 数量。

 

示例 1:

输入: nums = [1,2,2,3,3,4], k = 2

输出: 6

解释:

对前四个元素执行操作,nums 变为 [-1, 0, 1, 2, 3, 4],可以获得 6 个不同的元素。

示例 2:

输入: nums = [4,4,4,4], k = 1

输出: 3

解释:

nums[0] 加 -1,以及对 nums[1] 加 1,nums 变为 [3, 5, 4, 4],可以获得 3 个不同的元素。

 

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 109
  • 0 <= k <= 109

3397. 执行操作后不同元素的最大数量

作者 stormsunshine
2024年12月22日 13:15

解法

思路和算法

为了计算不同元素的最大数量,应将数组 $\textit{nums}$ 按升序排序,然后从小到大依次考虑每个元素执行操作之后的值。

排序之后,数组 $\textit{nums}$ 中的最小元素为 $\textit{nums}[0]$,将最小元素 $\textit{nums}[0]$ 更新后的值记为 $x_0$,则根据贪心策略,$x_0$ 应取可能的最小值,即 $x_0 = \textit{nums}[0] - k$。理由如下:将所有元素执行操作之后的最小值记为 $x_0$,如果 $x_0 > \textit{nums}[0] - k$,则将 $x_0$ 的值更新为 $\textit{nums}[0] - k$ 之后,所有元素执行操作之后的最小值更小,一定不会产生新的重复元素,不同元素的数量一定不变或增加。

确定 $x_0$ 之后,将次小元素 $\textit{nums}[1]$ 更新后的值记为 $x_1$,则 $\textit{nums}[1] - k \le x_1 \le \textit{nums}[1] + k$。由于 $x_0 = \textit{nums}[0] - k$ 且 $\textit{nums}[0] \le \textit{nums}[1]$,因此 $x_1 \ge x_0$,为了使不同元素的数量最大,$x_1$ 应取范围 $[\textit{nums}[1] - k, \textit{nums}[1] + k]$ 中的大于等于 $x_0 + 1$ 的最小值。理由如下。

  1. 如果 $x_1 = x_0$,则不同元素的数量不变,只有当 $x_1 > x_0$ 时才能使不同元素的数量增加。

  2. 如果 $x_1$ 的值大于可能的最小值,则将 $x_1$ 的值更新为可能的最小值之后,其余元素的取值范围更大,因此不同元素的最大数量不变或增加,不可能减少。

因此 $x_1 = \min(\max(\textit{nums}[1] - k, x_0 + 1), \textit{nums}[1] + k)$。

确定 $x_0$ 和 $x_1$ 之后,数组 $\textit{nums}$ 中的其余元素执行操作之后的值可以使用相同的方法确定。计算得到数组 $\textit{nums}$ 中的所有元素执行操作之后的值,即可得到不同元素的最大数量。

代码

###Java

class Solution {
    public int maxDistinctElements(int[] nums, int k) {
        int distinct = 0;
        Arrays.sort(nums);
        int prev = Integer.MIN_VALUE;
        for (int num : nums) {
            int curr = Math.min(Math.max(num - k, prev + 1), num + k);
            if (curr > prev) {
                distinct++;
                prev = curr;
            }
        }
        return distinct;
    }
}

###C#

public class Solution {
    public int MaxDistinctElements(int[] nums, int k) {
        int distinct = 0;
        Array.Sort(nums);
        int prev = int.MinValue;
        foreach (int num in nums) {
            int curr = Math.Min(Math.Max(num - k, prev + 1), num + k);
            if (curr > prev) {
                distinct++;
                prev = curr;
            }
        }
        return distinct;
    }
}

复杂度分析

  • 时间复杂度:$O(n \log n)$,其中 $n$ 是数组 $\textit{nums}$ 的长度。排序的时间是 $O(n \log n)$,排序之后需要遍历数组一次。

  • 空间复杂度:$O(\log n)$,其中 $n$ 是数组 $\textit{nums}$ 的长度。排序的递归调用栈空间是 $O(\log n)$。

❌
❌