前端面试考题合集---js篇✅✅✅

0 阅读9分钟

目标

将面试中js常考的题目搞清楚,弄明白。了解js的底层机制与原理,学会分析源码最好能手写出来。

js基础

es6新特性

  • 🚀1. 新增了let和const声明变量

let:具有块级作用域仅在{}内有效,适用于需要重新赋值的变量。 const:定义常量 且也是块级作用域仅在{}内有效,一旦赋值后不可更改,且必须在声明时初始化。

if (true) {
    var a = 10;   // `var` 具有函数作用域
    let b = 20;   // `let` 具有块级作用域
    const c = 30; // `const` 具有块级作用域
}
console.log(a); // ✅ 输出 10
console.log(b); // ❌ 报错:b is not defined
console.log(c); // ❌ 报错:c is not defined

还有就是var会变量提升到作用域的顶部,并初始化为underfined,let/const虽然也会变量提升,但其不会初始化,会处于暂时性死区中,无法访问。

console.log(a); // ✅ 输出 undefined
var a = 10;

console.log(b); // ❌ 报错:Cannot access 'b' before initialization
let b = 20;

对比图 image.png

  • 🧩 2. 箭头函数 箭头函数:更简洁的函数写法,使用=>定义。
// 传统函数
function add(a, b) {
    return a + b;
}
// 箭头函数
const add = (a, b) => a + b;
console.log(add(2, 3)); // 输出 5

那箭头函数与普通函数的区别呢?

image.png

// 箭头函数 this
const obj = {
  name: 'Alice',
  say: () => {
    console.log(this.name) // undefined (继承全局作用域的 this)
  },
}
obj.say()

// 普通函数 this
const obj = {
  name: 'Alice',
  say: function () {
    console.log(this.name) // "Alice" (this 指向 obj)
  },
}
obj.say()

// 箭头函数 不能作为构造函数
const Person = (name) => {
  this.name = name
}
const p = new Person('Alice') // TypeError: Person is not a constructor

// 普通函数 构造函数
function Person(name) {
  this.name = name
}
const p = new Person('Alice')
console.log(p.name) // "Alice"

// 箭头函数 ...args
const add = (...args) => {
  console.log(args) // [1, 2, 3]
}
add(1, 2, 3)

// 普通函数 arguments
function add() {
  console.log(arguments) // Arguments(3) [1, 2, 3]
}
add(1, 2, 3)

// 箭头函数 不支持 `bind/call/apply`
const obj = {
  value: 42,
}
const arrowFn = () => {
  console.log(this.value)
}
arrowFn.call(obj) // undefined

// 普通函数 支持 `bind/call/apply`
const obj = {
  value: 42,
}
function normalFn() {
  console.log(this.value)
}
normalFn.call(obj) // 42

📦 3. 模板字符串

