变量与数据类型
让程序能"记住"信息
🎯 学习目标
学完本章,你将能够:
- 能够正确使用 let 和 const 声明变量,并解释为什么优先使用 const
- 能够区分 JavaScript 的七种原始类型(number、string、boolean、Symbol、BigInt、undefined、null)
- 能够使用模板字符串拼接变量,避免繁琐的字符串连接
- 能够解释 null 与 undefined 的区别,以及 typeof 运算符的返回值
- 能够进行基本的类型转换(显式转换),识别常见的隐式转换陷阱
📌 有编程基础?如果你已经学过 C / Java / Python 的变量和类型,本章只有两处值得停留:JS 的隐式类型转换("5" + 3 的结果会让你意外)和 == vs ===(第 3 章详解)。其余可快速浏览,跳到第 8 章看 JS 特有的作用域与闭包。
你有没有想过
你打开一个购物网站,把商品加入购物车,关掉页面后再回来——购物车里的东西居然还在。它是怎么"记住"的?
答案很简单:程序里有东西帮它存储数据。这个东西就叫变量。
变量就像一个贴了标签的盒子。你往盒子里放东西,以后需要的时候看标签就能找到它。数据类型则是"盒子里能放什么"的规则——有的盒子放数字,有的放文字,有的放"是或否"。

图:变量的"盒子"模型
【AI 生图提示词】请生成一张中文教学示意图,风格简洁、配色柔和、适合初学者: - 画面主体:展示 3 个贴了标签的盒子,每个盒子里装着不同类型的数据 - 盒子 1:标签写 age,盒子里放着数字 25 - 盒子 2:标签写 name,盒子里放着文字 "张三" - 盒子 3:标签写 isStudent,盒子里放着 true - 每个盒子右侧用小箭头指向一行文字说明:分别是"数字类型"、"字符串类型"、"布尔类型" - 画面顶部居中写标题"变量 = 贴了标签的盒子" - 盒子样式:圆角矩形,浅米黄色底色,标签用橙色小长条贴在盒子上方 - 数据内容用深色等宽字体,标签用深橙色 - 配色建议:浅灰白背景,盒子用浅米黄,标签用橙色,箭头用浅蓝 - 画面比例:16:9 - 避免:过度装饰、3D 效果、阴影过重、文字过小 - 标注文字用中文
这个比喻只是为了帮你建立直观感受。在计算机内部,变量实际上是内存中的一块空间,通过地址来定位和访问,并不存在什么"盒子"和"标签"。
什么是变量
给数据起个名字,随时调用
变量(Variable)是程序中用来存储数据的命名容器。它的值可以在程序运行过程中被读取或修改。
说白了,变量就是给一份数据起个名字。以后用到这份数据时,喊名字就行,不用每次都把完整的数据重写一遍。
let username = "张三";
console.log(username); // "张三"
username = "李四"; // 变量可以重新赋值
console.log(username); // "李四"
三种声明方式
var、let、const——该用哪个?
JavaScript 提供了三种声明变量的关键字。它们的区别在于作用域和能否重新赋值。
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 是(值为 undefined) | 否(TDZ) | 否(TDZ) |
| 重复声明 | 允许 | 不允许 | 不允许 |
| 重新赋值 | 允许 | 允许 | 不允许 |
最佳实践:优先用 const,需要重新赋值时用 let,永远不要用 var。
var 的坑
为什么不推荐使用 var?看看这些"惊喜"
// 坑1:变量提升——声明被提到前面,但值没有
console.log(a); // undefined(不会报错!)
var a = 5;
// 坑2:忽略块级边界——if 里声明的变量跑出来了
if (true) {
var x = 10;
}
console.log(x); // 10——居然能访问到!
// 坑3:可以重复声明——不会报错,默默覆盖
var b = 1;
var b = 2; // 不报错,b 变成了 2
console.log(b); // 2
这三个问题加在一起,让 var 在大型项目中很容易引发 bug。ES6 引入 let 和 const 就是为了解决这些问题。
let 与 const
更安全、更现代的声明方式
// let:块级作用域,可以重新赋值
let score = 85;
score = 90; // OK
// const:块级作用域,不能重新赋值
const PI = 3.14159;
// PI = 3.14; // TypeError!
// 但 const 对象的内容可以修改
const arr = [1, 2, 3];
arr.push(4); // OK——修改数组内容
arr[0] = 100; // OK
// arr = []; // TypeError——不能换掉整个数组
const obj = { name: "张三" };
obj.name = "李四"; // OK
obj.age = 25; // OK
记住:const 防止的是重新赋值,不是防止修改内容。就像一把锁锁住了盒子上的标签,但没锁住盒子里的东西。

