一问就倒的JS基础,到底坑了多少面试者?这10题不过,真别写“熟悉”

186 阅读21分钟

前言

最近面试了不少候选人,发现一个普遍现象:很多人对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:数字,包括整数和浮点数,如423.14
  • Boolean:布尔值,truefalse
  • 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()
  • 其他引用类型用instanceofObject.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) 是指函数能够访问并记住其词法作用域中的变量,即使函数在其词法作用域之外执行。

闭包的形成需要三个条件:

  1. 函数嵌套
  2. 内部函数引用外部函数的变量
  3. 内部函数在外部函数之外执行
function createCounter() {
  let count = 0  // 外部变量
  
  return function() {  // 内部函数
    return ++count    // 引用外部变量
  }
}

const counter = createCounter()
console.log(counter()) // 1
console.log(counter()) // 2
// count变量一直被内部函数引用,不会被垃圾回收

闭包的应用场景:

  1. 模块化:创建私有变量和方法
  2. 防抖节流:保持函数调用间的状态
  3. 循环中的异步操作:保存循环变量的值
  4. 缓存:记忆复杂计算结果

面试官真正想听什么

这题考察你对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指向.png

  1. 默认绑定:普通函数中,this指向全局对象(浏览器中为window,严格模式下为undefined)
  2. 隐式绑定:方法调用时,this指向调用该方法的对象
  3. 显式绑定:使用call/apply/bind时,this指向指定的对象
  4. new绑定:构造函数中,this指向新创建的实例
  5. 箭头函数:没有自己的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>
  }
}

解决方案有三种:

  1. 在构造函数中bind:this.handleClick = this.handleClick.bind(this)
  2. 使用箭头函数:handleClick = () => { ... }
  3. 在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

继承实现方式:

  1. 原型链继承:子类原型指向父类实例
  2. 构造函数继承:在子类构造函数中调用父类构造函数
  3. 组合继承:结合原型链和构造函数继承
  4. 寄生组合继承:最理想的继承方式
// 寄生组合继承(推荐)
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'] - 保持不变

深拷贝实现方式:

  1. JSON方法:简单但有限制(不能处理函数、循环引用等)
  2. 递归实现:完整但需要处理边界情况
  3. 使用库: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基础扎实;答不上来,再花哨的项目经验也难让人信服。

每道题的核心不是死记硬背,而是理解:

  • 语言为什么这样设计(设计理念)
  • 你在项目中怎么应用的(实战经验)
  • 踩过什么坑、怎么解决的(问题解决能力)

学习建议:

  1. 理解而非记忆:搞懂每个概念背后的原理
  2. 动手实践:在控制台验证不确定的特性
  3. 总结反思:记录在项目中遇到的坑和解决方案

最近好多同学挂在 HR 面而不知道为什么,这些问题你都会吗:

  1. 对于互联网公司的快节奏工作,你是怎么看的?
  2. 能说说你印象最深的一次团队合作经历吗?
  3. 你觉得工作和生活应该怎么平衡?
  4. 你觉得什么样的公司文化最吸引你?

没有答题思路? 快来牛面题库看看吧,这是我们共同打造的面试学习一站式平台,拥有丰富的免费题库资源,AI模拟面试等等功能,加入我们,早日斩获Offer吧。

留言区互动: 这10题里,你觉得最难的是哪一道?或者你在面试中被问到过但答得不好的是哪题?

在评论区告诉我,点赞最高的问题,我会单独写一篇深度解析!