前端转Agent开发 | JavaScript 从 0 到 1 完全指南

JavaScript 从 0 到 1 完全指南(基于本仓库)

起点:你已经接触过 HTML/CSS,或正从纯前端往全栈转。本模块讲的是 JS 这门语言本身的底层本质 ,不依赖框架、不依赖浏览器。 目标:掌握 JS 的类型系统、隐式转换、作用域、闭包、原型、this、事件循环 这些「背了又忘、一用就懵」的核心,让你后面的 es6模块、node模块、typescript模块、nest模块、agent模块 全部能看懂底层。 主张:本模块尽量用零依赖的原生 Node 直接跑,把「一句话为什么会这样」讲透,而不是只背结论。


为什么你已经有一半基础

你在前端写的 const、箭头函数、map filter reduce、Promise,其实都是 JS。本模块不做重复教学,而是把「你一直当黑盒用、出了错才回来的那部分」补上:

text 复制代码
JS 语言内核(本模块:类型、作用域、闭包、原型、this、事件循环)
  → ES6 现代语法(es6模块:let/const、class、模块、async)
  → Node 运行时(node模块:V8、libuv、事件循环、流)
  → TypeScript 类型(typescript模块:把 JS 变成带类型的 JS)
  → NestJS 工程化(nest模块:依赖注入、模块、装饰器)
  → 数据库(mysql / postgresql)
  → Agent / AI 应用(agent模块)

一句话:语法是表象,语言内核才是本质。 你把本模块读透,再去看 typescript模块 里的 interface、nest模块 里的装饰器、agent模块 里的 async 流式处理,都会有一种「原来如此」的通透感。


目录

  1. JavaScript 是什么、在哪跑(V8、执行上下文、调用栈)
  2. 值与类型:7 种原始类型 + 对象(typeof 陷阱、装箱)
  3. 类型转换(coercion):== vs ===、falsy、隐式转换
  4. 变量、作用域与执行上下文(词法作用域)
  5. 闭包(Closure):函数为什么记得外部变量
  6. 引用与值传递(对象按引用、浅拷贝 / 深拷贝)
  7. 函数:一等公民、高阶函数、回调
  8. this 的绑定规则(调用点、call / apply / bind、箭头函数)
  9. 原型与继承([[Prototype]]、new 做了什么、class 语法糖)
  10. 内置对象与方法(Object、Array、String、JSON)
  11. 异步与事件循环(回调、微任务 / 宏任务、Promise 状态机)
  12. 错误处理(try/catch、Error、finally、异步错误)
  13. 模块化(脚本、CommonJS、ESM)
  14. 常见陷阱与调试
  15. 整体串联:JS → ES6 → TS → Node → Nest → Agent

1. JavaScript 是什么、在哪跑

1.1 概念

JavaScript(简称 JS)是一门**动态、弱类型、解释执行(或 JIT 编译)**的脚本语言。它最初用来让网页能动起来,后来靠 Node.js 跑到了服务端,现在几乎无处不在:浏览器、Node、小程序、Electron、甚至数据库(PostgreSQL 的 PL/pgSQL 是另一套,但概念相通)。

你写的不是「给机器看的代码」,而是一段文本 。要让它跑起来,必须交给一个引擎去解释、编译、执行。

1.2 底层本质:V8 与执行流程

现代浏览器和 Node 都用 V8(谷歌的 JS 引擎)。它拿到你的源码后大致做这几件事:

text 复制代码
源码字符串
  → 词法/语法分析(Parser)生成 AST 抽象语法树
  → 编译器(Ignition)把 AST 编成字节码
  → 解释器逐条执行字节码
  → 热点代码会被 TurboFan 优化成机器码(JIT)

所以 JS 不是「一次编译一次运行」,而是边解析边执行,并在运行时优化。这就是为什么有些错误要到运行到那一行才报出来,这也是为什么它叫「动态语言」。

真正执行时,引擎维护一个调用栈(call stack) :每调用一个函数,就压入一个执行上下文;函数返回,就弹出。栈顶永远是最新执行的那个函数。

