JSON.stringify 遇到 NaN 和 Infinity 会怎样?为什么不报错?
NaN、Infinity 和 -Infinity 都不是合法的 JSON 数字,JSON.stringify 会把它们统一写成 null,而且不报错。讲清为什么这样设计、往返之后为什么恢复不了、提交前怎么用 Number.isFinite 拦住,以及确实需要保留时怎么用 replacer 和 reviver 编码。
JavaScript面试题第 3 页,显示第 101–150 题,共找到 285 道完整解析,可继续按分类、标签与关键词缩小范围。
按稳定语义路径排序
NaN、Infinity 和 -Infinity 都不是合法的 JSON 数字,JSON.stringify 会把它们统一写成 null,而且不报错。讲清为什么这样设计、往返之后为什么恢复不了、提交前怎么用 Number.isFinite 拦住,以及确实需要保留时怎么用 replacer 和 reviver 编码。
Date 经过 JSON 往返会变成字符串,JSON.parse 不会自动转回来。讲清 reviver 怎么按字段把日期字符串恢复成 Date、它从里到外的调用顺序、返回 undefined 会删属性,以及为什么不能见到像日期的字符串就转,还有 2 月 30 日这种会被引擎悄悄挪走的日期。
JSON.stringify 的第二个参数 replacer 可以是函数或属性名数组。函数能逐个转换或过滤值,数组是属性白名单,而且对嵌套对象同样生效。讲清两种形式的区别、根节点空 key、调用顺序、数组元素返回 undefined 会变成 null,以及 toJSON 和 replacer 的先后。
undefined、函数和 Symbol 在 JSON 里都没有对应的类型:在对象里整个属性被删掉,在数组里变成 null,单独序列化直接返回 undefined。讲清三种位置为什么处理不同、PATCH 接口里「不传」和「传 null」的区别,以及 Symbol 键、稀疏数组这些细节。
s 标志让点号能匹配换行符,m 标志让 ^ 和 $ 能匹配每一行的开头和结尾,一个管点号、一个管锚点,互不影响。附日志多行提取、逐行匹配的例子,以及不开 s 时常见的 [\s\S] 写法。
带 g 或 y 标志的正则会用 lastIndex 记住上次匹配到哪里,复用同一个实例做校验就会出现 true、false 交替。讲清 lastIndex 的更新规则、三种修法、exec 循环里什么时候该重置,以及空匹配导致死循环的处理。
用千分位格式化、提取金额两个例子讲清 JavaScript 正则四种环视断言:前瞻看右边、后顾看左边,都只检查不吃字符。附密码规则多条件校验、断言写反位置的常见错误,以及老环境不支持后顾时的降级写法。
对比 exec 循环和 matchAll 拿全部匹配的两种写法:matchAll 返回带捕获组、命名组和 index 的迭代器,不用手动维护 lastIndex,也不改动原正则。附不带 g 报 TypeError、迭代器只能用一次等常见坑。
用解析日期的例子讲清 JavaScript 正则命名捕获组:怎么写、怎么通过 groups 读取、在 replace 字符串和回调里怎么引用、正则内部怎么反向引用。对比数字下标,说明它为什么在正则改动时更不容易取错,以及可选分支没命中时的 undefined 问题。
用匹配引号内容和 HTML 标签的例子,讲清 JavaScript 正则量词默认贪婪、加问号变懒惰的区别,以及背后的回溯过程。说明懒惰不等于最短匹配、为什么排除型字符类往往是更好的写法,并附灾难性回溯的常见追问。
g 从 lastIndex 开始往后找,y 必须恰好在 lastIndex 处匹配、不许跳过字符。用对比代码和一个真实的四则运算词法分析器讲清 sticky 标志的用途,以及它和 ^ 锚点、gy 组合在 match 和 replace 里的表现。
JavaScript 正则的 Unicode 属性转义怎么用?讲清 \p{Script=Han}、\p{Letter}、\p{Number} 分别匹配什么,为什么必须加 u 标志,和手写 [一-龥] 区间比强在哪,以及它处理不了 emoji 组合字符的边界。
流式读取 fetch 响应时,一个汉字的 UTF-8 字节可能被拆到两个 chunk 里,直接解码就会出现乱码。讲清 TextDecoder 的 stream: true 如何缓存半个字符、为什么必须复用同一个实例、最后为什么还要调一次 decode(),以及 SSE 场景下还要处理的半行问题。
TextDecoder 遇到非法 UTF-8 字节默认不报错,而是替换成 U+FFFD 继续解码;开启 fatal 后才会抛 TypeError。讲清两种模式各适合什么场景,ignoreBOM 为什么名字容易理解反,以及编码选错时 fatal 为什么救不了你。
TextEncoder 把字符串转成 Uint8Array,输出永远是 UTF-8,构造函数不接受编码参数。讲清一个字符会变成几个字节、为什么不能用 length 算字节数、encode 和 encodeInto 的区别,以及和能解多种编码的 TextDecoder 为什么不对称。
Uint8Array 这类类型化数组本质是 ArrayBuffer 上的一个视图:长度固定、元素类型固定、内存连续,写入 300 会变成 44,也没有 push。讲清 ArrayBuffer 和视图的关系、多个视图共享内存时的字节序问题,以及什么场景该用类型化数组。
Uint8Array 的 subarray 和 slice 参数一样,结果却完全不同:subarray 返回共享内存的视图,改它会改到原数组;slice 复制出一份独立数据。讲清两者的代价、复用缓冲区时 subarray 带来的数据被覆盖问题,以及和普通数组 slice 的区别。
不加 u 时,正则把一个 emoji 看成两个 UTF-16 代码单元,点号、量词、字符类都会把它拆成两半。本文用可运行的例子讲清 u 标志改变了什么、它为什么让语法更严格,以及它管不到的组合 emoji。
é 在 Unicode 里有两种写法:一个预组字符,或者 e 加一个组合重音。它们显示完全一样,但 === 比较是 false。本文讲清 normalize 的四种形式 NFC、NFD、NFKC、NFKD 的区别,以及搜索、去重、做索引时该怎么用。
经典的 split('').reverse().join('') 会把 emoji 拆成两个代理项再颠倒顺序,结果变成乱码。本文对比按代码单元、按码点、按字素簇三种反转方式,讲清 Array.from 为什么还不够,以及 Intl.Segmenter 怎么处理家庭 emoji、国旗和带重音的字母。
JavaScript 字符串用 UTF-16 编码,编号超过 U+FFFF 的字符要拆成一对代理项存储,所以 emoji 的 length 是 2。本文讲清代理对怎么算出来、for 循环和 for...of 遍历的区别、charCodeAt 与 codePointAt 的用法,以及统计用户可见字数的正确做法。
严格模式下访问 arguments.callee 会直接抛 TypeError。本文讲清它原本解决什么问题、为什么被禁用(妨碍引擎内联优化、暴露调用栈),以及用具名函数表达式实现递归的写法和它比外层变量递归更稳的原因。
arguments 有下标和 length,却没有 map、filter 这些数组方法。本文对比 Array.prototype.slice.call、Array.from、展开运算符三种转换写法的原理和差异,讲清类数组和可迭代对象的区别,以及为什么新代码直接用剩余参数。
arguments 记录的是这次调用实际传进来的所有值,形参是函数声明里起的局部变量名。讲清两者数量为什么会不一样、函数 length 和 arguments.length 的区别,以及只有非严格模式加简单参数时才会联动的历史规则。
箭头函数没有自己的 arguments,写在里面的 arguments 会按词法作用域找到外层普通函数的那一份。讲清为什么这样设计、防抖函数里怎么利用这一点、顶层读 arguments 会报什么错,以及拿箭头函数自己的参数为什么要用 rest。
箭头函数没有自己的 this,里面的 this 就是定义它的外层函数的 this,和怎么调用无关,所以 call、apply、bind 都改不了。讲清对象字面量里写箭头方法为什么拿不到对象、类字段箭头函数的取舍,以及什么时候不该用箭头函数。
异步生成器用 async function* 声明,里面既能 await 又能 yield,next() 返回的是 Promise,要用 for await...of 来消费。结合分页拉取数据的例子讲清它和普通生成器的区别、break 时 finally 如何清理,以及它为什么不会自动并发。
call 和 apply 是马上调用,只是传参方式不同;bind 不调用,而是返回一个 this 和前几个参数都固定好的新函数。讲清 bind 之后再 bind 或 call 为什么改不了 this、被 new 调用时 this 怎么变、length 怎么算,以及解绑事件时最常见的坑。
JavaScript 所有参数都是按值传递,对象参数传的是「引用」这个值的副本。所以在函数里改对象属性会影响外面,给形参重新赋值却不会。用一个 swap 函数说明为什么这不是按引用传递,并讲清浅拷贝、不可变更新的常见坑。
setTimeout(obj.method) 传出去的只是一个函数,obj 没有跟着过去,定时器之后单独调用它,this 就不再是 obj。讲清 this 由调用方式决定的原理、浏览器和 Node.js 里此时 this 分别是什么,以及 bind、箭头函数包一层、类字段箭头函数三种修法的取舍。
经典面试题:for 循环里用 var 声明 i,setTimeout 打印出来全是 3。原因是 var 整个函数只有一个 i,回调执行时循环早就跑完了。附 let、IIFE、setTimeout 第三个参数三种修法和常见追问。
闭包本身不会泄漏,泄漏的是被长期持有的闭包。讲清垃圾回收的可达性原理、事件监听和定时器这两种最常见的泄漏场景、同一作用域闭包共享环境的坑,以及用 DevTools 定位泄漏的步骤。
柯里化就是把 add(1, 2, 3) 变成 add(1)(2)(3)。讲清闭包怎样把已经传入的参数攒起来、参数够了再执行,手写一个支持分批传参的 curry,并说明 fn.length 的坑、和偏函数的区别以及实际用途。
默认参数不是定义函数时算一次,而是每次调用、对应实参是 undefined 时才计算。讲清从左到右的初始化顺序、后面参数引用前面参数的写法、传 null 为什么不触发默认值,以及和 Python 默认参数的区别。
函数声明整体提升,写在后面也能先调用;函数表达式只提升变量,var 写法提前调用报 TypeError,let 和 const 写法报 ReferenceError。附具名函数表达式的名字作用域、块级函数声明的坑和常见追问。
不是所有函数都有 prototype。普通函数和 class 有,箭头函数、async 函数、对象简写方法、bind 返回的函数没有;生成器有 prototype 却不能 new。讲清 prototype 和能否 new 的关系以及常见误判。
调用生成器函数不会执行函数体,只返回一个迭代器;每次调用 next 才运行到下一个 yield 并暂停。讲清 next 返回的 value 和 done、next 传参如何变成 yield 的结果、第一次 next 的参数为什么没用,以及实际用途。
生成器对象既是迭代器又是可迭代对象,Symbol.iterator 返回它自己,所以只能遍历一次;数组每次遍历都会新建迭代器,可以反复遍历。讲清可迭代协议和迭代器协议的区别,以及怎样写一个能多次遍历的对象。
生成器函数不能 new,会直接抛 TypeError;因为只能普通调用,函数体里的 new.target 永远是 undefined。讲清它明明有 prototype 却不能构造的原因、生成器对象的原型关系,以及想要可 new 又可遍历时该怎么写。
用 generator.return(value) 从外部结束生成器,finally 照样会执行;用 generator.throw(error) 在暂停的 yield 处抛出错误,内部能 catch 就继续,不能就向外传。讲清两者的区别、for...of 中途 break 时发生了什么和常见坑。
yield 交出一个值并暂停,之后还能继续;return 交出最后一个值并结束生成器。return 之后可以继续调用 next,但只会得到 value 为 undefined、done 为 true 的结果。附 for...of 和展开运算符为什么拿不到 return 的值。
var、let、const 其实都会提升,区别在初始化时机:var 提升后立刻是 undefined,let 和 const 在声明语句执行前处于暂时性死区,一读就抛 ReferenceError。附代码对比、typeof 为什么也会报错、函数声明和 class 的提升规则。
IIFE 是定义完马上调用的函数表达式。讲清为什么要在外面套一层括号、它在没有块作用域和模块的年代怎样隔离变量、怎样用闭包做私有状态,以及现在还会在哪些场景用到,比如 async IIFE 和一次性初始化。
两种 IIFE 都能立刻执行、都能隔离变量,真正的区别在函数本身:箭头函数没有自己的 this 和 arguments,用的是外层的;普通函数 IIFE 每次调用都有自己的一份。附对象方法里的对比代码、括号写法差异和 async IIFE 的用法。
函数的 length 是声明时的形参个数,但只数到第一个带默认值的参数为止,剩余参数不算,解构参数算一个。附代码逐个验证、bind 之后 length 怎么变,以及柯里化依赖 length 时会踩的坑。
词法作用域的意思是:函数能读到哪些变量,看它写在哪里,不看在哪里被调用。用一段代码证明调用位置不影响变量查找,讲清作用域链怎样一层层往外找、同名变量怎样遮蔽,以及它和闭包、原型链的关系。
匿名函数和箭头函数本身没写名字,但赋值给变量、写成对象属性、作为默认参数时,引擎会自动把左边的名字推断成函数的 name。附各种写法的输出对照、赋值给对象属性为什么推断不出来、bind 和 getter 的特殊前缀,以及为什么不能拿 name 做业务判断。
new 调用函数分四步:创建空对象、把它的原型连到构造函数的 prototype、以它为 this 执行函数、没有返回对象就返回它。附手写 new 的代码、构造函数返回对象会怎样、忘了写 new 会怎样,以及箭头函数为什么不能 new。
new.target 用来判断函数是不是被 new 调用的:new 调用时它指向被 new 的那个构造函数,普通调用时是 undefined。继承时父类构造函数里读到的是子类。箭头函数没有自己的 new.target,读的是外层函数的。附抽象基类、强制 new 调用的写法。
对象方法简写 fn() {} 不只是少写一个 function。它和 fn: function () {} 的 this、arguments 规则完全一样,区别有两个:简写方法里能用 super 调原型上的方法,并且简写方法不能被 new。附对比代码和它与箭头函数属性的区别。