JavaScript 基础 高频面试题

JavaScript 基础 高频面试题

1. 数据类型有哪些?如何判断数据类型?

基本类型(7种,值类型): string number boolean null undefined symbol(ES6) bigint(ES2020)

引用类型(1种): Object(包含 Array、Function、Date、RegExp、Map、Set、WeakMap、WeakSet 等)

基本类型 vs 引用类型的核心区别:

特性 基本类型 引用类型
存储位置 栈内存(Stack) 堆内存(Heap),栈中存引用地址
赋值行为 值拷贝(互不影响) 地址拷贝(共享同一对象)
比较方式 值比较 引用地址比较

四种类型判断方法:

javascript 复制代码
// ========== 1. typeof:适合判断基本类型(null 除外)==========
typeof 'hello'      // 'string'
typeof 42           // 'number'
typeof true         // 'boolean'
typeof undefined    // 'undefined'
typeof null         // 'object'  ⚠️ 历史遗留 bug(JS 诞生时的设计缺陷)
typeof Symbol()     // 'symbol'
typeof 10n          // 'bigint'
typeof function(){} // 'function'
typeof {}           // 'object'  ⚠️ 无法区分普通对象和数组
typeof []           // 'object'

// ========== 2. instanceof:判断引用类型(基于原型链)==========
[] instanceof Array   // true
{} instanceof Object  // true
[] instanceof Object  // true  ⚠️ 数组也是对象
// 缺点:不能跨 iframe 使用(不同 window 的 Array 构造函数不同)

// ========== 3. Object.prototype.toString.call():最准确 ✅ ==========
Object.prototype.toString.call([])        // '[object Array]'
Object.prototype.toString.call({})        // '[object Object]'
Object.prototype.toString.call(null)      // '[object Null]'
Object.prototype.toString.call(undefined) // '[object Undefined]'
Object.prototype.toString.call(new Date)  // '[object Date]'
Object.prototype.toString.call(/regex/)   // '[object RegExp]'
Object.prototype.toString.call(new Map)   // '[object Map]'

// 封装为通用类型判断函数
function getType(value) {
  return Object.prototype.toString.call(value).slice(8, -1).toLowerCase()
}
getType([])    // 'array'
getType(null)  // 'null'
getType(123)   // 'number'

// ========== 4. Array.isArray():专门判断数组 ==========
Array.isArray([])  // true
Array.isArray({})  // false
// 可以跨 iframe 正确判断,推荐使用

💡 面试加分点: typeof null === 'object' 是 JS 的历史遗留 bug------在最初的实现中,JS 用低 3 位存储类型标签,000 代表对象,而 null 的值是全零,因此被误判为对象。另外,NaN 的类型是 'number'typeof NaN === 'number'),可以用 Number.isNaN() 判断(不要用全局的 isNaN(),它会先做类型转换)。


2. undefined 和 null 的区别?

undefined 表示"未定义"------变量已声明但未赋值时的默认值。

null 表示"空值"------开发者主动赋值,表示"这里应该有值,但目前为空"。

javascript 复制代码
// ========== 基本区别 ==========
let a            // 声明但未赋值
console.log(a)   // undefined

let b = null     // 主动赋值为空
console.log(b)   // null

// ========== 类型判断 ==========
typeof undefined  // 'undefined'
typeof null       // 'object'(历史 bug)

// ========== 相等性 ==========
null == undefined   // true(宽松相等)
null === undefined  // false(严格不等,类型不同)
null == 0           // false(null 只和 undefined 宽松相等)
undefined == 0      // false

// ========== 数值转换 ==========
Number(undefined)   // NaN
Number(null)        // 0
undefined + 1       // NaN
null + 1            // 1

// ========== 常见出现场景 ==========
// undefined 出现的场景:
let x                     // 1. 已声明未赋值
function foo() {}
foo()                     // 2. 函数无返回值 → undefined
function bar(a) { return a }
bar()                     // 3. 函数参数未传递 → undefined
const obj = {}
obj.name                  // 4. 访问对象不存在的属性 → undefined
void 0                    // 5. void 运算符 → undefined

// null 出现的场景:
let el = document.getElementById('notExist')  // 1. DOM 查询无结果 → null
Object.getPrototypeOf(Object.prototype)       // 2. 原型链终点 → null
let cache = fetchData()
cache = null              // 3. 手动释放引用(帮助垃圾回收)
JSON.stringify({ a: undefined, b: null })     // '{"b":null}'(undefined 会被忽略)

💡 面试加分点: 在 JSON 序列化中,undefined 的属性会被忽略,而 null 会被保留。使用 void 0 比直接写 undefined 更安全(因为 undefined 在非严格模式下可以被重新赋值,但 void 0 永远返回真正的 undefined)。


3. JavaScript 有哪些内置对象?

JavaScript 的内置对象可以分为以下几类:

javascript 复制代码
// ========== 1. 基本包装类型 ==========
// Number ------ 数值相关
Number.isInteger(10)       // true  判断是否为整数
Number.isNaN(NaN)          // true  比全局 isNaN 更安全
Number.parseFloat('3.14')  // 3.14
Number.MAX_SAFE_INTEGER    // 9007199254740991(2^53 - 1)
(3.1415).toFixed(2)        // '3.14'

// String ------ 字符串相关
'hello'.includes('ell')    // true
'hello'.startsWith('he')   // true
'  hi  '.trim()            // 'hi'
'abc'.padStart(5, '0')     // '00abc'
'ha'.repeat(3)             // 'hahaha'
'a-b-c'.split('-')         // ['a', 'b', 'c']

// Boolean ------ 布尔值
Boolean(0)          // false
Boolean('')         // false
Boolean(null)       // false
Boolean(undefined)  // false
Boolean([])         // true(空数组是 truthy!)
Boolean({})         // true(空对象是 truthy!)

// ========== 2. Math 对象(静态方法,不能 new) ==========
Math.max(1, 2, 3)           // 3
Math.min(1, 2, 3)           // 1
Math.floor(4.7)             // 4(向下取整)
Math.ceil(4.1)              // 5(向上取整)
Math.round(4.5)             // 5(四舍五入)
Math.abs(-5)                // 5(绝对值)
Math.random()               // 0~1 之间的随机数
Math.pow(2, 3)              // 8 等同于 2 ** 3
// 生成 [min, max] 范围的随机整数
const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min

// ========== 3. Date 对象 ==========
const now = new Date()
now.getFullYear()       // 2026
now.getMonth()          // 0~11(注意:0 表示一月!)
now.getDate()           // 1~31
now.getDay()            // 0~6(0 表示周日)
now.getTime()           // 时间戳(毫秒)
Date.now()              // 当前时间戳(无需创建实例)
new Date('2026-01-01')  // 解析日期字符串

// ========== 4. Array 对象(常用方法) ==========
const arr = [1, 2, 3, 4, 5]
arr.push(6)              // 末尾添加,返回新长度
arr.pop()                // 末尾删除,返回删除的元素
arr.unshift(0)           // 头部添加
arr.shift()              // 头部删除
arr.splice(1, 2)         // 从索引1开始删除2个元素(会修改原数组)
arr.slice(1, 3)          // 截取索引 [1,3),不修改原数组
arr.indexOf(3)           // 查找元素索引,找不到返回 -1
arr.includes(3)          // 是否包含某元素
arr.find(x => x > 3)    // 找到第一个满足条件的元素
arr.findIndex(x => x > 3) // 找到第一个满足条件的索引
arr.concat([6, 7])       // 合并数组,返回新数组
arr.join('-')            // 转为字符串 '1-2-3-4-5'
arr.reverse()            // 反转(修改原数组)
arr.sort((a, b) => a - b) // 排序(修改原数组)
Array.from({length: 3}, (_, i) => i)  // [0, 1, 2]

// ========== 5. Object 对象(常用静态方法) ==========
const obj = { a: 1, b: 2, c: 3 }
Object.keys(obj)           // ['a', 'b', 'c']
Object.values(obj)         // [1, 2, 3]
Object.entries(obj)        // [['a',1], ['b',2], ['c',3]]
Object.assign({}, obj)     // 浅拷贝
Object.freeze(obj)         // 冻结对象(不可修改属性)
Object.isFrozen(obj)       // true
Object.create(null)        // 创建无原型的纯净对象
Object.hasOwn(obj, 'a')    // true(替代 obj.hasOwnProperty('a'))
Object.fromEntries([['a',1], ['b',2]])  // { a: 1, b: 2 }

// ========== 6. RegExp 正则表达式 ==========
const reg = /^\d{3}-\d{4}$/
reg.test('123-4567')     // true
'hello world'.match(/\w+/g)  // ['hello', 'world']
'a-b-c'.replace(/-/g, '_')  // 'a_b_c'

// ========== 7. Error 错误对象 ==========
// Error、TypeError、RangeError、ReferenceError、SyntaxError、URIError
try {
  null.fn()
} catch (e) {
  console.log(e instanceof TypeError)  // true
  console.log(e.message)               // "null.fn is not a function"
}

