3JavaScript高级程序设计(第4版)

类型、值和变量

数据基础

基本类型对象变量类型转换

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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):

javascript
// 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 变量声明

javascript
// 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 类型转换

javascript
// 显式转换
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); // false

3.4 解构赋值

javascript
// 数组解构
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 展开运算符

javascript
// 数组展开
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 相等性判断

javascript
// ==(宽松相等,会类型转换)
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 类型检查工具

javascript
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 不可变更新模式

javascript
// 使用展开运算符实现不可变更新
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、正则等)。

使用解构赋值实现一个函数,交换对象的两个属性值。

实现深拷贝函数(处理嵌套对象和循环引用)。


3/14类型、值
0%