前端面试题 ===> 【JavaScript - 基础】

3,117 阅读38分钟

JavaScript 基础 高频面试题

1. 数据类型有哪些?如何判断数据类型?区别是什么?

基本类型(7种,值类型)

string number boolean null undefined symbol(ES6) bigint(ES2020)

  • symbol:创建对象的唯一属性名,因此可以用来防止属性名冲突,保证属性名的唯一;详细见
  • bigint:大整数相加;

引用类型(1种)

Object(包含 Array、Function、Date、RegExp、Map、Set、WeakMap、WeakSet 等)

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

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

简单来说:

基本数据类型是存储在栈中的简单数据段,占据空间小,属于被频繁使用的数据;

引用数据类型是存储在堆内存中,占据空间大,引用数据类型在栈中存储了地址,该地址指向堆中该实体的起始地址,通过检索栈中的地址,取得地址后在堆中获得实体;

四种类型判断方法

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'

拓展: Object.prototype.toString.call() 判断类型的原理

面试回答:

  • 依据对象内部的[[Class]]内部隐藏属性(ES6之后为Symbol.toStringTag)。

  • Object原型上的toString方法会读取该隐藏标记,返回[object 类型名]格式的字符串

  • 使用call是为了改变this只想,避开子类重写后的toString方法,typeof只是简单判断,读取不到这个内部标记,因此精度差。


核心依据: 内部属性 [[Class]] (ES6之后叫 Symbol.toStringTag)


注意:

  • ES5及更早:每个对象有一个内部隐藏属性[[Class]],是一个字符串标记,用来标识原生内置对象的类型;
  • Es6+:废弃了 [[Class]],改用Symbol.toStringTag这个Symbol访问器属性。

执行过程:

Object.prototype.toString.call(xxx)

  1. toString 是 Object 原型上的方法,不是数组、Date自己重写过的toString;

  2. call(xxx),把 this 绑定到传入的变量 xxx上;

  3. 这个原型toString内部逻辑:

    • 获取该对象的内部类型标记
    • 返回格式字符串
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 表示"空值"——开发者主动赋值,表示"这里应该有值,但目前为空"。

2.1 基本区别

let a            // 声明但未赋值
console.log(a)   // undefined

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

2.2 类型判断

typeof undefined  // 'undefined'
typeof null       // 'object'(历史 bug)

2.3 相等性

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

2.4 数值转换

Number(undefined)   // NaN
Number(null)        // 0
undefined + 1       // NaN
null + 1            // 1

2.5 常见出现场景

// undefined 出现的场景:
// 1. 已声明未赋值
let x

function foo() {}
// 2. 函数无返回值 → undefined
foo()

function bar(a) { return a }
// 3. 函数参数未传递 → undefined
bar()

const obj = {}
// 4. 访问对象不存在的属性 → undefined
obj.name
// 5. void 运算符 → undefined
void 0

// null 出现的场景:
// 1. DOM 查询无结果 → null
let el = document.getElementById('notExist')

// 2. 原型链终点 → null
Object.getPrototypeOf(Object.prototype)
let cache = fetchData()

// 3. 手动释放引用(帮助垃圾回收)
cache = null

// '{"b":null}'(undefined 会被忽略)
JSON.stringify({ a: undefined, b: null })

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

补充:void 0

  • 为什么 void 0 永远返回真正的 undefined?

  • 核心原理:

    • void 是 JavaScript 的一个一元运算符,它会计算右侧表达式的值然后丢弃结果,返回 undefined。
    void 0      // → undefined
    void false  // → undefined
    void 'abc'  // → undefined
    void {}     // → undefined
    
  • 为什么 void 0 比 undefined 更可靠?

    • 关键在于 undefined 可以被篡改(非严格模式下):
    // 非严格模式下
    var undefined = '被篡改了!'
    console.log(undefined) // → '被篡改了!'  ❌ 不再是真正的 undefined
    
    // void 0 永远安全
    console.log(void 0)    // → undefined     ✅ 永远是真正的 undefined
    
  • 而 void 0 不受影响,因为:

    • void 运算符的返回值是 JS 引擎内部直接返回的 原始 undefined 值
    • 它不通过变量查找,不存在被覆盖的可能
  • 一句话总结:

    • void 运算符会丢弃右侧表达式的值并返回引擎内部的原始 undefined,不经过任何变量查找,因此无论是否在严格模式、无论全局 undefined 是否被篡改,void 0 永远是最可靠的 undefined。

