1. 这不是教程,是十年 JavaScript 实战者递给你的一把解剖刀
“万字长文带你走进 JavaScript 的世界”——看到这个标题,我第一反应不是点开,而是停顿三秒。不是因为它不够吸引人,恰恰相反,它太常见了。常见到泛滥,常见到让真正想沉下心学的人反而警惕:又是一篇堆砌概念、罗列 API、最后用“ES6 很酷”收尾的速食文章?我带过几十个从零起步的前端新人,也陪某高校实验室重构过三个大型教育平台的前端架构,更在某跨平台系统里亲手把一个 30 万行 JS 的老项目从 callback 地狱里拖出来重写为可维护的模块化结构。这些经历让我明白:JavaScript 不是语法书,它是一套活的、会呼吸的工程语言;它不靠“记住多少”,而靠“理解在哪种场景下该用什么机制去应对”。所以这篇“万字长文”,不讲 for 循环怎么写,不列 Map 和 Set 的所有方法,而是直接切开它的肌理——从最底层的执行上下文栈如何压入弹出,到事件循环里微任务和宏任务为何总在争抢执行权;从原型链上那个被无数人误解的__proto__到现代 class 语法糖背后真实的对象创建逻辑;从 V8 引擎如何把一段const a = 1 + 2编译成机器码,到为什么setTimeout(fn, 0)永远不会真的“立即执行”。你不需要有五年经验才能看懂,但你需要愿意暂时放下“我要马上做出按钮效果”的急迫感,跟我一起蹲下来,看清这门语言的骨骼与神经。适合谁?刚敲完第一个console.log('Hello World')的新手,能在这里建立对 JS 运行本质的直觉;写了三年 Vue/React 却总在调试异步问题时卡壳的中级开发者,能在这里找到性能瓶颈的根源;甚至负责技术选型的团队负责人,也能从中判断某个第三方库是否在滥用eval或过度依赖Proxy带来的不可控开销。它不承诺让你“三天精通”,但它保证:读完之后,你再打开 Chrome DevTools 的 Sources 面板,看到断点停在某一行时,心里清楚知道此刻调用栈里压着几个函数、闭包里捕获了哪些变量、Event Loop 正在处理哪个队列——这才是真正“走进”JavaScript 世界的第一步。
2. 核心设计哲学与运行机制拆解:为什么 JS 是这样子?
2.1 单线程 + 非阻塞 I/O:不是妥协,而是精密设计
很多人说“JavaScript 是单线程的,所以得靠回调、Promise 来解决异步”,这句话本身没错,但错在把“单线程”当成一个缺陷来解释。实际上,这是 V8 引擎(Chrome、Node.js 的核心)和浏览器宿主环境共同选择的主动设计,而非能力不足的无奈之举。我们来算一笔账:假设一个现代 CPU 有 8 个物理核心,每个核心每秒能执行约 30 亿条指令。如果 JS 引擎也搞多线程,那意味着每次 DOM 操作(比如修改一个 div 的 innerHTML)都得加锁——因为多个线程可能同时操作同一个页面元素。而 Web 页面的本质是单用户、强交互、高响应性的场景。用户点击按钮后,期望界面在 100 毫秒内给出反馈,否则就会觉得“卡了”。如果为了“理论上的并发能力”引入线程锁、上下文切换、死锁检测,那一次简单的按钮点击,光是线程调度开销就可能吃掉 50 毫秒,这完全违背了 Web 的设计初衷。
所以 JS 选择了另一条路:单线程主线程 + 宿主环境提供的异步能力。主线程只干一件事:执行 JS 代码。而所有耗时操作——网络请求(fetch)、文件读写(Node.js 的 fs)、定时器(setTimeout)、用户输入(click 事件)——全部交给浏览器或 Node.js 的底层 C++ 模块去处理。它们在后台线程池里跑,跑完后把结果“推”回 JS 主线程的一个队列里。这个队列,就是 Event Loop 的核心舞台。你可以把它想象成一家只有 1 个厨师(JS 主线程)的餐厅,但后厨有 10 个帮工(浏览器/Node 的线程池)。顾客点单(发起 fetch),厨师不自己去炒菜,而是把单子甩给帮工。帮工炒好后,把菜放在“出餐口”(Callback Queue)。厨师只要手头没活(当前执行栈为空),就立刻扫一眼出餐口,有菜就端走上桌。这个“扫一眼”的动作,就是 Event Loop 的每一次循环。
提示:这就是为什么
setTimeout(fn, 0)不会立刻执行。它只是把fn放进“宏任务队列”,等当前所有同步代码、所有微任务(Promise.then)执行完后,Event Loop 才会去取它。实测下来,即使设为 0,实际延迟也在 1~4 毫秒之间,这是浏览器强制的最小间隔。
2.2 原型链与动态类型:灵活性的双刃剑
JavaScript 的对象模型,是它最常被误解,也最体现其设计哲学的部分。“万物皆对象”不是一句口号,而是指所有值(除了 null 和 undefined)都有原型(prototype),都可以通过__proto__(非标准但广泛支持)或Object.getPrototypeOf()访问其原型对象。我们来看一个经典例子:
const arr = [1, 2, 3]; console.log(arr.map); // function map() { [native code] } console.log(arr.__proto__ === Array.prototype); // true console.log(Array.prototype.__proto__ === Object.prototype); // true这里没有“类继承”,只有对象委托。arr对象自身没有map方法,但它把查找请求“委托”给了自己的原型Array.prototype,而Array.prototype又委托给Object.prototype。这种委托链,就是原型链。它带来的最大好处是极致的动态性:你可以在运行时给Array.prototype添加新方法,所有数组立刻就能用。
Array.prototype.last = function() { return this[this.length - 1]; }; console.log([1,2,3].last()); // 3但硬币的另一面是可预测性的丧失。如果两个库都往Array.prototype上挂last方法,后加载的那个会覆盖前一个,导致难以排查的 bug。这也是为什么现代 JS 开发强烈推荐使用class语法(它本质是基于原型的语法糖)和模块化封装,把“动态污染”控制在最小范围内。
至于动态类型,它的价值在于快速迭代与表达力。写let user = { name: 'Alice' }; user = 123;在 JS 里完全合法,这在 Python 里会报错,在 Java 里根本编译不过。这种灵活性让 JS 成为前端开发的天然选择——UI 状态千变万化,数据结构经常在 API 响应里动态生成,强类型语言需要大量类型声明和转换,而 JS 直接“跟着数据走”。但代价是,你必须承担运行时类型错误的风险。这也是 TypeScript 兴起的根本原因:它不是要取代 JS,而是给 JS 的动态性加上一层“可选的、静态的护栏”,在编码阶段就发现user.name.toUpperCase()中user可能为null的问题,而不是等用户点到那个按钮才在控制台看到Cannot read property 'toUpperCase' of null。
2.3 作用域与闭包:内存管理的隐形指挥家
很多开发者把闭包理解为“函数记住了它定义时的词法环境”,这没错,但太抽象。我更喜欢把它看作 JS内存管理的隐形指挥家。我们来看一个真实场景:某在线教育平台的视频播放器,需要记录每个视频的播放进度,并在用户刷新页面后恢复。最 naive 的做法是全局变量:
// ❌ 危险!全局污染,且无法区分多个视频 let currentProgress = 0; function saveProgress(time) { currentProgress = time; } function loadProgress() { return currentProgress; }而闭包提供了一种优雅的封装:
// ✅ 每个 videoId 对应独立的“记忆空间” function createProgressManager(videoId) { let progress = 0; // 这个变量被“关”在闭包里,外部无法直接访问 return { save: (time) => { progress = time; }, load: () => progress, reset: () => { progress = 0; } }; } const video1Manager = createProgressManager('vid-001'); const video2Manager = createProgressManager('vid-002'); video1Manager.save(120); // 只影响 vid-001 的 progress console.log(video2Manager.load()); // 0,完全隔离关键点在于:progress变量的生命周期不由函数调用栈决定,而由是否有引用指向它决定。当createProgressManager执行完毕,其执行上下文本该被销毁,但返回的对象里的save和load函数,内部都持有了对progress的引用。因此,JS 引擎的垃圾回收器(GC)会识别到progress仍被“活引用”,就不会回收它。这个被“活引用”保护起来的变量空间,就是闭包。它让 JS 在没有类成员变量(private field)的时代,就实现了数据的私有化和状态的持久化。这也是为什么 React 的useStateHook 能让每个组件实例拥有自己独立的状态——它的底层,正是利用了闭包来隔离不同组件的 state 变量。
3. 核心机制深度解析与实操要点:从原理到代码现场
3.1 执行上下文与调用栈:代码运行的“实时地图”
当你写下console.log(add(1, 2));,JS 引擎并非简单地跳转到add函数。它启动了一套精密的“上下文管理”流程。整个过程可以分为三步:
第一步:创建全局执行上下文(Global Execution Context)
这是 JS 程序的起点。它包含两部分:
- 词法环境(Lexical Environment):存储所有
var声明的变量(初始化为undefined)和函数声明(初始化为函数体)。注意,let/const声明的变量在此阶段是“暂时性死区”(TDZ),不能访问。 - 变量环境(Variable Environment):早期规范中与词法环境分离,现在基本合并,但概念上仍区分
var(提升)和let/const(不提升)。
第二步:执行add(1, 2),创建函数执行上下文(Function Execution Context)
此时,调用栈(Call Stack)里压入了一个新帧:
- this 绑定:在非严格模式下,
this指向全局对象(浏览器是window);严格模式下为undefined。 - 词法环境:包含参数
a=1,b=2,以及函数内声明的变量(如let sum,此时在 TDZ)。 - 外部环境引用(outer):这是关键!它指向创建该函数时所处的词法环境,即全局环境。这决定了闭包的查找路径。
第三步:执行return a + b,函数执行完毕,执行上下文弹出add的执行上下文被销毁,sum变量消失,但a和b的值已作为返回值传递出去。
我们可以用 Chrome DevTools 的断点来亲眼见证这个过程。在add函数第一行打个断点,执行时,右侧的 “Scope” 面板会清晰显示 “Local”(当前函数上下文)、“Closure”(如果有闭包)、“Global”(全局上下文)。你甚至能看到a和b的实时值。这就是为什么理解执行上下文对调试至关重要:当你看到一个变量undefined,第一反应不应该是“代码写错了”,而是打开 Scope 面板,确认它是否真的在当前上下文里被声明,或者是否被提升到了错误的位置。
注意:
var的变量提升(hoisting)是 JS 最易踩坑的点之一。console.log(a); var a = 1;输出undefined,不是ReferenceError。因为引擎在创建阶段就把var a放进了变量环境,值为undefined;执行阶段才走到a = 1。而let a = 1; console.log(a);会直接报错,因为let声明的变量在声明前处于 TDZ,访问即抛错。这是 JS 为修复var设计缺陷而做的改进。
3.2 事件循环(Event Loop):微任务与宏任务的“时间管理大师”
Event Loop 的工作流程,是 JS 异步编程的绝对核心。它的伪代码逻辑极其简洁:
while (true) { // 1. 执行所有同步任务(执行栈) executeAllSyncTasks(); // 2. 执行所有微任务(Microtask Queue),直到队列为空 while (microtaskQueue.length > 0) { const task = microtaskQueue.shift(); executeTask(task); } // 3. 执行一个宏任务(Macrotask Queue),然后回到步骤1 if (macrotaskQueue.length > 0) { const task = macrotaskQueue.shift(); executeTask(task); } }关键差异在于:微任务队列会在每次宏任务执行完毕后,被“清空”;而宏任务队列每次只取一个。这就造成了执行顺序的微妙差别。我们来看一个经典对比:
console.log('1'); setTimeout(() => console.log('2'), 0); // 宏任务 Promise.resolve().then(() => console.log('3')); // 微任务 Promise.resolve().then(() => console.log('4')); // 微任务 console.log('5');输出顺序是:1→5→3→4→2。原因:
- 同步代码先执行:
1和5。 - 同步代码执行完,检查微任务队列:有两个
Promise.then,依次执行3和4。 - 微任务队列清空,Event Loop 取一个宏任务:
setTimeout的回调,执行2。
这个“微任务优先”原则,是 Promise 能够实现.then链式调用的基础。每一个.then返回的新 Promise,其 resolve/reject 处理器都会被放入微任务队列,确保它们在当前同步代码结束后、下一个宏任务开始前执行,从而形成无缝衔接。
实操心得:在 Vue/React 中,
nextTick或useEffect的清理函数,其底层都依赖微任务队列。如果你需要在 DOM 更新后立即执行某些操作(比如获取更新后的元素尺寸),用Promise.resolve().then(...)比setTimeout(..., 0)更可靠,因为它能确保在所有 DOM 渲染完成后的第一时间触发,而不是等待下一个宏任务周期。
3.3 V8 引擎的编译与优化:从源码到机器码的旅程
很多人以为 JS 是“解释执行”的,这是过时的认知。V8 引擎(Chrome 和 Node.js 的核心)采用的是解释 + 即时编译(JIT)的混合策略。整个过程像一条流水线:
- Parser(解析器):将源代码字符串解析成抽象语法树(AST)。例如
function add(a, b) { return a + b; }会被解析成一个包含FunctionDeclaration、Identifier、BinaryExpression等节点的树。 - Ignition(解释器):将 AST 编译成一种轻量级的字节码(Bytecode)。字节码比机器码小得多,节省内存,且可以快速启动。此时
add函数就是一个字节码对象。 - TurboFan(优化编译器):当某个函数被反复调用(称为“热点函数”),TurboFan 就会介入。它会收集运行时信息(比如
a和b总是数字),然后生成高度优化的机器码。这个过程叫“内联缓存(Inline Caching)”和“类型反馈(Type Feedback)”。
这个优化过程是双刃剑。它带来了惊人的性能,但也引入了“优化-去优化”的陷阱。例如:
function add(a, b) { return a + b; } console.log(add(1, 2)); // TurboFan 优化:假设 a,b 都是 number console.log(add('1', '2')); // 去优化:发现 a,b 是 string,丢弃之前优化的机器码,重新编译连续的去优化会严重拖慢性能。因此,最佳实践是保持函数参数类型的稳定性。在大型项目中,TypeScript 的类型标注不仅帮助开发,更是在为 V8 的 TurboFan 提供明确的“类型提示”,让它能更早、更激进地进行优化。
4. 实操过程与核心环节实现:构建一个微型“JS 运行时”观察器
4.1 构建执行上下文可视化工具
要真正理解执行上下文,最好的办法是“看见”它。我们来写一个极简的工具,模拟并打印出函数调用时的上下文快照。
// context-tracker.js const contextStack = []; // 模拟函数调用前的“压栈” function enterContext(funcName, args) { const context = { functionName: funcName, arguments: [...args], timestamp: Date.now(), // 模拟词法环境:捕获当前作用域的变量(简化版,仅展示思路) lexicalEnv: getLexicalEnvSnapshot() }; contextStack.push(context); console.group(`▶️ ${funcName}(${args.join(', ')})`); console.log('📌 进入执行上下文,当前栈深:', contextStack.length); } // 模拟函数返回前的“弹栈” function exitContext(funcName) { const context = contextStack.pop(); console.log('✅ 执行完毕,退出上下文'); console.groupEnd(); console.log(`📊 ${funcName} 执行耗时: ${Date.now() - context.timestamp}ms`); } // 简化的词法环境快照(实际中需更复杂,此处仅为演示) function getLexicalEnvSnapshot() { // 在真实环境中,这需要通过 DevTools Protocol 或其他方式获取 // 这里我们手动模拟一个“当前作用域变量”的概念 return { /* 实际项目中可注入变量名 */ }; } // 使用示例 function calculateTotal(items, taxRate) { enterContext('calculateTotal', [items, taxRate]); let subtotal = 0; for (let i = 0; i < items.length; i++) { subtotal += items[i].price; } const total = subtotal * (1 + taxRate); exitContext('calculateTotal'); return total; } // 调用 calculateTotal([{ price: 10 }, { price: 20 }], 0.08);运行这段代码,你会在控制台看到清晰的分组日志,直观地感受到“压栈”和“弹栈”的过程。虽然它不能获取真实的词法环境变量,但它强迫你在编写函数时,主动思考“此刻我的上下文里有什么”,这是一种极好的思维训练。
4.2 深度剖析 Promise 链与微任务队列
我们来写一个实验,精确测量微任务队列的执行时机,并验证其“清空”特性。
// promise-microtask-experiment.js console.log('【同步】Start'); // 创建一个“微任务计数器” let microtaskCount = 0; // 模拟一个会多次触发微任务的 Promise 链 function createLongChain() { let p = Promise.resolve(); for (let i = 0; i < 5; i++) { p = p.then(() => { microtaskCount++; console.log(`【微任务 #${microtaskCount}】执行`); // 在微任务中再创建新的微任务 if (i === 2) { Promise.resolve().then(() => console.log('【嵌套微任务】在 #3 后立即执行')); } return i; }); } return p; } createLongChain(); // 宏任务 setTimeout(() => { console.log('【宏任务】setTimeout 执行'); }, 0); console.log('【同步】End');输出结果会是:
【同步】Start 【同步】End 【微任务 #1】执行 【微任务 #2】执行 【微任务 #3】执行 【嵌套微任务】在 #3 后立即执行 【微任务 #4】执行 【微任务 #5】执行 【宏任务】setTimeout 执行这个实验铁证如山地证明了两点:1)所有微任务都在同步代码结束后、宏任务开始前执行;2)微任务队列是“深度优先”的,#3里创建的新微任务,会插队到#4和#5之前执行,因为队列在#3执行完后还没被清空。
4.3 内存泄漏诊断:闭包与事件监听器的“幽灵引用”
闭包是利器,也是内存泄漏的温床。最常见的场景是:为 DOM 元素添加事件监听器,监听器函数形成了闭包,捕获了外部大对象,而元素本身又被全局变量引用,导致整个闭包链无法被 GC 回收。
// memory-leak-demo.js function setupLeakyHandler() { const largeData = new Array(1000000).fill('leak'); // 模拟大对象 // ❌ 危险:监听器函数捕获了 largeData,且 button 是全局变量 const button = document.getElementById('myButton'); button.addEventListener('click', function() { console.log('Clicked! Large data size:', largeData.length); }); // ✅ 正确:使用弱引用或显式解绑 // 方案1:使用箭头函数 + 显式解绑 // const handler = () => console.log('Safe!'); // button.addEventListener('click', handler); // return () => button.removeEventListener('click', handler); // 方案2:使用 WeakMap 存储关联数据(高级技巧) } // 调用后,button 一直存在,largeData 就永远无法释放 setupLeakyHandler();诊断方法:在 Chrome DevTools 的 Memory 面板,点击 “Take heap snapshot”,然后在筛选框输入closure,查看哪些闭包持有大量内存。或者,在 Performance 面板录制一次操作,查看内存增长曲线是否在操作后不回落。
5. 常见问题与排查技巧实录:十年踩坑总结
5.1 “this 指向丢失”:不是 Bug,是规则
问题现象:在对象方法中,将方法赋值给变量后调用,this变成了undefined或window。
const obj = { name: 'Alice', greet: function() { console.log('Hello, ' + this.name); } }; obj.greet(); // Hello, Alice ✅ const fn = obj.greet; fn(); // Hello, undefined ❌原因:this的绑定发生在函数调用时,而非定义时。obj.greet()是“方法调用”,this绑定到obj;fn()是“函数调用”,在非严格模式下this指向全局,在严格模式下为undefined。
解决方案:
- 箭头函数:
greet: () => console.log('Hello, ' + this.name)—— 但注意,箭头函数的this是词法作用域的,它会捕获定义时外层的this,对于对象方法通常不合适。 - bind:
const fn = obj.greet.bind(obj); - call/apply:
fn.call(obj); - 包装函数:
const fn = () => obj.greet(); - Class 字段语法(现代):
greet = () => console.log('Hello, ' + this.name);—— 这是目前最推荐的方式,它在构造函数中自动为每个实例绑定this。
5.2 “异步地狱”与 Promise 链断裂
问题现象:.then链中某个环节忘记return,导致后续.then接收到undefined。
Promise.resolve(1) .then(x => { console.log(x); // 1 // 忘记 return x * 2; }) .then(y => console.log(y)); // undefined ❌排查技巧:在每个.then的回调函数末尾,强制加上return语句。或者,使用async/await,让代码看起来像同步的,天然避免链断裂:
async function run() { const x = await Promise.resolve(1); console.log(x); // 1 const y = x * 2; // 显式赋值,逻辑清晰 console.log(y); // 2 }5.3 “变量提升”引发的逻辑错乱
问题现象:在var声明前使用变量,得到undefined,但程序不报错,导致难以发现的逻辑错误。
console.log(foo); // undefined if (false) { var foo = 'bar'; // 这行永远不会执行,但 var 声明已被提升 }终极解决方案:全面禁用var,只用let和const。现代编辑器(VS Code)和 ESLint 规则(no-var)都能帮你强制执行这一点。let/const的 TDZ 特性,会让错误在发生时立刻暴露,而不是隐藏在undefined的迷雾中。
| 问题类型 | 典型症状 | 快速定位方法 | 一劳永逸的解决策略 |
|---|---|---|---|
this丢失 | 对象方法内this.xxx为undefined | 在方法第一行加console.log(this) | 使用 Class 字段语法method = () => {} |
| Promise 链断裂 | 后续.then接收到undefined | 检查每个.then回调是否都有return | 全面迁移到async/await |
var提升陷阱 | 变量在声明前可访问但值为undefined | 启用 ESLintno-var规则 | 项目初始化即配置eslint-config-airbnb |
我在某公司重构一个遗留系统时,曾花三天时间追踪一个“数据偶尔丢失”的 bug。最终发现,是某个
var声明被意外提升,导致一个条件判断永远为false。那次经历让我彻底放弃了var,并在团队推行了严格的 ESLint 配置。工具不能替代思考,但能消灭 90% 的低级错误。
6. 工程化实践与未来演进:从玩具到工业级
6.1 TypeScript:不是 JS 的替代品,而是它的“增强现实眼镜”
把 TypeScript 看作 JS 的“超集”是一种常见误解。更准确地说,TS 是 JS 的类型注解层。它在编译时(tsc)移除所有类型信息,输出纯 JS 代码。这意味着:
- 你的 TS 代码,100% 运行在 JS 引擎上,没有任何运行时开销。
- 所有 JS 的运行时行为(原型链、
this绑定、==的隐式转换)在 TS 中完全保留。 - TS 的类型系统是“结构性的”(Structural Typing),而非“名义性的”(Nominal Typing)。
interface User { name: string }和interface Person { name: string }是完全兼容的,只要结构相同。
因此,TS 的价值不在于“让 JS 变成 Java”,而在于在编码阶段,为你提供一份关于“数据形状”的、可执行的文档。当你看到一个函数签名function processUser(user: User): Promise<UserProfile>,你立刻知道输入必须有name属性,输出是一个 Promise,resolve 的值有avatarUrl和bio字段。这比任何 JSDoc 注释都更可靠,因为它是被编译器强制校验的。
6.2 WebAssembly(Wasm):JS 生态的“新大陆”,而非“掘墓人”
最近常听到“Wasm 将取代 JS”的论调,这完全是误读。Wasm 的设计目标是为 Web 提供一个安全、高效、可移植的二进制目标格式,它本身不处理 DOM、不管理内存(需要 JS 协助)、不提供网络 API。它最擅长的,是计算密集型任务:图像处理、音视频编解码、游戏物理引擎、密码学运算。
一个典型的 Wasm 应用架构是:JS 作为“胶水层”,负责 UI、DOM 操作、网络请求;Wasm 模块作为“计算引擎”,负责繁重的数学运算。两者通过WebAssembly.instantiate()加载,并通过import/export机制交换数据(通常是ArrayBuffer)。
所以,学习 JS 不仅不会过时,反而会变得更重要——因为你需要 JS 来驾驭 Wasm。未来的全栈工程师,很可能既要会写高效的 Rust/Wasm 模块,也要会写健壮的 JS 胶水代码。
6.3 Deno 与 Bun:运行时的“新势力”,但 JS 语言本身岿然不动
Deno 和 Bun 是新一代 JS/TS 运行时,它们解决了 Node.js 的一些历史包袱(如require、node_modules的嵌套地狱、缺乏内置 TypeScript 支持)。但请注意:它们运行的仍然是 JavaScript(和 TypeScript)语言规范。ECMAScript 标准(ES2023, ES2024...)由 TC39 委员会制定,所有运行时都必须遵循。Deno 和 Bun 的创新,在于模块系统(ESM 优先)、权限模型(默认沙箱)、内置工具链(Deno lint/test/format),而非改变 JS 本身的语法或语义。
这意味着,你今天学到的闭包、原型链、事件循环,明天在 Bun 里依然完全适用。运行时的演进,是基础设施的升级,而 JS 语言的核心机制,已经足够成熟和稳定。把精力花在理解这些核心机制上,才是对抗技术浪潮最坚固的堤坝。
我在某次技术分享会上,有位听众问:“现在学 JS,会不会几年后就被新语言淘汰?”我的回答是:“JS 语言本身,就像空气和水。我们不会讨论‘空气会不会被淘汰’,只会讨论‘如何更高效地呼吸’。Deno、Bun、Wasm,都是让我们呼吸得更顺畅的新肺叶,而不是取代肺的器官。”