第3章:类型、值和变量(Types, Values, and Variables)
一、导读
1.1 本章学习目标
第3章深入讲解JavaScript的类型系统。JavaScript的动态类型系统灵活但容易出错,理解类型系统是避免bug的关键。本章核心目标:
- 掌握7种基本类型和对象类型:number、string、boolean、null、undefined、symbol、bigint
- 理解变量声明的三种方式:var、let、const的区别
- 掌握类型转换规则:隐式转换和显式转换
- 学会使用解构赋值:数组解构和对象解构
- 理解相等性判断:==和===的区别
- 掌握展开运算符:数组和对象的展开
1.2 为什么本章如此重要
JavaScript的类型系统是它最容易让人困惑的部分。typeof null 返回 "object","5" + 3 是 "53" 而 "5" - 3 是 2——这些"奇怪"的行为背后都有明确的规则。理解这些规则,才能避免踩坑。
1.3 核心问题引导
为什么 typeof null 返回 "object"?这是bug吗?
var、let、const 有什么本质区别?为什么ES6引入了 let 和 const?
== 和 === 的区别是什么?为什么推荐使用 ===?
解构赋值在什么情况下会失败?
展开运算符是浅拷贝还是深拷贝?
二、核心概念详解
3.1 基本类型
JavaScript有7种基本类型(原始类型)和1种引用类型(Object):
// Number:64位双精度浮点数
let num = 42;
let float = 3.14;
let inf = Infinity;
let nan = NaN;
console.log(typeof NaN); // "number"(NaN是数字类型)
console.log(0.1 + 0.2 === 0.3); // false(浮点精度问题)
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
// String:Unicode字符序列
let str = "hello";
let template = `Hello, ${name}!`;
console.log("hello".length); // 5
console.log("hello"[1]); // "e"
// Boolean:true或false
let bool = true;
console.log(Boolean(0)); // false
console.log(Boolean("")); // false
console.log(Boolean(null)); // false
console.log(Boolean(undefined)); // false
console.log(Boolean(NaN)); // false
// 以上都是falsy值,其他都是truthy
// null:表示"空"或"无"
let empty = null;
console.log(typeof null); // "object"(历史bug)
// undefined:表示"未定义"
let notDefined;
console.log(notDefined); // undefined
console.log(typeof undefined); // "undefined"
// Symbol(ES6):唯一的标识符
let sym1 = Symbol("id");
let sym2 = Symbol("id");
console.log(sym1 === sym2); // false(每个Symbol都是唯一的)
// BigInt(ES2020):任意精度整数
let big = 9007199254740991n;
let bigger = 123456789012345678901234567890n;3.2 变量声明
// var:函数作用域,有变量提升
var x = 1;
if (true) {
var x = 2; // 同一个变量!
}
console.log(x); // 2
// let:块级作用域
let y = 1;
if (true) {
let y = 2; // 不同的变量
console.log(y); // 2
}
console.log(y); // 1
// const:块级作用域,不可重新赋值
const PI = 3.14159;
// PI = 3; // TypeError
// const对象的属性可以修改
const obj = {name: "Alice"};
obj.name = "Bob"; // OK
// obj = {}; // TypeError
// 变量提升
console.log(a); // undefined(var提升)
var a = 1;
// console.log(b); // ReferenceError(let不会提升)
let b = 2;
// 暂时性死区(TDZ)
// 在let/const声明之前访问会报错3.3 类型转换
// 显式转换
console.log(Number("42")); // 42
console.log(Number("hello")); // NaN
console.log(String(42)); // "42"
console.log(Boolean(0)); // false
console.log(Boolean("")); // false
// 隐式转换(自动转换)
console.log("5" + 3); // "53"(+号触发字符串拼接)
console.log("5" - 3); // 2(-号触发数字转换)
console.log("5" * "2"); // 10
console.log("5" == 5); // true(==触发类型转换)
console.log("5" === 5); // false(===不转换)
// 隐式转换规则
// + 运算符:如果有一方是字符串,则拼接
// - * / % 运算符:都转为数字
// 比较运算符:通常转为数字比较
// 常见陷阱
console.log([] + []); // ""(空字符串)
console.log([] + {}); // "[object Object]"
console.log({} + []); // 0({}被解析为空代码块)
console.log(null == undefined); // true
console.log(null === undefined); // false3.4 解构赋值
// 数组解构
let [a, b, c] = [1, 2, 3];
console.log(a, b, c); // 1, 2, 3
// 默认值
let [x = 0, y = 0] = [1];
console.log(x, y); // 1, 0
// 跳过元素
let [first, , third] = [1, 2, 3];
console.log(first, third); // 1, 3
// 剩余元素
let [head, ...tail] = [1, 2, 3, 4];
console.log(head); // 1
console.log(tail); // [2, 3, 4]
// 交换变量
let m = 1, n = 2;
[m, n] = [n, m];
console.log(m, n); // 2, 1
// 对象解构
let {name, age} = {name: "Alice", age: 20};
console.log(name, age); // "Alice", 20
// 重命名
let {name: userName, age: userAge} = {name: "Bob", age: 25};
console.log(userName, userAge); // "Bob", 25
// 默认值
let {title = "Mr", name: n} = {name: "Charlie"};
console.log(title, n); // "Mr", "Charlie"
// 嵌套解构
let {address: {city}} = {address: {city: "Beijing"}};
console.log(city); // "Beijing"
// 函数参数解构
function greet({name, greeting = "Hello"}) {
console.log(`${greeting}, ${name}!`);
}
greet({name: "Alice"}); // "Hello, Alice!"3.5 展开运算符
// 数组展开
let arr1 = [1, 2, 3];
let arr2 = [...arr1, 4, 5];
console.log(arr2); // [1, 2, 3, 4, 5]
let arr3 = [...arr1, ...[4, 5]];
console.log(arr3); // [1, 2, 3, 4, 5]
// 对象展开
let obj1 = {a: 1, b: 2};
let obj2 = {...obj1, c: 3};
console.log(obj2); // {a: 1, b: 2, c: 3}
// 覆盖属性
let obj3 = {...obj1, b: 10};
console.log(obj3); // {a: 1, b: 10}
// 合并对象
let defaults = {theme: "light", lang: "en"};
let userPrefs = {theme: "dark"};
let settings = {...defaults, ...userPrefs};
console.log(settings); // {theme: "dark", lang: "en"}
// 注意:展开是浅拷贝
let nested = {a: {b: 1}};
let copy = {...nested};
copy.a.b = 2;
console.log(nested.a.b); // 2(原对象也被修改了!)3.6 相等性判断
// ==(宽松相等,会类型转换)
console.log(5 == "5"); // true
console.log(0 == false); // true
console.log(null == undefined); // true
console.log("" == 0); // true
console.log([] == false); // true
// ===(严格相等,不转换)
console.log(5 === "5"); // false
console.log(0 === false); // false
console.log(null === undefined); // false
console.log("" === 0); // false
// Object.is(处理特殊值)
console.log(Object.is(NaN, NaN)); // true(NaN === NaN 是 false)
console.log(Object.is(-0, +0)); // false(-0 === +0 是 true)
console.log(Object.is(null, null)); // true
// 最佳实践:始终使用 ===三、实战案例
3.1 类型检查工具
function typeCheck(value) {
if (value === null) return "null";
if (Array.isArray(value)) return "array";
return typeof value;
}
console.log(typeCheck(42)); // "number"
console.log(typeCheck("hello")); // "string"
console.log(typeCheck(true)); // "boolean"
console.log(typeCheck(null)); // "null"
console.log(typeCheck(undefined)); // "undefined"
console.log(typeCheck({})); // "object"
console.log(typeCheck([])); // "array"
console.log(typeCheck(() => {})); // "function"
console.log(typeCheck(Symbol())); // "symbol"3.2 不可变更新模式
// 使用展开运算符实现不可变更新
let user = {name: "Alice", age: 20, address: {city: "Beijing"}};
// 更新属性
let updatedUser = {...user, age: 21};
// 添加属性
let withEmail = {...user, email: "alice@example.com"};
// 删除属性
let {age: _, ...withoutAge} = user;
// 嵌套更新(需要逐层展开)
let updatedAddress = {
...user,
address: {...user.address, city: "Shanghai"}
};四、本章小结
第3章深入讲解了JavaScript的类型系统:
7种基本类型:number、string、boolean、null、undefined、symbol、bigint。加上object共8种。
变量声明:let和const有块级作用域,var是函数作用域。推荐使用const,需要修改时用let。
类型转换:隐式转换规则复杂,推荐使用===避免意外行为。
解构赋值:简化了从数组和对象中提取值的过程。
展开运算符:方便地展开数组和对象,但注意是浅拷贝。
五、思考与练习
思考题
为什么JavaScript需要 let 和 const?var 有什么问题?
null 和 undefined 有什么实际使用上的区别?
为什么 NaN === NaN 是 false?如何正确判断NaN?
解构赋值在什么情况下会抛出错误?
展开运算符的浅拷贝在什么场景下会导致问题?
编程练习
编写函数判断任意值的精确类型(区分数组、null、正则等)。
使用解构赋值实现一个函数,交换对象的两个属性值。
实现深拷贝函数(处理嵌套对象和循环引用)。