JavaScript 面试题集合

目录

  • 基础部分
    • 1、var, let,const的区别
    • 2. JavaScript 数据类型有哪些?
    • 3. == 与 === 的区别?
    • 4. 什么是闭包(Closure)?
    • 5. this 是什么?在箭头函数中表现如何?
    • 6、防抖和节流的的理解
  • 二、中级部分
    • 1、原型链和继承机制
    • 2、事件循环
    • 3、Promise 是什么?如何实现链式调用?
  • 三、高级部分
    • 1. 深拷贝与浅拷贝的区别?如何实现深拷贝?
    • 2. async/await 是如何实现的?与 Promise 区别?
    • 3.箭头函数和普通函数的区别

基础部分

1、var, let,const的区别

特性 var let const
作用域 函数作用域 块级作用域 块级作用域
是否提升 ✅(提升为 undefined) ✅(但不初始化) ✅(但不初始化)
是否可以重复声明
是否可以修改
  • var :函数级作用域。在函数内部声明的变量仅在该函数内部有效;在函数外声明的变量则在全局范围内有效。
  • let :块级作用域。在 {} 包围的代码块中声明的变量仅在该代码块内有效。
  • const :块级作用域,与 let 相同。
function varTest() {
  var x = 1;
  if (true) {
    var x = 2; // 同一个函数内,var 声明的变量会被提升,后声明的会覆盖前声明的
    console.log(x); // 2
  }
  console.log(x); // 2
}

function letTest() {
  let y = 1;
  if (true) {
    let y = 2; // 块级作用域,这里声明的 y 不会影响外层的 y
    console.log(y); // 2
  }
  console.log(y); // 1
}

varTest();
letTest();

重复声明

  • var :允许在同一个作用域内重复声明变量,后面的声明会覆盖前面的声明。
  • let :不允许在同一个作用域内重复声明变量,否则会报错。
  • const :同样不允许在同一个作用域内重复声明变量,否则会报错。
var d = 1;
d = 2; // 合法,可以重新赋值
console.log(d); // 2

let e = 1;
e = 2; // 合法,可以重新赋值
console.log(e); // 2

const f = 1;
f = 2; // 报错,不能重新赋值

const g = { name: "Alice" };
g.name = "Bob"; // 合法,可以修改对象的属性
console.log(g.name); // "Bob"

const h = [1, 2, 3];
h.push(4); // 合法,可以修改数组的元素
console.log(h); // [1, 2, 3, 4]

提升(Hoisting)

  • var :变量会被提升到其所在作用域的顶部,但会初始化为 undefined。
  • let :变量也会被提升,但不会被初始化,所以在声明前访问变量会报错(处于暂时性死区)。
  • const :与 let 相同,变量会被提升,但在声明前访问会报错。
var d = 1;
d = 2; // 合法,可以重新赋值
console.log(d); // 2

let e = 1;
e = 2; // 合法,可以重新赋值
console.log(e); // 2

const f = 1;
f = 2; // 报错,不能重新赋值

const g = { name: "Alice" };
g.name = "Bob"; // 合法,可以修改对象的属性
console.log(g.name); // "Bob"

const h = [1, 2, 3];
h.push(4); // 合法,可以修改数组的元素
console.log(h); // [1, 2, 3, 4]

可变性

  • var :声明的变量可以重新赋值。
  • let :声明的变量也可以重新赋值。
  • const :声明的变量不能重新赋值,但如果是引用类型(如对象或数组),可以修改其内部的属性或元素。
- var d = 1;
d = 2; // 合法,可以重新赋值
console.log(d); // 2

let e = 1;
e = 2; // 合法,可以重新赋值
console.log(e); // 2

const f = 1;
f = 2; // 报错,不能重新赋值

const g = { name: "Alice" };
g.name = "Bob"; // 合法,可以修改对象的属性
console.log(g.name); // "Bob"

const h = [1, 2, 3];
h.push(4); // 合法,可以修改数组的元素
console.log(h); // [1, 2, 3, 4]

2. JavaScript 数据类型有哪些?

