Skip to content

JavaScript 基础面试题:核心概念与实战解析

前言

JavaScript 是前端开发的核心基础,也是面试中最常被考察的部分。掌握 JavaScript 的核心概念不仅帮助你在面试中脱颖而出,更能让你在日常开发中写出更高质量的代码。本节将 JavaScript 核心知识点分解为系统化的面试题,帮助深入理解每个概念的原理和应用场景。

1. 变量与数据类型

面试题 1:JavaScript 数据类型转换

题目:以下代码的输出结果是什么?

javascript
console.log([] == ![]); // 题目1
console.log([] == ![]); // 题目2
console.log([] == ![]); // 题目3

答案

true
true
false

解析: 这道题考察了 JavaScript 中的类型转换规则和运算符优先级:

  1. == vs ===

    • == 会进行类型转换
    • === 不会进行类型转换,直接比较值和类型
  2. 逻辑非运算符 !

    • ![] 将空数组转换为布尔值 false,然后取反得到 true
    • ![] 转换为 false,所以 [] == ![] 相当于 [] == false,结果为 true
  3. 类型转换机制

javascript
// 数组转布尔值
Boolean([]); // false
Boolean({}); // true

// 数字转字符串
String(123); // "123"

// 布尔值转数字
Number(true); // 1
Number(false); // 0

延伸知识点

  • falsy 值false, 0, "", null, undefined, NaN
  • truthy 值:除 falsy 值外的所有值
  • 类型转换优先级! > == > === > && > ||

面试题 2:隐式类型转换陷阱

题目:以下代码的输出结果是什么?

javascript
console.log([] + 1 + "1" + 1);
console.log("foo" + +"bar");
console.log("foo" + +(+"bar"));

答案

"11"
"fooNaNbar"
"foo2bar"

