JavaScript 高频面试题
一、基础概念
1. 什么是 JavaScript?
JavaScript 是一种脚本语言,主要用于 Web 开发,可在浏览器端运行,也可在服务器端通过 Node.js 运行。它是一种动态、弱类型、基于原型的语言,支持面向对象、函数式和命令式编程风格。
2. JavaScript 的数据类型有哪些?
- 基本数据类型:Number、String、Boolean、Null、Undefined、Symbol、BigInt
- 引用数据类型:Object(包括 Array、Function、Date、RegExp 等)
3. 基本数据类型和引用数据类型的区别?
- 存储位置:基本类型存储在栈中,引用类型存储在堆中,栈中存储引用地址
- 复制方式:基本类型复制值,引用类型复制引用地址
- 比较方式:基本类型比较值,引用类型比较引用地址
4. 什么是闭包?
闭包是指有权访问另一个函数作用域中变量的函数。闭包的形成需要满足两个条件:
- 函数嵌套
- 内部函数引用外部函数的变量
5. 闭包的应用场景有哪些?
- 模块化:封装私有变量和方法
- 防抖和节流:控制函数执行频率
- 函数柯里化:将多参数函数转换为单参数函数
- 回调函数:保存函数执行上下文
6. 什么是原型和原型链?
- 原型:每个函数都有一个 prototype 属性,指向一个对象,这个对象是该函数创建的实例的原型
- 原型链:当访问对象的属性时,如果对象本身没有该属性,会沿着原型链向上查找,直到找到该属性或到达 Object.prototype
7. 什么是 this?
this 是函数执行时的上下文对象,其值取决于函数的调用方式:
- 全局作用域:指向 window(浏览器)或 global(Node.js)
- 函数调用:指向 window(非严格模式)或 undefined(严格模式)
- 方法调用:指向调用该方法的对象
- 构造函数:指向新创建的实例
- call/apply/bind:指向指定的对象
8. 如何改变 this 的指向?
- call:立即执行函数,第一个参数是 this 指向,后续参数是函数参数
- apply:立即执行函数,第一个参数是 this 指向,第二个参数是参数数组
- bind:返回一个新函数,this 指向固定,不会立即执行
- 箭头函数:继承外层作用域的 this
9. 什么是事件冒泡和事件捕获?
- 事件冒泡:事件从目标元素向上传播到根元素
- 事件捕获:事件从根元素向下传播到目标元素
10. 如何阻止事件冒泡和默认行为?
- 阻止事件冒泡:e.stopPropagation()
- 阻止默认行为:e.preventDefault()
- 同时阻止:return false(仅在 jQuery 和 DOM0 事件中有效)
二、异步编程
11. 什么是同步和异步?
- 同步:代码按顺序执行,执行完一个操作后再执行下一个操作
- 异步:代码不按顺序执行,发起操作后继续执行后续代码,操作完成后通过回调等方式通知
12. JavaScript 中的异步编程方式有哪些?
- 回调函数:最早的异步编程方式
- Promise:ES6 引入,解决回调地狱
- async/await:ES7 引入,基于 Promise 的语法糖
- Generator:ES6 引入,可暂停和恢复执行的函数
13. 什么是 Promise?
Promise 是一个对象,用于表示一个异步操作的最终完成或失败。它有三种状态:
- pending:初始状态
- fulfilled:操作成功完成
- rejected:操作失败
14. Promise 的常用方法有哪些?
- then:处理成功和失败的回调
- catch:处理失败的回调
- finally:无论成功或失败都会执行的回调
- all:等待所有 Promise 完成
- race:等待第一个完成的 Promise
- allSettled:等待所有 Promise 完成(无论成功或失败)
- any:等待第一个成功的 Promise
15. 什么是 async/await?
async/await 是 ES7 引入的异步编程语法糖,基于 Promise,使异步代码看起来更像同步代码。
- async:声明一个异步函数,返回一个 Promise
- await:等待一个 Promise 完成,只能在 async 函数中使用
16. 什么是回调地狱?如何解决?
回调地狱是指多层嵌套的回调函数,使代码难以阅读和维护。 解决方法:
- 使用 Promise 链式调用
- 使用 async/await
- 使用 Generator 函数
三、ES6+ 特性
17. ES6 有哪些新特性?
- let 和 const:块级作用域变量
- 箭头函数:更简洁的函数语法
- 模板字符串:支持多行字符串和变量插值
- 解构赋值:方便地提取对象和数组的值
- 默认参数:函数参数默认值
- 剩余参数:收集剩余的函数参数
- 扩展运算符:展开数组和对象
- Promise:异步编程解决方案
- Class:类语法
- Module:模块化
- Set 和 Map:新的数据结构
- Symbol:唯一值类型
18. let、const 和 var 的区别?
- 作用域:var 是函数作用域,let 和 const 是块级作用域
- 变量提升:var 会变量提升,let 和 const 不会
- 重复声明:var 可以重复声明,let 和 const 不可以
- 修改:var 和 let 可以修改,const 不可以修改引用(但可以修改对象的属性)
19. 箭头函数和普通函数的区别?
- this 指向:箭头函数继承外层作用域的 this,普通函数的 this 取决于调用方式
- arguments:箭头函数没有 arguments 对象,普通函数有
- 构造函数:箭头函数不能作为构造函数,普通函数可以
- prototype:箭头函数没有 prototype 属性,普通函数有
- 语法:箭头函数语法更简洁
20. 什么是解构赋值?
解构赋值是一种从对象或数组中提取值并赋给变量的语法。
javascript
// 对象解构
const { name, age } = { name: '张三', age: 20 };
// 数组解构
const [a, b, ...rest] = [1, 2, 3, 4, 5];21. 什么是 Promise?
Promise 是一个对象,用于表示一个异步操作的最终完成或失败。它有三种状态:
- pending:初始状态
- fulfilled:操作成功完成
- rejected:操作失败
22. 什么是 async/await?
async/await 是 ES7 引入的异步编程语法糖,基于 Promise,使异步代码看起来更像同步代码。
- async:声明一个异步函数,返回一个 Promise
- await:等待一个 Promise 完成,只能在 async 函数中使用
四、DOM 操作
23. 如何获取 DOM 元素?
- getElementById:根据 ID 获取元素
- getElementsByClassName:根据类名获取元素集合
- getElementsByTagName:根据标签名获取元素集合
- querySelector:根据选择器获取第一个匹配的元素
- querySelectorAll:根据选择器获取所有匹配的元素
24. 如何操作 DOM 元素的属性?
- getAttribute:获取属性值
- setAttribute:设置属性值
- removeAttribute:移除属性
- 直接操作:element.property = value
25. 如何操作 DOM 元素的样式?
- style 属性:element.style.property = value
- className:修改类名
- classList:add、remove、toggle、contains 方法
26. 如何创建和添加 DOM 元素?
- createElement:创建元素
- createTextNode:创建文本节点
- appendChild:添加子元素
- insertBefore:在指定元素前插入
- replaceChild:替换子元素
27. 如何删除 DOM 元素?
- removeChild:删除子元素
- remove:删除元素本身(IE 不支持)
五、原型和继承
28. JavaScript 的继承方式有哪些?
- 原型链继承:通过原型链实现继承
- 构造函数继承:通过 call/apply 调用父构造函数
- 组合继承:结合原型链和构造函数继承
- 原型式继承:通过 Object.create() 实现
- 寄生式继承:在原型式继承的基础上增强对象
- 寄生组合式继承:结合寄生式继承和组合继承的优点
29. 什么是原型链?
原型链是指对象通过 proto 属性连接起来的链式结构。当访问对象的属性时,如果对象本身没有该属性,会沿着原型链向上查找,直到找到该属性或到达 Object.prototype。
30. Object.prototype 有哪些方法?
- toString:返回对象的字符串表示
- valueOf:返回对象的原始值
- hasOwnProperty:检查对象是否有指定的自有属性
- isPrototypeOf:检查对象是否是另一个对象的原型
- propertyIsEnumerable:检查属性是否可枚举
六、性能优化
31. JavaScript 性能优化的方法有哪些?
- 减少 DOM 操作:使用文档片段、批量操作
- 使用事件委托:减少事件监听器数量
- 防抖和节流:控制函数执行频率
- 使用 requestAnimationFrame:优化动画性能
- 减少重排和重绘:避免频繁修改样式
- 使用缓存:缓存计算结果、DOM 元素等
- 优化循环:减少循环次数、使用 break 和 continue
- 使用 Web Workers:处理复杂计算
- 代码分割:按需加载代码
- 使用 ES6+ 特性:更简洁高效的代码
32. 什么是防抖和节流?
- 防抖:在事件触发后等待一段时间执行函数,如果在等待期间事件再次触发,则重新计时
- 节流:在一定时间内只执行一次函数,无论事件触发多少次
33. 如何实现防抖和节流?
javascript
// 防抖
function debounce(func, delay) {
let timer;
return function() {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, arguments), delay);
};
}
// 节流
function throttle(func, delay) {
let lastTime = 0;
return function() {
const now = Date.now();
if (now - lastTime >= delay) {
func.apply(this, arguments);
lastTime = now;
}
};
}七、模块化
34. JavaScript 的模块化方案有哪些?
- CommonJS:Node.js 的模块化方案,使用 require 和 module.exports
- ES6 Module:ES6 引入的模块化方案,使用 import 和 export
- AMD:异步模块定义,如 RequireJS
- CMD:通用模块定义,如 SeaJS
35. CommonJS 和 ES6 Module 的区别?
- 加载方式:CommonJS 是运行时加载,ES6 Module 是编译时加载
- 导出方式:CommonJS 导出的是值的拷贝,ES6 Module 导出的是值的引用
- this 指向:CommonJS 模块中 this 指向 module.exports,ES6 Module 中 this 指向 undefined
- 循环依赖:处理方式不同
36. 如何在浏览器中使用 ES6 Module?
在 script 标签中添加 type="module" 属性:
html
<script type="module">
import { foo } from './module.js';
console.log(foo);
</script>八、设计模式
37. JavaScript 中常见的设计模式有哪些?
- 单例模式:确保一个类只有一个实例
- 工厂模式:创建对象的接口,由子类决定实例化哪个类
- 构造函数模式:使用构造函数创建对象
- 原型模式:使用原型创建对象
- 组合模式:将对象组合成树形结构
- 策略模式:定义一系列算法,封装起来,使它们可以互相替换
- 观察者模式:对象间的一对多依赖关系
- 装饰器模式:动态地给对象添加额外的职责
- 代理模式:为其他对象提供一种代理以控制对这个对象的访问
38. 如何实现单例模式?
javascript
// 方式一:使用闭包
const Singleton = (function() {
let instance;
return function() {
if (!instance) {
instance = this;
}
return instance;
};
})();
// 方式二:使用 ES6 类
class Singleton {
static instance;
constructor() {
if (!Singleton.instance) {
Singleton.instance = this;
}
return Singleton.instance;
}
}九、浏览器相关
39. 浏览器的事件循环是什么?
事件循环是浏览器处理异步操作的机制,包括以下步骤:
- 执行同步代码
- 处理微任务(Promise、MutationObserver 等)
- 处理宏任务(setTimeout、setInterval、DOM 事件等)
- 重复以上步骤
40. 什么是微任务和宏任务?
- 微任务:Promise、MutationObserver、process.nextTick(Node.js)
- 宏任务:setTimeout、setInterval、setImmediate(Node.js)、I/O 操作、DOM 事件
41. 浏览器的渲染过程是什么?
- 解析 HTML:生成 DOM 树
- 解析 CSS:生成 CSSOM 树
- 合并 DOM 和 CSSOM:生成渲染树
- 布局:计算元素的位置和大小
- 绘制:将元素绘制到屏幕上
- 合成:将图层合成到一起
42. 什么是重排和重绘?
- 重排:当元素的位置、大小等发生变化时,浏览器需要重新计算布局
- 重绘:当元素的样式发生变化但不影响布局时,浏览器需要重新绘制元素
43. 如何避免重排和重绘?
- 批量修改样式:使用 class 一次性修改多个样式
- 使用文档片段:批量操作 DOM
- 避免频繁读取布局属性:缓存布局属性值
- 使用 transform:transform 不会触发重排
- 使用 will-change:提示浏览器元素可能发生变化
十、其他
44. 什么是 JSON?如何解析和序列化?
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。
- 解析:JSON.parse() 将 JSON 字符串转换为 JavaScript 对象
- 序列化:JSON.stringify() 将 JavaScript 对象转换为 JSON 字符串
45. 什么是深拷贝和浅拷贝?如何实现?
- 浅拷贝:只复制对象的引用,修改新对象会影响原对象
- 实现:Object.assign()、扩展运算符
- 深拷贝:复制对象的所有属性,修改新对象不会影响原对象
- 实现:JSON.parse(JSON.stringify())、递归拷贝
46. 如何判断一个变量是数组?
- Array.isArray():ES6 方法
- instanceof Array:判断对象是否是 Array 的实例
- Object.prototype.toString.call():返回 "[object Array]"
47. 如何实现数组去重?
- Set:ES6 方法,new Set(array)
- filter + indexOf:filter(item => array.indexOf(item) === index)
- reduce:reduce((prev, curr) => prev.includes(curr) ? prev : [...prev, curr], [])
48. 如何实现数组扁平化?
- flat():ES6 方法,array.flat(depth)
- reduce:reduce((prev, curr) => prev.concat(Array.isArray(curr) ? flatten(curr) : curr), [])
- 递归:递归处理嵌套数组
49. 如何实现 Promise?
javascript
class MyPromise {
constructor(executor) {
this.status = 'pending';
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.status === 'pending') {
this.status = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(cb => cb(value));
}
};
const reject = (reason) => {
if (this.status === 'pending') {
this.status = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(cb => cb(reason));
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
const promise2 = new MyPromise((resolve, reject) => {
if (this.status === 'fulfilled') {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}
if (this.status === 'rejected') {
setTimeout(() => {
try {
const x = onRejected(this.reason);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}
if (this.status === 'pending') {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
});
}
});
return promise2;
}
resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
if (x instanceof MyPromise) {
x.then(resolve, reject);
} else if (typeof x === 'object' && x !== null) {
let called = false;
try {
const then = x.then;
if (typeof then === 'function') {
then.call(x, (y) => {
if (called) return;
called = true;
this.resolvePromise(promise2, y, resolve, reject);
}, (r) => {
if (called) return;
called = true;
reject(r);
});
} else {
resolve(x);
}
} catch (error) {
if (called) return;
called = true;
reject(error);
}
} else {
resolve(x);
}
}
catch(onRejected) {
return this.then(null, onRejected);
}
static resolve(value) {
return new MyPromise(resolve => resolve(value));
}
static reject(reason) {
return new MyPromise((resolve, reject) => reject(reason));
}
static all(promises) {
return new MyPromise((resolve, reject) => {
const results = [];
let count = 0;
promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(
value => {
results[index] = value;
count++;
if (count === promises.length) {
resolve(results);
}
},
reason => reject(reason)
);
});
});
}
static race(promises) {
return new MyPromise((resolve, reject) => {
promises.forEach(promise => {
MyPromise.resolve(promise).then(
value => resolve(value),
reason => reject(reason)
);
});
});
}
}50. 什么是 TypeScript?它与 JavaScript 有什么区别?
TypeScript 是 JavaScript 的超集,添加了类型系统和其他特性。
- 类型系统:TypeScript 有静态类型检查,JavaScript 是动态类型
- 编译:TypeScript 需要编译为 JavaScript 才能运行
- 特性:TypeScript 支持接口、泛型、枚举等特性
- 工具支持:TypeScript 提供更好的 IDE 支持和类型提示
- 兼容性:TypeScript 兼容所有 JavaScript 代码