💡 面试加分点: ES2023 新增了 Array.prototype.toSorted()toReversed()toSpliced() 等方法,它们返回新数组而不修改原数组。Object.groupBy()(ES2024)可以对数组按条件分组。


4. 如何区分数组和对象?

javascript 复制代码
const arr = [1, 2, 3]
const obj = { a: 1 }

// ✅ 方法1:Array.isArray()(最推荐)
Array.isArray(arr)  // true
Array.isArray(obj)  // false

// ✅ 方法2:Object.prototype.toString.call()
Object.prototype.toString.call(arr)  // '[object Array]'
Object.prototype.toString.call(obj)  // '[object Object]'

// ✅ 方法3:instanceof(不能跨 iframe)
arr instanceof Array  // true
obj instanceof Array  // false

// ✅ 方法4:constructor
arr.constructor === Array   // true
obj.constructor === Object  // true

// ❌ 不可靠:typeof 无法区分
typeof arr  // 'object'
typeof obj  // 'object'

5. 什么是伪数组(类数组)?如何转为真数组?

伪数组(类数组对象) 是具有 length 属性和数字索引的对象,但不具备数组方法 (如 pushforEachmap 等)。

javascript 复制代码
// ========== 常见的伪数组 ==========
// 1. 函数的 arguments 对象
function foo() {
  console.log(arguments)          // { 0: 'a', 1: 'b', length: 2 }
  console.log(arguments.length)   // 2
  // arguments.push('c')          // ❌ TypeError:arguments.push is not a function
}
foo('a', 'b')

// 2. DOM 查询结果
const divs = document.querySelectorAll('div')  // NodeList(伪数组)
const lis = document.getElementsByTagName('li') // HTMLCollection(伪数组)

// 3. 字符串也是类数组
const str = 'hello'
str[0]       // 'h'
str.length   // 5

// ========== 伪数组转真数组的方法 ==========
function demo() {
  // ✅ 方法1:Array.from()(推荐)
  const arr1 = Array.from(arguments)

  // ✅ 方法2:展开运算符(推荐)
  const arr2 = [...arguments]

  // ✅ 方法3:Array.prototype.slice.call()
  const arr3 = Array.prototype.slice.call(arguments)

  // ✅ 方法4:Array.of() + 展开
  // 注意:Array.of 是创建数组,不是转换伪数组
  // 这里只是演示组合用法

  // 转换后即可使用数组方法
  arr1.push('新元素')
  arr1.map(item => item.toUpperCase())
}

// ========== 自定义伪数组 ==========
const likeArray = { 0: 'a', 1: 'b', 2: 'c', length: 3 }
Array.from(likeArray)  // ['a', 'b', 'c']

// Array.from 还能接受映射函数
Array.from({ length: 5 }, (_, i) => i * 2)  // [0, 2, 4, 6, 8]

💡 面试加分点: ES6 的箭头函数中没有 arguments 对象,需要用 rest 参数 ...args 代替。NodeListforEach 方法(现代浏览器),但 HTMLCollection 没有,需要先转为数组。


6. let、const、var 的区别?

特性 var let const
作用域 函数作用域 块级作用域 {} 块级作用域 {}
变量提升 ✅(提升为 undefined) ⚠️(暂时性死区 TDZ) ⚠️(暂时性死区 TDZ)
重复声明 ✅ 允许 ❌ 报错 ❌ 报错
重新赋值 ❌ 不允许
全局属性 ✅(挂到 window)
javascript 复制代码
// ========== 1. 作用域区别 ==========
// var 是函数作用域
function testVar() {
  if (true) {
    var x = 1
  }
  console.log(x)  // 1(var 穿透了 if 块)
}

// let/const 是块级作用域
function testLet() {
  if (true) {
    let y = 2
    const z = 3
  }
  // console.log(y)  // ❌ ReferenceError
  // console.log(z)  // ❌ ReferenceError
}

// ========== 2. 变量提升 & 暂时性死区(TDZ) ==========
console.log(a)  // undefined(var 提升了声明)
var a = 1

// console.log(b)  // ❌ ReferenceError(let 处于 TDZ)
let b = 2

// ========== 3. 重复声明 ==========
var c = 1
var c = 2  // ✅ 不报错

// let d = 1
// let d = 2  // ❌ SyntaxError: Identifier 'd' has already been declared

// ========== 4. const 的"不可变"是指引用地址不可变 ==========
const obj = { name: '张三' }
obj.name = '李四'    // ✅ 可以修改属性(引用地址没变)
// obj = { name: '李四' }  // ❌ TypeError(不能重新赋值)

const arr = [1, 2, 3]
arr.push(4)     // ✅ 可以修改数组内容
// arr = [4, 5]  // ❌ TypeError

// 如果要真正冻结对象,使用 Object.freeze()
const frozen = Object.freeze({ name: '张三', info: { age: 25 } })
frozen.name = '李四'       // 静默失败(严格模式下报错)
frozen.info.age = 30       // ⚠️ 可以修改!freeze 只冻结第一层

// ========== 5. 经典面试题:循环中的 var vs let ==========
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)
}
// 输出:3, 3, 3(var 没有块级作用域,循环结束后 i = 3)

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 0)
}
// 输出:0, 1, 2(let 每次循环创建新的块级作用域)

// ========== 6. 全局声明的区别 ==========
var globalVar = 'hello'
console.log(window.globalVar)  // 'hello'(挂到 window 上)

let globalLet = 'world'
console.log(window.globalLet)  // undefined(不挂到 window 上)

💡 面试加分点: 最佳实践是默认使用 const,需要重新赋值时用 let永远不用 varconst 声明的是"绑定不可变"而非"值不可变"------对于引用类型,地址不可变但内容可以修改。


7. 原型和原型链是什么?

原型(Prototype) 是 JavaScript 实现继承的核心机制。每个对象都有一个内部属性 [[Prototype]](通过 __proto__Object.getPrototypeOf() 访问),指向其"原型对象"。

关键关系(必须牢记):

  • 每个函数 都有 prototype 属性(指向原型对象)
  • 每个对象 都有 __proto__ 属性(指向构造函数的 prototype
  • 原型对象的 constructor 属性指回构造函数
javascript 复制代码
// 原型链示意图:
dog ──__proto__──▶ Dog.prototype ──__proto__──▶ Animal.prototype ──__proto__──▶ Object.prototype ──__proto__──▶ null
                   ↑ constructor = Dog          ↑ constructor = Animal          ↑ constructor = Object
javascript 复制代码
// 构造函数继承(ES5 方式)
function Animal(name) {
  this.name = name
}
Animal.prototype.speak = function() {
  return `${this.name} makes a sound.`
}

function Dog(name) {
  Animal.call(this, name)  // 1. 继承实例属性(借用构造函数)
}
Dog.prototype = Object.create(Animal.prototype)  // 2. 继承原型方法
Dog.prototype.constructor = Dog                   // 3. 修复 constructor 指向
Dog.prototype.bark = function() {
  return `${this.name} barks!`
}

const dog = new Dog('Rex')
dog.speak()  // 'Rex makes a sound.'(从 Animal.prototype 上找到)
dog.bark()   // 'Rex barks!'(从 Dog.prototype 上找到)

// 原型链查找过程:
// dog.speak() → dog 自身没有 → Dog.prototype 没有 → Animal.prototype 找到 ✅
// dog.toString() → ... → Object.prototype 找到 ✅
// dog.xxx → ... → null → 返回 undefined

// 验证原型链
console.log(dog.__proto__ === Dog.prototype)              // true
console.log(Dog.prototype.__proto__ === Animal.prototype) // true
console.log(dog instanceof Dog)                           // true
console.log(dog instanceof Animal)                        // true
console.log(dog instanceof Object)                        // true

// ✅ hasOwnProperty:判断是自身属性还是原型链上的
dog.hasOwnProperty('name')   // true(自身属性)
dog.hasOwnProperty('speak')  // false(原型上的)

// ✅ ES6 class 语法(语法糖,底层仍是原型链)
class Cat extends Animal {
  constructor(name, color) {
    super(name)      // 等同于 Animal.call(this, name)
    this.color = color
  }
  meow() { return `${this.name} meows!` }
}

💡 面试加分点: Object.create(null) 创建的对象没有原型__proto__ 为 null),可以作为纯净的字典使用,不会被原型链上的属性干扰(如 toStringhasOwnProperty)。ES6 的 class 只是原型链的语法糖,typeof 一个 class 仍然是 'function'


8. this 的指向规则?

this 的指向不是在定义时确定的,而是在调用时根据调用方式动态决定的(箭头函数除外)。

this 绑定的优先级(从高到低):

  1. new 绑定 → 指向新创建的对象
  2. 显式绑定(call/apply/bind) → 指向指定对象
  3. 隐式绑定(方法调用) → 指向调用的对象
  4. 默认绑定(独立调用) → 全局对象 / undefined(严格模式)
