第 1 章 1.1 JavaScript 历史与现代生态

Simple tech illustration explaining a key concept about "第 1 章 1.1 JavaScript 历史与现代生态", infographic

AI comic creation scene, creative workspace with holographic UI panels, soft futuristic aesthetic, n

🎯 开场:为什么你该学 JavaScript?

你有没有遇到过这种情况?在网上看到一个小工具:「一键把 Excel 表格转成 JSON」「自动抓取网页数据」「做个待办清单 App」——点进去发现人家用的是 JavaScript,评论区一堆人在问「怎么运行的」「求教程」。

你眼馋了,但你不会。

或者你试过看 JavaScript 教程,但满屏的 document.getElementByIdaddEventListenercallback hell……看到第三章就放弃了。

这就是你来对地方的原因。

学完这一章,你就能明白 JavaScript 这门语言到底是怎么来的、为什么它能跑在浏览器里也能跑在服务器上、以及你写的代码到底是怎么被执行的。

等等,你说你是 Python 教学博主?嗯,我注意到了。但这套课程是 JavaScript 系列——不过放心,编程思想是相通的。Python 教会你的「变量 = 容器」「函数 = 机器」这些概念,在 JavaScript 里照样好使。你只需要适应一下语法就行。

🧱 基础:JavaScript 的前世今生

1.1.1 一门被浏览器「逼」出来的语言

1995 年,网景公司(Netscape)需要一门脚本语言,让网页能动起来。他们只给了 Brendan Eich 10 天时间——

结果你猜怎么着?

他还真做出来了。一门语言,名字本来想叫「Mocha」,后来改成「LiveScript」,最后因为 Java 当时很火,干脆蹭热度改成「JavaScript」。

类比:就像你本来想给公众号取名「小王的技术笔记」,后来发现「程序员小王」流量更大,就改了。蹭热点这事儿,1995 年就有先例了。

所以 JavaScript 名字里有「Java」,但它跟 Java 是完全不同的两门语言。就像「汽车」和「汽车挂件」的关系。

1.1.2 ES6:一次脱胎换骨的升级

JavaScript 早年版本被吐槽了很久:「语法奇怪」「没有类」「回调地狱」……

2015 年,ECMAScript 6(简称 ES6)发布,这是 JavaScript 历史上最重要的一次升级。

举几个例子:

// 老的写法(ES5):声明一个变量
var 购物车总价 = 0;

// 新的写法(ES6):let 和 const
const 商品单价 = 99;  // const 表示这个量不变
let 数量 = 3;         // let 表示这个量会变
// ES5 的函数
function 计算总价(单价, 数量) {
return 单价 * 数量;
}

// ES6 的箭头函数
const 计算总价 = (单价, 数量) => 单价 * 数量;
// ES5 的字符串拼接
var 欢迎语 = "亲爱的" + 用户名 + ",您有" + 未读消息数 + "条新消息";

// ES6 的模板字符串
const 欢迎语 = `亲爱的${用户名},您有${未读消息数}条新消息`;

说白了:ES6 就是给 JavaScript 做了个「换肤」。功能一样,但看起来更舒服、写起来更少犯错。

1.1.3 V8 引擎:JavaScript 是怎么跑起来的?

你写了一段 JavaScript 代码,浏览器怎么知道该怎么执行?

答案是:V8 引擎。

V8 是 Google 开发的 JavaScript 引擎(就是 Chrome 用的那个)。它的工作原理是:

  1. 解析(Parsing):把你的代码转成抽象语法树(AST)
  2. 编译(Compilation):把 AST 转成机器码
  3. 执行(Execution):机器码直接在 CPU 上跑

重点来了——V8 是用 C++ 写的,但它能运行 JavaScript。这就像一个外国友人能听懂中文,不是因为他脑子里装了中文,而是他带了个翻译。