3. 创建对象的方式

3.1 对象字面量(最常用)

const obj1 = { name: '张三', age: 25 }

3.2. new Object()

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

3.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)

3.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(没有原型链上的方法)

3.5. ES6 class(语法糖)

class Animal {
  constructor(name) {
    this.name = name
  }
  speak() { return `${this.name} makes a sound` }
}
const obj5 = new Animal('Cat')

3.6. 工厂函数

function createUser(name, age) {
  return {
    name,
    age,
    greet() { return `Hi, I'm ${name}` }
  }
}
const obj6 = createUser('孙七', 22)

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


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

4.1 创建函数方式

4.1.1 函数声明(会提升)
function add(a, b) { return a + b }
4.1.2 函数表达式(不会提升)
const subtract = function(a, b) { return a - b }
4.1.3 箭头函数(ES6,无 this/arguments/prototype)
const multiply = (a, b) => a * b
4.1.4 Function 构造函数(几乎不用,安全性差)
const divide = new Function('a', 'b', 'return a / b')
4.1.5 方法简写(对象中)
const obj = {
  greet() { return 'hello' }  // 等同于 greet: function() {}
}

4.2 具名函数与匿名函数的区别

4.2.1 基本区别
  • 具名函数:
    1. 可以先使用后声明;
    2. 不能作为事件处理函数;
    3. 可以用于构造函数;
  • 匿名函数:
    1. 必须先声明后使用;
    2. 可作为事件处理函数使用;
    3. 使用方式:
      • 函数表达式;
      • 立即执行函数;
        • 立即执行函数的格式:
        !(function(形参) { 函数体 })(实参);
        !(function(形参) { 函数体 } (实参));
        // ❗ 立即执行函数的最后面必须加分号
        
    • 不能作为构造函数使用;
4.2.2 具名函数:有名字,方便调试和递归
function factorial(n) {
  if (n <= 1) return 1
  return n * factorial(n - 1)  // 可以通过名字递归调用
}
4.2.3 匿名函数:没有名字,常用于回调
setTimeout(function() {
  console.log('匿名函数')
}, 1000)
4.2.4 具名函数表达式:名字只能在函数内部使用
const fn = function myFunc() {
  console.log(typeof myFunc)  // 'function'(函数内部可访问)
}
// console.log(typeof myFunc) // ❌ ReferenceError(外部不可访问)

4.3 函数声明 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'),但回调中的匿名函数仍然没有名字。


5. JS常用的内置对象,并列举该对象常用的方法

基本包装类型:Number、String、Boolean

5.1 Number ➡ 数值对象

方法或属性描述
Number.toFixed()采用定点计数法格式化数字(保留几位小数)
Number.toString()将一个数字转换成字符串
Number.isInterger()判断指定数字是否为整数
Number.NaN特殊的非数字值(NaN属于数字)
Number.valueOf()返回原始数值

5.2 String ➡ 字符串对象

方法或属性描述
length获取字符串的长度
substring()截取字符串,返回指定部分的字符串
split()将字符串分隔成数组
startsWith()检测是否以某段字符开头
endsWith()检测是否以某段字符结尾
includes()判断一个字符串是否包含在另一个字符串中
replace()替换字符串中指定的字符
toUpperCase()将字符串转换为大写
toLowerCase()将字符串转换为小写

5.3 Boolean ➡ 布尔对象

方法或属性描述
Boolean.toString()将布尔值转换成字符串
Boolean.valueOf()Boolean对象的布尔值

5.4 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

5.5 Array ➡ 数组

方法或属性描述
length数组的长度
shift()删除数组的第一个元素,并返回被删除的元素
pop()删除数组的最后一个元素,返回被删除的元素
unshift()给数组的最前面插入一个或多个元素,返回插入元素之后的数组长度
push()给数组的最后面添加一个或多个元素,返回添加元素之后的数组长度
splice()插入、删除、替换数组的元素,以数组的形式返回被修改的内容
sort()对数组元素进行排序,并返回排序好的数组
reverse()将数组中元素的位置颠倒,并返回颠倒元素之后的数组
forEach()遍历数组,无返回值
map()迭代数组,遍历原数组,把原数组中的每一项数据加工改造,形成一个新数组返回
filter()过滤原数组中的数据,把满足条件的数据放在一个新数组中返回
reduce()累加器,返回函数累计处理的结果
find()返回数组中满足条件的第一个元素
join()数组转换成字符串,返回一个字符串
some()判断数组中是否至少有一个元素满足条件,返回布尔值
every()判断数组中的所有元素是否都满足条件,返回布尔值
concat()合并数组,返回一个新数组
slice()获取指定的元素,返回一个含有指定元素的数组
includes()查看数组中是否有每一项数据,返回布尔值
indexOf()正向查看数组里卖弄指定数据的索引
flat()数组降维,返回一维数组