javascript 复制代码
// 1. 默认绑定:独立函数调用
function foo() { console.log(this) }
foo()  // window(浏览器非严格模式)/ undefined(严格模式)

// 2. 隐式绑定:作为对象方法调用,this 指向调用者
const obj = {
  name: '张三',
  greet() { return `Hello, ${this.name}` }
}
obj.greet()  // 'Hello, 张三'

// ⚠️ 隐式绑定丢失(赋值给变量后,调用方式变了)
const greetFn = obj.greet
greetFn()  // 'Hello, undefined'(默认绑定了全局/undefined)

// 3. 显式绑定:call / apply / bind
function greet(greeting) { return `${greeting}, ${this.name}` }
greet.call({ name: '李四' }, 'Hi')     // 'Hi, 李四'
greet.apply({ name: '王五' }, ['Hey'])  // 'Hey, 王五'
const boundGreet = greet.bind({ name: '赵六' })
boundGreet('Hello')  // 'Hello, 赵六'

// 4. new 绑定:this 指向新创建的对象
function Person(name) { this.name = name }
const p = new Person('张三')
p.name  // '张三'

// 5. 箭头函数:没有自己的 this,继承定义时外层的 this,且不可被改变
const timer = {
  seconds: 0,
  start() {
    setInterval(() => {
      this.seconds++  // this 指向 timer(继承 start 的 this)
    }, 1000)
  }
}

// ⚠️ 箭头函数的 this 不能通过 call/apply/bind 修改
const arrowFn = () => this
arrowFn.call({ name: '李四' })  // 仍然是外层 this

// ✅ 综合面试题
const obj2 = {
  name: '张三',
  fn1: function() { return this.name },
  fn2: () => this?.name,
  fn3: function() {
    return (() => this.name)()
  }
}
obj2.fn1()  // '张三'(隐式绑定)
obj2.fn2()  // undefined(箭头函数,this 是外层全局/模块的 this)
obj2.fn3()  // '张三'(箭头函数继承 fn3 的 this,fn3 被 obj2 调用)

💡 面试加分点: bind 创建的函数再次 bind 无效(this 以第一次为准)。new 的优先级高于 bindnew 一个 bind 后的函数,this 指向新对象而非 bind 的对象)。在 React 类组件中,事件处理函数需要 bind(this) 或使用箭头函数来保持正确的 this 指向。


9. 作用域和作用域链是什么?

作用域(Scope) 决定了变量和函数的可访问范围 。JavaScript 使用词法作用域(静态作用域)------作用域在代码编写时就确定了,不是在运行时。

三种作用域:

  1. 全局作用域: 最外层,整个程序都能访问
  2. 函数作用域: 函数内部,var 声明的变量在此
  3. 块级作用域: {}内部,let/const 声明的变量在此(ES6 新增)

作用域链: 当查找一个变量时,先在当前作用域查找,找不到则向外层作用域逐级查找,直到全局作用域。

javascript 复制代码
// 词法作用域:作用域在代码编写时确定,不是运行时

// 全局作用域
const globalVar = 'global'

function outer() {
  const outerVar = 'outer'

  function inner() {
    const innerVar = 'inner'
    // 作用域链:inner -> outer -> global
    console.log(innerVar)   // 'inner'(自身)
    console.log(outerVar)   // 'outer'(外层)
    console.log(globalVar)  // 'global'(全局)
  }

  inner()
}

// ========== 词法作用域 vs 动态作用域 ==========
const x = 10
function foo() {
  console.log(x)  // 10(词法作用域:看定义位置,不看调用位置)
}
function bar() {
  const x = 20
  foo()  // 输出 10,不是 20!
}
bar()

// ========== 块级作用域 ==========
{
  let blockVar = 'block'
  var funcVar = 'function'
}
// console.log(blockVar)  // ❌ ReferenceError
console.log(funcVar)      // 'function'(var 不受块级作用域限制)

// ========== 变量提升 ==========
console.log(a)  // undefined(提升了声明,未提升赋值)
var a = 1

// 函数提升(整个函数都提升)
foo()  // 'foo'(可以在声明前调用)
function foo() { console.log('foo') }

// let/const 暂时性死区
// console.log(b)  // ❌ ReferenceError
let b = 2

💡 面试加分点: JavaScript 的词法作用域意味着闭包的本质------内部函数"记住"了定义时的作用域链。eval()with 可以修改词法作用域(但严格模式下被禁止),这也是为什么它们不推荐使用。


10. 什么是类型转换?== 和 === 的区别?

JavaScript 中有两种类型转换:

  • 隐式转换(Type Coercion): 运算时自动转换,如 '5' + 3
  • 显式转换(Type Casting): 手动调用 Number()String()
javascript 复制代码
// ========== == 和 === 的区别 ==========
// === 严格相等:不做类型转换,类型和值都必须相同
// == 宽松相等:先做类型转换,再比较值

1 === 1     // true
1 === '1'   // false(类型不同)
1 == '1'    // true('1' 被转为 1)
null == undefined   // true(特殊规则)
null === undefined  // false
NaN == NaN          // false(NaN 不等于任何值,包括自身)
NaN === NaN         // false

// ✅ 始终使用 ===,避免隐式转换的坑

// ========== Object.is():比 === 更精确 ==========
Object.is(NaN, NaN)   // true(=== 返回 false)
Object.is(+0, -0)     // false(=== 返回 true)
Object.is(1, 1)       // true(与 === 相同)

// ========== 隐式类型转换的规则 ==========

// 1. 字符串拼接(+ 号有字符串时,其他值转为字符串)
'5' + 3     // '53'(数字 → 字符串)
'5' + true  // '5true'
'5' + null  // '5null'

// 2. 数学运算(- * / % 会将字符串转为数字)
'5' - 3     // 2(字符串 → 数字)
'5' * '2'   // 10
true + 1    // 2(true → 1)
false + 1   // 1(false → 0)

// 3. 布尔转换(Boolean() 或条件判断)
// Falsy 值(转为 false 的值,只有 7 个):
Boolean(0)          // false
Boolean(-0)         // false
Boolean('')         // false
Boolean(null)       // false
Boolean(undefined)  // false
Boolean(NaN)        // false
Boolean(0n)         // false(BigInt 零)
// 其余所有值都是 Truthy,包括 {}、[]、'0'、'false'

// ✅ 常见面试坑题
[] == false     // true  → [] → '' → 0 == 0
[] == ![]       // true  → [] == false → 0 == 0
{} + []         // 0     → {} 被解析为代码块,+[] → 0
'' == 0         // true
' ' == 0        // true  → ' ' → 0
null == 0       // false(null 只和 undefined 宽松相等)
undefined == 0  // false

// ========== 显式类型转换 ==========
Number('123')     // 123
Number('12px')    // NaN
Number(true)      // 1
Number(null)      // 0
Number(undefined) // NaN
parseInt('12px')  // 12(解析到非数字字符为止)
String(123)       // '123'
Boolean([])       // true

💡 面试加分点: Object.is() 可以正确判断 NaN === NaN(返回 true)和 +0 === -0(返回 false),比 === 更精确。React 的浅比较(shallowEqual)内部就是用 Object.is() 来比较每一层属性。


11. 什么是执行上下文和执行栈?

执行上下文(Execution Context) 是 JavaScript 代码执行时的环境,包含了变量、函数声明、this 的值等信息。

三种执行上下文:

  1. 全局执行上下文: 程序启动时创建,只有一个
  2. 函数执行上下文: 每次函数调用时创建
  3. Eval 执行上下文: eval() 函数中创建(避免使用)

执行上下文的组成:

  • 变量环境(Variable Environment): var 声明和函数声明
  • 词法环境(Lexical Environment): let/const 声明
  • this 绑定
javascript 复制代码
// ========== 执行栈(Call Stack)==========
// 后进先出(LIFO)结构,管理执行上下文

function first() {
  console.log('first 开始')
  second()
  console.log('first 结束')
}
function second() {
  console.log('second 开始')
  third()
  console.log('second 结束')
}
function third() {
  console.log('third')
}

first()
// 调用栈变化:
// [global] → [global, first] → [global, first, second]
// → [global, first, second, third] → third 执行完弹出
// → [global, first, second] → second 执行完弹出
// → [global, first] → first 执行完弹出 → [global]

// ========== 变量提升(Hoisting)的原理 ==========
// 其实是执行上下文创建阶段的行为

console.log(a)   // undefined(var 声明被提升,赋值没有)
console.log(b)   // ReferenceError(let 在暂时性死区中)
console.log(c)   // ReferenceError(const 同理)
foo()            // ✅ 可以调用(函数声明整体提升)
bar()            // ❌ TypeError(var bar 提升为 undefined)

var a = 1
let b = 2
const c = 3
function foo() { console.log('foo') }
var bar = function() { console.log('bar') }