原始类型:(Primitive):string、number、boolean、null、undefined、symbol、bigint
引用类型:object、array、function

3. == 与 === 的区别?

== 会进行类型转换(隐式)

=== 是严格等于(类型和值 都相同)

4. 什么是闭包(Closure)?

闭包是一个有权访问另一个函数作用域中变量的函数,创建闭包的常见方式是在一个函数内部创建另一个函数,并且这个内部函数访问了外部函数的变量,然后把内部函数作为返回值返回。

function createCounter() {
  let count = 0; // count 是外层函数的局部变量
  return function() {
    count++; // 内层函数访问并修改了外层函数的变量
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 输出 1
console.log(counter()); // 输出 2
console.log(counter()); // 输出 3

5. this 是什么?在箭头函数中表现如何?

  • this 取决于调用位置(运行时绑定)

  • 箭头函数不会绑定自己的 this,它继承自定义时的作用域。

const obj = {
  num: 42,
  func: function () {
    console.log(this.num);
  },
  arrow: () => {
    console.log(this.num); // undefined
  }
};

6、防抖和节流的的理解

  • 防抖多次点击只执行最后一次
    防抖适用于需要等待用户操作完全停止后再执行任务的场景,比如搜索框的输入提示、窗口大小调整后的布局调整等。

  • 下面是一个简单的防抖函数实现:

function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// 使用示例
const handleSearchDebounce = debounce(function(event) {
  console.log('搜索内容:', event.target.value);
}, 300);

// 绑定到输入事件上
document.getElementById('searchInput').addEventListener('input', handleSearchDebounce);
  • 节流一段时间内只执行一次
    不管事件在这段时间内被触发了多少次,函数只会在设定的时间间隔开始时或结束时执行一次。节流适用于需要定期执行任务的场景,比如滚动事件监听、鼠标移动事件监听等。
    节流的实现方式可以采用时间戳法或者定时器法。以下是时间戳法的节流函数实例
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// 使用示例
const handleSearchDebounce = debounce(function(event) {
  console.log('搜索内容:', event.target.value);
}, 300);

// 绑定到输入事件上
document.getElementById('searchInput').addEventListener('input', handleSearchDebounce);

二、中级部分

1、原型链和继承机制

面试可答:

  • 原型链是javascript中实现继承的一种机制,每个对象都有一个原型对象,当访问对象的属性货或者方法时,如果对象本身不存在这个属性和方法,就会沿着原型链向上查找。原型链由对象_proto_属性和构造函数的prototype属性构成,通过原型链,对象可以继承其他对象的属性和方法。
  • 继承 机制允许一个类继承另一个类的属性和方法,从而实现代码复用,在javascript中主要通过原型链来实现继承。一个常见的做法是将子类的原型设置为父类的实力,这样子类就可以继承父类的属性和方法。此外还需要修正子类构造函数的指向,以确保实力的狗砸函数正确
  • 例如,通过object。create 方法可以很方便的创建一个新对象,并将其原型设置为另一个对象,从而实现继承,这种方式不仅可以继承父类的属性和方法,还可以添加子类的特有的属性和方法。
  • 示例:原型链查找
const person = {
  name: "Alice",
  greet() {
    return `Hello, ${this.name}!`;
  }
};

const student = Object.create(person); // 创建一个以 person 为原型的新对象
student.age = 20;

console.log(student.greet()); // 输出 "Hello, Alice!"
// 解释:student 本身没有 greet(),但沿原型链找到了 person.greet

2、事件循环

JS 是单线程语言,依赖事件循环机制处理异步任务
答:

  • 事件循环是javascript中的一个非常重要的概念。它解释了javscript如何在单线程下执行异步操作。

  • javascript是单线程语言,这意味这所有代码都在一个线程上执行,包括事件处理、定时器、回调函数等。但由于浏览器提供了web API (如定时器、Http请求、DOM操作等),这些API大部分都是异步的,可以在后台执行。

事件循环的核心包括

  • 调用栈(Call Stack):用于跟踪正在执行的函数调用序列。当一个函数被调用时,它会被压入调用栈,当函数执行完毕时,它会被弹出调用栈。
  • 任务队列(Task Queue):用储存在调用栈中的代码执行完毕之后,需要执行异步回调函数。任务队列是一个先进先出(FIFO)的队列
  • 事件循环(Event Loop):不断监测调用栈和任务队列。当调用栈为空时,事件循环会从队列中取出任务并压入调用栈执行。

- 事件循环的工作流程如下:

  • 1、所有同步代码首先在调用栈中执行
  • 异步操作(如 setTimeOut、serInterval、fetch请求等)由web API处理,完成后会将回调函数放入任务队列
  • 事件循环会不断地检查调用栈是否为空。如果调用栈为空,它会从任务队列中取出任务并压入调用栈中执行
    实例:
console.log('Start');

setTimeout(() => {
  console.log('Timeout');
}, 0);

console.log('End');

输出结果是:

Start
End
Timeout

这是因为:

1、console.log(‘Start’) 是同步代码,首先执行并输出 ‘Start’。

2、setTimeout 是异步操作,将其回调函数放入 Web API 中执
行。即使延迟时间为 0,回调函数也会被放入任务队列,等待事件循环处理。

3、console.log(‘End’) 是同步代码,执行并输出 ‘End’。

4、当调用栈为空时,事件循环从任务队列中取出 setTimeout 的回调函数并执行,输出 ‘Timeout’。

事件循环确保了 JavaScript 能在单线程环境下高效地处理异步操作,避免了界面的卡顿。在实际开发中,合理利用事件循环机制可以优化代码的性能和用户体验。”

3、Promise 是什么?如何实现链式调用?

PromiseJavaScript 中用于处理异步操作的一种对象,它代表一个异步操作的最终完成(或失败)以及其结果值。Promise 有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。Promise 的状态一旦改变,就不会再变,这使得它可以安全地被传递和操作。
Promise 的链式调用是通过 .then() 方法实现的。.then() 方法本身返回一个新的 Promise,这允许我们将多个 .then() 调用串联起来,形成链式调用。这样可以更清晰地处理多个依赖的异步操作,避免嵌套的回调函数(回调地狱)。
下面是一个简单的 Promise 实现链式调用的例子:

// 创建一个简单的 Promise
const promise = new Promise((resolve, reject) => {
  // 模拟异步操作
  setTimeout(() => {
    resolve('Promise 成功');
  }, 1000);
});

// 链式调用
promise
  .then(result => {
    console.log(result); // 输出 "Promise 成功"
    return '第一个 then 的返回值';
  })
  .then(result => {
    console.log(result); // 输出 "第一个 then 的返回值"
    return '第二个 then 的返回值';
  })
  .then(result => {
    console.log(result); // 输出 "第二个 then 的返回值"
  });

在链式调用中,每个 .then() 方法都会接收上一个 .then() 方法返回的值作为参数,并且可以返回一个新的值或 Promise 给下一个 .then() 方法处理。这样可以实现异步操作的顺序执行和数据传递。
如果在 .then() 方法中返回一个新的 Promise,那么下一个 .then() 会等待这个新 Promise 的状态改变后再执行:

promise
  .then(result => {
    console.log(result); // 输出 "Promise 成功"
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve('新的 Promise 的返回值');
      }, 500);
    });
  })
  .then(result => {
    console.log(result); // 输出 "新的 Promise 的返回值"
  });