图:const 的"锁标签"模型
【AI 生图提示词】请生成一张中文教学示意图,风格简洁、配色柔和、适合初学者: - 画面主体:左右对比两个场景,用标题区分 - 左侧标题"不能重新赋值":一个盒子,标签写 arr,盒子外面有一把红色锁锁在标签上。旁边画一个红色叉号的箭头,从盒子指向另一个新盒子,表示"不能换掉整个盒子" - 右侧标题"可以修改内容":同一个盒子,标签写 arr,盒子是打开的,盒子里有 [1, 2, 3],一只手正往里面放数字 4。旁边画一个绿色对勾 - 盒子样式:圆角矩形,浅米黄色底色,标签用橙色小长条 - 红色锁用简单的挂锁图标即可,不需要写实 - 配色建议:浅灰白背景,盒子用浅米黄,标签用橙色,禁止用红色,允许用绿色,锁用红色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、阴影过重、文字过小 - 标注文字用中文,数据内容用等宽字体
这个比喻只是为了帮你建立直观感受。在计算机内部,const 保护的是变量的绑定关系(变量名指向的内存地址),而不是内存地址中存储的数据本身。
试一试
练习一
任务:以下代码哪些会报错?请逐行分析。
const name = "张三";
name = "李四";
let age = 25;
let age = 30;
var x = 1;
if (true) { var x = 2; }
console.log(x);
参考答案
// 第2行报错:const 不能重新赋值 → TypeError
// 第4行报错:let 不能重复声明 → SyntaxError
// 如果去掉报错行,console.log(x) 输出 2
// 因为 var 忽略块级边界,if 里的 x 覆盖了外面的
Part 1: 标识符与命名
变量有了,那名字该怎么起?好的命名让代码自己会说话
你已经会用 let 和 const 声明变量了。
但 let a = 1 这种名字,过几天你自己都不知道它是干什么的。
好的变量名就像好的路标,一眼就知道指向哪里。接下来看看 JS 对变量名有哪些硬性规则,以及驼峰命名等常见风格。
养成好习惯,以后读代码少头疼一半。
命名规则
不是所有名字都能当变量名
标识符(Identifier)是变量、函数、类等起名字用的字符串。JavaScript 对标识符有硬性规则。
- 必须以字母、
_或$开头 - 后续可以是字母、数字、
_、$ - 区分大小写:
myVar和myvar是两个变量 - 不能使用保留字:
let、class、return等
// 合法
let userName = "张三";
let _private = true;
let $jquery = "jQuery";
let age2 = 25;
// 非法
// let 2name = "错"; // 不能以数字开头
// let my-name = "错"; // 不能有连字符
// let class = "错"; // 保留字
| 风格 | 示例 | 场景 |
|---|---|---|
| camelCase | firstName | 变量、函数 |
| PascalCase | MyClass | 类、构造函数 |
| UPPER_SNAKE | MAX_COUNT | 常量 |
Part 2: 原始类型
JavaScript 有 7 种原始类型,它们是不可变的,按值传递
名字起好了,往变量里放什么?数字、文字、真假值——这些都是"原始类型"。JS 一共 7 种,比如 42 是 number,"hello" 是 string。乍看简单,但 0.1 + 0.2 为什么不等于 0.3?空字符串到底是真还是假?逐一认识它们,这些"反直觉"的问题就有答案了。
数字类型
JavaScript 里没有整数和浮点数之分
number 是 JavaScript 中所有数字的类型。不管是 42 还是 3.14,都是 number。它们内部统一使用 64 位双精度浮点数(IEEE 754)存储。
说白了,在 JS 里所有数字都是小数,只不过 42 的小数部分刚好是零。
let integer = 42;
let float = 3.14;
let scientific = 2.5e4; // 25000(科学计数法)
let hex = 0xff; // 255(十六进制)
let binary = 0b1010; // 10(二进制)
// 特殊数值
console.log(Infinity); // Infinity(无穷大)
console.log(NaN); // NaN(Not a Number)
console.log(typeof NaN); // "number"(讽刺吧?)
// BigInt——真正的大整数(ES2020)
let big = 9007199254740993n;
console.log(typeof big); // "bigint"
BigInt
突破数字的安全边界
JavaScript 的普通 number 能精确表示的整数范围是 ±2^53 - 1(约 9 千万亿)。超过这个范围,精度就会丢失。
为什么?JS 的 number 在底层用的是 IEEE 754 双精度浮点数——用 64 位二进制存储一个数,其中只有 53 位用来存整数部分。超过 53 位,多余的位就被截断了,所以 9007199254740992 和 9007199254740993 在计算机看来二进制长得一模一样。
// 普通数字的精度极限——两个不同的数竟然相等!
console.log(9007199254740992 === 9007199254740993); // true
// BigInt:在数字后面加 n,或用 BigInt() 函数
let big1 = 9007199254740993n;
let big2 = BigInt("9007199254740993");
console.log(big1 === big2); // true
console.log(typeof big1); // "bigint"
// BigInt 可以做精确的大数运算
console.log(1234567890123456789n * 2n); // 2469135780246913578n
1n + 1 会报 TypeError。需要显式转换:Number(1n) + 1 或 1n + BigInt(1)。
什么时候需要 BigInt?处理超长 ID(如数据库雪花 ID)、加密算法、高精度金融计算。日常开发中用得不多,但遇到大数时它是唯一的精确方案。
浮点精度陷阱
0.1 + 0.2 不等于 0.3?这不是 bug
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
// 为什么?因为 0.1 和 0.2 在二进制中无法精确表示
// 就像十进制无法精确表示 1/3 一样
99.99999999999999 之类的诡异数字。原因:IEEE 754 双精度浮点数只有 52 位有效尾数,部分十进制小数无法精确表示。
修复:处理金额时,以"分"为单位转为整数计算;或使用
toFixed(2) 格式化输出。需要精确计算时可用 BigInt 或第三方库。
// 修复方式:转为整数(分)计算
let price1 = 10.1 * 100; // 转为分
let price2 = 20.2 * 100;
let total = (price1 + price2) / 100;
console.log(total); // 30.3
// 或者用 toFixed 格式化
console.log((0.1 + 0.2).toFixed(2)); // "0.30"
字符串类型
用引号包起来的文字
string 是由零个或多个字符组成的文本序列。可以用单引号、双引号或反引号创建。
说白了,字符串就是一段用引号包起来的文字。不管是名字、地址还是一段 HTML,只要是文本就是字符串。
let single = 'hello';
let double = "world";
// 模板字符串——用反引号,可以嵌入变量
let name = "张三";
let age = 25;
let intro = `我叫${name},今年${age}岁`;
console.log(intro); // "我叫张三,今年25岁"
// 字符串是不可变的
let str = "hello";
// str[0] = "H"; // 不会生效!
str = "Hello"; // 只能重新赋值(创建新字符串)
console.log(str.length); // 5
模板字符串
反引号的三大超能力
// 超能力1:嵌入变量和表达式
let price = 99.9;
let msg = `价格:¥${price},打折后:¥${(price * 0.8).toFixed(2)}`;
// 超能力2:多行字符串
let html = `
<div>
<h1>${name}</h1>
<p>年龄: ${age}</p>
</div>
`;
// 超能力3:嵌套模板
let items = ["苹果", "香蕉", "橙子"];
let list = `
<ul>
${items.map(item => `<li>${item}</li>`).join('\n')}
</ul>
`;
console.log(list);
在 JS 项目中,优先使用模板字符串而不是 + 拼接。它更易读,更不容易出错。
布尔类型
只有 true 和 false,但无处不在
boolean 只有 true 和 false 两个值。它通常由比较运算产生,用于控制程序的执行路径。
let isActive = true;
let isDeleted = false;
// 比较运算产生布尔值
console.log(5 > 3); // true
console.log(5 === "5"); // false
// 其他类型可以转为布尔值
console.log(Boolean(0)); // false
console.log(Boolean("")); // false
console.log(Boolean(null)); // false
console.log(Boolean(NaN)); // false
console.log(Boolean(1)); // true
console.log(Boolean("hello")); // true
在 JS 中,只有 8 个 falsy 值:false、0、-0、0n、""、null、undefined、NaN。其余全部是 truthy。
Symbol 类型
独一无二的标识符
Symbol 是 ES6 引入的第七种原始类型。每次调用 Symbol() 都会创建一个全局唯一的值,不会和任何其他值相等——即使描述文字一样也不行。
// 每个Symbol都是独一无二的
let s1 = Symbol("描述");
let s2 = Symbol("描述");
console.log(s1 === s2); // false——即使描述相同
// 和字符串对比:相同的字符串是相等的
console.log("id" === "id"); // true
console.log(Symbol("id") === Symbol("id")); // false
初学阶段你很少会主动使用 Symbol。它主要用于给对象添加"隐藏属性"(不会和其他属性撞名),以及 JavaScript 内部机制(比如 Symbol.iterator 决定一个对象能否被 for...of 遍历)。后面的章节会陆续遇到它。
试一试
练习二
任务:预测以下每一行的输出结果。
console.log(Boolean([])); // ?
console.log(Boolean({})); // ?
console.log(Boolean("false")); // ?
console.log(Boolean(0)); // ?
参考答案
console.log(Boolean([])); // true(空数组是 truthy!)
console.log(Boolean({})); // true(空对象也是 truthy!)
console.log(Boolean("false")); // true(非空字符串都是 truthy)
console.log(Boolean(0)); // false(0 是 falsy)
Part 3: null 与 undefined
两个都表示"空",但意思完全不同
7 种原始类型都过了一遍,还剩一对"双胞胎"值得单独拿出来讲:null 和 undefined。
打印出来都是"空",但一个是你故意设的空,一个是引擎自动给的空。
搞混了,调试时就分不清"这个变量到底有没有赋过值"。
这一节帮你彻底分清它俩,顺便学会用 typeof 安全地检查类型。
undefined
"还没给值"——JavaScript 引擎自动赋予的
undefined 表示"变量已声明但还没有赋值"。它通常是 JavaScript 引擎自动赋予的,不需要你手动设置。
// 声明但未赋值
let x;
console.log(x); // undefined
// 函数没有返回值
function greet() {}
console.log(greet()); // undefined
// 访问不存在的属性
let obj = { name: "张三" };
console.log(obj.age); // undefined
// 数组越界
let arr = [1, 2, 3];
console.log(arr[10]); // undefined
null
"故意为空"——开发者主动设置的
null 表示"这里确实没有值,是我故意的"。它是开发者主动赋予的,表示一个空引用。
// 故意设置为空
let data = null;
// 常见用途:清除引用,便于垃圾回收(这里用缓存对象举例,Node 和浏览器都能跑)
let cache = { user: "小明", lastLogin: "2024-01-01" };
cache = null; // 不再需要这个缓存,主动断开引用
// typeof null 有个历史 bug
console.log(typeof null); // "object"(不是 "null"!)
typeof null 返回 "object",你可能以为 null 是对象。原因:这是 JS 第一版的遗留 bug。当时值用 32 位存储,前 3 位为 0 表示对象,而 null 全部为 0,被误判为对象。
修复:判断 null 用
value === null,不要依赖 typeof。
null vs undefined
一张表搞清它们的区别
| 特性 | undefined | null |
|---|---|---|
| 含义 | 还没给值 | 故意为空 |
| typeof | "undefined" | "object"(bug) |
| 转为数字 | NaN | 0 |
| == 比较 | null == undefined → true | |
| === 比较 | null === undefined → false | |
| 谁赋的 | 引擎自动 | 开发者手动 |
建议:用 === null 或 === undefined 做精确判断。用 == null 可同时匹配两者(这是唯一推荐使用 == 的场景)。
typeof 运算符
安全地检查变量类型
console.log(typeof 42); // "number"
console.log(typeof "hello"); // "string"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object"(历史 bug)
console.log(typeof []); // "object"
console.log(typeof {}); // "object"
console.log(typeof function(){}); // "function"
三个注意点:
typeof null返回"object",这是个 bug,别被骗了- 数组的
typeof也是"object",判断数组用Array.isArray() typeof对未声明的变量不会报错,返回"undefined"
类型转换
在数字、字符串、布尔之间切换
JavaScript 是动态类型语言,值的类型可以随时转换。转换分两种:显式转换(你主动调用)和隐式转换(引擎自动执行)。
// 显式转换——推荐!意图清晰,不会产生歧义
Number("42"); // 42
Number("hello"); // NaN
parseInt("42px"); // 42(忽略后面的非数字)
parseFloat("3.14"); // 3.14
String(42); // "42"
(42).toString(); // "42"
Boolean(0); // false
Boolean("hello"); // true
// 隐式转换——常见但容易出 bug
console.log("5" + 3); // "53"(数字被转成字符串拼接)
console.log("5" - 3); // 2(字符串被转成数字做减法)
console.log(true + 1); // 2(true 被转为 1)
原始 vs 引用
理解变量在内存中的两种存储方式
JavaScript 的数据类型分为两大阵营:原始类型(7 种)和引用类型(object、array、function 等)。它们在赋值时的行为完全不同。