// 等价于引擎处理后的代码:
// var a = undefined        ← 提升
// var bar = undefined      ← 提升
// function foo() {...}     ← 整体提升
// console.log(a)           // undefined
// foo()                    // ✅
// bar()                    // ❌ bar 是 undefined
// a = 1                    ← 赋值在原位置执行

💡 面试加分点: let/const 实际上也会被"提升"(在创建阶段分配内存),但在声明语句之前处于暂时性死区(TDZ) ,访问会报 ReferenceError。函数声明的优先级高于 var 声明------如果同名,函数声明会覆盖 var


12. JavaScript 中有哪些遍历方法?区别是什么?

方法 适用对象 可中断 返回值 遍历内容
for 数组 ✅ break - 索引
for...of 可迭代对象 ✅ break -
for...in 对象 ✅ break - 可枚举属性名(含继承)
forEach 数组 undefined
map 数组 新数组
filter 数组 新数组
reduce 数组 累积值
some 数组 ✅(返回 true 时) boolean
every 数组 ✅(返回 false 时) boolean
find 数组 ✅(找到时) 元素/undefined
javascript 复制代码
// ========== for...in vs for...of ==========

// for...in 遍历对象的可枚举属性(包括继承的!)
const obj = { a: 1, b: 2, c: 3 }
for (const key in obj) {
  console.log(key)  // 'a', 'b', 'c'
}

// ⚠️ for...in 的坑:会遍历原型链上的可枚举属性
const arr = [1, 2, 3]
Array.prototype.customMethod = function() {}
for (const key in arr) {
  console.log(key)  // '0', '1', '2', 'customMethod' ← 原型上的方法也被遍历!
}

// ✅ for...of 遍历可迭代对象的值(数组/字符串/Map/Set 等)
for (const value of arr) {
  console.log(value)  // 1, 2, 3
}

// ========== forEach vs map ==========
// forEach:仅遍历,无返回值
// map:遍历并返回新数组

const nums = [1, 2, 3]
const result1 = nums.forEach(n => n * 2)  // undefined
const result2 = nums.map(n => n * 2)      // [2, 4, 6]

// ⚠️ forEach 无法中断
[1, 2, 3, 4, 5].forEach(n => {
  if (n === 3) return  // ❌ 这只是跳过当前回调,不是中断循环
  console.log(n)       // 1, 2, 4, 5(3 被跳过但循环继续)
})

// ✅ 需要中断时用 for...of 或 some/every/find
[1, 2, 3, 4, 5].some(n => {
  if (n === 3) return true  // 中断
  console.log(n)  // 1, 2
  return false
})

// ========== reduce 示例 ==========
const numbers = [1, 2, 3, 4, 5]

// 求和
const sum = numbers.reduce((acc, n) => acc + n, 0)  // 15

// 数组转对象
const users = [{ id: 1, name: '张三' }, { id: 2, name: '李四' }]
const userMap = users.reduce((map, user) => {
  map[user.id] = user
  return map
}, {})
// { 1: { id: 1, name: '张三' }, 2: { id: 2, name: '李四' } }

// ========== 对象遍历推荐方式 ==========
const person = { a: 1, b: 2, c: 3 }
Object.keys(person)     // ['a', 'b', 'c'](自身可枚举键)
Object.values(person)   // [1, 2, 3](自身可枚举值)
Object.entries(person)  // [['a',1], ['b',2], ['c',3]]

💡 面试加分点: for...in 遍历属性名for...of 遍历 。遍历对象推荐用 Object.keys/values/entries + for...of,而非 for...in(避免遍历到原型链属性)。mapforEach 都不会修改原数组,但如果数组元素是引用类型,回调中修改元素属性会影响原数组。


13. 创建对象有哪些方式?

javascript 复制代码
// ========== 1. 对象字面量(最常用) ==========
const obj1 = { name: '张三', age: 25 }

// ========== 2. new Object() ==========
const obj2 = new Object()
obj2.name = '李四'
obj2.age = 30

// ========== 3. 构造函数 ==========
function Person(name, age) {
  this.name = name
  this.age = age
}
Person.prototype.greet = function() {
  return `Hello, I'm ${this.name}`
}
const obj3 = new Person('王五', 28)

// ========== 4. Object.create()(指定原型) ==========
const proto = {
  greet() { return `Hello, I'm ${this.name}` }
}
const obj4 = Object.create(proto)
obj4.name = '赵六'
obj4.greet()  // "Hello, I'm 赵六"

// Object.create(null) 创建无原型的纯净对象
const dict = Object.create(null)
dict.key = 'value'
// dict.toString  // undefined(没有原型链上的方法)

// ========== 5. ES6 class(语法糖) ==========
class Animal {
  constructor(name) {
    this.name = name
  }
  speak() { return `${this.name} makes a sound` }
}
const obj5 = new Animal('Cat')

// ========== 6. 工厂函数 ==========
function createUser(name, age) {
  return {
    name,
    age,
    greet() { return `Hi, I'm ${name}` }
  }
}
const obj6 = createUser('孙七', 22)

💡 面试加分点: 工厂函数和构造函数的区别------工厂函数不需要 new,返回的对象没有共同的原型(每个对象都是独立的 Object);构造函数通过 new 调用,所有实例共享同一个 prototype,可以用 instanceof 判断类型。


14. 创建函数有哪些方式?具名函数与匿名函数的区别?

javascript 复制代码
// ========== 创建函数的方式 ==========

// 1. 函数声明(会提升)
function add(a, b) { return a + b }

// 2. 函数表达式(不会提升)
const subtract = function(a, b) { return a - b }

// 3. 箭头函数(ES6,无 this/arguments/prototype)
const multiply = (a, b) => a * b

// 4. Function 构造函数(几乎不用,安全性差)
const divide = new Function('a', 'b', 'return a / b')

// 5. 方法简写(对象中)
const obj = {
  greet() { return 'hello' }  // 等同于 greet: function() {}
}

// ========== 具名函数 vs 匿名函数 ==========

// 具名函数:有名字,方便调试和递归
function factorial(n) {
  if (n <= 1) return 1
  return n * factorial(n - 1)  // 可以通过名字递归调用
}

// 匿名函数:没有名字,常用于回调
setTimeout(function() {
  console.log('匿名函数')
}, 1000)

// 具名函数表达式:名字只能在函数内部使用
const fn = function myFunc() {
  console.log(typeof myFunc)  // 'function'(函数内部可访问)
}
// console.log(typeof myFunc) // ❌ ReferenceError(外部不可访问)

// ========== 函数声明 vs 函数表达式的区别 ==========
foo()  // ✅ 可以调用(函数声明会提升)
bar()  // ❌ TypeError: bar is not a function

function foo() { console.log('foo') }
var bar = function() { console.log('bar') }
// bar 被 var 提升为 undefined,undefined() 会报 TypeError

💡 面试加分点: 具名函数在错误堆栈中会显示函数名,便于调试;匿名函数则显示 <anonymous>。ES2015 开始,赋值给变量的匿名函数会自动推断函数名(const fn = () => {}; fn.name === 'fn'),但回调中的匿名函数仍然没有名字。


15. 什么是冒泡排序?

冒泡排序是最基础的排序算法之一------重复遍历数组,比较相邻元素,如果顺序错误就交换,就像气泡一样把最大值"冒"到末尾。

时间复杂度: O(n²) | 空间复杂度: O(1) | 稳定排序

javascript 复制代码
// ========== 基本冒泡排序 ==========
function bubbleSort(arr) {
  const len = arr.length
  for (let i = 0; i < len - 1; i++) {          // 外层:n-1 轮
    for (let j = 0; j < len - 1 - i; j++) {    // 内层:每轮减少一次比较
      if (arr[j] > arr[j + 1]) {
        // 交换(ES6 解构赋值)
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
      }
    }
  }
  return arr
}

bubbleSort([5, 3, 8, 1, 2])  // [1, 2, 3, 5, 8]

// ========== 优化版:提前终止(已排序则跳出) ==========
function bubbleSortOptimized(arr) {
  const len = arr.length
  for (let i = 0; i < len - 1; i++) {
    let swapped = false  // 标记本轮是否有交换
    for (let j = 0; j < len - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
        swapped = true
      }
    }
    if (!swapped) break  // 如果没有交换,说明已经有序,提前结束
  }
  return arr
}

// 最好情况(已排序):O(n)
// 平均 & 最坏情况:O(n²)

// ========== 其他常见排序算法时间复杂度对比 ==========
// | 算法       | 最好   | 平均     | 最坏     | 空间   | 稳定 |
// |-----------|--------|---------|---------|--------|------|
// | 冒泡排序   | O(n)   | O(n²)   | O(n²)   | O(1)   | ✅   |
// | 选择排序   | O(n²)  | O(n²)   | O(n²)   | O(1)   | ❌   |
// | 插入排序   | O(n)   | O(n²)   | O(n²)   | O(1)   | ✅   |
// | 快速排序   | O(nlogn)| O(nlogn)| O(n²)  | O(logn)| ❌   |
// | 归并排序   | O(nlogn)| O(nlogn)| O(nlogn)| O(n)  | ✅   |