5.6 Object ➡ 基础对象

方法或属性描述
Object.constructor对象的构造函数
Object.keys()将对象的所有属性放到数组中
Object.values()将对象的所有属性值放到数组中
Object.hasOwnProperty()检查属性是否被继承
Object.isPrototypeOf()一个对象是否是另一个对象的原型
Object.propertyIsEnumerable()是否可以通过 for/in 循环看到属性
Object.toLocaleString()返回对象的本地字符串表示
Object.toString()定义一个对象的字符串表示
Object.valueOf()指定对象的原始值

5.7 Date ➡ 日期对象

方法或属性描述
Date.getDate()返回一个月中的某一天
Date.getDay()返回一周中的某一天
Date.getFullYear()返回 Date 对象的年份字段
Date.getHours()返回 Date 对象的小时字段
Date.getMilliseconds()返回 Date 对象的毫秒字段
Date.getMinutes()返回 Date 对象的分钟字段
Date.getMonth()返回 Date 对象的月份字段
Date.getSeconds()返回 Date 对象的秒字段
Date.getTime()返回 Date 对象的毫秒表示

5.8 RegExp ➡ 正则表达式对象

方法或属性描述
RegExp.exec()通用的匹配模式
RegExp.global正则表达式是否全局匹配
RegExp.ignoreCase正则表达式是否区分大小写
RegExp.lastIndex下次匹配的起始位置
RegExp.source正则表达式的文本
RegExp.test()检测一个字符串是否匹配某个模式
RegExp.toString()把正则表达式转换成字符串

5.9 Function ➡ 函数构造器

方法或属性描述
Function.apply()将函数作为一个对象的方法调用
Function.arguments[]传递给函数的参数
Function.call()将函数作为对象的方法调用
Function.caller调用当前函数的函数
Function.length已声明的参数的个数
Function.prototype对象类的原型
Function.toString()把函数转换成字符串

5.10 Arguments ➡ 函数参数集合

方法或属性描述
Arguments[]函数参数的数组
Arguments一个函数的参数和其他属性
Arguments.callee当前正在运行的函数
Arguments.length传递给函数的参数个数

5.11 Error ➡ 异常对象

方法或属性描述
Error.message可以读取的错误消息
Error.name错误的类型
Error.toString()把 Error 对象转换成字符串
EvalError不正确使用 eval()时抛出
SyntaxError抛出该错误用来通知语法错误
RangeError在数字超出合法范围时抛出
ReferenceError在数字超出合法范围时抛出
TypeError当一个值的类型错误时,抛出该异常
URIError由 URl 的编码和解码方法抛出

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


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

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'

补充:JS 类型判断:instanceof 与 constructor 原理深度解析

可移步至 - JavaScript 类型判断:instanceof 与 constructor 原理深度解析


7. Object.is()、==、=== 的区别

7.1 Object.is()

  • 是ES6中的一个新方法,它用于判断两个值是否为同一个值;
  • 语法:
    Object.is(val1, val2);
    
  • 对于基本数据类型,如果它们的值和数据类型相等,则返回 true;
  • 对于引用数据类型,仅当它们的地址相同时才会返回 true;

7.2 ==(等值符)

  • 只比较值;
  • 数据类型相同:
    • 去比较两边的值是否相等;
  • 数据类型不同:
    • 会尝试自动转换为相同类型,能转换就比较值,转换不了就是 false;

7.3 ===(等同符)

  • 既要判断值相等,也要判断数据类型相同;
  • 数据类型相同:
    • 直接比较值是否相等;
  • 数据类型不同:
    • 直接返回false;

7.4 Object.is() 与 === 区别

  • 主要区别:
    • 它们如何处理 NaN 与 -0;
  • 对于NaN:
    Object.is(NaN, NaN); // true
    NaN === NaN // false
    
  • 对于-0与+0:
    Object.is(-0, +0); // false
    -0 === +0 // true
    

