obj.fn?.() 怎么用?用可选链调用方法,this 会丢吗?
obj.fn?.() 在 fn 是 null 或 undefined 时跳过调用、返回 undefined,存在时正常调用,this 仍然是 obj,不会丢。讲清 obj?.fn() 和 obj.fn?.() 保护的对象不同、短路时参数不会求值、属性存在但不是函数时照样报错,以及回调和事件处理里的常见用法。
JavaScript面试题第 4 页,显示第 151–200 题,共找到 285 道完整解析,可继续按分类、标签与关键词缩小范围。
按稳定语义路径排序
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 的转发规则。
顶层 await 为什么会卡住导入它的父模块?因为 ESM 要保证父模块执行时依赖已经执行完,异步依赖没结束,父模块就不能跑。讲清阻塞的范围只限依赖链上的祖先模块,以及改成导出 Promise、动态 import() 两种规避方式。
ESM 循环依赖为什么不会像 CommonJS 那样拿到 undefined?因为 ESM 先把导入导出连成实时绑定再执行,读到的永远是对方变量本身。讲清两者加载流程的差别,以及 ESM 在循环里读到未初始化变量时会抛 ReferenceError 的坑。
ESM 循环依赖中,另一方还没执行完,导出的函数声明能不能直接调用?能,因为函数声明在模块实例化时就创建好了。但换成箭头函数、函数表达式或 class 就会抛 ReferenceError,函数体里用到对方未初始化的变量也会报错。
ESM 循环依赖中,在顶层读取对方还没初始化的 let 导出,会抛 ReferenceError: Cannot access before initialization。讲清暂时性死区为什么延伸到导入方,和 var、函数声明的差别,以及把读取挪进函数、拆出第三个模块的修法。
浏览器加载 ES 模块为什么要走 CORS?普通 script 标签跨域随便引,type=module 却会被拦。讲清模块请求默认用 cors 模式、同源模块为什么不受影响、本地双击 HTML 打不开模块的原因,以及 CDN 跨域模块要配哪些响应头。
ESM 同一个模块导出两个同名的名字,会在解析阶段直接报 SyntaxError,模块一行代码都不会执行。讲清报错的几种写法、同一个变量用不同名字导出为什么没问题、默认导出和具名导出的关系,以及 export * 名字冲突时静默丢弃的坑。
动态 import() 返回一个 Promise,resolve 后得到模块命名空间对象。它必须异步因为模块可能需要网络加载和执行。附 await 写法、错误处理、缓存机制和与静态 import 的本质区别。
语言规范允许 import() 接受任意表达式作为路径,但打包工具做静态分析时对动态路径有限制。附 Webpack 部分动态路径的写法、魔法注释的用法和常见踩坑场景。
静态 import 在编译期确定依赖,支持 tree-shaking;动态 import() 在运行时按需加载。附路由懒加载、条件加载、重型库延迟加载等典型场景和选用原则。
浏览器通过 script type="module" 加载 ES 模块,默认 defer 执行,自动严格模式,跨域需要 CORS。附模块脚本和普通脚本的行为差异、nomodule 降级、常见踩坑场景。
具名导出用大括号按名字导入,可以有多个;默认导出每个模块只能一个,导入时可以自由命名。附两种导出的语法差异、tree-shaking 影响、团队协作中的选用原则。
ESM 的 import 是对源模块导出变量的实时引用,源端改了值导入方立刻看到。CommonJS 的 require 拿到的是 module.exports 的值拷贝,基本类型修改后导入方看不到变化。附代码对比和常见误区。
用 const 导出的变量不能重新赋值,值永远是初始值(对象属性例外)。用 let 导出的可以在源模块里修改,导入方通过实时绑定读到新值。附代码对比和常见踩坑点。
export default 后面跟表达式时立刻求值,导出的是那一刻的值,没有实时绑定。跟函数或类声明时,声明会被提升,且保留模块内的本地绑定。附两种写法的实际差异和常见踩坑场景。
ESM 里导入的绑定是只读的,重新赋值会报 TypeError。这是因为 import 建立的是指向源模块变量的只读引用,只有源模块能修改。对象属性修改不受限制。附代码示例和常见误区。
export 列表和声明导出的导出名不能重复,否则报 SyntaxError。两种写法都是创建具名导出,区别在于声明导出同时声明变量,列表导出引用已有变量。附用 as 重命名和 index.js 汇总导出的写法。
具名导入只拉需要的导出,对 tree-shaking 最友好。命名空间导入理论上引入所有导出,但现代打包工具也能分析 ns.x 的用法做优化。附两种写法的实时绑定差异和实际打包效果对比。
ESM 的 import 声明会被提升到模块顶部,所有导入在任何代码执行之前就已经完成。这意味着 import 写在文件中间也能在前面使用导入的值。附模块执行顺序详解和与 require 的行为对比。
原生 ESM 导入 JSON 时,导入属性 with { type: "json" } 会让宿主校验资源类型,避免把意外返回的脚本当作模块执行。JSON 模块只有默认导出;旧版 assert 语法、打包器变换后的写法和浏览器原生加载也需要分开看。
浏览器不能像 Node.js 那样从 node_modules 查找裸模块名;import map 把 import "vue" 之类的说明符映射到 URL。imports 负责全局映射,scopes 可按导入方地址覆盖规则,前缀映射还要求键和值的路径形式一致;映射必须先于使用它的模块脚本生效。
import.meta.resolve 按模块解析规则把说明符变成 URL,过程不会加载或执行目标模块。它适合寻找 Worker、WASM 等资源地址;相对路径可与 new URL(path, import.meta.url) 比较,但包名和 import map 的解析不能简单用 URL 拼接代替。
import.meta.url 指向当前模块自身的地址,浏览器里通常是 HTTP URL,Node.js 文件模块里是 file URL。用 new URL 相对它定位图片或 Worker 文件,比手拼路径可靠;内联模块和打包后的资源地址则要结合实际宿主与构建产物判断。
同一个解析后的模块地址被多处导入,宿主通常只实例化并执行一次,随后导入复用同一模块记录。查询参数、不同解析结果或不同运行环境会形成不同身份;单例状态能否共享,取决于这些导入最终是否指向同一模块。
import * as ns 得到的模块命名空间会反映导出方的实时值,却不能由导入方改写、添加或删除导出。它的属性描述符看起来可写,Object.isFrozen 也可能返回 false;这与实际赋值失败并不矛盾,导出对象自身的内部属性仍可变。
import { count as total } 只更改导入方使用的名字,不会把值复制成快照。导出方更新 count 后,total 的后续读取仍能看到新值,但导入方不能给 total 赋值;默认导出重命名与解构赋值则分别遵循不同规则。
导出对象后修改其属性或数组元素,导入方能通过共享引用看到变化;导出方重新给绑定赋值时,ESM 的导入方也会读到新绑定。把对象解构成普通局部变量会形成另一个结果,CommonJS 导出对象的替换行为也不能直接套用 ESM 规则。
import "lodash" 这样的裸模块名在 Node.js 中按包解析规则定位,浏览器原生模块却只认识可解析的 URL。打包器能在构建时改写导入,import map 可在浏览器中映射名称,CDN 还可能提供可直接导入的 URL;三种做法的运行条件不同。
Node.js 中 .mjs 固定按 ESM 处理,.cjs 固定按 CommonJS 处理;.js 要结合最近的 package.json 中 type 字段和具体解析条件。没有明确标记的文件还可能触发语法检测;发布库时应明确文件格式与导出入口,避免两种模块系统相互引用时出错。
包的 exports 字段决定外部可导入的入口,未声明的深层路径即使文件存在也可能被拒绝。子路径、通配符以及 import、require 条件按匹配规则选择目标;条件对象的先后顺序会影响结果,types 和 default 的位置也应与工具链预期一致。
module 与 nomodule 可让支持 ESM 的浏览器加载现代脚本,让旧浏览器回退到转译脚本。两类脚本的执行时机和兼容性并不完全对称;某些旧版浏览器会重复下载或执行,支持模块也不代表支持现代代码用到的每一项语法。
import "./module" 不绑定任何导出,但仍会按模块依赖顺序执行目标模块的顶层代码,常用于加载 polyfill、样式或插件注册逻辑。同一模块通常只执行一次;打包时如果把 package.json 的 sideEffects 声明错了,这类导入可能被优化掉。
桶文件可以用 export ... from 转交其他模块的导出,而不先在本地导入。具名再导出、export * 和默认导出的行为各不相同:星号不会转交 default,两个星号来源撞名也不会自动任选其一,调用方必须明确解决歧义。
模块脚本默认延迟执行并启用严格模式,顶层变量不会挂到 window,跨域加载还要满足 CORS;普通脚本没有这些默认行为。把旧页面改为 type="module" 时,顶层 this、同地址脚本重复引用及本地 file URL 的加载限制都可能改变运行结果。
普通 import 完成模块加载与求值后取得导出;import source 取得的是可交给宿主后续实例化的源码阶段对象,例如 WebAssembly 模块。它与延迟求值的 import defer 关注不同阶段,具体能在哪些环境使用还取决于宿主实现和当前支持情况。
ES 模块天然采用严格模式,不需要也不能通过省略 use strict 来关闭。普通函数直接调用时 this 保留 undefined,给未声明变量赋值会报错,arguments 不再与形参联动,with 也不可用;旧脚本迁移为模块时这些差异会直接暴露。
严格模式会把部分静默失败变成异常,例如给只读属性赋值、删除不可配置属性或给未声明变量赋值。重名参数等写法甚至在解析阶段就失败;迁移旧脚本时需要区分语法错误与运行时错误,不能只看最终值是否改变。
普通函数直接调用时,非严格模式可能把 this 替换为全局对象,严格模式则保留 undefined。作为对象方法调用、显式绑定或构造调用时,结果另有规则;模块天然处于严格模式,迁移旧代码后依赖全局 this 的逻辑容易暴露问题。
ES 模块顶层的 this 是 undefined,普通经典脚本顶层则通常是全局对象。旧库若用顶层 this 挂载全局变量,改为模块后这段逻辑会失效;跨浏览器和 Node.js 使用时应明确选择 globalThis 或显式导出。
静态 import 只能放在模块顶层,写进 if、函数或循环会在解析阶段失败,因为依赖关系需要提前确定。按条件加载资源时应使用动态 import();它在运行时发起加载并返回 Promise,错误处理和执行时机也与静态导入不同。
模块顶层 await 会暂停本模块后续求值,并让依赖它的模块等待结果;依赖图中无关的分支仍可继续执行。配置加载等初始化场景适合它,但等待链过长会延迟应用启动,拒绝结果也会沿依赖关系传播。