解析: 这道题考察了 JavaScript 中的隐式类型转换和运算符优先级:

  1. [] + 1:空数组转数字为 00 + 1 = 1
  2. 1 + "1":数字与字符串相加,1 + "1" = "11"
  3. "foo" + + "bar"
    • "foo" + ++转为字符串,得到"foo+"`
    • +"bar" 字符串连接,"foo+" + "bar" = "fooNaNbar"
  4. + + +:一元 +,转为数字为 NaN
    • "foo" + NaN + "bar" = "foo2bar"

关键概念

  • 隐式类型转换规则
  • 运算符优先级和结合性
  • + 运算符的重载行为

2. 作用域与闭包

面试题 3:闭包与变量捕获

题目:以下代码的输出结果是什么?

javascript
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}

答案

3
3
3

解析: 这道题考察了 JavaScript 中的作用域和闭包概念:

  1. var 声明var 具有函数作用域,但在循环中声明的变量会被提升
  2. 闭包形成:每个 setTimeout 回调函数都形成了一个闭包,捕获了变量 i
  3. 异步执行时机setTimeout 是异步操作,所有回调函数在循环结束后才执行
  4. 变量值问题:由于所有回调函数共享同一个变量 i,而 i 在异步执行时已经等于 3

解决方案

javascript
// 方法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:闭包的实际应用

题目:以下代码实现了什么功能?有什么问题?

javascript
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 变量。

解析

  1. 闭包特性:每个返回的函数都访问了外层函数的 count 变量
  2. 变量共享问题:由于 var 的函数作用域特性,两个计数器实际上共享同一个变量
  3. 预期行为:开发者可能期望每个计数器独立工作

正确的实现

javascript
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 的指向问题

题目:以下代码的输出结果是什么?

javascript
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 的指向规则:

  1. 对象方法调用obj.getName()this 指向 obj
  2. 箭头函数:箭头函数没有自己的 this,会捕获外层的 this
  3. call/apply 调用:显式指定 this 的指向
  4. 函数赋值后调用:函数作为独立对象的方法调用时,this 指向全局对象(严格模式下为 undefined

this 指向规则总结

  • 默认绑定:直接调用函数,this 指向全局对象或 undefined
  • 隐式绑定:通过对象调用函数,this 指向该对象
  • 显式绑定:使用 call, apply, bind 指定 this
  • new 绑定:使用 new 调用构造函数,this 指向新创建的对象
  • 箭头函数:继承外层作用域的 this

面试题 6:构造函数中的 this

题目:以下代码的输出结果是什么?

javascript
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

答案

"张三"
"李四"
"张三"

解析

  1. 构造函数中的 thisnew Person("张三")this 指向新创建的对象实例
  2. 原型方法中的 thisp1.sayName()this 指向调用该方法的对象 p1
  3. apply 方法的作用p1.sayName.apply(p2) 将方法中的 this 指向 p2,但原型链上的 name 属性仍然是 p1

4. 原型与继承

面试题 7:原型链查找

题目:以下代码的输出结果是什么?

javascript
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"

解析

  1. 原型链查找:访问对象属性时,首先在对象本身查找,找不到时沿原型链向上查找
  2. 属性遮蔽:给 person 对象添加 sayHello 方法遮蔽了原型上的同名方法
  3. 原型共享:所有实例共享同一个原型对象

面试题 8:原型继承的实现

题目:如何手动实现 JavaScript 的继承?

javascript
function Parent(name) {
  this.name = name;
}

Parent.prototype.sayName = function () {
  console.log(this.name);
};

function Child(name, age) {
  // 继承 Parent 的属性和方法
}

// 实现 Child 继承 Parent

答案

javascript
// 方法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:箭头函数与普通函数的区别

题目:以下代码的输出结果是什么?

javascript
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

解析

  1. 箭头函数特性:箭头函数没有自己的 thisarguments,不能作为构造函数使用
  2. this 指向
    • 普通函数的 this 由调用方式决定
    • 箭头函数的 this 在定义时确定,不可改变
  3. 词法作用域绑定:箭头函数的 this 绑定到定义时的词法作用域

面试题 10:解构赋值与扩展运算符

题目:以下代码的输出结果是什么?

javascript
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

解析

  1. 扩展运算符 ...:用于展开数组或对象
  2. 解构赋值:从数组或对象中提取值并赋给变量
  3. 剩余参数:使用 ...rest 语法收集剩余的参数
  4. 应用场景:函数参数处理、数组操作、对象合并等

面试题 11:Promise 和 async/await

题目:以下代码的输出结果是什么?

javascript
// 题目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

解析

  1. 事件循环机制:同步代码 > 微任务(Promise.then) > 宏任务(setTimeout)
  2. async/await 本质:语法糖,底层仍然是 Promise
  3. 执行顺序
    • 同步代码立即执行
    • 微任务在当前脚本执行完后立即执行
    • 宏任务在下一轮事件循环中执行

6. 数组与对象操作

面试题 12:数组方法的高级应用

题目:实现以下数组操作方法

javascript
// 实现数组的扁平化
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 分组
}

答案

javascript
// 数组扁平化
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:深拷贝与浅拷贝

题目:以下代码的输出结果是什么?

javascript
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); // 扩展运算符

答案

北京
北京
广州
深圳

解析

  1. 浅拷贝:只拷贝对象的第一层属性,嵌套对象仍然是引用
    • Object.assign()
    • 扩展运算符 ...
  2. 深拷贝:递归拷贝所有层级的属性,创建完全独立的对象
    • JSON.parse(JSON.stringify())
    • 递归函数实现

深拷贝的实现

javascript
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:宏任务与微任务的执行顺序

题目:以下代码的输出顺序是什么?

javascript
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. 执行栈:同步代码优先执行(1, 6)
  2. 微任务队列:Promise.then 属于微任务,在当前宏任务执行完后立即执行
  3. 宏任务队列:setTimeout 属于宏任务,在下一轮事件循环执行

执行流程

  • 同步代码:打印 1, 6
  • 第一个微任务:打印 4,将 setTimeout(5) 加入宏任务队列
  • 第一个宏任务:打印 2,将 Promise.then(3) 加入微任务队列
  • 微任务:打印 3
  • 下一个宏任务:打印 5

宏任务与微任务分类

javascript
// 宏任务 (Macrotask)
// - setTimeout / setInterval
// - I/O 操作
// - UI 渲染
// - setImmediate (Node.js)

// 微任务 (Microtask)
// - Promise.then / catch / finally
// - async/await
// - MutationObserver
// - process.nextTick (Node.js)

面试题 15:async/await 的执行时机

题目:以下代码的输出结果是什么?

javascript
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

解析

  1. async/await 本质await 后面的代码相当于在 Promise.then 中执行
  2. 执行顺序
    • 同步代码:script start → async1 start → async2 → promise1 → script end
    • 微任务:async1 end → promise2
    • 宏任务:setTimeout

8. 模块化系统

面试题 16:ES Modules 与 CommonJS 的区别

题目:ES6 模块(ES Modules)和 CommonJS 模块有什么区别?

答案

特性ES ModulesCommonJS
加载方式编译时加载(静态)运行时加载(动态)
输出值的引用值的拷贝
this 指向undefined当前模块
循环依赖可以处理可能出现问题
使用场景浏览器、现代 Node.jsNode.js (传统)
导入语法import / exportrequire / module.exports

代码示例

javascript
// 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:模块循环依赖问题

题目:如何处理模块的循环依赖?

答案

javascript
// 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:事件委托的原理和应用

题目:什么是事件委托?如何实现一个高效的事件委托?

答案

事件委托:利用事件冒泡机制,将子元素的事件监听器委托给父元素处理。

优点

  • 减少内存消耗(只需一个监听器)
  • 动态添加的元素也能响应事件
  • 提升性能

实现示例

javascript
// 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:阻止事件冒泡和默认行为

题目:如何阻止事件冒泡和默认行为?它们有什么区别?

答案

javascript
// 阻止事件冒泡
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?

答案

特性MapObject
键的类型任意类型字符串或 Symbol
键的顺序插入顺序不保证顺序(ES2015+部分有序)
大小获取map.sizeObject.keys(obj).length
迭代直接可迭代需要 Object.keys()
性能频繁增删时更优简单场景足够
序列化不支持 JSON支持 JSON

代码示例

javascript
// 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 特点

  • 值必须是对象
  • 值是弱引用
  • 不可迭代

应用场景

javascript
// 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)函数。

答案

javascript
// 防抖:事件触发 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 中常见的内存泄漏场景有哪些?如何避免?

答案

常见内存泄漏场景

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.memory API

12. 高级函数技巧

面试题 24:函数柯里化的实现

题目:什么是函数柯里化?请实现一个通用的柯里化函数。

答案

柯里化:将多参数函数转换为一系列单参数函数的过程。

javascript
// 简单示例
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.bindcallapply 方法。

答案

javascript
// 实现 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 性能
  • 主要开销在于抛出错误时的堆栈跟踪
  • 不要在性能关键代码中过度使用

最佳实践

javascript
// 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 核心概念,将帮助你在面试中展现出扎实的技术功底,也能在日常开发中避免常见的陷阱和错误。

上次更新时间: