前端面试必备:JS 进阶知识点大揭秘

160 阅读13分钟

一、异步

1. 回调函数

回调函数是最早用来处理异步操作的办法,不过它有个大问题,容易写出回调地狱,代码可读性和维护性都变得很差。

2. Promise

ES6 引入的 Promise 解决了回调地狱这个麻烦。then 方法会返回一个新的 Promise,它的状态是根据上一个 Promise 的状态来定的。

function A() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('yes');
        }, 1000);
    });
}
function B() {
    console.log('B');
}
A().then(() => B()); // yes B

3. Generator

Generator 函数靠 * 和 yield 关键字来实现异步控制,它返回一个迭代器对象。

迭代器(Iterator)是一种特殊对象,它提供了一种统一的方式来遍历可迭代对象(如数组、字符串、Set、Map 等)中的元素。迭代器对象有一个 next() 方法,每次调用该方法时,它会返回一个包含两个属性的对象:value 和 donevalue 是当前迭代的值,done 是一个布尔值,表示迭代是否结束。

// 定义一个 Generator 函数 foo,接收一个参数 x
function *foo(x) {
    // 第一次调用 yield,暂停函数执行,并返回 x + 1 的值
    let y = 2 * (yield (x + 1));
    // 第二次调用 yield,暂停函数执行,并返回 y / 3 的值
    let z = yield (y / 3);
    // 返回最终结果 x + y + z
    return (x + y + z);
}

// 调用 Generator 函数,返回一个迭代器对象 it
let it = foo(5);

// 第一次调用 next() 方法,开始执行 Generator 函数,直到遇到第一个 yield
// 返回 { value: 6, done: false },value 是 yield 后面表达式的值,done 表示迭代未结束
console.log(it.next()); 

// 第二次调用 next() 方法,继续执行 Generator 函数,将传入的参数 12 赋给上一次 yield 的返回值
// 计算 y = 2 * 12 = 24,然后遇到第二个 yield,返回 { value: 8, done: false }
console.log(it.next(12)); 

// 第三次调用 next() 方法,继续执行 Generator 函数,将传入的参数 13 赋给上一次 yield 的返回值
// 计算 z = 13,然后执行 return 语句,返回 { value: 42, done: true },done 表示迭代结束
console.log(it.next(13)); 

在这个示例中,我们定义了一个 Generator 函数 foo,并调用它得到一个迭代器对象 it。通过多次调用 it.next() 方法,我们可以逐步执行 foo 函数中的代码,并在每次遇到 yield 时暂停执行。每次调用 next() 方法时传入的参数会作为上一次 yield 表达式的返回值。最终,当 Generator 函数执行完毕时,next() 方法返回的对象的 done 属性为 true,表示迭代结束 。

4. Async/Await

async/await其实是基于Promise的一种异步编程语法糖,用它写异步代码,看起来就跟同步代码差不多。

// 定义一个异步函数 A
async function A() {
    // 等待异步函数 B 执行完毕
    await B();
    // 当 B 函数执行完毕后,打印 2
    console.log(2);
}

// 定义一个异步函数 B
async function B() {
    console.log(3);
}

// 调用异步函数 A
A(); // 3 2

二、Event Loop

  • 线程之间可以同时工作,除了 js 线程浏览器渲染线程 。因为 js 可以修改 dom,可能会出现渲染冲突(js 的执行会阻塞 html)

  • 同步队列:

  • 异步队列:

    • 宏任务队列:同步代码的第一次执行也叫宏任务
    • 微任务队列: promise.then 、 MutationObserver(监听dom结构) 、 node环境独有:process 、 nextTick

JavaScript 是单线程的,得靠 Event Loop 来实现异步操作。执行顺序是这样的:

  1. 先执行同步代码(这属于宏任务)。
  1. 然后处理微任务队列里的任务。
  1. 要是有必要,就执行渲染。
  1. 接着再处理宏任务。
console.log('1');
setTimeout(() => {
    console.log('2');
    Promise.resolve().then(() => {
        console.log('3');
    });
}, 0);
Promise.resolve().then(() => {
    console.log('4');
    setTimeout(() => {
        console.log('5');
        Promise.resolve().then(() => {
            console.log('6');
        });
    }, 0);
});
console.log('7');
// 输出顺序:1 7 4 2 3 5 6