js 复制代码
function a() { return b() }
function b() { return c() }
function c() { console.log('c 在执行') }
a() // 调用栈:a -> b -> c

如果函数无限递归,栈会塞满,报 RangeError: Maximum call stack size exceeded。

1.3 可运行代码

本模块每个核心章节都有对应可运行示例,全部用 Node 原生能力,不需要装依赖:

bash 复制代码
cd "js模块/示例代码"
node 1-类型与typeof.js
node 2-隐式转换.js

1.4 串起来

  • node模块 第 1 章讲的就是 V8 + libuv:V8 执行 JS,libuv 做异步 I/O。本模块的「调用栈」就是 V8 那半边。
  • typescript模块 的类型,本质是在编译期(运行前)帮你把「运行期才会暴露的类型错误」提前拦下来。
  • agent模块 里的流式 async/await,底层依然是这个调用栈 + 事件循环,只是封装成了更顺手的语法。

2. 值与类型:7 种原始类型 + 对象

2.1 概念

JS 的值可以分成两大类:原始类型 和引用类型。

原始类型(primitive)有 7 种:

text 复制代码
string  number  boolean  null  undefined  symbol  bigint

除了这 7 种,剩下的全是对象 (object),包括数组、函数、正则、Date 等。记住关键区别:原始类型按值存,对象按引用存(第 6 章会展开)。

2.2 底层本质:typeof 的陷阱

typeof 用来判断类型,但它来自最初的 JS 设计,有很多坑:

js 复制代码
typeof 'hi'        // 'string'
typeof 1           // 'number'
typeof true        // 'boolean'
typeof undefined   // 'undefined'
typeof null        // 'object'   ← 历史遗留 bug!
typeof []          // 'object'   ← 数组其实也是对象
typeof function(){}// 'function' ← 函数比较特殊

typeof null === 'object' 是第一个版本的 bug,为了兼容一直没有改。要判断数组用 Array.isArray(x),要判断 null 用 x === null,不要靠 typeof。

2.3 底层本质:原始值被「装箱」

为什么 'hello'.toUpperCase() 能成立?'hello' 是字符串原始值,没有方法。引擎在调用属性方法那一刻,临时把字符串装箱 成一个 String 对象,调用完再拆回原始值。这就是「自动装箱 / 拆箱」。

js 复制代码
const s = 'hello'
console.log(s.toUpperCase()) // HELLO,s 本身还是原始字符串

2.4 串起来

  • typescript模块 的 string | number | boolean 等联合类型,就是在描述这套「有哪些值」。
  • postgresql模块 的 JSONB 字段,对应到 JS 就是「对象 / 数组」这种引用类型。
  • agent模块 里 JSON.parse 返回的就是对象(引用类型),所以你能直接取 .content、.tool_calls。

3. 类型转换(coercion):JS 最容易被坑的地方

3.1 概念

JS 是弱类型 的,不同类型之间经常被自动转换,这叫强制类型转换(coercion)。它让代码好写,也让人踩坑。

最典型的是 == 和 ===:

js 复制代码
1 == '1'    // true,会先把 '1' 转成数字
1 === '1'   // false,不转换,类型不同就是 false
null == undefined // true
null === undefined // false

结论:永远用 === / !==,不要用 ==,除非你明确知道要转换。

3.2 底层本质:falsy 值

在条件判断里,只有下面这些值会被当成「假」(falsy):

js 复制代码
''           // 空字符串
0  -0  NaN
null
undefined
false

除此之外全是真(truthy),包括 '0'、'false'、[]、{}、[] 这种「看起来是空但实际上不是」的值。所以:

js 复制代码
if ([]) console.log('这个会执行!')   // 空数组也是真
if ({}) console.log('这个也会执行!') // 空对象也是真

3.3 底层本质:+ 和 Number()

+ 是转换里最迷惑的。规则是:只要有一边是字符串,就会把另一边也转成字符串做拼接;否则两边都转成数字相加。

