Files
test-repo/docs/zh-cn/appendix/3-browser-and-frontend/javascript-deep-dive.md
T

21 KiB
Raw Blame History

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 的知识点很多,但我们不必一次全学完。按照下面这个顺序,逐步掌握核心概念:

  1. 基础语法 → 变量、数据类型、操作符
  2. 函数与作用域 → 函数声明、闭包、this
  3. 对象与原型 → 对象创建、原型链、继承
  4. 异步编程 → 回调、Promise、async/await
  5. 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 中,有三种声明变量的方式:varletconst。它们的区别看似简单,实则影响深远。

::: details 🤔 为什么有三种方式? 这是 JavaScript 的历史遗留问题。

  • var 是 ES5 时代的产物,有"变量提升"和"没有块级作用域"的问题
  • letconst 是 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) // 10a 不受影响)

// 引用类型:复制引用(地址)
let obj1 = {x: 10}
let obj2 = obj1     // obj2 指向同一个对象
obj2.x = 20        // 修改 obj2 会影响 obj1
console.log(obj1.x) // 20obj1 也变了!)

为什么引用类型会这样? 因为引用类型存储的是"地址",而不是"实际的值"。当你把 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 🎯 什么是闭包? 简单来说: 闭包是函数和它的词法环境的组合。

更直白地说: 内部函数可以访问外部函数的变量,即使外部函数已经执行完毕。

打个比方: 你出门时背了个背包,把当时看到的东西装进包里。即使你走了很远的路,依然可以从包里拿出当时装的东西 —— 这个背包就是"闭包"。 :::

闭包的实际应用:

  1. 数据私有化:模拟私有变量
function createCounter() {
  let count = 0  // 私有变量,外部无法直接访问
  return function() {
    count++
    return count
  }
}

const counter = createCounter()
console.log(counter())  // 1
console.log(counter())  // 2
// count 变量无法被外部直接修改,只能通过返回的函数操作
  1. 函数工厂:批量创建相似的函数
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
  1. 模块化:在 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 变量。

解决方法:

  1. let 代替 var(推荐)
  2. 用 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 指向的四个规则:

  1. 默认绑定fn()this 指向全局对象(浏览器中是 window
  2. 隐式绑定obj.fn()this 指向 obj
  3. 显式绑定fn.call(obj)this 指向 obj
  4. 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__),当我们访问对象的属性时:

  1. 先在对象自身查找
  2. 找不到就去它的原型对象上查找
  3. 还找不到就去原型的原型查找
  4. 一直查到 Object.prototype(最顶层的原型)
  5. 如果还找不到,返回 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 的执行机制:

  1. 执行同步代码(所有同步任务都在主线程执行)
  2. 主线程为空时,检查微任务队列(Microtask Queue
  3. 执行所有微任务Promise.then、MutationObserver
  4. 执行一个宏任务setTimeout、setInterval、I/O
  5. 重复步骤 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 的系统性认识。记住:不必一次全学会,循序渐进、持续实践,你终将掌握这门语言的精髓。 :::