严格模式下为什么不能用 arguments.callee?递归该怎么写?
严格模式下访问 arguments.callee 会直接抛 TypeError。本文讲清它原本解决什么问题、为什么被禁用(妨碍引擎内联优化、暴露调用栈),以及用具名函数表达式实现递归的写法和它比外层变量递归更稳的原因。
JavaScript · 函数与作用域面试题第 1 页,显示第 1–39 题,共找到 39 道完整解析,可继续按分类、标签与关键词缩小范围。
按稳定语义路径排序
严格模式下访问 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。附对比代码和它与箭头函数属性的区别。
obj.fn?.() 在 fn 是 null 或 undefined 时跳过调用、返回 undefined,存在时正常调用,this 仍然是 obj,不会丢。讲清 obj?.fn() 和 obj.fn?.() 保护的对象不同、短路时参数不会求值、属性存在但不是函数时照样报错,以及回调和事件处理里的常见用法。
把变量声明在函数里,只返回能读写它的方法,外面就只能通过这些方法操作它,这就是闭包实现的私有变量。讲清工厂函数和模块模式两种写法、每次调用为什么互不影响,并和下划线约定、class 的 #私有字段做对比。
剩余参数 ...args 是真正的数组,能直接 map、reduce;arguments 是类数组,要先转换。剩余参数只收集具名参数后面的部分,箭头函数里也能用,arguments 在箭头函数里根本不存在。附对比代码、非严格模式下 arguments 和形参联动的坑,以及剩余参数的语法限制。
纯函数有两个条件:同样的输入永远得到同样的输出,并且不改函数外面的任何东西。改参数、改全局变量、发请求、打日志、读当前时间都算副作用。附购物车的对比代码、为什么 React 和 Redux 要求纯函数,以及怎样把副作用隔离到边上。
函数默认参数有自己独立的作用域,参数名会遮蔽外层同名变量。写 x = x 时读的不是外面的 x 而是还没初始化的形参 x,直接触发 TDZ 报错。附参数求值顺序、前后参数依赖和常见踩坑场景。
普通函数的 this 由调用方式决定,四种规则按优先级排:new 绑定最高,bind/call/apply 显式绑定其次,obj.fn() 隐式绑定再次,最后是默认绑定。附代码验证、strict 模式差异和常见追问。
class 方法放在原型上,不会自动绑定实例,单独传出去当回调时 this 是 undefined。讲清它和普通对象方法的差别、构造器 bind 与箭头字段两种修法的取舍,以及解绑事件、子类覆写时的坑。
addEventListener 用普通函数注册时,this 指向挂监听器的那个元素,也就是 event.currentTarget;箭头函数没有自己的 this,拿的是外层的。附事件委托里 target 和 currentTarget 的区别、handleEvent 对象写法和解绑的坑。
var 只认函数边界,if 和 for 的花括号关不住它;let 和 const 只在所在的块里有效,声明前访问会报错。讲清函数作用域、块级作用域、变量提升和暂时性死区,以及 const 为什么挡不住对象被修改。
yield* 把另一个可迭代对象的值一个个转交出去,相当于在生成器里嵌入另一段迭代。讲清它和 yield 的区别、被委托生成器的 return 值去了哪里,以及用 yield* 递归遍历树的写法和 next、throw 的转发规则。