三、高级部分

1. 深拷贝与浅拷贝的区别?如何实现深拷贝?

  • 浅拷贝:只拷贝第一层引用

  • 深拷贝:拷贝所有层级的值

基本概念:

  • 浅拷贝:只拷贝对象或数组的最外层属性或元素,对于引用类型的属性或元素,拷贝的只是其引用(内存地址),而不是其包含的实际数据。如果原始对象或数组的引用类型属性或元素发生更改,浅拷贝后的对象或数组也会受到影响。

  • 深拷贝:不仅拷贝对象或数组的最外层属性或元素,还会递归地拷贝其内部的所有引用类型属性或元素,直到所有层级的数据都被拷贝。深拷贝后的对象或数组与原始对象或数组完全独立,彼此之间的更改不会相互影响。

  • 如何实现深拷贝
    使用 JSON.parse(JSON.stringify()) 方法
    适用于简单对象和数组,不包含函数、Date、RegExp、Map、Set 等特殊对象。
    代码示例:

const originalObject = { a: 1, b: { c: 2 } };
const deepCopiedObject = JSON.parse(JSON.stringify(originalObject));

递归实现深拷贝
可以处理更多数据类型,包括函数、Date、RegExp、Map、Set 等。
代码示例:

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null) return null; // 如果是 null,直接返回 null
  if (obj instanceof Date) return new Date(obj); // 日期对象直接返回新的日期对象
  if (obj instanceof RegExp) return new RegExp(obj); // 正则对象直接返回新的正则对象
  if (obj instanceof Map) return new Map(obj); // Map 对象直接返回新的 Map 对象
  if (obj instanceof Set) return new Set(obj); // Set 对象直接返回新的 Set 对象
  if (typeof obj !== 'object') return obj; // 如果不是对象,直接返回
  if (hash.has(obj)) return hash.get(obj); // 如果对象已拷贝过,直接返回已拷贝的对象,防止循环引用

  const cloneObj = Array.isArray(obj) ? [] : {}; // 判断是数组还是对象
  hash.set(obj, cloneObj); // 将对象存入 WeakMap,标记为已拷贝

  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 递归拷贝对象的属性
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }

  return cloneObj;
}

