JavaScript 引用数据类型:对象、数组与函数
如果说基本数据类型是乐高积木中的单个砖块,那么引用数据类型就是用这些砖块搭建出来的复杂模型。它们可以包含多个值,可以嵌套组合,能够表达更复杂的数据结构和业务逻辑。
引用类型 vs 基本类型
在深入学习引用类型之前,我们需要理解它与基本类型的根本区别。
存储方式的区别
javascript
// 基本类型:直接存储值
let num1 = 42;
let num2 = num1; // 复制值
num2 = 100; // 修改 num2 不影响 num1
console.log(num1); // 42
console.log(num2); // 100
// 引用类型:存储引用(地址)
let obj1 = { name: "Alice" };
let obj2 = obj1; // 复制引用,不是复制对象
obj2.name = "Bob"; // 通过 obj2 修改,obj1 也会变!
console.log(obj1.name); // "Bob"
console.log(obj2.name); // "Bob"这就像买房子:基本类型是直接拥有房子的副本,而引用类型是拿到了房子的地址。改变地址指向的房子,所有持有这个地址的人都会看到变化。
比较方式的区别
javascript
// 基本类型:比较值
console.log(5 === 5); // true
console.log("hello" === "hello"); // true
// 引用类型:比较引用(地址)
console.log({} === {}); // false(不同的对象,不同的地址)
console.log([] === []); // false(不同的数组,不同的地址)
let arr1 = [1, 2, 3];
let arr2 = arr1; // 同一个引用
console.log(arr1 === arr2); // true(指向同一个数组)
// 即使内容完全相同,如果是不同对象,也不相等
let person1 = { name: "Charlie", age: 30 };
let person2 = { name: "Charlie", age: 30 };
console.log(person1 === person2); // false(不同的对象)Object 对象类型
对象是 JavaScript 中最重要的数据类型,它像一个装满标签盒子的储物柜,每个盒子(属性)都有一个标签(键)和内容(值)。
创建对象
javascript
// 方式 1:对象字面量(最常用)
let user = {
name: "David",
age: 28,
email: "[email protected]",
};
// 方式 2:new Object()
let person = new Object();
person.name = "Emma";
person.age = 32;
// 方式 3:Object.create()
let student = Object.create(null);
student.grade = "A";
// 方式 4:构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
let john = new Person("John", 25);访问和修改属性
javascript
let user = {
name: "Sarah",
age: 27,
email: "[email protected]",
};
// 点语法访问
console.log(user.name); // "Sarah"
console.log(user.age); // 27
// 方括号语法访问
console.log(user["email"]); // "[email protected]"
// 修改属性
user.age = 28;
user["email"] = "[email protected]";
// 添加新属性
user.city = "London";
user["country"] = "UK";
console.log(user);
// {
// name: "Sarah",
// age: 28,
// email: "[email protected]",
// city: "London",
// country: "UK"
// }
// 删除属性
delete user.country;
console.log(user.country); // undefined方括号语法的应用场景
方括号语法更灵活,可以使用变量和特殊字符:
javascript
// 使用变量作为属性名
let propertyName = "age";
console.log(user[propertyName]); // 28
// 属性名包含空格或特殊字符
let config = {
"api-key": "abc123",
"user name": "Tom",
"is-active": true,
};
console.log(config["api-key"]); // "abc123"
console.log(config["user name"]); // "Tom"
// config.api-key // 这样会报错!
// 动态属性名
let key = "score";
let value = 95;
let exam = {
[key]: value, // ES6 计算属性名
};
console.log(exam.score); // 95对象的方法
对象可以包含函数作为属性,这些函数叫做方法:
javascript
let calculator = {
value: 0,
add: function (num) {
this.value += num;
return this;
},
subtract: function (num) {
this.value -= num;
return this;
},
// ES6 简化写法
multiply(num) {
this.value *= num;
return this;
},
getValue() {
return this.value;
},
};
calculator.add(10).multiply(2).subtract(5);
console.log(calculator.getValue()); // 15检查属性是否存在
javascript
let user = {
name: "Oliver",
age: 35,
};
// 方式 1:in 运算符
console.log("name" in user); // true
console.log("email" in user); // false
// 方式 2:hasOwnProperty 方法
console.log(user.hasOwnProperty("age")); // true
console.log(user.hasOwnProperty("email")); // false
// 方式 3:直接访问并检查(注意:值为 undefined 时也返回 false)
console.log(user.name !== undefined); // true
console.log(user.email !== undefined); // false遍历对象
javascript
let product = {
name: "Laptop",
price: 1200,
brand: "TechCorp",
inStock: true,
};
// for...in 循环
for (let key in product) {
console.log(key + ": " + product[key]);
}
// name: Laptop
// price: 1200
// brand: TechCorp
// inStock: true
// Object.keys()
let keys = Object.keys(product);
console.log(keys); // ["name", "price", "brand", "inStock"]
// Object.values()
let values = Object.values(product);
console.log(values); // ["Laptop", 1200, "TechCorp", true]
// Object.entries()
let entries = Object.entries(product);
console.log(entries);
// [
// ["name", "Laptop"],
// ["price", 1200],
// ["brand", "TechCorp"],
// ["inStock", true]
// ]
entries.forEach(([key, value]) => {
console.log(`${key}: ${value}`);
});Array 数组类型
数组是有序的数据集合,就像一排编了号的储物柜,每个位置(索引)可以存放一个值。
创建数组
javascript
// 方式 1:数组字面量(最常用)
let fruits = ["apple", "banana", "orange"];
let numbers = [1, 2, 3, 4, 5];
let mixed = [1, "hello", true, null, { name: "Test" }];
// 方式 2:Array 构造函数
let arr1 = new Array(); // 空数组
let arr2 = new Array(5); // 长度为 5 的空数组
let arr3 = new Array(1, 2, 3); // [1, 2, 3]
// 方式 3:Array.of()(ES6)
let arr4 = Array.of(5); // [5](避免构造函数的歧义)
let arr5 = Array.of(1, 2, 3); // [1, 2, 3]
// 方式 4:Array.from()(ES6)
let str = "hello";
let chars = Array.from(str); // ["h", "e", "l", "l", "o"]访问和修改数组元素
javascript
let colors = ["red", "green", "blue"];
// 访问元素(索引从 0 开始)
console.log(colors[0]); // "red"
console.log(colors[1]); // "green"
console.log(colors[2]); // "blue"
// 修改元素
colors[1] = "yellow";
console.log(colors); // ["red", "yellow", "blue"]
// 添加元素
colors[3] = "purple";
console.log(colors); // ["red", "yellow", "blue", "purple"]
// 数组长度
console.log(colors.length); // 4
// 访问最后一个元素
console.log(colors[colors.length - 1]); // "purple"数组的常用方法
javascript
let numbers = [1, 2, 3, 4, 5];
// 添加/删除元素
numbers.push(6); // 末尾添加:[1, 2, 3, 4, 5, 6]
numbers.pop(); // 末尾删除:[1, 2, 3, 4, 5]
numbers.unshift(0); // 开头添加:[0, 1, 2, 3, 4, 5]
numbers.shift(); // 开头删除:[1, 2, 3, 4, 5]
// splice:删除/插入/替换
let arr = [1, 2, 3, 4, 5];
arr.splice(2, 1); // 从索引 2 删除 1 个:[1, 2, 4, 5]
arr.splice(2, 0, 3); // 在索引 2 插入 3:[1, 2, 3, 4, 5]
arr.splice(2, 1, 99); // 在索引 2 替换 1 个为 99:[1, 2, 99, 4, 5]
// slice:提取子数组(不修改原数组)
let original = [1, 2, 3, 4, 5];
let sub = original.slice(1, 4); // [2, 3, 4]
console.log(original); // [1, 2, 3, 4, 5](原数组不变)
// concat:合并数组
let arr1 = [1, 2];
let arr2 = [3, 4];
let combined = arr1.concat(arr2); // [1, 2, 3, 4]
// join:转换为字符串
let fruits = ["apple", "banana", "orange"];
console.log(fruits.join()); // "apple,banana,orange"
console.log(fruits.join(" - ")); // "apple - banana - orange"
// reverse:反转数组(修改原数组)
let nums = [1, 2, 3];
nums.reverse();
console.log(nums); // [3, 2, 1]
// sort:排序(修改原数组)
let values = [3, 1, 4, 1, 5, 9];
values.sort();
console.log(values); // [1, 1, 3, 4, 5, 9]
// 自定义排序
values.sort((a, b) => b - a); // 降序
console.log(values); // [9, 5, 4, 3, 1, 1]数组的遍历
javascript
let fruits = ["apple", "banana", "orange"];
// for 循环
for (let i = 0; i < fruits.length; i++) {
console.log(fruits[i]);
}
// for...of 循环(ES6,推荐)
for (let fruit of fruits) {
console.log(fruit);
}
// forEach 方法
fruits.forEach(function (fruit, index) {
console.log(index + ": " + fruit);
});
// 简化写法
fruits.forEach((fruit, index) => {
console.log(`${index}: ${fruit}`);
});数组的高级方法
javascript
let numbers = [1, 2, 3, 4, 5];
// map:映射转换
let doubled = numbers.map((num) => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// filter:过滤
let evens = numbers.filter((num) => num % 2 === 0);
console.log(evens); // [2, 4]
// reduce:归约
let sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // 15
// find:查找第一个符合条件的元素
let found = numbers.find((num) => num > 3);
console.log(found); // 4
// some:检查是否有元素符合条件
let hasEven = numbers.some((num) => num % 2 === 0);
console.log(hasEven); // true
// every:检查是否所有元素都符合条件
let allPositive = numbers.every((num) => num > 0);
console.log(allPositive); // trueFunction 函数类型
函数在 JavaScript 中是第一类对象(First-class Object),可以像变量一样传递、赋值和作为返回值。
函数的创建方式
javascript
// 方式 1:函数声明
function greet(name) {
return "Hello, " + name + "!";
}
// 方式 2:函数表达式
let sayHi = function (name) {
return "Hi, " + name + "!";
};
// 方式 3:箭头函数(ES6)
let welcome = (name) => {
return "Welcome, " + name + "!";
};
// 箭头函数简化写法
let hey = (name) => "Hey, " + name + "!";
// 方式 4:Function 构造函数(不推荐)
let multiply = new Function("a", "b", "return a * b");函数作为值
javascript
// 函数赋值给变量
let calculate = function (a, b) {
return a + b;
};
console.log(calculate(5, 3)); // 8
// 函数作为参数(回调函数)
function executeOperation(a, b, operation) {
return operation(a, b);
}
let add = (x, y) => x + y;
let multiply = (x, y) => x * y;
console.log(executeOperation(5, 3, add)); // 8
console.log(executeOperation(5, 3, multiply)); // 15
// 函数作为返回值(高阶函数)
function createMultiplier(factor) {
return function (number) {
return number * factor;
};
}
let double = createMultiplier(2);
let triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15其他引用类型
Date 日期对象
javascript
// 创建日期对象
let now = new Date();
let specificDate = new Date("2024-01-15");
let timestamp = new Date(1609459200000);
console.log(now); // 当前日期时间
// 日期方法
console.log(now.getFullYear()); // 年份
console.log(now.getMonth()); // 月份(0-11)
console.log(now.getDate()); // 日期(1-31)
console.log(now.getDay()); // 星期(0-6,0 是周日)
console.log(now.getHours()); // 小时
console.log(now.getMinutes()); // 分钟
console.log(now.getTime()); // 时间戳(毫秒)RegExp 正则表达式
javascript
// 创建正则表达式
let pattern1 = /hello/i; // 字面量方式
let pattern2 = new RegExp("world", "i"); // 构造函数方式
// 测试匹配
let text = "Hello World";
console.log(/hello/i.test(text)); // true
// 查找匹配
let email = "[email protected]";
let match = email.match(/@(.+)$/);
console.log(match[1]); // "example.com"值传递 vs 引用传递
这是理解 JavaScript 的关键概念:
javascript
// 基本类型:值传递
function changeNum(x) {
x = 100;
}
let num = 42;
changeNum(num);
console.log(num); // 42(不受影响)
// 引用类型:引用传递
function changePerson(obj) {
obj.name = "Changed";
}
let person = { name: "Original" };
changePerson(person);
console.log(person.name); // "Changed"(被修改了!)
// 但是重新赋值不会影响外部
function reassignPerson(obj) {
obj = { name: "New Object" };
}
reassignPerson(person);
console.log(person.name); // "Changed"(没有变成 "New Object")深拷贝与浅拷贝
浅拷贝
javascript
// 方式 1:展开运算符
let original = { name: "Alice", age: 25 };
let copy1 = { ...original };
copy1.age = 30;
console.log(original.age); // 25(不受影响)
// 方式 2:Object.assign()
let copy2 = Object.assign({}, original);
// 浅拷贝的问题:嵌套对象仍是引用
let user = {
name: "Bob",
address: {
city: "New York",
},
};
let userCopy = { ...user };
userCopy.address.city = "London";
console.log(user.address.city); // "London"(被影响了!)深拷贝
javascript
// 方式 1:JSON 方式(简单但有限制)
let original = {
name: "Charlie",
scores: [85, 90, 95],
};
let deepCopy = JSON.parse(JSON.stringify(original));
deepCopy.scores[0] = 100;
console.log(original.scores[0]); // 85(不受影响)
// 方式 2:递归实现
function deepClone(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(deepClone);
}
let clone = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}
return clone;
}总结
引用类型是 JavaScript 编程的核心,理解它们的工作原理对于编写高质量代码至关重要。
本节要点回顾:
- 引用类型存储的是引用(地址),不是值本身
- Object:键值对集合,JavaScript 中最基础的引用类型
- Array:有序的值的集合,提供丰富的操作方法
- Function:可执行的代码块,也是对象
- 引用类型的比较是比较引用,不是比较值
- 函数参数传递引用类型时,传递的是引用的副本
- 理解浅拷贝与深拷贝的区别和应用场景
- 基本类型和引用类型在赋值、比较、传递时的行为完全不同