从 Python 回到前端:一个 AI Native 开发者的 JavaScript 底层基础补全
v009 学了 Python------从 Model Scope 到 NoteBook,从 List 切片到 LLM API 调用,一天之内完成了"Python 入门 → AI 工程认知"的螺旋。
今天,v010,我回到 JavaScript。
但不是写业务代码,不是搭落地页(v003),不是做全栈项目(v006)。今天做了一件更底层的事:理解 JavaScript 的变量声明机制。
学的是 ES6 的 var、let、const,以及作用域、变量提升这些"基础中的基础"。听起来很枯燥,但学完之后有两个收获:
第一,我终于理解了 for + setTimeout 那个经典面试题的底层原理------不是"记住答案",而是从作用域和执行机制的角度理解"为什么会这样"。
第二,我看到了 JavaScript 和 Python 在语言设计哲学上的根本差异------JS 是一周赶工出来的"KPI 项目",Python 是追求"人生苦短"的简洁主义。两种设计选择,各有取舍。
这篇文章聊三个东西:① 作用域的三层结构 ② var/let/const 的本质区别 ③ 变量提升的底层机制。比前九篇更"底层",但底层的东西,才是真正决定你能走多远的东西。
一、JavaScript 的"出身":一个一周赶工的 KPI 项目
要理解 var 的"问题",先得理解 var 的"出身"。
1995 年,网景公司(Netscape)需要一种能在浏览器里运行的脚本语言,给网页添加交互------幻灯片切换、表单验证、动态内容。他们找到了 Brendan Eich,给了他一周时间。
一周。
一周设计出来的语言,能有多少深思熟虑?var 就是在这种背景下诞生的。它没有块级作用域,因为当时没人想到 JS 会用来写大型项目;变量提升是编译器实现的副产品,不是有意为之的设计决策。
JS 还蹭了一波 Java 的热度------名字叫 JavaScript(其实和 Java 没关系),语法也借鉴了 Java 的花括号和分号。这导致很多人以为 JS 是 Java 的"轻量版",实际上两者的底层逻辑完全不同。
ES6(2015 年发布)是一个转折点。let、const、箭头函数、模板字符串、解构赋值------ES6 让 JavaScript 从"玩具语言"变成了能支撑企业级开发的工程语言。但 ES6 不是推翻重来,而是在保持向后兼容的基础上"打补丁"------let 和 const 就是对 var 的补丁。
v009 说 Python 的设计哲学是"信任程序员,少写废话"。JS 的设计哲学更接近"先把东西做出来,问题以后再说"。两种哲学各有代价:Python 的灵活性带来了类型安全问题,JS 的快速上线带来了 var 这样的历史包袱。
理解一门语言的历史,比学会它的语法更重要。 因为历史决定了它的"瑕疵"在哪里,而"瑕疵"决定了你需要什么样的工程实践来规避问题。
二、作用域的三层结构:Global → Local → Block
作用域是什么?一句话:变量的"可见范围"------在哪个范围内,你能访问这个变量。
JavaScript 有三层作用域:
全局作用域(Global Scope)
在任何函数、代码块之外声明的变量,属于全局作用域。整个脚本的任何位置都能访问它。
javascript
var height = 1000
height 是全局变量,在任何地方都能读取。
函数局部作用域(Local Scope)
函数内部声明的变量,只在函数内有效。函数执行完毕后,局部变量的内存被垃圾回收机制释放。
javascript
function setWidth() {
var width = 5
console.log(width, height) // 5, 1000
}
setWidth()
console.log(width) // ❌ ReferenceError: width is not defined
width 在 setWidth() 内部声明,出了函数就访问不到了。但函数内部可以访问外部的 height------这就是作用域的"嵌套"特性。
块级作用域(Block Scope)------ ES6 新增
用 { } 包裹的代码块,形成一个独立的作用域。let 和 const 支持块级作用域,var 不支持。
javascript
{
const name = "张三"
}
console.log(name) // ❌ ReferenceError: name is not defined
name 在 { } 内部声明,出了花括号就访问不到了。
变量查找规则:冒泡机制
当代码中引用一个变量时,JS 引擎会按以下顺序查找:
- 先查当前作用域 → 找到了,直接用
- 找不到?向外层作用域查找 → 一层一层往外"冒泡"
- 直到全局作用域 → 还没有?报错:
ReferenceError: xxx is not defined
这就是为什么函数内部能访问全局变量------width 在函数作用域找不到,往外冒泡到全局作用域,找到了 height。
v006 聊模块化时说"每一个模块都有自己的边界"。作用域就是变量的"模块边界"------变量属于哪个作用域,就在哪个范围内可见。理解作用域,就是理解变量的"管辖范围"。
三、var 的"历史包袱":不支持块级作用域
var 是 ES5 的变量声明方式,只有全局作用域和函数作用域,没有块级作用域。
这意味着什么?在 if、for 等代码块中用 var 声明的变量,会"泄漏"到外部作用域。
最经典的问题是 for + setTimeout:
javascript
for (var i = 0; i < 10; i++) {
console.log(i) // 同步代码:0, 1, 2, ... 9
setTimeout(() => {
console.log(`This number is ${i}`) // 异步代码:10 个 10
}, 1000)
}
直觉告诉我们,setTimeout 应该打印 0, 1, 2, ... 9。但实际上打印的是 10 个 10。
为什么?
因为 var i 只有一个变量。for 循环是同步代码,i 依次变成 0, 1, 2, ... 9, 10。但 setTimeout 是异步代码------它要等 1 秒后才执行。1 秒后 for 循环早就跑完了,i 已经是 10。所以 10 个 setTimeout 回调访问的都是同一个 i,值都是 10。
这不是 bug,是 var 的设计缺陷------它不支持块级作用域,所以 for 循环里的 var i 实际上是全局的 i。
v008 聊数组去重时说"JS 的类型系统需要更多技巧来规避问题"。var 的作用域问题也一样------它不是不能用,而是需要额外的心智负担来处理。ES6 的 let 就是为此而生的。
四、let 和 const:ES6 的"补丁"
let:块级作用域变量
let 修复了 var 的最大问题------支持块级作用域。
javascript
for (let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(`This number is ${i}`)
}, 1000)
}
// 输出:0, 1, 2, ... 9
为什么 let 就对了?
因为 let 支持块级作用域,每次循环都会创建一个新的 i。第 1 次循环的 i 是 0,第 2 次循环的 i 是 1......它们是 10 个不同的变量,不是同一个。所以 setTimeout 回调访问的是各自循环轮次的 i,值分别是 0, 1, 2, ... 9。
let 的其他特性:
- 可以先声明后赋值:
let a; a = 5 - 值和类型都可以改变(但不推荐改类型)
const:块级作用域常量
const 声明的变量不能重新赋值。但有一个重要细节:简单数据类型和复杂数据类型的"不可变"含义不同。
javascript
// 简单数据类型 ------ 值不可变
const PI = 3.14
PI = 3.15 // ❌ Assignment to constant variable
// 复杂数据类型 ------ 值可变,类型不可变
const person = { name: '张三', age: 18 }
person.age++ // ✅ 可以修改属性
person = {} // ❌ 不能重新赋值(改变引用)
为什么复杂数据类型可以修改属性?
因为 const 锁定的是变量的引用 (指针),不是变量的值。person 指向一个对象,const 确保 person 永远指向这个对象,但对象内部的属性可以随意修改。这就像你锁定了一个房间号(引用),但房间里的人可以进出(属性变化)。
const 的使用原则:如果你确定一个变量不会被重新赋值,就用 const。 这是一种代码规范------告诉阅读代码的人"这个值不会变"。
五、变量提升(Hoisting):JS 的"反直觉"设计
JavaScript 执行代码有两个阶段:
- 编译阶段 :扫描代码,准备执行上下文(变量环境),把
var声明"提升"到作用域顶部 - 执行阶段:逐行执行代码
变量提升就是编译阶段的产物。
javascript
console.log(pizza) // undefined(不是报错!)
var pizza = 'Deep Dish'
直觉上,pizza 还没声明就访问,应该报错。但实际上输出 undefined。为什么?
因为编译阶段,var pizza 被提升到了作用域顶部,等价于:
javascript
var pizza = undefined // 编译阶段
console.log(pizza) // 执行阶段:undefined
pizza = 'Deep Dish' // 赋值
var pizza 的声明被提升了,但赋值没有提升。所以第一次 console.log(pizza) 看到的是 undefined,而不是 'Deep Dish'。
let 修复了这个问题:
javascript
console.log(dog) // ❌ ReferenceError: Cannot access 'dog' before initialization
let dog = 'Pug'
let 不支持变量提升------在声明之前访问,直接报错。这更符合直觉:你还没声明的变量,就不应该能访问。
为什么说变量提升是"不好的东西"?
因为它和代码的书写顺序、程序员的直觉不一致。你写了 console.log(pizza) 在 var pizza 之前,直觉上应该报错,但实际上返回 undefined。这种"反直觉"的行为容易产生隐蔽的 bug,尤其是代码量大的时候。
ES6 的 let 和 const 从语言层面修复了这个问题。这就是为什么现代 JavaScript 开发推荐永远不用 var,只用 let 和 const。
六、从底层理解语言设计:JS vs Python 的设计哲学
v009 学 Python 时,我被它的简洁震撼了------没有花括号、没有分号、缩进就是语法。v010 学 JS 的变量声明,我被它的"历史包袱"震撼了------var 的作用域问题、变量提升的反直觉行为。
两种语言的设计哲学完全不同:
| 维度 | JavaScript | Python |
|---|---|---|
| 设计时间 | 一周赶工 | 多年迭代 |
| 设计目标 | 浏览器脚本 | 通用编程语言 |
| 变量声明 | var/let/const(三种) | 直接赋值(一种) |
| 作用域 | ES6 后才完善 | 天然支持 |
| 变量提升 | var 有,let/const 没有 | 没有 |
| 类型系统 | 弱类型 | 弱类型但更严格 |
JS 选择了"快速上线",代价是 var 这样的历史包袱。Python 选择了"简洁优雅",代价是灵活性略低(比如没有 JS 那么自由的对象操作)。
但 ES6 的进化说明了一件事:语言会自我修正。 var 的问题不是不能解决------let 和 const 就是解决方案。Python 也在不断进化------类型注解(Type Hints)就是对"类型安全"问题的回应。
v009 说"做产品用 JS,做 AI 工程用 Python"。今天补上后半句:不管用哪种语言,都要理解它的底层设计------知道它的"好"是怎么来的,知道它的"瑕疵"为什么要这样修。
结语
今天学到的不只是 var、let、const 的语法区别。是三件事:
第一,作用域是变量的"边界"。 全局作用域、函数作用域、块级作用域------三层结构决定了变量在哪里可见。理解作用域,就是理解变量的"管辖范围"。
第二,let/const 是 ES6 对 var 的修正。 var 没有块级作用域,导致 for + setTimeout 这样的经典问题。let 修复了这个问题,const 增加了"不可变"约束。现代 JS 开发只用 let 和 const。
第三,变量提升是 JS 的历史包袱。 var 的声明被提升到作用域顶部,导致"未声明就访问"不报错而是返回 undefined。let 修复了这个问题。
回顾十篇文章的完整路径:
- v001-v004:AI 工具链(OPC → Prompt → Agent → CLI)
- v005-v006:工程基本功(Git → 模块化)
- v007:业务视角(FDE)
- v008-v009:编程基本功 + 语言扩展(数组去重 → Python + API)
- v010:JavaScript 底层基础(作用域 / var / let / const / 变量提升)
两条线在交替推进:v009 往下扎到 Python 基础和 AI 工程,v010 回到前端底层补 JavaScript 基本功。AI Native 开发者的双语能力,不只是"会用"两种语言,而是理解两种语言的底层设计------知道它们为什么这样设计,知道它们的瑕疵在哪里,知道怎么规避。
下篇见。