7.5 Object.is() 与 == 区别

  • ==: 两边类型不一样时,自动转成相同类型再对比
  • Object.is():绝不转换类型,类型不同直接 false

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

  • 伪数组(类数组对象) 是具有 length 属性和数字索引的对象,但不具备数组方法(如 push、forEach、map 等)。
// ========== 常见的伪数组 ==========
// 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]

真数组 和 伪数组 的区别

  1. 共同点:
    • 都可以通过索引取值,都有length属性;
  2. 不同点:
    • 原型不同:
      • 数组原型:
        • Array.prototype;
      • 伪数组原型:
        • 是一种集合,不具备数组的方法;

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


9. 多维数组降维

  1. 使用 flat()方法:
    • 可以给该方法传递参数,一种是传递当前数组的维数,还有一种就是直接传递Infinity(不管是几维数组,最后都会降为一维);
  2. 递归:
    const arr = [1, 2, [2, 3, [4, 5, [6, 7, [1, 2]], [{a: 1}], {c: 12}]], 2, 3];
    const flatArr = (arr) => {
        let newArr = [];
        for(const item of arr) {
            if (!Array.isArray(item)) {
                newArr.push(item);
                continue;
            }
            newArr.push(...flatArr(item));
        }
        return newArr;
    };
    
  3. ❌ 先使用 数组的 join() 方法将数组转换为字符串,再使用 字符串的 split()方法:
    • 缺点:
      • 如果元素为数字,转换之后是字符串了;
      • 不能转换对象类型的元素; image.png
    const arr = [[1, 2, 3], [4, 5, 6]];
    const str = arr.join(); // 字符串 => 1, 2, 3, 4, 5, 6
    const newArr = str.split(','); // ['1', '2', '3', '4', '5', '6']
    

10. 怎么判断两个对象相等?

  1. ES6中有一个方法判断两个对象是否相等,这个方法判断的是两个对象的地址是否一致:
    const obj1 = { a: 1 };
    const obj2 = { a: 1 };
    const obj3 = obj1;
    console.log(Object.is(obj1, obj2)); // false
    console.log(Object.is(obj1, obj3)); //true
    
    • 当前需求是比较两个对象内容是否一致时就没用了;
  2. 想要比较两个对象内容是否一致,思路是要遍历对象的所有键名和键值是否都一致:
const obj1 = { a: 1, b: { c: 2 } };
ocnst obj2 = { b: { c: 3 }, a: 1 };

function isObjectValueEqual(a, b) {
    // 判断两个对象是否指向同一内存,指向同一内存返回 true
    if (a === b) return true;
    // 获取两个对象键值数组
    let aProps = Object.getOwnPropertyNames(a);
    let bProps = Object.getOwnPropertyNames(b);
    // 判断两个对象键值数组长度是否一致,不一致返回 false
    if (aProps.length !== bProps.length) return false;
    // 遍历对象的键值
    for (let prop in a) {
        // 判断 a 的键值,在 b 中是否存在,不存在,返回 false
        if (b.hasOwnProperty(prop)) {
            // 判断 a 的键值是否为对象,是则递归,不是对象直接判断键值是否相等,不相等返回false
            if (typeof a[prop] === 'object') {
                if (!isObjectValueEqual(a[prop], b[prop])) return false;
            } else if (a[prop] !== b[prop]) {
                return false;
            }
        } else {
            return false;
        }
    }
    return true;
}

console.log(isObjectValueEqual(obj1, obj2)); // false

11. 列举 强制转换类型 和 隐式类型转换

  1. 强制类型转换:
    • 转数字:
      • Number()、parseInt()、parseFloat();
      Number(null); // 0
      Number(''); // 0
      Number(); // 0
      parseInt('11.11'); // 11
      parseInt('11s1.ss'); // 11
      parseFloat('11.11'); // 11.11
      parseFloat('11s1.ss'); // 11
      
    • 转字符串:
      • String()、toString();
    • 转布尔:
      • Boolean();
  2. 隐式类型转换:
    • +、-、*、/、%、==、>=、<=;

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

