强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 4 章 · 变量与数据类型

第 4 章 · 变量与数据类型

4.1 变量声明:let、const、var

三者对比

特性varletconst
作用域函数作用域块级作用域块级作用域
提升(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 返回值
数字Number42, 3.14, NaN, Infinity"number"
大整数BigInt9007199254740991n"bigint"
字符串String'hello', "world", `template`"string"
布尔值Booleantrue, false"boolean"
undefinedundefinedundefined"undefined"
nullnullnull"object"(历史遗留 Bug)
SymbolSymbolSymbol('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('[email protected]')); // 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 可以同时检查 nullundefined

⚠️ 注意 typeof [] 返回 "object":检测数组请使用 Array.isArray()

⚠️ 注意 typeof null 返回 "object":这是 JavaScript 的历史遗留 Bug,检测 null 请使用 value === null

⚠️ 浮点数精度问题:不要用 === 比较浮点数计算结果,使用 Math.abs(a - b) < Number.EPSILON 或第三方库。

⚠️ 对象浅拷贝 vs 深拷贝:解构赋值和展开运算符只做浅拷贝,嵌套对象仍然共享引用。

业务场景

  1. 表单数据验证:理解类型转换规则,正确处理用户输入
  2. API 响应处理:使用可选链和空值合并安全访问嵌套数据
  3. 数据去重:利用 SetArray.from 实现数组去重
  4. 不可变数据:使用 Object.freezestructuredClone 确保数据不被意外修改

扩展阅读


上一章第 3 章 · Hello World 下一章第 5 章 · 模块系统 — 深入理解 CommonJS、ESM 和模块解析机制。