前言
最近面试了不少候选人,发现一个普遍现象:很多人对JavaScript基础概念一知半解。问到"let和const的区别",能说出"一个是变量一个是常量",但追问"暂时性死区是什么",就卡壳了。
这就是典型的"表面理解":知道概念名词,不懂底层原理。
今天这10道JavaScript基础题,每道题我都会告诉你:面试官为什么问这个、标准答案怎么说、什么回答会让你直接出局。每题都配"速记公式",面试前一晚看这篇就够了。
1. JavaScript有哪些数据类型?如何判断数据类型?
速记公式:基对函未空符,typeof加instance
- 基本类型:String、Number、Boolean、Undefined、Null、Symbol、BigInt
- 引用类型:Object(Array、Function、Date等都算Object)
- 判断方法:typeof、instanceof、Object.prototype.toString.call()
标准答案
JavaScript数据类型分为两大类:基本类型(原始类型)和引用类型。
基本类型有7种:
- String:字符串,如
'hello' - Number:数字,包括整数和浮点数,如
42、3.14 - Boolean:布尔值,
true或false - Undefined:未定义,声明但未赋值的变量
- Null:空值,表示"无"的对象
- Symbol:唯一且不可变的值,ES6新增
- BigInt:大整数,ES2020新增,如
123n
引用类型主要是Object,包括:
- Object:普通对象,
{name: '张三'} - Array:数组,
[1, 2, 3] - Function:函数,
function() {} - Date:日期,
new Date() - RegExp:正则表达式,
/abc/
类型判断方法:
typeof:适合判断基本类型(除null外)instanceof:判断引用类型的具体类型Object.prototype.toString.call():最准确的类型判断
// 类型判断示例
typeof 'hello' // 'string'
typeof 42 // 'number'
typeof true // 'boolean'
typeof undefined // 'undefined'
typeof null // 'object' (历史遗留问题)
typeof Symbol() // 'symbol'
typeof 123n // 'bigint'
[] instanceof Array // true
({}) instanceof Object // true
Object.prototype.toString.call([]) // '[object Array]'
面试官真正想听什么
这题考察你对JavaScript类型系统的理解深度。
很多人只知道6种数据类型(不知道Symbol和BigInt),更说不清typeof的坑(null返回'object')。面试官想看你是否关注语言的新特性。
加分回答
"我在实际项目中遇到过类型判断的坑。比如用typeof null返回'object',这是JavaScript的历史遗留问题。在做参数校验时,我通常用Object.prototype.toString.call()来获得最准确的结果。
ES6的Symbol我在做对象属性名去重时用过,确保属性名不会冲突。BigInt在处理大数字(如订单ID、时间戳)时很有用,避免数字精度丢失的问题。
类型判断的最佳实践:
- 基本类型用
typeof - 数组用
Array.isArray() - 其他引用类型用
instanceof或Object.prototype.toString.call()"
减分回答
❌ "JavaScript有6种数据类型"(不知道ES6新增的Symbol和BigInt)
❌ "typeof能准确判断所有类型"(不知道null的坑)
❌ 说不出实际应用场景(理论派)
2. var、let、const的区别?什么是暂时性死区?
速记公式:var提升无块域,let不升有块域,const常量必初始化
- var:函数作用域,变量提升,可重复声明
- let:块级作用域,无变量提升,不可重复声明
- const:块级作用域,必须初始化,不可重新赋值
标准答案
var、let、const的核心区别:
var是函数作用域,存在变量提升(声明被提升到作用域顶部),可以在同一作用域内重复声明。
let和const是块级作用域(ES6新增),不存在变量提升,在同一作用域内不可重复声明。
const用于声明常量,必须在声明时初始化,且不能重新赋值(但可以修改对象属性)。
暂时性死区(Temporal Dead Zone, TDZ): 在代码块内,使用let/const声明变量之前,该变量都是不可用的。这在语法上称为"暂时性死区"。
// 变量提升
console.log(a) // undefined
var a = 1
// 暂时性死区
console.log(b) // ReferenceError: Cannot access 'b' before initialization
let b = 2
// 块级作用域
{
var c = 1
let d = 2
}
console.log(c) // 1
console.log(d) // ReferenceError: d is not defined
// const必须初始化
const e // SyntaxError: Missing initializer in const declaration
const f = {name: 'Tom'}
f.name = 'Jerry' // ✅ 可以修改属性
f = {} // ❌ 不能重新赋值
面试官真正想听什么
这题考察你对ES6新特性的理解,以及是否遇到过作用域相关的问题。
很多人知道let/const是块级作用域,但说不清暂时性死区是什么,更不知道在实际项目中怎么选择。
加分回答
"我在重构老项目时深有体会。原来用var声明循环变量:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100) // 输出3,3,3
}
因为var是函数作用域,循环结束后i变成3,所有定时器都引用同一个i。
改成let后:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100) // 输出0,1,2
}
每次循环都有独立的块级作用域,问题解决。
我的使用原则:
- 默认用const,除非需要重新赋值
- 需要重新赋值时用let
- 基本不用var(只在维护老代码时用)
暂时性死区让我养成了先声明后使用的好习惯,避免了很多潜在的bug。"
减分回答
❌ "let和const差不多,随便用"(不理解const必须初始化)
❌ "暂时性死区就是不能变量提升"(理解不准确)
❌ 说不出实际应用场景(理论派)
3. 什么是作用域?JavaScript的作用域链是怎样的?
速记公式:作用域分全局局部,链式查找不停步
- 作用域:变量和函数的可访问范围
- 作用域链:从当前作用域向外层作用域层层查找的机制
- 词法作用域:作用域在代码书写时就确定,与执行位置无关
标准答案
作用域(Scope) 定义了变量和函数的可访问范围。JavaScript主要有三种作用域:
- 全局作用域:在代码任何地方都能访问
- 函数作用域:在函数内部声明的变量
- 块级作用域:ES6引入,let/const声明的变量
作用域链(Scope Chain) 是JavaScript查找变量的机制。当访问一个变量时,会从当前作用域开始查找,如果找不到就向外层作用域查找,直到全局作用域。如果全局作用域也找不到,就报ReferenceError。
词法作用域(Lexical Scope) 意味着作用域在代码书写阶段就确定了,而不是在执行阶段。这与动态作用域的语言不同。
var globalVar = 'global'
function outer() {
var outerVar = 'outer'
function inner() {
var innerVar = 'inner'
console.log(innerVar) // 'inner' - 当前作用域
console.log(outerVar) // 'outer' - 外层作用域
console.log(globalVar) // 'global' - 全局作用域
console.log(notExist) // ReferenceError
}
inner()
}
outer()
面试官真正想听什么
这题考察你对JavaScript执行机制的理解。
作用域链是理解闭包、this指向等概念的基础。面试官想看你是否理解JavaScript的查找机制。
加分回答
"理解作用域链对调试很有帮助。有一次我遇到变量值为undefined的问题,通过作用域链分析发现是变量名拼写错误,JavaScript在作用域链中找不到这个变量,但也没报错(非严格模式),而是返回undefined。
闭包就是作用域链的典型应用:
function createCounter() {
let count = 0
return function() {
return ++count // 访问外层函数的count
}
}
内部函数能访问外部函数的变量,就是因为作用域链的存在。
在ES6之前,我们常用IIFE创建函数作用域来避免变量污染:
(function() {
var temp = '局部变量'
// 不会污染全局作用域
})()
现在可以用块级作用域替代,代码更简洁。"
减分回答
❌ "作用域就是{}包起来的范围"(不准确,函数作用域不是{}决定的)
❌ "作用域链就是在代码里找变量"(理解太表面)
❌ 说不清词法作用域和动态作用域的区别(概念混淆)
4. ==和===的区别?JavaScript的类型转换规则?
速记公式:三等严格比类型,二等转换再相等
- ===:严格相等,不进行类型转换
- ==:宽松相等,会进行类型转换
- 类型转换规则:ToNumber、ToString、ToBoolean
标准答案
==和===的核心区别:
===是严格相等运算符,比较时不进行类型转换。只有类型相同且值相等时才返回true。
==是宽松相等运算符,比较时会进行类型转换。转换规则比较复杂,容易产生意想不到的结果。
类型转换规则: JavaScript在比较时会尝试将操作数转换为相同类型,主要转换规则:
- ToNumber:将其他类型转为数字
- ToString:将其他类型转为字符串
- ToBoolean:将其他类型转为布尔值
// === 严格相等
1 === 1 // true
1 === '1' // false
0 === false // false
// == 宽松相等(类型转换)
1 == 1 // true
1 == '1' // true(字符串'1'转为数字1)
0 == false // true(false转为数字0)
'' == 0 // true(空字符串转为数字0)
// 奇葩结果
[] == 0 // true(数组转字符串'',再转数字0)
[] == '' // true(数组转字符串'')
[] == [] // false(引用类型比较地址)
null == undefined // true
面试官真正想听什么
这题考察你对JavaScript隐式类型转换的理解,以及是否因此踩过坑。
很多人知道要用===,但说不清为什么,更不知道==的转换规则。面试官想看你是否有严谨的编码习惯。
加分回答
"我在代码审查时经常看到用==导致的bug。比如:
if (user.age == 18) {
// 如果age是字符串'18',这里也会执行
}
这种隐式转换很危险,特别是处理用户输入或接口数据时。
我的经验法则:
- 总是使用===,除非有特殊需求
- 在需要类型转换时显式转换:
Number(input) === 18
String(value) === '18'
Boolean(flag) === true
理解类型转换规则对调试很有帮助。有一次遇到[] == false返回true的问题,就是由于数组先转字符串再转数字的规则。
显式转换比隐式转换更安全、更清晰。"
减分回答
❌ "==和===差不多"(完全没理解区别)
❌ "==性能更好"(错误认知)
❌ 说不出常见的类型转换结果(理论不扎实)
5. 什么是闭包?闭包的应用场景?
速记公式:函数返回函数,访问外部变量
- 闭包定义:函数能够访问并记住其词法作用域中的变量
- 形成条件:函数嵌套 + 内部函数引用外部变量 + 内部函数在外部执行
- 应用场景:模块化、私有变量、防抖节流、循环绑定事件
标准答案
闭包(Closure) 是指函数能够访问并记住其词法作用域中的变量,即使函数在其词法作用域之外执行。
闭包的形成需要三个条件:
- 函数嵌套
- 内部函数引用外部函数的变量
- 内部函数在外部函数之外执行
function createCounter() {
let count = 0 // 外部变量
return function() { // 内部函数
return ++count // 引用外部变量
}
}
const counter = createCounter()
console.log(counter()) // 1
console.log(counter()) // 2
// count变量一直被内部函数引用,不会被垃圾回收
闭包的应用场景:
- 模块化:创建私有变量和方法
- 防抖节流:保持函数调用间的状态
- 循环中的异步操作:保存循环变量的值
- 缓存:记忆复杂计算结果
面试官真正想听什么
这题考察你对JavaScript核心机制的理解,以及实际应用能力。
闭包是JavaScript的重要特性,能答好这题说明你对语言机制有深入理解。
加分回答
"我在项目中用闭包实现过模块模式:
const module = (function() {
let privateVar = 0
function privateMethod() {
return privateVar
}
return {
publicMethod: function() {
return privateMethod() + 1
}
}
})()
module.publicMethod() // 可以访问
module.privateMethod() // 报错,私有方法
防抖函数也是闭包的典型应用:
function debounce(fn, delay) {
let timer
return function(...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
闭包虽然强大,但要注意内存泄漏。如果闭包引用的大对象不再需要,要及时解除引用。
理解闭包让我对JavaScript的作用域和垃圾回收机制有了更深的认识。"
减分回答
❌ "闭包就是函数里面套函数"(理解太表面)
❌ "闭包会导致内存泄漏,要避免使用"(片面理解)
❌ 说不出具体的应用场景(理论派)
6. this的指向规则?箭头函数的this有何不同?
速记公式:普函看调用,箭函看定义,new构实例,上下可指定
- 普通函数:this指向调用者
- 箭头函数:this指向定义时的上下文
- 构造函数:this指向新创建的实例
- 可改变:call/apply/bind可改变this指向
标准答案
this的指向规则:
- 默认绑定:普通函数中,this指向全局对象(浏览器中为window,严格模式下为undefined)
- 隐式绑定:方法调用时,this指向调用该方法的对象
- 显式绑定:使用call/apply/bind时,this指向指定的对象
- new绑定:构造函数中,this指向新创建的实例
- 箭头函数:没有自己的this,继承外层作用域的this
// 1. 默认绑定
function normalFunc() {
console.log(this) // 浏览器中: window (严格模式: undefined)
}
normalFunc()
// 2. 隐式绑定
const obj = {
name: 'Tom',
sayName() {
console.log(this.name) // 'Tom'
}
}
obj.sayName()
// 3. 显式绑定
function greet() {
console.log(`Hello, ${this.name}`)
}
const person = {name: 'Jerry'}
greet.call(person) // 'Hello, Jerry'
// 4. new绑定
function Person(name) {
this.name = name
}
const p = new Person('Mike')
console.log(p.name) // 'Mike'
// 5. 箭头函数
const arrowObj = {
name: 'Arrow',
normal: function() {
console.log(this.name) // 'Arrow'
},
arrow: () => {
console.log(this.name) // undefined (指向外层作用域)
}
}
面试官真正想听什么
这题考察你对JavaScript执行上下文的理解,以及在实际项目中如何处理this问题。
很多人知道各种规则,但遇到复杂场景就混乱。面试官想看你是否真的理解this的动态特性。
加分回答
"我在React类组件中深有体会。最开始在事件处理中直接传递函数:
class MyComponent extends React.Component {
handleClick() {
console.log(this) // undefined
// 因为React调用时不是通过obj.method()方式
}
render() {
return <button onClick={this.handleClick}>点击</button>
}
}
解决方案有三种:
- 在构造函数中bind:
this.handleClick = this.handleClick.bind(this) - 使用箭头函数:
handleClick = () => { ... } - 在render中使用箭头函数:
onClick={() => this.handleClick()}
我倾向于第二种,代码更简洁。
箭头函数的this在定义时就确定了,这个特性在定时器中很有用:
class Timer {
constructor() {
this.count = 0
}
start() {
// 普通函数,this指向window
setInterval(function() {
this.count++ // 报错,this.count undefined
}, 1000)
// 箭头函数,this指向Timer实例
setInterval(() => {
this.count++ // 正常工作
}, 1000)
}
}
理解this指向让我避免了很多潜在的bug。"
减分回答
❌ "this指向函数自己"(完全错误的理解)
❌ "箭头函数和普通函数的this规则一样"(概念混淆)
❌ 说不出call/apply/bind的区别(基础不扎实)
常见追问
Q: call、apply、bind的区别?
A: 都是改变this指向,call和apply立即执行,bind返回新函数。call参数逐个传递,apply参数以数组形式传递。
Q: 箭头函数可以用作构造函数吗?
A: 不能,箭头函数没有prototype属性,也不能使用new关键字调用,没有arguments对象。
7. 原型和原型链是什么?如何实现继承?
速记公式:原型对象共享,原型链式查找,继承组合最优
- 原型:每个函数都有prototype属性,每个对象都有__proto__属性
- 原型链:通过__proto__连接形成的链式结构,用于属性查找
- 继承方式:原型链继承、构造函数继承、组合继承、寄生组合继承
标准答案
原型(Prototype):
- 每个函数都有一个prototype属性,指向原型对象
- 每个对象都有一个__proto__属性,指向创建它的构造函数的原型对象
- 原型对象上的属性和方法可以被实例共享
原型链(Prototype Chain): 当访问对象的属性时,如果对象本身没有该属性,会通过__proto__向上查找,直到找到或到达原型链末端(null)
function Person(name) {
this.name = name
}
// 在原型上添加方法
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`)
}
const person = new Person('Tom')
// 原型链查找
person.sayHello() // 1. person自身 → 2. Person.prototype → 3. Object.prototype → 4. null
console.log(person.__proto__ === Person.prototype) // true
console.log(Person.prototype.__proto__ === Object.prototype) // true
继承实现方式:
- 原型链继承:子类原型指向父类实例
- 构造函数继承:在子类构造函数中调用父类构造函数
- 组合继承:结合原型链和构造函数继承
- 寄生组合继承:最理想的继承方式
// 寄生组合继承(推荐)
function Parent(name) {
this.name = name
}
Parent.prototype.sayName = function() {
console.log(this.name)
}
function Child(name, age) {
Parent.call(this, name) // 继承实例属性
this.age = age
}
// 继承原型方法
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
Child.prototype.sayAge = function() {
console.log(this.age)
}
面试官真正想听什么
这题考察你对JavaScript面向对象编程的理解深度。
原型机制是JavaScript的核心特性,能清晰解释原型链说明你对语言机制有深刻理解。
加分回答
"我在阅读源码时发现,很多库都用到原型机制。比如jQuery通过原型实现方法共享:
// 类似jQuery的实现
function jQuery(selector) {
return new jQuery.fn.init(selector)
}
jQuery.fn = jQuery.prototype = {
constructor: jQuery,
// 共享方法
css: function() { /* ... */ },
show: function() { /* ... */ }
}
jQuery.fn.init.prototype = jQuery.fn
ES6的class本质也是原型继承的语法糖:
class Parent {
constructor(name) {
this.name = name
}
sayName() {
console.log(this.name)
}
}
class Child extends Parent {
constructor(name, age) {
super(name) // 相当于Parent.call(this, name)
this.age = age
}
}
// 等同于寄生组合继承
理解原型链对性能优化也有帮助。如果在原型链太深的位置查找属性,会影响性能。我们应该尽量把常用方法放在对象本身或较近的原型上。"
减分回答
❌ "原型就是继承"(理解太片面)
❌ "__proto__和prototype是一样的"(概念混淆)
❌ 说不出至少两种继承方式的优缺点(理解不深入)
常见追问
Q: Object.create(null)创建的对象有什么特点?
A: 没有原型链,不继承Object.prototype的方法,适合做纯字典使用。
Q: 如何判断属性是对象自身的还是继承的?
A: 使用obj.hasOwnProperty('key'),返回true表示是自身属性。
8. 深拷贝和浅拷贝的区别?如何实现深拷贝?
速记公式:浅拷共享引用,深拷完全独立
- 浅拷贝:只拷贝第一层属性,嵌套对象共享引用
- 深拷贝:完全拷贝所有层级,新旧对象完全独立
- 实现方式:JSON方法、递归拷贝、使用第三方库
标准答案
浅拷贝(Shallow Copy) 只复制对象的第一层属性,如果属性是引用类型,复制的是引用地址,新旧对象共享嵌套对象。
深拷贝(Deep Copy) 递归复制所有层级的属性,创建完全独立的新对象,不共享任何引用。
const original = {
name: 'Tom',
hobbies: ['reading', 'coding'],
address: {
city: 'Beijing',
street: 'Main St'
}
}
// 浅拷贝
const shallowCopy = Object.assign({}, original)
shallowCopy.hobbies.push('gaming')
console.log(original.hobbies) // ['reading', 'coding', 'gaming'] - 被修改了!
// 深拷贝
const deepCopy = JSON.parse(JSON.stringify(original))
deepCopy.hobbies.push('swimming')
console.log(original.hobbies) // ['reading', 'coding'] - 保持不变
深拷贝实现方式:
- JSON方法:简单但有限制(不能处理函数、循环引用等)
- 递归实现:完整但需要处理边界情况
- 使用库:lodash的cloneDeep
// 递归深拷贝实现
function deepClone(obj, hash = new WeakMap()) {
// 处理基本类型和null
if (obj === null || typeof obj !== 'object') {
return obj
}
// 处理循环引用
if (hash.has(obj)) {
return hash.get(obj)
}
// 处理日期
if (obj instanceof Date) {
return new Date(obj)
}
// 处理数组
if (Array.isArray(obj)) {
const cloneArr = []
hash.set(obj, cloneArr)
obj.forEach(item => {
cloneArr.push(deepClone(item, hash))
})
return cloneArr
}
// 处理对象
const cloneObj = {}
hash.set(obj, cloneObj)
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], hash)
}
}
return cloneObj
}
面试官真正想听什么
这题考察你对内存管理和对象操作的理解,以及实际项目中的数据安全意识。
拷贝问题是前端开发中的常见痛点,能处理好说明你有扎实的工程实践能力。
加分回答
"我在状态管理库如Redux中深刻体会到深拷贝的重要性。Redux要求reducer必须是纯函数,返回新的state:
function reducer(state = initialState, action) {
switch (action.type) {
case 'UPDATE_USER':
// ❌ 错误:直接修改原state
// state.user.name = action.payload
// return state
// ✅ 正确:返回新state
return {
...state,
user: {
...state.user,
name: action.payload
}
}
}
}
实际项目中我会根据场景选择拷贝方式:
- 简单数据用扩展运算符:
const copy = {...obj} - 复杂但无特殊类型用JSON方法
- 有函数、循环引用等用lodash.cloneDeep
循环引用是深拷贝的难点:
const obj = {name: 'Tom'}
obj.self = obj // 循环引用
// JSON方法会报错
// JSON.parse(JSON.stringify(obj)) // TypeError
// 递归实现需要WeakMap记录已拷贝对象
理解拷贝机制让我在性能优化时更有针对性,避免不必要的深拷贝开销。"
减分回答
❌ "深拷贝就是JSON.parse(JSON.stringify())"(不知道局限性)
❌ "扩展运算符...就是深拷贝"(概念错误)
❌ 处理不了循环引用情况(实现不完整)
常见追问
Q: 扩展运算符...是深拷贝还是浅拷贝? A: 浅拷贝,只拷贝第一层,嵌套对象仍然是引用。
Q: 什么情况下必须使用深拷贝? A: 当需要完全独立的对象,且后续操作可能修改嵌套对象时,如状态管理、撤销重做功能等。
9. 数组常用方法有哪些?map、filter、reduce的用法?
速记公式:增删改查遍历,map过滤reduce聚
- 增删改:push/pop/shift/unshift/splice
- 查询:find/findIndex/includes/indexOf
- 遍历:forEach/map/filter/reduce
- 其他:some/every/sort/slice
标准答案
数组常用方法分类:
1. 增删元素:
push/pop:末尾添加/删除unshift/shift:开头添加/删除splice:指定位置增删concat:合并数组
2. 查询搜索:
indexOf/lastIndexOf:查找元素位置includes:是否包含元素find/findIndex:查找满足条件的元素
3. 遍历处理:
forEach:遍历执行回调map:映射为新数组filter:过滤满足条件的元素reduce/reduceRight:累积计算
4. 其他操作:
slice:截取数组sort:排序reverse:反转some/every:部分/全部满足条件
const numbers = [1, 2, 3, 4, 5]
// map - 映射
const doubled = numbers.map(n => n * 2) // [2, 4, 6, 8, 10]
// filter - 过滤
const evens = numbers.filter(n => n % 2 === 0) // [2, 4]
// reduce - 累积
const sum = numbers.reduce((acc, cur) => acc + cur, 0) // 15
// 链式调用
const result = numbers
.filter(n => n > 2) // [3, 4, 5]
.map(n => n * 3) // [9, 12, 15]
.reduce((a, b) => a + b) // 36
面试官真正想听什么
这题考察你对函数式编程的理解,以及数据处理能力。
数组方法是日常开发中最常用的API,能熟练使用说明你有良好的编码习惯和数据处理思维。
加分回答
"我在数据处理中大量使用这些方法。比如处理API返回的列表数据:
// 从API数据中提取需要的信息
const userList = apiResponse.data
.filter(user => user.status === 'active') // 过滤活跃用户
.map(user => ({
id: user.id,
name: `${user.firstName} ${user.lastName}`,
age: new Date().getFullYear() - new Date(user.birthday).getFullYear()
})) // 转换格式
.sort((a, b) => a.age - b.age) // 按年龄排序
// reduce的强大用法 - 数据分组
const people = [
{name: 'Tom', department: 'Engineering'},
{name: 'Jerry', department: 'Marketing'},
{name: 'Mike', department: 'Engineering'}
]
const byDepartment = people.reduce((acc, person) => {
const dept = person.department
if (!acc[dept]) acc[dept] = []
acc[dept].push(person)
return acc
}, {})
// {Engineering: [...], Marketing: [...]}
性能注意点:
- 大数据量时forEach比for循环慢
- 链式调用会创建中间数组,大数据量时可考虑用reduce一次完成
- 改变原数组的方法要小心使用(splice、sort等)
这些方法让代码更声明式、更易读。"
减分回答
❌ "forEach和map差不多"(不理解返回值区别)
❌ "reduce只能做求和"(不知道其他应用场景)
❌ 用for循环处理所有数组操作(代码不够函数式)
常见追问
Q: forEach和map的区别?
A: forEach只是遍历执行,没有返回值;map返回新数组,保持原数组不变。
Q: 如何判断一个值是否为数组?
A: 使用Array.isArray(),比instanceof更可靠。
10. 如何实现防抖和节流?应用场景有哪些?
速记公式:防抖合并多次,节流均匀执行,闭包保存状态
- 防抖:连续触发时,只执行最后一次
- 节流:连续触发时,按固定频率执行
- 实现核心:闭包 + 定时器
- 应用场景:搜索、滚动、 resize、按钮提交
标准答案
防抖(Debounce):事件触发后等待一定时间再执行,如果在这段时间内再次触发,重新计时。
节流(Throttle):事件触发后立即执行,但在指定时间内不再响应后续触发。
// 防抖实现
function debounce(fn, delay) {
let timer = null
return function(...args) {
clearTimeout(timer) // 清除上次定时器
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
// 节流实现
function throttle(fn, delay) {
let lastTime = 0
return function(...args) {
const now = Date.now()
if (now - lastTime >= delay) {
fn.apply(this, args)
lastTime = now
}
}
}
// 带立即执行选项的防抖
function debounceImmediate(fn, delay, immediate = false) {
let timer = null
return function(...args) {
const callNow = immediate && !timer
clearTimeout(timer)
timer = setTimeout(() => {
timer = null
if (!immediate) {
fn.apply(this, args)
}
}, delay)
if (callNow) {
fn.apply(this, args)
}
}
}
应用场景
防抖适用场景:
- 搜索框输入建议(等待用户停止输入)
- 窗口resize(调整完成后计算布局)
- 表单验证(输入完成后再验证)
节流适用场景:
- 滚动加载更多(固定间隔检查位置)
- 按钮防重复点击(一段时间内只响应一次)
- 鼠标移动事件(避免过于频繁触发)
// 实际使用示例
const searchInput = document.getElementById('search')
// 防抖:搜索建议
searchInput.addEventListener('input', debounce(function(e) {
fetchSuggestions(e.target.value)
}, 300))
// 节流:滚动加载
window.addEventListener('scroll', throttle(function() {
if (reachBottom()) {
loadMore()
}
}, 1000))
// 立即执行的防抖:按钮提交
submitBtn.addEventListener('click', debounceImmediate(function() {
submitForm()
}, 2000, true)) // 2秒内只能提交一次
面试官真正想听什么
这题考察你对性能优化的理解,以及实际项目中的用户体验意识。
防抖节流是前端优化的经典方案,能熟练使用说明你关注用户体验和性能。
加分回答
"我在项目中根据不同场景选择防抖或节流:
搜索框用防抖:用户连续输入时不需要每次都要请求,停止输入300ms后再请求,减少服务器压力。
const searchDebounce = debounce(searchAPI, 300)
无限滚动用节流:滚动时每500ms检查一次是否到达底部,避免频繁计算。
const scrollThrottle = throttle(checkScrollPosition, 500)
按钮防重复点击用立即执行防抖:点击立即执行,但2秒内不能再次点击。
const submitDebounce = debounceImmediate(handleSubmit, 2000, true)
React Hooks中的实现:
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
}, [value, delay])
return debouncedValue
}
理解这些模式让我在性能优化时更有针对性。"
减分回答
❌ "防抖和节流差不多"(不理解本质区别)
❌ "用setTimeout就行"(不知道具体实现细节)
❌ 说不出具体的应用场景(缺乏实战经验)
常见追问
Q: 防抖和节流的主要区别是什么?
A: 防抖合并多次执行为一次,节流保证在一定时间内只执行一次。
Q: 为什么要用闭包实现?
A: 闭包可以保存timer或lastTime状态,避免全局变量污染。
总结
这10道JavaScript基础题,是前端面试的必考内容。能清晰回答这些问题,说明你的JavaScript基础扎实;答不上来,再花哨的项目经验也难让人信服。
每道题的核心不是死记硬背,而是理解:
- 语言为什么这样设计(设计理念)
- 你在项目中怎么应用的(实战经验)
- 踩过什么坑、怎么解决的(问题解决能力)
学习建议:
- 理解而非记忆:搞懂每个概念背后的原理
- 动手实践:在控制台验证不确定的特性
- 总结反思:记录在项目中遇到的坑和解决方案
最近好多同学挂在 HR 面而不知道为什么,这些问题你都会吗:
- 对于互联网公司的快节奏工作,你是怎么看的?
- 能说说你印象最深的一次团队合作经历吗?
- 你觉得工作和生活应该怎么平衡?
- 你觉得什么样的公司文化最吸引你?
没有答题思路? 快来牛面题库看看吧,这是我们共同打造的面试学习一站式平台,拥有丰富的免费题库资源,AI模拟面试等等功能,加入我们,早日斩获Offer吧。
留言区互动: 这10题里,你觉得最难的是哪一道?或者你在面试中被问到过但答得不好的是哪题?
在评论区告诉我,点赞最高的问题,我会单独写一篇深度解析!