图:原始类型 vs 引用类型的内存模型
【AI 生图提示词】请生成一张中文教学示意图,风格简洁、配色柔和、适合初学者: - 画面主体:左右对比两幅内存示意图,用竖线分隔 - 左侧标题"原始类型——复制值": 1. 画两个小矩形代表变量 a 和 b,分别标注名字 2. a 矩形内写 42,b 矩形内写 42(各自独立的副本) 3. b 下方画一个箭头指向一个新值 100,表示 b 被修改 4. a 仍然指向 42,用绿色对勾标注"a 不受影响" - 右侧标题"引用类型——复制地址": 1. 画两个小矩形代表变量 x 和 y,分别标注名字 2. 两个矩形内都写着同一个箭头/地址标识(如 0x1A),表示它们存的是同一个地址 3. 下方画一个大圆角矩形代表内存中的数组 [1, 2, 3],地址标注 0x1A 4. x 和 y 通过箭头都指向这个大矩形 5. 大矩形旁边画一个加号 +4,变成 [1, 2, 3, 4],用橙色标注"改一个,两个都变" - 配色建议:浅灰白背景,原始类型用浅蓝色调,引用类型用浅橙色调,地址/箭头用深灰色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、阴影过重、文字过小 - 标注文字用中文,数据值用等宽字体
原始类型:复制值
let a = 42;
let b = a; // 复制值
b = 100;
console.log(a); // 42
// a 不受 b 的影响
每个变量有自己独立的值,互不干扰。
引用类型:复制地址
let x = [1, 2, 3];
let y = x; // 复制地址
y.push(4);
console.log(x); // [1,2,3,4]
// x 和 y 指向同一份数据!
多个变量共享同一份数据,改一个全受影响。
这就是为什么 const arr = [1, 2] 之后 arr.push(3) 不报错——const 锁定的是变量的地址(绑定关系),不是地址指向的数据内容。
字符串方法
操作文本的常用工具箱
let str = "Hello, World!";
// 查找
str.length; // 13
str.indexOf("World"); // 7
str.includes("World"); // true
// 截取
str.slice(7, 12); // "World"
str.slice(-6); // "orld!"
// 修改
str.toUpperCase(); // "HELLO, WORLD!"
str.replace("World", "JS"); // "Hello, JS!"
// 分割与合并
"a,b,c".split(","); // ["a", "b", "c"]
["a", "b"].join("-"); // "a-b"
// 实用技巧:补零
"5".padStart(3, "0"); // "005"
"ha".repeat(3); // "hahaha"
Math 对象
数字运算的瑞士军刀
// 取整
Math.round(4.5); // 5(四舍五入)
Math.floor(4.9); // 4(向下取整)
Math.ceil(4.1); // 5(向上取整)
// 最值与绝对值
Math.max(1, 5, 3); // 5
Math.min(1, 5, 3); // 1
Math.abs(-42); // 42
// 幂与开方
Math.pow(2, 10); // 1024
Math.sqrt(16); // 4
// 随机数——生成 1~100 的随机整数
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
console.log(randomInt(1, 100));
Math 对象就像一个计算器工具箱。这个比喻只是为了帮你建立直观感受。在计算机内部,Math 的方法是通过底层浮点运算指令直接调用的。
常见错误诊断
问题一:TypeError: Assignment to constant variable
症状:代码运行到某行突然报错,提示不能给常量赋值。
原因:用 const 声明的变量尝试了重新赋值。
修复:如果确实需要修改值,把 const 改成 let。
问题二:ReferenceError: x is not defined
症状:代码里用了一个变量,但报错说它未定义。
原因:变量没声明就使用了,或者声明在块级作用域之外访问不到。
修复:检查变量是否拼写正确,是否在当前作用域内声明过。
问题三:0.1 + 0.2 结果不是 0.3
原因:浮点数精度问题,详见前面的"浮点精度陷阱"幻灯片。
修复:用整数运算,或用 toFixed() 格式化。
综合练习
练习三:购物小票
任务:创建 receipt.js,用变量存储商品信息,输出一张格式化的购物小票。
$ node receipt.js
=== 购物小票 ===
商品A ¥29.9 x2 ¥59.8
商品B ¥15.5 x3 ¥46.5
------------------------
总计:¥106.3
提示:用 const 存储单价,注意浮点精度(可以先用分为单位计算整数,最后再除以 100)。
参考答案
const priceA = 2990; // 以分计算,避免浮点问题
const priceB = 1550;
const qtyA = 2;
const qtyB = 3;
console.log("=== 购物小票 ===");
console.log(`商品A ¥${(priceA/100).toFixed(1)} x${qtyA} ¥${(priceA*qtyA/100).toFixed(1)}`);
console.log(`商品B ¥${(priceB/100).toFixed(1)} x${qtyB} ¥${(priceB*qtyB/100).toFixed(1)}`);
console.log("------------------------");
const total = (priceA * qtyA + priceB * qtyB) / 100;
console.log(`总计:¥${total.toFixed(1)}`);
练习四:类型侦探
任务:预测以下每行的输出结果,然后运行验证你的答案。
console.log(typeof "42" + typeof 42);
console.log(typeof null === "object");
console.log(Boolean("0") === Boolean(0));
console.log(Number("") === Number(null));
console.log(String(false) === "false");
参考答案
// "stringnumber" — typeof 返回字符串,+ 变成字符串拼接
// true — typeof null 的经典 bug,返回 "object"
// false — Boolean("0") 是 true,Boolean(0) 是 false
// true — Number("") 是 0,Number(null) 也是 0
// true — String(false) 就是字符串 "false"
📝 要点总结
本章核心知识点
变量声明
const 优先,let 次之,不用 var
七种原始类型
number、string、boolean、Symbol、BigInt、undefined、null
模板字符串
用反引号和 ${} 优雅拼接字符串
类型转换
显式转换用函数,隐式转换要小心
引用类型
对象和数组是引用类型,赋值的是地址
下一章
现在你已经能让程序"记住"数据了。下一章我们学习运算符与表达式——让程序能对数据进行计算和判断。