目录


1. JavaScript 原型,原型链

核心概念

JavaScript 是基于原型(Prototype)的面向对象语言,而非传统的类继承,核心是「原型对象」和「原型链」:

(1)原型(Prototype)
  • 每个函数(除箭头函数)都有 prototype 属性(原型对象),默认包含 constructor(指向函数本身)和 __proto__(指向父级原型)
  • 每个实例对象都有 __proto__ 属性(隐式原型),指向其构造函数的 prototype
  • 实例可访问原型上的属性 / 方法,实现「属性复用」
(2)原型链
  • 当访问对象的属性 / 方法时,JS 先在对象自身查找,找不到则通过 __proto__ 向上查找父级原型,直到 Object.prototype(顶端),这个链式结构就是「原型链」
  • Object.prototype.__proto__ === null(原型链终点)
  • 作用:实现继承(如原型继承、组合继承),属性共享

示例

function Person(name) {
  this.name = name;
}

Person.prototype.sayHi = function() { // 原型上的方法
  console.log(`Hi, ${this.name}`);
};

const p1 = new Person('张三');
p1.sayHi(); // 自身无sayHi,通过__proto__找到原型上的方法

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

原型链图示

p1 (实例)
  __proto__ → Person.prototype
                __proto__ → Object.prototype
                              __proto__ → null

2. 事件代理是什么?冒泡和捕获?

(1)事件代理(事件委托)

核心:利用事件冒泡,将子元素的事件绑定到父元素上,由父元素统一处理

优势

  • 减少事件绑定数量(优化性能)
  • 支持动态新增子元素(无需重新绑定)

场景:列表项点击、动态生成的 DOM 元素事件处理

(2)事件流(冒泡 + 捕获)

JS 事件触发分为三个阶段:

  1. 捕获阶段:事件从 window 向下传播到目标元素(从上到下)
  2. 目标阶段:事件到达目标元素
  3. 冒泡阶段:事件从目标元素向上传播回 window(从下到上)

事件冒泡:默认阶段,事件从目标元素向上冒泡到父级 / 祖先元素(如点击按钮 → 触发按钮事件 → 触发父 div 事件 → 触发 body 事件)

事件捕获:需通过 addEventListener 第三个参数 useCapture: true 开启,事件从顶层向下捕获到目标元素

示例(事件代理)

<ul id="list">
  <li>项1</li>
  <li>项2</li>
</ul>

<script>
const list = document.getElementById('list');

list.addEventListener('click', (e) => {
  if (e.target.tagName === 'LI') { // 判断目标元素
    console.log('点击了', e.target.textContent);
  }
});

// 动态新增li仍能触发事件
const newLi = document.createElement('li');
newLi.textContent = '项3';
list.appendChild(newLi);
</script>

事件流图示

捕获阶段:window → document → html → body → div → button
目标阶段:button (事件触发)
冒泡阶段:button → div → body → html → document → window

3. 如何解决跨域问题?

跨域:浏览器因「同源策略」(协议、域名、端口一致)限制,禁止不同源的 AJAX 请求 / 资源访问。

常见解决方案对比

方案 原理 / 适用场景 优缺点
CORS(跨域资源共享) 后端在响应头配置 Access-Control-* 标准方案,支持所有请求类型;需后端配合
JSONP 利用 <script> 标签无跨域限制,回调传数据 仅支持 GET;易受 XSS 攻击
代理服务器 前端请求同源代理,代理转发到目标服务器 无跨域限制;需部署代理
WebSocket 双向通信协议,无同源限制 适用于实时通信;需服务端支持
postMessage 窗口间通信 API,可跨域传数据 适用于 iframe / 多窗口通信

示例(CORS,后端 Node.js)

// 后端配置响应头
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*'); // 允许所有源(生产环境指定具体域名)
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
  next();
});

JSONP 示例

// 前端
function jsonp(url, callback) {
  const script = document.createElement('script');
  script.src = `${url}?callback=${callback}`;
  document.body.appendChild(script);
}

// 后端返回:callback({ data: 'xxx' })

4. for 循环和 forEach 循环,哪个性能更好?

结论for 循环性能 > forEach(及 for...of/for...in

核心原因

  • for 循环:纯语法结构,无额外函数调用 / 上下文开销,可通过 break/continue 中断,执行效率最高
  • forEach:数组原型方法,本质是「遍历 + 回调函数」,每次迭代需创建回调函数上下文、处理 this 绑定,且无法中断(return 仅跳过当前迭代)
  • 性能排序:原生 for 循环 > for...of > forEach > for...infor...in 需遍历原型属性,最慢)

