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 流式处理,都会有一种「原来如此」的通透感。
目录
- JavaScript 是什么、在哪跑(V8、执行上下文、调用栈)
- 值与类型:7 种原始类型 + 对象(
typeof陷阱、装箱) - 类型转换(coercion):
==vs===、falsy、隐式转换 - 变量、作用域与执行上下文(词法作用域)
- 闭包(Closure):函数为什么记得外部变量
- 引用与值传递(对象按引用、浅拷贝 / 深拷贝)
- 函数:一等公民、高阶函数、回调
this的绑定规则(调用点、call/apply/bind、箭头函数)- 原型与继承(
[[Prototype]]、new做了什么、class语法糖) - 内置对象与方法(Object、Array、String、JSON)
- 异步与事件循环(回调、微任务 / 宏任务、Promise 状态机)
- 错误处理(
try/catch、Error、finally、异步错误) - 模块化(脚本、CommonJS、ESM)
- 常见陷阱与调试
- 整体串联: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 的核心,而不是死记输出。