// 使用示例
const originalObj = {
  a: 1,
  b: { c: 2 },
  d: [3, 4],
  e: new Date(),
  f: /regex/g,
  g: new Map([['key', 'value']]),
  h: new Set([1, 2, 3]),
  i: function () { console.log('function'); }
};

const deepCopiedObj = deepClone(originalObj);

面试可答:
深拷贝和浅拷贝是 JavaScript 中对象和数组拷贝的两种方式。
浅拷贝只拷贝最外层的属性或元素,引用类型的属性或元素只是拷贝了引用,因此原始数据和拷贝数据在引用类型部分是共享的。如果原始数据的引用类型部分发生更改,浅拷贝的数据也会受到影响。常见的浅拷贝方法有对象展开运算符(…)、Array.prototype.slice() 和 Object.assign()。

深拷贝不仅拷贝最外层的属性或元素,还会递归地拷贝所有层级的引用类型数据,使得原始数据和拷贝数据完全独立。常见的深拷贝方法包括使用 JSON.parse(JSON.stringify())(适用于简单对象和数组)和递归实现的深拷贝函数(可以处理更多数据类型,如函数、Date、RegExp、Map、Set 等)。

实现深拷贝时需要注意处理循环引用问题,避免栈溢出。通常可以通过使用 WeakMap 来记录已拷贝的对象,避免重复拷贝。
例如,使用递归实现深拷贝时,可以处理对象、数组、日期、正则、Map、Set 和函数等多种数据类型,并通过 WeakMap 解决循环引用问题。”

2. async/await 是如何实现的?与 Promise 区别?

async 返回一个 Promise

await 等待 Promise 解析结果(使代码像同步)
面试可答:
async/await 是基于 Promise 的语法糖,它使得异步代码的书写更接近同步代码,提高了代码的可读性和可维护性。

async 函数会返回一个 Promise 对象,而 await 关键字用于等待 Promise 的解析完成。与 Promise 相比,async/await 的语法更简洁,错误处理更直观(可以使用 try/catch),调试体验更好。