三、浏览器基础考点

1. 事件机制

事件传播三阶段:

捕获阶段(从外到内)、目标阶段(事件目标)、冒泡阶段(从内到外)

  • 第三个参数默认为 false
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>事件机制演示</title>
    <style>
        /* 三个嵌套的盒子样式 */
       .box1{ width:500px; height:500px; background:red; }
       .box2{ width:300px; height:300px; background:green; }
       .box3{ width:100px; height:100px; background:blue; }
    </style>
</head>
<body>
    <!-- 嵌套的三层盒子结构 -->
    <div class="box1">
        <div class="box2">
            <div class="box3"></div>
        </div>
    </div>
    <script>
        // 获取三个盒子元素
        const box1 = document.querySelector('.box1');
        const box2 = document.querySelector('.box2');
        const box3 = document.querySelector('.box3');
        
        // 事件监听设置:
        // 1. box1在捕获阶段触发
        box1.addEventListener('click', () => {
            console.log('box1 red'); // 捕获阶段触发
        }, true);

        // 2. box2在冒泡阶段触发
        box2.addEventListener('click', () => {
            console.log('box2 green'); // 冒泡阶段触发
        }, false);

        // 3. box3在冒泡阶段触发
        box3.addEventListener('click', () => {
            console.log('box3 blue'); // 目标阶段触发
        }, false);
    </script>
</body>
</html>

image.png

动画.gif

所以当点击最内层的蓝色盒子(box3)时:

box1 red(捕获阶段)

box3 blue(目标阶段)

box2 green(冒泡阶段)

2. 事件委托

事件委托就是把事件监听器加到父元素上,利用事件冒泡的原理,来处理多个子元素的事件。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>5</li>
    </ul>
    <script>
        // 给父元素ul添加一个监听器
        let ul = document.querySelector('ul');
        ul.addEventListener('click', function(e) {
            // e.target就是被点击的子元素(li)
            console.log(e.target.innerText);
        });
    </script>
</body>
</html>

1. 减少内存占用

// 传统方式(每个子元素绑定事件)
lis.forEach(li => li.addEventListener('click', handler));

// 事件委托(父元素统一处理)
ul.addEventListener('click', handler);

优势:当子元素数量多(如 1000 个 li)时,内存占用从 1000 个监听器减少到 1 个。

2. 自动支持动态内容

想象餐厅服务员(父元素)负责上菜,无论厨房新增多少道菜(动态子元素),服务员都能自动处理,无需重新培训每个厨师(绑定事件)。

// 新增动态元素后自动生效
ul.innerHTML += '<li>6</li>'; // 传统方式需要重新绑定,事件委托无需操作

优势:动态添加的子元素(如通过 AJAX 加载的数据)无需重新绑定事件。

3. 代码简洁统一

4. 性能优化

3. 跨域 存储 缓存

1. 跨域

详细可看:跨域问题全解析:七种方法轻松拿捏跨域

是因为浏览器有个同源策略(协议域名端口号),从一个域名的网页去请求另一个域名的资源就会受限。

跨域方案

方案核心原理优点缺点典型场景复杂度兼容性安全性
JSONPscript 标签跨域加载兼容性好,简单易用仅 GET,XSS 风险简单数据请求
CORS服务端响应头控制支持全方法,标准方案需服务端配置,预请求前后端分离项目现代
反向代理中间服务器转发请求前端透明,支持复杂请求需要额外服务器企业级项目
WebSocket独立协议长连接实时双向通信协议不同,需单独实现聊天 / 推送现代
postMessage窗口间通信 API安全可控仅限窗口通信iframe 交互现代
Node 代理开发环境中间件转发开发调试方便生产环境不适用前端开发阶段现代
document.domain主域名设置历史方案受浏览器限制,不安全已废弃

常见的解决方案有:CORS 适用于需要支持多种 HTTP 方法(如 GET、POST、PUT 等)的现代 Web 应用,nginx 反向代理适用于前后端分离的项目,可以在服务器层面统一处理跨域问题。

2. 存储