类比:V8 就像你的手机翻译 App。你说中文,它实时翻译成英文给老外听。V8 就是那个「实时翻译」,只不过翻译的是「代码」到「机器能懂的指令」。

1.1.4 浏览器 vs Node.js:两个跑 JavaScript 的地方

JavaScript 最早只能跑在浏览器里。但 2009 年,有人把 V8 引擎单独拎出来,做了个 Node.js——让 JavaScript 也能跑在服务器上。

浏览器里的 JavaScript:
你写的代码 → V8 引擎 → 操作 DOM(网页元素)、发起网络请求

Node.js 里的 JavaScript:
你写的代码 → V8 引擎 → 读写文件、操作数据库、创建服务器
浏览器 Node.js
能访问网页吗? ✅ DOM、BOM ❌ 没有网页概念
能读写文件吗? 受限(安全沙箱) ✅ 完全可以
能创建服务器吗?
谁在用? 前端开发者 后端开发者、全栈

举个例子:你写一个「读取文件夹里所有文件名」的脚本,浏览器做不到(安全原因),但 Node.js 只需要 3 行:

const fs = require('fs');           // 引入文件系统模块
fs.readdir('./', (err, files) => {  // 读取当前目录
console.log(files);               // 打印所有文件名
});

1.1.5 TC39 提案:JavaScript 的「议会制度」

JavaScript 不是一个人拍脑袋定的。它有个「议会」——TC39 委员会,由各大公司(Google、Microsoft、Apple、Mozilla 等)的工程师组成。

新语法要进入 JavaScript,得走这个流程:

Stage 0(草案)→ Stage 1(提案)→ Stage 2(初稿)→ Stage 3(候选)→ Stage 4(定稿)

比如「可选链」obj?.prop 这个语法,2019 年还是 Stage 3,2020 年就进入 Stage 4,成为标准。

类比:就像一个法律草案要经过人大审议一样。TC39 就是 JavaScript 世界的「人大」。

🔥 实战:3 个小项目带你快速上手

项目 1:你的第一个 JavaScript 程序(5 分钟)

场景:你想在浏览器里输出一句话「Hello, World!」

完整代码(可以直接打开浏览器控制台粘贴运行):

// 在浏览器控制台里运行这段代码
const 我的第一句话 = "Hello, World!";
console.log(我的第一句话);

// 输出:Hello, World!

预期输出

Hello, World!

一句话解释console.log() 就像 Python 里的 print(),把内容打印到控制台。

再试一个有意思的

// 计算 1+2+...+100
let 总和 = 0;
for (let i = 1; i <= 100; i++) {
总和 += i;
}
console.log(`1 到 100 的和是:${总和}`);

// 输出:1 到 100 的和是:5050

项目 2:读取 JSON 数据并筛选(15 分钟)

场景:你有一份 JSON 格式的「班级成绩单」,需要找出不及格的同学。

数据(假设从文件或 API 读取)

const 成绩单 = [
{ 姓名: "张三", 数学: 92, 英语: 88 },
{ 姓名: "李四", 数学: 45, 英语: 67 },
{ 姓名: "王五", 数学: 78, 英语: 85 },
{ 姓名: "赵六", 数学: 33, 英语: 55 }
];

完整代码

// 找出不及格(分数 < 60)的同学
const 不及格名单 = 成绩单.filter(学生 => 学生.数学 < 60 || 学生.英语 < 60);

console.log("不及格同学:");
不及格名单.forEach(学生 => {
console.log(`${学生.姓名} - 数学:${学生.数学},英语:${学生.英语}`);
});

// 输出:
// 不及格同学:
// 李四 - 数学:45,英语:67
// 赵六 - 数学:33,英语:55

一句话解释filter() 就像一个「过滤器」,符合条件(返回 true)的留下来;forEach() 就像「遍历每一个人」。

项目 3:做个简易待办清单工具(15 分钟)

场景:你需要一个命令行工具,能添加待办事项、查看列表、删除已完成的事项。