方法适用对象可中断返回值遍历内容
for数组✅ break-索引
for...of可迭代对象✅ break-值
for...in对象✅ break-可枚举属性名(含继承)
forEach数组❌undefined值
map数组❌新数组值
filter数组❌新数组值
reduce数组❌累积值值
some数组✅(返回 true 时)boolean值
every数组✅(返回 false 时)boolean值
find数组✅(找到时)元素/undefined值
// ========== 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. 谈谈对变量的理解

  1. 变量就是一个用来存储数据的容器;
  2. 本质:
    • 内存里的一块空间,用来存储数据;
  3. 初始化:
    • 声明变量并及进行赋值操作;
  4. 命名规则:
    • 只能是数字、英文字母、美元符、下划线组成;
    • 开头不能是数字;
  5. 声明变量:let;
  6. 使用:
    • 必须是先声明再使用;

14. let、const、var的区别

  • let / const:
    1. ES6新增的;
    2. 不存在变量提升;
    3. 有块作用域;
    4. 必须先声明再使用;
    5. 不能声明重复变量;
    6. let:
      • 声明变量;
      • 有暂时性死区;
    7. const:
      • 声明常量;
      • 声明的时候必须进行初始化;
  • var:
    1. 存在变量提升;
    2. 没有块作用域;
    3. 既可以声明变量也可以声明常量;
    4. 可以先声明后使用(变量提升);
特性varletconst
作用域函数作用域块级作用域 {}块级作用域 {}
变量提升✅(提升为 undefined)⚠️(暂时性死区 TDZ)⚠️(暂时性死区 TDZ)
重复声明✅ 允许❌ 报错❌ 报错
重新赋值✅✅❌ 不允许
全局属性✅(挂到 window)❌❌
// ========== 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 声明的是"绑定不可变"而非"值不可变"——对于引用类型,地址不可变但内容可以修改。


15. for-in 与 for-of 的区别

15.1 for-in

  • 适用:
    • 对象、数组、伪数组、字符串;
  • 遍历当前对象可枚举属性及其原型链上的属性;
  • 遍历的是 属性或索引;
  • 得到的索引是 字符串类型;

15.2 for-of

  • 适用:
    • 数组、伪数组、字符串;
  • 遍历的是(可迭代的数据)数组/伪数组的元素、字符串的值
  • 得到的索引是数字类型;
  • 如果遍历数组的时候,要使用数组的索引,可以使用arr.entries()方法(该方法的返回值是一个由给定对象自有的可枚举字符串键属性的键值对组成的数组,每个键值对都是一个包含两个元素的数组,第一个元素是属性的键(始终是字符串),第二个元素是属性值);
    • image.png

16. 什么是冒泡排序?

  • 冒泡排序:

    • 是最基础的排序算法之一——重复遍历数组,比较相邻元素,如果顺序错误就交换;
    • 就像气泡一样把最大值"冒"到末尾。
  • 时间复杂度: O(n²);

  • 空间复杂度: O(1);

// ========== 基本冒泡排序 ==========
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)。


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

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

// ========== 数组解构 ==========
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 参数: 将独立元素"收集"为数组
// ========== 展开运算符(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. 什么是短路求值?逻辑运算符有哪些妙用?

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

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

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

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

// ========== || vs ?? 的区别(重要!)==========
const count = 0
count || 10    // 10 ⚠️ 0 是 false,被当成"没值"
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. 什么是隐式类型转换?有哪些常见的坑?

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

'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. 什么是模板字符串?有哪些高级用法?

// ========== 基本用法 ==========
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 值都是唯一的,常用作对象属性的键以避免命名冲突。

// ========== 创建 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 与普通对象和数组的区别?

// ========== 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. 什么是宿主对象和原生对象?请指出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'

区别

  1. 原生对象:
    • “独立于宿主环境” 的 ECMAScript 实现提供的对象;
    • 包含:Object、Function、Array、String、Boolean、Number、Date、RegExp、Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError、URIError;
  2. 内置对象:
    • 我们不必明确实例化内置对象,它已经被内部实例化了;
    • 同样是 “独立于宿主环境”。而 ECMA-262 只定义了 两个内置对象,即 Global、Math;
  3. 宿主对象:
    • BOM 和 DOM都是宿主对象;
    • 因为其对于不同的宿主环境所展示的内容不同;
    • 简单来说,ECMAScript官方未定义的对象都宿主对象,因为其未定义的对象大多数都是自己通过ECMAScript程序创建的对象;

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


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

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

for...of 专门遍历可迭代对象。

