变量与数据类型
变量是存放数据的容器,类型则决定了数据能参与哪些运算。JavaScript 是动态弱类型语言,变量本身没有类型,类型由存放的值决定。这一章先把变量声明的规则讲透,再系统梳理 8 种数据类型与五花八门的转换行为。
声明变量的三种方式
JavaScript 提供 var、let、const 三种声明关键字。现代开发优先使用 const,其次 let,尽量不使用 var。
var oldWay = 1; // 旧时代写法,函数作用域
let mutable = 2; // 可重新赋值的变量,块级作用域
const fixed = 3; // 常量,块级作用域,不可重新赋值var、let、const 对比
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 提升且初始化为 undefined | 提升但不初始化(TDZ) | 提升但不初始化(TDZ) |
| 暂时性死区 | 无 | 有 | 有 |
| 重复声明 | 允许 | 不允许 | 不允许 |
| 重新赋值 | 允许 | 允许 | 不允许(对象可修改内部) |
| 全局声明挂载 | 挂到 window 上 | 不挂到 window 上 | 不挂到 window 上 |
块级作用域示例
if (true) {
var a = 1; // var 没有块级作用域
let b = 2; // let 只在块内有效
const c = 3; // const 只在块内有效
}
console.log(a); // 1,var 变量逃出了 if 块
console.log(b); // ReferenceError: b is not defined
console.log(c); // ReferenceError: c is not defined变量提升与暂时性死区
var 声明的变量会被提升到函数顶部,且初始值为 undefined,因此声明前访问不会报错:
console.log(x); // undefined,不会报错
var x = 10;let 和 const 同样会提升,但提升后处于"未初始化"状态,声明之前访问会抛出 ReferenceError。声明语句之前到声明之间的区域称为暂时性死区(TDZ,Temporal Dead Zone):
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 10;重复声明与 const 的"不可变"
var n = 1;
var n = 2; // 允许,n 变为 2
let m = 1;
let m = 2; // SyntaxError: Identifier 'm' has already been declared
const k = 1;
k = 2; // TypeError: Assignment to constant variableconst 禁止的是"重新绑定",不禁止修改对象内部内容:
const obj = { name: "js" };
obj.name = "javascript"; // 允许,修改的是对象属性
obj = {}; // TypeError,重新赋值不允许数据类型总览
JavaScript 共 8 种数据类型,分为原始类型与对象类型两大类。
| 类型 | typeof 结果 | 示例 | 说明 |
|---|---|---|---|
| undefined | "undefined" | undefined | 未赋值变量的默认值 |
| null | "object" | null | 表示空值(历史 bug 导致 typeof 误判) |
| boolean | "boolean" | true / false | 布尔值 |
| number | "number" | 42、3.14 | 双精度浮点数,涵盖整数与小数 |
| bigint | "bigint" | 10n | 任意精度大整数 |
| string | "string" | "hello" | 字符串 |
| symbol | "symbol" | Symbol("s") | 唯一标识符,常用于对象属性键 |
| object | "object" | {}、[]、函数 | 对象、数组、函数等引用类型 |
原始类型按值存储和比较,对象类型按引用存储:
let a = 1;
let b = a; // 复制值
b = 2;
console.log(a); // 1,a 不受影响
let arr1 = [1, 2];
let arr2 = arr1; // 复制引用,两个变量指向同一数组
arr2.push(3);
console.log(arr1); // [1, 2, 3],arr1 同步变化各类型详解
undefined 与 null
undefined:变量声明但未赋值时自动获得的值,是语言自动生成的"未定义"状态。null:开发者主动赋值的"空值",表示"此处有意为空"。
let x; // x 是 undefined
function f() {} // 无 return 时返回 undefined
console.log(f()); // undefined
let user = null; // 主动置空
console.log(user); // null判断两者时优先使用严格相等:x === null、x === undefined,也可用 x == null 同时匹配两者(宽松相等中两者相等且与对方相等)。
number 与 bigint
number 是 IEEE 754 双精度浮点数,能安全表示的整数范围是 -(2^53 - 1) 到 2^53 - 1,超出范围的大整数应使用 bigint:
const big = 9007199254740993n; // 超过安全整数范围,用 bigint
console.log(big + 1n); // 9007199254740994n
const unsafe = 9007199254740993; // number 会丢失精度
console.log(unsafe); // 9007199254740992bigint 与 number 不能混用运算(10n + 5 会抛 TypeError),但可以比较大小(10n > 5 为 true)。
string 与模板字符串
字符串有三种写法,ES6 模板字符串支持多行与插值:
const name = "张三";
const str1 = "双引号";
const str2 = '单引号';
const str3 = `你好,${name}`; // 模板字符串,输出 "你好,张三"
const multi = `第一行
第二行`; // 模板字符串保留换行boolean 与 symbol
boolean 只有 true 和 false 两个值。symbol 用于生成独一无二的标识,常用于对象属性键以避免命名冲突:
const s1 = Symbol("id");
const s2 = Symbol("id");
console.log(s1 === s2); // false,即使描述相同也不相等
const obj = {};
obj[s1] = "属性一";
obj[s2] = "属性二";
console.log(obj[s1]); // "属性一"typeof 运算符
typeof 是唯一能对未声明变量安全操作的运算符(返回 "undefined" 而不抛错),常用于类型判断:
typeof undeclaredVar; // "undefined",不会报错
function isNumber(v) {
return typeof v === "number";
}
console.log(isNumber(42)); // true
console.log(isNumber("42")); // falsetypeof null 的历史 bug
typeof null 返回 "object",这是 JavaScript 最著名的历史遗留 bug。早期引擎中对象类型的类型标签是 0,而 null 的空指针表示恰好也是 0,判断逻辑直接把两者混淆。修复会破坏大量存量代码,因此规范决定保留该行为。判断 null 请使用:
const v = null;
console.log(v === null); // true,推荐
console.log(typeof v); // "object",不可靠显式类型转换
显式转换由开发者主动调用转换函数完成,行为可预期。
Number、String、Boolean 三大转换函数
// 转数字
Number("42"); // 42
Number("42px"); // NaN,无法解析整个字符串
Number(""); // 0
Number(true); // 1
Number(null); // 0
Number(undefined); // NaN
// 转字符串
String(42); // "42"
String(true); // "true"
String(null); // "null"
String([1, 2, 3]); // "1,2,3"
// 转布尔
Boolean(1); // true
Boolean(0); // false
Boolean("hello"); // true
Boolean(""); // false
Boolean([]); // true,空数组是真值
Boolean({}); // true,空对象是真值parseInt 与 parseFloat
parseInt 从头解析数字前缀,遇到无法解析的字符即停止,适合处理 "42px" 这类字符串;parseFloat 支持小数:
parseInt("42px"); // 42,解析到 "px" 停止
parseInt("42.9"); // 42,parseInt 只取整数部分
parseInt("a42"); // NaN,首个字符不是数字
parseFloat("3.14radius"); // 3.14
// parseInt 第二个参数指定进制
parseInt("10", 2); // 2,二进制 "10"
parseInt("ff", 16); // 255,十六进制注意 parseInt 与 Number 的差异:parseInt("42px") 是 42,Number("42px") 是 NaN;parseInt("") 是 NaN,而 Number("") 是 0。
隐式类型转换
弱类型的代价是运算符会悄悄转换类型,理解规则才能避免踩坑。
加号运算符的陷阱
+ 一遇到字符串就变成拼接,且拼接优先级最高;其他算术运算符(-、*、/)则优先转数字:
console.log(1 + "2"); // "12",字符串拼接
console.log(1 + 2 + "3"); // "33",先算 1+2=3,再拼 "3"
console.log("3" - 1); // 2,字符串转数字
console.log("6" * "2"); // 12,两个都转数字
console.log(1 + 2 + 3); // 6,纯数字正常相加
console.log(1 + true); // 2,true 转 1
console.log(1 + null); // 1,null 转 0
console.log(1 + undefined); // NaN,undefined 转 NaN相等比较的陷阱
宽松相等 == 会做类型转换,严格相等 === 不做任何转换,一律使用 ===:
console.log(1 == "1"); // true,转换后相等
console.log(1 === "1"); // false,类型不同直接不相等
console.log(0 == false); // true
console.log(0 == ""); // true
console.log(null == undefined); // true,仅这两者宽松相等
console.log(null == 0); // false,null 不参与数字转换
console.log(NaN == NaN); // false,NaN 不等于任何值包括自身假值列表
转换为 boolean 后为 false 的值称为假值(falsy),JavaScript 中只有 6 个:
| 假值 | 说明 |
|---|---|
false | 布尔假 |
0 | 数字零 |
-0 | 负零 |
"" | 空字符串(含 ''、``` `) |
null | 空值 |
undefined | 未定义 |
NaN | 非数字 |
除上述之外的一切值(包括 [] 空数组、{} 空对象、"0" 字符串)都是真值。判断时注意 "0" 是字符串非空因此为真:
const vals = [0, "", null, undefined, NaN, false, [], {}, "0"];
vals.forEach((v) => console.log(Boolean(v)));
// 前六个输出 false,[]、{}、"0" 输出 true
if ([]) { // 空数组为真值,会进入分支
console.log("空数组进入分支");
}Number 精度问题与特殊值
0.1 + 0.2 为什么不等于 0.3
number 采用二进制浮点数存储,而 0.1、0.2 无法用二进制精确表示,只能存近似值,累加后误差显现:
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false常用的解决思路:
// 方案一:放大为整数再计算(乘除相同倍数)
const sum = (0.1 * 10 + 0.2 * 10) / 10; // 0.3
// 方案二:比较时容忍误差
console.log(Math.abs(0.1 + 0.2 - 0.3) < 1e-10); // true
// 方案三:四舍五入到所需精度
console.log((0.1 + 0.2).toFixed(1)); // "0.3",注意结果是字符串
// 方案四:涉及金额用 bigint 存最小单位(分)
const feeInCents = 100n + 200n; // 300n,即 3 元NaN 与 Infinity
- NaN(Not a Number):非法数值运算的结果。它不等于自身,判断必须用
Number.isNaN。 - Infinity 与 -Infinity:超出 number 表示范围的数值,分别表示正无穷与负无穷。
console.log(0 / 0); // NaN
console.log(Number("abc")); // NaN
console.log(Math.sqrt(-1)); // NaN
console.log(NaN === NaN); // false,NaN 不等于任何值
console.log(isNaN(NaN)); // true,但全局 isNaN 会先转类型,有隐患
console.log(isNaN("abc")); // true,"abc" 被转为 NaN
console.log(isNaN("42")); // false,"42" 能转成数字
console.log(Number.isNaN("abc")); // false,Number.isNaN 不做类型转换,更严谨
console.log(1 / 0); // Infinity
console.log(-1 / 0); // -Infinity
console.log(Infinity + 1); // Infinity
console.log(Infinity / Infinity); // NaN| 特殊值 | 含义 | 判断方式 |
|---|---|---|
NaN | 非数字,任何涉及它的比较都为 false | Number.isNaN(v) |
Infinity | 正无穷,如 1/0 | v === Infinity 或 Number.isFinite(v) |
-Infinity | 负无穷,如 -1/0 | v === -Infinity |
-0 | 负零,1 / -0 得到 -Infinity | Object.is(v, -0) |
判断数值是否有限使用 Number.isFinite(v),它同时排除了 NaN 与 Infinity:
console.log(Number.isFinite(42)); // true
console.log(Number.isFinite("42")); // false,不做类型转换
console.log(Number.isFinite(Infinity)); // false
console.log(Number.isFinite(NaN)); // false小结
变量声明的核心规则一句话概括:var 是函数作用域且可重复声明,let 是块级作用域且不能重复声明,const 在 let 的基础上禁止重新绑定,三者中 let 与 const 都受暂时性死区保护。数据类型上,7 种原始类型按值存储、对象按引用存储;typeof null 是历史 bug,判断 null 用 ===。类型转换中,+ 遇字符串做拼接、其他运算符转数字、比较一律用 ===;只有 6 个假值,其余全是真值。最后牢记 0.1+0.2 不等于 0.3、NaN 不等于自身,涉及金额与高精度计算时用 bigint 或整数分单位存储。