尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

深入理解JavaScript迭代协议,破解is not iterable报错

深入理解JavaScript迭代协议,破解is not iterable报错 1. 先搞懂这个报错到底在说什么——intermediate value 的生成机制如果你在 Chrome 控制台或者 Node 环境里跑代码时撞见这个提示第一反应大概率是懵的Uncaught TypeError: {(intermediate value)(intermediate value)} is not iterable。它不像Cannot read property of undefined那么直白光看这段英文既没有明确告诉你是哪个变量出的问题也没说在哪一行样子还很奇怪——怎么会有两个intermediate value叠在一起要拿下这个面试题第一步不是背答案而是把这个报错本身“翻译”成人话。1.1 错误信息的组成拆解我把这个报错拆成三块来看Uncaught TypeError说明这是一个未捕获的类型错误。所谓“类型错误”指的是程序在运行时发现某个值的类型不符合当前操作的预期。比如你用for...of去遍历一个数字或者对null调用.length都会触发这类错误。{(intermediate value)(intermediate value)}这是 V8 引擎对“出错的表达式”的一种格式化输出。intermediate value表示“中间值”也就是表达式计算过程中的临时结果。两个intermediate value叠在一起意味着引擎在尝试把一个中间结果当作另一个“可迭代目标”来使用。is not iterable这是最关键的部分。iterable可迭代是 ES2015 引入的一个协议概念。简单说如果一个对象实现了Symbol.iterator方法那它就是可迭代的可以被for...of、展开运算符...、解构赋值等语法消费。不是可迭代对象你偏要拿它去展开或遍历引擎就会抛这个错。{(intermediate value)(intermediate value)}这个花括号加括号的写法其实对应的是代码里的“函数调用”动作。也就是说你的代码里大概率写了类似这样的结构const result [...someFunction()];或者const [a, b] something() ;这里的someFunction()或something()就是一个“函数调用”它的返回值就是引擎口中的intermediate value。因为函数调用本身会先生成一个值这个值再被丢给后面的展开/解构语法去消费所以报错信息里就用(intermediate value)来指代它。如果调用的是一个方法比如obj.getList()而obj本身又是一个表达式的结果那就会出现两个(intermediate value)叠在一起的情况。比如const first [...getConfig().merge()];这里的getConfig()是一个中间值.merge()的返回值是另一个中间值引擎对这个返回值执行展开时发现它不可迭代于是报出{(intermediate value)(intermediate value)} is not iterable。1.2 为什么是“intermediate value”而不是真实的函数名很多人会觉得奇怪我明明调用的是getList为什么报错信息里不写getList is not iterable而是写一个抽象的(intermediate value)这恰恰是 V8 在这里的“偷懒”行为。展开运算符、解构赋值、for...of这类语法在执行时要消费的是一个表达式的最终计算结果而不是某个具名变量本身。引擎在生成错误信息时只能拿到“当前正在被消费的值”如果这个值来自函数调用、三元表达式、链式调用等复合表达式它就没有办法像foo或者arr那样直接打印出变量名。所以 V8 统一用(intermediate value)来占位。这也解释了为什么这个报错比Cannot read properties of undefined更难排查后者的错误信息里会带上属性名和对象名比如reading startTime你能直接定位到是哪一行、哪个对象而这里的报错只告诉你是“某个中间值不可迭代”具体是哪个中间值得靠你自己看源码和调用栈。1.3 什么操作会触发“is not iterable”检查需要明确一点is not iterable这个错误并非某一种特定方法的专利而是所有依赖迭代协议的语法都会触发。我整理了一张表方便你对照排查操作语法示例触发条件展开运算符[...value]value不是可迭代对象数组解构const [a] valuevalue不是可迭代对象对象展开注意区分{...value}value本身可迭代则展开属性否则直接拷贝其自身属性通常不会报错for...of循环for (const item of value)value不是可迭代对象Promise.all/Promise.racePromise.all(value)value不是可迭代对象生成器委托yield* valuevalue不是可迭代对象new Set(value)/new Map(value)new Set(value)value不是可迭代对象接受可迭代对象作为构造参数在众多触发场景里最高频、也最容易让新手困惑的其实是第一类展开运算符作用在一个函数调用的返回值上。你原本以为这个函数返回的是数组结果它返回了undefined、对象或者null引擎在展开时一检查迭代协议直接就炸了。其实要真正理解这个报错你只需要抓住一个核心展开、解构、遍历这类语法之所以能工作靠的不是值本身而是值身上挂着的Symbol.iterator方法。只要这个方法是缺失的、或者不是函数类型引擎就会严厉地把这个值判定为“不可迭代”。后面你要做的所有排查和修复本质都是在围绕“如何让一个值变成可迭代的”或者“如何避免把一个不可迭代的值丢给迭代语法”做文章。2. 最高频的“翻车现场”与背后的语言机制写代码的时候我几乎可以打包票地说十次里九次踩到is not iterable都是下面三个场景之一。把这三个场景吃透了你不仅在实战中能快速定位问题面试时也能让追问变得更加从容。2.1 展开运算符直接作用在函数调用结果上这是最常见的一种情况。很多人写代码喜欢“链式”地调用函数觉得这样紧凑、优雅function getTags() { // 某个逻辑可能返回 undefined } const tags [...getTags()];这段代码在getTags()返回数组时没有毛病但只要函数内部由于某个if分支没有执行return或者返回了一个对象、null、undefined展开运算符就会立刻抛错。我见过不少项目里藏着这样一种模式后端接口返回的数据结构变了原本data.list是一个数组后来变成了一个对象前端代码里[...data.list]这种写法就当场崩溃。报错信息里出现的正是{(intermediate value)(intermediate value)} is not iterable——因为data.list本身是从data这个对象上读取的属性读取动作产生了中间值展开动作消费了中间值两边一叠加就是这个报错。2.2 解构赋值时右侧不是可迭代对象解构赋值看起来比展开运算符“温和”但其实它对可迭代协议的要求同样严格const { data } await fetchSomething(); const [firstItem] data; // data 如果是对象或 undefined直接报错这里有个特别容易忽略的坑数组解构和对象解构走的是两套完全不同的机制。对象解构const { key } obj不要求对象可迭代它走的是属性读取机制而数组解构const [a] arr走的是迭代协议要求右侧的值必须可迭代。所以const [first] abc是合法的因为字符串可迭代但const [first] { length: 3 }就不行因为普通对象默认没有实现Symbol.iterator。经验之谈一旦报错信息里出现is not iterable你先看一眼是不是数组解构语句的右侧出问题了。很多时候你以为是解构的一个数组实际上是undefined——因为你的await调用返回的是一个{ data: undefined }之类的结构。2.3for...of循环踩到非迭代对象第三种高频场景是把for...of用在了非迭代对象上const user { name: 张三, age: 30 }; for (const key of user) { console.log(key); }这个例子会稳定报错。普通对象默认没有迭代器你要遍历对象应该用for...in遍历键名或者Object.keys()/Object.entries()。有个反直觉的点我在这里展开说一下很多同学以为“只要是集合就可以for...of”结果把for...of用在了一个普通的对象字面量上。其实for...of的适用对象是数组、字符串、Set、Map、生成器对象这些“本身就实现了迭代协议”的类型。普通对象要支持for...of你必须手动给它挂上Symbol.iterator方法或者直接Object.entries()后遍历键值对数组。这三个场景的根本原因其实是同一个你把一个“没有实现迭代协议”的值交给了“依赖迭代协议”的语法。只要想通这一点后面所有修复方案都是有方向感的不是在瞎试。3. 拿下这个报错的完整排查链路部分资料上来就甩修法但面试官想知道的是你能不能独立地把这个问题定位清楚。因为在真实项目里报错信息里的(intermediate value)只是给你指了个方向真正的“真凶”藏在你自己的代码逻辑里。我把自己处理这类报错的完整排查链路拆成三段每一步都写清楚为什么这么做。3.1 复现构建最小触发条件拿到一个is not iterable报错第一步永远不是修而是复现。我通常在本地写一个独立的小文件把业务代码里报错的那个表达式单独抽出来// 假设线上报错的原始代码长这样 const mergedList [...buildList(baseConfig).items]; // 我先把 buildList 简化成能复现问题的样子 function buildList(config) { if (config.enabled) { return { items: [1, 2, 3] }; // 返回对象 } return undefined; // 或者 undefined } console.log([...buildList({ enabled: false }).items]);当我把问题“隔离”到最小程度之后报错信息就变得非常清晰buildList(...)返回了undefined然后undefined.items这一读就已经炸了。实际上这里报的应该是Cannot read properties of undefined如果报的是is not iterable那就说明引擎已经把.items读取出来了只是items本身不可迭代。复现这件事的意义在于它能帮你区分“到底是哪一层出的问题”——是拿不到值还是拿到了值但类型不对。拿不到值是一类修法拿到的值不可迭代是另一类修法。混为一谈的话你会在错误的层面浪费大量时间。3.2 定位从调用栈和源码位置反推网上很多讲法会让读者“直接展开堆栈看哪一行”但在 V8 的报错信息里is not iterable这种错误往往只给出一个很短的堆栈有时候甚至只指向那个.js文件的一行代码。这时候我会做两件事第一看最终消费迭代语法的那个表达式。展开运算符写在哪儿、for...of写在哪儿那个“不可迭代的值”一定是被送到这一个语法入口的。第二从后往前“倒推”。报错信息里有两个(intermediate value)说明中间经历了至少两次表达式计算。比如const arr [...loadConfig().parse()];loadConfig()产生第一个中间值.parse()产生第二个中间值展开运算符消费第二个中间值。如果.parse()返回的是null报错信息里的两个括号就对应这一串链式调用。反推的时候我会在可疑的链式调用每一段后面加console.log看实际类型const config loadConfig(); console.log(config is, typeof config, config); const parsed config.parse(); console.log(parse returns, Array.isArray(parsed), parsed);这样一打印所有中间值的“真实身份”就现形了。你不需要去猜哪个中间值不可迭代直接看Array.isArray(parsed)的结果就行。3.3 验证修复代码之后如何确认不再踩坑很多人修完一个 bug 就跑了根本没有验证这一步。实际上由于这个报错往往和异步数据、运行时状态绑定在一起单纯改完代码不验证下一次上线照样可能出事。我建议的验证方式是在原来抛错的那个表达式周围写一个“防御性断言”确认修复前后的数据类型一致性。举例function toList(value, fallback []) { if (value null) return fallback; if (typeof value[Symbol.iterator] ! function) { return Object.entries(value); // 如果是普通对象就转成键值对数组 } return value; } // 修复后 const arr [...toList(loadConfig().items)];然后你在控制台跑一批用例合法的数组、undefined、普通对象、类数组对象、字符串。只要toList的返回值在所有这些输入下都能被安全展开你的修复就站得住脚。验证的关键不是“不报错”而是明确每一种输入下你期望得到什么输出。这是我个人在排错里最有价值的一步它能倒逼你把函数对外的契约定清楚。4. 修复方案汇总与取舍——不同场景不同解法很多面试题解到这个份上说的是“为什么报错”但真正的高频追问是“你会怎么改”。修复方案没有银弹不同场景的修法完全不同。我按“从治标到治本”的顺序给出四种方案并说明每种方案的适用边界。4.1 把函数调用结果先存成变量再判断这是最基础也最稳妥的一种修法。核心思想是不要让展开运算符直接消费函数调用的中间值而是先把中间值变成一个具名变量再用条件判断确保它可迭代。// 修复前 const arr [...loadItems()]; // 修复后 const items loadItems(); const arr items typeof items[Symbol.iterator] function ? [...items] : [];这套写法的好处是不只处理了undefined和null还处理了“是可迭代对象但类型不对”的情况。比如loadItems()返回一个普通对象items不为空但也不可迭代这个分支依然能安全兜底。坏处是稍微啰嗦了一些。所以如果你的函数本身返回结果可控我更推荐在函数内部处理边界情况而不是每处调用都这么防御。写两遍体面写十遍就会变成噪音代码。4.2 把普通对象显式转换成可迭代的数组有些时候你会明确知道自己拿到的是一个“长得像数组但不是数组”的对象比如后端返回的{ 0: a, 1: b, length: 2 }或者一个普通对象{ name: 张三, age: 30 }。这时候你不是“兜底”而是“规范化”。把普通对象转成数组常用两种方式// 类数组对象 → 数组 const realArray Array.from(arrayLikeObj); // 普通对象 → 键值对数组 const entries Object.entries(plainObj); // [[name,张三], [age,30]] const keys Object.keys(plainObj); const values Object.values(plainObj);这里有个很容易踩坑的细节Array.from对“可迭代对象”和“类数组对象”是双模支持。如果一个对象既没有Symbol.iterator又没有length属性Array.from也救不了它。而Object.entries适用于所有“可枚举自有属性”的普通对象它返回的是标准的二维数组天然可迭代拿来展开和解构都没有问题。4.3 给自定义类手动挂上迭代器如果你的业务里有一个自定义类你想让它被for...of、展开运算符直接消费而不需要每次手动转成数组那就需要手动实现迭代协议class ItemCollection { constructor(items) { this.items items; } *[Symbol.iterator]() { yield* this.items; } } const collection new ItemCollection([1, 2, 3]); const expanded [...collection]; // [1, 2, 3]注意这里的yield*它是生成器函数里的“委托迭代”它的工作方式就是“消费一个可迭代对象并逐个产出”。如果this.items不是可迭代对象yield*同样会抛is not iterable。所以在真实项目里我通常在Symbol.iterator方法里加一层类型检查*[Symbol.iterator]() { if (!Array.isArray(this.items)) { throw new TypeError(ItemCollection.items 必须是一个数组); } yield* this.items; }这种方式的好处是把迭代能力封装进了类型本身符合面向对象的设计思路坏处是一旦修改了类的内部结构迭代器也要跟着维护。4.4 防御性检查与默认兜底第四种方案是“防御性编程”它往往不是单独使用的而是和前面几种方案搭配。我习惯在项目里定义一个工具函数用来“保证得到一个数组”这样所有调用方都不再需要时刻担心类型问题function ensureArray(value) { if (Array.isArray(value)) return value; if (value null) return []; if (typeof value[Symbol.iterator] function) { return Array.from(value); } if (typeof value object) { return Object.entries(value); } return [value]; }这个工具函数的适用场景是你的数据来源非常杂——可能来自接口、可能来自本地缓存、可能是历史遗留代码传过来的奇怪结构。ensureArray把“保证展开不炸”变成一个统一出口业务侧代码只需要写const arr [...ensureArray(getData())];必须承认这种“什么都能接住”的函数有一定的危险性它会让类型错误被悄悄掩盖。如果接口本应该返回数组却返回了普通对象ensureArray会把它转成键值对数组业务逻辑里可能拿到的就不是你期望的数据了。所以这个函数更适合放在“不可控边界”比如接口返回层而不是业务核心层。核心层我还是建议要明确类型该抛错就抛错别把错误吞掉。5. 面试官想知道什么这个错误的考点与回答框架标题里特意标注了【面试题】这道题在真实面试中出现的频率相当高。但很多人在面试时只会回答“哦这不是 iterable 就报错了”然后就被追问哑火了。其实面试官问这个错误的背后藏着好几层考察每一层都想探测不同的知识边界。5.1 从错误信息里能看出的三层境界面试官看你对这个报错的理解程度通常在心里给你分三层第一层你认得出这是“展开语法炸了”。你能说出[...foo]这种写法在foo不是可迭代对象时会报错——这是最基础的掌握大概只有 60 分。第二层你知道什么是迭代协议。你能讲出Symbol.iterator、next()方法、{ done, value }的结构并且知道数组、字符串、Set、Map 为什么可迭代而普通对象为什么默认不可迭代。能说到这一层面试官基本认定你读过 ES2015 的迭代协议章节分数会拉到 85 分左右。第三层你知道 V8 的报错输出机制。(intermediate value)(intermediate value)意味着引擎是在“链式表达式的临时结果”上做迭代检查你能反向推导出代码形态比如[...a().b()]这类结构。能答到这里说明你是真的在浏览器里调试过这类问题的人分数可以冲到 95 分以上。5.2 一套稳妥的面试回答路径我建议你在面试时按下面这条路径回答每一步都“踩在考点上”先翻译报错直接说“这个错误是 V8 在展开或遍历一个不可迭代的值时抛出的(intermediate value)表示表达式计算过程中的临时值这里的代码多半是[...someFn()]或者[a] someFn()的形态。”解释迭代协议再补一句“展开运算符、数组解构、for...of依赖的是迭代协议也就是对象身上有没有Symbol.iterator方法没有这个方法或者该方法不是函数引擎就认为不可迭代。”给排查思路这时候展开你的实战经验——“我会先找到消费迭代语法的表达式倒查中间值的生成过程在链式调用的每一段打日志确认类型再根据实际类型决定是兜底、转数组还是修数据源。”给出一个修复示例直接在面试官面前写一个ensureArray或者一版带类型检查的展开判断让对方看到你是有完整方案的。最后谈边界提一句“普通对象默认不可迭代需要手动挂Symbol.iterator或转成Object.entries()的结果”这就把知识面的广度和深度同时展示出来了。整个回答不超过 3 分钟但每一句都在证明你不是背题库、而是真的写过这类代码。5.3 相似但不同的错误——not iterable与cannot read properties of undefined的区分面试官还特别喜欢让你把is not iterable和其他典型运行时错误做区分这是一个“送命题”也是“加分题”。我给你一个最常用也最容易踩坑的对比方向报错时机不同。Cannot read properties of undefined (reading startTime)这类报错发生在“属性读取”阶段。你先要拿obj.startTime但obj本身就是undefined所以连属性都读不到。它的问题核心是“上游值为空”。is not iterable这类报错发生在“迭代消费”阶段。值不一定为空甚至可能是一个正常对象但它就是缺少迭代器。它的问题核心是“值的形状不满足协议要求”。举一个真实例子接口返回data你想取data.list结果data是undefined那报错就是Cannot read properties of undefined (reading list)。如果data存在、data.list也存在但data.list是一个普通对象而不是数组你想[...data.list]那报错就是is not iterable。两者在面试中的“考点倾向”也不同前者考察的是你对“空值安全链”的敏感度后者考察的是你对“语言协议”的理解。你把这两个错误的边界讲清楚面试官基本就能判断出你平时写代码时是“靠着错误信息在猜”还是“真知道引擎在做什么”。我在实际带新人的时候最怕的不是他们报错而是他们看到is not iterable就只会“加一个|| []”。修得了表面修不了本质。能区分清楚这两个错误的人写出来的代码边界感明显更好——哪里该防御、哪里该抛错、哪里该转换心里的把握是完全不同的。6. 那些容易续报的相似错误读属性失败与 Promise 分支按标题相关的热搜词来看很多人搜完is not iterable之后还会紧接着搜Uncaught TypeError: Cannot read properties of undefined (reading startTime)和Uncaught (in promise) TypeError: Cannot read properties of undefined (reading writeText)。这并不奇怪——它们往往出现在同一段异步数据流代码里而且都是“运行时数据形状和预期不符”惹的祸。我把它们单拎出来讲因为它们虽然信息长得不一样排查思路却是一致的。6.1 读取失败型undefined 对象上的属性访问Cannot read properties of undefined (reading startTime)这类报错核心原因只有一个你在一个undefined或null上读了属性。V8 给出的reading startTime直接提示了你读的是哪个属性其实比is not iterable要友好得多。结合项目里最常见的场景这套报错通常长在这种代码里const { data } await getActivity(); const startTime data.startTime; // data 是 undefined 就炸表面上的修法是加可选链const startTime data?.startTime;但面试官如果追一句“你这样修了之后startTime的值是什么”你就要想清楚data为undefined时data?.startTime的值是undefined它不会替代成默认值。要真正把“缺省值”也处理好得加上空值合并const startTime data?.startTime ?? ;所以这一类错误的完整解法分两步第一步保证“不炸”用可选链第二步保证“有意义”用空值合并。只做第一步你的代码确实不抛错了但后面如果用startTime去格式化日期依然会出问题。我在实际项目里见过太多次“不报错了但结果错了”的情况所以强烈建议两步一起做。6.2 Promise 分支里的读取失败为什么有时会多出“(in promise)”有些同学还会看到Uncaught (in promise) TypeError: Cannot read properties of undefined。注意开头的(in promise)它和标题里的展开报错拼在一起很容易让人误以为是“Promise 的锅”。其实(in promise)只是告诉你这个错误发生在一个 Promise 的执行链里并且这个 Promise 的错误没有被 catch 捕获。它并不一定代表 Promise 本身用错了。比如你在.then()回调里写fetchData().then((res) { const name res.data.user.name; // res.data 是 undefined → 报错 });这段代码如果报错控制台就会加上(in promise)前缀。原因是.then()回调里的异常会变成 Promise 的 rejection如果后面没有接.catch()这个 rejection 就会被 V8 当作未捕获的 Promise 异常抛出来。碰到带(in promise)的报错你的排查顺序应该是先看是哪个 Promise 链抛的异常看异常是在哪一层回调里触发的修完数据读取问题之后顺手检查整条 Promise 链有没有 .catch。后面这一点常常被忽略。修好了数据读取但如果 Promise 链本身缺一个兜底 catch下一次接口异常时照样会在控制台刷红色报错。比如fetchData() .then((res) { // 业务处理 }) .catch((err) { console.error(接口异常兜底, err); });有了这个.catch即使.then内部再出问题你也能拿到一个可控的日志而不是一段不可调试的红色堆栈。6.3 把三类错误串起来看所有“数据形状不可控”问题的统一解法如果你同时碰上is not iterable、reading startTime、以及writeText——这里补一句navigator.clipboard.writeText在某些环境下比如非安全上下文或 disabled clipboard会返回 rejected Promise报错时也会出现Cannot read properties of undefined的变体——你要意识到它们其实是同一大类问题的三个侧面数据从不可控边界进入代码后没有先做形状校验就直接消费。所以我在项目里逐渐养成了一个习惯凡是数据从接口进来第一件事就是抽一个 map 函数做清洗把字段拍平、把空值填上默认值再交给业务逻辑。function mapActivity(raw) { const data raw?.data ?? {}; return { startTime: data.startTime ?? , tags: Array.isArray(data.tags) ? data.tags : [], user: { name: data.user?.name ?? 未知用户, }, }; }这样做的好处是业务侧永远拿到一个“确定形状”的对象展开、解构、读属性都不会炸。你不需要在每个使用点写可选链、写三元判断、写防御分支只需要在一处把数据洗干净。维护代码的人看到mapActivity就能知道“这个模块的数据到底长什么样”这个信息的价值比省几行代码要大得多。7. 从一道报错题延伸出去我在实际调试中的几点体会把这个报错追完之后有一说一它带给我的收获其实不是“我记住了Symbol.iterator”而是整个调试思路变得更系统化了。在收尾前我把自己反复踩过、也复盘过的一些体会写在这里不一定每个都对应的面试考点但实战中非常管用。7.1 报错信息里写的类型永远不如运行时打印的类型可靠is not iterable的报错信息只告诉你“不可迭代”但不会告诉你“它到底是什么”。很多同学看到报错就会默认“那是个对象吧”然后去对象上一通找。我的习惯是看到这个报错第一件事永远是在报错表达式前面打console.log把值的typeof、Array.isArray结果和实际内容都打出来。这样做的原因很简单你脑子里预设的“类型”和运行时的“实际类型”经常是两回事。特别在一次 TypeError 之后你会下意识地去猜而调试最忌讳的就是“猜”。打印一次结论就实锤了。这比在编辑器里盯三分钟代码都高效。7.2 “修复报错”和“修复数据”是两件事有一次我修了一个is not iterable的 bug把展开运算符改成了Object.entries(...)页面不报错了但表格里列数全部错位。原因是我把对象转成了键值对数组但下游逻辑预期的是一个纯值数组。这个经历让我记住了修复代码之前先想清楚下游要消费什么形状的数据。每次修这类问题我会先问自己三句话这个值在正常情况下“应该”是什么形状它在异常情况下“实际”可能是什么形状修复之后下游代码拿到的形状是否和正常情况下一致如果三句话的答案都是明确的再动手改代码也不迟。7.3 面试中引用“真实出错现场”比你背概念要打动人得多如果你在面试里遇到这道题我强烈建议你不要只回答“这是展开运算符的错”而是把话题引到你真实调过的场景里。哪怕你当时踩的坑只是一个很小的[...res.data.list]报错你可以顺势讲一句“我之前在调一个列表页接口时遇到过这个报错当时后端返回结构从数组变成了对象导致展开运算符直接崩了。后来我把接口入口统一改成先过一层数据清洗函数用Array.isArray和Symbol.iterator双检查兜底后面类似的报错基本就绝迹了。”这种回答为什么更打动人因为它证明了你不只知道“是什么”还知道“怎么解”并且对解法背后的取舍有真实认知。面试官要的不是一个背诵器而是一个能处理实际问题的人。我自己在处理这类问题时最后的感受是JavaScript 的迭代协议其实并没有多复杂难的是在真实的、混沌的业务数据流里保持对“值形状”的敏感。报错信息只是冰山一角水面之下是类型系统、协议设计和数据边界的工程问题。能把一道报错题讲出这一层面试结果大概率不会差。
返回列表