变量与数据类型

让程序能"记住"信息

🎯 学习目标

学完本章,你将能够:

  • 能够正确使用 let 和 const 声明变量,并解释为什么优先使用 const
  • 能够区分 JavaScript 的七种原始类型(number、string、boolean、Symbol、BigInt、undefined、null)
  • 能够使用模板字符串拼接变量,避免繁琐的字符串连接
  • 能够解释 null 与 undefined 的区别,以及 typeof 运算符的返回值
  • 能够进行基本的类型转换(显式转换),识别常见的隐式转换陷阱

📌 有编程基础?如果你已经学过 C / Java / Python 的变量和类型,本章只有两处值得停留:JS 的隐式类型转换("5" + 3 的结果会让你意外)和 == vs ===(第 3 章详解)。其余可快速浏览,跳到第 8 章看 JS 特有的作用域与闭包。

你有没有想过

你打开一个购物网站,把商品加入购物车,关掉页面后再回来——购物车里的东西居然还在。它是怎么"记住"的?

答案很简单:程序里有东西帮它存储数据。这个东西就叫变量。

变量就像一个贴了标签的盒子。你往盒子里放东西,以后需要的时候看标签就能找到它。数据类型则是"盒子里能放什么"的规则——有的盒子放数字,有的放文字,有的放"是或否"。

图:变量的"盒子"模型

这个比喻只是为了帮你建立直观感受。在计算机内部,变量实际上是内存中的一块空间,通过地址来定位和访问,并不存在什么"盒子"和"标签"。

什么是变量

给数据起个名字,随时调用

变量(Variable)是程序中用来存储数据的命名容器。它的值可以在程序运行过程中被读取或修改。

说白了,变量就是给一份数据起个名字。以后用到这份数据时,喊名字就行,不用每次都把完整的数据重写一遍。

let username = "张三";
console.log(username); // "张三"

username = "李四";     // 变量可以重新赋值
console.log(username); // "李四"

三种声明方式

var、let、const——该用哪个?

JavaScript 提供了三种声明变量的关键字。它们的区别在于作用域和能否重新赋值。

特性varletconst
作用域函数作用域块级作用域块级作用域
变量提升是(值为 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 的"锁标签"模型

这个比喻只是为了帮你建立直观感受。在计算机内部,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 = "错";    // 保留字
风格示例场景
camelCasefirstName变量、函数
PascalCaseMyClass类、构造函数
UPPER_SNAKEMAX_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
注意:BigInt 和 number 不能混合运算。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

一张表搞清它们的区别

特性undefinednull
含义还没给值故意为空
typeof"undefined""object"(bug)
转为数字NaN0
== 比较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)
最佳实践:优先使用显式转换。隐式转换虽然方便,但行为不直观,是 JavaScript 最常见的 bug 来源之一。下一章会深入讲解隐式转换的陷阱。

原始 vs 引用

理解变量在内存中的两种存储方式

JavaScript 的数据类型分为两大阵营:原始类型(7 种)和引用类型(object、array、function 等)。它们在赋值时的行为完全不同。

图:原始类型 vs 引用类型的内存模型

原始类型:复制值

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

模板字符串

用反引号和 ${} 优雅拼接字符串

类型转换

显式转换用函数,隐式转换要小心

引用类型

对象和数组是引用类型,赋值的是地址

下一章

现在你已经能让程序"记住"数据了。下一章我们学习运算符与表达式——让程序能对数据进行计算和判断。

1 / X