💡 面试加分点: 实际开发中使用 Array.prototype.sort(),V8 引擎内部对小数组使用插入排序,大数组使用 TimSort(归并 + 插入的混合算法)。sort() 默认按字符串 Unicode 排序,数字排序必须传比较函数:arr.sort((a, b) => a - b)


16. 什么是宿主对象和原生对象?

javascript 复制代码
// ========== 原生对象(Native Objects)==========
// 由 ECMAScript 规范定义,与宿主环境无关
// Object、Array、Function、Date、RegExp、Map、Set、Promise、Symbol、Error 等

const arr = new Array(1, 2, 3)
const date = new Date()
const promise = new Promise(resolve => resolve())

// ========== 宿主对象(Host Objects)==========
// 由运行环境(浏览器/Node.js)提供,不属于 ECMAScript 标准

// 浏览器环境提供的宿主对象:
window              // 全局对象
document            // DOM 文档对象
navigator           // 浏览器信息
location            // URL 信息
history             // 浏览历史
localStorage        // 本地存储
sessionStorage      // 会话存储
XMLHttpRequest      // AJAX 请求
fetch               // 网络请求
console             // 控制台
setTimeout / setInterval  // 定时器

// Node.js 环境提供的宿主对象:
// process、Buffer、__dirname、__filename、require、module 等

// ========== 区分方法 ==========
// 原生对象:所有 JS 环境都有(浏览器、Node.js、Deno 等)
// 宿主对象:特定环境才有(如 document 只在浏览器中,process 只在 Node.js 中)

typeof window    // 浏览器中 'object',Node.js 中 'undefined'
typeof process   // Node.js 中 'object',浏览器中 'undefined'
typeof document  // 浏览器中 'object',Node.js 中 'undefined'

💡 面试加分点: globalThis(ES2020)是跨平台获取全局对象的统一方式------浏览器中等于 window,Node.js 中等于 global,Worker 中等于 self。使用它可以编写环境无关的代码。


17. 什么是解构赋值?有哪些高级用法?

解构赋值(Destructuring) 是 ES6 引入的语法,允许从数组或对象中按模式提取值并赋给变量。

javascript 复制代码
// ========== 数组解构 ==========
const [a, b, c] = [1, 2, 3]
console.log(a, b, c)  // 1 2 3

// 跳过元素
const [first, , third] = [1, 2, 3]
console.log(first, third)  // 1 3

// 默认值
const [x = 10, y = 20] = [1]
console.log(x, y)  // 1 20

// 交换变量(无需临时变量)
let m = 1, n = 2
;[m, n] = [n, m]
console.log(m, n)  // 2 1

// rest 元素
const [head, ...tail] = [1, 2, 3, 4, 5]
console.log(head)  // 1
console.log(tail)  // [2, 3, 4, 5]

// ========== 对象解构 ==========
const { name, age } = { name: '张三', age: 25 }
console.log(name, age)  // '张三' 25

// 重命名
const { name: userName, age: userAge } = { name: '李四', age: 30 }
console.log(userName, userAge)  // '李四' 30

// 默认值 + 重命名
const { role = 'user', level: lv = 1 } = { role: 'admin' }
console.log(role, lv)  // 'admin' 1

// 嵌套解构
const { address: { city, zip } } = {
  address: { city: '深圳', zip: '518000' }
}
console.log(city, zip)  // '深圳' '518000'

// rest 属性
const { id, ...rest } = { id: 1, name: '张三', age: 25 }
console.log(id)    // 1
console.log(rest)  // { name: '张三', age: 25 }

// ========== 函数参数解构(最实用)==========
function createUser({ name, age = 18, role = 'user' } = {}) {
  return { name, age, role }
}
createUser({ name: '张三' })  // { name: '张三', age: 18, role: 'user' }
createUser()                   // { name: undefined, age: 18, role: 'user' }

// ========== 动态属性名解构 ==========
const key = 'email'
const { [key]: emailValue } = { email: 'test@example.com' }
console.log(emailValue)  // 'test@example.com'

// ========== 实用场景 ==========
// 1. 从函数返回多个值
function getMinMax(arr) {
  return { min: Math.min(...arr), max: Math.max(...arr) }
}
const { min, max } = getMinMax([3, 1, 4, 1, 5])
console.log(min, max)  // 1 5

// 2. 导入模块的部分功能
// import { useState, useEffect } from 'react'

// 3. 接口响应数据提取
const { data: { list = [], total = 0 } } = await fetchAPI('/users')

💡 面试加分点: 解构赋值是浅拷贝 ,嵌套对象解构出来的仍然是引用。对 nullundefined 进行解构会报错(const { a } = null → TypeError),所以函数参数解构要加默认值 = {}


18. 什么是展开运算符和 rest 参数?

展开运算符(Spread ...rest 参数 使用相同的 ... 语法,但作用相反:

  • 展开运算符: 将数组/对象"展开"为独立元素
  • rest 参数: 将独立元素"收集"为数组
javascript 复制代码
// ========== 展开运算符(Spread) ==========

// 1. 数组展开
const arr1 = [1, 2, 3]
const arr2 = [...arr1, 4, 5, 6]  // [1, 2, 3, 4, 5, 6]

// 2. 数组浅拷贝
const copy = [...arr1]
copy.push(4)
console.log(arr1)  // [1, 2, 3](不影响原数组)

// 3. 函数传参
console.log(Math.max(...arr1))  // 3(等同于 Math.max(1, 2, 3))

// 4. 对象展开(浅拷贝 + 合并)
const defaults = { theme: 'light', lang: 'zh', fontSize: 14 }
const userConfig = { theme: 'dark', fontSize: 16 }
const config = { ...defaults, ...userConfig }
// { theme: 'dark', lang: 'zh', fontSize: 16 }(后者覆盖前者)

// 5. 条件展开
const isAdmin = true
const user = {
  name: '张三',
  ...(isAdmin && { role: 'admin', permissions: ['read', 'write'] })
}

// ========== rest 参数 ==========

// 1. 函数参数收集
function sum(...numbers) {
  return numbers.reduce((acc, n) => acc + n, 0)
}
sum(1, 2, 3, 4, 5)  // 15

// 2. 与普通参数结合(rest 必须在最后)
function log(level, ...messages) {
  console.log(`[${level}]`, ...messages)
}
log('ERROR', '连接失败', '重试中...')
// [ERROR] 连接失败 重试中...

// 3. 解构中的 rest
const { a, b, ...remaining } = { a: 1, b: 2, c: 3, d: 4 }
console.log(remaining)  // { c: 3, d: 4 }

// ========== 实用技巧 ==========

// 合并去重
const merged = [...new Set([...arr1, ...arr2])]

// 将 NodeList 转为数组
const divs = [...document.querySelectorAll('div')]

// 字符串转字符数组(正确处理 emoji)
const chars = [..."Hello 👋"]  // ['H', 'e', 'l', 'l', 'o', ' ', '👋']

💡 面试加分点: 展开运算符做的是浅拷贝 (nested objects still share references)。对象展开的顺序很重要------后面的属性会覆盖前面的同名属性,这在 React 的 props 传递中很常用:<Component {...defaultProps} {...customProps} />


19. 什么是短路求值?逻辑运算符有哪些妙用?

javascript 复制代码
// ========== 短路求值原理 ==========
// && 和 || 不一定返回 boolean,而是返回决定结果的那个操作数

// || 逻辑或:返回第一个 truthy 值,都为 falsy 则返回最后一个
0 || '' || null || '默认值' || 'hello'  // '默认值'

// && 逻辑与:返回第一个 falsy 值,都为 truthy 则返回最后一个
1 && 'hello' && { name: '张三' }  // { name: '张三' }
0 && 'hello'                       // 0(遇到 falsy 直接返回)

// ?? 空值合并(ES2020):只在 null 或 undefined 时使用默认值
0 ?? '默认'         // 0(0 不是 null/undefined)
'' ?? '默认'        // ''(空字符串不是 null/undefined)
null ?? '默认'      // '默认'
undefined ?? '默认'  // '默认'

// ========== || vs ?? 的区别(重要!)==========
const count = 0
count || 10    // 10 ⚠️ 0 是 falsy,被当成"没值"
count ?? 10    // 0  ✅ 0 不是 null/undefined,保留原值

const text = ''
text || '默认文本'   // '默认文本' ⚠️
text ?? '默认文本'   // '' ✅

// ========== 常见用法 ==========

// 1. 设置默认值
function greet(name) {
  name = name ?? '匿名'        // 推荐用 ??
  return `Hello, ${name}!`
}

// 2. 条件执行(代替简单的 if)
const isLoggedIn = true
isLoggedIn && showDashboard()      // 登录了才执行
isLoggedIn || redirectToLogin()    // 没登录就跳转

// 3. 安全访问属性(可选链 ?.)
const user = { profile: { address: { city: '深圳' } } }
const city = user?.profile?.address?.city ?? '未知'  // '深圳'
const zip = user?.profile?.address?.zip ?? '000000'  // '000000'

// 4. 可选链调用函数
const callback = null
callback?.()  // 不会报错,返回 undefined

// 5. 逻辑赋值运算符(ES2021)
let a = null
a ??= '默认值'   // a = a ?? '默认值' → '默认值'
let b = ''
b ||= '默认值'   // b = b || '默认值' → '默认值'(空字符串被覆盖)
let c = 0
c &&= 10        // c = c && 10 → 0(c 是 falsy,不赋值)

💡 面试加分点: ?? 只关心 nullundefined,而 || 会把 0''falseNaN 都当成"无效值"。在处理表单数据、API 响应时,?? 更安全。?? 不能直接和 &&|| 混用,需要加括号:(a ?? b) || c


20. 什么是隐式类型转换?有哪些常见的坑?

javascript 复制代码
// ========== + 号的二义性(最大的坑)==========
// + 号既是加法运算符,也是字符串拼接运算符
// 规则:只要有一方是字符串,就做拼接

'5' + 3        // '53'(数字 → 字符串)
3 + '5'        // '35'
'5' + true     // '5true'
'5' + null     // '5null'
'5' + undefined // '5undefined'
'5' + {}       // '5[object Object]'
'5' + []       // '5'([] → '')
1 + 2 + '3'   // '33'(先算 1+2=3,再 3+'3'='33')
'1' + 2 + 3   // '123'(从左到右,'1'+2='12','12'+3='123')

// ========== - * / 只做数学运算 ==========
'5' - 3        // 2(字符串 → 数字)
'5' * '2'      // 10
'abc' - 1      // NaN('abc' 无法转为数字)
true + true    // 2(true → 1)
null + 1       // 1(null → 0)
undefined + 1  // NaN(undefined → NaN)

// ========== 对象的隐式转换 ==========
// 对象转为原始值时,按顺序调用:
// 1. [Symbol.toPrimitive](hint)(如果存在)
// 2. valueOf()
// 3. toString()

const obj = {
  valueOf() { return 42 },
  toString() { return '对象' }
}
obj + 1       // 43(优先调用 valueOf)
`${obj}`      // '对象'(模板字符串调用 toString)

// 自定义 Symbol.toPrimitive
const money = {
  amount: 100,
  currency: 'CNY',
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') return this.amount
    if (hint === 'string') return `${this.amount} ${this.currency}`
    return this.amount  // default
  }
}
+money        // 100
`${money}`    // '100 CNY'
money + 50    // 150

