Skip to content

深挖技术

深挖技术

考点一:宏任务、微任务与 requestAnimationFrame 的“三角恋”关系

面试官常问:“setTimeout(()=>{}, 0)Promise.resolve().then()requestAnimationFrame,谁先执行?”

  • 核心认知

    • 微任务(Promise.then):在本轮宏任务结束前清空(优先级最高)。
    • 渲染(requestAnimationFrame):在微任务清空后、浏览器即将进行屏幕绘制(Paint)之前执行。
    • 宏任务(setTimeout):在下一次事件循环(新一轮)开始时执行。
  • 执行铁律同步代码 > 微任务 > (rAF) > 渲染 > 宏任务

  • 代码验证(深水区)

    javascript
    console.log('1. 同步');
    setTimeout(() => console.log('5. 下一轮宏任务'), 0);
    Promise.resolve().then(() => console.log('2. 微任务'));

requestAnimationFrame(() => { console.log('3. 渲染前(帧开始)'); // 注意:浏览器会在执行完所有 rAF 回调后才进行 Paint });

console.log('4. 同步结束'); // 输出顺序:1 -> 4 -> 2 -> 3 -> 5

- **业务结合**:“在 WeLink 消息滚动加载时,为了防止频繁触发布局抖动,我会把 DOM 批量更新的逻辑放在 `requestAnimationFrame` 里,而不是 `setTimeout`,因为 rAF 能确保更新在本次渲染帧内完成,不会造成丢帧。”

---

### 考点二:`new` 操作符的“四步手术刀”与手写实现

面试官会让你现场手写 `new`,并追问:“如果构造函数返回了一个对象,`new` 的结果是什么?”

- **四步原理**:
  1. 创建一个空对象 `obj`。
  2. 将 `obj` 的 `__proto__` 指向构造函数的 `prototype`(原型继承)。
  3. 执行构造函数,将 `this` 绑定到 `obj` 上(传参赋值)。
  4. **判断返回值**:如果构造函数显式返回了一个**对象**(或函数),则返回该对象;否则返回新创建的 `obj`。
- **手写代码**:
```javascript
function myNew(constructor, ...args) {
  // 1 & 2: 创建对象并链接原型
  const obj = Object.create(constructor.prototype);
  // 3: 绑定 this 并执行
  const result = constructor.apply(obj, args);
  // 4: 判断返回值(如果是对象/函数则返回它,否则返回新对象)
  return (typeof result === 'object' && result !== null) || typeof result === 'function' 
          ? result : obj;
}
// 测试
function Person(name) { this.name = name; return {}; } // 返回空对象
const p = myNew(Person, 'Tom'); 
console.log(p instanceof Person); // false(因为构造器返回了对象,打断了原型链)

考点三:垃圾回收(GC)与“闭包内存泄漏”的真实场景

面试官不考定义,他给一段代码问:“count 变量会不会被释放?为什么?”

  • 核心认知:闭包引用的变量,如果被外部函数持有引用,不会被标记清除(Mark-and-Sweep)

  • 代码陷阱(重点!)

    javascript
    function outer() {
    let bigData = new Array(1000000).fill('data'); // 大内存对象
    return function inner() {
      // 虽然 inner 只用了 `bigData.length`,但闭包持有整个 `bigData` 对象的引用!
      console.log(bigData.length);
    }
    }
    const fn = outer(); // 只要 fn 存在,bigData 就永远活在内存里,无法回收!
  • 解决方案(进阶回答):在 outer 最后手动释放 bigData = null,或者利用 WeakMap / WeakSet(弱引用,不影响 GC)。

  • 业务结合:“在 IAMP 点云平台,Three.js 的场景对象如果没在组件卸载时调用 dispose(),就会因为闭包引用导致显存泄漏。我利用 WeakMap 缓存了渲染器实例,当 DOM 元素被移除时,自动触发 GC 回收。”


考点四:typeofinstanceof 的“类型底裤” + Object.prototype.toString

面试官问:“typeof null 为什么是 object?怎么判断一个对象是不是数组?”

  • typeof 原理:它是基于 JS 底层类型标签(Type Tag) 判断的。null 的机器码标签是 0(指向空对象指针),所以被误判为 object
  • 终极判断方法
    • Object.prototype.toString.call(obj):这是最准的“照妖镜”。

      javascript
      Object.prototype.toString.call(null);      // "[object Null]"
      Object.prototype.toString.call([]);        // "[object Array]"
      Object.prototype.toString.call(new Date()); // "[object Date]"
      // 面试手写万能类型检测
      function getType(obj) {
      return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
      }

考点五:浏览器跨域(CORS)的“预检请求(OPTIONS)”机制

面试官追问:“为什么有时候 POST 请求会发两次,有时候只发一次?”

  • 分类

    • 简单请求(只发一次):GET/HEAD/POST,且 Content-Type 为 text/plainapplication/x-www-form-urlencodedmultipart/form-data
    • 非简单请求(发两次):带自定义头(如 X-Token)、Content-Type: application/json
  • 预检(OPTIONS)机制:浏览器先发一个 OPTIONS 请求问服务器:“我能不能跨域?允许哪些请求头?” 服务器返回 Access-Control-Allow-* 后,浏览器才发真正的 POST

  • 深水区代码(后端 Nginx 配置)

    nginx
    # 处理 OPTIONS 预检请求
    if ($request_method = 'OPTIONS') {
      add_header 'Access-Control-Allow-Origin' '*';
      add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
      add_header 'Access-Control-Allow-Headers' 'DNT,X-Token,Content-Type';
      return 204; # 无内容返回,告诉浏览器 OK
    }

考点六:ES Module 的“静态分析”与 CommonJS 的“运行时”差异

面试官问:“为什么 import 必须写在顶部,而 require 可以写在 if 里?”

  • CommonJS(Node.js)运行时加载require 本质是拷贝(浅拷贝)。它在代码执行到这一行时才去读取文件,返回的是一个对象副本。

  • ES Module(浏览器)编译时加载(静态)。JS 引擎在代码执行前(预编译阶段)就会解析 import,建立只读引用(Live Binding)。因此它不能放在 if 里(条件语句在运行时才有值)。

  • 代码深水区(区别巨大!)

    javascript
    // ----- CommonJS (node) -----
    // counter.js
    let count = 1;
    module.exports = { count, add: () => count++ };

// main.js const obj = require('./counter.js'); console.log(obj.count); // 1 obj.add(); console.log(obj.count); // 1 (拷贝的值,没有变!)

// ----- ES Module (浏览器) ----- // counter.mjs export let count = 1; export const add = () => count++;

// main.mjs import { count, add } from './counter.mjs'; console.log(count); // 1 add(); console.log(count); // 2 (实时引用,变了!)


- **业务结合**:“在 WeLink 打包时,我利用 ES Module 的静态特性做 **Tree Shaking**(摇树),移除未使用的代码;而 Node.js 后端因为需要动态加载配置文件(如根据环境变量 `NODE_ENV` 切换),则必须保留 `require`。”

---

### 🚀 终极面试话术(把这些点串起来)

如果面试官最后问:“你刚讲了很多底层,你觉得这些对你日常开发有什么实际意义?”

> “**面试官,了解这些实现,让我能做到‘知其然更知其所以然’。**
> 
> 1. **排查 Bug**:当页面卡顿时,我脑子里会出现 **Event Loop** 的流水线,立刻判断是宏任务阻塞了渲染,还是微任务积压。
> 2. **性能优化**:写 **虚拟列表** 时,我刻意用 `requestAnimationFrame` 配合 `transform` 触发 GPU 合成,因为这能绕过 **重排(Reflow)** 的底层限制。
> 3. **内存管理**:封装组件时,我习惯用 **`WeakMap`** 或 `dispose` 模式,因为我深知 **GC(垃圾回收)** 标记清除的机制,必须手动断开闭包对大对象的引用。
> 
> **一句话:底层原理决定了我能写出多久不卡顿、多大规模不崩溃、多高复杂度不混乱的代码。**”

把这六个考点的代码在 IDE 里敲一遍,二面无论怎么深挖,你都能对答如流!加油!
最近更新