性能测试示例

const arr = new Array(1000000).fill(1);

// for循环
console.time('for');
for (let i = 0, len = arr.length; i < len; i++) { arr[i]; }
console.timeEnd('for'); // ~1ms

// forEach
console.time('forEach');
arr.forEach(item => item);
console.timeEnd('forEach'); // ~5ms

注意

  • 日常开发中forEach 可读性更高,除非处理超大数据量(百万级 +),否则优先考虑可读性
  • 若需中断遍历,只能用 for/for...offorEachbreak

5. 变量提升的理解?

变量提升(Hoisting):JS 引擎在执行代码前,会将变量 / 函数声明「提升」到其作用域顶部,但赋值操作不提升。

(1)变量提升分类

声明方式 提升规则 示例
var 变量 声明提升,赋值不提升;默认值为 undefined console.log(a); // undefined; var a = 1;
let/const 变量 「暂时性死区(TDZ)」:声明提升但不可访问 console.log(b); // 报错; let b = 2;
函数声明 声明 + 赋值全提升,优先级高于 var fn(); // 执行; function fn() {}
函数表达式 仅变量提升(同 var),赋值不提升 fn2(); // 报错; var fn2 = function() {}

(2)核心原理

JS 执行分为「编译阶段」和「执行阶段」:

  • 编译阶段:引擎扫描代码,将变量 / 函数声明存入「词法环境」,此时 var 变量初始化为 undefinedlet/const 未初始化(TDZ)
  • 执行阶段:按顺序执行代码,赋值操作生效

示例

// 1. var提升
console.log(a); // undefined(声明提升,赋值未提升)
var a = 10;

// 2. 函数声明提升
foo(); // "hello"(全提升)
function foo() { console.log('hello'); }

// 3. let TDZ
console.log(b); // Uncaught ReferenceError(TDZ内不可访问)
let b = 20;

执行顺序示例

console.log(a); // undefined
var a = 1;
function a() {}
console.log(a); // 1

// 等价于:
function a() {} // 函数声明优先级最高
var a; // var 声明(被函数声明覆盖)
console.log(a); // function a() {}
a = 1; // 赋值
console.log(a); // 1

6. 防抖和节流的理解?

核心场景

解决高频事件(如 resizescroll、输入框输入、按钮点击)触发过多导致的性能问题,本质是「控制函数执行频率」。

(1)防抖(Debounce)

原理:触发事件后,延迟 n 秒执行函数;若 n 秒内再次触发,重置延迟时间(「最后一次触发」后才执行)

场景:输入框搜索联想、窗口 resize、按钮防重复点击

示例(防抖)
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer); // 重置定时器
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// 使用:输入框联想
const input = document.querySelector('input');
input.addEventListener('input', debounce((e) => {
  console.log('搜索:', e.target.value);
}, 500));

(2)节流(Throttle)

原理:触发事件后,n 秒内仅执行一次函数(「固定频率」执行,无视中间触发)

场景:滚动加载、鼠标移动、高频点击

示例(节流)
function throttle(fn, interval) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= interval) { // 达到间隔才执行
      fn.apply(this, args);
      lastTime = now;
    }
  };
}

// 使用:滚动加载
window.addEventListener('scroll', throttle(() => {
  console.log('滚动中...');
}, 1000));

核心区别

特性 防抖(Debounce) 节流(Throttle)
执行时机 延迟执行,最后一次触发后执行 固定频率执行
适用场景 输入框搜索、resize 滚动加载、鼠标移动
比喻 电梯等人(最后一个人进来才关门) 地铁发车(固定时间发车)

防抖:「延迟执行,最后一次生效」(如输入框停止输入后才搜索)

节流:「固定频率执行」(如每 1 秒执行一次滚动逻辑)


7. 如何渲染几万条数据并不卡住界面?

核心问题

JS 单线程,一次性渲染几万条 DOM 会阻塞主线程(UI 渲染 / 事件响应暂停),导致界面卡顿。

解决方案(核心:分批次渲染 + 非阻塞主线程)

1. 分片渲染(requestAnimationFrame + 分批)

原理:利用 requestAnimationFrame(与浏览器重绘同步,60fps),每次渲染少量数据(如 20 条),分批次完成,不阻塞主线程

优势:贴合浏览器渲染节奏,无卡顿