// ========== 内置可迭代对象 ==========
// 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 做不到的。


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

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

// ========== 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 处理空值的最佳方式。


27. 展开运算符为什么不能展开有些基本数据类型?

直接结论

const num = 42
console.log(...num)       // ❌ TypeError: number is not iterable

const str = 'hello'
console.log(...str)       // ✅ 可以!字符串是可迭代的
console.log(...str)       // → 'h', 'e', 'l', 'l', 'o'

const bool = true
console.log(...bool)      // ❌ TypeError: boolean is not iterable

不是"不能展开",而是只有满足"可迭代协议"的对象才能被展开。

核心原理:展开运算符依赖「可迭代协议」

... 本质上是调用对象的 Symbol.iterator 方法:

// ...obj 等价于:
// Array.from(obj)
// 或 [...obj] → 内部调用 obj[Symbol.iterator]()
  • 有 Symbol.iterator → 可迭代 → 能展开
  • 没有 Symbol.iterator → 不可迭代 → 报错

各类型的情况

类型能否展开原因
String✅原生实现了 Symbol.iterator,逐字符迭代
Number❌没有 Symbol.iterator
Boolean❌没有 Symbol.iterator
undefined/null❌没有 Symbol.iterator
Array✅原生实现了 Symbol.iterator
Map/Set✅原生实现了 Symbol.iterator
Arguments✅原生实现了 Symbol.iterator
普通 Object❌没有 Symbol.iterator(除非手动加)

验证:给对象加上 Symbol.iterator 就能展开了

const obj = { a: 1, b: 2 }

// ❌ 默认不能
console.log(...obj)  // TypeError: obj is not iterable

// ✅ 手动添加 Symbol.iterator 后
obj[Symbol.iterator] = function* () {
  yield this.a
  yield this.b
}
console.log(...obj)  // → 1, 2

那数字/布尔值能不能"强行"展开?

不能直接展开,但可以包装成数组:

const num = 42
console.log(...[num])     // → 42(包一层数组)

const obj = { a: 1 }
console.log(...Object.values(obj))  // → 1(转成可迭代对象再展开)

面试标准答案:展开运算符 ... 只能作用于可迭代对象(即拥有 Symbol.iterator 方法的对象)。基本数据类型中只有字符串实现了该接口(按字符迭代),而 number、boolean 等没有实现 Symbol.iterator,所以无法展开。这不是语言"禁止"的,而是它们不满足可迭代协议。


补充:生成器函数详解

obj[Symbol.iterator] = function* () {
  yield this.a
  yield this.b
}

逐行拆解

obj[Symbol.iterator]
  • 给对象 obj 添加一个名为 Symbol.iterator 的方法。这个属性名是一个内置 Symbol,JS 引擎通过它来判断对象是否可迭代。

  • 等价于:

    obj['Symbol.iterator'] = ...  // Symbol.iterator 本质是个 key
    
function* ()
  • 这是生成器函数(Generator Function),* 号标识它是一个生成器。

    • 普通函数用 function()
    • 生成器函数用 function*()
    • 返回的是一个迭代器对象,自带 next() 方法
yield
  • yield 是生成器内部的"暂停键",每次遇到 yield 就吐出一个值并暂停执行。
yield this.a   // 第一次调用 next() 时吐出 obj.a 的值 → 1
yield this.b   // 第二次调用 next() 时吐出 obj.b 的值 → 2

完整执行流程

const obj = { a: 1, b: 2 }

// 添加迭代器后
obj[Symbol.iterator] = function* () {
  yield this.a
  yield this.b
}

// 展开时内部发生了什么?
console.log(...obj)  // → 1, 2
  • 展开运算符内部实际调用了:
const iterator = obj[Symbol.iterator]()  // 1. 获取迭代器
iterator.next()  // → { value: 1, done: false }  2. 第一次 next,得到 1
iterator.next()  // → { value: 2, done: false }  3. 第二次 next,得到 2
iterator.next()  // → { value: undefined, done: true }  4. 结束

this 指向问题

  • 注意 yield this.a 里的 this — 因为是用方法简写形式赋值给 obj 的属性,所以 this 指向 obj 本身:
// 等价于:
obj[Symbol.iterator] = function* () {
  // this === obj
  yield this.a  // obj.a → 1
  yield this.b  // obj.b → 2
}

对比:不用生成器的写法

// 生成器写法(简洁)
obj[Symbol.iterator] = function* () {
  yield this.a
  yield this.b
}

