JavaScript 基础面试题:核心概念与实战解析
前言
JavaScript 是前端开发的核心基础,也是面试中最常被考察的部分。掌握 JavaScript 的核心概念不仅帮助你在面试中脱颖而出,更能让你在日常开发中写出更高质量的代码。本节将 JavaScript 核心知识点分解为系统化的面试题,帮助深入理解每个概念的原理和应用场景。
1. 变量与数据类型
面试题 1:JavaScript 数据类型转换
题目:以下代码的输出结果是什么?
console.log([] == ![]); // 题目1
console.log([] == ![]); // 题目2
console.log([] == ![]); // 题目3答案:
true
true
false解析: 这道题考察了 JavaScript 中的类型转换规则和运算符优先级:
==vs===:==会进行类型转换===不会进行类型转换,直接比较值和类型
逻辑非运算符
!:![]将空数组转换为布尔值false,然后取反得到true![]转换为false,所以[] == ![]相当于[] == false,结果为true
类型转换机制:
// 数组转布尔值
Boolean([]); // false
Boolean({}); // true
// 数字转字符串
String(123); // "123"
// 布尔值转数字
Number(true); // 1
Number(false); // 0延伸知识点:
- falsy 值:
false,0,"",null,undefined,NaN - truthy 值:除 falsy 值外的所有值
- 类型转换优先级:
!>==>===>&&>||
面试题 2:隐式类型转换陷阱
题目:以下代码的输出结果是什么?
console.log([] + 1 + "1" + 1);
console.log("foo" + +"bar");
console.log("foo" + +(+"bar"));答案:
"11"
"fooNaNbar"
"foo2bar"解析: 这道题考察了 JavaScript 中的隐式类型转换和运算符优先级:
[] + 1:空数组转数字为0,0 + 1 = 11 + "1":数字与字符串相加,1 + "1" = "11""foo" + + "bar":"foo" ++将+转为字符串,得到"foo+"`+"bar"字符串连接,"foo+" + "bar" = "fooNaNbar"
+ + +:一元+,转为数字为NaN"foo" + NaN + "bar" = "foo2bar"
关键概念:
- 隐式类型转换规则
- 运算符优先级和结合性
+运算符的重载行为
2. 作用域与闭包
面试题 3:闭包与变量捕获
题目:以下代码的输出结果是什么?
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}答案:
3
3
3解析: 这道题考察了 JavaScript 中的作用域和闭包概念:
var声明:var具有函数作用域,但在循环中声明的变量会被提升- 闭包形成:每个
setTimeout回调函数都形成了一个闭包,捕获了变量i - 异步执行时机:
setTimeout是异步操作,所有回调函数在循环结束后才执行 - 变量值问题:由于所有回调函数共享同一个变量
i,而i在异步执行时已经等于3
解决方案:
// 方法1:使用 let 声明
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 方法2:使用立即执行函数
for (var i = 0; i < 3; i++) {
(function (j) {
setTimeout(() => console.log(j), 100);
})(i);
}
// 方法3:使用块级作用域
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}面试题 4:闭包的实际应用
题目:以下代码实现了什么功能?有什么问题?
function createCounter() {
var count = 0;
return function () {
count++;
return count;
};
}
var counter1 = createCounter();
var counter2 = createCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1
console.log(counter2()); // 2
console.log(counter1()); // 3
console.log(counter2()); // 4答案: 实现了两个独立的计数器,但存在的问题是两个计数器共享了同一个 count 变量。
解析:
- 闭包特性:每个返回的函数都访问了外层函数的
count变量 - 变量共享问题:由于
var的函数作用域特性,两个计数器实际上共享同一个变量 - 预期行为:开发者可能期望每个计数器独立工作
正确的实现:
function createCounter() {
return function () {
// 每个函数都有自己的 count 变量
var count = 0;
return function () {
count++;
return count;
};
};
}
// 或者使用 ES6 语法
const createCounterES6 = () => {
let count = 0;
return () => {
count++;
return count;
};
};3. this 关键字
面试题 5:this 的指向问题
题目:以下代码的输出结果是什么?
var name = "global";
var obj = {
name: "object",
getName: function () {
return this.name;
},
getNameArrow: () => {
return this.name;
},
};
console.log(obj.getName()); // 题目1
console.log(obj.getNameArrow()); // 题目2
console.log(obj.getName.call({ name: "called" })); // 题目3
console.log(obj.getNameArrow.call({ name: "called" })); // 题目4
// 题目5
var getName = obj.getName;
console.log(getName()); // 题目5
console.log(getName.call({ name: "called" })); // 题目6答案:
"object"
"global"
"called"
"global"
"object"
"called"解析: 这道题全面考察了 JavaScript 中 this 的指向规则:
- 对象方法调用:
obj.getName()中this指向obj - 箭头函数:箭头函数没有自己的
this,会捕获外层的this call/apply调用:显式指定this的指向- 函数赋值后调用:函数作为独立对象的方法调用时,
this指向全局对象(严格模式下为undefined)
this 指向规则总结:
- 默认绑定:直接调用函数,
this指向全局对象或undefined - 隐式绑定:通过对象调用函数,
this指向该对象 - 显式绑定:使用
call,apply,bind指定this - new 绑定:使用
new调用构造函数,this指向新创建的对象 - 箭头函数:继承外层作用域的
this
面试题 6:构造函数中的 this
题目:以下代码的输出结果是什么?
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function () {
console.log(this.name);
};
var p1 = new Person("张三");
p1.sayName(); // 输出1
var p2 = Person("李四");
p2.sayName(); // 输出2
// 使用 apply 调用
p1.sayName.apply(p2); // 输出3答案:
"张三"
"李四"
"张三"解析:
- 构造函数中的
this:new Person("张三")中this指向新创建的对象实例 - 原型方法中的
this:p1.sayName()中this指向调用该方法的对象p1 apply方法的作用:p1.sayName.apply(p2)将方法中的this指向p2,但原型链上的name属性仍然是p1的
4. 原型与继承
面试题 7:原型链查找
题目:以下代码的输出结果是什么?
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
console.log(`你好,我是${this.name}`);
};
var person = new Person("张三");
person.sayHello(); // 输出1
person.sayHello = function () {
console.log(`你好,我是${this.name},年龄${this.age}`);
};
person.sayHello(); // 输出2答案:
"你好,我是张三"
"你好,我是张三,年龄undefined"解析:
- 原型链查找:访问对象属性时,首先在对象本身查找,找不到时沿原型链向上查找
- 属性遮蔽:给
person对象添加sayHello方法遮蔽了原型上的同名方法 - 原型共享:所有实例共享同一个原型对象
面试题 8:原型继承的实现
题目:如何手动实现 JavaScript 的继承?
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child(name, age) {
// 继承 Parent 的属性和方法
}
// 实现 Child 继承 Parent答案:
// 方法1:原型链继承
function ChildPrototype(name, age) {
Parent.call(this, name);
this.age = age;
}
ChildPrototype.prototype = Object.create(Parent.prototype);
ChildPrototype.prototype.constructor = ChildPrototype;
ChildPrototype.prototype.sayAge = function () {
console.log(`${this.age}岁`);
};
// 方法2:构造函数继承
function ChildConstructor(name, age) {
Parent.call(this, name);
this.age = age;
}
ChildConstructor.prototype = new Parent();
ChildConstructor.prototype.constructor = ChildConstructor;
ChildConstructor.prototype.sayAge = function () {
console.log(`${this.age}岁`);
};
// 方法3:组合继承
function ChildComposition(name, age) {
Parent.call(this, name);
this.age = age;
}
ChildComposition.prototype = Object.assign(Object.create(Parent.prototype), {
constructor: ChildComposition,
sayAge: function () {
console.log(`${this.age}岁`);
},
});
// 方法4:ES6 Class 继承
class ParentES6 {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class ChildES6 extends ParentES6 {
constructor(name, age) {
super(name);
this.age = age;
}
sayAge() {
console.log(`${this.age}岁`);
}
}继承方式对比:
- 原型链继承:简单,但无法向父构造函数传参
- 构造函数继承:可以传参,但继承父原型会创建不必要的父实例
- 组合继承:结合前两种优点,是最推荐的方式
- ES6 Class 继承:语法简洁,底层仍然是原型继承
5. ES6+ 新特性
面试题 9:箭头函数与普通函数的区别
题目:以下代码的输出结果是什么?
const obj = {
name: "张三",
age: 25,
};
function logName() {
console.log(this.name);
}
const arrowLogName = () => {
console.log(this.name);
};
logName.call(obj); // 题目1
arrowLogName.call(obj); // 题目2
// 作为对象方法
const person = {
name: "李四",
logName: logName,
arrowLogName: arrowLogName,
};
person.logName(); // 题目3
person.arrowLogName(); // 题目4答案:
"张三"
undefined
"李四"
undefined解析:
- 箭头函数特性:箭头函数没有自己的
this,arguments,不能作为构造函数使用 - this 指向:
- 普通函数的
this由调用方式决定 - 箭头函数的
this在定义时确定,不可改变
- 普通函数的
- 词法作用域绑定:箭头函数的
this绑定到定义时的词法作用域
面试题 10:解构赋值与扩展运算符
题目:以下代码的输出结果是什么?
const arr = [1, 2, 3];
const obj = { a: 1, b: 2 };
// 题目1
console.log([...arr, 4, 5]);
// 题目2
console.log([1, ...arr, 4, ...arr]);
// 题目3
console.log({ ...obj, c: 3 });
// 题目4
const [first, ...rest] = arr;
console.log(first, rest);
// 题目5
const { a, b, ...rest } = obj;
console.log(a, b, rest);答案:
[1, 2, 3, 4, 5]
[1, 1, 2, 3, 4, 5]
{ a: 1, b: 2, c: 3 }
1 [2, 3]
1 2解析:
- 扩展运算符
...:用于展开数组或对象 - 解构赋值:从数组或对象中提取值并赋给变量
- 剩余参数:使用
...rest语法收集剩余的参数 - 应用场景:函数参数处理、数组操作、对象合并等
面试题 11:Promise 和 async/await
题目:以下代码的输出结果是什么?
// 题目1
console.log("start");
setTimeout(() => {
console.log("timeout");
}, 0);
Promise.resolve().then(() => {
console.log("promise");
});
// 题目2
async function testAsync() {
console.log("start");
await new Promise((resolve) => {
setTimeout(() => {
console.log("timeout");
resolve();
}, 0);
});
console.log("promise");
}
testAsync();答案:
start
timeout
promise
start
timeout
promise解析:
- 事件循环机制:同步代码 > 微任务(Promise.then) > 宏任务(setTimeout)
- async/await 本质:语法糖,底层仍然是 Promise
- 执行顺序:
- 同步代码立即执行
- 微任务在当前脚本执行完后立即执行
- 宏任务在下一轮事件循环中执行
6. 数组与对象操作
面试题 12:数组方法的高级应用
题目:实现以下数组操作方法
// 实现数组的扁平化
function flatten(arr) {
// 输入: [1, [2, 3], [4, [5, 6]]]
// 输出: [1, 2, 3, 4, 5, 6]
}
// 实现数组的去重
function unique(arr) {
// 输入: [1, 2, 2, 3, 4, 3, 5]
// 输出: [1, 2, 3, 4, 5]
}
// 实现数组的分组
function groupBy(arr, key) {
// 输入: [{name: '张三', age: 25}, {name: '李四', age: 25}, {name: '王五', age: 30}]
// 按 age 分组
}答案:
// 数组扁平化
function flatten(arr) {
return arr.reduce((acc, val) => {
return acc.concat(Array.isArray(val) ? flatten(val) : val);
}, []);
}
// 数组去重
function unique(arr) {
return [...new Set(arr)];
}
// 或者使用 filter
function unique2(arr) {
return arr.filter((item, index) => {
return arr.indexOf(item) === index;
});
}
// 数组分组
function groupBy(arr, key) {
return arr.reduce((acc, obj) => {
const groupKey = obj[key];
if (!acc[groupKey]) {
acc[groupKey] = [];
}
acc[groupKey].push(obj);
return acc;
}, {});
}面试题 13:深拷贝与浅拷贝
题目:以下代码的输出结果是什么?
const obj1 = {
name: "张三",
info: {
age: 25,
address: {
city: "北京",
},
},
};
const obj2 = Object.assign({}, obj1);
const obj3 = JSON.parse(JSON.stringify(obj1));
const obj4 = { ...obj1 };
obj2.info.address.city = "上海";
obj3.info.address.city = "广州";
obj4.info.address.city = "深圳";
console.log(obj1.info.address.city); // 原始对象
console.log(obj2.info.address.city); // Object.assign
console.log(obj3.info.address.city); // JSON 方式
console.log(obj4.info.address.city); // 扩展运算符答案:
北京
北京
广州
深圳解析:
- 浅拷贝:只拷贝对象的第一层属性,嵌套对象仍然是引用
Object.assign()- 扩展运算符
...
- 深拷贝:递归拷贝所有层级的属性,创建完全独立的对象
JSON.parse(JSON.stringify())- 递归函数实现
深拷贝的实现:
function deepClone(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof Array) {
return obj.map((item) => deepClone(item));
}
const cloned = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key]);
}
}
return cloned;
}7. 事件循环与任务队列
面试题 14:宏任务与微任务的执行顺序
题目:以下代码的输出顺序是什么?
console.log("1");
setTimeout(() => {
console.log("2");
Promise.resolve().then(() => {
console.log("3");
});
}, 0);
Promise.resolve().then(() => {
console.log("4");
setTimeout(() => {
console.log("5");
}, 0);
});
console.log("6");答案:
1
6
4
2
3
5解析:
- 执行栈:同步代码优先执行(1, 6)
- 微任务队列:Promise.then 属于微任务,在当前宏任务执行完后立即执行
- 宏任务队列:setTimeout 属于宏任务,在下一轮事件循环执行
执行流程:
- 同步代码:打印 1, 6
- 第一个微任务:打印 4,将 setTimeout(5) 加入宏任务队列
- 第一个宏任务:打印 2,将 Promise.then(3) 加入微任务队列
- 微任务:打印 3
- 下一个宏任务:打印 5
宏任务与微任务分类:
// 宏任务 (Macrotask)
// - setTimeout / setInterval
// - I/O 操作
// - UI 渲染
// - setImmediate (Node.js)
// 微任务 (Microtask)
// - Promise.then / catch / finally
// - async/await
// - MutationObserver
// - process.nextTick (Node.js)面试题 15:async/await 的执行时机
题目:以下代码的输出结果是什么?
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
async1();
new Promise((resolve) => {
console.log("promise1");
resolve();
}).then(() => {
console.log("promise2");
});
console.log("script end");答案:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout解析:
- async/await 本质:
await后面的代码相当于在Promise.then中执行 - 执行顺序:
- 同步代码:script start → async1 start → async2 → promise1 → script end
- 微任务:async1 end → promise2
- 宏任务:setTimeout
8. 模块化系统
面试题 16:ES Modules 与 CommonJS 的区别
题目:ES6 模块(ES Modules)和 CommonJS 模块有什么区别?
答案:
| 特性 | ES Modules | CommonJS |
|---|---|---|
| 加载方式 | 编译时加载(静态) | 运行时加载(动态) |
| 输出 | 值的引用 | 值的拷贝 |
| this 指向 | undefined | 当前模块 |
| 循环依赖 | 可以处理 | 可能出现问题 |
| 使用场景 | 浏览器、现代 Node.js | Node.js (传统) |
| 导入语法 | import / export | require / module.exports |
代码示例:
// ES Modules
// module.js
export let counter = 0;
export function increment() {
counter++;
}
// main.js
import { counter, increment } from "./module.js";
console.log(counter); // 0
increment();
console.log(counter); // 1 (引用,会同步更新)
// ===================================
// CommonJS
// module.js
let counter = 0;
function increment() {
counter++;
}
module.exports = { counter, increment };
// main.js
const { counter, increment } = require("./module.js");
console.log(counter); // 0
increment();
console.log(counter); // 0 (拷贝,不会更新)面试题 17:模块循环依赖问题
题目:如何处理模块的循环依赖?
答案:
// a.js
import { b } from "./b.js";
export const a = "a";
console.log("a.js:", b);
// b.js
import { a } from "./a.js";
export const b = "b";
console.log("b.js:", a);
// main.js
import "./a.js";ES Modules 处理方式:
- 模块在执行前会被"预处理",形成模块记录
- 导入的是引用,而不是值的拷贝
- 即使存在循环依赖,也能正确访问到导出的变量
CommonJS 处理方式:
- 模块首次加载时会被缓存
- 循环依赖时可能获取到不完整的导出对象
- 需要注意导出时机和顺序
9. DOM 操作与事件
面试题 18:事件委托的原理和应用
题目:什么是事件委托?如何实现一个高效的事件委托?
答案:
事件委托:利用事件冒泡机制,将子元素的事件监听器委托给父元素处理。
优点:
- 减少内存消耗(只需一个监听器)
- 动态添加的元素也能响应事件
- 提升性能
实现示例:
// HTML 结构
// <ul id="list">
// <li data-id="1">项目 1</li>
// <li data-id="2">项目 2</li>
// <li data-id="3">项目 3</li>
// </ul>
// 传统方式(不推荐)
const items = document.querySelectorAll("#list li");
items.forEach((item) => {
item.addEventListener("click", function () {
console.log(this.dataset.id);
});
});
// 事件委托方式(推荐)
const list = document.querySelector("#list");
list.addEventListener("click", function (e) {
// 确保点击的是 li 元素
if (e.target.tagName === "LI") {
console.log(e.target.dataset.id);
}
});
// 更通用的事件委托函数
function delegate(parent, selector, event, handler) {
parent.addEventListener(event, function (e) {
const target = e.target;
if (target.matches(selector)) {
handler.call(target, e);
}
});
}
// 使用
delegate(list, "li", "click", function (e) {
console.log(this.dataset.id);
});面试题 19:阻止事件冒泡和默认行为
题目:如何阻止事件冒泡和默认行为?它们有什么区别?
答案:
// 阻止事件冒泡
element.addEventListener("click", function (e) {
e.stopPropagation(); // 阻止事件向上冒泡
console.log("clicked");
});
// 阻止默认行为
link.addEventListener("click", function (e) {
e.preventDefault(); // 阻止链接跳转
console.log("link clicked but not navigated");
});
// 同时阻止冒泡和默认行为
button.addEventListener("click", function (e) {
e.stopPropagation();
e.preventDefault();
// 或者简写
return false; // 在传统事件处理中
});区别:
stopPropagation():阻止事件在 DOM 树中向上冒泡preventDefault():阻止元素的默认行为(如表单提交、链接跳转)stopImmediatePropagation():阻止事件冒泡,并阻止同一元素上其他监听器执行
10. ES6+ 数据结构
面试题 20:Map 与 Object 的区别
题目:Map 和普通对象有什么区别?什么时候使用 Map?
答案:
| 特性 | Map | Object |
|---|---|---|
| 键的类型 | 任意类型 | 字符串或 Symbol |
| 键的顺序 | 插入顺序 | 不保证顺序(ES2015+部分有序) |
| 大小获取 | map.size | Object.keys(obj).length |
| 迭代 | 直接可迭代 | 需要 Object.keys() |
| 性能 | 频繁增删时更优 | 简单场景足够 |
| 序列化 | 不支持 JSON | 支持 JSON |
代码示例:
// Map 的使用
const map = new Map();
// 键可以是任意类型
map.set("string", "字符串键");
map.set(1, "数字键");
map.set(true, "布尔键");
map.set({}, "对象键");
console.log(map.size); // 4
console.log(map.get(1)); // '数字键'
console.log(map.has("string")); // true
// 迭代
for (let [key, value] of map) {
console.log(key, value);
}
// 转换为数组
const arr = [...map]; // [[key1, value1], [key2, value2], ...]
const keys = [...map.keys()];
const values = [...map.values()];
// Object 的使用
const obj = {
string: "字符串键",
1: "数字键会被转为字符串",
true: "布尔键会被转为字符串",
};
console.log(obj[1]); // '数字键会被转为字符串'
console.log(obj["1"]); // 同上使用场景:
- 使用 Map:需要非字符串键、频繁增删、需要保持插入顺序
- 使用 Object:简单的键值对存储、需要 JSON 序列化、与 JSON API 交互
面试题 21:WeakMap 和 WeakSet 的应用
题目:WeakMap 和 WeakSet 有什么特点?适用于什么场景?
答案:
WeakMap 特点:
- 键必须是对象
- 键是弱引用,不影响垃圾回收
- 不可迭代,没有
size属性
WeakSet 特点:
- 值必须是对象
- 值是弱引用
- 不可迭代
应用场景:
// 1. 存储私有数据
const privateData = new WeakMap();
class Person {
constructor(name) {
privateData.set(this, { name });
}
getName() {
return privateData.get(this).name;
}
}
const person = new Person("张三");
console.log(person.getName()); // '张三'
// person 对象被销毁后,privateData 中对应的数据也会被回收
// 2. 缓存计算结果
const cache = new WeakMap();
function complexCalculation(obj) {
if (cache.has(obj)) {
return cache.get(obj);
}
const result = /* 复杂计算 */ obj.value * 2;
cache.set(obj, result);
return result;
}
// 3. DOM 节点关联数据
const elementData = new WeakMap();
function attachData(element, data) {
elementData.set(element, data);
}
function getData(element) {
return elementData.get(element);
}
// 当 DOM 节点被移除后,关联的数据会自动被回收
// 4. 使用 WeakSet 追踪对象
const visitedObjects = new WeakSet();
function traverse(obj) {
if (visitedObjects.has(obj)) {
return; // 避免循环引用
}
visitedObjects.add(obj);
// 处理对象...
}11. 性能优化
面试题 22:防抖和节流的实现
题目:请实现防抖(debounce)和节流(throttle)函数。
答案:
// 防抖:事件触发 n 秒后才执行,如果在 n 秒内再次触发,则重新计时
function debounce(func, wait) {
let timeout;
return function (...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
// 使用场景:搜索框输入、窗口调整
const handleInput = debounce(function (e) {
console.log("搜索:", e.target.value);
}, 500);
document.querySelector("#search").addEventListener("input", handleInput);
// 节流:在 n 秒内只执行一次,再次触发不会重新计时
function throttle(func, wait) {
let timeout;
let previous = 0;
return function (...args) {
const context = this;
const now = Date.now();
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
};
}
// 时间戳版本(立即执行)
function throttleTimestamp(func, wait) {
let previous = 0;
return function (...args) {
const now = Date.now();
if (now - previous > wait) {
func.apply(this, args);
previous = now;
}
};
}
// 定时器版本(延迟执行)
function throttleTimeout(func, wait) {
let timeout;
return function (...args) {
if (!timeout) {
timeout = setTimeout(() => {
func.apply(this, args);
timeout = null;
}, wait);
}
};
}
// 使用场景:滚动事件、鼠标移动
const handleScroll = throttle(function () {
console.log("滚动位置:", window.scrollY);
}, 200);
window.addEventListener("scroll", handleScroll);应用场景对比:
| 函数 | 特点 | 应用场景 |
|---|---|---|
| 防抖 | 最后一次触发后执行 | 搜索框输入、表单验证、窗口调整 |
| 节流 | 固定时间间隔执行 | 滚动加载、鼠标移动、播放进度 |
面试题 23:内存泄漏的常见场景
题目:JavaScript 中常见的内存泄漏场景有哪些?如何避免?
答案:
常见内存泄漏场景:
// 1. 意外的全局变量
function createLeak() {
leak = "我是全局变量"; // 没有使用 var/let/const
this.anotherLeak = "另一个泄漏"; // 非严格模式下 this 指向 window
}
// 解决方案:使用严格模式,声明变量
("use strict");
function noLeak() {
const safe = "安全的局部变量";
}
// 2. 被遗忘的定时器
const data = fetchLargeData();
const timer = setInterval(() => {
const node = document.getElementById("node");
if (node) {
node.innerHTML = JSON.stringify(data);
}
}, 1000);
// 解决方案:及时清除定时器
function cleanup() {
clearInterval(timer);
}
// 3. 闭包引用
function outer() {
const largeData = new Array(1000000);
return function inner() {
console.log("使用闭包");
// largeData 会一直被引用,无法回收
};
}
const fn = outer();
// 解决方案:不需要时解除引用
fn = null;
// 4. DOM 引用
const elements = {
button: document.getElementById("button"),
};
function removeButton() {
document.body.removeChild(elements.button);
// elements.button 仍然引用已删除的 DOM
}
// 解决方案:删除引用
function removeButtonProperly() {
document.body.removeChild(elements.button);
delete elements.button;
}
// 5. 事件监听器未移除
class Component {
constructor() {
this.handleClick = this.handleClick.bind(this);
document.addEventListener("click", this.handleClick);
}
handleClick() {
console.log("clicked");
}
destroy() {
// 必须移除事件监听器
document.removeEventListener("click", this.handleClick);
}
}检测内存泄漏的方法:
- Chrome DevTools 的 Memory Profiler
- Performance Monitor 监控内存使用
- 使用
performance.memoryAPI
12. 高级函数技巧
面试题 24:函数柯里化的实现
题目:什么是函数柯里化?请实现一个通用的柯里化函数。
答案:
柯里化:将多参数函数转换为一系列单参数函数的过程。
// 简单示例
function add(a, b, c) {
return a + b + c;
}
// 柯里化后
function curriedAdd(a) {
return function (b) {
return function (c) {
return a + b + c;
};
};
}
console.log(curriedAdd(1)(2)(3)); // 6
// 通用柯里化函数实现
function curry(fn) {
return function curried(...args) {
// 如果参数数量足够,直接调用原函数
if (args.length >= fn.length) {
return fn.apply(this, args);
}
// 否则返回一个新函数,继续收集参数
return function (...nextArgs) {
return curried.apply(this, [...args, ...nextArgs]);
};
};
}
// 使用示例
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
// 实际应用场景
const multiply = (a, b, c) => a * b * c;
const curriedMultiply = curry(multiply);
const double = curriedMultiply(2);
const doubleAndTriple = double(3);
console.log(doubleAndTriple(4)); // 24面试题 25:实现 bind、call、apply
题目:手动实现 Function.prototype.bind、call 和 apply 方法。
答案:
// 实现 call
Function.prototype.myCall = function (context, ...args) {
// 如果没有传入上下文,默认为全局对象
context = context || globalThis;
// 创建唯一的属性名,避免覆盖
const fnSymbol = Symbol("fn");
context[fnSymbol] = this;
// 调用函数
const result = context[fnSymbol](...args);
// 删除临时属性
delete context[fnSymbol];
return result;
};
// 实现 apply
Function.prototype.myApply = function (context, args = []) {
context = context || globalThis;
const fnSymbol = Symbol("fn");
context[fnSymbol] = this;
const result = context[fnSymbol](...args);
delete context[fnSymbol];
return result;
};
// 实现 bind
Function.prototype.myBind = function (context, ...bindArgs) {
const fn = this;
return function (...callArgs) {
// 合并参数
const args = [...bindArgs, ...callArgs];
// 如果使用 new 调用,this 指向新对象
if (this instanceof fn) {
return new fn(...args);
}
// 否则使用指定的上下文
return fn.apply(context, args);
};
};
// 测试
const obj = { name: "张三" };
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`;
}
console.log(greet.myCall(obj, "你好", "!")); // 你好, 张三!
console.log(greet.myApply(obj, ["你好", "!"])); // 你好, 张三!
const boundGreet = greet.myBind(obj, "你好");
console.log(boundGreet("!")); // 你好, 张三!13. 错误处理
面试题 26:try-catch 的性能影响
题目:try-catch 对性能有什么影响?如何优雅地处理错误?
答案:
try-catch 的影响:
- 现代 JavaScript 引擎已经优化了 try-catch 性能
- 主要开销在于抛出错误时的堆栈跟踪
- 不要在性能关键代码中过度使用
最佳实践:
// 1. 使用条件判断代替 try-catch
// 不推荐
function getUserAge(user) {
try {
return user.profile.age;
} catch (e) {
return null;
}
}
// 推荐
function getUserAge(user) {
return user?.profile?.age ?? null;
}
// 2. 集中错误处理
class ErrorHandler {
static handle(error, context = "") {
console.error(`[${context}]`, error);
// 根据错误类型分类处理
if (error instanceof TypeError) {
// 类型错误处理
} else if (error instanceof NetworkError) {
// 网络错误处理
}
// 上报错误
this.report(error);
}
static report(error) {
// 发送到错误追踪服务
}
}
// 3. 自定义错误类
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
class NetworkError extends Error {
constructor(message, statusCode) {
super(message);
this.name = "NetworkError";
this.statusCode = statusCode;
}
}
// 使用
function validateUser(user) {
if (!user.email) {
throw new ValidationError("邮箱不能为空", "email");
}
}
try {
validateUser({});
} catch (error) {
if (error instanceof ValidationError) {
console.log(`字段 ${error.field} 验证失败: ${error.message}`);
}
}
// 4. Promise 错误处理
async function fetchData() {
try {
const response = await fetch("/api/data");
if (!response.ok) {
throw new NetworkError("请求失败", response.status);
}
return await response.json();
} catch (error) {
ErrorHandler.handle(error, "fetchData");
throw error; // 重新抛出,让调用者处理
}
}
// 5. 全局错误捕获
window.addEventListener("error", (event) => {
console.error("全局错误:", event.error);
ErrorHandler.handle(event.error, "global");
});
window.addEventListener("unhandledrejection", (event) => {
console.error("未处理的 Promise 拒绝:", event.reason);
ErrorHandler.handle(event.reason, "unhandledPromise");
});总结
JavaScript 面试题通常围绕以下几个核心概念:
1. 数据类型与转换:理解 JavaScript 的类型系统和隐式转换规则
2. 作用域与闭包:掌握函数作用域、闭包的形成和应用
3. this 关键字:理解不同调用方式下 this 的指向规则
4. 原型与继承:掌握原型链、继承的实现方式和应用场景
5. ES6+ 新特性:熟悉箭头函数、解构赋值、Promise、async/await 等
6. 数组与对象操作:熟练使用数组方法和对象操作技巧
7. 事件循环机制:理解宏任务、微任务的执行顺序和时机
8. 模块化系统:掌握 ES Modules 和 CommonJS 的区别和应用
9. DOM 操作与事件:熟悉事件委托、事件冒泡和捕获机制
10. 数据结构:理解 Map、Set、WeakMap、WeakSet 的特点和应用
11. 性能优化:掌握防抖、节流、内存管理等优化技巧
12. 高级函数技巧:理解柯里化、bind/call/apply 的实现原理
13. 错误处理:掌握优雅的错误处理方式和最佳实践
面试建议:
- 理解原理:不仅要知道结果,更要理解背后的原理
- 动手实践:亲自编写代码加深理解
- 举一反三:考虑不同场景下的边界情况
- 联系实际:将知识点与实际开发场景结合
- 系统学习:按照主题系统性地学习,而不是零散记忆
- 注重深度:深入理解核心概念,而不仅仅是背诵答案
掌握这些 JavaScript 核心概念,将帮助你在面试中展现出扎实的技术功底,也能在日常开发中避免常见的陷阱和错误。