js 复制代码
'1' + 2     // '12',字符串拼接
1 + '2'     // '12'
1 + 2       // 3
'1' + '2'   // '12'
true + 1    // 2,true 转成 1
null + 1    // 1,null 转成 0
undefined + 1 // NaN,undefined 转成 NaN

Number('123') → 123;Number('abc') → NaN;Boolean('') → false。这些显式转换能帮你绕开隐式的坑。

3.4 串起来

  • mysql模块 / postgresql模块 设参时,如果前端传的是字符串 '1',数据库可能报类型错。这就是为什么参数化查询 + 类型校验很重要。
  • typescript模块 的类型系统,就是为了不让 '1' + 2 这类「合法但错误」的代码溜进生产。
  • agent模块 的 JSON.parse 拿到字符串后,必须走 zod 或手写校验,正是因为 JS 的隐式转换会让「看起来对」的代码出错。

4. 变量、作用域与执行上下文

4.1 概念

作用域(scope)是「变量在哪些地方可见」。JS 是词法作用域 (lexical scope),也就是说:变量在哪定义,就决定了它在哪能被访问,跟你在哪调用无关。 记住这句话,闭包和 this 就懂了一半。

作用域分几层:

text 复制代码
全局作用域
  → 函数作用域(var 和函数声明)
    → 块级作用域(let / const:花括号 {} 就是一个块)

var 只有函数作用域,所以 for(var i...) 里的 i 会泄漏到函数外;let / const 有块级作用域,不会泄漏。

4.2 底层本质:执行上下文与作用域链

每次调用函数,引擎会创建一个执行上下文(execution context),里面有三块关键内容:

text 复制代码
变量对象(存参数、函数声明、变量)
作用域链(从当前函数一路指向外层的能访问的变量)
this 绑定

访问一个变量时,引擎顺着作用域链 一层层往上找:当前作用域 → 外层 → ... → 全局。找不到就报 ReferenceError。

这是为什么:

js 复制代码
const name = 'global'
function outer() {
  const name = 'outer'
  function inner() {
    console.log(name) // 找到的是 outer 的 name
  }
  inner()
}
outer()

inner 能访问 outer 的 name,因为词法作用域决定了它在 outer 内部定义,作用域链里就包含 outer。

4.3 可运行代码

bash 复制代码
node 3-作用域与闭包.js

4.4 串起来

  • es6模块 第 2 章 let / const 就是「块级作用域」的现代写法。
  • typescript模块 的 let / const 类型收窄、enum、namespace,都建立在这套作用域模型上。
  • nest模块 的依赖注入,本质是「在一个很长的作用域链里,把依赖实例塞给需要的类」。

5. 闭包(Closure):面试必问、生产必用

5.1 概念

闭包 是:一个函数,保留了它定义时所在作用域里的变量。哪怕外层函数已经返回,内层函数仍然能「记住」那些变量。

js 复制代码
function makeCounter() {
  let count = 0
  return function () {
    count += 1
    return count
  }
}
const counter = makeCounter()
console.log(counter()) // 1
console.log(counter()) // 2
console.log(counter()) // 3

count 明明在 makeCounter 里,函数却还能继续改它,因为返回的那个内部函数把这个作用域「闭」住了。

5.2 底层本质

函数对象在创建时,会带一个指向定义时作用域的引用 (通常叫 [[Environment]] 或外部引用)。当内部函数被返回并保存,它一直持有那个作用域引用,所以那里面的变量不会被垃圾回收。

console.log 能读到 count,不是因为它「重新查找」,而是因为它的作用域链里挂着 makeCounter 的作用域。闭包不是「复制的值」,而是「活着的引用」。

5.3 经典坑:循环里的 var

用 var 在循环里创建多个闭包,所有闭包都指向同一个 var i,所以打印的都是最终值:

js 复制代码
const fns = []
for (var i = 0; i < 3; i++) {
  fns.push(function () { return i })
}
console.log(fns[0]()) // 3,因为共享同一个 i