// 等价的手动迭代器写法(啰嗦)
obj[Symbol.iterator] = function () {
  let i = 0
  const keys = ['a', 'b']
  return {
    next: () => {
      if (i < keys.length) {
        return { value: this[keys[i++]], done: false }
      }
      return { value: undefined, done: true }
    }
  }
}
  • 生成器函数只是语法糖,让定义迭代器变得更简单。

  • 一句话总结:function* + yield 是 ES6 生成器语法,通过 Symbol.iterator 让普通对象变成可迭代的,展开时 JS 引擎会依次调用 next() 取出每个 yield 产出的值。

29. console.log (0.1 + 0.2) 为什么不等于 0.3,原因是什么

一句话总结

0.1 + 0.2 = 0.30000000000000004,根源:JS 的 Number 遵循 IEEE 754 双精度浮点数标准,用二进制存储小数,0.1 和 0.2 在二进制里是无限循环小数,存的时候就被截断近似了,相加后误差放大,结果不等于 0.3。

拆开讲(面试可以这么说)

  1. 计算机底层只有二进制(0、1),整数很好转,但十进制小数转二进制小数很多是无限循环

    • 0.1(十进制) 转二进制 ≈ 0.0001100110011…(无限循环 0011)
    • 0.2(十进制) 转二进制 ≈ 0.001100110011…(无限循环 0011) 但是双精度浮点数只有 52 位存有效数字,存不下无限循环,只能截断、近似保存,也就是说:你代码里写的 0.1,内存里根本不是精确的 0.1,只是一个很接近它的近似值。
  2. 两个近似值相加,得到的也是近似值,不是精确的 0.3 0.3 本身转二进制同样是无限循环小数,内存里 0.3 也是一个近似值,两个近似相加的结果 ≠ 0.3 的那个近似,于是打印出来就是 0.30000000000000004

注意:不是 JS 独有的!Java、Python、C++ 用 double 类型都有这个问题,是 IEEE754 标准的共性问题。

延伸考点(金融前端必问)

  • ❌ 不要直接用 0.1 + 0.2 === 0.3 判断相等
  • ✅ 判断浮点数相等:设置一个极小误差(机器 epsilon)
const EPS = 1e-8
function equal(a,b){
  return Math.abs(a - b) < EPS
}
equal(0.1+0.2, 0.3) // true
  • ✅ 金融场景:不要直接浮点数运算,使用:整数分单位 /decimal.js/big.js

面试官常追加一问:0.1.toFixed(2) 是 "0.10" 没问题,为什么 1.005.toFixed(2) 得到 "1.00"?

答:

  1. 首先 1.005 本身存的就是近似值,比真实 1.005 略小;
  2. toFixed 不是我们日常的四舍五入,是银行家舍入(四舍六入五成双) ,不是简单逢 5 进 1,金融对账不能依赖它。

面试精简背诵版

因为 JS 的 Number 是 IEEE754 双精度浮点数。十进制的 0.1 和 0.2 转换成二进制小数是无限循环小数,存储时会截断近似,本身就有误差。两个带误差的近似值相加,最终结果就不等于 0.3。这个不是 JSbug,是浮点数存储标准带来的问题,金融场景要避免直接用浮点数做金额运算。


30. 对于金额数据要求保留两位小数,该怎么做才是最保险的

核心一句话先背下来:金融场景绝对不能直接用 toFixed,优先用基于整数分(最小单位)存储 + 运算,展示层做格式化;运算要避开浮点数精度陷阱。

先讲坑,再给方案,最后给代码,面试这样说逻辑最稳。

1、先说为什么 Number.toFixed(2) 不保险(面试官最爱听)

JS 浮点数是 IEEE754 二进制存储,存在精度丢失:

0.1 + 0.2 // 0.30000000000000004
(1.005).toFixed(2) // "1.00",预期1.01,出错!

toFixed 不是四舍五入,是银行家舍入(四舍六入五成双) ,金融对账不允许,财务、支付、结算会出资金差异。 👉 结论:toFixed 只能做简单展示,不能用于计算、不能作为最终金额取值。

2、最保险方案:分「存储 / 运算层」和「展示层」(金融行业标准做法)

✅ 方案 A:后端统一用【分为单位,整数传输】【金融首选,最推荐】