// ========== 经典面试题 ==========
[] + []         // ''(两个空数组 → 两个空字符串 → '')
[] + {}         // '[object Object]'('' + '[object Object]')
{} + []         // 0({} 被解析为代码块,+[] → +'' → 0)
({}) + []       // '[object Object]'(加括号后 {} 是对象)

[] == ![]       // true  → ![] = false → [] == false → '' == 0 → 0 == 0
'' == false     // true('' → 0,false → 0)
' ' == false    // true(' ' → 0,false → 0)

// ========== 安全的类型转换方式 ==========
// 转数字
Number('123')       // 123(严格转换,失败返回 NaN)
parseInt('12.5px')  // 12(解析整数,忽略后续非数字字符)
parseFloat('12.5px') // 12.5
+'123'              // 123(一元 + 运算符)

// 转字符串
String(123)         // '123'
(123).toString()    // '123'
123 + ''            // '123'(不推荐,隐式转换)

// 转布尔
Boolean(0)          // false
!!0                 // false(双重否定,简写方式)
!!''                // false
!!null              // false
!!undefined         // false
!!NaN               // false
!![]                // true(空数组是 truthy!)
!!{}                // true(空对象是 truthy!)

💡 面试加分点: == 的隐式转换规则是面试高频考点------null == undefined 为 true(特殊规则),NaN == NaN 为 false,其他情况按 Number → 转换后比较。实际开发中始终使用 === 和显式类型转换,避免隐式转换带来的 bug。


21. 什么是模板字符串?有哪些高级用法?

javascript 复制代码
// ========== 基本用法 ==========
const name = '张三'
const age = 25
console.log(`Hello, ${name}! 你今年 ${age} 岁。`)

// ========== 多行字符串 ==========
const html = `
  <div class="card">
    <h2>${name}</h2>
    <p>年龄: ${age}</p>
  </div>
`

// ========== 表达式求值 ==========
console.log(`${age >= 18 ? '成年' : '未成年'}`)  // '成年'
console.log(`总价: ${(9.9 * 3).toFixed(2)} 元`)  // '总价: 29.70 元'

// ========== 嵌套模板 ==========
const items = ['苹果', '香蕉', '橙子']
const list = `
  <ul>
    ${items.map(item => `<li>${item}</li>`).join('\n    ')}
  </ul>
`

// ========== 标签模板(Tagged Templates)------ 高级用法 ==========
// 标签函数接收字符串片段和表达式的值
function highlight(strings, ...values) {
  return strings.reduce((result, str, i) => {
    const value = values[i] !== undefined ? `<mark>${values[i]}</mark>` : ''
    return result + str + value
  }, '')
}

const keyword = 'JavaScript'
const output = highlight`学习 ${keyword} 是前端必备技能`
// '学习 <mark>JavaScript</mark> 是前端必备技能'

// ========== 实用:防 XSS 的 HTML 标签模板 ==========
function safeHtml(strings, ...values) {
  const escape = (str) => String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
  return strings.reduce((result, str, i) => {
    return result + str + (values[i] !== undefined ? escape(values[i]) : '')
  }, '')
}

const userInput = '<script>alert("xss")</script>'
const safe = safeHtml`<p>用户输入: ${userInput}</p>`
// '<p>用户输入: &lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;</p>'

// ========== 实用:CSS-in-JS 风格 ==========
function css(strings, ...values) {
  return strings.reduce((result, str, i) =>
    result + str + (values[i] ?? ''), '')
}
const color = 'red'
const style = css`color: ${color}; font-size: ${16}px;`

