JavaScript
原始素材:
3-浏览器与前端/3.01-javascript-deep-dive.md
前言
在 vibecoding 里,AI 会帮你写大部分代码。但你至少得能看懂代码在做什么,否则 AI 写错了你也发现不了。读完这篇,你就能:
- 读懂 AI 写的代码在做什么
- 看出代码哪里有问题
- 用清晰的话告诉 AI 怎么改
这篇文章会带你学什么?
| 章节 | 内容 | 学完能干嘛 |
|---|---|---|
| 第 1 章 | JavaScript 是什么 | 明白它在网页里扮演什么角色 |
| 第 2 章 | 数据与变量 | 知道程序怎么存东西、怎么用东西 |
| 第 3 章 | 函数与逻辑 | 看懂代码的判断、循环和复用逻辑 |
| 第 4 章 | DOM 与事件 | 知道代码怎么控制网页、怎么响应用户操作 |
| 第 5 章 | 实战技巧 | 拿到 AI 代码怎么读、遇到报错怎么说 |
每一章都从"能识别代码"开始,不需要你会手写。遇到不懂的代码,随时回来查就行。
1. JavaScript 是什么
为什么网页需要 JavaScript? HTML 和 CSS 已经能让网页有内容、有样式了,为什么还要学一门新语言?
1.1 从"静态网页"到"动态应用"
📄 没有 JavaScript 的网页
- 内容固定,无法交互
- 填写表单提交不了
- 页面不会自动更新
就像一张纸质海报,只能看
🚀 有 JavaScript 的网页
- 输入文字实时搜索
- 滚动自动加载内容
- 数据实时更新显示
就像一个真正的应用程序
用一句话理解三者的关系:
| 技术 | 比喻 | 作用 |
|---|---|---|
| HTML | 骨架 | 定义网页的结构和内容 |
| CSS | 皮肤 | 定义网页的外观和样式 |
| JavaScript | 肌肉和神经系统 | 让网页能响应、能交互、能思考 |
1.2 为什么 vibecoding 也需要懂 JavaScript?
刚学 JS 的开发者踩坑记
他问 AI 为啥没效果,AI 解释了一通,但他看不懂代码里的 count = count + 1 是什么意思,也不知道 AI 改的是不是这个地方。只能反复说"加 2 没效果",AI 又改了好几版,有的把初始值改成 2,有的在完全不相关的地方加了 2。
最后他看了第 2 章"变量"的概念,明白了 count = count + 1 是在把 count 的值加 1 再存回去。然后他对 AI 说:"把 count + 1 改成 count + 2。"
一次就改对了。
这就是为什么要懂 JavaScript——不是为了手写代码,而是为了在 AI 没改对时,你能一眼看出问题在哪,一句话说到点子上。
1.3 先睹为快:一段真实的 AI 代码
在深入学习之前,让我们先看一段 AI 生成的真实代码。不要担心看不懂,只要有个印象,后面我们会逐一讲解每个部分。
// 定义一组颜色
const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4']
let currentIndex = 0
// 找到页面上的按钮
const button = document.querySelector('#changeBtn')
currentIndex = (currentIndex + 1) % colors.length
document.body.style.backgroundColor = colors[currentIndex]
})
这段代码在做什么?
| 代码 | 作用 | 对应章节 |
|---|---|---|
const colors = [...] |
定义一组颜色数据 | 第 2 章:数组 |
let currentIndex = 0 |
记录当前显示第几个颜色 | 第 2 章:变量 |
document.querySelector(...) |
找到页面上的按钮 | 第 4 章:DOM 查找 |
你不需要现在就理解每一行代码。只要记住:JavaScript 代码就是一系列指令,告诉浏览器"当用户做某事时,应该发生什么"。
2. 数据篇:变量与数据类型
程序是怎么"记住"东西的? 用户输入的内容、从服务器获取的数据、计算过程中的中间结果——这些信息都存在哪里?
2.1 变量:给数据起个名字
变量就像一个有标签的盒子 ——你可以把数据放进去,以后通过标签来取用。
const name = "张三" // 名字不会变,用 const
let age = 25 // 年龄可能会变,用 let
为什么要区分 const 和 let?
想象一下:你的身份证号码(const)这辈子都不会变,但你的年龄(let)每年都会变。JavaScript 让你用不同的关键字来表达这种"变与不变"的意图。
| 关键字 | 能否修改 | 使用场景 | 示例 |
|---|---|---|---|
const |
❌ 不能 | 值不会变的数据 | 身份证号、配置项、颜色列表 |
let |
✅ 能 | 值会变化的数据 | 计数器、当前选中的选项、用户输入 |
2.2 数据类型:JavaScript 里的几种"东西"
JavaScript 把数据分成几种类型,最常用的有三种:
| 类型 | 说明 | 示例 | 实际场景 |
|---|---|---|---|
string(字符串) |
文本内容 | "hello", '你好' |
用户名、商品描述、提示信息 |
number(数字) |
数值 | 42, 3.14 |
价格、数量、评分 |
boolean(布尔值) |
是/否 | true, false |
是否登录、是否完成、是否可见 |
还有两个特殊值需要知道:
undefined→ 变量声明了,但还没给值null→ 故意设为空(表示"这里没有值")
在 AI 代码里,你经常会看到用反引号(```)包裹的字符串,里面还有 ${...}:
const name = "张三"
const age = 25
// 传统写法(麻烦)
const message = "我叫" + name + ",今年" + age + "岁"
// 模板字符串(简洁)
const message = `我叫${name},今年${age}岁`
// 结果:"我叫张三,今年25岁"
识别要点 :看到反引号和 ${},就知道是在把变量插入到文本中。
2.3 对象和数组:把数据组织起来
对象 = 一组有名字的属性 (像一张个人信息表)
const user = {
name: "张三",
age: 25,
isVIP: true
}
// 使用点号访问属性
console.log(user.name) // "张三"
console.log(user.age) // 25
数组 = 一组有顺序的数据 (像一个列表)
const colors = ['红色', '绿色', '蓝色']
// 用索引访问(从 0 开始)
console.log(colors[0]) // "红色"
console.log(colors[1]) // "绿色"
嵌套结构:对象里套数组、数组里套对象
这是 AI 代码中最常见的数据结构:
const todos = [
{ id: 1, text: "学习 JavaScript", done: false },
{ id: 2, text: "做项目", done: true },
{ id: 3, text: "写文档", done: false }
]
// 访问:先取数组的第 0 项,再取它的 text 属性
console.log(todos[0].text) // "学习 JavaScript"
- 看到
{}→ 这是一个对象,里面是一组名字: 值 - 看到
[]→ 这是一个数组,里面是一组按顺序排列的值 - 看到
data[0].name→ 先取数组第 0 项,再取它的 name 属性
2.4 值与引用:一个容易踩的坑
这是新手最常遇到的问题之一!
基本类型(string、number、boolean)赋值 = 复制一份全新的数据:
let a = 10
let b = a // b 得到 a 的副本
b = 20
console.log(a) // 10(a 不受影响)
对象和数组赋值 = 复制的是"地址"(指向同一个东西):
let user1 = { name: "张三" }
let user2 = user1 // user2 指向同一个对象
user2.name = "李四" // 修改 user2 会影响 user1
console.log(user1.name) // "李四"(user1 也变了!)
为什么要创建副本?
在 React/Vue 中,直接修改数据会导致界面不更新。所以 AI 代码里经常看到 [...array] 或 {...obj}——它在创建副本,避免互相影响。
// 用展开运算符创建副本
const arr1 = [1, 2, 3]
const arr2 = [...arr1] // 创建新数组
arr2.push(4)
console.log(arr1) // [1, 2, 3](不受影响)
console.log(arr2) // [1, 2, 3, 4]
2.5 解构与展开:现代 JavaScript 的快捷写法
这两个语法在 AI 代码里到处都是,不认识就读不懂代码。
解构赋值:从对象或数组里快速提取数据
const user = { name: "张三", age: 25, city: "北京" }
// 传统写法(麻烦)
const name = user.name
const age = user.age
// 解构写法(简洁)
const { name, age } = user
// 效果一样,但一行搞定
展开运算符:复制并扩展数据
// 复制数组并添加新元素
const arr1 = [1, 2, 3]
const arr2 = [...arr1, 4, 5] // [1, 2, 3, 4, 5]
// 复制对象并添加新属性
const user1 = { name: "张三", age: 25 }
const user2 = { ...user1, city: "北京" }
// { name: "张三", age: 25, city: "北京" }
- 看到
const { name, age } = person→ 从 person 对象里提取 name 和 age - 看到
...array或...obj→ 把数组或对象展开铺平 - 你不需要能手写,但必须能读懂
3. 逻辑篇:函数与流程控制
代码是怎么"做决定"和"重复做事"的? 程序需要根据条件执行不同的操作,也需要重复执行某些任务——这些逻辑怎么表达?
3.1 条件判断:如果...就...否则...
if/else:最基本的条件判断
const age = 18
if (age >= 18) {
console.log("成年人")
} else {
console.log("未成年")
}
三元运算符:简写的 if/else
// 完整写法(4 行)
let message
if (age >= 18) {
message = "成年人"
} else {
message = "未成年"
}
// 三元运算符(1 行)
const message = age >= 18 ? "成年人" : "未成年"
// 格式:条件 ? 条件为真时的值 : 条件为假时的值
& & 短路写法:React 代码里常见
// 只有 isLoggedIn 为 true 时才显示用户面板
isLoggedIn && <UserPanel />
// 等价于
if (isLoggedIn) {
return <UserPanel />
}
- 看到
? :→ 这是三元运算符,简写的 if/else - 看到
&&→ 前面为 true 才执行后面
3.2 函数:把操作打包起来
函数 = 一道菜的配方
- 定义函数 = 写下配方
- 调用函数 = 按配方做菜
- 参数 = 原料
- 返回值 = 成品
// 定义函数(写下配方)
function greet(name) {
return "Hello " + name
}
// 调用函数(按配方做菜)
console.log(greet("张三")) // "Hello 张三"
console.log(greet("李四")) // "Hello 李四"
三种写法,一眼识别:
// 1. function 声明(传统写法)
function greet(name) {
return "Hello " + name
}
// 2. 箭头函数(AI 代码里用得最多)
const greet = (name) => {
return "Hello " + name
}
// 3. 箭头函数简写(只有一行时)
const greet = (name) => "Hello " + name
- 看到
function或=>→ 这是一个函数 - 看到
fn()→ 在调用这个函数
3.3 数组方法:处理列表的利器
在 React/Vue 里,几乎每个列表渲染都会用到这些方法。
const todos = [
{ id: 1, text: "学习", done: false },
{ id: 2, text: "工作", done: true }
]
// .map():把数组的每一项变成另一个东西
const texts = todos.map(todo => todo.text)
// ["学习", "工作"]
// .filter():筛选出符合条件的项
const unfinished = todos.filter(todo => !todo.done)
// [{ id: 1, text: "学习", done: false }]
// .find():找到第一个符合条件的项
const found = todos.find(todo => todo.id === 1)
// { id: 1, text: "学习", done: false }
- 看到
.map()→ 对数组做变换,返回新数组 - 看到
.filter()→ 筛选数组 - 看到
items.map(item => <li>{item.name}</li>)→ 把每个数据项变成列表标签
3.4 作用域:变量的"可见范围"
用"房间"比喻:
- 函数内部的变量就像房间里的东西,外面看不到
- 但房间里的人可以看到走廊(外层作用域)的东西
const global = "全局变量" // 走廊里的东西
function room() {
const local = "房间里的东西" // 房间里的东西
console.log(global) // ✅ 能看到走廊
}
console.log(local) // ❌ 报错!外面看不到房间里的东西
核心直觉: 代码写在哪里,决定了它能看到什么变量。
🔍 作用域:变量的"可见范围"
作用域链可视化:全局作用域 | 函数 greet() 作用域 | if 块作用域
| 作用域层级 | 可访问变量 | 来源 |
|---|---|---|
| 🌐 全局作用域 | appName = "Todo" |
全局声明 |
| 📦 函数 greet() 作用域 | appName + message = "你好" |
全局 ↑ + 函数内 |
| 📁 if 块作用域 | appName + message + greeting = message + appName |
全局 ↑ + 函数 ↑ + 块内 |
在全局作用域,只能使用全局变量
appName;内层可访问外层变量,反之不行(作用域链单向查找)。
对应代码
const appName = "Todo" // 全局作用域
function greet() {
const message = "你好" // 函数作用域
if (true) {
const greeting = message + appName // 块级作用域
console.log(greeting)
}
console.log(greeting) // ❌ 报错!外层看不到内层
}
3.5 闭包:函数"记住"了它诞生时的环境
不要把它当成独立的概念,从一个具体场景理解:
function setupCounter() {
let count = 0 // 这个变量在函数内部
return {
}
}
const counter = setupCounter()
console.log(counter.add()) // 1
console.log(counter.add()) // 2
console.log(counter.getCount()) // 2
核心直觉: 函数在被创建时,会"记住"它周围的变量,即使外层函数已经执行完了。
3.6 this:函数被谁调用
不讲复杂的绑定规则,只讲最常见的场景:
场景 1:在对象的方法里,this 指向这个对象
const user = {
name: "张三",
sayHi() {
console.log("你好,我是" + this.name) // this 指向 user
}
}
user.sayHi() // "你好,我是张三"
场景 2:在事件监听里,this 指向触发事件的元素
button.addEventListener('click', function() {
console.log(this) // this 指向 button 元素
})
// 但箭头函数不会改变 this
console.log(this) // this 指向外层的 this
})
如果 AI 代码里出现 this 相关的 bug(比如 Cannot read property of undefined),告诉 AI:"这个方法里的 this 指向不对,改成箭头函数或者用 bind"
4. 交互篇:DOM、事件与异步
4.1 DOM:JavaScript 看到的网页
网页在 JavaScript 眼里是一棵"树",每个 HTML 标签都是树上的一个"节点"。
<html>
<body>
<h1>标题</h1>
<p>段落</p>
<ul>
<li>项目1</li>
<li>项目2</li>
</ul>
</body>
</html>
JS 操控网页 = 找到节点 + 修改节点 + 创建/删除节点
DOM 树:JavaScript 看到的网页
4.2 查找与修改元素
查找元素:
// 根据 CSS 选择器查找(最常用)
const title = document.querySelector('h1') // 找第一个 h1
const button = document.querySelector('#btn') // 找 id="btn" 的元素
const items = document.querySelectorAll('.item') // 找所有 class="item" 的元素
修改元素:
// 改文字
title.textContent = "新标题"
// 改样式
element.style.color = "red"
element.style.fontSize = "20px"
// 改 CSS 类
element.classList.add('active') // 添加类
element.classList.remove('hidden') // 移除类
element.classList.toggle('open') // 切换类(有就移除,没有就添加)
- 看到
document.querySelector→ 在查找网页元素 - 看到
.textContent→ 改文字 - 看到
.style.xxx→ 改样式 - 看到
.classList.add/remove/toggle→ 改 CSS 类
4.3 事件:当用户做了某个操作时...
addEventListener:给元素添加事件监听
})
常见事件:
| 事件 | 触发时机 | 实际场景 |
|---|---|---|
input |
输入框内容变化 | 实时搜索、表单验证 |
submit |
表单提交 | 登录、注册、提交数据 |
scroll |
滚动页面 | 懒加载、回到顶部 |
事件对象:获取更多信息
input.addEventListener('input', (e) => {
console.log(e.target.value) // 获取输入框的值
e.preventDefault() // 阻止默认行为(比如表单提交后刷新页面)
})
4.4 异步:为什么有些操作不是立刻完成的
餐厅比喻:
点菜后不用站在厨房门口等,可以先做别的事,菜好了服务员会端过来。
最常见场景:从服务器获取数据
// 同步写法(会卡住页面,不要用)
const data = fetch('/api/data') // ❌ 这样写会卡住
// 异步写法(正确)
async function loadData() {
try {
const response = await fetch('/api/data')
const data = await response.json()
console.log(data)
} catch (error) {
console.error('出错了:', error)
}
}
async/await 语法:
-
async→ 标记这个函数里有异步操作 -
await→ 等待这个操作完成(但不会卡住页面) -
try/catch→ 处理可能出现的错误 -
看到
async/await→ 在等待耗时操作 -
看到
fetch()→ 在从服务器获取数据 -
看到
try/catch→ 在处理可能的错误
4.5 事件循环:JavaScript 到底怎么工作的
不用术语"微任务/宏任务",用一个简单的模型理解:
JS 是一个"单人工位" ,同时只做一件事,但有一个"待办便签栏"(任务队列)。
当遇到要等待的操作(网络请求、定时器),JS 不是傻等,而是把"等好了之后做什么"贴到便签栏,自己继续往下执行。等当前事情做完了,才去看便签栏。
console.log("1")
console.log("3")
// 输出:1, 3, 2(不是 1, 2, 3!)
为什么?
- 执行
console.log("1")→ 输出 1 - 遇到
setTimeout→ 把回调贴到便签栏,继续往下 - 执行
console.log("3")→ 输出 3 - 当前代码执行完了,去看便签栏
- 执行
setTimeout的回调 → 输出 2
事件循环:JavaScript 的执行机制
代码队列(事件循环演示)
代码:1. console.log("1") 2.(空) 3. console.log("3") 4.(空) 5. console.log("5")
| 组件 | 状态 | 说明 |
|---|---|---|
| 🏭 工位(单线程) | 执行 console.log("1") |
正在执行 |
| 📝 便签栏(任务队列) | 暂无待办任务 | 异步任务在此排队 |
| 📋 输出日志 | 还未开始 | 执行顺序 ≠ 书写顺序 (1,2,3,4,5) |
代码从上到下写的,但执行顺序不一定从上到下——异步操作会被"推迟"到当前代码执行完之后。
💡 如果 AI 代码里数据还没获取到页面就渲染了,告诉 AI:"数据还没加载完就开始渲染了,需要添加 loading 状态,等数据到了再渲染"
4.6 模块:import 和 export
AI 生成的 React/Vue 代码第一行几乎都是 import。
import = 从别的文件引入功能
// 从工具文件引入函数
import { formatDate } from './utils'
// 从第三方包引入
import React from 'react'
import { useState } from 'react'
export = 把功能暴露出去给别人用
// utils.js
export function formatDate(date) {
// ...
}
// 或者默认导出
export default function formatDate(date) {
// ...
}
npm 包 = 别人写好的工具,安装后就能用
// 安装包:npm install lodash
// 使用包
import _ from 'lodash'
- 看到
import→ 从别的文件引入功能 - 看到
export→ 把功能暴露给别人用 - 看到
from 'react'→ 从 React 包引入 - 看到
from './utils'→ 从本地文件引入
5. 实战篇:读懂代码、看懂报错、精准描述
前面学了这么多语法,实际拿到 AI 代码时怎么用? 怎么快速读懂代码?遇到报错怎么办?怎么让 AI 准确地帮你改代码?
5.1 拿到 AI 代码后怎么读
四步法:
| 步骤 | 看什么 | 示例 |
|---|---|---|
| 第一步:看整体结构 | 有几个函数?分别做什么? | loadData() 加载数据,renderList() 渲染列表 |
| 第三步:追踪数据流 | 数据从哪里来?到哪里去? | 从 API 获取 → 解析 → 渲染到页面 |
| 第四步:看细节逻辑 | 具体函数里怎么处理的? | 循环、判断、计算 |
// 第一步:整体结构
// - 一个颜色数组
// - 一个变量记录当前索引
// 第二步:入口点
// 第三步:数据流
// colors(颜色数组)→ currentIndex(当前索引)→ backgroundColor(背景色)
// 第四步:细节逻辑
// currentIndex = (currentIndex + 1) % colors.length
// 这个公式的意思:每次 +1,但不超过数组长度(循环)
5.2 常见报错速查
| 报错 | 大白话解释 | 怎么跟 AI 说 |
|---|---|---|
TypeError: Cannot read properties of undefined |
你想从一个不存在的东西上取值 | "第 X 行报错,某某变量是 undefined,检查它的赋值逻辑" |
ReferenceError: xxx is not defined |
用了一个没有声明过的变量名 | "变量 xxx 没有定义,是不是拼写错了或者忘了导入" |
TypeError: xxx is not a function |
把一个不是函数的东西当函数调用了 | "xxx 不是函数,检查一下它的类型和来源" |
SyntaxError: Unexpected token |
语法写错了(括号不匹配、少了逗号等) | "第 X 行语法错误,检查括号和标点" |
CORS error |
浏览器阻止了跨域请求 | "遇到 CORS 错误,需要配置跨域资源共享" |
404 Not Found |
请求的资源不存在 | "API 返回 404,检查接口地址是否正确" |
5.3 如何精准描述问题
新手和熟练开发者的差距,往往就体现在描述问题的精准度 上。
| ❌ 差的描述 | ✅ 好的描述 |
|---|---|
| "样式不对" | "标题应该居中,现在是左对齐" |
| "数据显示不出来" | "fetch 请求返回了数据(控制台能看到),但页面没有重新渲染" |
| "加一个功能" | "在用户列表页面添加一个搜索框,输入时实时过滤列表,按 name 字段模糊匹配" |
一个实战练习:
// 有 bug 的代码
function deleteTodo(index) {
todos.splice(index, 1) // 总是删除最后一项
}
// 错误现象:无论点哪个删除按钮,删的都是最后一项
❌ 差的描述: "删除功能有 bug"
5.4 你现在应该能识别的代码
- 看到
const/let→ 知道变量能不能重新赋值 - 看到
{}→ 对象 / 看到[]→ 数组 - 看到
{...obj}或[...arr]→ 在创建副本 - 看到
function或=>→ 定义了一段可重复执行的操作 - 看到
if/else或? :→ 代码在做判断 - 看到
.map()/.filter()→ 在变换或筛选数组 - 看到
document.querySelector→ 在查找网页元素 - 看到
addEventListener→ 在监听用户操作 - 看到
async/await→ 在等待耗时操作 - 看到
import/export→ 在引入或导出模块 - 遇到报错 → 能读懂大意并精准描述给 AI
如果你认真读了每章的"深入"部分,你还掌握了这些核心概念:
- 值 vs 引用 :基本类型复制值,对象/数组复制的是地址
- 作用域与闭包 :函数能"记住"它诞生时周围的变量
- this 的本质 :取决于函数被谁调用,而不是写在哪里
- 事件循环 :JS 是单线程的,靠任务队列实现"不阻塞"
这些概念会帮你更快定位问题。
- "第 X 行报错 XXX,帮我看看是什么问题"
- "这个函数的逻辑是 XXX,但结果不对,应该是 XXX"
- "我想修改 XXX 功能,具体要求是 XXX"