Node.js 开发指南 / 第 4 章 · 变量与数据类型
第 4 章 · 变量与数据类型
4.1 变量声明:let、const、var
三者对比
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 提升(Hoisting) | ✅ 初始化为 undefined | ✅ 但不初始化(TDZ) | ✅ 但不初始化(TDZ) |
| 重复声明 | ✅ 允许 | ❌ 不允许 | ❌ 不允许 |
| 重新赋值 | ✅ 允许 | ✅ 允许 | ❌ 不允许 |
| 全局对象属性 | ✅ window/global | ❌ | ❌ |
| 推荐度 | ❌ 避免使用 | ⚠️ 需要重新赋值时 | ✅ 默认首选 |
var 的问题
// 问题 1:变量提升(Hoisting)
console.log(x); // undefined(不会报错!)
var x = 10;
// 等价于:
var x;
console.log(x);
x = 10;
// 问题 2:没有块级作用域
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:3, 3, 3(而不是 0, 1, 2)
// 问题 3:可以重复声明
var count = 1;
var count = 2; // 不报错,容易掩盖 bug
let 和 const 的改进
// let 有暂时性死区(TDZ)
{
// console.log(y); // ReferenceError!
let y = 10;
console.log(y); // 10
}
// let 解决循环问题
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:0, 1, 2
// const 不可重新赋值
const API_KEY = 'abc123';
// API_KEY = 'xyz'; // TypeError: Assignment to constant variable.
// 但 const 对象的属性可以修改!
const config = { port: 3000 };
config.port = 8080; // 允许!
// config = {}; // TypeError!
// 如果需要完全不可变:
const frozenConfig = Object.freeze({ port: 3000 });
frozenConfig.port = 8080; // 静默失败(严格模式下报错)
console.log(frozenConfig.port); // 3000
最佳实践
// ✅ 默认使用 const
const MAX_RETRIES = 3;
const API_BASE_URL = 'https://api.example.com';
// ✅ 需要重新赋值时使用 let
let currentPage = 1;
let isProcessing = false;
// ❌ 避免使用 var
// var oldStyle = '不要这样写';
// ❌ 不要使用 let 声明不会改变的变量
// let port = 3000; // 应该用 const
4.2 基本类型(Primitive Types)
JavaScript 有 7 种基本类型:
| 类型 | 关键字 | 示例 | typeof 返回值 |
|---|---|---|---|
| 数字 | Number | 42, 3.14, NaN, Infinity | "number" |
| 大整数 | BigInt | 9007199254740991n | "bigint" |
| 字符串 | String | 'hello', "world", `template` | "string" |
| 布尔值 | Boolean | true, false | "boolean" |
| undefined | undefined | undefined | "undefined" |
| null | null | null | "object"(历史遗留 Bug) |
| Symbol | Symbol | Symbol('id') | "symbol" |
Number 类型
// 整数和浮点数
const int = 42;
const float = 3.14;
const negative = -10;
const hex = 0xff; // 255
const octal = 0o777; // 511
const binary = 0b1010; // 10
// 特殊值
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER); // -9007199254740991
console.log(Number.MAX_VALUE); // 1.7976931348623157e+308
console.log(Number.MIN_VALUE); // 5e-324
console.log(Number.POSITIVE_INFINITY); // Infinity
console.log(Number.NEGATIVE_INFINITY); // -Infinity
console.log(Number.NaN); // NaN
// NaN 的特性
console.log(NaN === NaN); // false
console.log(isNaN('hello')); // true(会先转换!)
console.log(Number.isNaN('hello')); // false(推荐使用)
// 浮点精度问题
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
// 解决浮点精度问题
function roundTo(num, decimals) {
return Math.round(num * 10 ** decimals) / 10 ** decimals;
}
console.log(roundTo(0.1 + 0.2, 2)); // 0.3
// 或使用 toFixed
console.log((0.1 + 0.2).toFixed(2)); // "0.3"(返回字符串)
// parseInt 和 parseFloat
console.log(parseInt('42px')); // 42
console.log(parseInt('0xff', 16)); // 255
console.log(parseFloat('3.14abc')); // 3.14
console.log(Number('42')); // 42
console.log(Number('42px')); // NaN
BigInt
// 超过安全整数范围时使用 BigInt
const big = 9007199254740991n;
const alsoBig = BigInt('9007199254740991');
// BigInt 运算
console.log(1n + 2n); // 3n
console.log(10n * 20n); // 200n
// BigInt 和 Number 不能混合运算
// console.log(1n + 1); // TypeError!
console.log(1n + BigInt(1)); // 2n
console.log(Number(1n) + 1); // 2
// 比较可以混合
console.log(1n === 1); // false(不同类型)
console.log(1n == 1); // true(宽松相等)
console.log(2n > 1); // true
String 类型
// 字符串创建方式
const single = 'hello';
const double = "world";
const template = `Hello, ${single} ${double}!`;
// 多行字符串
const multiLine = `
第一行
第二行
第三行
`;
// 常用方法
const str = ' Hello, World! ';
console.log(str.trim()); // 'Hello, World!'
console.log(str.trimStart()); // 'Hello, World! '
console.log(str.trimEnd()); // ' Hello, World!'
console.log(str.includes('World'));// true
console.log(str.startsWith(' ')); // true
console.log(str.endsWith(' ')); // true
console.log(str.indexOf('World')); // 9
console.log(str.replace('World', 'Node')); // ' Hello, Node! '
console.log(str.replaceAll('l', 'L')); // ' HeLLo, WorLd! '
console.log(str.split(', ')); // [' Hello', 'World! ']
console.log(str.slice(2, 7)); // 'Hello'
console.log(str.repeat(2)); // 重复两次
console.log(str.padStart(20, '*'));// '*** Hello, World! '
// 字符串与 Unicode
console.log('😀'.length); // 2(代理对)
console.log([...'😀'].length); // 1(正确计算)
console.log('😀'.codePointAt(0)); // 128512
Symbol 类型
// Symbol 是唯一的标识符
const id1 = Symbol('id');
const id2 = Symbol('id');
console.log(id1 === id2); // false
// 用作对象属性键(避免冲突)
const user = {
[Symbol('name')]: 'Alice',
[Symbol('name')]: 'Bob',
};
// 两个 name 属性互不冲突
// 全局 Symbol 注册表
const globalId = Symbol.for('app.id');
const sameId = Symbol.for('app.id');
console.log(globalId === sameId); // true
console.log(Symbol.keyFor(globalId)); // 'app.id'
4.3 引用类型
Object
// 对象创建
const user = {
name: 'Alice',
age: 30,
hobbies: ['reading', 'coding'],
address: {
city: 'Beijing',
district: 'Haidian',
},
greet() {
return `Hi, I'm ${this.name}`;
},
};
// 可选链操作符(Optional Chaining)
console.log(user.address?.city); // 'Beijing'
console.log(user.phone?.number); // undefined
// 空值合并操作符(Nullish Coalescing)
const port = user.port ?? 3000; // 3000(null 或 undefined 时使用默认值)
const name = user.name ?? 'Guest'; // 'Alice'(已有值不替换)
// 展开运算符
const updated = { ...user, age: 31, role: 'admin' };
// 解构赋值
const { name: userName, age, role = 'user' } = user;
console.log(userName, age, role); // Alice 30 user
Array
// 数组创建
const arr = [1, 2, 3, 4, 5];
const arr2 = new Array(5).fill(0); // [0, 0, 0, 0, 0]
const arr3 = Array.from({ length: 5 }, (_, i) => i); // [0, 1, 2, 3, 4]
// 常用方法(不修改原数组)
console.log(arr.map(x => x * 2)); // [2, 4, 6, 8, 10]
console.log(arr.filter(x => x > 3)); // [4, 5]
console.log(arr.reduce((sum, x) => sum + x, 0)); // 15
console.log(arr.find(x => x > 3)); // 4
console.log(arr.findIndex(x => x > 3)); // 3
console.log(arr.every(x => x > 0)); // true
console.log(arr.some(x => x > 4)); // true
console.log(arr.includes(3)); // true
console.log(arr.flat()); // 展平嵌套数组
// 修改原数组的方法
arr.push(6); // 末尾添加
arr.pop(); // 末尾删除
arr.unshift(0); // 头部添加
arr.shift(); // 头部删除
arr.splice(1, 1); // 从索引 1 删除 1 个元素
arr.sort((a, b) => a - b); // 排序
arr.reverse(); // 反转
// 解构赋值
const [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(first, second, rest); // 1 2 [3, 4, 5]
Date
// 创建日期
const now = new Date();
const specific = new Date('2026-05-10T12:00:00+08:00');
const fromParts = new Date(2026, 4, 10); // 月份从 0 开始
// 获取日期部分
console.log(now.getFullYear()); // 2026
console.log(now.getMonth()); // 0-11
console.log(now.getDate()); // 1-31
console.log(now.getDay()); // 0-6(周日=0)
console.log(now.getHours()); // 0-23
console.log(now.getMinutes()); // 0-59
console.log(now.getSeconds()); // 0-59
console.log(now.getTime()); // 毫秒时间戳
// ISO 格式
console.log(now.toISOString()); // 2026-05-10T04:00:00.000Z
console.log(now.toISOString().split('T')[0]); // '2026-05-10'
// 日期计算
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
const diffMs = specific - now; // 毫秒差
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
// 推荐使用第三方库 date-fns 或 dayjs 进行复杂日期操作
RegExp
// 正则表达式创建
const re1 = /pattern/flags;
const re2 = new RegExp('pattern', 'flags');
// 常用标志
// g - 全局匹配
// i - 忽略大小写
// m - 多行模式
// s - dotAll 模式(. 匹配换行符)
// u - Unicode 模式
// 基本使用
const emailRe = /^[\w.-]+@[\w.-]+\.\w+$/;
console.log(emailRe.test('user@example.com')); // true
console.log(emailRe.test('invalid')); // false
// 提取匹配
const str = 'Hello, Alice! Hello, Bob!';
const matches = str.matchAll(/Hello, (\w+)!/g);
for (const match of matches) {
console.log(match[0]); // 完整匹配
console.log(match[1]); // 捕获组
}
// 命名捕获组
const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2026-05-10'.match(dateRe);
console.log(match.groups); // { year: '2026', month: '05', day: '10' }
4.4 类型检测与转换
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"
console.log(typeof Symbol()); // "symbol"
console.log(typeof 42n); // "bigint"
更准确的类型检测
// 使用 Object.prototype.toString
function getType(value) {
return Object.prototype.toString.call(value).slice(8, -1);
}
console.log(getType(42)); // "Number"
console.log(getType('hello')); // "String"
console.log(getType(null)); // "Null"
console.log(getType([])); // "Array"
console.log(getType({})); // "Object"
console.log(getType(/regex/)); // "RegExp"
console.log(getType(new Date()));// "Date"
// Array 检测
console.log(Array.isArray([])); // true
console.log(Array.isArray({})); // false
// null 检测
console.log(value === null); // 只能用 === 比较
// NaN 检测
console.log(Number.isNaN(NaN)); // true(推荐)
console.log(Object.is(NaN, NaN)); // true
类型转换规则
// 隐式转换(容易出错!)
console.log('5' + 3); // "53"(字符串拼接)
console.log('5' - 3); // 2(数学运算)
console.log('5' * 2); // 10
console.log(true + 1); // 2
console.log(false + ''); // "false"
console.log(null + 1); // 1
console.log(undefined + 1); // NaN
// == 和 === 的区别
console.log(0 == ''); // true(类型转换后相等)
console.log(0 === ''); // false(类型不同)
console.log(null == undefined); // true
console.log(null === undefined); // false
// 显式转换
console.log(Number('42')); // 42
console.log(Number('')); // 0
console.log(Number(true)); // 1
console.log(Number(null)); // 0
console.log(Number(undefined)); // NaN
console.log(String(42)); // "42"
console.log(String(true)); // "true"
console.log(String(null)); // "null"
console.log(Boolean(0)); // false
console.log(Boolean('')); // false
console.log(Boolean(null)); // false
console.log(Boolean(undefined)); // false
console.log(Boolean(NaN)); // false
console.log(Boolean('0')); // true(非空字符串为 true)
console.log(Boolean([])); // true(空数组为 true!)
console.log(Boolean({})); // true(空对象为 true!)
Truthy 和 Falsy 值
// Falsy 值(以下 8 个值转换为 false)
const falsyValues = [
false, 0, -0, 0n, '', null, undefined, NaN
];
// 其他所有值都是 Truthy(转换为 true)
// 包括容易混淆的:
console.log(Boolean([])); // true
console.log(Boolean({})); // true
console.log(Boolean('0')); // true
console.log(Boolean('false')); // true
4.5 值传递与引用传递
// 基本类型:值传递
let a = 10;
let b = a;
b = 20;
console.log(a); // 10(不受影响)
// 引用类型:引用传递
const obj1 = { name: 'Alice' };
const obj2 = obj1;
obj2.name = 'Bob';
console.log(obj1.name); // 'Bob'(被修改了!)
// 安全复制对象
const original = { name: 'Alice', address: { city: 'Beijing' } };
// 浅拷贝
const shallow = { ...original };
shallow.name = 'Bob';
console.log(original.name); // 'Alice'(不受影响)
shallow.address.city = 'Shanghai';
console.log(original.address.city); // 'Shanghai'(受影响!)
// 深拷贝
const deep = structuredClone(original); // Node.js 17+
deep.address.city = 'Guangzhou';
console.log(original.address.city); // 'Shanghai'(不受影响)
// 也可以使用 JSON(有局限性)
const jsonDeep = JSON.parse(JSON.stringify(original));
// 局限性:无法处理 Date、RegExp、函数、undefined、循环引用
注意事项
⚠️ 始终使用
===进行比较:==会进行隐式类型转换,导致不可预期的结果。唯一的例外是value == null可以同时检查null和undefined。
⚠️ 注意
typeof []返回"object":检测数组请使用Array.isArray()。
⚠️ 注意
typeof null返回"object":这是 JavaScript 的历史遗留 Bug,检测 null 请使用value === null。
⚠️ 浮点数精度问题:不要用
===比较浮点数计算结果,使用Math.abs(a - b) < Number.EPSILON或第三方库。
⚠️ 对象浅拷贝 vs 深拷贝:解构赋值和展开运算符只做浅拷贝,嵌套对象仍然共享引用。
业务场景
- 表单数据验证:理解类型转换规则,正确处理用户输入
- API 响应处理:使用可选链和空值合并安全访问嵌套数据
- 数据去重:利用
Set和Array.from实现数组去重 - 不可变数据:使用
Object.freeze和structuredClone确保数据不被意外修改
扩展阅读
- MDN JavaScript 数据类型
- JavaScript 类型转换表
- tc39 提案 — 跟踪 JavaScript 新特性
上一章:第 3 章 · Hello World 下一章:第 5 章 · 模块系统 — 深入理解 CommonJS、ESM 和模块解析机制。