💡 面试加分点: 标签模板(Tagged Templates)是 styled-components、lit-html、GraphQL(gql`````)等库的核心技术。String.raw 是一个内置的标签函数,可以获取原始字符串(不处理转义符):String.raw\\n`'\n'`(两个字符,不是换行)。


22. 什么是 Symbol?有什么用途?

Symbol 是 ES6 新增的第七种基本类型 ,每个 Symbol 值都是唯一的,常用作对象属性的键以避免命名冲突。

javascript 复制代码
// ========== 创建 Symbol ==========
const s1 = Symbol('描述')
const s2 = Symbol('描述')
console.log(s1 === s2)  // false(每个 Symbol 都是唯一的,即使描述相同)
console.log(typeof s1)  // 'symbol'

// ❌ 不能 new Symbol()
// new Symbol()  // TypeError: Symbol is not a constructor

// ========== 作为对象属性键(避免命名冲突)==========
const ID = Symbol('id')
const user = {
  name: '张三',
  [ID]: 12345  // Symbol 属性
}
user[ID]  // 12345
user.ID   // undefined(点语法无法访问 Symbol 属性)

// Symbol 属性不会被普通遍历方法枚举到
Object.keys(user)         // ['name']
JSON.stringify(user)      // '{"name":"张三"}'
for (const key in user) {}  // 只遍历到 'name'

// 获取 Symbol 属性需要专门的方法
Object.getOwnPropertySymbols(user)  // [Symbol(id)]
Reflect.ownKeys(user)               // ['name', Symbol(id)]

// ========== Symbol.for():全局共享 Symbol ==========
const s3 = Symbol.for('shared')
const s4 = Symbol.for('shared')
console.log(s3 === s4)  // true(全局注册表中复用)

Symbol.keyFor(s3)  // 'shared'(查找全局 Symbol 的 key)

// ========== 内置 Symbol(Well-known Symbols)==========
// 1. Symbol.iterator ------ 定义迭代行为
const range = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    let current = this.from
    const last = this.to
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { done: true }
      }
    }
  }
}
[...range]  // [1, 2, 3, 4, 5]

// 2. Symbol.toPrimitive ------ 自定义类型转换
class Temperature {
  constructor(celsius) { this.celsius = celsius }
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') return this.celsius
    if (hint === 'string') return `${this.celsius}°C`
    return this.celsius
  }
}
const temp = new Temperature(36.5)
+temp         // 36.5
`${temp}`     // '36.5°C'
temp + 0      // 36.5

// 3. Symbol.hasInstance ------ 自定义 instanceof 行为
class EvenNumber {
  static [Symbol.hasInstance](num) {
    return typeof num === 'number' && num % 2 === 0
  }
}
4 instanceof EvenNumber   // true
5 instanceof EvenNumber   // false

// ========== 实用场景 ==========
// 1. 定义常量枚举(确保唯一性)
const STATUS = {
  PENDING: Symbol('pending'),
  FULFILLED: Symbol('fulfilled'),
  REJECTED: Symbol('rejected'),
}

// 2. 私有属性模拟
const _password = Symbol('password')
class Account {
  constructor(pwd) { this[_password] = pwd }
  verify(pwd) { return this[_password] === pwd }
}

💡 面试加分点: Symbol 是基本类型,不是对象。常见内置 Symbol 还有 Symbol.toStringTag(自定义 Object.prototype.toString 的输出)和 Symbol.asyncIterator(异步迭代器)。TypeScript 4.3 开始支持用 Symbol 作为类的私有字段键。


23. Map 和 Set 与普通对象和数组的区别?

javascript 复制代码
// ========== Map vs Object ==========
const map = new Map()
const obj = {}

// 1. 键的类型:Map 的键可以是任意类型,Object 的键只能是字符串或 Symbol
map.set(1, 'number key')
map.set(true, 'boolean key')
map.set({ id: 1 }, 'object key')
map.set(null, 'null key')

obj[1] = 'number key'       // 实际键是字符串 '1'
obj[true] = 'boolean key'   // 实际键是字符串 'true'
obj[{}] = 'object key'      // 实际键是字符串 '[object Object]'

// 2. 顺序保证:Map 按插入顺序迭代,Object 在大多数情况下也按插入顺序
//    但 Object 的整数键会被排到前面
const o = {}
o['b'] = 1; o['2'] = 2; o['a'] = 3; o['1'] = 4
Object.keys(o)  // ['1', '2', 'b', 'a'](整数键排到前面!)

const m = new Map()
m.set('b', 1); m.set('2', 2); m.set('a', 3); m.set('1', 4)
[...m.keys()]  // ['b', '2', 'a', '1'](严格按插入顺序)

// 3. 大小:Map 有 size 属性,Object 需要 Object.keys().length
map.size  // 4
Object.keys(obj).length  // 3

// 4. 性能:频繁增删操作 Map 更快

// 5. Map 常用操作
map.set('key', 'value')   // 设置
map.get('key')             // 获取
map.has('key')             // 是否存在
map.delete('key')          // 删除
map.clear()                // 清空
map.forEach((val, key) => console.log(key, val))

// Map 与数组互转
const entries = [['a', 1], ['b', 2]]
const mapFromArr = new Map(entries)
const arrFromMap = [...mapFromArr]  // [['a', 1], ['b', 2]]

// Map 与对象互转
const objFromMap = Object.fromEntries(mapFromArr)
const mapFromObj = new Map(Object.entries(objFromMap))

// ========== Set vs Array ==========
const set = new Set([1, 2, 3, 2, 1])
console.log(set.size)  // 3(自动去重)
console.log([...set])  // [1, 2, 3]

// Set 常用操作
set.add(4)       // 添加
set.has(3)       // 是否存在(O(1) 时间复杂度,比数组 includes 的 O(n) 快)
set.delete(2)    // 删除
set.clear()      // 清空

// ========== Set 实现集合运算 ==========
const setA = new Set([1, 2, 3, 4])
const setB = new Set([3, 4, 5, 6])

// 并集
const union = new Set([...setA, ...setB])  // {1, 2, 3, 4, 5, 6}

// 交集
const intersection = new Set([...setA].filter(x => setB.has(x)))  // {3, 4}

// 差集
const difference = new Set([...setA].filter(x => !setB.has(x)))  // {1, 2}

// ES2025 新增原生方法(部分浏览器已支持)
// setA.union(setB)
// setA.intersection(setB)
// setA.difference(setB)
// setA.symmetricDifference(setB)
// setA.isSubsetOf(setB)
// setA.isSupersetOf(setB)

💡 面试加分点: Map 的 has/get/set 操作时间复杂度是 O(1),在需要频繁查找的场景(如缓存、计数器)比 Object 更高效。Set 的 has 是 O(1),Array 的 includes 是 O(n)。React 中用 Set 存储选中项 ID 比数组更高效。


24. 什么是 for...of 和可迭代协议?

可迭代协议(Iterable Protocol) :一个对象实现了 [Symbol.iterator]() 方法,返回一个迭代器,就是可迭代对象。for...of 专门遍历可迭代对象。

javascript 复制代码
// ========== 内置可迭代对象 ==========
// Array、String、Map、Set、arguments、NodeList、TypedArray

// 数组
for (const item of [1, 2, 3]) {
  console.log(item)  // 1, 2, 3
}

// 字符串(正确处理 Unicode)
for (const char of '你好👋') {
  console.log(char)  // '你', '好', '👋'(比 for 循环更安全)
}

// Map(解构键值对)
const userMap = new Map([['name', '张三'], ['age', 25]])
for (const [key, value] of userMap) {
  console.log(`${key}: ${value}`)
}

// Set
for (const item of new Set([1, 2, 3])) {
  console.log(item)
}

// ========== 普通对象不可迭代 ==========
const obj = { a: 1, b: 2 }
// for (const item of obj) {}  // ❌ TypeError: obj is not iterable

// ✅ 遍历对象的方式
for (const [key, value] of Object.entries(obj)) {
  console.log(key, value)
}

// ========== 使用可迭代对象的语法 ==========
const arr = [1, 2, 3]

// 1. 展开运算符
const copy = [...arr]

// 2. 解构赋值
const [a, b, c] = arr

// 3. Array.from()
Array.from(arr)

// 4. Promise.all() / Promise.race() 等
Promise.all(arr.map(n => Promise.resolve(n)))

// 5. Map/Set 构造函数
new Set(arr)

// ========== for...of vs for...in 总结 ==========
// for...of:遍历可迭代对象的 **值**
// for...in:遍历对象的可枚举 **属性名**(包括原型链)

const arr2 = ['a', 'b', 'c']
for (const val of arr2) console.log(val)   // 'a', 'b', 'c'(值)
for (const idx in arr2) console.log(idx)   // '0', '1', '2'(索引,字符串类型)

// ========== for...of 可以中断 ==========
for (const item of [1, 2, 3, 4, 5]) {
  if (item === 3) break  // ✅ 可以使用 break
  console.log(item)      // 1, 2
}

💡 面试加分点: for...of 遍历字符串时能正确处理 4 字节 Unicode 字符(如 emoji),而传统 for 循环或 charAt 不行。for...of 可以用 break/continue 控制流程,这是 forEach 做不到的。


25. 什么是严格模式?有什么作用?

严格模式(Strict Mode) 是 ES5 引入的限制性 JavaScript 变体,让代码更安全、更规范,帮助发现潜在错误。

javascript 复制代码
// ========== 开启严格模式 ==========
// 1. 整个脚本开启
'use strict'

// 2. 函数级别开启
function strictFn() {
  'use strict'
  // 只在这个函数内生效
}

// 3. ES6 模块和 class 内部自动开启严格模式

// ========== 严格模式的主要限制 ==========

// 1. ❌ 禁止使用未声明的变量
// x = 10  // ReferenceError: x is not defined

// 2. ❌ 禁止删除变量/函数
// let y = 1
// delete y  // SyntaxError

// 3. ❌ 禁止参数重名
// function fn(a, a) {}  // SyntaxError: Duplicate parameter name not allowed

// 4. ❌ 禁止 with 语句
// with (Math) { ... }  // SyntaxError

// 5. this 不再默认指向全局对象
function strictThis() {
  'use strict'
  console.log(this)  // undefined(非严格模式下是 window)
}
strictThis()

// 6. ❌ 禁止八进制字面量
// const oct = 010  // SyntaxError(非严格模式下等于 8)
const oct = 0o10    // ✅ ES6 八进制写法

// 7. 对只读属性赋值会报错
'use strict'
const frozen = Object.freeze({ name: '张三' })
// frozen.name = '李四'  // TypeError(非严格模式下静默失败)

// 8. eval 有自己的作用域
'use strict'
eval('var evalVar = 1')
// console.log(evalVar)  // ReferenceError(非严格模式下会泄漏到外层)

// ========== 为什么要用严格模式? ==========
// 1. 消除 JavaScript 的不合理、不严谨之处
// 2. 提高编译器效率,增加运行速度
// 3. 为未来新版本 JavaScript 做铺垫
// 4. 使 debug 更容易(错误会被抛出而不是静默失败)

💡 面试加分点: 现代开发中几乎不需要手动写 'use strict'------ES6 模块(import/export)和 class 内部默认就是严格模式。打包工具(Webpack/Vite)处理后的代码通常也是严格模式。


26. 什么是 IIFE(立即执行函数表达式)?

IIFE(Immediately Invoked Function Expression) 是定义后立即执行的匿名函数,常用于创建独立作用域、避免变量污染全局。

javascript 复制代码
// ========== IIFE 的几种写法 ==========

// 1. 经典写法(推荐)
;(function() {
  const privateVar = 'hello'
  console.log(privateVar)
})()

// 2. 箭头函数版
;(() => {
  console.log('箭头函数 IIFE')
})()

// 3. 带参数
;(function(name, age) {
  console.log(`${name}, ${age}`)
})('张三', 25)

// 4. 带返回值
const result = (function() {
  return 42
})()
console.log(result)  // 42

// ========== IIFE 的经典应用 ==========

// 1. 避免变量污染全局(ES6 之前的模块模式)
const module = (function() {
  let _count = 0  // "私有"变量
  return {
    increment() { return ++_count },
    decrement() { return --_count },
    getCount() { return _count },
  }
})()
module.increment()  // 1
module.getCount()   // 1
// _count 无法从外部访问

// 2. 解决循环中的闭包问题(var 时代)
for (var i = 0; i < 3; i++) {
  ;(function(j) {
    setTimeout(() => console.log(j), 100)
  })(i)
}
// 输出:0 1 2(每次循环创建新的 IIFE 作用域)

// 3. 初始化代码(避免临时变量污染)
const config = (() => {
  const env = process.env.NODE_ENV
  const baseURL = env === 'production'
    ? 'https://api.example.com'
    : 'http://localhost:3000'
  return { env, baseURL, timeout: 5000 }
})()

// ========== 现代替代方案 ==========
// ES6 的块级作用域(let/const)可以替代很多 IIFE 场景
{
  const privateVar = 'hello'
  console.log(privateVar)
}
// console.log(privateVar)  // ❌ ReferenceError

💡 面试加分点: IIFE 前面加 ; 是防御性编程------避免与前一行代码意外连接(如果前一行没分号,() 可能被当成函数调用)。ES6 之后 IIFE 的使用场景大大减少,但在一些库的源码和立即初始化的场景中仍然常见。


27. 什么是事件循环中的微任务和宏任务?执行顺序如何?

javascript 复制代码
// ========== 微任务(Microtask)==========
// Promise.then/catch/finally
// MutationObserver
// queueMicrotask()
// process.nextTick()(Node.js,优先级最高的微任务)

// ========== 宏任务(Macrotask)==========
// setTimeout / setInterval
// setImmediate()(Node.js)
// I/O 操作
// UI 渲染
// MessageChannel
// requestAnimationFrame(浏览器在渲染前执行)

// ========== 执行顺序规则 ==========
// 1. 执行同步代码(当前宏任务)
// 2. 清空所有微任务(包括微任务中产生的微任务)
// 3. 渲染(如果需要)
// 4. 执行下一个宏任务
// 5. 回到步骤 2

// ========== 综合面试题1 ==========
console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve()
  .then(() => {
    console.log('3')
    setTimeout(() => console.log('4'), 0)
  })
  .then(() => console.log('5'))
setTimeout(() => console.log('6'), 0)
console.log('7')

// 输出:1 → 7 → 3 → 5 → 2 → 6 → 4
// 分析:
// 同步:1, 7
// 微任务队列:Promise.then(3), 然后 then(5)
// 宏任务队列:setTimeout(2), setTimeout(6), setTimeout(4)(在微任务中添加)

// ========== 综合面试题2 ==========
async function foo() {
  console.log('foo start')
  await bar()
  console.log('foo end')  // 等同于 bar().then(() => console.log('foo end'))
}

async function bar() {
  console.log('bar')
}

console.log('script start')
setTimeout(() => console.log('setTimeout'), 0)
foo()
new Promise(resolve => {
  console.log('promise executor')
  resolve()
}).then(() => console.log('promise then'))
console.log('script end')

// 输出:script start → foo start → bar → promise executor → script end
//       → foo end → promise then → setTimeout

// ========== queueMicrotask 的使用 ==========
console.log('start')
queueMicrotask(() => console.log('microtask 1'))
Promise.resolve().then(() => console.log('microtask 2'))
queueMicrotask(() => console.log('microtask 3'))
console.log('end')

// 输出:start → end → microtask 1 → microtask 2 → microtask 3
// 微任务按照加入队列的顺序执行

// ========== 微任务"饥饿"问题 ==========
// ❌ 微任务无限添加会阻塞渲染和宏任务
function badIdea() {
  Promise.resolve().then(badIdea)  // 无限递归微任务 → 页面卡死
}
// ✅ 用 setTimeout 让出执行权
function betterIdea() {
  setTimeout(betterIdea, 0)  // 每次宏任务之间可以渲染
}

💡 面试加分点: await 之后的代码相当于放入微任务队列(等价于 .then())。requestAnimationFrame 既不是宏任务也不是微任务------它在浏览器渲染前执行,每帧一次。Node.js 的 process.nextTick 优先级高于 Promise.then


28. 如何判断一个变量是否为空?

javascript 复制代码
// "空"的含义在不同上下文中不同,以下是各种情况的判断方式

// ========== 1. 判断 null 或 undefined ==========
const isNullOrUndefined = (val) => val == null  // 宽松相等,同时匹配 null 和 undefined
isNullOrUndefined(null)       // true
isNullOrUndefined(undefined)  // true
isNullOrUndefined(0)          // false
isNullOrUndefined('')         // false

// 或者更明确地
const isNil = (val) => val === null || val === undefined

// ========== 2. 判断空字符串 ==========
const isEmptyString = (val) => val === ''
// 注意:' '(空格)不是空字符串
const isBlankString = (val) => typeof val === 'string' && val.trim() === ''
isBlankString('  ')   // true
isBlankString('\n\t') // true

// ========== 3. 判断空数组 ==========
const isEmptyArray = (val) => Array.isArray(val) && val.length === 0
isEmptyArray([])     // true
isEmptyArray([0])    // false

// ========== 4. 判断空对象 ==========
const isEmptyObject = (val) => {
  return val !== null
    && typeof val === 'object'
    && !Array.isArray(val)
    && Object.keys(val).length === 0
}
isEmptyObject({})            // true
isEmptyObject({ a: 1 })     // false
isEmptyObject([])            // false(不是对象)

// ========== 5. 通用的"空值"判断 ==========
function isEmpty(val) {
  if (val == null) return true                               // null, undefined
  if (typeof val === 'boolean') return false                 // boolean 永远不算"空"
  if (typeof val === 'number') return false                  // 数字永远不算"空"(包括 0)
  if (typeof val === 'string') return val.trim().length === 0  // 空字符串或纯空白
  if (Array.isArray(val)) return val.length === 0            // 空数组
  if (val instanceof Map || val instanceof Set) return val.size === 0
  if (typeof val === 'object') return Object.keys(val).length === 0
  return false
}

isEmpty(null)       // true
isEmpty(undefined)  // true
isEmpty('')         // true
isEmpty('  ')       // true
isEmpty([])         // true
isEmpty({})         // true
isEmpty(new Map())  // true
isEmpty(0)          // false
isEmpty(false)      // false
isEmpty('hello')    // false

// ========== 实际使用场景 ==========
// API 响应数据处理
function processResponse(data) {
  if (data?.list?.length > 0) {
    renderList(data.list)
  } else {
    showEmptyState()
  }
}

// 表单验证
function validateForm(fields) {
  const errors = {}
  if (!fields.name?.trim()) errors.name = '姓名不能为空'
  if (!fields.email?.trim()) errors.email = '邮箱不能为空'
  return Object.keys(errors).length > 0 ? errors : null
}

💡 面试加分点: val == null 是判断 null/undefined 的简写(这是少数推荐使用 == 的场景)。Boolean(val) 会把 0''NaN 都当成"空",不适合所有场景。可选链 ?. 配合 ?? 是现代 JS 处理空值的最佳方式。

相关推荐
何时梦醒2 小时前
Docker 容器化入门:从「我电脑能跑」到「哪台机器都能跑」
后端·docker·面试
颜进强2 小时前
11 - 从需求拆解到 OpenSpec:为什么不要直接敲 /opsx:explore
前端·后端·ai编程
禁止摆烂_才浅2 小时前
HTML 高频面试题
前端·面试·html
一拳不是超人2 小时前
Godot 信号不是线程安全的:我是怎么在后台线程里翻车的
前端·架构
无糖可可果2 小时前
从零理解 Docker:为什么我的代码在你的电脑上跑不起来?
javascript·后端
林太白2 小时前
What did Trea work help me with in this optimization process
前端·后端
一拳不是超人2 小时前
被 Tauri「体积小」种草后,我拿它做了个本地 AI 桌面工具,然后踩了这些坑
前端·架构
hyf3266333 小时前
高收益玩法:站群泛程序结合本地服务引流变现全流程
运维·服务器·前端·搜索引擎·蜘蛛池
IT_陈寒3 小时前
Vite热更新失效?八成是这个配置在搞鬼
前端·人工智能·后端