完整代码(Node.js 环境运行):

// 待办清单工具
let 待办列表 = [];

// 添加待办
function 添加(事项) {
待办列表.push({ 内容: 事项, 完成: false });
console.log(`✅ 已添加:「${事项}」`);
}

// 查看列表
function 查看() {
if (待办列表.length === 0) {
console.log("📝 待办列表是空的");
return;
}
待办列表.forEach((item, 索引) => {
const 状态 = item.完成 ? "✓" : "○";
console.log(`${索引 + 1}. [${状态}] ${item.内容}`);
});
}

// 标记完成
function 完成(索引) {
if (索引 > 0 && 索引 <= 待办列表.length) {
待办列表[索引 - 1].完成 = true;
console.log(`✅ 已标记完成:「${待办列表[索引 - 1].内容}」`);
}
}

// 删除待办
function 删除(索引) {
if (索引 > 0 && 索引 <= 待办列表.length) {
const 被删除 = 待办列表.splice(索引 - 1, 1);
console.log(`🗑️ 已删除:「${被删除[0].内容}」`);
}
}

// 测试
添加("买牛奶");
添加("写周报");
添加("给妈妈打电话");
查看();
完成(2);
删除(3);
查看();

预期输出

✅ 已添加:「买牛奶」
✅ 已添加:「写周报」
✅ 已添加:「给妈妈打电话」
1. [○] 买牛奶
2. [○] 写周报
3. [○] 给妈妈打电话
✅ 已标记完成:「写周报」
🗑️ 已删除:「给妈妈打电话」
1. [○] 买牛奶
2. [✓] 写周报

一句话解释:这个工具用数组存储待办事项,用对象记录每个事项的内容和状态。splice() 是数组的「剪切」操作,可以删除指定位置的元素。

💪 进阶:新手最容易踩的坑

坑 1:var vs let vs const 的作用域陷阱

// ❌ 错误示例:用 var 容易出 bug
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:3, 3, 3(不是你想要的 0, 1, 2)

// ✅ 正确示例:用 let 才有块级作用域
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:0, 1, 2

坑来了:var 是函数作用域,let 是块级作用域。在循环里用 var,异步回调拿到的都是最后一个值。

坑 2:异步回调地狱

// ❌ 错误示例:层层嵌套,难以阅读
fs.readFile('a.txt', function(err, data) {
fs.readFile('b.txt', function(err, data) {
fs.readFile('c.txt', function(err, data) {
  console.log(data);
});
});
});

// ✅ 正确示例:用 Promise 或 async/await
const 读取所有文件 = async () => {
const a = await fs.promises.readFile('a.txt');
const b = await fs.promises.readFile('b.txt');
const c = await fs.promises.readFile('c.txt');
console.log(a, b, c);
};

坑 3:箭头函数里没有 this

// ❌ 错误示例
const 计数器 = {
count: 0,
增加: () => {
this.count++;  // 这里的 this 不是计数器对象!
}
};

// ✅ 正确示例:用普通函数
const 计数器 = {
count: 0,
增加: function() {
this.count++;
}
};

坑 4:比较运算的强制类型转换

// ❌ 错误示例
console.log(0 == false);   // true(意外吧?)
console.log('' == false);  // true
console.log(null == false); // false

// ✅ 正确示例:用严格等于 ===
console.log(0 === false);   // false
console.log('' === false);  // false
console.log(null === false); // false

注意! 永远用 ===!==,别用 ==!=

坑 5:数组方法不修改原数组

// ❌ 错误示例:以为 sort() 会返回排序后的新数组
const 数字 = [3, 1, 2];
const 排序后 = 数字.sort();
console.log(数字);        // [1, 2, 3] - 原数组被改了!
console.log(排序后 === 数字); // true

