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 属性和数字索引的对象,但不具备数组方法 (如 push、forEach、map 等)。
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代替。NodeList有forEach方法(现代浏览器),但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,永远不用var。const声明的是"绑定不可变"而非"值不可变"------对于引用类型,地址不可变但内容可以修改。
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),可以作为纯净的字典使用,不会被原型链上的属性干扰(如toString、hasOwnProperty)。ES6 的class只是原型链的语法糖,typeof一个 class 仍然是'function'。
8. this 的指向规则?
this 的指向不是在定义时确定的,而是在调用时根据调用方式动态决定的(箭头函数除外)。
this 绑定的优先级(从高到低):
new绑定 → 指向新创建的对象- 显式绑定(
call/apply/bind) → 指向指定对象 - 隐式绑定(方法调用) → 指向调用的对象
- 默认绑定(独立调用) → 全局对象 /
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的优先级高于bind(new一个 bind 后的函数,this 指向新对象而非 bind 的对象)。在 React 类组件中,事件处理函数需要bind(this)或使用箭头函数来保持正确的 this 指向。
9. 作用域和作用域链是什么?
作用域(Scope) 决定了变量和函数的可访问范围 。JavaScript 使用词法作用域(静态作用域)------作用域在代码编写时就确定了,不是在运行时。
三种作用域:
- 全局作用域: 最外层,整个程序都能访问
- 函数作用域: 函数内部,
var声明的变量在此 - 块级作用域:
{}内部,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 的值等信息。
三种执行上下文:
- 全局执行上下文: 程序启动时创建,只有一个
- 函数执行上下文: 每次函数调用时创建
- 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(避免遍历到原型链属性)。map和forEach都不会修改原数组,但如果数组元素是引用类型,回调中修改元素属性会影响原数组。
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')
💡 面试加分点: 解构赋值是浅拷贝 ,嵌套对象解构出来的仍然是引用。对
null或undefined进行解构会报错(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,不赋值)
💡 面试加分点:
??只关心null和undefined,而||会把0、''、false、NaN都当成"无效值"。在处理表单数据、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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
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>用户输入: <script>alert("xss")</script></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 处理空值的最佳方式。