改用 let,每轮都是一个新的块级绑定,就能各自记住:

js 复制代码
const fns = []
for (let i = 0; i < 3; i++) {
  fns.push(function () { return i })
}
console.log(fns[0]()) // 0

5.4 串起来

  • es6模块 第 5 章「默认参数与展开」、第 2 章 let,都和闭包作用域有关。
  • node模块 的 EventEmitter、文件读取回调,本质就是「把闭包传给异步操作,操作完成后回调里还能访问到那个闭包的状态」。
  • agent模块 的 withClient() 返回 { config, close },close 就是一个闭包,记住了 mock 服务端口和生命周期。

6. 引用与值传递

6.1 概念

原始类型(number、string、boolean 等)按值传递:赋值就是复制一份值。

js 复制代码
let a = 1
let b = a
b = 2
console.log(a) // 1,a 没变

对象 / 数组 / 函数是引用类型:变量存的是「指向对象的引用」,赋值复制的是引用,不是对象本身。

js 复制代码
const obj = { name: 'a' }
const copy = obj
copy.name = 'b'
console.log(obj.name) // b,因为 copy 和 obj 指向同一个对象

6.2 底层本质:浅拷贝 vs 深拷贝

因为引用是共享的,所以修改一个会影响另一个。要真正「复制一份对象」,需要自己拷贝:

  • 浅拷贝 :只复制第一层属性。Object.assign({}, obj) 或 {...obj}。
  • 深拷贝 :递归复制所有嵌套。JSON.parse(JSON.stringify(obj)) 或 structuredClone(obj)。
js 复制代码
const origin = { a: 1, nested: { b: 2 } }
const shallow = { ...origin }
shallow.nested.b = 99
console.log(origin.nested.b) // 99!浅拷贝只复制了第一层,nested 还是同一个对象

注意 JSON.parse(JSON.stringify()) 会丢掉函数、undefined、Date 等;structuredClone 更安全,也能处理循环引用。

6.3 可运行代码

bash 复制代码
node 4-引用与深拷贝.js

6.4 串起来

  • 前端很常见的 items.map(x => ({ ...x })) 就是浅拷贝,改嵌套对象还是会「串」。
  • react / vue 的响应式靠「新的引用」触发更新,所以不写浅拷贝会引发「改了但页面不刷新」。
  • postgresql模块 的 JSONB 读回来是对象引用;你再改它并写库,要小心是不是改到了共享的旧对象。

7. 函数:一等公民与高阶函数

7.1 概念

在 JS 里,函数是一等公民:它可以像普通值一样被赋值、作为参数传入、作为返回值返回。这意味着你可以在运行期「把行为传来传去」。

js 复制代码
function greeting(name) { return '你好 ' + name }
const fn = greeting        // 函数也是值
console.log(fn('张三'))     // 你好 张三

function applyTwice(f, x) {
  return f(f(x))
}
console.log(applyTwice(n => n * 2, 3)) // 12

高阶函数 就是「接收函数作为参数,或返回函数」的函数。map、filter、reduce 都是高阶函数。

7.2 底层本质:函数表达式、箭头函数与 IIFE

函数有几种写法,作用域和行为有细微差别:

js 复制代码
function a() {}        // 函数声明,整体提升
const b = function () {} // 函数表达式,赋值给 b
const c = () => {}      // 箭头函数,lexical this(见第 8 章)
;(function () {        // IIFE:立即执行函数表达式,用来包一层作用域
  const private = 1
})()

var 时代的模块就是这么用 IIFE 包一层作用域来实现「私有变量」的。现在有了 let / const 和模块,IIFE 用得少了,但你会在老代码和打包产物里频繁看到它。

7.3 可运行代码

bash 复制代码
node 5-函数与高阶函数.js