// 正确示例:slice() 返回新数组
const 原数组 = [3, 1, 2];
const 副本 = 原数组.slice();
副本.sort();
console.log(原数组); // [3, 1, 2] - 原数组不变

调试技巧:console 不只是 log

// 打印对象时用 table 更清晰
const 用户们 = [
{ 名字: "张三", 年龄: 25 },
{ 名字: "李四", 年龄: 30 }
];
console.table(用户们);

// 打印带样式的日志
console.log("%c醒目文字", "color: red; font-size: 20px");

// 计时代码执行时间
console.time("排序");
const 排序结果 = 大数组.sort((a, b) => a - b);
console.timeEnd("排序");

✏️ 练习题

练习 1(2 分钟):改个变量名

// 下面代码会输出什么?如果把 let 改成 const 会怎样?
let 数字 = 10;
数字 = 20;
console.log(数字);
  • 输入:无
  • 预期输出:20
  • 提示:let 允许重新赋值

练习 2(3 分钟):加个判断

在项目 1 的「计算 1+2+...+100」代码里,加一个判断:如果总和大于 5000,额外打印「大数字!」。

  • 输入:无
  • 预期输出:5050大数字!
  • 提示:加一行 if (总和 > 5000) { console.log('大数字!'); }

练习 3(5 分钟):处理新数据

用项目 2 的 filter 方法,从以下数据中找出「成绩都及格」的学生:

const 学生成绩 = [
{ 姓名: "小明", 语文: 85, 数学: 92 },
{ 姓名: "小红", 语文: 58, 数学: 73 },
{ 姓名: "小李", 语文: 90, 数学: 88 }
];
  • 输入:上述数据
  • 预期输出:只输出小明的记录
  • 提示:两个条件都要满足才及格

练习 4(5 分钟):组合两个项目

把项目 2 的「成绩单筛选」和项目 3 的「待办清单」组合起来:
- 把不及格学生自动加入待办清单「需要补考」
- 然后查看待办清单

  • 输入:项目 2 的成绩单数据
  • 预期输出:不及格名单 + 待办清单里多了「需要补考」这一项
  • 提示:先筛选,再 forEach 里调用 添加()

练习 5(10 分钟):读懂报错

假设你运行以下代码,控制台报错了:

const 数字 = [1, 2, 3];
数字 = [4, 5, 6];  // 这一行报错
console.log(数字);
  • 输入:上述代码
  • 预期输出:TypeError: Assignment to constant variable.
  • 提示:看看报错信息,再看看第 1 行声明用的什么关键词

📚 作业:做一个「章节学习记录器」

需求描述
做一个命令行工具,记录你学习本教程的进度,并保存到本地文件。

功能点
1. 添加一条学习记录(章节名 + 学习时长 + 备注)
2. 查看所有学习记录
3. 导出记录为 JSON 文件
4. 从 JSON 文件加载记录

加分项
1. 支持删除某条记录
2. 显示总学习时长

验收标准
- 能跑起来(Node.js 环境)
- 添加后查看能看到新记录
- 关闭再打开程序,加载 JSON 后记录还在

提交方式:评论区贴代码或 GitHub 链接


📚 总结

这一章我们学了 3 件事:

  1. JavaScript 怎么来的:1995 年被浏览器「逼」出来,2015 年 ES6 大升级
  2. 代码怎么跑起来的:V8 引擎翻译你的代码,让机器能懂
  3. 能在哪儿跑:浏览器里做网页交互,Node.js 里做服务器/工具

延伸资源


互动钩子

你在学习这章的时候,有没有想过「这玩意儿跟 Python 比起来……」?比如两种语言的变量声明、函数写法有什么不同?评论区聊聊你在两种语言间切换的经历,老粉优先回复!


下章剧透:下一章我们会学到——写了代码总得运行看效果吧?浏览器自带的「开发者工具」就是你的好帮手,能实时看到代码执行、调试网络请求、修改网页样式……学会它,你就真正入门前端开发了。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。