格式比较乱是因为早期一题一题面试面完整过来,量太大新版更新时间需要比较久(
Vue 的响应式原理用到了什么设计模式,发布订阅跟观察者模式的区别
Vue 的响应式原理核心用到了 观察者模式(Observer Pattern),并且借助了一些 发布-订阅模式(Publish-Subscribe) 的思想。
Vue 响应式原理用到的设计模式
- 观察者模式(Observer Pattern)
- Vue 在响应式系统中使用了 数据劫持 + 依赖收集 的机制。
- 当数据发生变化时,触发
setter,通知对应的 Watcher(观察者)执行更新。 - 核心角色:
- Subject(被观察者):响应式数据对象
- Observer(观察者):Watcher(组件渲染函数或计算属性)
- 当依赖发生变化时,所有订阅该依赖的观察者都会收到通知并更新。
- 发布-订阅模式(思想层面)
- Vue 中的 Dep(依赖管理器)更像一个 中间调度中心,收集依赖(Watcher)并在数据变化时统一通知。
- 这种做法和发布-订阅模式类似:数据变化 = 发布事件,Watcher = 订阅者。
发布-订阅模式 vs 观察者模式 区别
| 特性 | 观察者模式 | 发布-订阅模式 |
|---|---|---|
| 依赖关系 | 观察者直接订阅目标对象(Subject),耦合度较高 | 发布者和订阅者不直接联系,通过事件中心解耦 |
| 实现方式 | Subject 维护 Observer 列表,状态变化时逐个通知 | 通过事件总线(Event Bus)或消息队列,发布者只发消息到中介 |
| 场景 | Vue 响应式系统、DOM 事件监听 | Node.js EventEmitter、消息总线、系统解耦 |
简单理解:
- 观察者模式:一对多,直接通知(Vue 响应式数据 → Watcher)
- 发布订阅模式:一对多,通过第三方调度中心(发布者 → 中间事件中心 → 订阅者)
Vue 2 的实现亮点
- 通过
Object.defineProperty劫持属性,拦截getter收集依赖,拦截setter通知更新。 - Dep 类作为 调度中心,存储当前依赖的 Watcher。
- Watcher 订阅响应式属性,当属性变化时被 Dep 通知。
Vue 3 的变化
- 改用
Proxy + Reflect实现响应式,核心依旧基于观察者模式思想,但细节更灵活。 - 引入
effect和 依赖追踪(track)/ 触发(trigger),优化依赖收集。
前端开发中经常会用到哪些设计模式?说个具体例子
- 观察者模式(Observer Pattern)
- 用于实现对象间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。
- 实际开发中,可以用来实现事件监听和订阅-发布机制。比如 Vue 的数据响应式系统就是基于观察者模式实现的。
- 示例:DOM 事件的监听和处理、Redux 中的状态管理等。
- 单例模式(Singleton Pattern)
- 用于确保一个类只有一个实例,并提供一个全局访问点。
- 实际开发中,可以用来管理全局状态、资源池、配置对象等。比如管理全局的数据缓存、路由管理器等。
- 示例:全局状态管理库(如 Redux、Vuex)、路由管理器(如 React Router、Vue Router)等。
- 工厂模式(Factory Pattern)
- 用于创建对象,但将对象的创建过程封装在工厂类中,客户端无需知道具体的创建细节,只需要通过工厂类来创建对象。
- 实际开发中,可以用来创建具有相似结构的对象,提高代码的灵活性和可维护性。
- 示例:创建不同类型的 UI 组件、创建 XMLHttpRequest 对象等。
- 适配器模式(Adapter Pattern)
- 用于解决两个接口不兼容的情况,通过适配器将一个接口转换为另一个接口,使得原本不兼容的类可以协同工作。
- 实际开发中,可以用来兼容不同版本的接口、封装第三方库等。
- 示例:兼容不同浏览器的事件处理、封装不同的数据格式转换器等。
- 装饰器模式(Decorator Pattern)
- 用于在不改变原有对象的结构的情况下,动态地给对象添加额外的功能。
- 实际开发中,可以用来实现插件化的功能、动态扩展对象的功能等。
- 示例:Vue 中的 mixin、React 中的 Higher Order Components(HOC)等。
各种 HTTP 请求头及其作用
1. 通用头(General Headers)
用于请求和响应的通用信息。
| 头字段 | 作用 | 示例 |
|---|---|---|
| Cache-Control | 缓存控制,如是否缓存、缓存时长 | Cache-Control: no-cache |
| Connection | 管理连接方式(保持或关闭) | Connection: keep-alive |
| Date | 请求发送的时间 | Date: Wed, 27 Aug 2025 12:00:00 GMT |
2. 请求头(Request Headers)
客户端向服务器发送请求时附带的信息。
(1)客户端信息
| 头字段 | 作用 | 示例 |
|---|---|---|
| Host | 指定请求的主机名和端口(HTTP/1.1 必须) | Host: www.example.com |
| User-Agent | 客户端信息(浏览器、系统等) | User-Agent: Mozilla/5.0... |
| Referer | 表示请求的来源页面 URL | Referer: https://example.com/page |
| Accept | 告诉服务器客户端能接收的 MIME 类型 | Accept: text/html,application/json |
| Accept-Language | 客户端期望的语言 | Accept-Language: zh-CN, en-US |
| Accept-Encoding | 支持的压缩方式 | Accept-Encoding: gzip, deflate, br |
(2)认证与安全
| 头字段 | 作用 | 示例 |
|---|---|---|
| Authorization | 认证信息,如 Token、Basic Auth | Authorization: Bearer <token> |
| Cookie | 携带客户端的 Cookie,用于会话保持 | Cookie: sessionId=abc123 |
| Origin | 表示请求的来源(跨域时使用) | Origin: https://example.com |
| X-CSRF-Token | 防止 CSRF 攻击的安全令牌 | X-CSRF-Token: abc123 |
(3)缓存控制
| 头字段 | 作用 | 示例 |
|---|---|---|
| If-Modified-Since | 校验资源是否在指定时间后修改过 | If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT |
| If-None-Match | 配合 ETag 校验资源是否变更 | If-None-Match: "etag12345" |
| Pragma | HTTP/1.0 缓存控制,通常 no-cache | Pragma: no-cache |
(4)请求体相关
| 头字段 | 作用 | 示例 |
|---|---|---|
| Content-Type | 请求体的 MIME 类型 | Content-Type: application/json |
| Content-Length | 请求体的字节长度 | Content-Length: 123 |
3. 自定义头(Custom Headers)
| 头字段 | 作用 | 示例 |
|---|---|---|
| X-Requested-With | 标识 AJAX 请求 | X-Requested-With: XMLHttpRequest |
| X-Forwarded-For | 记录客户端真实 IP(经过代理时) | X-Forwarded-For: 203.0.113.195 |
| X-Real-IP | 表示客户端真实 IP | X-Real-IP: 203.0.113.195 |
4. HTTP/2 专用伪头
| 头字段 | 作用 |
|---|---|
:method | 请求方法(GET、POST 等) |
:path | 请求路径 |
:authority | 主机和端口 |
:scheme | 协议(http/https) |
面试常问问题总结
- Content-Type 和 Accept 区别? Content-Type 表示请求体的格式,Accept 表示期望服务器返回的格式。
- If-Modified-Since 和 If-None-Match 区别? 前者基于时间(Last-Modified),后者基于资源标识(ETag),后者更精确。
- Referer 的作用? 表示请求来源页面 URL,用于防盗链、流量分析,但受
Referrer-Policy控制,HTTPS → HTTP 时可能被移除。
前端当中图片格式各自的使用场景,在项目中如何压缩图片来优化性能
一、前端常见图片格式及使用场景
- JPEG / JPG
- 特点:有损压缩,体积小,色彩丰富,支持 24 位真彩色,不支持透明。
- 使用场景:照片、色彩丰富的背景图(如 Banner、大图)。
- 优点:压缩率高,体积小。
- 缺点:有损压缩导致细节丢失,不适合图标或文字。
- PNG
- 特点:无损压缩,支持透明(Alpha 通道),支持 8 位、24 位、32 位色。
- 使用场景:图标、Logo、透明背景图片。
- 优点:清晰度高,支持透明。
- 缺点:体积大,不适合大图。
- GIF
- 特点:支持动画,256 色,支持简单透明。
- 使用场景:动图、表情包。
- 优点:支持动画,兼容性强。
- 缺点:色彩少,文件较大,效果有限。
- WebP
- 特点:Google 推出的新格式,支持有损/无损压缩,支持透明和动画。
- 使用场景:替代 JPEG / PNG,特别是在移动端和性能敏感场景。
- 优点:比 JPEG、PNG 体积更小(30%-80%)。
- 缺点:部分旧版浏览器不支持(可通过 polyfill)。
- SVG
- 特点:基于 XML 的矢量图,可无限缩放,代码描述图形。
- 使用场景:Logo、Icon、简单图形。
- 优点:体积小,支持 CSS、JS 操作,可缩放不失真。
- 缺点:不适合复杂图像(照片)。
- AVIF
- 特点:新一代图片格式,压缩比优于 WebP,支持透明。
- 使用场景:极致性能优化的场景。
- 优点:更小体积,图像质量更好。
- 缺点:浏览器兼容性尚未完全普及。
二、项目中压缩图片优化性能的策略
- 构建时图片优化
- 使用工具:image-webpack-loader(Webpack 插件)、gulp-imagemin(Gulp 任务)、svgo(SVG 压缩)
- 做法:JPEG 使用 mozjpeg,PNG 使用 pngquant,开启压缩选项;在打包时自动压缩,减少体积。
- 格式优化:优先使用 WebP 或 AVIF 代替 JPEG/PNG(配合
<picture>标签做回退)。 - 按需加载(懒加载):使用
loading="lazy"或 Intersection Observer API。 - 压缩传输:开启 Gzip 或 Brotli 压缩,减少网络传输体积。
- CDN 处理
- 使用 CDN 按需裁剪 + 压缩 + WebP 转换,比如阿里云 OSS、七牛云。
- 可根据
Accept头自动返回最佳格式。
- 运行时压缩(上传前)
- 使用前端压缩库:compressorjs(JPEG 压缩)、pica(图片缩放)、browser-image-compression(支持多格式)
实践总结:
- 大图:WebP / AVIF 优先,JPEG 次选。
- 图标:SVG 优先,小图用 PNG。
- 动图:用 video 或 Lottie 替代 GIF。
- 构建阶段压缩 + CDN 转码 + 懒加载。
interface & type 对比
- 合并
interface可以多次声明,会自动合并成一个接口,便于在不同位置扩展同一接口。type不能重复声明同名别名,否则会导致编译错误。
- 可选属性
- 在
interface中,使用?标记属性为可选。 - 在
type中,同样使用?标记可选属性(而| undefined仅表示属性值可以为undefined,但属性本身仍是必须存在的,不属于”可选”语义)。
- 在
- 扩展
interface可以通过extends关键字扩展其他接口或类。type可以使用交叉类型&组合多个类型。
- 类的实现
interface可以被类通过implements实现,确保类具备特定结构。type如果表示一个对象类型(非联合、非交叉等),也可以被类实现,但更推荐使用interface,因为interface在设计上更专注于对象形状,且支持声明合并和扩展。
- 类型声明与赋值
interface和type都只能用于类型声明,不能作为值直接赋值给变量(二者都是类型层面的工具,不会产生运行时值)。
- 适用场景
- 使用
interface描述对象形状、类的实现和扩展,以及需要声明合并的场景。 - 使用
type定义联合类型、交叉类型、元组、映射类型等复杂类型别名,以及无法用interface表达的更灵活的类型结构。
- 使用
说一说 WebSocket
WebSocket 是一种在单个 TCP 连接上进行 全双工通信 的网络协议。与传统的 HTTP 协议不同,WebSocket 允许客户端和服务器之间实时地双向通信,而不需要频繁地发起请求和响应。
关键特点和用途:
- 全双工通信:允许客户端和服务器之间同时进行双向通信,客户端可以发送消息给服务器,服务器也可以主动向客户端推送消息,实现实时性通信。
- 持久连接:连接一旦建立,会保持持久连接,不需要每次通信都重新建立连接,减少了连接和关闭的开销。
- 低延迟:由于连接是持久的,消息传递的延迟相对较低,适用于实时应用场景,如聊天应用、实时游戏等。
- 较小的数据帧开销:WebSocket 的数据帧相对于 HTTP 请求和响应的开销要小,因为不需要像 HTTP 那样包含大量的头信息。
- 跨域支持:与普通的 HTTP 请求不同,WebSocket 允许跨域通信,但仍然需要服务器端支持 CORS。
- 应用场景:需要实时性交互的应用,如在线聊天、多人实时游戏、实时数据监控、推送通知等。
API 使用:new WebSocket(url) 建立连接后,onopen/onmessage/onclose 监听事件,send() 发送给服务器,最后 .close() 关闭。
为什么用户相互通信不用 HTTP/2 服务器推送? HTTP/2 服务器推送是基于客户端对某个资源的请求触发的,服务器根据这个请求主动推送相关资源。这种模式适用于单向通信,即服务器向客户端推送资源,而客户端无法直接向服务器发起推送请求。用户相互通信需要双向通信,而 HTTP/2 的服务器推送功能并不支持客户端向服务器发起推送请求,因此无法满足需求。
如何确保 WebSocket 连接性:心跳机制(定时发送 ping/pong),断线重连,超时检测。
setTimeout 不准确的情况
- 事件循环机制:JavaScript 是单线程的,基于事件循环机制。如果在主线程上有其他耗时的任务,会影响
setTimeout回调的执行。例如主线程上有大量计算或同步代码,定时器的回调可能会被延迟。 - 最小延迟时间:浏览器或环境可能对定时器的最小延迟时间进行了限制,通常为 4 毫秒。即使设置更短的延迟,定时器也会被约束在这个最小值。
- 页面隐藏或后台运行:如果页面被切换到后台、最小化或者浏览器标签被隐藏,浏览器可能会降低对定时器的处理优先级,导致定时器执行的不准确。
- 系统资源限制:定时器的准确性还受系统资源限制的影响,包括 CPU 使用率、内存等。
协商缓存和强缓存的使用场景
- 强缓存使用场景:强缓存用于在一段时间内完全避免与服务器通信,直接使用本地缓存。这对于静态资源(如图片、CSS、JS 文件等)非常有用,因为它们通常不会频繁变化。
Cache-Control和Expires都可以用来设置强缓存。 - 协商缓存使用场景:协商缓存用于在资源可能发生变化时,检查是否需要获取更新的资源。这通过向服务器发送请求并使用
If-None-Match(基于 ETag)或If-Modified-Since(基于 Last-Modified 时间)头部来实现。如果服务器返回状态码为 304(Not Modified),则表示客户端的缓存仍然有效。协商缓存通常适用于一些可能会变动但不频繁变动的资源,如网页内容。
1. Cache-Control 和 Expires 的作用
Cache-Control:现代的、相对较为灵活的头部字段,用于控制缓存的行为。可以指定缓存的最大存储时间(max-age)、是否允许缓存被重新验证(must-revalidate)、是否允许共享缓存(public或private)、是否允许缓存的内容被修改(immutable)等。Expires:较早的头部字段,指定了资源的过期时间。服务器通过返回的时间戳告诉客户端,资源将在这个时间之后过期,从而客户端可以决定是否继续使用缓存或从服务器获取新资源。
2. 为什么同时使用这两个字段
Cache-Control为了提供更精细的缓存控制,可以根据各种情况指定不同的策略,比如设置不同的max-age、no-cache、no-store等。它的优势是可以更灵活地控制缓存的行为。Expires可以提供一个绝对的过期时间点,相对来说更简单。但它的问题是,如果客户端和服务器时间不同步,可能会导致缓存无法正常工作。因此Cache-Control更被推荐使用,特别是在现代的网络环境中。
qiankun 的 JS 隔离
qiankun 把 JS 代码包裹了一层 function,再把内部的 window 用 Proxy 包一层,这样内部的代码就被完全隔离了,实现了一个 JS 沙箱。原理是 function 包裹了一层,所以代码放在了单独作用域跑,又用 with 修改了 window,所以 window 也被隔离了。
三个沙箱:快照沙箱、单应用代理沙箱、多应用代理沙箱。
快照沙箱:在沙箱激活的时候记录 window 当时的状态(也称为快照)
- 恢复上一次沙箱失活时记录的沙箱运行过程中对 window 做的状态改变。
- 在沙箱失活的时候:
- 记录 window 上有哪些状态发生了变化(沙箱自激活开始,到失活的这段时间);
- 清除沙箱在激活之后在 window 上改变的状态——让 window 此时的属性状态和刚激活时候的 window 的属性状态进行对比,不同的属性状态就以快照为准,恢复到未改变之前的状态。
- 两个问题:一个会污染 window,无法多应用,会出现状态紊乱;另一个是需要通过 for in 去遍历 window 的所有属性,所以慢。
单应用代理沙箱:通过三个变量来记住沙箱激活后 window 发生变化过的所有属性,这样在后续的状态还原时就不再需要遍历 window 的所有属性来进行对比,提升了程序运行的性能。但是这仍然改变不了这种机制污染了 window 状态的事实,因此也就无法承担起同时支持多个微应用运行的任务。
多应用代理沙箱:用了 Proxy 解决这个问题(每个应用一个 fakeWindow,不污染真实 window)。
微前端原理,怎么实现 JS 隔离、样式隔离
微前端怎么实现?一句话:当路由切换的时候,去下载对应应用的代码,然后跑在容器里。 single-spa 可以根据 URL 路由加载对应的子应用,用户访问不同的路由时,只需要加载需要的子应用。
qiankun 做了样式隔离,有 shadow dom 和 scoped 两种方案,但都有问题:
- shadow dom 自带样式隔离,但是 shadow dom 内的样式和外界互不影响,导致挂在弹窗的样式会加不上。父应用也没法设置子应用的样式。
- scoped 的方案是给选择器加了一个
data-qiankun='应用名'的选择器,这样父应用能设置子应用样式,能隔离样式,但同样有挂在 body 的弹窗样式设置不上的问题,因为 qiankun 的 scoped 不支持全局样式。
而 react 和 vue 项目本身都会用 scoped css 或者 css modules 的组件级别样式隔离方案,这俩方案都支持传递样式给子元素、设置全局样式等,只是实现和使用方式不同。
现在的 vue、react 项目基本都做了组件样式隔离了,有点全局样式也是可控的,真没必要用 qiankun 的那个。
微前端解决了什么问题(增量升级):老应用技术栈过时,不需要一次性重写,新功能用新框架开发,通过微前端逐步整合,实现平滑迁移。
面试场景题
- 页面 1 点击歌曲 a,打开页面 2 播放歌曲 a,然后页面 1 更换歌曲 b,页面 2 更换播放歌曲 b。如何实现?(跨页面通信 + 播放同步)
- 页面 1 携带参数请求分页第三页数据,用户一进来就点击请求第 2 页,第三页数据请求比较慢,导致先看到第 2 页的数据,然后第三页的数据请求完后又看到第三页的数据。如何解决?(竞态问题:请求取消/序列号标记/只取最后一次结果)
- 如何取消请求:XHR 用
abort();fetch 用AbortController。 - 实现多个接口并发请求,按顺序返回结果:先发送异步请求,返回的结果也是 promise,对结果进行链式调用。注意面试官的意思是”按顺序获取结果”——拿到一个结果打印一下再去拿下一个,要求不用 promise.all,即对结果的串行调用。
- 为什么 B 站的弹幕不会挡住视频中的人物:
mask-image,例如-webkit-mask-image: url("mask.svg"); -webkit-mask-size: 668px 376px;——用带人像蒙版的图片作为遮罩,弹幕图层在蒙版透明处被遮住。
2 个页面之间通信的方法
- URL 参数传递:通过 URL 的查询字符串传递参数,例如
http://example.com/page2?param1=value1。在第一个页面中通过链接或 JavaScript 跳转并添加查询参数,第二个页面中解析 URL 获取参数值。 - Local Storage:使用 HTML5 的 LocalStorage API 在浏览器中存储数据,同一域名下的其它页面都可以访问。第一个页面存储数据,第二个页面读取。
- Session Storage:与 LocalStorage 类似,不同的是数据只能在同一个会话中共享,关闭浏览器窗口或标签页数据就会被清除。
- Cookies:在浏览器中存储数据,同一域名下的其它页面可访问。
- postMessage API:可以在两个页面之间进行跨域通信。第一个页面调用
window.postMessage()发送消息,第二个页面通过监听message事件接收。
聊重构收益
迁移后项目整体功能应与迁移前保持一致,但依赖的组件包、打包模式、内部逻辑的部分写法等会进行升级重构。
新项目将不会使用 kfx 静态部署,因为 kfx 不支持 SSR,暂时还使用老的部署流水线,后续升级为 SSR 流水线。即使在老的流水线下,webpack 迁移至 vite 后,流水线整体时间也降低了 50%。
Webpack 的流程
Webpack 是一个现代的前端构建工具,主要功能是将多个模块(JavaScript、CSS、图片等)打包成一个或多个静态资源文件。基本流程:
- 入口(Entry):指定 Webpack 开始构建的入口文件,根据入口文件构建依赖图谱,找到所有需要打包的模块。
- 模块解析(Module Resolution):根据入口文件中的依赖关系,逐步解析模块之间的依赖关系。根据配置中的规则确定如何解析不同类型的模块。
- 加载器(Loaders):对于非 JavaScript 类型的模块,使用加载器处理,将模块转换成 JavaScript 模块或在加载前处理。例如 Babel 加载器将 ES6 语法转换为 ES5,CSS 加载器处理 CSS 文件。
- 代码转换(Transformation):对通过加载器获取到的模块进行代码转换,包括语法转换、压缩、优化等。
- 模块打包(Module Bundling):将所有模块打包成一个或多个静态资源文件。
- 代码分割(Code Splitting):支持将代码分割成多个块(chunks),按需加载,优化加载速度。
- 资源输出(Output):根据配置指定输出的目录和文件名,将打包好的静态资源输出到指定位置。
- 插件(Plugins):插件系统可以执行更广泛的任务,如代码压缩、资源优化、生成 HTML 文件等,在构建过程的不同阶段介入。
SPA 是什么
SPA(Single Page Application,单页应用)是一种应用程序或网站的设计模式,特点是在单个 HTML 页面上加载所有必要的代码(HTML、CSS、JavaScript),并根据用户与应用的交互动态更新页面内容。相对于传统的多页面应用,SPA 通过减少页面加载次数和服务器请求,提高了用户体验和应用性能。
优点:
- 更快的页面加载速度:只需加载一次 HTML、CSS 和 JavaScript,后续页面跳转时只需通过 AJAX 请求获取数据并更新页面内容。
- 更好的用户体验:页面间切换无需重新加载,提供类似于桌面应用的体验。
- 减轻服务器负担:客户端负责渲染页面,服务器只需提供数据。
缺点:
- 首次加载时间较长:需要加载整个应用的代码和资源。
- 不利于 SEO:内容是动态生成的,搜索引擎可能无法正确索引页面内容。
- 框架依赖:通常需要使用前端框架(如 Angular、React 或 Vue)来实现。
hash 和 history 的原理
hash 和 history 都是实现前端路由的技术,前端路由使得 SPA 可以在不请求服务器的情况下实现页面内容的更新。
- hash:URL 中
#后面的字符串,它的变化不会导致浏览器向服务器发送请求。前端路由通过监听hashchange事件,根据 hash 的变化动态更新页面内容。例如http://example.com/#/home中的#/home是 hash 值。 - history:HTML5 新增的 API,允许开发者在不触发页面刷新的情况下修改浏览器的历史记录。利用
history.pushState和history.replaceState这两个方法(前者新增一条记录,后者替换最后一条记录),同时通过监听popstate事件来监听 URL 的变化。与 hash 相比,history 能够实现更友好的 URL。
浏览器引擎的种类
- WebKit:开源浏览器引擎,由苹果公司开发,是 Safari 的内核,也被很多其他浏览器采用。
- Blink:谷歌开发的引擎,是 Chrome、Edge 和 Opera 等浏览器的内核。Blink 是从 WebKit 分离出来的,与 WebKit 有很多相似之处。
- Gecko:Mozilla 基金会开发,是 Firefox 的内核。
- Trident:微软开发,曾是 Internet Explorer 的内核。在微软推出基于 Blink 内核的新版 Edge 后使用逐渐减少。
静态资源怎么储存
- 文件系统:直接存储在服务器的文件系统中,服务器收到请求时根据 URL 查找对应的静态文件并返回。
- CDN:内容分发网络,将静态资源缓存到离用户较近的服务器上,减少延迟和提高加载速度。
- 对象存储:云存储服务,将静态资源上传到云端并通过 URL 访问。优点是高可用性、可扩展性和低成本,如 Amazon S3、Google Cloud Storage、Azure Blob Storage 等。
- 缓存:将常用的静态资源存储在客户端(浏览器缓存)或服务器端(代理缓存、Redis 等),显著提升加载速度,减轻服务器负担。
零散简答题
- 纯函数:相同输入永远得到相同输出,且没有副作用。
- 数据结构有哪些:数组、链表、队列、堆、栈、树、图、散列表。
- 红绿灯组件:某个路口的红绿灯,按照红灯亮 5s、黄灯亮 2s、绿灯亮 3s 的顺序无限循环,每一秒打印当前在亮的灯。(Promise 链 + 循环)
- 响应式布局:媒体查询、rem、百分比布局、vw/vh 视口单位、flex 布局、UI 库自带适配。
- H5 适配方案:
- 怎么实现字体大小随页面缩放:设置根元素字体大小,底下用 rem,根用媒体查询。
- 一种是在 sass / less 中写自定义函数
pxToRem;另一种是直接写 px,编译过程利用插件(如 vue-cli3 中装 postcss-pxtorem)全部转成 rem。这样 dom 中元素的大小就会随屏幕宽度变化而变化。
- 吃透移动端 1px:归根结底两种方案,一种是利用 CSS 的
transform: scaleY(0.5),另一种是媒体查询根据不同 DPR 缩放。利用伪元素::after+transform进行缩放——为什么用伪元素?因为伪元素独立于当前元素,可以单独对其缩放而不影响元素本身的缩放。 - 0.5px 的线如何实现:同上,
transform: scale(0.5)或媒体查询 + DPR。 - rem、em、vh、vw 的区别:rem 相对根元素、em 相对父元素(当前元素字体大小)、vh 视口高度、vw 视口宽度。
- css 省略号:
text-overflow、white-space。white-space 控制元素内文本的空格和换行处理方式;text-overflow 控制元素内容超出部分的处理方式。(配合overflow: hidden+-webkit-line-clamp可做多行省略) - 垂直居中
- 绝对定位 +
margin-top负一半高度 或transform: translate(0, -50%) table-cell+vertical-align: middle(适用于块级元素,父元素需有固定高度)- flex:
align-items: center/justify-content: center/ 子项margin: auto 0 - grid 同理
- 绝对定位 +
- flex: 1:
flex-grow: 1; flex-shrink: 1; flex-basis: 0%。 - CSS 实现三角形:
border+width: 0; height: 0,三边transparent。 - z-index 属性在什么情况下会失效?
- 父元素 position 为 relative 时,子元素的 z-index 失效。解决:父元素 position 改为 absolute 或 static。
- 元素没有设置 position 属性为非 static 属性。解决:设置 position 为 relative、absolute 或 fixed 中的一种。
- 元素在设置 z-index 的同时还设置了 float 浮动。解决:去除 float,改为
display: inline-block。
- 原生 JS 方法拿到 ul 下的 li:
getElementsByTagName、querySelectorAll。 - 类数组转数组:
Array.prototype.slice.call()、Array.from()、扩展运算符(…)。 - js 数组方法 改变原数组 / 不改变原数组:改变:pop、push、shift、unshift、reverse、sort(还有 splice);不改变:concat、join、filter、reduce、find、findIndex、slice(返回一个索引和另一个索引之间的内容)。
- ES6+ 的新特性:const/let、模板字符串、箭头函数、函数参数默认值、解构赋值、for…of(用于数组等可迭代对象;for…in 用于对象,还会遍历原型链上的属性)、Promise、展开运算符(…)、对象字面量简写、class(原型链的语法糖)、迭代器和生成器。
- 和 = 有什么区别,分别在什么情况使用?
==会隐式类型转换后比较,===严格比较类型和值。日常推荐===;==一般只在x == null判断空时使用。===少了类型转换的步骤,比较快。 - undefined 和 null:undefined 的字面意思就是”未定义的值”(声明未赋值);null 表示”空对象指针”,当需要释放一个对象时,直接赋值为 null 即可。
- 对 JSON 的理解:JSON 是一种轻量级的数据交换格式,可以被任何语言读取和作为数据格式传递。项目开发中可作为前后端数据交换的一种方式。JS 中提供
JSON.stringify()和JSON.parse()两个函数处理 JSON。 - JSON 深拷贝缺点
- obj 里有时间对象,序列化后再解析,时间将只是字符串形式,而不是对象形式
- obj 里有 RegExp、Error 对象,序列化的结果将只得到空对象
- obj 里有函数、undefined,序列化的结果会丢失
- obj 里有 NaN、Infinity 和 -Infinity,序列化的结果会变成 null
- 只能序列化对象的可枚举的自有属性,构造函数生成的对象深拷贝后会丢弃 constructor
- 对象中存在循环引用的情况无法正确实现深拷贝
- instanceof 判断不了的(百度一面):
null instanceof null报TypeError: Right-hand side of 'instanceof' is not an object;{} instanceof Object报语法错误——因为 instanceof 的前操作数需要是一个对象表达式,但{}同时也是空的代码块,JS 无法识别{}是代码块还是空对象(需要加括号({}) instanceof Object)。 - Map 和 WeakMap 的区别
- Map 的键可以是任意类型,WeakMap 只接受对象作为键
- Map 的键实际上跟内存地址绑定,只要内存地址不一样就视为两个键;WeakMap 的键是弱引用,键所指向的对象可以被垃圾回收,此时键无效
- Map 可以被遍历,WeakMap 不能被遍历;正由于弱引用,WeakMap 的 key 是不可枚举的(没有方法能给出所有的 key),如果需要 key 列表应该使用 Map
- Map 和 object 的区别
- 存储键的类型不同:Map 可以将任意类型的值作为键,Object 只能将字符串或 Symbol 类型的值作为键
- 内存开销不同:存储大量数据时 Map 的内存开销比 Object 更小
- 遍历顺序不同:Map 按插入顺序遍历,Object 的键值对按照一定顺序(数字键升序、字符串键按插入序)遍历
- 拓展性不同:Map 可以使用扩展运算符和解构赋值进行扩展和复制
- Map 的键值对个数可以通过
size属性获取
- Object.create() 和 {} 有什么区别:
Object.create()接受一个对象作为参数,并使用这个对象作为新对象的原型,新对象可以继承原型对象的属性和方法;{}创建的新对象原型指向 Object.prototype。想让新对象继承指定对象就用Object.create(),只想创建普通空对象用{}。 - ES6 中 5 种遍历对象属性的方法
for-in——自身和继承的可枚举属性(除 Symbol)Object.keys()——自身非继承的可枚举属性(除 Symbol)Object.getOwnPropertyNames()——自身所有属性键名(包括不可枚举、除 Symbol)Object.getOwnPropertySymbols()——自身的所有 Symbol 属性的键名Reflect.ownKeys()——自身的所有键名
- 怎么进行异步编程:回调函数(最基本)、事件监听、发布/订阅、Promise、async/await、Generator。
- 怎么捕获 async/await 的错误:1. try/catch;2.
.then()/.catch()(因为返回的是 Promise);3. 使用 await-to-js 库来处理异常。 - async await:es7 提出的异步解决方案,是 generator 函数的语法糖,返回值是一个 promise 对象,比 generator 返回的 iterator 更方便,常和 promise 搭配使用。async 把 promise 包装了一下,使用 await 就不用写 then。
- Generator 与协程:Generator 是 ES6 提出的一种异步编程方案,用于更方便地创建 iterator,是一个返回值为 iterator 对象的函数。ES6 创造了新的遍历命令 for…of,Iterator 接口主要供 for…of 消费。Generator 可以暂停(yield)和恢复执行,实现协程思想。
- 在 React 中实现前端埋点:在页面中加入 JavaScript 代码监测用户行为,使用 XMLHttpRequest 或 fetch API 发送数据到服务器;使用第三方埋点工具(Google Analytics、Mixpanel 等);使用生命周期函数或自定义钩子函数监测用户行为(如 componentDidMount 中添加监测,componentWillUnmount 中发送数据)。
- 如果金融系统使用定时器每隔一秒钟用 Promise 发送请求,可能的问题:负载增加(大量请求导致服务器负载过高);安全风险(可能被利用做 DDoS 攻击);网络带宽消耗。应该谨慎考虑发送请求的频率,避免发送过多请求。
- DOM0 和 DOM2 级事件
- DOM0 级事件就是直接通过 onclick 绑定到 html 元素的事件,同一元素的同一事件只能绑定一个函数,后面的事件会覆盖前面的
- DOM2 级事件是通过 addEventListener 绑定的事件(removeEventListener 移除),可以绑定多个函数,按顺序执行
- DOM1 一般只有设计规范没有具体实现,所以一般跳过
- 死锁的产生条件:互斥条件、占用和等待条件、不可抢占条件、循环等待条件。解决方法:预防死锁(破坏四个必要条件之一)、避免死锁(银行家算法)、检测和恢复、忽略死锁。
- 进程和线程:进程是资源分配的最小单位,每个进程有自己独立的内存空间和系统资源,进程之间相互独立;线程是进程的一个执行单元,是 CPU 调度的最小单位,同一进程的线程共享进程的资源。IPC(进程间通信):管道、信号量、消息队列、套接字(Socket)。
- 手写发布订阅:维护一个事件表
events,on(event, cb)注册、emit(event, ...args)触发、off(event, cb)移除、once(event, cb)触发一次后移除。 - 手写洗牌算法:Fisher-Yates,从后往前遍历,每次用随机数交换当前元素与前面随机位置的元素。
- 实现一个轮播图组件 / 手写一个轮播图:核心是容器 overflow hidden + 内部横向排列 + transform 位移 + 定时器/触摸事件切换 + 无缝衔接(首尾克隆)。
- 获取 dom 的原生方法,怎么创建 dom 元素和修改元素:获取:getElementById、getElementsByClassName、getElementsByTagName、querySelector、querySelectorAll;创建:createElement、createTextNode;修改:appendChild、insertBefore、removeChild、replaceChild、修改属性/class/style。
- 手写遍历 dom 所有节点,返回标签名:递归(深度优先)或用
TreeWalker/document.querySelectorAll('*')遍历,收集node.tagName。
职业规划
为什么想来 XX?平台大,希望学习到技术,对技术热爱。规划是什么?第一年向前辈们学习,能做好自己的事情;三年后在技术深度和广度上都有发展,能独立完成较复杂的业务需求;五年后不局限于技术,而且有一种项目管理的思维,能带项目。有考公考研的打算吗?没有,我大二时便做好就业的规划,打算本科就业。什么时候到岗?五一后。能实习多久?长期实习。能接受加班吗?接受加班,但不接受无意义的内卷。
回答思路:
- 首先表示考虑过这个问题(有规划),如何谈一谈自己的现状(结合实际)。
- 接着从工作本身出发,谈谈自己会如何出色完成本职工作,如何对团队贡献、如何帮助带领团队其他成员创造更多的价值、如何帮助团队扩大影响力。
- 最后从学习出发,谈谈自己会如何精进领域知识、如何通过提升自己专业能力,如何反哺团队。
React
不可变数据
在 React 中,不可变数据(Immutable Data)是指数据一旦创建后,就不能再被直接修改或改变其值。任何对不可变数据的修改都会返回一个新的数据副本,而不是在原有数据上进行修改。
React 鼓励使用不可变数据,因为它带来了一些重要的优势:
- 性能优化:帮助 React 更高效地进行虚拟 DOM 的比较,减少不必要的渲染操作。
- 跟踪变化:通过对比前后两个不同的数据副本,React 可以轻松地跟踪数据的变化,精确更新 DOM。
- 数据稳定性:确保数据在多个组件之间传递时保持稳定,不被意外修改。
- 撤销与重做:可以方便地实现撤销与重做功能,因为可以简单地保存历史数据副本。
在 JavaScript 中,可以通过 Object.assign、展开运算符(…)、数组的 map 和 filter 方法等实现不可变数据,也可以使用第三方库(如 Immutable.js)。
介绍 hooks
useState:定义函数组件的状态。接收一个参数作为初始值;返回一个数组,第一个值为状态,第二个值为改变状态的函数。为什么返回 array 而不是 object:数组可以顺便对变量命名,代码干净;对象的话返回的值必须和内部实现同名,多次使用必须重命名。useEffect:又称副作用 hooks。作用:给没有生命周期的组件,添加结束渲染的信号。执行时机:在渲染结束之后执行(useLayoutEffect 是渲染结束之前同步执行)。模拟卸载:useEffect 第一个参数里返回的函数就是清理函数(模拟 componentWillUnmount);第二个参数为空数组时只在挂载后执行一次。useMemo:传递一个创建函数和依赖项,复杂计算逻辑的优化。缓存函数的结果(返回值可以是任何,函数、对象等)。会记录上一次执行 create 的返回值,并把它绑定在函数组件对应的 fiber 对象上,只要组件不销毁缓存值就一直存在,deps 中有一项改变就会重新执行 create。使用场景:大量计算操作避免每次渲染重新计算;对子组件传递相同内容的对象时进行性能优化。useCallback:对传过来的回调函数优化,返回 memoized 的函数。可以简单看作useMemo(() => Fn, deps)相当于useCallback(Fn, deps)。目的是不希望子组件进行多次渲染,并不是为了函数进行缓存——将回调函数传递给子组件作为 props 时,确保只有在依赖项发生变化时才创建新的函数,避免不必要的子组件重新渲染。useMemo 用于缓存函数的结果,而 useCallback 用于缓存函数本身。useRef:返回一个可变的 ref 对象,.current属性被初始化为传入的参数。返回的 ref 对象在组件的整个生命周期内保持不变。主要功能:获取 DOM 元素或组件实例;保存在组件生命周期内不会变化的值(长久保存数据)。注意事项:保存的对象发生改变不通知,属性变更不会重新渲染。- 自定义 hook:如 useHttp——useRef 做错误重试,useEffect 监听缓存的 key 有没有变,useState 存请求的结果和状态。怎么同步拿到 useState 的最新值:自定义 hook 结合 useRef 通过回调函数去同步拿到最新值。
Fiber 实现原理
Fiber 把一个渲染任务分解为多个渲染任务,而不是一次性完成,把每一个分割得很细的任务视作一个”执行单元”,React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去,任务会被分散到多个帧里面,中间可以返回至主进程控制执行其他任务,最终实现更流畅的用户体验。
即实现了”增量渲染”:可中断与恢复,恢复后也可以复用之前的中间状态,并给不同的任务赋予不同的优先级,其中每个任务更新单元为 React Element 对应的 Fiber 节点。
实现方式是 requestIdleCallback 这一 API,但 React 团队 polyfill 了这个 API(调度器 Scheduler 用 MessageChannel 实现),使其浏览器兼容性更好且拓展了特性。
React 素质 N 连
- useRef 主要的功能是获取 DOM 元素或组件实例,还可以保存在组件生命周期内不会变化的值。作用:操作 DOM 元素,集成第三方 DOM 库,DOM 元素内容设置及媒体播放,焦点控制(计算坐标)。
- 之所以将事件委托从 document 中移到
id = root的 DOM 元素,是为了可以更加安全地进行新旧版本 React 树的嵌套。 - 每一个 hooks 方法都会生成一个类型为 Hook 的对象,用来存储一些信息,函数组件 fiber 中的 memoizedState 会存储 hooks 链表,每个链表结点的结构就是 Hook。(这也是 hook 不能在分支语句中使用的原因——React 要求每次渲染时 Hooks 必须按照相同的顺序调用,顺序变化会引起错误。)
- 16 版本先执行原生事件,当冒泡到 document 时统一执行合成事件;17 版本在原生事件执行前先执行合成事件捕获阶段,原生事件执行完毕执行冒泡阶段的合成事件,通过根节点来管理所有的事件。
hooks 原理
React Hooks 是一种在函数组件中使用状态和其他 React 特性的方式,允许在不编写 class 的情况下使用状态。
原理是利用了 JavaScript 闭包的特性,在函数组件中提供访问私有状态的方法,这些状态可以在函数的生命周期内保持不变。实现上,函数组件 fiber 的 memoizedState 存储了 hooks 链表,调用 useState 时按调用顺序从链表上取对应的 Hook 对象——这也是 Hooks 必须按相同顺序调用(不能放在分支里)的原因。
React.memo() 和 useMemo() 的主要区别
React.memo()是一个高阶组件,可以用来包装不想重新渲染的组件,除非其中的 props 发生变化。useMemo()是一个 React Hook,在组件中包装函数,确保该函数中的值仅在其依赖项之一发生变化时才重新计算。
为什么 setState 异步
保证内部一致性、启用并发更新。
setState 在 react 18 之前:在组件的生命周期或者 react 的合成事件中是异步的;在 setTimeout、Promise 或者原生 dom 事件中是同步的。18 之后默认所有操作都放到批处理中(异步处理),如果希望同步拿到可以使用 flushSync。
React 生命周期 / 常用 API
常用 API:
React.createElement():创建 React 元素ReactDOM.render():将 React 元素渲染到 DOM 中React.Component:创建组件类的基类React.PureComponent:创建纯组件类的基类React.memo():创建纯函数组件的高阶组件useState()/useEffect()/useContext()/useRef()
生命周期(类组件):
- Mounting(第一次渲染):constructor() → getDerivedStateFromProps() → render() → componentDidMount()
- Updating(props 或 state 改变):getDerivedStateFromProps() → shouldComponentUpdate() → render() → getSnapshotBeforeUpdate() → componentDidUpdate()
- Unmounting(从 DOM 中移除):componentWillUnmount()
函数组件和类组件的区别
设计思想:
- 类组件的根基是 OOP(面向对象编程),所以它会有继承,有内部状态管理等
- 函数组件的根基是 FP(函数式编程)
Diff 算法(React)
Diff 算法用于比较前后两个虚拟 DOM 树的差异,只更新发生变化的部分,避免不必要的 DOM 操作,提高渲染性能。
实现流程:
- 比较两个根节点,如果节点类型不同,则直接替换整个节点。
- 如果节点类型相同,则比较节点属性,将有变化的属性更新到 DOM 上。
- 比较子节点,如果子节点数量不同,则直接替换整个节点。
- 如果子节点数量相同,则逐个比较子节点。
- 如果子节点类型相同,则递归执行 Diff 算法。
- 如果子节点类型不同,则直接替换整个节点。
优化策略:将相邻的文本节点合并、设置 key 值优化列表更新等。需要注意的是 Diff 算法可以减少 DOM 操作但不是万能的,某些情况下性能可能不如手动操作 DOM。
react18 和 17、16 有啥区别
- 批处理:React 18 中批处理支持的操作范围扩大了:Promise、setTimeout、native event handlers 等这些非 React 原生的事件内部的更新也会得到合并。
- Transitions:React 中一个用于区分高优更新和非高优更新的新概念。当用户拉动滚动条时,下方树的每一个节点都会重新渲染带来卡顿,上方滚动条也无法实时跟着交互移动。常见做法是 debounce 或 throttle,React 18 提供了原生方式:
startTransition和useTransition。startTransition:用于标记非紧急的更新,包裹起来告诉 React 这部分渲染优先级不高,可以优先处理其它更重要的渲染。useTransition:除了提供 startTransition 以外,还能提供一个变量跟踪当前渲染的执行状态(isPending)。
- createRoot:新的 root API。旧版本通过
ReactDOM.render渲染,React 18 中只有使用ReactDOM.createRoot才能使用新特性。另外 react18 不再支持 IE。
React 架构演进
- React 15:还没有 concurrent 的概念,主要分为 Reconciler 和 Renderer 两部分:Reconciler 负责生成虚拟 DOM 并进行 diff,Renderer 负责将变化的组件渲染到不同的宿主环境中。
- React 16:架构改动较大,多了一层 Scheduler,并且 Reconciler 部分基于 Fiber 完成了重构。
- React 17:相较先前没有大的架构改动,是稳定 concurrent mode 的过渡版本,使用 Lanes 重构了优先级算法。
- React 18:正式落地并发特性。
react 调度器怎么判断事件优先级 / 调度器,diff,fiber 优先级打断
Scheduler 通过优先级(Lane 模型,17 之前是 expirationTime)为任务排序,配合时间切片(每帧检查剩余时间)实现可中断的渲染:高优先级任务可以打断低优先级任务的执行,被打断的任务之后会重新调度恢复。
你是怎么理解 React 的?
从数据驱动 ui = f(state)、响应式 fiber + 异步可中断更新、组件化 component、hooks、跨平台 scheduler / reconciler / render / vdom、合成事件系统等方面答。
vue 和 react diff 算法的区别 / vue 和 react 区别
- vue:template 转为 render 函数 div → h 函数;react:div →
React.createElement() - vue 内部数据劫持,精确知道数据变化后调用 render;react 只会在 setState 之后重走渲染流程,根据新数据重新渲染虚拟 DOM 再对比
- vue 实现了数据的双向绑定,监听每个属性的变化进行响应式更新(基于 Proxy);react 是函数式思想、单向数据流、推崇数据不可变性
- 在渲染时 vue 会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树;react 在应用状态被改变时全部子组件都会重新渲染,可通过 shouldComponentUpdate 控制,Vue 将此视为默认优化
- 模板语法:vue 是指令+模板语法,react 是 jsx 函数式编程
- diff:Vue 基于 snabbdom,使用双向链表边对比边更新 DOM;React 使用 diff 队列保存需要更新的 DOM,得到 patch 树再统一批量更新
- 事件机制:vue 直接是原生事件;react 是合成事件——事件冒泡到根节点进行事件委托处理,且做了跨端兼容处理
React DOM 的 XSS 防护
React DOM 在渲染所有输入内容之前默认会进行转义,可以确保应用中永远不会注入那些并非自己明确编写的内容,所有内容在渲染之前都被转换成了字符串,有效防止 XSS 攻击。
闭包与垃圾回收
细嗦闭包
闭包(closure)指有权访问另一个函数作用域中变量的函数。——《JavaScript 高级程序设计》
闭包是一种特殊的对象,它包含了一个函数以及与函数相关的引用环境。当这个函数被调用时,它能够访问并修改它的引用环境中的变量。在 JavaScript 中,闭包可以通过在一个函数内部定义另一个函数来创建,常常被用来创建可以访问私有变量的函数。
内部函数可以访问其外部函数中声明的变量,调用外部函数返回内部函数后,即使外部函数执行结束了,内部函数引用外部函数的变量依然保存在内存中。
闭包有什么用:
- 延伸变量作用域范围,读取函数内部的变量
- 让这些变量的值始终保持在内存中
作用:
- 独立作用域,避免变量污染
- 实现缓存计算结果,延长变量生命周期
- 创建私有变量
运用:防抖节流、模拟块级作用域、对象中创建私有变量、hook。
缺陷:常驻内存当中,增加内存的使用量,使用不当会造成内存泄漏(对应的内存泄漏场景:闭包引用的大对象无法释放——给返回的那个内部函数置为 null 可解除引用)。
标记清除
这是 JavaScript 中最常用的垃圾回收方式。当变量进入执行环境时,就标记这个变量为”进入环境”,从逻辑上讲永远不能释放进入环境的变量所占用的内存。当变量离开环境时,则将其标记为”离开环境”。垃圾收集器运行时会释放所有”离开环境”变量的内存。
引用计数
跟踪记录每个值被引用的次数。声明一个变量并将引用类型赋值给该变量时,这个值的引用次数就是 1;如果引用这个值的变量又取得了另外一个值,引用次数就减 1。当引用次数变成 0 时,就说明没有办法再访问这个值了,垃圾收集器下次运行时就会释放那些引用次数为 0 的值所占的内存。
引用计数容易在循环引用时出现问题——循环引用时计数并不会归零,导致无法释放内存。解决循环引用带来的内存泄露,只需要把循环引用中的变量设为 null 即可。
V8 垃圾回收机制
V8 采用分代垃圾回收机制,将内存分为新生代和老生代两个区域。新生代存储新创建的对象,老生代存储长期存在的对象。新生代对象很快会被释放,采用更轻量级的垃圾回收策略(Scavenger);老生代采用更严格的垃圾回收策略(Mark-Compact)。
V8 中有两个垃圾收集器:主要的 GC 使用 Mark-Compact(标记-整理) 算法,从整个堆中收集垃圾;小型 GC 使用 Scavenger 算法收集新生代垃圾。
Scavenge 算法的工作过程(副垃圾回收器,半空间分为对象区域和空闲区域):
- 将堆内存划分为 From 空间和 To 空间,From 存放当前正在使用的对象,To 空闲。
- 对象被创建时分配到 From 空间。
- 当 From 空间快要满时触发垃圾回收,执行 Scavenge 算法。
- 将 From 空间中的存活对象复制到 To 空间,同时清理非存活对象。
- 完成复制后 From 和 To 空间角色互换(角色翻转)。如果对象两次存活就会被置入老生区。
由于新生代中对象生命周期短,大多数对象都是垃圾,Scavenge 算法能快速清理非存活对象并复制存活对象,大大减少堆内存的碎片化。
Mark-Compact(主 GC):标记过程与标记-清除一样,但后续不是直接清理可回收对象,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存(内存压缩,避免内存碎片)。
全停顿(Stop-The-World):JavaScript 运行在主线程上,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。
为了降低老生代垃圾回收造成的卡顿,V8 将标记过程分为一个个的子标记过程,让垃圾回收标记和 JavaScript 应用逻辑交替进行直到标记阶段完成,这就是增量标记(Incremental Marking)算法。它把一个完整的垃圾回收任务拆分为很多小任务,穿插在其他 JavaScript 任务中间执行,执行动画时就不会因为垃圾回收任务而感受到页面卡顿。
两种算法应对不同场景:
- Scavenger 算法主要处理存活周期短的对象中的可访问对象。
- Mark-Compact 算法主要处理存活周期长的对象中的不可访问对象。
因为新生代中存活的可访问对象占少数,老生代中的不可访问对象占少数,所以这两种回收算法配合使用十分高效。
js 数据类型
分为基本数据类型和引用数据类型。
基本数据类型存储在栈中,被引用或拷贝时会创建一个完全相等的变量,因为占据空间小、大小固定、频繁被使用,所以被放入栈中存储。
引用数据类型存储在堆内存中,占据空间大、大小不固定,引用类型在栈中存了指针,指向堆中该内存的起始地址,解释器寻找引用值时会首先检索其在栈中的地址,取得地址后从堆中获得实体。
JavaScript 一共有 8 种数据类型,其中 7 种基本数据类型:Undefined、Null、Boolean、Number、String、Symbol(es6 新增,表示独一无二的值)和 BigInt(es10 新增);引用类型 Object。
BigInt、Proxy、Reflect
- BigInt:内置对象,提供了一种方法来表示大于 2^53 - 1 的整数(Number 可表示的最大数字),可以表示任意大的整数。在整数字面量后面加
n定义(如10n),或调用函数BigInt()(不包含 new 运算符)传递整数值或字符串值。 - Proxy:用于修改某些操作的默认行为,等同于在语言层面做出修改,属于”元编程”。作为构造函数使用,传入两个参数:第一个是所要拦截的目标对象,第二个是 handler 拦截属性(set、get 等)。
- Reflect:与 Proxy 一样,是 ES6 为了操作对象而提供的新 API。设计目的:
- 将 Object 对象的一些明显属于语言内部的方法(如 Object.defineProperty)放到 Reflect 对象上,未来的新方法将只部署在 Reflect 对象上。
- 修改某些 Object 方法的返回结果让其变得更合理。如
Object.defineProperty(obj, name, desc)在无法定义属性时会抛出错误,而Reflect.defineProperty(obj, name, desc)则会返回 false。 - Reflect 对象的方法与 Proxy 对象的方法一一对应。
原型与原型链
在 JavaScript 中,原型是一种特殊的对象,用于为其他对象提供属性和方法。每个构造函数都有一个 prototype 属性,指向的就是显式原型对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法;每个实例对象上有一个 __proto__ 属性,指向的就是隐式原型对象。
原型链是一种特殊的继承关系,用于定义对象之间属性和方法的继承关系。查找一个属性先在自身查找,如果找不到,就沿着 __proto__ 属性在原型对象上进行查找,如果还找不到,就沿着原型对象的 __proto__ 属性进行查找,直到找到 Object 的原型对象,如果还没有找到就会返回 undefined。沿着 __proto__ 查找属性(方法)的这条链就是原型链。
说一说箭头函数
箭头函数是 JavaScript 中的一种简写形式,可以让你更方便地书写函数。特征:
- 它没有自己的 this,它的 this 是继承自外面函数的(词法作用域)。
- 它没有 arguments 对象,如果要用,可以用 rest 参数代替(…args)。
- 它不能作为构造函数,也就是说不能使用 new 操作符来调用。
- 隐式返回(优点),多行要用一个括号包起来。
BFC
BFC(块级格式化上下文)就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之也如此。
生成 BFC 的方法:
- 元素是浮动元素(float 不是 none)
- 元素是绝对定位元素(position 为 absolute 或 fixed)
- 元素的 display 属性为 inline-block、table-cell、table-caption、flex、inline-flex 或 grid
- 元素是 overflow 属性不为 visible 的块级元素
- 元素是弹性布局中的项目(子元素),且父元素的 display 属性为 flex 或 inline-flex
Promise 的理解
Promise 对象是异步编程的一种解决方案。Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态:pending、fulfilled 和 rejected。实例的状态只能由 pending 转变为 fulfilled 或者 rejected,并且状态一经改变,无法再被改变。
状态的改变是通过传入的 resolve() 和 reject() 函数来实现的:调用 resolve 回调函数时,会执行 Promise 对象的 then 方法传入的第一个回调函数;调用 reject 回调函数时,会执行 then 方法传入的第二个回调函数,或者 catch 方法传入的回调函数。
CSS 方案
总体来说,如果是进行基础组件的开发,使用”有规范约束”的原生 css(比如遵守 BEM 规范),或者 less 之类的预处理语言比较合适,能最大幅度减小组件库体积,也能为业务方提供样式覆盖能力。
如果是业务开发,推荐 css-in-js 方案:不仅能在组件中直接编写 css,还能直接使用组件中的 js 变量,有效解决”组件样式随着数据变化”的问题。业务开发迭代速度快、人员流动性大,直接使用规范约束 css 有风险,项目规模变大后可读性变差,也会出现 css 互相影响的情况。
CSS Module 在业务开发中也是不错的方案,但一般前端开发者会用”-“为样式命名,放到组件中就只能使用 style['form-item'] 的方式引用。
没有一项技术能解决全部问题,针对不同场景选择最合适的技术才是最优解。
CSS in JS
优点:
- 不需要额外的配置来支持 css modules
- 避免全局样式的污染
- 动态样式
- 会自动添加 auto-prefix
缺点:
- 运行时解析的实现版本增加了运行时性能压力
- 增加了包体积
SPA 首屏优化方式
- 代码分割:将 SPA 应用程序的代码分割成多个小块,在需要时动态加载,将加载时间分散到多个时间点,减少首屏加载时间。
- 懒加载:仅在需要时动态加载组件和资源,减少初始加载量。
- 预加载:提前加载下一个页面所需的资源,减少页面切换时的加载时间。
- 使用 CDN:静态资源放到 CDN 上,减少网络延迟和带宽占用。
- SSR:服务端渲染,在服务器端生成 HTML 页面,减少客户端加载时间并提高 SEO 效果。
- 静态化页面:将某些页面转化为静态页面通过 CDN 分发,减少服务器压力和流量消耗。
具体展开:
- 减小入口文件体积:常用手段是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求时单独打包,使入口文件变小。
- 静态资源本地缓存:后端返回资源采用 HTTP 缓存,设置 Cache-Control、Last-Modified、Etag 等响应头;前端合理利用 localStorage。
- UI 框架按需加载
- 组件重复打包:修改 CommonsChunkPlugin 的配置
minChunks: 3。 - 图片资源压缩
- 开启 Gzip 压缩
- 使用 SSR
常规的性能优化手段
- 减少 HTTP 请求:合并文件、CSS 精灵(
background-position) - 减少 DNS 查询:DNS 缓存、将资源分布到恰当数量的主机名,平衡并行下载和 DNS 查询
- 非必须组件延迟加载
- 使用 CDN 将静态资源放到 CDN 上
- 添加 Expires 或者 Cache-Control 响应头
- 对组件使用 Gzip 压缩
- 配置 ETag
内存泄漏
- 意外的全局变量
- 闭包(给返回的那个内部函数置为 null 解除引用)
- 事件监听未被移除
- 定时器未清理
- 脱离 DOM 的引用
如何用 webpack 来优化前端性能
- 压缩代码:如 UglifyJsPlugin(或 TerserPlugin)
- 利用 CDN 加速:构建过程中将引用的静态资源路径修改为 CDN 上对应的路径,通过
output.publicPath指定静态资源的访问路径。 - Tree Shaking
- Code Splitting:将代码按路由维度或者组件分块(chunk),做到按需加载,同时充分利用浏览器缓存。
谈谈对 window.requestAnimationFrame 的理解
让浏览器在下次重绘之前调用指定的回调函数更新动画。
优点:
- CPU 节能:使用 setTimeout 实现的动画,当页面被隐藏或最小化时仍然在后台执行动画任务,浪费 CPU 资源。而 requestAnimationFrame 会跟着系统步伐走——页面未激活时屏幕刷新任务被系统暂停,渲染也停止,页面激活时动画从上次停留的地方继续执行,有效节省 CPU 开销。
- 函数节流:在高频率事件(resize、scroll 等)中,使用 requestAnimationFrame 可保证每个刷新间隔内函数只被执行一次,既能保证流畅性也能节省函数执行的开销。显示器每 16.7ms 刷新一次,多次绘制并不会在屏幕上体现出来。
webpack 常用插件及优化方式
插件:
- HtmlWebpackPlugin:用于生成 HTML 文件,并自动将打包后的 JS、CSS 等文件注入到 HTML 中。
- CleanWebpackPlugin:用于清理打包后的文件,避免文件夹中出现无用的文件。
优化:
- Tree Shaking:通过静态分析代码,找到不会被使用到的代码并从 bundle 中删除,减小文件大小。
- 代码分割、图片压缩。
随着应用增长代码包也会随之增长,尤其整合了体积巨大的第三方库的情况下,需要关注代码包中包含的代码,避免体积过大导致加载时间过长。
webpack 构建优化
- 缩小搜索范围:
include/exclude、resolve.extensions、resolve.alias - 缓存:babel-loader 开启 cacheDirectory、cache: filesystem(webpack5 持久化缓存)
- 多进程:thread-loader、HappyPack
- DllPlugin 预编译第三方库
- 按需引入 polyfill、合理使用 sourceMap
HTTP 与网络
说说状态码
- 200 请求成功 √
- 204 成功处理,但没有返回内容 √
- 301 (Permanently Moved) 永久性重定向,请求的资源已不存在,需用新的 URL 访问 √
- 302 (Temporarily Moved) 临时重定向,请求的资源还在,暂时需要用另一个 URL 来访问 √
- 304 缓存重定向,不具有跳转含义,可继续使用缓存资源。服务器只根据请求头中的 If-None-Match 和响应头中的 ETag 比较判断是否返回 304,一致返回 304 √
- 400 请求的语法错误(一般为参数错误)√
- 401 需要进行 http 认证,如果之前发送过请求就说明认证失败
- 403 禁止访问资源,可能是客户端权限不对 √
- 404 资源不存在(错误 URL)或找不到请求的网页,也可用于拒绝请求且不想说明理由 √
- 500 Internal Server Error,与 400 类型,笼统通用 √
- 502 Bad Gateway,通常是服务器作为网关或代理时返回,服务器自身工作正常,访问后端服务器发生错误 √
HTTP&HTTPS
- http:无连接,无状态,不安全,明文传输。报文组成部分:请求报文、响应报文。
- HTTPS 和 HTTP 的区别:
- https 协议需要到 ca 申请证书,一般免费证书较少,因而需要一定费用。
- http 是超文本传输协议,信息是明文传输;https 则是具有安全性的 ssl 加密传输协议。
- http 和 https 使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443。
- http 的连接很简单,是无状态的;HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 http 协议安全。
- TLS/SSL 的功能实现主要依赖三类基本算法:散列函数 hash、对称加密、非对称加密。
http 的无状态含义
HTTP 的无状态性指的是协议本身不保存任何关于客户端请求的状态信息。每个客户端请求被视为一次独立的、无关的请求,服务器不会保留前一次请求的信息。
简单来说,无状态性意味着每个 HTTP 请求都是相互独立的,每个请求都必须包含所有必要的信息,以便服务器可以理解和处理请求,而无需依赖之前的请求。
为了维护状态,HTTP 应用程序通常使用一些机制:
- Cookies:在客户端保存一些标识信息,服务器在每个请求中识别客户端。
- 会话(Session):服务器通过会话机制在多个请求之间保存状态信息。客户端第一次访问时服务器创建一个唯一的会话标识并发送到客户端的 Cookie 中,后续请求客户端带上这个会话标识。
无状态性使得服务器的处理变得简单和高效,降低了服务器的负担并提高了性能。
GET 和 POST 的请求的区别
- get 请求一般是去获取数据(其实也可以提交,但常见的是获取数据);post 请求一般是去提交数据。
- get 的参数在 url 中,所以数据类型和长度都受限;get 请求的报文中实体部分为空,post 请求的报文中实体部分一般为向服务器发送的数据。
- get 请求可以被缓存,post 请求不会被缓存、不会被保存在浏览器记录中。
http 2 优点
二进制协议、多路复用、数据流、头信息压缩、服务器推送。
HTTP 1.0 和 HTTP 1.1 之间有哪些区别
- 持久连接
- 资源请求方面引入 range 头域,允许只请求资源的某个部分
- 引入了更多的缓存控制策略(etag、if-match 等)
- 新增 host 字段用来指定服务器的域名
http 队头阻塞问题
HTTP 队头堵塞是指浏览器发出 HTTP 请求时,所能处理的并发连接数有限,如果在这个限制内的请求都处于等待响应的状态,后续的请求就会被堵塞,直到有请求完成并释放一个连接,才能处理新的请求。这个问题通常发生在网站加载速度较慢的情况下。
解决方法:
- 优化网站性能,使网站加载速度更快
- 减少页面中的资源数量,特别是对于不必要的资源
- 使用内容分发网络 (CDN) 加速静态资源的加载
- 使用 HTTP/2 协议,支持多路复用,允许一个连接上同时传输多个请求和响应
- 在浏览器端使用懒加载技术,减少页面初始加载时的资源请求数量
TCP 和 UDP
TCP 和 UDP 都是传输层协议,都属于 TCP/IP 协议族。
UDP 的全称是用户数据报协议,是一种无连接的协议:
- 面向无连接:不需要和 TCP 一样在发送数据前进行三次握手建立连接,想发数据就可以开始发送。只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。发送端应用层将数据传递给传输层的 UDP 协议,UDP 只给数据增加一个 UDP 头就传递给网络层;接收端 UDP 只去除 IP 报文头就传递给应用层。
- 有单播,多播,广播的功能:不止支持一对一的传输方式,同样支持一对多,多对多,多对一。
- 面向报文:对应用层交下来的报文,既不合并,也不拆分,保留报文的边界,应用程序必须选择合适大小的报文。
- 不可靠性。
TCP 的全称是传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议:
- 面向连接:发送数据之前必须在两端建立连接(三次握手),为数据的可靠传输打下基础。
- 仅支持单播传输:每条 TCP 传输连接只能有两个端点,只能进行点对点的数据传输,不支持多播和广播。
- 面向字节流:不像 UDP 那样一个个报文独立地传输,而是在不保留报文边界的情况下以字节流方式进行传输。
- 可靠传输:判断丢包、误码靠的是 TCP 的段编号以及确认号。给每个包一个序号保证按序接收;接收端对已成功收到的字节发回确认(ACK);发送端在合理的往返时延(RTT)内未收到确认就会重传。
- 提供拥塞控制:当网络出现拥塞的时候,TCP 能够减小向网络注入数据的速率和数量,缓解拥塞。
- 提供全双工通信:连接两端都设有缓存,临时存放双向通信的数据(最大数据段大小取决于 MSS)。
TCP 和 UDP 的区别
| UDP | TCP | |
|---|---|---|
| 是否连接 | 无连接 | 面向连接 |
| 是否可靠 | 不可靠传输,不使用流量控制和拥塞控制 | 可靠传输(数据顺序和正确性),使用流量控制和拥塞控制 |
| 连接对象个数 | 支持一对一,一对多,多对一和多对多交互通信 | 只能是一对一通信 |
| 传输方式 | 面向报文 | 面向字节流 |
| 首部开销 | 首部开销小,仅 8 字节 | 首部最小 20 字节,最大 60 字节 |
| 适用场景 | 适用于实时应用,例如视频会议、直播 | 适用于要求可靠传输的应用,例如文件传输 |
跨域
跨域是指在浏览器中从一个域(例如 www.example.com)向另一个域(例如 api.example.com)发送请求时可能会遇到的问题。这是因为浏览器的同源策略(same-origin policy)限制了脚本可以访问的网站范围。
同源策略:协议、域名、端口,只要不满足其中任意一个要求,就不符合同源策略,就会出现”跨域”。
解决方法:
- JSONP(JSON with Padding):通过在 HTML 页面中包含一个
<script>标签,并通过回调函数将数据返回给前端实现跨域,通常只能用于 GET 请求。 - CORS(Cross-Origin Resource Sharing):浏览器和服务器之间的一种协议,通过在服务器端添加特殊的 HTTP 头,允许浏览器在跨域情况下访问服务器资源。
- 代理:在前端应用程序的服务器中设置代理,将请求转发到后端 API,前端通过与代理服务器的同源连接访问后端 API。
什么时候会触发预检请求:非简单请求(如使用 PUT/DELETE 方法、Content-Type 为 application/json、包含自定义头部等)会先发送 OPTIONS 预检请求。
fetch 和 xhr 的区别
fetch 默认不发 cookie,要通过 credentials: 'include' 设置;400、500 都是走 resolve 逻辑,只有网络错误才会 reject,只能通过 response.ok 来判断。
优点:
- 语法简洁,更加语义化
- 基于标准 promise 实现,支持 async/await
- 更加底层,提供的 API 丰富(request、response)
- 脱离了 XHR,是 ES 规范里新的实现方式
缺点:
- 只有网络错误这些导致请求不能完成时才会调用 reject,对 400、500 这种错误并不会 reject
- 默认不会带 cookie,需要添加配置项
fetch(url, { credentials: 'include' })(防止 CSRF) - 不支持 abort(注:现在可用 AbortController),不支持超时控制,使用 setTimeout 及 promise.reject 实现的超时控制不能阻止请求过程继续在后台运行,会造成流量浪费
- fetch 没办法原生检测请求的进度,XHR 可以(
xhr.onprogress)
关于跨域:fetch 支持跨域请求;XHR 默认也支持跨域请求,但浏览器存在同源策略,默认情况下 XHR 在发送跨域请求时会受到限制。
对 AJAX 的理解
ajax 的全称就是异步的 JS 和 XML,是一种创建交互式网页的开发技术方案。它的出现使得传统的 web 应用程序从”一次请求得到完整的被渲染后的网页文件”到可以进行局部刷新。
应用:用户上传头像的预览功能,注册、登录的验证操作(比如用户名是否重复、密码是否正确)。原生的 ajax 实现依赖于 XMLHttpRequest。
session 和 cookie 的区别
Session 和 Cookie 都是 Web 开发中用来保存用户状态的技术,主要区别:
- 数据存储位置:Cookie 在客户端浏览器中存储,Session 在服务器端存储。
- 存储容量:Cookie 存储容量较小(通常 4KB 左右),Session 可以存储大量数据。
- 安全性:Cookie 的数据可以被用户查看、修改和删除,敏感数据需要加密处理;Session 数据存储在服务器端,相对更安全。
- 生命周期:Cookie 可以设置过期时间;Session 在用户关闭浏览器或一定时间内没有访问网站后自动失效。
- 使用方式:Cookie 可以在客户端通过 JavaScript 操作;Session 需要在服务器端操作。
会话机制:用户第一次访问 Web 应用时,服务器会创建一个会话并分配一个唯一的会话 ID,将该会话 ID 存储在用户的浏览器 cookie 中。客户端每次请求都会将该会话 ID 一并发送给服务器,服务器根据该 ID 找到对应的会话。
Session 的主要作用是在用户和服务器之间维护一个状态:记录用户的登录状态和权限信息;保存用户的个性化设置(语言偏好、主题等);存储用户的购物车或订单信息等。
session cookie 等等
- Session 是在服务端保存的一个数据结构,用来跟踪用户的状态,可以保存在数据库、文件中(因为 http 无状态)。
- Cookie 是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现 Session 的一种方式。
- 由于浏览器的安全策略,localstorage 是无法跨域的,也无法让子域名继承父域名的 localstorage 数据,使用 postMessage 支持跨域。
- sessionStorage 在同源的情况下 a 跳转 b 会把 a 的数据拷贝一份作为 b 的初始缓存值。
cookie sessionStorage localStorage 区别
共同点:都是保存在浏览器端、且同源的。
区别:
- cookie 数据始终在同源的 http 请求中携带(即使不需要),即 cookie 在浏览器和服务器间来回传递;sessionStorage 和 localStorage 不会自动把数据发送给服务器,仅在本地保存。cookie 数据还有路径(path)的概念,可以限制 cookie 只属于某个路径下。
- 存储大小限制不同:cookie 数据不能超过 4K,且每次 http 请求都会携带,只适合保存很小的数据如会话标识;sessionStorage 和 localStorage 可达 5M 或更大。
- 数据有效期不同:sessionStorage 仅在当前浏览器窗口关闭之前有效;localStorage 始终有效,窗口或浏览器关闭也一直保存,用作持久数据;cookie 只在设置的过期时间之前有效,即使窗口关闭。
- 作用域不同:sessionStorage 不在不同的浏览器窗口中共享,即使是同一个页面;localStorage 在所有同源窗口中共享;cookie 也在所有同源窗口中共享。(所谓同源:域名、协议、端口相同)
说说 HttpOnly
HttpOnly 是 Set-Cookie HTTP 响应头中包含的附加标志。生成 cookie 时使用 HttpOnly 标志有助于降低客户端脚本访问受保护 cookie 的风险(如果浏览器支持)。值为 true 时,不允许通过脚本 document.cookie 去更改这个值,这个值在 document.cookie 中也不可见,但在发送请求时依旧会携带此 Cookie。
samesite 用来限制第三方 cookie,防止 CSRF 攻击,有三个属性:Strict(任何情况都不发送)、Lax(大多数情况不发送第三方 Cookie,但导航到目标网址的 Get 请求除外)、None(关闭限制)。
说说 jwt
JWT(JSON WEB TOKEN)是一个基于 JSON 的开放标准,用于通信双方之间传递安全信息的简洁的、URL 安全的表述性声明规范,经常用于身份验证。
JWT 使用数字签名的方式来保证信息的完整性和可靠性,可以用于身份认证和授权场景。例如在 Web 应用或 API 中,JWT 可以在客户端和服务器之间传递身份认证信息,以便服务器端验证用户的身份并进行相应的授权操作。
JWT 由三个部分组成:
- 头部(Header):描述 JWT 的元数据,例如使用的签名算法。
- 载荷(Payload):包含了 JWT 的声明,例如身份信息、权限信息等。
- 签名(Signature):使用私钥生成的数字签名,用于验证 JWT 的完整性。
格式:xxxxx.yyyyy.zzzzz,头部和载荷是 base64 编码的 JSON 字符串,三部分用点号分隔。
使用时通常将 JWT 发送给客户端存储在浏览器本地,客户端请求中携带 JWT 与服务器通信,服务器使用密钥验签确认完整性,解析声明信息进行授权操作。
前端登入注册的具体实现
- 在 HTML 中创建登录和注册表单,包括输入用户名和密码的输入框以及提交按钮。
- 使用 JavaScript 在前端验证表单输入的内容,例如验证用户名和密码的格式。
- 使用 AJAX 技术后端与服务器通信,发送登录或注册请求并处理响应。
- 在服务器端编写登录和注册的逻辑,处理请求并返回响应。
- 在前端处理服务器返回的响应,根据结果更新界面。
注意:实际应用中需要注意安全性,使用 HTTPS 保证通信安全,对密码加密存储,对表单输入进行防 XSS 安全措施。
输入 url 到页面渲染
dns 解析:DNS 是用于将域名解析为 IP 地址的协议。浏览器输入域名回车后:
- 浏览器在本地缓存中查找该域名的 IP 地址(浏览器缓存 → 操作系统缓存 → 本地 DNS 服务器缓存)。
- 本地缓存没有则向本地 DNS 服务器发送请求。
- 本地 DNS 服务器没有记录则向根域名服务器发送请求,根域名服务器把请求转发给对应的顶级域名服务器,再转发给权威域名服务器,最终返回完整信息。
一般用户向本地 DNS 服务器发送请求是递归查询(只需发出一次请求);本地 DNS 服务器向其他域名服务器请求是迭代查询(每次只返回单次查询结果,下一级由本地 DNS 服务器自己查询)。
之后还有 TCP 连接、发送 HTTP 请求、服务器处理返回、浏览器解析渲染页面。
浏览器
浏览器的渲染过程
- 解析 HTML,生成 DOM 树,解析 CSS,生成 CSSOM 树
- 将 DOM 树和 CSSOM 树结合,生成渲染树(Render Tree)
- Layout(回流):根据渲染树进行回流,得到节点的几何信息(位置、大小)
- Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
- Display:将像素发送给 GPU,展示在页面上
回流重绘
回流的代价大于重绘。
造成回流:改变宽高、内外边距,添加删除 DOM;查询一些属性如 offset、scroll;改变字体大小、激活伪类。
造成重绘:color、text-decoration 等。
如何优化回流和重绘:
- 使用 CSS3 动画:用 CSS3 动画代替 JavaScript 动画(例如 transform 和 opacity 属性),可以利用 GPU 加速,减少回流和重绘的开销。
- 避免频繁访问布局信息:避免在 JavaScript 中频繁访问元素的布局信息(offsetTop、offsetLeft、clientWidth、clientHeight 等),这会强制浏览器进行回流操作。
- 批量修改样式:对需要修改样式的元素进行批量操作,将多个样式的修改合并为一次操作。
- 使用 transform 属性:进行位移、缩放和旋转操作不会引起回流,只会触发重绘。
浏览器进程线程
浏览器包含的进程:
- 主进程:协调控制其他子进程(创建、销毁);浏览器界面显示、用户交互、前进后退收藏;将渲染进程得到的内存中的 Bitmap 绘制到用户界面上;处理不可见操作、网络请求、文件访问等。
- 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建。
- GPU 进程:用于 3D 绘制等。
- 渲染进程:即浏览器内核。负责页面渲染、脚本执行、事件处理等;每个 tab 页一个渲染进程。
渲染进程包含的线程:
- GUI 渲染线程:负责渲染页面、布局和绘制;页面需要重绘和回流时执行;与 JS 引擎线程互斥,防止渲染结果不可预期。
- JS 引擎线程:负责处理解析和执行 JavaScript 脚本程序;只有一个(单线程);与 GUI 渲染线程互斥。
- 事件触发线程:控制事件循环(鼠标点击、setTimeout、ajax 等);当事件满足触发条件时,将事件放入到 JS 引擎所在的执行队列中。
- 定时触发器线程:setInterval 与 setTimeout 所在的线程;定时任务并不是由 JS 引擎计时的,计时完毕后通知事件触发线程。
- 异步 http 请求线程:单独的线程处理 AJAX 请求;请求完成时若有回调函数,通知事件触发线程。
为什么 JavaScript 是单线程的:作为浏览器脚本语言,主要实现用户与浏览器的交互以及操作 DOM,这决定了它只能是单线程,否则会带来很复杂的同步问题。历史原因(创建 javascript 这门语言时多进程多线程架构并不流行);多线程的复杂性(需要加锁,编码复杂性增高);如果同时操作 DOM,多线程不加锁会导致 DOM 渲染结果不可预期。
为什么 GUI 渲染线程与 JS 引擎线程互斥:JS 可以操作 DOM,如果同时修改元素属性并同时渲染界面(JS 线程和 UI 线程同时运行),渲染线程前后获得的元素就可能不一致。因此浏览器设定两者互斥:JS 引擎线程执行时 GUI 渲染线程会被挂起,GUI 更新保存在队列中等待 JS 引擎线程空闲时立即执行。
主要区别:在 then 的第一个函数里抛出了异常,后面的 catch 能捕获到,而 then 的第二个函数捕获不到。
什么是 Event Loop
- JS 分为同步任务和异步任务
- 同步任务都在 JS 引擎线程上执行,形成一个执行栈
- 事件触发线程管理一个任务队列,异步任务触发条件达成,将回调事件放到任务队列中
- 执行栈中所有同步任务执行完毕,JS 引擎线程空闲,系统会读取任务队列,将可运行的异步任务回调事件添加到执行栈中开始执行
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染
- 渲染完毕后,JS 线程继续接管,开始下一个宏任务(从事件队列中获取)
宏任务主要包括:setTimeout、setInterval、setImmediate(node 的方法)、I/O。微任务主要包括:Promise.then、process.nextTick()(node)、MutationObserver。
事件循环(另一种表述):JS 引擎在执行代码时会产生执行栈,当调用异步 API(setTimeout、setInterval、Promise 等)回调触发时会进入异步任务队列;同步代码执行完成后,就会去异步队列取出回调函数并执行,这样形成了一个事件循环。
工作流程:主线程执行同步任务并将异步任务加入任务队列 → 主线程空闲时事件循环检查任务队列 → 有任务则加入执行栈执行 → 任务执行完毕后再次检查,直到任务队列为空。执行规则:同步代码直接进入主线程执行,JS 引擎判断主线程是否为空,为空则读取微任务 Event Queue 中所有的消息依次执行;主线程和微任务队列都为空后,读取宏任务队列中的第一个消息进入主线程执行,如此往复。
关键渲染路径
html → css → (DOM 树 + CSSOM 树 → 渲染树 → 布局 → 绘制)
前端安全(XSS、CSRF、SQL 注入)
前端安全是指保护网站或应用程序免受客户端攻击的措施。
XSS 攻击
XSS(跨站脚本攻击)是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如 cookie 等。本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起,浏览器无法分辨哪些脚本是可信的。
攻击者可以进行的操作:获取页面的数据(DOM、cookie、localStorage);DOS 攻击,占用服务器资源;破坏页面结构;流量劫持(将链接指向某网站)。
XSS 攻击通常注入的位置:用户输入字段(表单、搜索框、评论框);URL 参数;Cookie;HTTP 头部;内联脚本;第三方内容(广告、社交媒体小部件)。
如何防御:
- 对用户输入进行过滤和转义,防止恶意脚本被注入。
- 使用 CSP(内容安全策略),本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。
- 使用 HTTPOnly 标记的 Cookie,防止 Cookie 被窃取。
CSRF 攻击
CSRF(跨站请求伪造)指攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登录状态,攻击者就可以利用这个登录状态绕过后台验证,冒充用户执行操作。攻击者通常在其他网站上设置包含恶意请求的链接或表单,受害者点击时浏览器自动发送请求到目标网站。
CSRF 的本质是利用 cookie 会在同源请求中携带发送给服务器的特点,实现用户的冒充。
防御:
- 进行同源检测,使用请求中的 Referer 字段,与服务器期望的不同则拒绝请求。
- 在请求地址中添加 token 并验证:HTTP 请求中以参数形式加入随机产生的 token,服务器端拦截器验证,没有 token 或不正确则拒绝。
- 使用 SameSite 属性的 Cookie,限制 Cookie 的发送范围。
- 使用 HTTPS 加密通信,防止信息被窃取。
浏览器缓存
浏览器缓存就是将已经请求过的资源存储在本地上,再次请求的时候可以直接从本地获取,这样可以节省服务器的资源而且可以加快页面加载的速度。在服务器端设置响应头的 Cache-Control 和 Expires 字段来控制浏览器缓存(详见”协商缓存和强缓存”一节)。
前端埋点的实现
前端埋点是指在前端页面中加入用于监测用户行为的代码,这些代码可以收集用户的操作数据并发送到服务器端,帮助了解用户在使用网站或应用时的真实行为。
常见实现方式:
- 在页面中加入 JavaScript 代码监测用户行为,使用 XMLHttpRequest 或 fetch API 发送数据到服务器。
- 使用第三方的前端埋点工具,例如 Google Analytics、Mixpanel 等,提供简单的 API。
- 使用前端框架(如 React、Vue 等)的插件或扩展实现埋点功能;React 中可以用生命周期函数或自定义钩子函数监测用户行为(如 componentDidMount 中添加监测、componentWillUnmount 中发送数据)。
工程化与包管理
pnpm 跟 npm 和 yarn 最主要的区别
依赖安装方式:NPM 和 Yarn 都会在 node_modules 目录下为每个包单独安装一份依赖(扁平化提升),而 PNPM 共享依赖,将所有包的依赖都安装在全局目录(store)中,并通过符号链接(硬链接)的方式链接到各个包中。大大减少磁盘空间的占用,加快依赖的安装速度。
安装速度:PNPM 相对于 NPM 和 Yarn 更快一些,能够更高效地利用磁盘空间和缓存,减少重复下载和解压依赖的时间。
对于本地包的支持:npm 会将本地包的完整拷贝放置在项目的 node_modules 目录下;PNPM 通过符号链接的方式将本地包链接到项目中,全局目录安装一次即可在多个项目中共享使用,方便本地包的开发和测试。PNPM 在处理本地包依赖关系时,可以自动检测并安装本地包所依赖的其他包,而 NPM 需要手动将本地包的依赖添加到 package.json 文件中。
幽灵依赖:指在 package.json 文件中没有直接声明,但在项目中被使用的依赖。这种情况通常发生在 npm/yarn 扁平化安装后,项目使用了某个依赖的子依赖(被提升到 node_modules 根目录可以直接 import)。由于这些依赖没有在 package.json 中声明,包升级或移除后会导致项目无法正常运行。pnpm 默认严格隔离,杜绝幽灵依赖。
monorepo 和 multirepo 的区别
monorepo 是将多个项目放在同一个版本库中的组织方式,multirepo 是将每个项目都放在各自的版本库中。
- 协作方式不同:monorepo 所有项目共用同一个版本库,协作更方便;multirepo 每个项目有自己的版本库,协作需要更多协调工作。
- 依赖管理方式不同:monorepo 可以直接在项目之间使用内部依赖;multirepo 需要使用远程依赖或者打包发布。
- 测试和构建方式不同:monorepo 可以用单一的测试和构建工具管理所有项目;multirepo 每个项目可能有自己的测试和构建工具。
webpack loader plugin 都知道哪些
Loader 用于转换某些类型的模块,例如将 TypeScript 转换为 JavaScript 或将 SASS 转换为 CSS。常用 Loader:
- babel-loader:将 JavaScript 代码转换为浏览器可以理解的形式
- css-loader:加载并解析 CSS 文件
- style-loader:将 CSS 生成一个内容为最终解析完的 CSS 代码的 Style 标签,放到 head 标签里
- file-loader:加载并输出文件,例如图像或字体
- sass-loader:将 SASS/SCSS 转换为 CSS
- ts-loader:将 TypeScript 转换为 JavaScript
Plugin 用于执行自定义任务的扩展,例如打包优化、资源管理和注入环境变量。常用 Plugin:
- clean-webpack-plugin:清理输出目录
- html-webpack-plugin:生成 HTML 文件
- mini-css-extract-plugin:分离样式文件,把 CSS 提取为独立文件
- optimize-css-assets-webpack-plugin:压缩 CSS 文件
- webpack-bundle-analyzer:生成包的可视化报告
Webpack 的 loader 和 plugin 的区别
- Loader 本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。Loader 在
module.rules中配置,作为模块的解析规则,类型为数组,每一项是一个 Object,内部包含 test(类型文件)、loader、options(参数)等属性。 - Plugin 基于事件流框架 Tapable,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。Plugin 在
plugins中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数通过构造函数传入。
说说 webpack 的 tree shaking
Webpack 的 tree shaking 功能可以帮助你在打包的过程中移除没有使用到的代码,辅助减小最终的输出文件体积,提升应用的加载速度。
原理:去除代码冗余的过程中,程序从入口文件出发,扫描所有的模块依赖以及模块的子依赖,将它们链接起来形成一个”抽象语法树”(AST)。随后运行所有代码,查看哪些代码是用到过的做好标记,最后将 AST 中没有用到的代码”摇落”。
使用条件:需要使用 ES6 语法(import 和 export),在 webpack 配置中设置 mode: "production",并使用支持 tree shaking 的压缩工具(如 Terser/UglifyJS)。
注意:tree shaking 仅能移除没有被使用到的代码,而不能移除没有被执行的代码。如果未使用的函数被作为回调函数或 Promise 的 resolve 函数传入了其他函数,依然不会被移除。
css 预处理器的作用(sass、less)
增强了 CSS 语法,让你可以在 CSS 中使用变量、循环、嵌套、mixin、函数等功能。
ts 使用 / 说说 ts
TypeScript 是一种由微软开发的强类型编程语言,是 JavaScript 的超集。它增加了类型系统、接口、命名空间和类等特性,使得 JavaScript 代码更加类型安全,易于维护和扩展。
- 增强代码的可维护性,尤其在大型项目时效果显著
- 友好地在编辑器里提示错误,编译阶段就能检查类型发现大部分错误
- 支持最新的 JavaScript 新特性
- 周边生态繁荣,vue3 已全面支持 typescript
Git
Git 的操作?如何合并分支?git fetch 和 git pull 有什么区别?
Git 是一个版本控制系统,它允许你跟踪文件的更改,并在需要时回滚更改。常见操作:
- 克隆现有仓库:
git clone <repository> - 添加文件:
git add <file> - 提交更改:
git commit -m "message" - 推送更改:
git push - 检查状态:
git status - 创建分支:
git branch <branch name> - 切换分支:
git checkout <branch name> - 合并分支:先切换到要合并到的分支,然后
git merge <branch to merge> - 删除分支:
git branch -d <branch name> - 拉取更改:
git pull
git fetch 和 git pull 的区别:git fetch 从远程仓库获取最新版本,但不会自动合并到本地仓库,让你决定是否合并;git pull 是组合命令,包含 git fetch 和 git merge 的功能,获取最新版本后自动合并。
建议使用 git fetch 获取远程更改后手动合并,这样有更多的灵活性和控制,可以避免一些意外的合并冲突。
git 代码冲突解决
- 使用
git status检查冲突:冲突发生后查看冲突文件。 - 编辑冲突文件:打开冲突文件,在
<<<<<<< HEAD和>>>>>>> branch-name之间是你的更改和对方的更改,手动编辑保留认为正确的部分。 - 解决冲突:删除多余的标记(
<<<<<<<、=======、>>>>>>>),只保留正确的代码。 - 添加和提交:解决后将文件添加到暂存区并提交。
- 推送到远程仓库。
如何解决 git 冲突,merge 和 rebase 区别
- git merge:将两个分支的提交历史完整地合并到一起,并创建一个新的提交记录。合并后的提交历史可以很清楚地看到分支的合并关系,更简单常用。
- git rebase:把当前分支的提交记录取出来,放在另一个分支的最新提交之后,重新打包成新的提交记录,当前分支的提交记录会变成一条直线,历史更整洁。
冲突处理:merge 冲突时手动解决后提交;rebase 冲突时停止在冲突的地方,解决后 git add 标记已解决,然后 git rebase --continue 继续,想取消用 git rebase --abort。
CommonJS & ESModule
使用模块化可以带来以下好处:
- 解决命名冲突
- 提供复用性
- 提高代码可维护性
CommonJS 规范加载模块是同步的,只有加载完成,才能执行后面的操作,所以只适合用在服务端(Node.js);ES6 模块在浏览器端和服务端都可以使用(服务端需要遵循特殊规则,或用 Babel/Webpack 转码)。
CommonJS 规范中的 module、exports 和 require:
- 每个文件就是一个模块,有自己的作用域。每个模块内部,module 变量代表当前模块,是一个对象,它的 exports 属性(即 module.exports)是对外的接口。
- module.exports 属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取 module.exports 变量。
- 为了方便,Node 为每个模块提供一个 exports 变量,指向 module.exports:
let exports = module.exports。 - require 命令用于加载模块文件。
两者的核心区别:
- CommonJS 模块输出的是一个值的拷贝,而 ES6 模块输出的是值的引用(动态引用,真正取值的时候能取到最终的值)。
- CommonJS 模块是运行时加载,而 ES6 模块是编译时输出接口,使得对 JS 的模块进行静态分析成为可能(Tree Shaking 的基础)。
- 循环加载的表现不同:CommonJS 遇到循环依赖时只会输出已经执行的部分,后续的输出或变化不会影响已经输出的变量;ES6 模块使用 import 加载一个变量,变量不会被缓存。
- 模块顶层 this 指向:CommonJS 顶层 this 指向当前模块;ES6 模块中 this 指向 undefined。
- 互相引用:ES6 模块支持加载 CommonJS 模块,但 CommonJS 并不能 require ES6 模块,在 NodeJS 中两种模块方案是分开处理的。
Redux
数据如何通过 Redux 流动?
- 用户(通过 View)发出 Action,发出方式用 dispatch 方法。
- Store 自动调用 Reducer,传入两个参数:当前 State 和收到的 Action,Reducer 返回新的 State。
- State 一旦有变化,Store 就会调用监听函数,来更新 View。
Redux 由以下组件组成:
- Action:描述发生了什么事情的对象。
- Reducer:确定状态将如何变化的地方。
- Store:整个程序的状态/对象树保存在 Store 中。
- View:只显示 Store 提供的数据。
解释 Reducer 的作用
Reducers 是纯函数,它规定应用程序的状态怎样因响应 ACTION 而改变。Reducers 接受先前的状态和 action,然后返回一个新的状态。根据操作的类型确定需要执行哪种更新;如果不需要完成任务,会返回原来的状态。
redux 三大原则
- 单一数据源:整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
- State 是只读的:唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
- 使用纯函数来执行修改:为了描述 action 如何改变 state tree,需要编写 reducers。
范式化 state 可参考 Redux 官方文档 Normalizing State Shape。
杂项
服务稳定 & 降级方案
之前使用 CSR 加载页面的方式,资源加载方式简单,服务挂载到 Nginx 上就可以运行,基本无需担心服务的稳定性。切换到服务端渲染后,新增了服务端的 Node 层,需要通过以下方式确保服务可用性以及可降级为 CSR 渲染的能力。
Node 服务挂掉时,整体降级:
- 在 bpm-approval-redirect 项目内,通过 Kconf 配置降级为 CSR 渲染
- Nginx 层处理降级
单次请求页面异常:
- Next.js 框架处理:当服务端渲染有异常报错时(包括但不限于接口、逻辑异常),页面会直接停止服务端渲染切换为客户端渲染
SSR 和 CSR 的区别
SSR 优点:首屏渲染,安全性,SEO,可访问性。
SSR 缺点:额外的开发要求,服务器压力。
react 路由模块的原理
监听 URL 变化(hash 的 hashchange 事件 / history 的 popstate 事件),匹配路由配置渲染对应组件,配合 History API(pushState/replaceState)实现不刷新页面的导航。
深浅拷贝
浅拷贝只复制对象第一层(Object.assign、展开运算符、slice),嵌套的引用类型仍共享;深拷贝完全独立(递归、structuredClone、JSON.parse(JSON.stringify)——后者有诸多缺点见前文)。
跨页面同步播放音乐(场景)
页面 1 与页面 2 通信方式任选(postMessage / localStorage + storage 事件 / BroadcastChannel),页面 2 监听消息后同步播放器状态。
长列表渲染实现
在代码中通过创建一个容器元素来显示数据列表,并通过监听容器元素的滚动事件来动态更新可见区域的数据。同时根据可见区域的高度和滚动距离计算出当前可见区域的起始和结束元素下标,以便只渲染当前可见区域的数据。
长列表渲染和虚拟滚动都是在滚动时只渲染当前可见区域的数据,避免将整个列表都渲染出来,从而提高性能。其中,长列表渲染通常是通过设置容器元素的高度和滚动事件来实现的,而虚拟滚动则是将整个列表分成若干个块,只渲染当前可见的块,并且在滚动时动态更新块的内容。
懒加载则是一种在用户需要时才加载数据的技术,可以用于优化页面加载速度。例如在图片展示中,可以只加载当前可见区域的图片。
虚拟列表和懒加载可以结合使用:在一个虚拟滚动的长列表中,采用懒加载的方式,只在用户滚动到一定区域时才加载该区域的数据,避免一次性加载所有数据造成的性能问题,同时节省带宽和资源开销。
实现大量数据渲染
分页、懒加载、定时器(分批渲染,requestAnimationFrame 分帧插入 DOM)。
为什么要做这些项目
设备兼容问题解决:rem/vw 适配、媒体查询、polyfill、浏览器兼容测试。
手写代码汇总
- 手写发布订阅(on/emit/off/once)
- 手写洗牌算法(Fisher-Yates)
- 手写遍历 DOM 所有节点返回标签名
- 手写一个轮播图
- 手写深拷贝(处理循环引用用 WeakMap 缓存)
- 防抖节流
待补充问题清单(面试被问过,还没整理答案)
- git fetch 和 pull 的区别(已整理见上)
- hook 怎么模拟生命周期(useEffect 清理函数)
- 状态码 301 302(已整理见上)
- ts 使用(见上)
- 响应式布局(见上)
- 关键渲染路径(见上)
- axios 封装:拦截器(请求拦截加 token、响应拦截统一错误处理)、实例配置、取消请求、重试机制、统一 loading。
- 复杂度分析:时间复杂度大 O 表示法、常见排序/查找复杂度。
- generator 说协程(见上)
- 怎么同步拿到 useState(见上)
- hook 模拟生命周期(见上)
- 微前端解决了什么问题(增量升级)
- 微前端的实现方式
- 多准备一下项目
- 聊聊重构收益(见上)
- 服务稳定 & 降级方案(见上)
- 移动端 1px、0.5px 线(见上)
- B 站弹幕不挡人物(见上)
- instanceof 判断不了的(见上)
- 和 = 哪个比较快(=== 更快,省去类型转换)
- 数组的方法哪些 es6 新增:map、filter、reduce、find、findIndex、includes、fill、flat、flatMap、from、of、entries/keys/values、扩展运算符等。
- flex: 1(见上)
- bind apply call 区别(见下)
- 说说 httponly(见上)
- http 和 https(见上)
- TCP UDP(见上)
- bigint proxy reflect(见上)
- 0.5px 的线如何实现(见上)
- css 省略号(见上)
- HTML5 语义化标签及为何要使用:header、nav、main、article、section、aside、footer 等;利于 SEO、可访问性(屏幕阅读器)、代码可读性维护性。
- 如何实现同源下多页面通信(见 2 个页面之间通信)
- react 怎么做 diff(见 React Diff)
- 什么时候会触发预检请求(见跨域)
- 深浅拷贝(见上)
- 自定义 hook(见上)
- useEffect 中模拟卸载、第二个参数为空数组(见上)
- http 的无状态含义(见上)
- 对 JSON 的理解(见上)
- 浏览器的渲染过程(见上)
- 回流重绘(见上)
- 浏览器进程线程(见上)
- 为什么 JavaScript 是单线程的(见上)
- 什么是 Event Loop(见上)
- HTTPS 和 HTTP 的区别(见上)
- redux 相关(见上)
- react18 和 17、16 有啥区别(见上)
- 架构演进(见上)
- 和 = 有什么区别(见上)
- 对 AJAX 的理解(见上)
- z-index 属性在什么情况下会失效(见上)
- redux 三大原则(见上)
- 多准备一下项目(面试前过一遍项目细节、亮点、难点、量化数据)
- 怎么进行异步编程(见上)
- 怎么捕获 await async 的错误(见上)
- js 数组方法改变/不改变原数组(见上)
bind apply call 区别
作用:都可以改变函数内部的 this 指向。
区别点:
- call 和 apply 会调用函数,并且改变函数内部 this 指向。
- call 和 apply 传递的参数不一样:call 传递参数 arg1, arg2… 形式,apply 必须数组形式 [arg]。
- bind 不会调用函数,可以改变函数内部 this 指向(返回新函数)。