存储类型容量生命周期作用域核心特点典型场景
Cookie约 4KB可设置过期时间(默认会话级)同源 + 指定路径自动随 HTTP 请求发送,支持 Secure/HttpOnly/SameSite用户会话管理、跟踪行为
LocalStorage5-10MB永久(需手动清除)同源窗口共享同步 API,数据不参与 HTTP 通信用户偏好、离线缓存
SessionStorage5-10MB窗口关闭即清除当前标签页 / 窗口同步 API,新开窗口不共享临时表单、单页应用会话状态
IndexedDB无固定上限永久同源共享异步 API,支持复杂查询和事务,存储二进制数据大量数据存储(离线应用、媒体缓存)

3. 缓存

缓存类型控制方式是否与服务器交互优先级典型应用
强缓存Cache-Control/Expires长期不变的静态资源
协商缓存ETag/Last-Modified是(验证)可能更新的动态资源
Service Worker自定义脚本可拦截 / 控制灵活离线应用、复杂缓存策略

4. 渲染过程

1. 解析 HTML 得到 DOM 树

浏览器拿到 HTML 文档后,就开始一行一行解析。碰到标签,就创建对应的 DOM 节点对象,最后搭建成一棵 DOM 树。

2. 解析 CSS 得到 CSS 规则树

与此同时,浏览器解析 CSS 样式表,把 CSS 规则变成它能懂的内部形式,形成 CSS 规则树。像div { color: red; },就会生成一条针对div元素的样式规则,里面有颜色属性color和值red,好多这样的规则组成 CSS 规则树。

3. 将 DOM 树和 CSS 规则树合并成 Render 树(只包含可见的节点)

把 DOM 树和 CSS 规则树结合起来,按照 CSS 规则给 DOM 树里每个节点算最终样式,生成 Render 树。像设置了display: none的元素,就不会出现在 Render 树里,因为它看不见。

4. 根据 Render 树计算每个节点的位置 (回流)

计算 Render 树里每个节点在页面上的几何位置,像元素的宽度、高度、位置这些。要是页面布局变了,比如说元素的width、height、margin、padding这些几何属性改了,就会触发回流。回流这个操作挺耗性能的,因为得重新算页面上所有元素的位置和大小。

5. 将每个节点绘制到屏幕上 (重绘)

等每个节点的样式和位置都确定好了,浏览器就按照顺序把这些节点画到屏幕上,这就是最终看到的页面。要是元素的外观属性,像color、background-color、border-style这些变了,就会触发重绘。重绘相对回流来说,开销小一点,但也会影响性能。

image.png

1. 回流重绘

回流:当元素几何属性(如位置、尺寸、边距、字体大小等)改变,或触发布局变动时,浏览器需重新计算元素布局,更新渲染树结构,此过程为回流。例如:通过 JavaScript 修改元素 width 属性,或调整窗口大小,均会触发回流,其计算成本高,对页面性能影响较大。

重绘:若元素外观属性变化(如颜色、背景、阴影、透明度等),但不影响布局,浏览器重新绘制元素外观的过程即重绘。如修改 color 或 background-color,仅触发重绘,开销相对回流较小,但大量重绘仍会影响性能。

2. 浏览器的优化机制:队列处理

浏览器通过队列机制优化回流重绘:

  • 批量处理:将多次回流重绘操作存入队列,待操作数量达标或时间阈值到达,统一执行,降低渲染损耗。
  • 强制刷新:读取布局属性(如 offsetTopclientWidthscrollHeight 等)或调用 getComputedStyle() 时,会强制刷新队列,立即执行所有待处理操作,可能影响性能。

3. 减少回流重绘的优化策略

1. 批量样式修改

反例:逐行修改样式,频繁触发回流重绘。

element.style.color = 'red';  
element.style.fontSize = '16px';  

优化:通过 CSS 类批量应用样式。

.optimize-style {  
    color: red;  
    font-size: 16px;  
}  
element.className = 'optimize-style';  
2. 脱离文档流操作

对需大量回流的元素,先脱离文档流,完成操作后再回归。

应用场景:复杂动画、大规模 DOM 修改。

示例

// 脱离文档流  
element.style.position = 'absolute';  
// 执行复杂操作  
// ...  
// 恢复原布局  
element.style.position = 'static';  
3. 利用文档碎片(DocumentFragment)

文档碎片是内存中的轻量级 DOM 容器,操作时不触发页面回流。

示例:批量添加列表项。