示例
function renderBigList(data, batchSize = 20) {
  const container = document.getElementById('list');
  let index = 0; // 当前渲染索引

  function renderBatch() {
    const end = Math.min(index + batchSize, data.length);
    for (; index < end; index++) {
      const li = document.createElement('li');
      li.textContent = data[index];
      container.appendChild(li);
    }

    // 未渲染完则在下一帧继续
    if (index < data.length) {
      requestAnimationFrame(renderBatch);
    }
  }

  requestAnimationFrame(renderBatch);
}

// 使用:渲染10万条数据
const bigData = new Array(100000).fill(0).map((_, i) => `数据项${i+1}`);
renderBigList(bigData);
2. 虚拟列表(Virtual List)

原理:仅渲染「可视区域」的 DOM,滚动时动态替换内容(计算可视区域范围,只渲染可见项)

优势:无论数据量多大,DOM 节点数固定(仅可视区域数量),性能最优

场景:长列表(如电商商品列表、聊天记录)

简化版虚拟列表示例
<div id="virtualList" style="height: 500px; overflow: auto; position: relative;">
  <div id="content" style="position: absolute; top: 0; left: 0; width: 100%;"></div>
</div>

<script>
const list = document.getElementById('virtualList');
const content = document.getElementById('content');
const itemHeight = 30; // 每条数据高度
const visibleCount = Math.ceil(list.clientHeight / itemHeight); // 可视区域条数
const data = new Array(100000).fill(0).map((_, i) => `虚拟列表项${i+1}`);

// 设置总高度(让滚动条正常显示)
content.style.height = `${data.length * itemHeight}px`;

function renderVisible() {
  const scrollTop = list.scrollTop;
  const start = Math.floor(scrollTop / itemHeight); // 可视区域起始索引
  const end = start + visibleCount; // 可视区域结束索引
  const visibleData = data.slice(start, end); // 可视区域数据

  // 清空并渲染可视区域
  content.innerHTML = '';
  visibleData.forEach((item, i) => {
    const li = document.createElement('li');
    li.style.height = `${itemHeight}px`;
    li.style.position = 'absolute';
    li.style.top = `${(start + i) * itemHeight}px`;
    li.textContent = item;
    content.appendChild(li);
  });
}

// 滚动时触发渲染(加节流优化)
const throttledRender = throttle(renderVisible, 16);
list.addEventListener('scroll', throttledRender);

// 初始化渲染
renderVisible();
</script>
3. Web Worker(可选)

原理:若数据需预处理(如格式化、过滤),将耗时计算放到 Web Worker(子线程),避免阻塞主线程

配合分片渲染:Worker 处理数据后,分批次传给主线程渲染

示例
// main.js
const worker = new Worker('data-processor.js');
worker.postMessage(bigData);
worker.onmessage = (e) => {
  const processedData = e.data;
  renderBigList(processedData);
};

// data-processor.js
self.onmessage = (e) => {
  const data = e.data;
  // 处理数据
  const processed = data.map(item => processItem(item));
  self.postMessage(processed);
};

关键优化点

  1. 减少 DOM 操作:批量创建 DOM(如 DocumentFragment),避免频繁 appendChild
  2. 避免同步重排 / 重绘:渲染时隐藏容器(display: none),渲染完成后显示
  3. 防抖滚动事件:虚拟列表的 scroll 事件加节流,减少渲染次数

方案对比

方案 适用场景 优势 劣势
分片渲染 小数据量(万级) 实现简单,无卡顿 DOM 节点多,内存占用大
虚拟列表 大数据量(十万/百万级) DOM 节点少,性能最优 实现复杂,需固定高度
Web Worker 数据预处理耗时 不阻塞主线程 需额外线程,通信开销

总结

  • 小数据量(万级):分片渲染(requestAnimationFrame)足够
  • 大数据量(十万 / 百万级):虚拟列表(Virtual List)是最优解
  • 核心思想:「减少一次性 DOM 操作」+「仅渲染必要内容」+「不阻塞主线程」

最佳实践总结

原型与原型链

  • 理解 prototype__proto__ 的区别
  • 掌握原型链查找机制
  • 合理使用原型实现继承

事件处理

  • 优先使用事件代理处理动态元素
  • 理解事件冒泡和捕获的区别
  • 合理使用事件委托优化性能

跨域解决方案

  • 现代项目优先使用 CORS
  • 了解 JSONP 的局限性
  • 开发环境可使用代理服务器

性能优化

  • 大数据量遍历优先使用 for 循环
  • 高频事件使用防抖或节流
  • 长列表使用虚拟列表或分片渲染

变量提升

  • 优先使用 let/const 避免提升问题
  • 理解 TDZ(暂时性死区)
  • 注意函数声明和函数表达式的区别
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