Skip to content

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 = [123];
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(nameage) {
  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(([keyvalue]) => {
  console.log(`${key}: ${value}`);
});

Array 数组类型

数组是有序的数据集合,就像一排编了号的储物柜,每个位置(索引)可以存放一个值。

创建数组

javascript
// 方式 1:数组字面量(最常用)
let fruits = ["apple""banana""orange"];
let numbers = [12345];
let mixed = [1"hello"truenull, { name: "Test" }];

// 方式 2:Array 构造函数
let arr1 = new Array(); // 空数组
let arr2 = new Array(5); // 长度为 5 的空数组
let arr3 = new Array(123); // [1, 2, 3]

// 方式 3:Array.of()(ES6)
let arr4 = Array.of(5); // [5](避免构造函数的歧义)
let arr5 = Array.of(123); // [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 = [12345];

// 添加/删除元素
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 = [12345];
arr.splice(21); // 从索引 2 删除 1 个:[1, 2, 4, 5]
arr.splice(203); // 在索引 2 插入 3:[1, 2, 3, 4, 5]
arr.splice(2199); // 在索引 2 替换 1 个为 99:[1, 2, 99, 4, 5]

// slice:提取子数组(不修改原数组)
let original = [12345];
let sub = original.slice(14); // [2, 3, 4]
console.log(original); // [1, 2, 3, 4, 5](原数组不变)

// concat:合并数组
let arr1 = [12];
let arr2 = [34];
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 = [123];
nums.reverse();
console.log(nums); // [3, 2, 1]

// sort:排序(修改原数组)
let values = [314159];
values.sort();
console.log(values); // [1, 1, 3, 4, 5, 9]

// 自定义排序
values.sort((ab) => 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 (fruitindex) {
  console.log(index + ": " + fruit);
});

// 简化写法
fruits.forEach((fruitindex) => {
  console.log(`${index}: ${fruit}`);
});

数组的高级方法

javascript
let numbers = [12345];

// 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((accnum) => 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); // true

Function 函数类型

函数在 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 (ab) {
  return a + b;
};

console.log(calculate(53)); // 8

// 函数作为参数(回调函数)
function executeOperation(aboperation) {
  return operation(a, b);
}

let add = (xy) => x + y;
let multiply = (xy) => x * y;

console.log(executeOperation(53, add)); // 8
console.log(executeOperation(53, 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: [859095],
};

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:可执行的代码块,也是对象
  • 引用类型的比较是比较引用,不是比较值
  • 函数参数传递引用类型时,传递的是引用的副本
  • 理解浅拷贝与深拷贝的区别和应用场景
  • 基本类型和引用类型在赋值、比较、传递时的行为完全不同