7.4 串起来

  • es6模块 第 3 章「箭头函数」、第 5 章「展开/默认参数」,都是函数写法的现代版本。
  • node模块 的 fs.readFile(path, callback),callback 就是「把函数当参数传,I/O 完成后再调用」。
  • typescript模块 的函数类型 (x: number) => string,就是在描述「这个值是个函数」。
  • agent模块 的 onDelta 回调、withClient() 里的 close 函数,全是「用高阶函数 + 闭包组织控制流」。

8. this 的绑定规则

8.1 概念

this 是 JS 最让人头大的概念。它不是 指向函数本身,也不是指向定义函数的作用域,而是由「函数是怎么被调用的」决定的(叫调用点 call site)。

8.2 底层本质:四条规则

this 的绑定按优先级从高到低(后面的覆盖前面的):

text 复制代码
1. new 绑定:new 调用的函数,this 指向新建的实例
2. 显式绑定:call / apply / bind 手动指定 this
3. 隐式绑定:以「对象.方法」方式调用,this 指向那个对象
4. 默认绑定:独立调用(普通函数调用),this 指向全局对象(浏览器 window / Node global)
js 复制代码
function show() { console.log(this) }

const obj = {
  name: 'obj',
  show,
}
obj.show()      // 隐式绑定 -> this = obj
show()          // 默认绑定 -> this = global/window
show.call(obj)  // 显式绑定 -> this = obj

function Person(name) { this.name = name }
const p = new Person('张三') // new 绑定 -> this = p

箭头函数是个例外:它没有自己的 this ,会从定义它的外层作用域「继承」this,而且不能被 call/apply/bind 改变。所以回调里想保留外层 this,用箭头函数最方便。

8.3 可运行代码

bash 复制代码
node 6-this绑定.js

8.4 串起来

  • es6模块 第 3 章「箭头函数与 this」专门讲这个例外。
  • node模块 里 EventEmitter.on('x', () => {}) 常用箭头,就是为了让回调里的 this 指向外部类实例。
  • typescript模块 的 this 参数类型、nest模块 的 @Injectable 类实例方法,底层都是这套 this 规则。
  • react 类组件里的 this.handleClick 要用 bind 或箭头,就是因为方法被单独传递后 this 丢了。

9. 原型与继承

9.1 概念

JS 的继承不是「类继承」,而是原型链(prototype chain) 。每个对象内部都有一个隐藏的 [[Prototype]](平时用 __proto__ 访问),指向它的原型对象。读一个属性时,先在对象自己身上找,找不到就顺着原型往上找,直到尽头。

js 复制代码
const obj = { name: 'x' }
console.log(obj.toString) // 对象自己没定义,顺着原型找到 Object.prototype 上的方法

9.2 底层本质:new 做了什么

new Person('张三') 大致做了四件事:

text 复制代码
1. 创建一个空对象
2. 把这个空对象的 [[Prototype]] 指向 Person.prototype
3. 用这个人字 this 调用 Person,执行构造函数体
4. 如果构造器返回对象就返回它,否则返回这个新对象

这就是为什么:

js 复制代码
function Person(name) { this.name = name }
const p = new Person('张三')
console.log(p.name)                 // 张三
console.log(p instanceof Person)    // true
console.log(Object.getPrototypeOf(p) === Person.prototype) // true

9.3 底层本质:class 是语法糖

ES6 的 class 只是把「构造函数 + 原型方法」写得更好看,底层仍然是原型链:

js 复制代码
class Person {
  constructor(name) { this.name = name }
  hello() { return this.name }
}

等价于上面的构造函数写法。extends 底层也是把子类的 [[Prototype]] 指到父类,并调用 super 触发父类构造。

9.4 可运行代码

bash 复制代码
node 7-原型与new.js

9.5 串起来

  • es6模块 第 8 章 class、typescript模块 的 class 与 interface implements,都是「原型连起来的对象」的另一种表达。
  • node模块 的 EventEmitter 就是靠原型,让自定义类能 extends EventEmitter。
  • nest模块 的 @Injectable() 类,通过装饰器往原型上挂元数据,依赖注入再读这些元数据,本质仍是「原型 + 属性」。

10. 内置对象与方法

10.1 概念

