每日一学V010: 从 Python 回到前端:一个 AI Native 开发者的 JavaScript 底层基础补全

从 Python 回到前端:一个 AI Native 开发者的 JavaScript 底层基础补全

v009 学了 Python------从 Model Scope 到 NoteBook,从 List 切片到 LLM API 调用,一天之内完成了"Python 入门 → AI 工程认知"的螺旋。

今天,v010,我回到 JavaScript。

但不是写业务代码,不是搭落地页(v003),不是做全栈项目(v006)。今天做了一件更底层的事:理解 JavaScript 的变量声明机制。

学的是 ES6 的 varletconst,以及作用域、变量提升这些"基础中的基础"。听起来很枯燥,但学完之后有两个收获:

第一,我终于理解了 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 年发布)是一个转折点。letconst、箭头函数、模板字符串、解构赋值------ES6 让 JavaScript 从"玩具语言"变成了能支撑企业级开发的工程语言。但 ES6 不是推翻重来,而是在保持向后兼容的基础上"打补丁"------letconst 就是对 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

widthsetWidth() 内部声明,出了函数就访问不到了。但函数内部可以访问外部的 height------这就是作用域的"嵌套"特性。

块级作用域(Block Scope)------ ES6 新增

{ } 包裹的代码块,形成一个独立的作用域。letconst 支持块级作用域,var 不支持。

javascript 复制代码
{
  const name = "张三"
}
console.log(name)  // ❌ ReferenceError: name is not defined

name{ } 内部声明,出了花括号就访问不到了。

变量查找规则:冒泡机制

当代码中引用一个变量时,JS 引擎会按以下顺序查找:

  1. 先查当前作用域 → 找到了,直接用
  2. 找不到?向外层作用域查找 → 一层一层往外"冒泡"
  3. 直到全局作用域 → 还没有?报错:ReferenceError: xxx is not defined

这就是为什么函数内部能访问全局变量------width 在函数作用域找不到,往外冒泡到全局作用域,找到了 height

v006 聊模块化时说"每一个模块都有自己的边界"。作用域就是变量的"模块边界"------变量属于哪个作用域,就在哪个范围内可见。理解作用域,就是理解变量的"管辖范围"。

三、var 的"历史包袱":不支持块级作用域

var 是 ES5 的变量声明方式,只有全局作用域和函数作用域,没有块级作用域

这意味着什么?在 iffor 等代码块中用 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 执行代码有两个阶段:

  1. 编译阶段 :扫描代码,准备执行上下文(变量环境),把 var 声明"提升"到作用域顶部
  2. 执行阶段:逐行执行代码

变量提升就是编译阶段的产物。

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 的 letconst 从语言层面修复了这个问题。这就是为什么现代 JavaScript 开发推荐永远不用 var,只用 letconst

六、从底层理解语言设计: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"。今天补上后半句:不管用哪种语言,都要理解它的底层设计------知道它的"好"是怎么来的,知道它的"瑕疵"为什么要这样修。

结语

今天学到的不只是 varletconst 的语法区别。是三件事:

第一,作用域是变量的"边界"。 全局作用域、函数作用域、块级作用域------三层结构决定了变量在哪里可见。理解作用域,就是理解变量的"管辖范围"。

第二,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 开发者的双语能力,不只是"会用"两种语言,而是理解两种语言的底层设计------知道它们为什么这样设计,知道它们的瑕疵在哪里,知道怎么规避。

下篇见。

相关推荐
之歆2 小时前
Day21_电商详情页核心技术实战:从LESS预处理到复杂交互实现
开发语言·前端·javascript·css·交互·less
海鸥两三2 小时前
基于 Vue 3 + 高德地图的网格规划系统实战(有源码)
前端·javascript·vue.js
逸A2 小时前
某里v2反混淆 codec 化路上踩到的两个隐蔽坑:被清零的 salt 与 opaque loop bound
javascript·人工智能·目标跟踪
丷丩2 小时前
MapLibre GL JS第11课:获取鼠标指针坐标
前端·javascript·gis·地图·mapbox·maplibre gl js
就叫_这个吧2 小时前
JavaScript基础数据类型、运算符、数组、函数的定义及DOM方式应用
开发语言·前端·javascript
作业逆流成河2 小时前
别再一次性重构枚举了:如何把一个真实后台项目的状态字典,渐进式迁移到enum-plus?
前端·javascript·开源
ct9783 小时前
TypeScript 中的泛型
前端·javascript·typescript
qq_420362033 小时前
前端国际化方案
前端·javascript·vue.js·国际化·reactjs
向上的车轮3 小时前
React 19 快速入门:拥抱服务端组件与新特性的现代化开发
前端·javascript·react.js