使用反引号 `, 支持多行字符串变量插值

const a = 5;
const b = 10;
const message = `The sum of ${a} and ${b} is ${a + b}.`;
console.log(message); // 输出: The sum of 5 and 10 is 15.

📚 4. 解构赋值 快速提取对象或数组中的数据。

比如数组解构:

const [a, b, c] = [1, 2, 3];
console.log(a, b, c); // 输出 1 2 3

对象解构:

const person = { name: "Alice", age: 25 };
const { name, age } = person;
console.log(name, age); // 输出 Alice 25

🛠️ 5. ... 扩展运算符

用于展开数组或对象。

const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5];
console.log(arr2); // 输出 [1, 2, 3, 4, 5]

🧪 6. Promise以及async/await promise可以参考我的这篇文章🧠前端面试高频考题---promise,从五个方面搞定它🛠️ 前言 在面试之中关于promise经常被问起!这也许是 - 掘金

async/await下面会专门来讲。

🧱 8. 类

在es6之前js实现面向对象编程主要是通过构造函数加原型,但这样比较繁琐,es6引入了类使其更清晰易懂。

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    greet() {
        console.log(`Hi, my name is ${this.name}`);
    }
}

const p1 = new Person("Alice", 25);
p1.greet(); // Hi, my name is Alice

🚀9. 新增setmap两种新的数据结构 Set 是一种不允许重复值的数据结构,类似于数组,但不会存储重复值

Map键值对存储结构,类似于对象 {},但键可以是任何类型(包括对象、函数)。

image.png

JS 原型和原型链

这可以参考我的这篇文章:js原型

JS 继承以及继承有几种方式?

1. 原型链继承

核心思路:  让子类的 prototype 指向父类实例。

function Parent() {
  this.name = 'Parent'
}
Parent.prototype.sayHello = function () {
  console.log('Hello from Parent')
}

function Child() {}
Child.prototype = new Parent() // 继承 Parent
Child.prototype.constructor = Child

const child = new Child()
console.log(child.name) // "Parent"
child.sayHello() // "Hello from Parent"

✅ 优点:  父类方法可复用 ❌ 缺点:  1. 共享引用类型属性(如 arr = [] 会被多个实例共享),2. 无法向父类构造函数传参

2. 借用构造函数继承

核心思路:  在子类构造函数中使用 call 继承父类属性。

function Parent(name) {
  this.name = name
}
function Child(name, age) {
  Parent.call(this, name) // 继承 Parent
  this.age = age
}
const child = new Child('Rain', 18)
console.log(child.name, child.age) // "Rain", 18

✅ 优点:  1. 解决原型链继承共享问题,2. 可传参 ❌ 缺点:  无法继承父类原型上的方法

3. 组合继承(原型链 + 构造函数继承,最常用)

核心思路:  结合前两种方式,继承属性用构造函数,继承方法用原型链

function Parent(name) {
  this.name = name
}
Parent.prototype.sayHello = function () {
  console.log('Hello from Parent')
}

function Child(name, age) {
  Parent.call(this, name) // 第 1 次调用 Parent
  this.age = age
}

Child.prototype = new Parent() // 第 2 次调用 Parent
Child.prototype.constructor = Child

const child = new Child('Rain', 18)
console.log(child.name, child.age) // "Rain", 18
child.sayHello() // "Hello from Parent"

✅ 优点:  解决了前两种方法的缺陷 ❌ 缺点:  调用两次 Parent 构造函数(一次 call,一次 Object.create())

4. Object.create()和寄生式继承 这两种都是基于原型链继承来的,都差不多。

5.es6的class继承

ES6 引入了 class 和 extends 关键字,使得继承更加简洁和直观。

class Parent {
  constructor(name) {
    this.name = name;
  }

  sayName() {
    console.log(this.name);
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name); // 调用父类的构造函数
    this.age = age;
  }
}

const child = new Child('Child', 5);
child.sayName(); // 输出: Child
console.log(child.age); // 输出: 5

一般在实际开发中更推荐使用这个。

JS 作用域和作用域链

作用域:由可访问范围分为全局作用域,函数作用域,块级作用域。 作用域链:变量查找机制,从当前作用域逐级向上查找,直到全局作用域。

var a = 'global'

function outer() {
  var b = 'outer'

  function inner() {
    var c = 'inner'
    console.log(a, b, c) // ✅ global outer inner
  }

  inner()
}

outer()
console.log(b) // ❌ ReferenceError: b is not defined
  • inner() 内部:

    • c 在当前作用域内找到,直接使用。
    • b 当前作用域没有,向上查找 outer() 作用域,找到并使用。
    • a outer() 作用域没有,再向上查找全局作用域,找到并使用。
  • 查找顺序当前作用域 → 父级作用域 → 全局作用域

在es6中引入了let和const块级作用域避免了变量提升,以及闭包利用作用域链保留外部作用域的变量。

JS 闭包

闭包的定义:

闭包是指一个函数能够记住并访问它的词法作用域,即使这个函数在其词法作用域之外执行。换句话说,闭包使得函数可以“捕获”并保持对外部变量的引用,即使创建该函数的外部函数已经执行完毕。

实现闭包的基本步骤

  1. 定义外部函数:首先定义一个外部函数,在这个函数中声明一些局部变量。
  2. 定义内部函数:在外部函数内部定义一个或多个内部函数。这些内部函数可以访问外部函数的局部变量、参数以及整个作用域链。
  3. 返回内部函数:让外部函数返回内部函数(或者包含内部函数的对象)。这样即使外部函数已经执行结束,返回的内部函数仍然可以访问外部函数中的变量,因为它们形成了闭包。
function createCounter() {
  let count = 0 // 私有变量,外部无法直接访问
  return {
    increment: () => ++count,
    decrement: () => --count,
    getCount: () => count,
  }
}

const counter = createCounter()
console.log(counter.increment()) // 1
console.log(counter.increment()) // 2
console.log(counter.getCount()) // 2
console.log(counter.count) // undefined(外部无法直接访问)

闭包的缺点:

  1. 可能导致内存泄漏
  • 闭包会持有外部变量的引用,导致变量无法被垃圾回收
  • 解决方案:手动将变量置为 null 或谨慎管理作用域
  1. 滥用闭包可能影响性能
  • 每次调用都会创建新的作用域,影响垃圾回收机制
  • 适度使用,避免不必要的闭包

如何理解 JS 单线程?

什么是js单线程?

JavaScript 是 单线程 的意思是它只有一个线程来执行代码,这意味着它一次只能执行一个任务。所有的 JavaScript 代码,默认情况下,都会按照顺序在同一个线程中依次执行。单线程的特性使得 JavaScript 相比多线程语言在处理并发时有一些限制,但它也有一套机制来处理异步操作,避免阻塞主线程。 AmazedOmNomGIF.gif 为什么要单线程?

JavaScript 的设计目的是为了简化开发,尤其是在浏览器环境中。单线程可以避免多线程带来的复杂性,比如线程同步、资源竞争等问题。为了不让长时间的任务阻塞 UI 渲染,JavaScript 提供了异步编程的机制。

如何处理并发任务?

  1. 事件循环 :JavaScript 使用事件循环来管理异步任务。通过事件循环,JavaScript 可以在任务执行时不中断主线程的执行。异步任务(比如 setTimeoutPromiseXHR 等)会先进入 消息队列 ,当主线程空闲时,再从队列中取出任务执行。
  2. Web APIs:浏览器提供了 Web APIs(如 setTimeoutfetchDOM 等)来处理一些异步操作。这些操作会被交给浏览器的 API 处理,处理完后通过事件循环机制将回调函数推送到消息队列,等待主线程执行。
  3. 异步编程:通过 setTimeoutPromiseasync/await 等方式,JavaScript 可以非阻塞地处理 I/O 操作,避免卡住整个程序的执行。

什么是异步?异步的意义是什么?异步函数有哪些?异步函数与同步函数的区别?

什么是异步函数:异步(Asynchronous) 是指代码的执行不会阻塞后续操作,而是允许其他任务继续进行。例如,在 JavaScript 中,某些操作(如网络请求、文件读取等)需要较长时间执行,若使用同步方式(同步代码会阻塞后续代码执行),用户体验会变差,而异步可以让程序继续执行其他任务,在操作完成后再处理结果。

异步的意义:

  • 提升性能:异步操作不会阻塞主线程,可以并行执行多个任务,提高程序运行效率。

  • 提高用户体验:在前端开发中,异步请求(如 fetch)不会卡住页面,用户可以继续交互。

  • 避免程序卡死:如果使用同步操作读取大文件或请求服务器,可能会长时间卡住程序,而异步可以避免这个问题。

  • I/O 处理:异步特别适用于 I/O 密集型任务(如数据库查询、文件读写、网络请求等)。

常见的异步函数:回调函数,promise,async/await,事件监听。

同步函数和异步函数的区别: image.png

promise的定义及其使用

对于promise可以移步我的这篇文章🧠前端面试高频考题---promise,从五个方面搞定它🛠️ 前言 在面试之中关于promise经常被问起!这也许是 - 掘金

async/await

async/await是基于promise的语法糖,优化了代码风格,可以更清晰的编写异步函数,提高可读性。

  • async 关键字:用于声明一个异步函数,返回值始终是 Promise。
  • await 关键字:只能在 async 函数中使用,等待 Promise 解析(resolve)并返回结果,而不会阻塞线程。

Event Loop ,异步执行顺序,宏任务和微任务

可以查看我之前写的这篇文章传送门

总结

js在前端面试中有着很重比例,值得我们去深挖总结,祝各位面试顺利。

ForeignerSaluteTrendingGIF.gif