你每天敲的 Object.keys、Array.prototype.map、String.prototype.trim、JSON.parse,都是引擎内置好的方法。它们不是语言语法,而是挂在内置构造函数原型上的方法,所以所有对象 / 数组 / 字符串实例都继承得到。

10.2 底层本质:数组方法都是「遍历 + 回调」

map / filter / reduce / forEach 的本质都是:

text 复制代码
遍历数组每一项 -> 把每一项和 (元素, 索引, 数组自己) 传给回调 -> 根据回调返回值做处理
js 复制代码
const nums = [1, 2, 3]
const doubled = nums.map(n => n * 2)   // [2, 4, 6],返回新数组
const evens = nums.filter(n => n % 2 === 0) // [2],筛选
const sum = nums.reduce((acc, n) => acc + n, 0) // 6,累积

注意 map / filter 返回新数组 ,不改原数组;sort / splice / push 会原地修改。分清「返回新值」还是「原地改」,是写出无 bug 代码的关键。

10.3 底层本质:JSON 是文本,不是对象

JSON.stringify(obj) 把对象变成字符串;JSON.parse(str) 把字符串变成对象。它们是序列化 / 反序列化 ,不是「对象转对象」。所以在网络上传输的永远是字符串,到对方那端再 parse 成对象。

js 复制代码
const obj = { a: 1 }
const text = JSON.stringify(obj) // '{"a":1}'
const back = JSON.parse(text)    // { a: 1 }

10.4 可运行代码

bash 复制代码
node 8-数组与对象方法.js

10.5 串起来

  • frontend 里 data.map() 是渲染列表;typescript模块 里数组泛型 Array<T> 就是描述这些方法。
  • mysql模块 / postgresql模块 把查询结果转成对象数组,Node 里就是遍历这些数组。
  • agent模块 的 messages.map(m => m.content)、JSON.parse(tool_calls),全是这里的方法。

11. 异步与事件循环

11.1 概念

JS 是单线程 的,但它能同时处理很多异步任务(读文件、发请求、查数据库)。原理是:主线程执行同步代码,遇到底层耗时操作就交给运行时,操作完成后再把回调放回队列,等主线程空闲了再执行。

这个「不断排队、取任务、执行」的循环,叫事件循环(event loop)。

11.2 底层本质:宏任务与微任务

任务分成两种,执行优先级不同:

text 复制代码
宏任务(macrotask):整个脚本、setTimeout、setInterval、I/O 回调
微任务(microtask):Promise.then / catch、queueMicrotask

执行顺序是:每执行完一个宏任务,就清空所有微任务,再执行下一个宏任务。

js 复制代码
console.log('start')
setTimeout(() => console.log('timeout'), 0)
Promise.resolve().then(() => console.log('promise'))
console.log('end')
// 输出顺序:start -> end -> promise -> timeout

Promise.then 是微任务,所以先于 setTimeout 执行。这就是为什么 await 之后(微任务)能赶上下一轮宏任务之前。

11.3 底层本质:Promise 状态机

Promise 是一个有状态的容器:pending(进行中)→ fulfilled(成功)或 rejected(失败) ,一旦转变就不可逆。then 注册的是「成功回调」,catch 注册的是「失败回调」。async/await 只是把 then/catch 写成同步的样子。

js 复制代码
async function main() {
  const data = await fetch('https://x').then(r => r.json())
  console.log(data)
}

11.4 可运行代码

bash 复制代码
node 9-事件循环与微任务.js

11.5 串起来

  • es6模块 第 10-11 章 Promise / async,是「把事件循环包成好用的语法」。
  • node模块 第 10 章事件循环、第 11 章异步流程,讲的是 libuv 层的调度。
  • agent模块 的 streamChat 用 await reader.read() 循环,就是「等一块数据、处理、再等」,本质是事件循环里的异步队列。

12. 错误处理

12.1 概念

JS 用 throw 抛错误,用 try/catch/finally 捕获。抛出的通常是 Error 对象,带 message(人话)和 stack(调用栈)。

