面试中常常会遇到一些反复被问及的知识点,它们背后考察的往往不是死记硬背,而是对语言特性、运行机制和工程实践的真正理解。这里整理了十五个高频考题,结合代码和简短的说明,希望能帮你理清思路。
ES6 常用特性速览
刚接触 ES6 时,let 和 const 让我告别了 var 的变量提升烦恼。let 有自己的块级作用域,const 保证引用不变,不过对象属性仍然可以修改。
let x = 10;
const y = 20;
// y = 30; // 报错,但 y.a = 1 可以
箭头函数不仅写法更短,还解决了 this 的动态绑定问题——它本身没有 this,会直接捕获定义时的外层 this,在回调里特别好用。
const add = (a, b) => a + b;
模板字符串让字符串拼接和换行不再难受:
const name = "John";
const greeting = `Hello, ${name}!`;
解构赋值省掉了很多临时变量:
const [a, b] = [1, 2];
const { name, age } = { name: "John", age: 25 };
默认参数让函数更清晰:
function greet(name = "Guest") {
return `Hello, ${name}`;
}
扩展运算符在数组合并、对象浅拷贝中几乎取代了 concat 和 Object.assign:
const arr2 = [...[1,2], 3, 4];
const obj2 = { ...{ a:1 }, b: 2 };
对象属性简写也很方便:
const x = 10, y = 20;
const point = { x, y }; // { x: 10, y: 20 }
Promise 的出现让异步代码告别了回调地狱,后面 async/await 更是让代码看起来像同步:
const promise = new Promise((resolve) => setTimeout(() => resolve("Done"), 1000));
async function fetchData() {
const data = await promise;
return data;
}
ES6 还带来了 class 语法糖,底层仍然是原型链,但写起来舒服多了:
class Person {
constructor(name) { this.name = name; }
greet() { return `Hello, I'm ${this.name}`; }
static create(name) { return new Person(name); }
}
除此之外,模块化的 import/export、新的数据结构 Set/Map、for...of 循环、生成器函数、Proxy/Reflect 以及后来 ES2020 的可选链 ?. 和空值合并 ?? 也都值得关注。
跨域问题与解决方案
浏览器的同源策略(协议、域名、端口任一不同即跨域)是安全基石,但在开发中经常需要绕开它。
http://example.com:80 → https://example.com:80 (协议不同)
http://example.com:80 → http://api.example.com:80 (域名不同)
http://example.com:80 → http://example.com:8080 (端口不同)
最常见的跨域方案是 CORS。服务器设置几个响应头就能让特定来源的请求被允许:
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
res.setHeader('Access-Control-Allow-Credentials', 'true');
CORS 的请求分为简单请求和需要预检的请求。简单请求直接发 GET/POST/HEAD,且 Content-Type 有限制;其他情况(如自定义头、PUT 等)会先发一个 OPTIONS 探路。
JSONP 是历史方案,利用 <script> 标签不受跨域限制的特性,动态加载一个带回调的脚本。现在用得很少,但面试还是会问:
function handleResponse(data) { console.log(data); }
const script = document.createElement('script');
script.src = 'http://api.example.com/data?callback=handleResponse';
document.body.appendChild(script);
// 服务器返回:handleResponse({data:"some data"});
开发时最常用的还是代理。webpack 的 devServer 可以轻松把 /api 转发到后端:
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://api.example.com',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
}
}
};
WebSocket 天生不受同源策略限制,可以做实时通信;生产环境常用 Nginx 反向代理,配置类似:
location /api/ {
proxy_pass http://api.example.com/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
Vue 中的响应式原理
Vue2 通过 Object.defineProperty 劫持数据属性的 getter/setter,但对于数组无法直接监听索引变化,所以 Vue2 重写了数组的七个方法(push、pop、shift、unshift、splice、sort、reverse),在其中加入依赖通知。
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
['push','pop','shift','unshift','splice','sort','reverse'].forEach(method => {
const original = arrayProto[method];
Object.defineProperty(arrayMethods, method, {
value: function(...args) {
const result = original.apply(this, args);
const ob = this.__ob__;
let inserted;
switch(method) {
case 'push': case 'unshift': inserted = args; break;
case 'splice': inserted = args.slice(2); break;
}
if(inserted) ob.observeArray(inserted);
ob.dep.notify();
return result;
}
});
});
const arr = [1, 2, 3];
Object.setPrototypeOf(arr, arrayMethods);
Vue3 用 Proxy 彻底解决了这个问题,可以直接代理整个对象和数组,包括索引赋值和 length 变化。
function reactiveArray(arr) {
return new Proxy(arr, {
get(target, key) {
if(key === 'length') return target.length;
const value = target[key];
if(typeof value === 'function') return value.bind(target);
return value;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
const type = Number(key) < target.length ? 'SET' : 'ADD';
if(type === 'ADD' || oldValue !== value) {
trigger(target, key, type);
}
return result;
}
});
}
v-if 和 v-show 的区别
简单来说,v-if 决定节点是否渲染(DOM 直接移除),v-show 只是切换 display 属性。
<div v-if="show">使用 v-if</div>
<div v-show="show">使用 v-show</div>
这意味着:
- 初始渲染时,
v-if为 false 不会创建 DOM,开销小;v-show无论如何都会渲染,开销大。 - 切换时,
v-if会重建/销毁组件,触发生命周期;v-show只是样式变化,开销小。
所以频繁切换的场景(如 Tab 切换)用 v-show,条件很少改变时(如权限控制)用 v-if。v-if 还能配合 v-else 和 v-else-if 使用。
从编译结果看:
// v-if
function render() { return show ? createElement('div', '内容') : createEmptyVNode(); }
// v-show
function render() { return createElement('div', {
directives: [{ name: 'show', value: show }],
style: { display: show ? '' : 'none' }
}, '内容'); }
网页加载性能优化实战
Core Web Vitals 是现在衡量体验的核心指标:LCP < 2.5s、FID < 100ms、CLS < 0.1。优化可以从这几方面入手。
代码层面:路由懒加载和代码分割是首屏优化的利器。Vue 中() => import() 就能按需加载组件,webpack 的 splitChunks 配置则把第三方库抽离为 vendor。
const LazyComponent = () => import('./LazyComponent.vue');
// webpack.config.js
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors' }
}
}
}
资源优化:图片用 srcset 提供不同分辨率,loading="lazy" 实现原生懒加载;关键资源(字体、核心 CSS)用 <link rel="preload"> 提前获取。
<img src="image.jpg"
srcset="image-320w.jpg 320w, image-480w.jpg 480w"
sizes="(max-width: 600px) 480px, 800px"
loading="lazy" alt="描述文本">
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
<link rel="prefetch" href="next-page.html">
缓存策略:Service Worker 可以离线缓存关键资源,配合 Lighthouse 命令行能快速审计。
self.addEventListener('install', event => {
event.waitUntil(
caches.open('v1').then(cache => cache.addAll(['/', '/styles/main.css', '/script/main.js']))
);
});
npx lighthouse https://example.com --output=json --output-path=report.json
浏览器渲染流程
浏览器拿到 HTML 后依次构建 DOM 树和 CSSOM 树,两者合并成渲染树,然后经历布局(计算位置大小)和绘制(填充像素),最后合成图层。
重排(Reflow)会重新计算几何属性,开销大;重绘(Repaint)只是颜色等变化,开销较小。合成(Composite)则只触发 GPU 层的变化,比如 transform,开销最小。
避免强制同步布局是个好习惯:不要在修改样式后立即读取几何属性,否则浏览器会立即重排以获取最新值。
// 不好
const width = element.offsetWidth; // 读
element.style.width = width + 10 + 'px'; // 写
// 更好:批量读,再批量写
const width = element.offsetWidth;
const height = element.offsetHeight;
element.style.width = width + 10 + 'px';
element.style.height = height + 10 + 'px';
防抖与节流
防抖(Debounce)让高频事件在停止触发 n 秒后才执行一次,适合输入框搜索、窗口 resize 等场景。
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
节流(Throttle)则保证在一段时间内只执行一次,适合滚动加载、按钮点击等。实现方式有两种:时间戳比较直观,定时器版能保证最后一次也执行。
// 时间戳
function throttle(fn, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if(now - lastTime >= delay) {
fn.apply(this, args);
lastTime = now;
}
};
}
// 定时器
function throttle(fn, delay) {
let timer = null;
return function(...args) {
if(!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
};
}
实际更常用的是加强版,能控制首尾触发:
function enhancedThrottle(fn, delay, { leading = true, trailing = true } = {}) {
let timer = null, lastTime = 0, lastArgs = null;
return function(...args) {
const now = Date.now();
const remaining = delay - (now - lastTime);
lastArgs = args;
if(remaining <= 0) {
if(timer) { clearTimeout(timer); timer = null; }
if(leading) fn.apply(this, args);
lastTime = now;
} else if(!timer && trailing) {
timer = setTimeout(() => {
fn.apply(this, lastArgs);
timer = null;
lastTime = Date.now();
}, remaining);
}
};
}
理解闭包
闭包就是函数能够访问其外部作用域变量的能力,在 JavaScript 里很常见。它常用于创建私有变量、工厂函数和模块模式。
function outer() {
const name = "John";
return function inner() { console.log(name); };
}
const closureFn = outer();
closureFn(); // "John"
一个经典例子是数据私有化:
function createCounter() {
let count = 0;
return {
increment() { return ++count; },
decrement() { return --count; },
getCount() { return count; }
};
}
不过闭包也可能造成内存泄漏,如果内部函数持有大对象的引用且一直不释放。解决办法就是在用完后主动解除引用。
function noLeak() {
let largeArray = new Array(1000000).fill('data');
const result = largeArray.length;
largeArray = null;
return function() { return result; };
}
浏览器线程与事件循环
浏览器主线程承担着 DOM 渲染、JS 执行和用户交互,而 JS 是单线程的。于是有了事件循环来协调异步任务:微任务先于宏任务执行。
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');
// start, end, promise, timeout
遇到耗时计算可以用 Web Worker 开启另一个线程:
const worker = new Worker('worker.js');
worker.postMessage({ data: 'Hello' });
worker.onmessage = (e) => console.log(e.data);
// worker.js: self.onmessage = (e) => self.postMessage(e.data.toUpperCase());
Vue2 到 Vue3 的核心变化
Vue3 改用 Proxy 实现响应式,性能更好且能监听数组索引。新增 Composition API 让逻辑复用更灵活,对 TypeScript 支持也到位了。
<script setup>
import { ref, computed, onMounted } from 'vue';
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() { count.value++; }
onMounted(() => console.log('mounted'));
return { count, doubleCount, increment };
</script>
体积上,Vue3 支持 tree-shaking,打包能小 40% 左右。它还允许 Fragment(多根节点)、Teleport、Suspense 等新特性。编译时还有静态提升和 patch flag,减少运行时比对开销。
new 操作符做了什么
new Fn() 的过程可以拆解为:创建一个空对象,将其原型指向 Fn.prototype,执行构造函数并绑定 this,最后根据返回值决定是返回那个对象还是本身的 this。
function myNew(constructor, ...args) {
const obj = Object.create(constructor.prototype);
const result = constructor.apply(obj, args);
return result instanceof Object ? result : obj;
}
call、apply、bind 的区别
三者都能改变 this 指向。call 和 apply 立即执行,只是传参方式不同(逐个 vs 数组);bind 返回新函数,可以延迟调用,且可以柯里化部分参数。
const person = { name: 'John', greet(greeting, punc) { return `${greeting}, ${this.name}${punc}`; }};
const another = { name: 'Jane' };
person.greet.call(another, 'Hello', '!'); // Hello, Jane!
person.greet.apply(another, ['Hi', '!!']); // Hi, Jane!!
const bound = person.greet.bind(another, 'Hey');
bound('?'); // Hey, Jane?
手写实现也是面试常考,核心思路是:将原函数临时挂到上下文对象上,用 Symbol 避免属性冲突,执行后再删除。bind 还要处理 new 调用的情况。
TypeScript 装饰器入门
装饰器本质上是对类、方法、属性、参数或访问器进行'劫持'的函数。可以搭配装饰器工厂传递参数。
function log(target: any, key: string, desc: PropertyDescriptor) {
const original = desc.value;
desc.value = function(...args: any[]) {
console.log(`Calling ${key}`);
return original.apply(this, args);
};
}
class UserService {
@log
addUser(name: string) { /* ... */ }
}
实际项目中,装饰器可用于日志记录、自动绑定 this、参数校验等,让代码更声明式和可维护。
大数据量列表渲染优化
面对 10 万条数据,直接全量渲染浏览器会直接卡死。虚拟列表的思路是只渲染可视区域及上下少量缓冲区,动态计算偏移。
在 Vue 组件中,监听滚动计算 startIndex 和 endIndex,通过 transform: translateY 将可见内容放在正确位置。
<template>
<div ref="container" @scroll="handleScroll" style="height:500px;overflow:auto;position:relative">
<div :style="{ height: totalHeight + 'px' }"></div>
<div :style="{ transform: `translateY(${offset}px)` }">
<div v-for="item in visibleData" :key="item.id" :style="{ height: itemHeight + 'px' }">
{{ item.content }}
</div>
</div>
</div>
</template>
另一种思路是时间分片渲染,用 requestAnimationFrame 或 requestIdleCallback 把大数据切割成小批次,避免长时间阻塞主线程。Web Worker 处理数据也是常见手段。
时间切片与浏览器空闲回调
时间切片的本质是在浏览器空闲时执行小段任务,避免长任务影响交互。requestIdleCallback 可以让我们在每一帧有空余时间时处理工作。
function processBigData(data, processItem) {
return new Promise(resolve => {
let index = 0;
function run(deadline) {
while (index < data.length && deadline.timeRemaining() > 0) {
processItem(data[index]);
index++;
}
if (index < data.length) requestIdleCallback(run);
else resolve();
}
requestIdleCallback(run);
});
}
React 18 的 Concurrent Mode 也用类似思想,useDeferredValue 和 startTransition 能标记非紧急更新,这些更新可以被更高优先级的交互中断。

