尧图精选

JS读取txt文件避坑指南:编码、大文件与流式处理

🕒 发布时间:2026/10/2 10:21:51 📁 来源:尧图网络
上周帮同事看一个小问题一个纯前端的统计小工具让用户选一个 txt页面把内容读出来做个词频统计。功能写完了本地双击 HTML 就跑文件一选页面上出来一堆问号。他以为是 FileReader 坏了换了三种写法都没救。最后发现那个 txt 是 Windows 记事本另存的编码是 ANSI。js 读取 txt 文件内容这件事看着就是一行代码的事实际上一路埋着雷浏览器和 Node 是两套完全不同的 IO 模型编码选错直接乱码文件一大内存就爆同一个 input 第二次选同一个文件还不触发事件。这篇就把这几条路从头走一遍该给代码给代码该说为什么说为什么看完你至少能少踩一半的坑。1. 浏览器侧的三条读取路径先确认自己站在哪一层浏览器读本地 txt不是一个 API 能解决的事得看你的文件从哪来是用户手动选的、拖进来的还是页面自己去请求的。这三种来源对应三套完全不同的机制很多人混着用问题就出在这。1.1 用户主动选文件input FileReader 的完整写法这是最经典也最稳的一条路兼容性最好任何还在维护的浏览器都支持。核心是FileReader它是个异步的读文件器读完之后把结果塞到result属性里。const input document.querySelector(#file-input); input.addEventListener(change, (e) { const file e.target.files[0]; if (!file) return; // 大文件读文本时先看看大小心里有个数 console.log(文件大小, (file.size / 1024 / 1024).toFixed(2), MB); const reader new FileReader(); reader.onload () { // reader.result 是字符串 const lines reader.result.split(/\r\n|\r|\n/); console.log(总行数, lines.length); }; reader.onerror () { console.error(读取失败, reader.error); }; reader.onprogress (evt) { if (evt.lengthComputable) { console.log(进度, ((evt.loaded / evt.total) * 100).toFixed(1) %); } }; reader.readAsText(file, UTF-8); });readAsText的第二个参数是编码默认就是UTF-8。这个参数是很多人第一个踩坑点——它不会帮你猜编码你给什么它就用什么解码给错了出来的就是乱码。onprogress这个钩子在小文件上没意义但用户选了一个 50MB 的日志文件时你能给个进度条体验完全不一样。注意FileReader是异步的reader.result在onload之外读是null。见过不少人在readAsText后面直接写console.log(reader.result)然后困惑为什么是空的。1.2 file.text() / arrayBuffer()少写十几行但有个前提如果你不需要进度条也不想管事件回调现代浏览器给 File 对象挂了几个快捷方法直接返回 Promise// 直接拿字符串 const text await file.text(); // 拿 ArrayBuffer自己控制解码 const buf await file.arrayBuffer(); const text2 new TextDecoder(utf-8).decode(buf);file.text()内部其实就是readAsText(file, utf-8)的封装行为一致但代码干净太多。file.arrayBuffer()更值得关注——拿到原始字节之后你可以自己做编码判断、自己分块解码、自己处理 BOM这是后面处理乱码问题的关键入口。这两个方法在 Chrome 76、Firefox 69、Safari 14 都能用。如果你的目标用户里有老设备file.text()之前记得做个存在性判断回退到FileReader。async function readFileText(file) { if (typeof file.text function) { return await file.text(); } return new Promise((resolve, reject) { const r new FileReader(); r.onload () resolve(r.result); r.onerror () reject(r.error); r.readAsText(file, utf-8); }); }1.3 不经过用户fetch 与 File System Access API 的边界有些场景下你想读的是随页面一起部署的 txt比如一份省市区编码对照表、一份词表、一份配置。这时候不该让用户去选文件而是让页面自己去拿const res await fetch(./assets/area-code.txt); if (!res.ok) throw new Error(加载失败: res.status); const text await res.text();这里有个必须说清楚的前提fetch走的是 HTTP 协议页面必须是从服务器哪怕是本地起的 dev server加载的。你双击打开 HTML地址栏是file:///...这时候fetch一个相对路径会直接被同源策略拦掉控制台报 CORS 或者Failed to fetch。这不是代码写错了是协议本身不允许。开发阶段用npx serve、python -m http.server起一个都行。另一条路是File System Access API也就是window.showOpenFilePicker()。它能拿到文件句柄支持后续写回甚至能做最近打开的文件这种功能。但它是 Chromium 系独占的Safari 和 Firefox 都没有而且必须在安全上下文HTTPS 或 localhost下才生效。所以真要用得这么写async function pickTxt() { if (!window.showOpenFilePicker) { // 回退到 input return null; } const [handle] await window.showOpenFilePicker({ types: [{ description: 文本文件, accept: { text/plain: [.txt] } }], }); const file await handle.getFile(); return await file.text(); }2. Node 侧读 txt四种写法的取舍与真实内存表现换到 Node 环境规则完全变了。浏览器里那一套FileReader、File全都不存在取而代之的是fs模块。而且 Node 里读 txt 有个浏览器根本不会遇到的麻烦文件真的可能非常大几百 MB 的日志文件是家常便饭。2.1 同步、回调、Promise 三种读法的真实差别最省事的是同步读const fs require(fs); const text fs.readFileSync(./data.txt, utf8);一行搞定后面直接当字符串用。它的代价是把整个文件内容一次搬进内存并且在读完之前主线程完全卡住。写构建脚本、写一次性数据清洗脚本的时候这么干完全没问题几十 KB 的配置就更无所谓了。到了服务端运行时代码里同步读就是灾难。一个 HTTP 请求进来读一次文件如果文件是 20MB那这 20MB 的读取期间整个进程处理不了任何其他请求。所以服务里要用异步// 回调风格老项目里还常见 fs.readFile(./data.txt, utf8, (err, data) { if (err) throw err; console.log(data.length); }); // Promise 风格现在首选 const fsp require(fs/promises); const text await fsp.readFile(./data.txt, utf8);fs/promises和fs.readFile的底层实现是一样的只是包装方式不同性能上没有区别。选 Promise 版本纯粹是为了能自然地配合async/await写 try/catch。三种方式的对照我整理了一下选型的时候对着看方式阻塞主线程内存占用适用场景readFileSync是全量脚本、CLI 工具、启动时加载配置fs.readFile回调否全量老代码维护fs/promises.readFile否全量服务端读取中小文件createReadStream否分块大文件、日志分析、逐行处理2.2 大文件必须走流createReadStream readline超过 50MB 左右全量读就该换成流式了。Node 原生提供了readline模块配合fs.createReadStream就是一套标准的逐行读取方案const fs require(fs); const readline require(readline); const rl readline.createInterface({ input: fs.createReadStream(./big-log.txt, { encoding: utf8 }), crlfDelay: Infinity, }); let lineNo 0; for await (const line of rl) { lineNo; if (lineNo % 100000 0) { console.log(已处理, lineNo, 行); } }几个细节值得单独拎出来说。crlfDelay: Infinity这个参数非常关键它让\r\n被当成一个换行符处理而不是\r和\n各算一次——不加的话Windows 生成的文件每一行后面会多出一个空行。encoding: utf8是给ReadStream的不是给readline的别写错位置。for await...of这种写法是 Node 12 之后才稳的好处是它天然做了背压处理处理速度慢的时候读取会自动降速不会把内存撑爆。对比一下用data事件手写的版本后者很容易在数据处理逻辑里堆任务队列反而把内存吃掉更多。还有一个容易被忽略的点/\r\n|\r|\n/这种切分方式在流式场景下不能用因为一个 chunk 的边界可能正好落在\r和\n中间。这就是为什么流式处理要用readline而不是自己split。2.3 什么时候要 Buffer什么时候直接写编码给readFile传utf8是最省事的但它有个硬伤Node 不猜编码。只要文件不是 UTF-8出来的就是乱码而且它不会报错静默给你一堆替换字符。所以当你面对来源不明的 txt正确做法是先不加编码拿 Bufferconst buf fs.readFileSync(./unknown.txt); // 不加编码得到 Buffer console.log(buf[0], buf[1], buf[2]); // 看头几个字节判断 BOM console.log(buf.length); // 字节数不是字符数Buffer 是字节数组buf.length是字节长度。这个区别在中文字符上非常明显一个 UTF-8 汉字占 3 个字节所以Buffer.byteLength(中文)是 6而中文.length是 2。做文件大小校验、做分片偏移量计算时用错这个数就会出问题。拿到 Buffer 之后就可以进入下一步——判断它到底是什么编码。3. 中文乱码的真正来源不是你代码写错了几乎每个处理过中文 txt 的人都遇到过乱码然后第一反应是是不是 FileReader 有问题。不是。乱码的本质是编码和解码用了不同的映射表文件里的字节按 GBK 写进去你按 UTF-8 读出来同一串字节被翻译成了完全不同的字符。3.1 为什么记事本存出来的 txt 一读就是乱码Windows 上旧版本的记事本默认保存编码是 ANSI在简体中文环境下就是 GBK更准确地说是 GB18030 的一个子集。这份文件用记事本打开完全正常因为它自己知道用什么解码但浏览器和 Node 默认都按 UTF-8 处理一读就是问号或者锟斤拷。所以遇到乱码第一件事不是改代码而是确认文件的真实编码。几个判断办法用 VS Code 打开右下角会显示当前识别的编码点一下可以通过编码重新打开来验证。看文件头三个字节EF BB BF是 UTF-8 BOM 的标记。如果文件里正确解码后能出现成片的中文说明编码猜对了出现的是文件这种拉丁字母组合说明文件是 UTF-8 但你按别的编解码了。确认下来是 GBK 之后浏览器侧和 Node 侧的处理方式完全不同这也是很多人在浏览器里找不到解法的原因。3.2 TextDecoder 支持哪些编码Safari 为什么不一样浏览器里做编码转换唯一的标准工具是TextDecoder。它的能力边界得心里有数编码标签Chrome / EdgeFirefoxSafariutf-8支持支持支持gbk/gb18030支持支持视版本需探测big5支持支持视版本需探测shift_jis支持支持视版本需探测别靠记忆直接做能力探测几行代码的事function canDecode(label) { try { new TextDecoder(label); return true; } catch (e) { return false; } } // 用的时候 const decoder canDecode(gbk) ? new TextDecoder(gbk) : new TextDecoder(utf-8); // 兜底 const text decoder.decode(await file.arrayBuffer());new TextDecoder(label)在标签不被识别时会直接抛RangeError所以这个 try/catch 是必须的不然在 Safari 上会直接白屏。如果目标用户必须完整支持 GBK 又跨浏览器那就得引入编码转换库代价是包体积变大值不值得得自己权衡。还有一个常被忽略的细节TextDecoder默认会自动剥离 UTF-8 BOMignoreBOM默认 false表示识别并去掉。所以如果你用new TextDecoder(utf-8).decode(buf)读带 BOM 的文件第一个字符是干净的。但如果你用buf.toString(utf8)或者 Node 的readFileSync(path, utf8)BOM 会原样保留为\uFEFF。同一个文件两种读法结果差一个不可见字符——这个后面会详细讲它怎么坑人。3.3 Node 里的探测与转换组合拳Node 没有 TextDecoder 的编码标签那套限制但 Node 自带的Buffer只认 utf8、latin1、utf16le 这几个GBK 不在其中。所以要用第三方库最常用的是iconv-liteconst fs require(fs); const iconv require(iconv-lite); const buf fs.readFileSync(./legacy.txt); const text iconv.decode(buf, gbk);iconv-lite是纯 JS 实现的不需要编译安装即用。支持的编码很全gbk、gb18030、big5、shift_jis 都有。如果连编码是什么都不知道可以再配一个jschardet做自动探测const jschardet require(jschardet); const buf fs.readFileSync(./unknown.txt); const result jschardet.detect(buf); // { encoding: GB2312, confidence: 0.99, language: zh } const label (result.encoding || utf-8).toLowerCase(); const text iconv.decode(buf, label);这里有个必须提醒的地方探测结果不能无条件相信。jschardet返回的confidence低于 0.8 的时候误判概率很高尤其是短文件——几十个字节的样本任何探测器都猜不准。稳妥的做法是给一个置信度阈值低于阈值时用业务层面的兜底策略比如先按 UTF-8 试解一次如果解出来包含大量替换字符\uFFFD再换 GBK 重试。function decodeSmart(buf) { const r jschardet.detect(buf); if (r.encoding r.confidence 0.8) { return iconv.decode(buf, r.encoding.toLowerCase()); } // 低置信度先用 utf-8 试 const tryUtf8 buf.toString(utf8); const badChars (tryUtf8.match(/\uFFFD/g) || []).length; if (badChars / tryUtf8.length 0.001) return tryUtf8; return iconv.decode(buf, gb18030); }这个替换字符比例的判断思路比单纯看置信度靠谱得多因为它检查的是解码结果本身的质量而不是一个概率值。4. 大文件不能一把梭分块解码与行边界处理浏览器端处理大文件有个很尴尬的现实file.text()和readAsText都是全量读一个 200MB 的 txt 读进来字符串本身就占 400MB 左右JS 字符串是 UTF-16再加上后续split生成的数组内存直接起飞页面卡死。4.1 一个汉字被切成两半会怎样流式读取的第一直觉是按固定大小切片每片解码成字符串写成代码大概是这样// 错误示范 const reader file.stream().getReader(); while (true) { const { done, value } await reader.read(); if (done) break; console.log(new TextDecoder(utf-8).decode(value)); // 每块独立解码 }这段代码在纯英文文件上跑得挺好一遇到中文就出问题。原因是 UTF-8 的一个汉字占 3 个字节如果 chunk 的边界正好落在这 3 个字节中间解码器就拿到一个不完整的序列输出\uFFFD替换字符。文件越大、中文越多这种断裂出现的次数就越多。修法只有一个告诉解码器我还没解完后面还有。const decoder new TextDecoder(utf-8); while (true) { const { done, value } await reader.read(); if (done) break; const chunk decoder.decode(value, { stream: true }); // 关键 handleChunk(chunk); } handleChunk(decoder.decode()); // 最后再调一次把残留字节吐出来{ stream: true }这个选项的作用是让解码器把结尾处不完整的字节序列留在内部缓冲区等下一个 chunk 来了再接上。循环结束后必须再调一次decoder.decode()不带参数否则缓冲区里那最后几个字节就永远丢了。这个坑我被坑过一次现象是文件末尾的文字偶尔少一两个字而且只在某些文件上出现排查了很久才定位到是漏了最后一次 flush。4.2 浏览器端的流式读取两种写法如果目标浏览器比较新直接用TextDecoderStream最干净它把上面那套逻辑封装好了const stream file.stream().pipeThrough(new TextDecoderStream(utf-8)); const reader stream.getReader(); while (true) { const { done, value } await reader.read(); if (done) break; console.log(value); // 已经是字符串且不会有半个字符 }TextDecoderStream在 Chrome 105、Safari 16.4、Firefox 105 可用。要兼容更老的浏览器就用手动版async function readByChunk(file, onChunk) { const decoder new TextDecoder(utf-8); const reader file.stream().getReader(); while (true) { const { done, value } await reader.read(); if (done) break; onChunk(decoder.decode(value, { stream: true })); } onChunk(decoder.decode()); }这两个方案的内存表现是一样的同一时刻内存里只有一个 chunk 加上一个残留缓冲区不会随文件大小线性增长。200MB 的文件处理起来内存峰值基本稳定在几 MB 到十几 MB。4.3 行边界缓冲区怎么维护分块拿到字符串之后麻烦还没完。因为 chunk 的边界同样可能落在一行的中间直接split(\n)会把最后一行切一半。标准解法是维护一个tail变量let tail ; let lineCount 0; const handleLine (line) { lineCount; // 这里做真正的一行处理 }; await readByChunk(file, (chunk) { const parts (tail chunk).split(\n); tail parts.pop(); // 最后一段可能不完整留着 for (const p of parts) handleLine(p); }); if (tail) handleLine(tail); // 收尾别漏了最后一行这里有两个细节。第一parts.pop()取出来的是数组最后一项它一定是不完整的除非 chunk 恰好以换行结尾所以必须留到下一轮。第二循环结束后tail里剩下的就是文件的最后一行内容如果文件不是以换行符结尾这行就完全没被处理——漏掉文件最后一行是个非常隐蔽的 bug因为大部分测试文件的最后一行都是空的。需要处理\r\n的话把split(\n)换成先在 handleLine 里line.replace(/\r$/, )比在切分时就上正则更省性能。5. 从一坨文本到能用的数据清洗、切分与容错内容读出来只是第一步真正花时间的往往是把这坨字符串变成能用的结构。这一步做得好不好直接决定你的代码是能跑通 demo 还是能上生产。5.1 三种换行符与统一切分txt 文件的换行符至少有三种来源Windows 是\r\nUnix / macOS 是\n老版本 Mac 用的是\r。用户给你一份 txt你没法预判它是哪种所以小文件场景下用这个正则统一切const lines text.split(/\r\n|\r|\n/);正则有性能成本在几百万行的文件上这个正则比单纯的split(\n)慢大概两到三倍。所以我的做法是小文件几 MB 以内用正则图省事大文件先判断一次整体换行风格再决定用哪种切法。const hasCRLF text.indexOf(\r\n) ! -1; const lines hasCRLF ? text.split(\r\n) : text.split(\n);判断一次的开销是 O(n) 但常数极小而且只需要做一次摊到整体处理上可以忽略不计。5.2 清洗阶段该丢掉什么、该保留什么真实的 txt 数据远没有教科书里那么干净。空行、行尾空格、注释行、重复行一个都不能不处理但也不能无脑处理。const cleaned []; const seen new Set(); for (const raw of lines) { const line raw.trim(); if (!line) continue; // 空行 if (line.startsWith(#)) continue; // 注释行 if (seen.has(line)) continue; // 去重 seen.add(line); cleaned.push(line); }去重这里用的是Set这是有讲究的。很多人习惯写if (arr.includes(line)) continue在几千行的数据上没问题但数据量到几十万行的时候includes是 O(n)整个循环变成 O(n²)几十万行的文件能跑上几分钟。Set的has是 O(1)换成 Set 之后同样的数据一秒内跑完。提示trim()能处理大部分空白字符包括全角空格\u3000和 BOM\uFEFF。但它处理不了零宽字符\u200B、\u200C、\u200D——这些在从网页复制或者爬取的数据里很常见会让两个看起来一模一样的字符串比较不相等。需要的话再做一层line.replace(/[\u200B-\u200D]/g, )。另外清洗要克制。我见过有人把trim()加在每一行上结果把本来有意义的缩进结构全毁了。如果你的 txt 是配置文件或者缩进敏感的文本就别动行首空格。5.3 字段切分与类型转换的静默失败如果 txt 是结构化的比如 Excel 导出的制表符分隔文件那接下来就是按分隔符拆列。Excel 导出 txt 默认用 Tab 分隔这一点跟 CSV 的逗号不一样不能想当然。const rows cleaned.map((line) { const cells line.split(\t); return { id: cells[0], name: cells[1], score: Number(cells[2]), }; });这里藏着 JS 里最容易被忽视的一类问题类型转换的静默失败。看看这几个表达式的结果Number() // 0 ← 空字符串变成了 0 Number( ) // 0 Number(12abc) // NaN parseInt(12abc) // 12 ← 悄悄截断了 Number(1,234) // NaN ← 千分位分隔符 Number() // NaN ← 全角数字Number()返回 0 这件事特别危险如果你的数据里有空字段本来应该标记为缺失结果全变成了 0后面做统计的时候多了一堆莫名其妙的零值。所以字段转换一定要显式校验function toNumber(v, fallback null) { const s String(v).trim(); if (!s) return fallback; const n Number(s); return Number.isFinite(n) ? n : fallback; }Number.isFinite比isNaN更严格因为isNaN(12)会先做类型转换返回 false而Number.isFinite(12)对字符串直接返回 false。用错了会漏掉真正的数据类型问题。字段切分还有一个常被低估的复杂度如果字段内容里本身包含分隔符比如某个商品名里带了逗号那split(,)就会把一行拆成错误的列数。处理标准 CSV 要考虑引号包裹的规则这时候自己split就不合适了得用完整的 CSV 解析器。判断依据很简单——处理前后每一行的列数是否一致不一致就说明有字段包含分隔符该换方案了。6. 几个真正会踩到的坑按踩坑频率排序前面讲的是怎么把事做对这一段讲的是那些代码看着没问题、跑起来就是不对的情况。这几条都是我在实际项目里真金白银踩出来的。6.1 同一个文件选第二次change 事件不触发这是新手最常遇到的玄学问题用户选了一个文件发现选错了改完文件再选同一个页面没反应。原因是input typefile的change事件只在value发生变化时触发。选同一个文件value没变事件自然不触发。这个行为在规范里是明确规定的不是浏览器 bug。解法是在处理完之后把value清空input.addEventListener(change, async (e) { const file e.target.files[0]; if (!file) return; await handleFile(file); input.value ; // 允许重复选择同一个文件 });注意input.value 要放在读取完成之后。如果放在读取之前e.target.files会被清空file引用还在但某些老浏览器上会出错。放到await之后最稳妥。顺带说一句input.value 并不会真的清掉用户磁盘上的文件它只是重置了表单控件的状态不会有任何副作用放心用。6.2 BOM 让字符串比较莫名其妙地失败这个坑我在一次配置读取里栽过。txt 第一行是MODEprod代码里判断if (firstLine MODEprod)怎么都不成立。打印出来看字符串明明一模一样——因为第一个字符是个不可见的\uFEFF。BOMByte Order Mark是 UTF-8 文件开头的EF BB BF三个字节解码后会变成\uFEFF。有的编辑器特别是 Windows 上的记事本和某些版本的 Excel 导出会默认带上它。处理方式分两层。如果你用TextDecoder解码默认就会帮你去掉如果你用Buffer.toString(utf8)或者 Node 的readFileSync(path, utf8)BOM 会保留。最稳的做法是不管怎么样都清一次// 方式一正则替换 text text.replace(/^\uFEFF/, ); // 方式二判断字符码 if (text.charCodeAt(0) 0xFEFF) { text text.slice(1); }要主动检测文件有没有 BOM看字节最直接const buf fs.readFileSync(./config.txt); const hasBOM buf[0] 0xEF buf[1] 0xBB buf[2] 0xBF;同样的 BOM 问题也会出现在 GBK 文件上不过 GBK 的 BOM 不是标准实践中很少遇到重点盯 UTF-8 就行。6.3 file:// 下的 fetch 和拖拽的默认行为这两个问题严格说不算读取失败但都会让你觉得代码明明是对的。第一个是fetch读本地文件。前面提过双击打开 HTML 是file://协议fetch(./data.txt)会被拦。有意思的是有些浏览器对file://之间的请求是放宽的有些不是导致同样的代码在不同浏览器上表现不一样很容易让人误判。别去研究各家的策略差异直接起个本地服务一劳永逸。第二个是拖拽上传。很多人只监听了drop事件结果文件拖到页面上时浏览器直接把它打开了页面跳转走了。原因是拖拽的默认行为就是导航到被拖入的文件。必须显式阻止const zone document.querySelector(#drop-zone); zone.addEventListener(dragover, (e) { e.preventDefault(); // 必须 zone.classList.add(active); }); zone.addEventListener(dragleave, () { zone.classList.remove(active); }); zone.addEventListener(drop, async (e) { e.preventDefault(); // 必须否则浏览器会打开文件 const file e.dataTransfer.files[0]; if (!file) return; console.log(await file.text()); });dragover里那个preventDefault()特别容易漏因为不写它页面看着也没报错只是drop事件永远不触发。记住一点想让元素成为合法的放置目标就必须在dragover里阻止默认行为。另外拖拽进来的文件file.name在部分浏览器上可能为空或带上路径片段如果你要用文件名做判断记得做空值兜底。6.4 编码探测的置信度不能全信也别反复探测最后说一个工程上的经验。我在一个数据导入功能里用了自动编码探测测试阶段一直很顺上线后陆续收到几个导入失败的反馈查下来都是短文件——只有一两行内容的那种。前面提过样本太短的时候任何探测器都测不准而那时候我还没加替换字符比例的兜底判断。从那之后我的做法变成两段式能拿到编码信息就不探测拿不到才探测探测结果只作为候选。如果数据来源是自己能控制的比如用户上传前页面上有编码选择、或者上游系统有约定那就把编码当成一个明确参数传进来别让程序去猜。自动探测本质是个概率游戏把它当成最后一道防线而不是主要手段。还有一个细节不要在循环里反复探测。jschardet.detect本身是要遍历字节的如果每个 chunk 都调一次性能会掉得很明显。正确的做法是取文件的前 8KB 到 16KB 做一次探测拿到结果后用同一个编码解码整个文件。这个样本大小足够覆盖中文编码的特征又不至于花太多时间。const probeSize 16 * 1024; const probe buf.subarray(0, probeSize); const { encoding, confidence } jschardet.detect(probe); // 一次探测全文件复用这个结果subarray是浅拷贝不复制数据开销可以忽略。用slice也差不多但subarray语义上更明确——我只要一个视图不需要新的一份内存。写到这里回头看看js 读取 txt 文件内容这件事代码层面的复杂度其实很低大部分 API 都是一行能解决的。真正花时间的是编码判断、内存控制、边界处理这些不写在文档里的东西。我自己的习惯是任何涉及文件读取的工具都先拿三个测试文件跑一遍一个 UTF-8 无 BOM、一个 UTF-8 带 BOM、一个 GBK 带中文三份都能正确出来再往下写业务逻辑。这个习惯帮我省掉的调试时间比写这三份测试文件的时间多得多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →