Skip to content

JavaScript 高频面试题

一、基础概念

1. 什么是 JavaScript?

JavaScript 是一种脚本语言,主要用于 Web 开发,可在浏览器端运行,也可在服务器端通过 Node.js 运行。它是一种动态、弱类型、基于原型的语言,支持面向对象、函数式和命令式编程风格。

2. JavaScript 的数据类型有哪些?

  • 基本数据类型:Number、String、Boolean、Null、Undefined、Symbol、BigInt
  • 引用数据类型:Object(包括 Array、Function、Date、RegExp 等)

3. 基本数据类型和引用数据类型的区别?

  • 存储位置:基本类型存储在栈中,引用类型存储在堆中,栈中存储引用地址
  • 复制方式:基本类型复制值,引用类型复制引用地址
  • 比较方式:基本类型比较值,引用类型比较引用地址

4. 什么是闭包?

闭包是指有权访问另一个函数作用域中变量的函数。闭包的形成需要满足两个条件:

  1. 函数嵌套
  2. 内部函数引用外部函数的变量

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. 浏览器的事件循环是什么?

事件循环是浏览器处理异步操作的机制,包括以下步骤:

  1. 执行同步代码
  2. 处理微任务(Promise、MutationObserver 等)
  3. 处理宏任务(setTimeout、setInterval、DOM 事件等)
  4. 重复以上步骤

40. 什么是微任务和宏任务?

  • 微任务:Promise、MutationObserver、process.nextTick(Node.js)
  • 宏任务:setTimeout、setInterval、setImmediate(Node.js)、I/O 操作、DOM 事件

41. 浏览器的渲染过程是什么?

  1. 解析 HTML:生成 DOM 树
  2. 解析 CSS:生成 CSSOM 树
  3. 合并 DOM 和 CSSOM:生成渲染树
  4. 布局:计算元素的位置和大小
  5. 绘制:将元素绘制到屏幕上
  6. 合成:将图层合成到一起

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 代码

Released under the MIT License.