21 KiB
JavaScript 语言由浅入深
::: tip 前言 现代网页为什么可以像应用程序一样交互? 淘宝可以筛选商品、地图可以缩放平移、文档可以实时保存——这些功能背后的"控制逻辑"是什么?HTML 定义了网页的结构,CSS 负责视觉呈现,而 JavaScript 则是让网页"动起来"的编程语言。本章将带你理解 JavaScript 的核心工作机制,掌握实现网页交互的关键技术。 :::
1. JavaScript 是什么?
1.1 从"只能看"到"可以用":网页的进化
早期的网页就像一本电子杂志——你只能看,不能改。内容是固定的,你点击什么都不会改变。
但现代网页完全不同了。它们更像桌面软件:
- 在线文档可以像 Word 一样编辑
- 地图网站可以像 GPS 一样导航
- 聊天应用可以像微信一样实时收发消息
这种转变的核心技术就是 JavaScript——它让网页从"展示信息"变成了"可以交互的工具"。
1.2 JavaScript 在网页中的角色
想象你在经营一家网上商店:
| 层次 | 角色 | 做什么 |
|---|---|---|
| HTML | 店铺的"货架" | 摆放商品、标注价格 |
| CSS | 店铺的"装修" | 美化布局、调整颜色 |
| JavaScript | 店铺的"收银系统" | 处理订单、计算总价、更新库存 |
没有 JavaScript,网页就像一个没有收银员的商店——顾客只能看商品,但无法完成购买。
::: tip 💡 三者如何配合 HTML 搭建结构,CSS 负责美观,JavaScript 处理交互。三者缺一不可,但 JavaScript 是让网页"能用"的关键。
打个比方:HTML 是房子的框架,CSS 是装修风格,JavaScript 是让房子"智能"的电器系统。没有电器,房子仍然可以住人,但不会有现代生活的便利。 :::
1.2 一个真实的成长故事
::: warning 从踩坑到顿悟 小王是一名前端工程师,刚入职时只会简单的 DOM 操作。
有一次,他遇到一个 bug:修改数组中的某个元素后,页面上显示的却是旧数据。他花了整整一天调试,最后才发现 —— 原来数组是引用类型,直接修改元素不会触发 Vue 的响应式更新。
这个坑让他明白了一个道理: 不理解 JavaScript 的核心概念(比如引用类型、响应式原理),写出的代码就像在雷区跳舞,随时可能踩中看不见的坑。
从那以后,他开始系统地学习 JavaScript 的底层原理。半年后,他不仅能快速定位问题,还能写出性能更好的代码,甚至能帮团队解决复杂的技术难题。
深入理解一门语言,是成为高级工程师的必经之路。 :::
::: info 💡 核心启示 JavaScript 看起来简单,实则精妙。它的设计哲学和实现机制,值得每个前端开发者深入理解。掌握了这些,你不仅能写出更好的代码,还能更快地学习新技术(因为很多框架都是基于 JavaScript 的特性构建的)。 :::
2. JavaScript 的核心概念全景
::: tip 🗺️ 学习路线图 JavaScript 的知识点很多,但我们不必一次全学完。按照下面这个顺序,逐步掌握核心概念:
- 基础语法 → 变量、数据类型、操作符
- 函数与作用域 → 函数声明、闭包、this
- 对象与原型 → 对象创建、原型链、继承
- 异步编程 → 回调、Promise、async/await
- ES6+ 新特性 → let/const、箭头函数、解构等
本章节将聚焦于最核心、最容易出错的部分,帮你建立坚实的知识体系。 :::
JavaScript 的核心概念可以分为以下几个层次:
| 层次 | 包含内容 | 学习重点 | 实际应用 |
|---|---|---|---|
| 基础语法 | 变量、数据类型、操作符 | let/const/var、原始类型 vs 引用类型 | 避免常见的类型错误和变量泄漏 |
| 函数与作用域 | 函数声明、闭包、this | 作用域链、闭包机制、this 指向 | 理解函数的工作方式和数据私有化 |
| 对象与原型 | 对象创建、原型链、继承 | 原型链查找机制、继承方式 | 掌握面向对象编程和框架原理 |
| 异步编程 | 回调、Promise、async/await | 事件循环、Promise 链式调用 | 处理网络请求、动画等异步操作 |
| ES6+ 新特性 | 箭头函数、解构、模块 | 新语法的使用场景 | 写出更简洁、更现代的代码 |
::: tip 💡 从表格中你能看到什么? 基础语法 是所有其他知识的基础,必须掌握。
函数与作用域、对象与原型 是 JavaScript 的核心机制,理解了它们,你就理解了这门语言的灵魂。
异步编程 是 JavaScript 的特色,因为它是单线程的,必须依赖异步来处理耗时操作。
ES6+ 新特性 让代码更简洁,但本质都是基于前面提到的核心机制。
学习建议: 先打好基础(变量、类型、函数),再深入理解机制(作用域、原型、异步),最后学习新特性(ES6+)。这样循序渐进,不会感到混乱。 :::
3. 变量与数据类型
3.1 变量声明:let、const、var 的区别
在 JavaScript 中,有三种声明变量的方式:var、let、const。它们的区别看似简单,实则影响深远。
::: details 🤔 为什么有三种方式? 这是 JavaScript 的历史遗留问题。
var是 ES5 时代的产物,有"变量提升"和"没有块级作用域"的问题let和const是 ES6 新增的,解决了var的问题,更安全
现在推荐始终使用 const,需要重新赋值时才用 let,完全不要用 var。
:::
三个关键词的区别:
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 重复声明 | ✅ 可以 | ❌ 不可以 | ❌ 不可以 |
| 重新赋值 | ✅ 可以 | ✅ 可以 | ❌ 不可以 |
| 变量提升 | ✅ 提升 | ✅ 提升(但不可访问) | ✅ 提升(但不可访问) |
| 全局对象属性 | ✅ 是 | ❌ 否 | ❌ 否 |
| 推荐使用 | ❌ 不推荐 | ✅ 需要重新赋值时 | ✅ 默认首选 |
::: tip 💡 如何选择? 记住这个简单规则:
- 默认用
const:因为大多数变量不需要重新赋值,使用const更安全(防止意外修改) - 需要重新赋值时用
let:比如循环计数器、累加器 - 永远不要用
var:除非你在维护老项目
示例:
// ✅ 好的做法
const name = "张三" // 名字不会变
const age = 25 // 年龄不会变
let score = 0 // 分数会变化,用 let
score += 10 // ✓ 正确
// name = "李四" // ✗ 报错!const 不能重新赋值
// ❌ 不好的做法
var name = "张三" // 不要用 var
:::
👇 动手试试看: 下面这个演示展示了 let、const、var 的区别:
3.2 数据类型:原始类型 vs 引用类型
JavaScript 有两种数据类型:原始类型(Primitive)和引用类型(Reference)。理解它们的区别,能帮你避免很多莫名其妙的 bug。
::: tip 🎯 什么是"类型"? 简单来说,类型就是数据的"种类"。
- 原始类型是最基本的数据,比如数字、字符串、布尔值
- 引用类型是更复杂的数据结构,比如对象、数组、函数
它们的区别在于存储方式:原始类型存储"实际的值",引用类型存储"指向数据的地址"。 :::
七种原始类型:
| 类型 | 说明 | 示例 |
|---|---|---|
| Number | 数字(整数和小数) | 42, 3.14, NaN |
| String | 字符串(文本) | "hello", '你好' |
| Boolean | 布尔值(真/假) | true, false |
| Undefined | 未定义 | let x; // x 是 undefined |
| Null | 空值 | let x = null; |
| Symbol | 独一无二的值(ES6) | Symbol("id") |
| BigInt | 大整数(ES2020) | 9007199254740991n |
引用类型:
| 类型 | 说明 | 示例 |
|---|---|---|
| Object | 对象(键值对) | {name: "张三", age: 25} |
| Array | 数组(有序列表) | [1, 2, 3] |
| Function | 函数(可执行的代码) | function() {} |
| Date | 日期对象 | new Date() |
| RegExp | 正则表达式 | /^test$/ |
::: details 🔍 原始类型 vs 引用类型的区别 这是最容易踩坑的地方!
赋值时的区别:
// 原始类型:复制值
let a = 10
let b = a // b 得到 a 的副本
b = 20
console.log(a) // 10(a 不受影响)
// 引用类型:复制引用(地址)
let obj1 = {x: 10}
let obj2 = obj1 // obj2 指向同一个对象
obj2.x = 20 // 修改 obj2 会影响 obj1
console.log(obj1.x) // 20(obj1 也变了!)
为什么引用类型会这样?
因为引用类型存储的是"地址",而不是"实际的值"。当你把 obj1 赋值给 obj2 时,只是复制了地址,两个变量指向内存中的同一个对象。
实际影响:
- 函数参数传递时,引用类型可能会被修改
- 比较两个对象时,比较的是地址,不是内容
- 深拷贝 vs 浅拷贝的问题
解决方法:
- 如果需要复制对象,使用"深拷贝"(
JSON.parse(JSON.stringify(obj))或structuredClone(obj)) - 如果只需要复制第一层,使用"浅拷贝"(
Object.assign({}, obj)或{...obj}) :::
👇 动手试试看: 下面这个演示展示了原始类型和引用类型的区别:
4. 函数与闭包
4.1 函数是"一等公民"
在 JavaScript 中,函数是"一等公民"(First-Class Citizen)。这意味着函数可以:
- 被赋值给变量
- 作为参数传递给其他函数
- 作为返回值从函数中返回
- 存储在数据结构中(如数组、对象)
::: tip 🤔 什么是"一等公民"? "一等公民"是编程语言的一个术语,意思是某种东西可以像其他数据一样被使用。
在 JavaScript 中,数字、字符串是"一等公民",函数也是。这让 JavaScript 非常灵活。
在其他语言中(如 Java 8 之前),函数不是一等公民,你必须用对象或接口来包装它。 :::
函数的声明方式:
| 方式 | 语法 | 特点 | 使用场景 |
|---|---|---|---|
| 函数声明 | function name() {} |
会提升,可被提前调用 | 普通函数 |
| 函数表达式 | const name = function() {} |
不会提升 | 需要条件性创建函数 |
| 箭头函数 | const name = () => {} |
没有 this,更简洁 |
回调函数、简短函数 |
::: tip 💡 如何选择?
- 普通函数:用函数声明或函数表达式
- 回调函数:优先用箭头函数(更简洁)
- 需要
this指向调用者:不要用箭头函数(箭头函数没有自己的this)
示例:
// 函数声明
function greet(name) {
return "Hello " + name
}
// 函数表达式
const greet = function(name) {
return "Hello " + name
}
// 箭头函数(最简洁)
const greet = name => "Hello " + name
:::
4.2 闭包:函数"记住"了它的出生环境
闭包(Closure)是 JavaScript 中最重要、也最容易被误解的概念之一。
::: tip 🎯 什么是闭包? 简单来说: 闭包是函数和它的词法环境的组合。
更直白地说: 内部函数可以访问外部函数的变量,即使外部函数已经执行完毕。
打个比方: 你出门时背了个背包,把当时看到的东西装进包里。即使你走了很远的路,依然可以从包里拿出当时装的东西 —— 这个背包就是"闭包"。 :::
闭包的实际应用:
- 数据私有化:模拟私有变量
function createCounter() {
let count = 0 // 私有变量,外部无法直接访问
return function() {
count++
return count
}
}
const counter = createCounter()
console.log(counter()) // 1
console.log(counter()) // 2
// count 变量无法被外部直接修改,只能通过返回的函数操作
- 函数工厂:批量创建相似的函数
function makeMultiplier(times) {
return function(n) {
return n * times
}
}
const double = makeMultiplier(2)
const triple = makeMultiplier(3)
console.log(double(5)) // 10
console.log(triple(5)) // 15
- 模块化:在 ES6 模块出现之前,常用闭包实现模块
::: warning 闭包的坑 闭包虽然强大,但使用不当会导致内存泄漏。
问题示例:
function createHandlers() {
const handlers = []
for (var i = 0; i < 3; i++) {
handlers.push(function() {
console.log(i)
})
}
return handlers
}
const handlers = createHandlers()
handlers[0]() // 输出 3(不是 0!)
handlers[1]() // 输出 3(不是 1!)
handlers[2]() // 输出 3(不是 2!)
原因: var 没有块级作用域,所有闭包共享同一个 i 变量。
解决方法:
- 用
let代替var(推荐) - 用 IIFE(立即执行函数)创建独立作用域
// 方法 1:用 let
for (let i = 0; i < 3; i++) { // ← 用 let
handlers.push(function() {
console.log(i)
})
}
// 方法 2:用 IIFE
for (var i = 0; i < 3; i++) {
(function(j) { // ← 用 IIFE 捕获当前值
handlers.push(function() {
console.log(j)
})
})(i)
}
:::
👇 动手试试看: 下面这个演示展示了闭包的工作原理:
5. this 与执行上下文
5.1 this 是什么?
this 是 JavaScript 中最让人困惑的关键字之一。它的值取决于函数如何被调用,而不是函数在哪里定义。
::: tip 🎯 核心规则
记住这句话: this 的值是在函数调用时确定的,不是定义时确定的。
判断 this 指向的四个规则:
- 默认绑定:
fn()→this指向全局对象(浏览器中是window) - 隐式绑定:
obj.fn()→this指向obj - 显式绑定:
fn.call(obj)→this指向obj - new 绑定:
new Fn()→this指向新创建的对象 :::
this 指向规则速查表:
| 调用方式 | this 指向 | 示例 |
|---|---|---|
| 普通函数调用 | 全局对象(非严格模式)或 undefined(严格模式) |
fn() |
| 对象方法调用 | 调用方法的对象 | obj.method() |
| 构造函数调用 | 新创建的对象 | new Constructor() |
| call/apply/bind | 显式指定的对象 | fn.call(obj) |
| 箭头函数 | 外层作用域的 this |
() => {} |
::: tip 💡 常见误区
误区 1: "箭头函数的 this 指向定义它的对象"
- ❌ 错误:箭头函数没有自己的
this - ✅ 正确:箭头函数的
this继承外层作用域
误区 2: "this 总是指向函数本身"
- ❌ 错误:
this不是指向函数本身 - ✅ 正确:
this指向调用函数的对象
误区 3: "嵌套函数的 this 不变"
- ❌ 错误:嵌套的普通函数有自己的
this - ✅ 正确:用箭头函数可以继承外层
this:::
👇 动手试试看:
下面这个演示展示了不同场景下 this 的指向:
6. 原型与继承
6.1 原型链:JavaScript 的继承机制
JavaScript 没有"类"(ES6 之前),它通过"原型链"(Prototype Chain)实现继承。
::: tip 🎯 什么是原型链?
每个对象都有一个"原型"(__proto__),当我们访问对象的属性时:
- 先在对象自身查找
- 找不到就去它的原型对象上查找
- 还找不到就去原型的原型查找
- 一直查到
Object.prototype(最顶层的原型) - 如果还找不到,返回
undefined
这条查找链条就是"原型链"。 :::
原型链示例:
function Person(name) {
this.name = name
}
Person.prototype.greet = function() {
return "Hello, I'm " + this.name
}
const p = new Person("张三")
// 访问 p.greet() 时的查找过程:
// 1. 在 p 自身上查找 → 没有 greet 方法
// 2. 在 p.__proto__(即 Person.prototype)上查找 → 找到了!
// 3. 执行 greet 方法
console.log(p.greet()) // "Hello, I'm 张三"
原型链的关系:
实例对象 (p)
__proto__ → Person.prototype
__proto__ → Object.prototype
__proto__ → null
::: tip 💡 class 语法
ES6 引入了 class 语法,让面向对象编程更接近传统语言。但记住:class 只是语法糖,底层仍然是原型链。
示例:
// ES6 class 语法
class Person {
constructor(name) {
this.name = name
}
greet() {
return "Hello, I'm " + this.name
}
}
// 等价于 ES5 的写法
function Person(name) {
this.name = name
}
Person.prototype.greet = function() {
return "Hello, I'm " + this.name
}
:::
👇 动手试试看: 下面这个演示展示了原型链的工作原理:
7. 异步编程
7.1 为什么需要异步?
JavaScript 是单线程的,这意味着它同一时间只能做一件事。如果所有操作都是同步的,那么耗时的操作(如网络请求)会阻塞整个程序,页面就会"卡死"。
::: tip 🎯 同步 vs 异步 同步(Synchronous): 按顺序执行,前一个任务完成后才开始下一个任务。
- 优点:简单直观
- 缺点:耗时操作会阻塞
异步(Asynchronous): 不等待耗时操作完成,先去做其他事,操作完成后再回来处理。
- 优点:不阻塞,性能更好
- 缺点:代码更复杂,需要处理回调 :::
实际对比:
// 同步方式(会阻塞)
console.log("1")
console.log("2") // 等上面执行完
console.log("3")
// 输出:1, 2, 3
// 异步方式(不阻塞)
console.log("1")
setTimeout(() => console.log("2"), 1000) // 1秒后执行
console.log("3")
// 输出:1, 3, 2(注意顺序!)
7.2 异步编程的演进
JavaScript 的异步编程经历了三个阶段:
| 阶段 | 方式 | 优点 | 缺点 |
|---|---|---|---|
| 回调函数 | callback |
简单直接 | 回调地狱 |
| Promise | then/catch |
链式调用,可读性更好 | 仍然不够简洁 |
| async/await | async/await |
像同步代码一样写异步 | 需要 Promise 支持 |
代码对比:
// 1. 回调函数(回调地狱)
getData(function(a) {
getMoreData(a, function(b) {
getMoreData(b, function(c) {
// 无限嵌套...
})
})
})
// 2. Promise 链式调用
getData()
.then(a => getMoreData(a))
.then(b => getMoreData(b))
.then(c => console.log(c))
.catch(err => console.error(err))
// 3. async/await(最优雅)
async function fetchData() {
try {
const a = await getData()
const b = await getMoreData(a)
const c = await getMoreData(b)
console.log(c)
} catch (err) {
console.error(err)
}
}
7.3 事件循环(Event Loop)
JavaScript 如何实现异步?答案是事件循环。
::: tip 🎯 事件循环的工作原理 JavaScript 的执行机制:
- 执行同步代码(所有同步任务都在主线程执行)
- 主线程为空时,检查微任务队列(Microtask Queue)
- 执行所有微任务(Promise.then、MutationObserver)
- 执行一个宏任务(setTimeout、setInterval、I/O)
- 重复步骤 2-4
关键点:
- 微任务优先级高于宏任务
- 每次执行完一个宏任务后,都会清空所有微任务
- setTimeout 是宏任务,Promise.then 是微任务 :::
经典面试题:
console.log("1")
setTimeout(() => console.log("2"), 0) // 宏任务
Promise.resolve().then(() => console.log("3")) // 微任务
console.log("4")
// 输出顺序:1, 4, 3, 2
// 解析:
// 1. 执行同步代码:输出 1, 4
// 2. 执行微任务:输出 3
// 3. 执行宏任务:输出 2
👇 动手试试看: 下面这个演示展示了异步编程和事件循环:
8. 总结
让我们用一张表格回顾 JavaScript 的核心概念:
| 概念 | 一句话总结 | 关键要点 | 常见坑 |
|---|---|---|---|
| 变量声明 | 优先用 const,其次 let,不用 var |
块级作用域、不可变性 | var 的变量提升、作用域泄漏 |
| 数据类型 | 原始类型存值,引用类型存地址 | 七种原始类型、引用类型 | 引用类型的赋值和比较 |
| 函数 | JavaScript 的"一等公民" | 函数声明、箭头函数 | 箭头函数没有自己的 this |
| 闭包 | 函数"记住"了外部变量 | 数据私有化、函数工厂 | 内存泄漏、循环中的闭包 |
| this | 取决于函数如何被调用 | 四种绑定规则 | 嵌套函数的 this 丢失 |
| 原型链 | JavaScript 的继承机制 | __proto__、原型链查找 |
属性查找的顺序 |
| 异步编程 | 用同步的方式写异步代码 | 事件循环、微任务/宏任务 | 回调地狱、执行顺序 |
::: info 写在最后 JavaScript 是一门看似简单、实则精妙的语言。它的核心概念——作用域、闭包、this、原型链、异步——构成了前端开发的基础。
深入理解这些概念,你不仅能写出更好的代码,还能更快地学习新技术(因为 Vue、React 等框架都是基于这些特性构建的)。
希望这篇文章能帮助你建立起对 JavaScript 的系统性认识。记住:不必一次全学会,循序渐进、持续实践,你终将掌握这门语言的精髓。 :::