这是银行、证券、支付系统主流方案,面试优先讲这个!

  • 后端:金额以整数 “分” 返回,比如 100.50 元 → 返回 10050
  • 前端:不做任何金额计算,只负责展示格式化
  • 所有加减乘除、利息、手续费、对账全部交给后端,前端只负责渲染
  • 好处:完全避开 JS 浮点数问题,对账 100% 一致,风险最低

格式化示例(分转元,保留 2 位)

/**
 * 分转元格式化
 * @param {number} fen 分,整数
 * @returns string 100.00
 */
function fenToYuan(fen) {
  const yuan = fen / 100
  return yuan.toFixed(2)
}

面试话术:金融核心原则:金额计算下沉后端,前端只做展示,前端不承担资金计算逻辑,这是风险最小的方案。

✅ 方案 B:前端必须做计算(特殊场景,比如本地预览、表单临时计算)

如果业务强制前端算(比如理财计算器),不能用浮点数直接运算,两种可靠实现:

  1. 放大为整数运算(乘以 100 转整数,算完再缩小)
  2. 使用成熟高精度库:decimal.js / big.js(生产推荐)

示例:放大整数运算(手写,无第三方库)

// 加法
function add(a, b) {
  return Math.round(a * 100 + b * 100) / 100
}
// 四舍五入保留2位(普通四舍五入,财务常用)
function round2(num) {
  return Math.round(num * 100) / 100
}

⚠️ 缺点:乘除场景(利率、折扣)复杂时,手写容易有边界 bug,复杂计算不要手写。

👉 复杂计算直接上 decimal.js,面试一定要提:

import Decimal from 'decimal.js'
const a = new Decimal('1.005')
console.log(a.toFixed(2)) // "1.01",符合预期四舍五入

重点:实例化的时候传字符串,不要传数字,避免传入时就精度丢失!new Decimal('1.005') ✅ new Decimal(1.005) ❌

3、展示格式化额外注意点(金融细节加分项)

  1. 入参优先字符串:后端返回金额建议返回字符串 "100.05",而不是数字,防止大数精度丢失(超过 Number.MAX_SAFE_INTEGER)

  2. 区分两种舍入规则,面试可以主动说:

    • 普通交易金额:四舍五入
    • 银行利息、部分结算:向下取整(截尾) 、向上取整,按产品规则,不能写死
    // 向下保留两位(抹分)
    Math.floor(num * 100) / 100
    // 向上保留两位
    Math.ceil(num * 100) / 100
    
  3. 展示增强:千分位分隔 12,345.00,可以搭配 Intl.NumberFormat

new Intl.NumberFormat('zh-CN', {
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
}).format(12345.1) // '12,345.10'

注意:Intl 格式化只是展示,同样不要拿它的结果去参与计算

4、整理成一段流畅面试回答(直接背)

在金融项目里金额保留两位小数,我会优先遵循一个原则:计算交给后端,前端只负责展示,这是最保险的。 最佳方案是前后端约定以「分」为整数单位传输,前端拿到分之后除以 100 格式化展示,完全规避 JS 浮点数精度问题,对账不会出错。

如果业务场景确实需要前端本地计算,我不会直接用 toFixed,因为它是银行家舍入,而且浮点数本身有精度丢失问题,比如 1.005.toFixed (2) 得到 1.00。简单计算可以放大 100 倍转整数运算;涉及利率、复利这类复杂运算,生产环境直接使用 decimal.js,并且传入字符串初始化,避免一开始就丢精度。

同时展示层可以用 Intl.NumberFormat 做千分位 + 固定两位小数美化。另外还要注意区分业务要求的舍入模式:四舍五入、向下取整还是向上取整,金融不同结算场景规则不一样,不能统一写死。

还有一个细节:大额金额后端尽量返回字符串,防止超出安全整数范围导致失真。

5、面试官延伸追问预判(顺带准备)

  1. 问:什么时候可以用 toFixed? 答:只在纯展示、不参与后续计算、非资金对账类的次要场景,金融核心金额不建议。
  2. 问:decimal.js 和 big.js 区别? 答:decimal.js 功能更强,支持更多运算、配置舍入模式;big.js 轻量,只满足基础小数运算,金融项目一般选 decimal.js。
  3. 问:如果后端直接返回元(数字)怎么办? 答:和后端沟通改成分;暂时改不了就前端转字符串丢给 decimal 处理,同时要在文档标注风险。