js 复制代码
function divide(a, b) {
  if (b === 0) throw new Error('除数不能为 0')
  return a / b
}
try {
  divide(1, 0)
} catch (err) {
  console.log('出错了:', err.message)
} finally {
  console.log('无论对错都会执行')
}

12.2 底层本质:错误是「向上抛」

没有 try 接住时,错误会沿着调用栈一直往上抛 ,直到被捕获,否则进程崩溃 / 页面报错。finally 无论是否抛出都会执行,适合做清理(关连接、取消定时器、释放资源)。

异步里要注意:throw 在普通函数里同步抛出,但 Promise 的异常要 catch,async 函数里的异常会自动变成 rejected 的 Promise。

js 复制代码
async function bad() {
  throw new Error('x')
}
bad().catch(err => console.log(err.message)) // x

忘记 await 或忘记 catch,错误就会被「吞掉」或变成未处理的 Promise rejection。

12.3 可运行代码

bash 复制代码
node 10-错误处理.js

12.4 串起来

  • node模块 的健壮性章节、nest模块 的异常过滤器(ExceptionsFilter),都是把这套 try/catch 在 HTTP 层统一化。
  • mysql模块 / postgresql模块 的连接、事务一定要 finally 里释放,否则连接池被占满。
  • agent模块 的 withClient() 用 try { ... } finally { await close() } 保证永不泄漏连接,就是这个原则的实战。

13. 模块化

13.1 概念

模块化就是把代码拆成多个文件,通过「导出 / 导入」复用。JS 历史上主要有两套:

text 复制代码
CommonJS(CJS):require / module.exports,Node 默认同步加载
ESM:import / export,浏览器和现代 Node 支持,静态解析、异步
js 复制代码
// CommonJS
const fs = require('fs')
module.exports = { add: (a, b) => a + b }

// ESM
import fs from 'node:fs'
export const add = (a, b) => a + b

13.2 底层本质:为什么 Node 用 CommonJS,浏览器用 ESM

  • Node 出生时 JS 还没有官方的模块标准,于是自己定了 CommonJS,用 require 在运行时同步加载。
  • 后来浏览器需要静态分析(才能在页面加载前就知道要下载哪些文件),所以 ESM 用 import 做了静态导入,语法在解析阶段就能确定。
  • Node 现在也全面支持 ESM:.mjs 后缀,或 package.json 里 "type": "module"。

require 是同步 的,会阻塞主线程等文件读完;import 在支持它的环境下通常是异步的,浏览器能提前并行加载。

13.3 可运行代码

bash 复制代码
node 11-模块化.js   # CommonJS 版
node 12-ESM.mjs    # ESM 版(需 .mjs 后缀)

13.4 串起来

  • es6模块 第 9 章「import/export」,是本仓库 ESM 的完整版。
  • node模块 第 3 章、第 12 章讲 CommonJS 和 node_modules。
  • agent模块 的 src/llm.js 用 export async function,examples 用 import,就是 ESM。
  • typescript模块 的 module: 'CommonJS' / 'ESNext' 配置,决定编译后转成哪种模块系统。

14. 常见陷阱与调试

14.1 概念

下面这些是 JS 初学者(甚至老手)最常踩的坑,知道原理就能避免:

text 复制代码
1. 用 == 而不是 ===,触发隐式转换
2. 用 typeof 判断数组 / null,得到 'object'
3. var 泄漏作用域、闭包闭住同一个循环变量
4. 修改了引用对象没做浅拷贝,导致多处联动
5. this 丢失(把方法单独传出去用)
6. 忘记 await,Promise 没被消费,错误被吞
7. try 里 return 但漏了 finally 清理资源
8. 用 JSON.parse(JSON.stringify()) 处理含函数 / Date 的对象

14.2 调试建议

js 复制代码
console.log(value)        // 快速看值
console.dir(obj, { depth: null }) // 看对象完整结构
console.assert(条件, '提示') // 条件不成立才打印
console.time('t'); // 计时