const fragment = document.createDocumentFragment();  
for (let i = 0; i < 10; i++) {  
    const li = document.createElement('li');  
    li.textContent = `Item ${i}`;  
    fragment.appendChild(li);  
}  
document.getElementById('myList').appendChild(fragment);  
4. 克隆 DOM 操作

克隆元素,在副本上操作,最后替换原元素,减少对真实 DOM 的直接操作。

示例:修改复杂表格。

const originalTable = document.getElementById('table');  
const cloneTable = originalTable.cloneNode(true);  
// 在克隆表格上操作:添加行、修改数据等  
 
originalTable.parentNode.replaceChild(cloneTable, originalTable);  
5. 读写分离

避免混合读写布局属性,先集中写操作,再统一读操作。

反例:读写混合,强制刷新队列。

element.style.width = '200px';  
const height = element.offsetHeight;  
element.style.height = '300px';  

优化:先写后读。

element.style.width = '200px';  
element.style.height = '300px';  
const width = element.offsetWidth;  
const height = element.offsetHeight;  
6. 硬件加速

利用 CSS 属性触发 GPU 加速,减少回流重绘。

示例

.accelerate-element {  
    transform: translateZ(0); /* 启用 GPU 加速 */  
    will-change: transform; /* 提示浏览器优化 */  
}  

四、设计模式 - 发布订阅模式

发布订阅模式在 JavaScript 开发里用得挺多,它定义了一种一对多的依赖关系,就是好多订阅者对象同时监听一个发布者对象。发布者对象状态一变,就通知所有订阅者对象,让它们自动更新。

这种模式的核心优势在于:

  • 解耦性:发布者和订阅者无需直接关联,只需通过事件名称进行交互
  • 扩展性:新增订阅者或修改发布者逻辑时无需改动另一方代码
  • 异步通信:事件触发与处理可以异步执行,提升系统响应能力

1. 核心组件解析

1. 发布者(Publisher)

  • 负责触发特定事件
  • 不关心谁在监听这些事件
  • 示例:DOM 元素的事件触发机制

2. 订阅者(Subscriber)

  • 注册监听特定事件
  • 接收事件通知并执行回调
  • 示例:JavaScript 中的事件监听函数

3. 事件中心(Event Bus)

  • 维护事件与订阅者的映射关系
  • 提供订阅、取消订阅和触发事件的接口
  • 示例:Vue.js 的EventEmitter或 React 的自定义事件系统

2. 基础实现与代码解析

class EventEmitter {
  constructor() {
    this.events = {}; // 存储事件与回调的映射
  }

  // 订阅事件
  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }

  // 触发事件
  emit(eventName, ...args) {
    this.events[eventName]?.forEach(callback => {
      callback(...args);
    });
  }

  // 取消订阅
  off(eventName, callback) {
    if (!this.events[eventName]) return;
    this.events[eventName] = this.events[eventName]
      .filter(cb => cb !== callback);
  }

  // 单次订阅
  once(eventName, callback) {
    const wrapper = (...args) => {
      callback(...args);
      this.off(eventName, wrapper);
    };
    this.on(eventName, wrapper);
  }
}

关键特性说明:

1. 多事件管理:通过对象存储不同事件的回调列表

2. 参数传递:支持事件触发时传递任意参数

3. 取消订阅:提供 off 方法避免内存泄漏

4. 单次触发:once 方法实现只执行一次的订阅

3. 典型应用场景

1. 前端框架中的组件通信

  • React:通过 Context API + 自定义事件实现跨层级通信
  • Vue:早期版本的Event Bus模式
  • 示例代码:
// 父子组件通信
const emitter = new EventEmitter();

// 子组件触发事件
emitter.emit('update', { data: 'new value' });

// 父组件监听事件
emitter.on('update', (payload) => {
  console.log('Received:', payload.data);
});

2. 异步编程中的事件驱动

  • 网络请求完成通知
  • 文件上传进度监听
  • 实时数据更新推送

3. 游戏开发中的事件系统

  • 碰撞检测事件
  • 角色状态变更通知
  • 用户输入响应

4. 与观察者模式的区别

特性发布订阅模式观察者模式
解耦程度完全解耦(通过事件中心)部分解耦(主题与观察者直接关联)
事件触发方式主动触发 emit 方法状态变化自动通知
应用场景跨系统通信单一系统内的状态同步
典型实现消息队列(RabbitMQ)Vue 的响应式系统