在实际开发中,async/await 和 Promise 可以互换使用,但 async/await 通常能让代码更清晰,尤其是在处理多个异步操作时。

  • async/await 的实现原理:
    async 函数 :async 函数会返回一个 Promise 对象。如果函数中没有显式返回值,则返回一个自动解析为 undefined 的 Promise。如果函数中返回非 Promise 类型的值,则会自动包装成一个解析为该值的 Promise。

  • await 关键字 :只能在 async 函数内部使用,用于等待一个 Promise 对象的解析完成。await 表达式会暂停函数的执行,直到 Promise 被解析或拒绝,并返回解析后的值。如果 Promise 被拒绝,await 表达式会抛出错误,可以用 try/catch 来捕获。
    基本实现原理 :async/await 内部是基于 Promise 和生成器(Generator)实现的。async 函数本质上是一个生成器函数,它通过 yield 表达式暂停和恢复函数的执行。await 关键字会将异步操作的 Promise 对象包装成一个生成器的 yield 表达式,然后通过一个状态机来管理异步操作的流程控制。

  • async/await 与 Promise 的区别
    语法简洁性 :async/await 的语法更简洁,更接近同步代码的书写方式,避免了 .then() 链的嵌套,使得代码更易读、更易维护。
    错误处理 :Promise 的错误处理通常需要使用 .catch() 方法或者在 .then() 链的最后添加一个错误处理函数。而 async/await 可以使用 try/catch 语句块来捕获错误,更符合传统同步代码的错误处理习惯。

调试体验 :async/await 的代码在调试时更方便,因为它的代码结构更接近同步代码,栈跟踪更清晰,方便开发者进行调试和排查错误。

功能一致性 :async/await 和 Promise 在功能上是一致的,都可以处理异步操作。async/await 实际上是对 Promise 的语法糖,它内部还是基于 Promise 来实现异步逻辑。

3.箭头函数和普通函数的区别

**面试可答:

  • 箭头函数和普通函数最本质的区别是:
  • 箭头函数没有自己的 this,它会继承定义时的外层作用域;
  • 它也没有 arguments、super、new.target;
  • 不能用作构造函数;
  • 一般适合回调函数、定时器、或需要保留上下文 this 的地方。**
      1. this 的指向不同(最重要!)普通函数:this 是 调用者对象

✅ 1. this 的指向不同(最重要!)
普通函数:this 是 调用者对象

const obj = {
  name: 'Tom',
  sayHello: function () {
    console.log(this.name); // this -> obj
  }
};
obj.sayHello(); // Tom

箭头函数:this 是 定义时所在的作用域(不会被绑定)

const obj = {
  name: 'Tom',
  sayHello: () => {
    console.log(this.name); // this -> window(或 undefined 严格模式)
  }
};
obj.sayHello(); // undefined(浏览器中)

面试关键词:箭头函数没有自己的 this,它会“捕获”外围作用域的 this。

✅ 2. 是否有 arguments 对象
普通函数中有 arguments,表示所有传入的参数。

箭头函数中没有 arguments,只能用 rest 参数 …args。

function normalFn() {
  console.log(arguments); // 类数组对象
}

const arrowFn = (...args) => {
  console.log(args); // 真数组
};

✅ 3. 能否作为构造函数使用(new)
普通函数可以作为构造函数用

箭头函数不能用 new,没有 [[Construct]]

function Person(name) {
  this.name = name;
}
const p = new Person('Tom'); // ✅

const Arrow = (name) => {
  this.name = name;
};
// const a = new Arrow('Tom'); ❌ TypeError: Arrow is not a constructor

✅ 4. 有无原型 prototype
普通函数有 .prototype

箭头函数没有 .prototype 属性

function fn() {}
console.log(fn.prototype); // {}

const arrow = () => {};
console.log(arrow.prototype); // undefined

✅ 5. 行为差异场景:定时器、事件监听
✅ 定时器中的 this

function Timer() {
  this.count = 0;
  setInterval(function () {
    this.count++; // ❌ this → window
    console.log(this.count);
  }, 1000);
}

// 用箭头函数解决
function TimerFixed() {
  this.count = 0;
  setInterval(() => {
    this.count++; // ✅ this → TimerFixed 实例
    console.log(this.count);
  }, 1000);
}

✅ 总结对比表格

特性 普通函数 箭头函数
this 调用时动态绑定 定义时静态绑定
arguments
new 构造 ✅ 可使用 ❌ 报错
prototype 属性
用作事件处理器 this 为触发元素 this 为外层作用域

你可能感兴趣的:(javascript,前端)