JavaScript面试题
1.js的数据类型有哪些以及它们的区别
有7种原始类型:String,Number,Boolean,Undefined,Null,Symbol,BigInt和引用类型:Object、Array、Function
Symbol和BigInt是ES6,ES11新增的数据类型
- Symbol代表创建后不可变的独一无二的数据类型,为了解决可能出现的全局变量冲突的问题
- BigInt表示任意精度格式的整数,可以更安全地存储和操作大整数
可以分为原始数据类型和引用数据类型
- 原始数据类型:
放在栈区(栈区内存由编译器自动分配释放,栈区数据先进后出),属于大小固定,频繁被使用的数据
String,Bumber,Boolean,Undefined,NullSymbol,BigInt
- 原始数据类型:
- 引用数据类型:
放在堆区,(堆区内存由程序员分配释放或垃圾回收机制回收,堆按照优先级进行排序)
- 引用数据类型:
2.let/const/var区别
var 是老版本 JavaScript 的变量声明方式,容易因为作用域和提升导致问题;
let 和 const 是 ES6 引入的新特性,推荐优先使用。
let 可以重新赋值,但不能重复声明;const 不能重新赋值,适合声明常量。
比如在循环或条件语句中,用 let 可以避免变量泄露成全局作用域
3.闭包
闭包就是函数能记住它创建时的环境。
外面可以读取数据,但是不能修改。在实际开发中,闭包的主要作用是保存变量的状态,实现数据的私有化和模块化,或者在异步操作中保存上下文,实现数据的私有,保护数据,封装私有状态
一般闭包都是由一个函数包起来的(如这里的outer)对里面的变量起到保护作用
闭包应用:实现节流和防抖
1 | function outer() { |
在F12->Sources->右侧的Scope下面的Closure中可以看到闭包
闭包必须return吗
闭包不一定要return,如果外部想要使用闭包里的变量,此时需要return把值返出去
闭包一定会内容泄露吗
- 闭包不一定会内存泄漏;内存泄露的场景如图,由于result没有被销毁所以count没有被销毁,count出现了内存泄露
闭包是指函数可以访问其定义时所在词法作用域中的变量,即使在该函数执行时,这些变量已经不在作用域内。闭包的主要作用是保存变量的状态,实现数据的私有化和模块化。在JavaScript中,闭包常用于创建私有变量和方法,以及实现函数柯里化和记忆化等高级功能。需要注意的是,过度使用闭包可能会导致内存泄漏,因此在使用闭包时需要注意内存管理。
4.== 和 === 的区别
== 会自动转换类型再比较,比如数字和字符串会转成同一种类型再对比;而 === 会直接判断值和类型是否完全一致。为了避免隐式转换的坑,通常用 ===,尤其是处理表单输入或 JSON 数据时,确保类型和值都正确
==(宽松相等):比较值前会进行类型转换(隐式类型转换)
===(严格相等):比较值和类型,不进行类型转换
1 | 1 == '1'; // true(隐式转换为 1 == 1) |
5.事件循环(Event Loop)的基本原理
JavaScript是单线程的,事件循环是 JavaScript 处理异步的核心机制
执行流程:同步代码 → 清空微任务队列 → 执行一个宏任务 → 重复
比如下面的例子,console.log(‘A’) 和 console.log(‘D’) 是同步代码先执行;Promise.then 是微任务,会插队到同步代码后面执行;而 setTimeout 是宏任务,最后执行。
理解这一点对调试异步代码特别重要,比如处理数据加载或动画时
1 | console.log('A'); |
执行顺序:
同步代码:直接执行。
微任务(Microtask):Promise.then、queueMicrotask 等由js引擎发起。
(promise本身是同步的,但是里面的then/catch的回调函数的异步的微任务)
宏任务(Macrotask):setTimeout、setInterval、DOM 事件,I/O、UI 渲染,由宿主环境发起。
执行流程:同步代码 → 清空微任务队列 → 执行一个宏任务 → 重复
js事件循环,同步与异步
js是浏览器脚本语言,是单线程(同一时间只能做一件事),为了防止阻塞代码,把代码分为同步和异步
- 同步代码
- 立即放入Js引擎(执行栈)执行,并原地等待效果
- 异步代码:耗时的(定时器,ajax/fetch,事件绑定)
- 先放入宿主环境(浏览器、Node),(不阻塞主线程继续执行),发生后的回调函数放到任务队列,执行栈内容结束后从任务队列取任务,执行回调函数
执行栈会反复从任务队列中查看有没有事件,这个过程称为事件循环
- 先放入宿主环境(浏览器、Node),(不阻塞主线程继续执行),发生后的回调函数放到任务队列,执行栈内容结束后从任务队列取任务,执行回调函数
宏任务包裹微任务–代码例子
1 | console.log('Start'); // 同步代码1 |
考察的是 JavaScript 的事件循环机制,也就是宏任务和微任务的执行顺序
- 执行同步代码
同步代码会直接进入调用栈执行
所以前两行的输出是 Start 和 End,这是同步代码的执行结果 - 处理宏任务
宏任务(setTimeout)会被放入宏任务队列中,等待下一轮事件循环执行
这两个宏任务不是立刻执行的,它们会等到当前宏任务(主代码块)结束后,再进入事件循环的下一轮 - 事件循环开始
当前调用栈空了(同步代码执行完毕),事件循环开始从 宏任务队列 中取出第一个宏任务执行
执行宏任务1(Timeout 1)
console.log(‘Timeout 1’) → 输出 Timeout 1
遇到 Promise.resolve().then(…) → 这是一个 微任务,会被放入微任务队列
执行微任务队列,微任务1(Microtask 1)被立即执行 → 输出 Microtask 1
微任务队列清空后,事件循环进入下一轮,处理下一个宏任务。
这里关键点是:微任务会插在宏任务之间执行。即使宏任务中嵌套了微任务,微任务也会在当前宏任务结束后立即执行 - 执行宏任务2
最终的输出顺序是:Start → End → Timeout 1 → Microtask 1 → Timeout 2 - 事件循环的入栈出栈流程图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
191. 调用栈执行同步代码:
[Start, End] → 输出 Start, End
2. 宏任务队列:
[Timeout 1, Timeout 2]
3. 事件循环第一轮:
- 执行 Timeout 1:
- 调用栈: [Timeout 1]
- 输出 Timeout 1
- 生成 Microtask 1 → 微任务队列: [Microtask 1]
- 执行 Microtask 1:
- 调用栈: [Microtask 1]
- 输出 Microtask 1
4. 事件循环第二轮:
- 执行 Timeout 2:
- 调用栈: [Timeout 2]
- 输出 Timeout 2
1 | console.log('Start'); |
6.typeof 和 instanceof 的区别
typeof 主要用来判断基本类型,比如数字、字符串,但对对象、数组、null 都会返回 “object”,所以不够准确。而 instanceof仅适用于引用类型,无法检测基本类型,能判断对象是否属于某个类的实例,比如数组是否是 Array 的实例。
注意instanceof依赖原型链,如果对象的原型被修改,结果可能不准确。实际开发中,我会结合两者使用,比如先用 typeof 判断类型,再用 instanceof 深入验证
typeof:返回字符串,用于检测基本数据类型(如 number、string、object)。
instanceof:用于检测对象是否属于某个构造函数的实例(基于原型链)
typeof:快速但不准确。
instanceof:适用于引用类型。
Object.prototype.toString.call():最可靠。
Array.isArray():检测数组专用
1 | typeof 123; // "number" |
其他数据类型检测方式
1. 严谨的检测方法Object.prototype.toString.call()
使用Object对象的原型方法 toString 来判断数据类型
返回对象的内部 [[Class]] 属性,返回格式为 [object 类型名]
1 | const toString = Object.prototype.toString; |
2. ES6的array.isArray(),专门检测数组类型
快速且安全,避免 instanceof 的跨 iframe 问题
1 | console.log(Array.isArray([])); // true |
7.什么是原型链?如何实现继承
原型链是 JavaScript 实现继承的核心机制。
原型链:JavaScript 的对象通过
__proto__
(隐式原型)链接到其构造函数的 prototype(显式原型),形成链式结构。
继承实现:
原型继承:子类的原型指向父类的实例(Child.prototype = new Parent())。
组合继承:结合构造函数和原型继承,避免共享引用类型属性。
比如下面的例子,Dog 的原型指向 Animal 的实例,这样 Dog 就能继承 Animal 的方法。
实际开发中,常用组合继承来避免引用类型共享的问题,比如用户权限模块中,通过原型链继承公共方法,用构造函数初始化私有属性。
1 | function Animal(name) { |
原型和原型链(旧笔记)
原型:
- 每个函数都有的prototype属性,称为原型
(由于这个属性的值是个对象,所以也称为原型对象) - 原型作用:
- 存放一些属性和方法(挂载到原型上)
- js中实现继承(比如数组身上有很多方法,数组实例可以使使用Array原型身上的方法,因为有__proto__属性的存在)
- __proto__属性
- 每个对象都有这个属性
- 作用:指向它的原型对象
原型链:
通过__proto__一层一层向上寻找原型,直到最顶层找不到则返回Null,这样形成的链式结构称为原型链
- 作用:指向它的原型对象
作用域链
作用域链是JavaScript中查找变量的一种机制。当在函数内部访问一个变量时,JavaScript引擎会首先在当前作用域中查找该变量,如果找不到,则会向上一级作用域继续查找,直到找到该变量或到达全局作用域。
作用域链由当前执行环境的变量对象和所有父级执行环境的变量对象组成,形成了一个链式结构。
8.this 的指向问题
- 普通函数的 this:动态绑定,指向取决于调用方式。
比如对象方法调用时 this 指向对象本身;
单独调用函数时 this 是全局对象;
用 call/apply 可以手动指定 this。
在项目中,常用 bind 来绑定事件处理函数的 this,比如点击按钮时确保 this 指向组件实例
默认绑定:独立函数调用时,this 指向全局对象(严格模式下为 undefined)。
隐式绑定:对象方法调用时,this 指向调用者对象。
显式绑定:通过 call/apply/bind 强制绑定 this。
new 绑定:构造函数中 this 指向新创建的对象
1 | const obj = { |
三个问题
this 的指向本质上由函数的调用方式决定,而箭头函数通过作用域 “捕获” 外层的 this,使其保持固定
- 箭头函数和普通函数的 this 有什么区别?
答:箭头函数的 this 是静态绑定的,定义时会继承外层作用域的 this(比如外层函数或全局),且无法改变;普通函数的 this 是动态绑定的,调用时根据上下文确定(如obj.fn()中 this 指向obj)。例如,在事件回调中用箭头函数可以避免 this 指向 DOM 元素,而是保持外层的 this。 - 构造函数中的 this 指向哪里?为什么箭头函数不能作为构造函数?
答:构造函数用new调用时,this 指向新创建的实例,用于初始化实例属性。箭头函数没有自己的 this,会继承外层的 this(通常是全局),无法绑定到新实例,所以不能作为构造函数,且箭头函数没有prototype属性,无法添加原型方法。 - 回调函数中如何处理 this 指向?
答:如果是普通函数作为回调,this 可能指向调用者(如事件中的 DOM 元素),若需要保持外层 this,可:
用箭头函数替代普通函数,继承外层 this;
用bind(this)手动绑定 this;
用const self = this保存外层 this。
call、apply、bind
call、apply、bind 是 Function 对象的 方法,用于 强制绑定函数的 this 指向。
call 和 apply:立即调用函数,并传入 this 和参数。
bind:返回一个新函数,绑定 this 和参数,不立即调用
请简述JavaScript中的this(详)
- 普通函数:运行时绑定,取决于调用方式
- 默认绑定:
function(){}
→ 非严格模式指向window
,严格模式undefined
- 默认绑定:
- 隐式绑定:
obj.fn()
→ 指向obj
- 隐式绑定:
- 显式绑定:
call/apply/bind
→ 指向指定对象
- 显式绑定:
- 其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。
- call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。
- 箭头函数:继承外层作用域的
this
,无法被修改
箭头函数与普通函数区别
箭头函数与普通函数区别
- 箭头函数无独立
this
,会捕获外层作用域的this- 无
arguments
对象,可用剩余参数(...args) =>
- 不能作为构造函数使用(
new
调用会报错),箭头函数没有自己的this,无法绑定到新实例- 无
prototype
属性- 嵌套函数中的 this 指向全局对象(非严格模式)或 undefined(严格模式)
- 简写语法:
const sum = (a,b) => a+b
Vue中,如果在 methods 或生命周期钩子中使用传统函数,可能会导致 this 指向错误
1 | // 传统函数中的 this 问题 |
何时不应该使用箭头函数?
对象方法:若方法内部使用 this,箭头函数会指向全局对象。
构造函数:箭头函数没有 prototype,无法创建实例。
需要 arguments 对象的场景:箭头函数无 arguments。
9.map 和 forEach 的区别
map 会生成一个新数组,适合需要转换数据的场景,比如将用户列表映射成选项数组;
而 forEach 只是遍历,适合直接操作原数组或执行副作用。
比如在表单校验中,会用forEach遍历输入项并逐个验证,而 map更适合数据预处理
map:对数组每个元素执行回调,返回新数组,原数组不变。
forEach:对数组每个元素执行回调,无返回值,直接修改原数组
1 | const numbers = [1, 2, 3]; |
10.Promise.all 和 Promise.race 的区别
Promise.all适合需要等待所有异步任务完成的场景,比如同时请求多个接口再合并数据;
而Promise.race适合超时控制,比如给接口请求设置一个最大等待时间。如果某个请求超时了,race会优先返回超时的结果
Promise.all:所有 Promise 成功时返回结果数组;任意失败则立即拒绝。
Promise.race:最快完成的 Promise 决定结果(成功或失败)
1 | const p1 = Promise.resolve(3); |
11.函数提升(Hoisting)的原理
函数提升是JavaScript 在编译阶段将函数声明移到作用域顶部的行为,所以可以先调用后定义。
而var声明的变量只会提升声明,赋值留在原地,所以未赋值前访问是 undefined
实际开发中,多用let/const避免提升带来的问题,或者在代码顶部统一声明变量
变量提升:var 声明的变量会被提升到作用域顶部,但赋值不会提升(TDZ)。
函数提升:函数声明会被整体提升,可在定义前调用
1 | console.log(x); // undefined(变量提升) |
12.节流和防抖(详细代码见本文章后面内容)
防抖:指连续触发事件但是在设定的一段时间内只执行最后一次
在执行期间如果有新的事情进来,会重新计时,重新计算
巧记:回城取消再回城(回城移动≈清除前面的倒计时,根据最后一次回城按下的时间开始重新准备回城)
应用场景:搜索框输入,文本编辑器实时保存
实现思路:定时器
节流:指连续触发事件但是在设定的一段时间内只执行一次
在执行期间如果触发多次,也只会在设定时间后只执行一次
巧记:技能冷却(用完进入CD再按也没有用)
应用场景:高频事件(快速点击、鼠标滑动、resize事件、scroll时间)、下拉加载、视频播放事件
实现思路:定时器
开发中一般用lodash库的debounce(防抖)和throttle(节流)来实现
13. Promise 和 async/await 的区别
Promise是异步编程的基础,通过链式调用处理多个异步操作,常用在数据请求;而 async/await是语法糖,让异步代码看起来像同步代码,更易读,但底层还是依赖 Promise。
两者的区别就像面条 vs 面包:Promise 是面条,需要一节节连接;async/await 是面包,直接切片吃。
Promise:基于链式调用的异步解决方案,支持 .then() 和 .catch()。
async/await:基于 Promise 的语法糖,使异步代码更同步化
1 | // Promise |
1 | fetch('/api/data') |
14. 如何现深拷贝
深拷贝需要递归复制对象的所有层级。比如在处理表单数据时,直接赋值会导致修改副本时原数据也被修改,这时候就需要深拷贝。JSON.stringify 的方式,但要注意它不能处理函数和循环引用,所以复杂场景会自己写递归函数。
浅拷贝:只复制一层属性(如 Object.assign、展开运算符 …)。
深拷贝:递归复制所有层级(JSON.parse(JSON.stringify()) 有局限性)
15.解构赋值
数组解构-按位置匹配元素,支持默认值和跳过元素
1 | // 基本用法 |
- 对象解构:按属性名匹配,支持重命名和默认值(注意嵌套解构)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28// 基本用法
const { name, age } = { name: 'Doubao', age: 18 };
console.log(name); // Doubao
console.log(age); // 18
// 重命名
const { name: userName, age: userAge } = { name: 'Doubao', age: 18 };
console.log(userName); // Doubao
// 默认值
const { city = 'Beijing' } = { name: 'Doubao' };
console.log(city); // Beijing
// 嵌套解构
const user = {
name: 'Doubao',
address: {
city: 'Beijing',
district: 'Haidian'
}
};
const { address: { city } } = user;
console.log(city); // Beijing
// 剩余属性
const { name: n, ...rest } = user;
console.log(rest); // { address: { city: 'Beijing', district: 'Haidian' } }
解构赋值有什么实际应用场景?
提取 API 响应数据const { data, status } = await fetch('/api/users');
CommonJS
- Node.js 模块规范,核心概念:
- 模块加载:
require('path')
- 模块导出:
module.exports
或exports
- 特点:
- 同步加载(适用于服务端)
- 模块缓存(相同路径只加载一次)
- 文件即模块(每个文件独立作用域)
- 模块加载:
异步编程详解(我之前的模糊点)
JavaScript 单线程与异步编程的关系
异步编程正是解决单线程阻塞问题的方法
异步编程是指在执行某个任务时,不需要等待该任务完成,可以继续执行其他任务,当该任务完成后,再通过回调函数或其他方式通知程序进行处理
异步编程可以提高程序的执行效率和响应速度,特别适用于处理网络请求和耗时任务。
- 单线程:JavaScript 只有一个主线程,不能同时执行多个任务。如果直接同步执行耗时操作(如死循环),会导致页面卡顿
- 异步编程:通过 事件循环(Event Loop) 和 浏览器多线程协作,让 JavaScript 在等待耗时操作(如网络请求)时,继续执行其他任务
- I/O密集型操作:网络请求(API调用);文件读写(Node.js);数据库操作
异步编程利用了浏览器的多线程能力(如Web Workers)来处理耗时任务,从而保持主线程的响应性
关键机制:
- 调用栈(Call Stack):
同步代码立即执行
函数调用形成栈帧 - 任务队列(Task Queues):
微任务队列(Promise、MutationObserver)
宏任务队列(setTimeout、DOM事件) - 事件循环(Event Loop)
常用的异步编程方式
在JavaScript中,常用的异步编程方式包括回调函数、Promise和async/await。
- 回调函数是最早的异步编程方式,但容易出现回调地狱的问题;
- Promise是一种更优雅的异步编程方式,可以链式调用,解决回调地狱的问题;
- async/await是建立在Promise之上的语法糖,使得异步代码看起来像同步代码,更加易于阅读和维护。
关于何时使用async
async用于声明一个异步函数,使得函数内可以使用await来等待Promise的结果。
当需要处理耗时操作(如网络请求
fetch 或 axios 请求数据(如 API 调用、图片加载)
、文件读取、定时器)时,应该使用异步编程。async 和 await 是处理这些操作的推荐方式async/await是语言特性,可以与任何Promise兼容的库一起使用,包括axios和Fetch
节流和防抖代码详解:
- 节流(Throttle):
让一个函数在规定的时间间隔内,最多只执行一次。以第一次调用为准,忽略短时间内后续的调用,直到时间间隔结束。
特点:它的作用是让一个函数在规定的时间间隔内只执行一次,适用于需要持续响应但频率受限的场景。 - 防抖(Debounce):
当事件在一定时间间隔内多次触发时,以最后一次调用为准,只有最后一次触发后,经过设定的延迟时间,函数才会执行。
特点:确保函数在用户操作结束后一段时间执行,适用于需要等待用户输入稳定后再处理的场景。
函数防抖
防抖函数用于限制函数的执行频率;当事件在一定时间间隔内多次触发时,只有最后一次触发后,经过设定的延迟时间,才会执行回调函数。
常用于搜索框输入、窗口大小调整场景
1 | function debounce(fn, delay) { |
测试函数:
1 | function search(query) { |
代码两种写法的对比
看了多篇相关文章防抖函数this部分有两种写法,但是更推荐写法1:
1 | // 写法一:直接使用箭头函数的 this |
写法一:使用箭头函数的 this(继承自外层作用域);支持动态绑定 this,适用场景如事件处理
写法二:显式保存 this 到 content 变量,不支持动态绑定,适用于静态方法
为什么写法一更推荐
- 箭头函数的 this 继承机制:
箭头函数没有自己的 this,它继承自外层函数(即 debounce 返回的箭头函数)
调用时动态绑定 this:当调用 onSearch.call(obj, “query”) 时,this 会指向 obj,从而在 fn.apply(this, args) 中正确传递上下文- 灵活性:支持动态绑定:
调用防抖函数时可以传入不同的 this,适用于事件处理、对象方法等场景- 写法二:固定 this:content = this 会在 debounce 定义时绑定 this,后续调用无法动态修改
防抖函数中,为什么选择使用箭头函数而不是普通函数?
箭头函数没有自己的 this,它会继承外层作用域的 this。在防抖函数中,我们希望调用者能够动态绑定 this(例如事件处理函数的 this 指向 DOM 元素),而箭头函数能保证 this 的一致性,避免在 setTimeout 中丢失上下文。
函数节流
是一种控制函数执行频率的技术。它的作用是让一个函数在规定的时间间隔内最多只执行一次。
在一定时间间隔内,如果一个事件被多次触发,只以第一次被触发为准,忽略短时间内后续的调用,直到时间间隔结束。c
1 | function throttle(fn, delay) { |
原型链作用域链闭包
造函数的prototype
属性(原型对象)。
原型对象也是一个对象,它也有自己的原型,这样一层一层向上直到一个对象的原型为null
(Object.prototype
的原型就是null
),形成链条,即原型链。
当我们访问一个对象的属性时,如果对象本身没有这个属性,就会沿着原型链向上查找,直到找到或者到原型链顶端(null
)为止。
问题:什么是原型链?
回答:原型链是JavaScript实现继承的一种机制。每个对象都有一个指向其原型(prototype)的内部链接。当试图访问一个对象的属性时,它不仅仅在该对象上查找,还会在该对象的原型上,以及原型的原型上,依次层层向上查找,直到找到一个名字匹配的属性或到达原型链的末尾(null)。这种链接关系形成的链条就叫做原型链。
问题:如何实现继承?
回答:在JavaScript中,我们通常使用原型链来实现继承。比如,我们可以通过将子类的原型设置为父类的实例
作用域链是JavaScript中用于查找变量的一种机制。在查找变量时,会沿着作用域链一级一级向上查找,直到找到或者到全局作用域(如果还没找到就报错)。
闭包就是函数内部定义的函数,它可以访问外部函数的变量。闭包的主要作用有两个:一是可以创建私有变量,避免全局污染;二是可以让这些变量的值始终保持在内存中(不会被垃圾回收),通过闭包提供的函数来操作这些变量
TypeScript类型系统
1 | interface User{ |