真跑起来报错,第一件事是看 stack,从最上面的第一行找到你自己的文件,那才是问题源头。

14.3 串起来

  • 这些问题,typescript模块 的类型能在编译期拦掉一部分(比如 ===、参数类型)。
  • nest模块 的 ValidationPipe 在输入层校验,拦截前端传进来的错误类型。
  • agent模块 的 try/finally、zod 校验,就是把这些防御写进 AI 应用的每个环节。

15. 整体串联:JS → ES6 → TS → Node → Nest → Agent

15.1 一条清晰的路径

你把这个仓库从 JS 开始学,等于把「一门语言为什么长这样」一步一步串起来:

text 复制代码
1. JS 内核(本模块)
   类型 / 作用域 / 闭包 / 原型 / this / 事件循环

2. ES6 语法(es6模块)
   let/const、箭头函数、class、模块、Promise、async/await

3. TypeScript(typescript模块)
   给 JS 加上静态类型与接口、泛型、装饰器

4. Node(node模块)
   V8 + libuv + 事件循环 + CommonJS + Buffer + 流 + HTTP

5. NestJS(nest模块)
   用 class / 装饰器 / 依赖注入把 Node 代码工程化

6. 数据库(mysql / postgresql)
   把应用数据持久化,再用 pgvector 做向量检索

7. Agent / AI(agent模块)
   用 fetch、Promise、async、工具调用、RAG 做 AI 应用

15.2 每个阶段解决什么问题

阶段 解决的核心问题
JS 内核 理解这门语言本身,读得懂、写得出、能分析错误
ES6 用现代、简洁、安全的方式写 JS
TypeScript 让大型项目可维护、可重构
Node 让 JS 能上服务端、处理 I/O
Nest 工程化组织后端,提供可测试的架构
数据库 数据持久化与检索
Agent 让大模型编排你已有的工程系统

15.3 你现在的目标

你目标是从前端转型到 Agent / AI 应用工程师 。这条路径的终点不是「会用某个框架」,而是:你能用 JS 这门语言,把一个 AI 应用从语言内核、到后端架构、到数据、到模型调用端到端串起来。

本模块是第 1 块拼图。学会它,后面每块都会变得顺理成章。


附录:快速自测

把下面这段读一遍,讲清楚每个输出为什么,你就掌握了 JS 内核:

js 复制代码
console.log(typeof null)              // 'object'
console.log(1 == '1')                 // true
console.log('1' + 2)                  // '12'
console.log([] ? 'truthy' : 'falsy')  // truthy
console.log(Boolean('0'))             // true

let a = 1
let b = a
b = 2
console.log(a)                        // 1

const obj = { nested: { n: 1 } }
const copy = { ...obj }
copy.nested.n = 2
console.log(obj.nested.n)             // 2(浅拷贝)

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0) // 0 1 2,let 每轮独立
}

如果你能解释每一条「为什么」,说明你已经真正理解了 JS 的核心,而不是死记输出。

相关推荐
开开心心就好1 小时前
课堂随机点名工具,绿色版免安装双击即用
java·服务器·前端·ffmpeg·音视频·perl·symfony
不像程序员的程序媛1 小时前
springboot支持的web容器
前端·spring boot·后端
xiaobaoyu1 小时前
目录合集
前端
han_hanker1 小时前
前端转后端入门实践日志2-nacos
前端
Suprman1 小时前
从 B/S 到 Electron:平面排版套料系统桌面版开发实战
javascript·平面·electron·排序算法
u0111026751 小时前
Vue 3 统一右侧编辑面板:布局、滚动与底部操作区设计
javascript·vue.js·ecmascript
change_fate1 小时前
Vue 3 Composable 函数指南
前端·javascript·vue.js
tryCbest1 小时前
Vue3——第 9 篇:组件通信全攻略(下)——Provide / Inject 与事件总线
前端·javascript·vue.js
lichenyang4531 小时前
技术穿刺怎么做?以企业知识库问答为例,验证风险、方案和投入
前端·后端