一、对象的基本概念
1.1 什么是对象
JavaScript 中的对象是键值对(key-value)的集合,是一种复合数据类型。它具有以下特点:
- 键(key):必须是字符串或 Symbol 类型,作为属性名使用
- 值(value):可以是任意JavaScript数据类型,包括:
- 基本类型(number, string, boolean, null, undefined)
- 函数(此时称为"方法")
- 其他对象或数组
- 甚至是更复杂的数据结构
对象本质上是一种无序的数据结构,用于描述现实世界中的实体,包含其属性和行为。在JavaScript中,几乎所有东西都是对象或可以表现为对象(如数组、函数等)。
详细示例
// 描述"用户"的对象
const user = {
// 基本属性
name: "张三", // 字符串类型属性
age: 25, // 数字类型属性
isStudent: false, // 布尔类型属性
address: null, // null值属性
// 方法(函数作为属性值)
sayHello: function() {
console.log(`大家好,我是${this.name}`);
},
// 嵌套对象
contact: {
email: "zhangsan@example.com",
phone: "13800138000"
},
// 数组属性
hobbies: ["阅读", "游泳", "编程"],
// 计算属性名(ES6特性)
[Symbol.for('id')]: 12345,
// 简写方法定义(ES6特性)
introduce() {
console.log(`我叫${this.name},今年${this.age}岁`);
}
};
// 访问对象属性
console.log(user.name); // "张三"
console.log(user["age"]); // 25
user.sayHello(); // 调用方法
// 动态添加新属性
user.gender = "男";
user["occupation"] = "工程师";
// 删除属性
delete user.address;
1.2 对象的核心特性
1. 动态性
JavaScript 对象具有高度动态性:
- 属性可动态增删:不需要预先定义类或结构
- 属性可随时修改:包括值和其特性(可枚举性、可写性等)
- 支持多种属性定义方式:
- 字面量定义
- 动态添加
- 通过Object.defineProperty定义
const car = { make: "Toyota" };
// 动态添加属性
car.model = "Camry";
car.year = 2020;
// 动态添加方法
car.start = function() {
console.log("Engine started!");
};
// 修改属性
car.year = 2021;
// 删除属性
delete car.model;
2. 引用类型
- 存储机制:对象存储在堆内存中,变量保存的是指向对象的引用地址
- 赋值行为:赋值对象时传递的是引用而非对象副本
- 比较行为:比较的是引用地址而非对象内容
const obj1 = { a: 1 };
const obj2 = obj1; // obj2 和 obj1 指向同一个对象
obj2.a = 2;
console.log(obj1.a); // 2,因为两者引用同一对象
const obj3 = { a: 1 };
const obj4 = { a: 1 };
console.log(obj3 === obj4); // false,因为引用地址不同
3. 原型继承
- 原型链:每个对象都有一个原型对象(通过
__proto__
访问) - 继承机制:对象可以继承其原型的属性和方法
- 原型方法查找:访问属性时,先查找对象自身,再沿原型链向上查找
// 创建一个对象
const animal = {
eats: true,
walk() {
console.log("Animal walking");
}
};
// 以animal为原型创建新对象
const rabbit = {
jumps: true,
__proto__: animal // 设置原型
};
// rabbit可以访问animal的属性和方法
console.log(rabbit.eats); // true
rabbit.walk(); // "Animal walking"
// 原型方法可以被覆盖
rabbit.walk = function() {
console.log("Rabbit hopping");
};
rabbit.walk(); // "Rabbit hopping"(覆盖了原型方法)
二、对象的创建方式
2.1 字面量方式(最常用)
字面量方式是创建对象最简洁直观的方式,直接使用花括号{}
定义对象内容,适合创建不需要复用逻辑的单个对象。这种语法在ES6中得到了增强,支持更简洁的方法定义方式。
语法详解:
const obj = {
// 普通属性
key1: value1,
key2: value2,
// 计算属性名(ES6+)
[dynamicKey]: computedValue,
// 方法的简写形式(ES6+)
methodName() {
// 方法体
},
// 传统方法定义方式
oldMethod: function() {
// 方法体
}
};
实际应用示例:
const book = {
// 基本属性
title: "JavaScript高级程序设计",
author: "Nicholas C. Zakas",
publishYear: 2020,
publisher: "人民邮电出版社",
ISBN: "9787115545388",
// 方法简写
getInfo() {
return `${this.title}(${this.author}著,${this.publishYear}年出版)`;
},
// 带参数的方法
setDiscount(rate) {
this.discountRate = rate;
console.log(`已设置${rate * 100}%折扣`);
},
// 使用计算属性名
["book" + "Status"]: "在售"
};
console.log(book.getInfo()); // 输出:JavaScript高级程序设计(Nicholas C. Zakas著,2020年出版)
book.setDiscount(0.8); // 输出:已设置80%折扣
console.log(book.bookStatus); // 输出:在售
2.2 构造函数方式
构造函数方式适合需要批量创建具有相同结构的对象场景,可以通过内置的Object
构造函数或自定义构造函数来实现。
2.2.1 内置构造函数Object
使用new Object()
可以创建一个空对象,然后动态添加属性。这种方式不如字面量简洁,但在某些动态场景下有用。
更完整的示例:
// 创建空对象
const employee = new Object();
// 动态添加属性
employee.name = "张明";
employee.department = "研发部";
employee.position = "高级工程师";
employee.id = "DEV0025";
// 动态添加方法
employee.showInfo = function() {
console.log(`${this.name} - ${this.department} ${this.position} (ID: ${this.id})`);
};
// 直接传入键值对创建对象
const project = new Object({
name: "电商平台重构",
deadline: "2023-12-31",
budget: 500000,
manager: "王芳",
getStatus() {
return `${this.name}项目由${this.manager}负责,预算${this.budget}元`;
}
});
employee.showInfo(); // 输出:张明 - 研发部 高级工程师 (ID: DEV0025)
console.log(project.getStatus()); // 输出:电商平台重构项目由王芳负责,预算500000元
2.2.2 自定义构造函数
自定义构造函数适合创建具有相同属性和方法的多个对象,是面向对象编程的基础。
扩展的Person构造函数示例:
// 改进的Person构造函数
function Person(name, age, gender) {
// 验证参数
if (!name || typeof name !== 'string') {
throw new Error('姓名必须是非空字符串');
}
// 实例属性
this.name = name;
this.age = age;
this.gender = gender || '未知';
this.createdAt = new Date(); // 记录创建时间
// 实例方法
this.introduce = function() {
console.log(`我叫${this.name},今年${this.age}岁,性别${this.gender}`);
};
this.setAge = function(newAge) {
if (newAge < 0 || newAge > 150) {
console.error('年龄不合法');
return;
}
this.age = newAge;
};
// 静态方法(通过原型共享)
Person.prototype.getBirthYear = function() {
const currentYear = new Date().getFullYear();
return currentYear - this.age;
};
}
// 创建实例
const person1 = new Person("王五", 22, "男");
const person2 = new Person("赵六", 28, "女");
const person3 = new Person("钱七", 35);
// 使用实例
person1.introduce(); // 输出:我叫王五,今年22岁,性别男
person2.introduce(); // 输出:我叫赵六,今年28岁,性别女
person3.introduce(); // 输出:我叫钱七,今年35岁,性别未知
console.log(person1.getBirthYear()); // 输出出生年份
person1.setAge(23); // 修改年龄
person1.introduce(); // 输出:我叫王五,今年23岁,性别男
// 错误示例
// const invalidPerson = new Person("", 30); // 抛出错误:姓名必须是非空字符串
// const invalidPerson2 = new Person(123, 30); // 抛出错误:姓名必须是非空字符串
2.3 Object.create()方式
Object.create()
方法创建一个新对象,使用现有的对象作为新创建对象的原型。这是实现原型继承的核心方法,也是ES5中引入的创建对象的重要方式。
更详细的语法说明:
const newObj = Object.create(protoObj, {
// 属性描述符对象
property1: {
value: '初始值',
writable: true, // 是否可修改
enumerable: true, // 是否可枚举
configurable: true // 是否可删除或修改属性特性
},
// 更多属性...
});
扩展的动物管理示例:
// 更完整的原型对象
const animalProto = {
// 原型方法
eat() {
console.log(`${this.name}正在吃${this.food || '食物'}`);
},
sleep() {
console.log(`${this.name}正在睡觉`);
},
// 原型属性
isAlive: true,
// 原型方法
die() {
this.isAlive = false;
console.log(`${this.name}已经死亡`);
}
};
// 创建新对象并指定属性特性
const dog = Object.create(animalProto, {
name: {
value: "旺财",
writable: true,
enumerable: true,
configurable: false
},
breed: {
value: "金毛",
enumerable: true
},
age: {
value: 5,
writable: true
},
food: {
value: "狗粮",
enumerable: true
},
// 私有属性(不可枚举)
_vaccinations: {
value: ["狂犬疫苗", "细小病毒疫苗"],
enumerable: false
}
});
// 使用对象
dog.eat(); // 输出:旺财正在吃狗粮
dog.sleep(); // 输出:旺财正在睡觉
// 修改属性
dog.name = "大黄"; // 成功(writable为true)
console.log(dog.name); // 输出:大黄
// 尝试删除属性
delete dog.breed; // 失败(configurable默认为false)
console.log(dog.breed); // 输出:金毛
// 检查原型链
console.log(Object.getPrototypeOf(dog) === animalProto); // true
// 创建另一个对象
const cat = Object.create(animalProto);
cat.name = "咪咪";
cat.food = "猫粮";
cat.eat(); // 输出:咪咪正在吃猫粮
// 原型方法共享
console.log(dog.eat === cat.eat); // true
// 检查属性枚举
console.log(Object.keys(dog)); // ["name", "breed", "food"](不包括不可枚举属性)
三、对象属性的操作
3.1 属性的访问与修改
3.1.1 点语法(常用)
点语法是访问对象属性的最常用方式,适用于以下场景:
- 属性名是合法的JavaScript标识符(不包含空格、连字符等特殊字符)
- 属性名不是JavaScript保留关键字
- 属性名已知且固定(不是动态生成的)
典型应用场景示例:
- 访问嵌套对象属性
- 调用对象方法
- 读取简单的配置项
const car = {
brand: "Tesla",
model: "Model 3",
features: {
autopilot: true,
battery: "75kWh"
},
startEngine: function() {
console.log("Engine started");
}
};
// 访问嵌套属性
console.log(car.features.autopilot); // 输出:true
// 调用方法
car.startEngine(); // 输出:Engine started
// 修改属性
car.model = "Model Y";
console.log(car.model); // 输出:Model Y
// 添加新属性
car.color = "red";
console.log(car.color); // 输出:red
3.1.2 方括号语法
方括号语法更灵活,适用于以下特殊情况:
- 属性名包含特殊字符(空格、连字符等)
- 属性名是动态生成的变量
- 属性名是Symbol类型
- 属性名是数字(如数组索引)
实际开发中的常见用例:
- 处理API返回的带有特殊字符的JSON数据
- 动态访问属性(根据用户输入或条件)
- 实现计算属性名
const phone = {
"screen-size": 6.7, // 必须用引号包裹
"brand-name": "Apple",
5: "five", // 数字属性名
[Symbol("serial")]: "X12345" // Symbol属性
};
// 1. 访问特殊属性名
console.log(phone["screen-size"]); // 输出:6.7
// 2. 使用变量访问属性
const propName = "brand-name";
console.log(phone[propName]); // 输出:Apple
// 3. 访问Symbol属性
const symKeys = Object.getOwnPropertySymbols(phone);
console.log(phone[symKeys[0]]); // 输出:X12345
// 4. 动态添加属性
const dynamicKey = "os" + "Version";
phone[dynamicKey] = "iOS 15";
console.log(phone.osVersion); // 输出:iOS 15
// 5. 数字属性名
console.log(phone["5"]); // 输出:five
console.log(phone[5]); // 输出:five(数字会自动转为字符串)
3.2 属性的删除
delete
操作符用于删除对象的自有属性,但需要注意以下限制和特性:
- 删除成功:返回
true
,即使属性不存在 - 删除失败(严格模式下报错):
- 属性是
configurable: false
- 属性是继承的(非自有属性)
- 属性是
- 与undefined的区别:
- 设为
undefined
:属性仍存在,值为undefined
- 使用
delete
:属性完全从对象中移除
- 设为
const fruit = {
name: "apple",
price: 5,
origin: "China"
};
// 1. 删除自有属性
console.log(delete fruit.price); // 输出:true
console.log(fruit.price); // 输出:undefined
console.log("price" in fruit); // 输出:false
// 2. 尝试删除不存在的属性
console.log(delete fruit.weight); // 输出:true(不会报错)
// 3. 设为undefined与delete的区别
fruit.origin = undefined;
console.log("origin" in fruit); // 输出:true(属性仍存在)
delete fruit.name;
console.log("name" in fruit); // 输出:false(属性已移除)
// 4. 无法删除继承属性
console.log(delete fruit.toString); // 输出:true(但实际没删除)
console.log(fruit.toString); // 仍可访问
// 5. 配置不可删除的属性
Object.defineProperty(fruit, "vitamin", {
value: "C",
configurable: false
});
console.log(delete fruit.vitamin); // 输出:false(严格模式下报错)
3.3 属性描述符(高级特性)
属性描述符提供了对对象属性的精细控制,分为两种类型:
3.3.1 数据属性描述符
控制标准数据属性的行为,包含四个配置项:
value
:属性值(默认undefined
)writable
:是否可修改(默认false
)enumerable
:是否可枚举(默认false
)configurable
:是否可配置(默认false
)
典型应用场景:
- 创建不可变的常量属性
- 隐藏内部实现细节
- 防止意外修改
const config = {};
// 定义单个属性
Object.defineProperty(config, "apiUrl", {
value: "https://api.example.com",
writable: false, // 不可修改
enumerable: true, // 可枚举
configurable: false // 不可删除或重新配置
});
// 尝试修改
config.apiUrl = "https://new.api.com"; // 静默失败(严格模式下报错)
console.log(config.apiUrl); // 输出原值
// 尝试删除
console.log(delete config.apiUrl); // 输出:false
// 定义多个属性
Object.defineProperties(config, {
maxRetry: {
value: 3,
writable: true
},
timeout: {
value: 5000,
enumerable: false // 隐藏此属性
}
});
// 测试枚举
console.log(Object.keys(config)); // 只输出:["apiUrl"]
console.log(Object.getOwnPropertyNames(config)); // 输出所有属性名
// 获取属性描述符
const desc = Object.getOwnPropertyDescriptor(config, "apiUrl");
console.log(desc);
/*
{
value: "https://api.example.com",
writable: false,
enumerable: true,
configurable: false
}
*/
3.3.2 访问器属性描述符
通过getter/setter方法控制属性访问,不直接存储值,适合需要:
- 数据验证
- 计算属性
- 数据监听/响应式编程
- 访问控制
const bankAccount = {
_balance: 1000, // 约定:下划线开头表示私有
transactions: []
};
// 定义访问器属性
Object.defineProperty(bankAccount, "balance", {
get: function() {
console.log("查询余额");
return this._balance;
},
set: function(newBalance) {
console.log("修改余额");
const change = newBalance - this._balance;
this.transactions.push({
type: change > 0 ? "DEPOSIT" : "WITHDRAW",
amount: Math.abs(change),
date: new Date()
});
this._balance = newBalance;
},
enumerable: true
});
// 使用属性
console.log(bankAccount.balance); // 输出:查询余额 → 1000
bankAccount.balance = 1500; // 输出:修改余额
console.log(bankAccount.transactions);
/*
[
{
type: "DEPOSIT",
amount: 500,
date: ...
}
]
*/
// 尝试设置无效值
bankAccount.balance = "abc"; // 静默失败(可添加验证逻辑)
console.log(bankAccount.balance); // 仍为1500
// 与数据属性结合使用
Object.defineProperty(bankAccount, "formattedBalance", {
get: function() {
return `$${this._balance.toFixed(2)}`;
},
enumerable: true
});
console.log(bankAccount.formattedBalance); // 输出:$1500.00
四、对象的遍历方式
4.1 for...in循环
for...in
循环是 JavaScript 中用于遍历对象所有可枚举属性的传统方法,它会遍历对象的自有属性以及从原型链继承的可枚举属性。这种方法特别适用于需要完整查看对象所有可枚举属性的场景,但需要注意处理继承属性。
工作原理:
- 遍历对象本身的可枚举属性
- 沿着原型链向上查找可枚举属性
- 对每个找到的属性执行循环体
典型应用场景:
- 调试时查看对象所有属性
- 需要处理继承属性的情况
- 对象属性数量未知时的遍历
详细示例:
// 创建基础对象
const car = {
brand: 'Toyota',
model: 'Camry'
};
// 添加原型可枚举属性
Object.prototype.year = 2020;
// 遍历所有属性(包括继承的)
for (const key in car) {
if (car.hasOwnProperty(key)) {
console.log(`自有属性: ${key} = ${car[key]}`);
// 输出:
// 自有属性: brand = Toyota
// 自有属性: model = Camry
} else {
console.log(`继承属性: ${key} = ${car[key]}`);
// 输出: 继承属性: year = 2020
}
}
注意事项:
- 使用
hasOwnProperty()
检查是必要的,可以避免意外处理原型链上的属性 - 遍历顺序不总是与属性定义顺序一致(特别是数字键会被特殊处理)
- 在 ES6 环境中,可以使用
Object.setPrototypeOf(null)
创建无原型的对象来简化遍历
4.2 Object.keys()/Object.values()/Object.entries()
这一组方法提供了更现代的属性遍历方式,只关注对象自身的可枚举属性,不涉及原型链。
方法对比:
方法 | 返回值 | 用途 |
---|---|---|
Object.keys() |
属性名数组 | 获取所有可枚举属性名 |
Object.values() |
属性值数组 | 获取所有可枚举属性值 |
Object.entries() |
[key,value]数组 | 获取所有键值对 |
实际应用示例:
const employee = {
id: 'E1001',
name: '李四',
department: '研发部',
salary: 15000,
[Symbol('bonus')]: 3000 // Symbol属性不会被包含
};
// 1. 获取所有属性名
const propNames = Object.keys(employee);
console.log(propNames); // ["id", "name", "department", "salary"]
// 2. 获取所有属性值
const propValues = Object.values(employee);
console.log(propValues); // ["E1001", "李四", "研发部", 15000]
// 3. 获取键值对并处理
const employeeInfo = Object.entries(employee)
.map(([key, value]) => `${key}: ${value}`)
.join(', ');
console.log(employeeInfo); // "id: E1001, name: 李四, department: 研发部, salary: 15000"
// 与for...of配合使用
for (const [key, value] of Object.entries(employee)) {
console.log(`员工${key}信息: ${value}`);
}
性能考虑:
- 这些方法会创建新的数组,对于大型对象可能有内存开销
- 在只需要遍历而不需要保留结果时,直接使用
for...in
可能更高效
4.3 Object.getOwnPropertyNames()
这个方法返回对象所有自有属性名的数组,包括不可枚举属性,但不包括 Symbol 属性。
与Object.keys()的区别:
Object.keys()
只返回可枚举属性Object.getOwnPropertyNames()
返回所有自有属性,无论是否可枚举
深入示例:
const settings = {
theme: 'dark',
fontSize: 14
};
// 添加不可枚举属性
Object.defineProperty(settings, 'apiKey', {
value: '123-456-789',
enumerable: false,
writable: false
});
// 添加Symbol属性
settings[Symbol('version')] = '1.0.0';
console.log(Object.keys(settings));
// ["theme", "fontSize"] (只包含可枚举属性)
console.log(Object.getOwnPropertyNames(settings));
// ["theme", "fontSize", "apiKey"] (包含不可枚举属性)
console.log(Object.getOwnPropertySymbols(settings));
// [Symbol(version)] (仅Symbol属性)
实用技巧:
- 检查对象是否包含特定属性(无论是否可枚举)
- 调试时查看对象的完整属性结构
- 与
Object.getOwnPropertyDescriptor()
配合使用可以获取属性的完整描述
性能优化提示:
对于大型对象,可以先使用 Object.getOwnPropertyNames()
获取所有属性名,然后按需处理,而不是多次调用不同的属性获取方法。
五、对象的高级特性
5.1 原型与原型链详解
JavaScript 中的原型和原型链是理解对象继承机制的核心概念。每个对象都有一个内部属性 [[Prototype]]
(称为隐式原型),可以通过 Object.getPrototypeOf(obj)
方法来访问。原型链则是 JavaScript 实现继承的主要方式。
原型的工作原理
当访问一个对象的属性时,JavaScript 引擎会按照以下步骤进行查找:
- 首先在对象自身的属性中查找
- 如果找不到,则沿着原型链向上查找
- 这个过程会一直持续到找到属性或者到达原型链的终点(
null
)为止
详细示例分析
// 创建一个原型对象
const animal = {
eat() {
console.log("吃东西");
},
sleep() {
console.log("睡觉");
}
};
// 使用Object.create基于animal创建新对象
const cat = Object.create(animal);
cat.name = "小花";
cat.meow = function() {
console.log("喵喵叫");
};
// 访问自有属性
console.log(cat.name); // 输出:"小花"
cat.meow(); // 输出:"喵喵叫"
// 访问继承的原型属性
cat.eat(); // 输出:"吃东西"(来自animal原型)
cat.sleep(); // 输出:"睡觉"(来自animal原型)
// 原型链关系验证
console.log(Object.getPrototypeOf(cat) === animal); // true
console.log(animal.isPrototypeOf(cat)); // true(另一种验证方式)
// 继续向上追溯原型链
console.log(Object.getPrototypeOf(animal) === Object.prototype); // true
console.log(Object.prototype.hasOwnProperty('toString')); // true(内置方法)
// 原型链终点验证
console.log(Object.getPrototypeOf(Object.prototype) === null); // true
原型链的完整路径
在上述例子中,完整的原型链路径是: cat
→ animal
→ Object.prototype
→ null
实际应用场景
- 方法共享:多个对象可以共享原型上的方法,节省内存
- 继承实现:通过原型链可以实现类似传统面向对象语言的继承
- 扩展内置对象:可以给内置对象的原型添加方法(但不推荐在生产环境这样做)
原型相关的重要方法
Object.create(proto)
- 创建一个新对象,使用现有对象作为新对象的原型Object.getPrototypeOf(obj)
- 获取对象的原型Object.setPrototypeOf(obj, proto)
- 设置对象的原型(性能较差,慎用)obj.hasOwnProperty(prop)
- 检查属性是否是对象自身的(非继承的)属性
注意事项
- 原型链查找会对性能有轻微影响,链越长影响越大
- 现代JavaScript中,
class
语法糖背后仍然是基于原型的机制 - 过度修改原型(特别是内置对象的原型)可能导致难以维护的代码
5.2 对象的解构赋值(ES6+)
对象解构赋值是 ES6 引入的一项重要特性,它允许我们通过模式匹配的方式,从对象中提取属性值并赋值给对应的变量。这种语法简洁直观,能显著减少代码量,提高开发效率。
5.2.1 基本解构
基本用法
const user = {
name: "李四",
age: 30,
address: {
city: "北京",
street: "朝阳路"
},
hobbies: ["阅读", "旅游", "摄影"]
};
// 解构提取name和age
const { name, age } = user;
console.log(name, age); // 输出:李四 30
嵌套解构
可以解构嵌套的对象结构:
// 解构嵌套属性
const { address: { city, street } } = user;
console.log(city, street); // 输出:北京 朝阳路
// 同时解构外层和内层属性
const { name, address: { city } } = user;
console.log(name, city); // 输出:李四 北京
重命名属性
当变量名需要避免冲突时,可以使用冒号语法重命名:
// 重命名属性(避免变量名冲突)
const { name: userName, age: userAge } = user;
console.log(userName, userAge); // 输出:李四 30
默认值设置
当解构的属性不存在时,可以设置默认值:
// 设置默认值(属性不存在时使用)
const { gender = "男", email = "default@example.com" } = user;
console.log(gender, email); // 输出:男 default@example.com
// 默认值可以和重命名结合使用
const { nickname: userNickname = "匿名用户" } = user;
console.log(userNickname); // 输出:匿名用户
解构数组属性
对象中的数组属性也可以解构:
// 解构数组属性
const { hobbies: [firstHobby, secondHobby] } = user;
console.log(firstHobby, secondHobby); // 输出:阅读 旅游
5.2.2 函数参数解构
基本用法
在函数参数中直接使用解构,可以简化函数调用:
function printUser({ name, age = 18 }) {
console.log(`姓名:${name},年龄:${age}`);
}
const user1 = { name: "王五" };
const user2 = { name: "赵六", age: 22 };
printUser(user1); // 输出:姓名:王五,年龄:18
printUser(user2); // 输出:姓名:赵六,年龄:22
嵌套解构参数
function printAddress({ address: { city, street } }) {
console.log(`城市:${city},街道:${street}`);
}
printAddress(user); // 输出:城市:北京,街道:朝阳路
复杂解构示例
function processUser({
name,
age,
address: { city, street = "未知街道" },
hobbies: [mainHobby]
}) {
console.log(`${name}(${age}岁)居住在${city}的${street},主要爱好是${mainHobby}`);
}
processUser(user); // 输出:李四(30岁)居住在北京的朝阳路,主要爱好是阅读
默认参数与解构结合
function createUser({
name = "匿名用户",
age = 0,
isAdmin = false
} = {}) {
return { name, age, isAdmin };
}
console.log(createUser()); // 输出:{ name: '匿名用户', age: 0, isAdmin: false }
console.log(createUser({ name: "张三" })); // 输出:{ name: '张三', age: 0, isAdmin: false }
这种函数参数解构方式在 React 组件开发中特别常见,例如处理组件的 props 对象时,可以清晰地看到组件接收哪些属性。
5.3 对象的扩展运算符(ES6+)
扩展运算符不仅适用于数组,在ES6之后也支持用于对象操作,可以实现对象的复制、合并等常见操作,简化了对象操作语法。
5.3.1 复制对象(浅拷贝)
const obj1 = {
a: 1,
b: 2,
nested: {
x: 10,
y: 20
}
};
// 使用扩展运算符浅拷贝obj1到obj2
const obj2 = { ...obj1 };
// 修改基本类型属性
obj2.b = 3;
console.log(obj1.b); // 输出:2(obj1的基本类型属性不受影响)
// 修改嵌套对象属性
obj2.nested.x = 100;
console.log(obj1.nested.x); // 输出:100(嵌套对象是引用传递)
注意事项:
- 扩展运算符执行的是浅拷贝(Shallow Copy),只复制对象的第一层属性
- 对于基本类型值(Number, String, Boolean等)会复制值本身
- 对于引用类型值(Object, Array等)会复制引用,因此修改嵌套对象会影响原对象
- 适用于不需要深度拷贝的简单场景
应用场景:
- 快速创建对象副本
- 需要保持对象不可变性时创建新对象
- 在React中创建新的state对象
5.3.2 合并对象
const baseConfig = {
apiUrl: 'https://api.example.com',
timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
};
const customConfig = {
timeout: 10000,
headers: {
'Authorization': 'Bearer token123'
},
debug: true
};
// 合并两个配置对象,后面对象的属性会覆盖前面的同名属性
const finalConfig = {
...baseConfig,
...customConfig
};
console.log(finalConfig);
/* 输出:
{
apiUrl: 'https://api.example.com',
timeout: 10000,
headers: {
'Authorization': 'Bearer token123'
},
debug: true
}
*/
合并规则:
- 从左到右依次合并,后面的属性会覆盖前面的同名属性
- 对于嵌套对象,也是浅合并(不会递归合并嵌套属性)
- 可以合并多个对象:
{...obj1, ...obj2, ...obj3}
高级用法:
// 合并时添加新属性
const updatedObj = {
...originalObj,
newProp: 'value',
[dynamicKey]: dynamicValue
};
// 与解构赋值结合使用
const { a, ...rest } = { a: 1, b: 2, c: 3 };
console.log(rest); // { b: 2, c: 3 }
实际应用场景:
- 配置对象合并
- 默认参数与用户自定义参数合并
- Redux中的state更新
- React组件props合并
5.4 对象的深拷贝与浅拷贝
在处理对象复制时,需区分浅拷贝和深拷贝,二者的核心差异在于是否对嵌套对象进行递归复制。理解这两种拷贝方式对避免数据污染和内存泄漏至关重要。
5.4.1 浅拷贝
浅拷贝仅复制对象的表层属性,若属性值为引用类型(如对象、数组),则复制的是引用地址,修改新对象的嵌套属性会影响原对象。这种特性在需要共享数据时很有用,但也容易导致意外的副作用。
常用浅拷贝方法汇总:
扩展运算符(...):适用于普通对象和数组,是最简洁的浅拷贝方式。
const obj = { a: 1, b: { c: 2 } }; const shallowCopy = { ...obj }; shallowCopy.b.c = 3; console.log(obj.b.c); // 输出:3(原对象嵌套属性被修改)
Object.assign():将多个源对象的属性复制到目标对象,同样只处理表层属性。
const target = {}; const source = { a: 1, b: { c: 2 } }; Object.assign(target, source); target.b.c = 3; console.log(source.b.c); // 输出:3
数组的slice()/concat():仅对数组有效,属于浅拷贝。
const arr = [1, { a: 2 }]; const arrCopy = arr.slice(); arrCopy[1].a = 3; console.log(arr[1].a); // 输出:3
Array.from():创建新数组的浅拷贝方式。
const arr = [{x: 1}, {y: 2}]; const arrCopy = Array.from(arr); arrCopy[0].x = 10; console.log(arr[0].x); // 输出:10
5.4.2 深拷贝(重点补充)
深拷贝会递归复制对象的所有层级,新对象与原对象完全独立,修改嵌套属性不会相互影响。这在需要完全隔离数据副本的场景中非常有用,如状态管理、数据持久化等。
常用深拷贝方法:
JSON.parse(JSON.stringify())(简单场景适用):
const obj = { a: 1, b: { c: 2 }, d: new Date(), e: () => console.log("test"), f: undefined }; const deepCopy = JSON.parse(JSON.stringify(obj)); console.log(deepCopy.d); // 输出:字符串格式的日期(如"2025-09-09T00:00:00.000Z"),而非Date对象 console.log(deepCopy.e); // 输出:undefined(函数被丢失) console.log(deepCopy.f); // 输出:undefined(但实际JSON中不会存在该属性)
原理:先将对象转为 JSON 字符串(序列化),再将字符串转回对象(反序列化)。
局限性:
- 无法处理 function、Symbol、undefined 等特殊类型
- Date 对象会被转为字符串
- RegExp 对象会被转为空对象 {}
- 会丢失循环引用的对象
- 无法处理 Map、Set 等ES6新数据结构
自定义递归函数(灵活处理特殊类型):
通过递归遍历对象的所有属性,对不同类型(基本类型、引用类型、特殊类型)分别处理,实现完整的深拷贝。
function deepClone(target, map = new WeakMap()) { // 处理基本类型和null if (typeof target !== "object" || target === null) { return target; } // 处理循环引用 if (map.has(target)) { return map.get(target); } // 处理Date类型 if (target instanceof Date) { return new Date(target); } // 处理RegExp类型 if (target instanceof RegExp) { return new RegExp(target.source, target.flags); } // 处理Map类型 if (target instanceof Map) { const cloneMap = new Map(); map.set(target, cloneMap); target.forEach((value, key) => { cloneMap.set(deepClone(key, map), deepClone(value, map)); }); return cloneMap; } // 处理Set类型 if (target instanceof Set) { const cloneSet = new Set(); map.set(target, cloneSet); target.forEach(value => { cloneSet.add(deepClone(value, map)); }); return cloneSet; } // 处理数组和普通对象(创建新的空容器) const cloneTarget = Array.isArray(target) ? [] : {}; map.set(target, cloneTarget); // 递归复制属性(包括Symbol属性) Reflect.ownKeys(target).forEach(key => { cloneTarget[key] = deepClone(target[key], map); }); return cloneTarget; } // 测试用例 const obj = { a: 1, b: { c: 2 }, d: new Date(), e: /test/g, f: Symbol("key"), g: new Map([["key", "value"]]), h: new Set([1, 2, 3]) }; // 添加循环引用 obj.self = obj; const cloneObj = deepClone(obj); cloneObj.b.c = 3; console.log(obj.b.c); // 输出:2(原对象不受影响) console.log(cloneObj.d instanceof Date); // true console.log(cloneObj.e instanceof RegExp); // true console.log(cloneObj.g instanceof Map); // true console.log(cloneObj.h instanceof Set); // true console.log(cloneObj.self === cloneObj); // true(循环引用正确处理)
第三方库(生产环境推荐):
如 Lodash 的
_.cloneDeep()
方法,已成熟处理各种边缘场景,无需手动编写递归逻辑。// 需先引入Lodash库 const _ = require("lodash"); const obj = { a: 1, b: { c: 2 }, d: new Date(), e: /test/g, f: new Map([["key", "value"]]), g: new Set([1, 2, 3]) }; const cloneObj = _.cloneDeep(obj); cloneObj.b.c = 3; console.log(obj.b.c); // 输出:2 console.log(cloneObj.d instanceof Date); // true console.log(cloneObj.e instanceof RegExp); // true console.log(cloneObj.f instanceof Map); // true console.log(cloneObj.g instanceof Set); // true
性能考虑:
- 对于简单对象,
JSON.parse(JSON.stringify())
是最快的深拷贝方法 - 对于复杂对象或需要保留特殊类型的情况,自定义递归或第三方库更合适
- 在性能敏感的场景中,应考虑是否需要完整的深拷贝,或者是否可以改用浅拷贝+部分深拷贝的混合策略
5.5 对象的冻结与密封(防止修改)
5.5.1 Object.freeze()(冻结对象)
详细特性
Object.freeze()
方法会创建一个冻结对象,该对象具有以下特性:
- 不能添加新属性
- 不能删除现有属性
- 不能修改已有属性的值
- 不能修改已有属性的可枚举性、可配置性、可写性
- 对象的原型也不能被修改
实际应用场景
- 配置对象(如API配置、系统设置)
- 常量定义
- 不希望被修改的全局状态
代码示例扩展
// 创建并冻结一个配置对象
const appConfig = {
api: {
baseUrl: "https://api.example.com/v3",
endpoints: {
users: "/users",
products: "/products"
}
},
settings: {
maxRetries: 3,
timeout: 5000
}
};
Object.freeze(appConfig);
// 尝试修改浅层属性
appConfig.settings = {}; // 静默失败(严格模式下会抛出TypeError)
console.log(appConfig.settings.maxRetries); // 3
// 尝试修改深层属性(仍然可以)
appConfig.api.endpoints.users = "/new-users";
console.log(appConfig.api.endpoints.users); // "/new-users"
深冻结实现改进
function deepFreeze(obj) {
// 获取所有属性名(包括Symbol)
const propNames = Reflect.ownKeys(obj);
// 先冻结自身
Object.freeze(obj);
// 递归冻结所有属性
propNames.forEach(name => {
const prop = obj[name];
if (typeof prop === 'object' && prop !== null && !Object.isFrozen(prop)) {
deepFreeze(prop);
}
});
return obj;
}
// 使用示例
const secureConfig = {
db: {
host: "localhost",
credentials: {
user: "admin",
password: "secret"
}
}
};
deepFreeze(secureConfig);
// 尝试修改深层属性
secureConfig.db.credentials.user = "hacker"; // 在严格模式下会抛出错误
console.log(secureConfig.db.credentials.user); // "admin"(未被修改)
5.5.2 Object.seal()(密封对象)
详细特性
Object.seal()
方法会创建一个密封对象,该对象具有以下特性:
- 不能添加新属性
- 不能删除已有属性
- 现有属性可以被修改
- 现有属性的特性(如configurable)不能被修改(全部变为configurable: false)
与freeze()的区别
特性 | seal() | freeze() |
---|---|---|
添加属性 | ❌ | ❌ |
删除属性 | ❌ | ❌ |
修改属性值 | ✅ | ❌ |
修改属性特性 | ❌ | ❌ |
实际应用场景
- 需要保护对象结构但允许修改值的场景
- 表单验证规则的存储对象
- 部分需要保护的API响应对象
代码示例扩展
// 创建并密封一个用户对象
const userProfile = {
id: "u12345",
username: "js_developer",
preferences: {
theme: "dark",
fontSize: 14
}
};
Object.seal(userProfile);
// 允许的操作
userProfile.username = "js_master"; // 修改现有属性
userProfile.preferences.theme = "light"; // 修改嵌套对象
// 不允许的操作
userProfile.email = "user@example.com"; // 添加新属性(静默失败)
delete userProfile.id; // 删除属性(静默失败)
// 尝试重新配置属性
Object.defineProperty(userProfile, "username", {
enumerable: false // TypeError: Cannot redefine property: username
});
深密封实现
function deepSeal(obj) {
// 获取所有自有属性
const propNames = Object.getOwnPropertyNames(obj);
// 先密封自身
Object.seal(obj);
// 递归密封所有对象类型的属性
propNames.forEach(name => {
const prop = obj[name];
if (typeof prop === 'object' && prop !== null && !Object.isSealed(prop)) {
deepSeal(prop);
}
});
return obj;
}
// 使用示例
const account = {
id: "acc123",
settings: {
notifications: true,
security: {
twoFactor: false
}
}
};
deepSeal(account);
// 测试
account.settings.security.twoFactor = true; // 允许修改值
account.settings.newOption = "value"; // 无法添加新属性
delete account.settings.notifications; // 无法删除属性
其他相关方法
Object.preventExtensions()
最基础的保护级别,仅禁止添加新属性,允许删除和修改现有属性。
检测方法
Object.isFrozen()
Object.isSealed()
Object.isExtensible()
注意事项
- 在严格模式下,违反这些限制的操作会抛出TypeError
- 这些方法都是浅操作,默认不会影响嵌套对象
- 这些操作是不可逆的,一旦应用无法撤销
- 性能考虑:频繁冻结/密封大量对象可能影响性能
六、对象的常见问题与解决方案
6.1 this 指向异常(高频问题)
在 JavaScript 中,this
的绑定规则常导致开发者困惑。对象方法中的 this
指向取决于调用方式而非定义位置,这是很多问题的根源。以下是几种常见异常场景及解决方案:
6.1.1 方法单独调用(this 指向全局对象)
const obj = {
name: "张三",
sayName() {
console.log(this.name);
}
};
// 正确调用方式
obj.sayName(); // 输出:"张三"
// 问题场景:方法被单独提取调用
const sayName = obj.sayName;
sayName(); // 输出:undefined(浏览器中)或 ""(Node.js中)
问题分析:当方法被赋值给变量后调用,this
会丢失原绑定,在非严格模式下指向全局对象,严格模式下则是 undefined
。
解决方案:
使用
call()
或apply()
显式绑定this
:sayName.call(obj); // 输出:"张三" sayName.apply(obj); // 输出:"张三"
使用
bind()
创建永久绑定函数:const boundSayName = obj.sayName.bind(obj); boundSayName(); // 输出:"张三"
使用箭头函数定义方法(ES6+):
const obj = { name: "张三", sayName: () => { console.log(this.name); // 箭头函数没有自己的this,继承外层作用域 } };
6.1.2 回调函数中的 this 丢失
const obj = {
count: 0,
increment() {
setInterval(function() {
this.count++; // 问题:this指向window或undefined
console.log(this.count); // 输出:NaN
}, 1000);
}
};
obj.increment();
问题分析:回调函数中的 this
会重新绑定,不再指向原对象。
解决方案:
使用箭头函数(推荐):
increment() { setInterval(() => { this.count++; // 正确继承外层this console.log(this.count); // 输出:1, 2, 3... }, 1000); }
保存
this
引用:increment() { const that = this; setInterval(function() { that.count++; console.log(that.count); // 输出:1, 2, 3... }, 1000); }
使用
bind()
:increment() { setInterval(function() { this.count++; console.log(this.count); }.bind(this), 1000); }
6.2 对象属性名冲突(Symbol 的应用)
当多个模块或库需要向同一对象添加属性时,字符串属性名容易发生冲突。ES6 引入的 Symbol
类型可以创建唯一的属性键。
// 模块A
const moduleAId = Symbol('moduleA_id');
const obj = {
[moduleAId]: 'A001'
};
// 模块B
const moduleBId = Symbol('moduleB_id');
obj[moduleBId] = 'B002';
// 访问各自的Symbol属性
console.log(obj[moduleAId]); // 输出:"A001"
console.log(obj[moduleBId]); // 输出:"B002"
// 常规遍历方法不会包含Symbol属性
console.log(Object.keys(obj)); // 输出:[]
console.log(JSON.stringify(obj)); // 输出:"{}"
// 获取Symbol属性
const symbolProps = Object.getOwnPropertySymbols(obj);
console.log(symbolProps); // 输出:[Symbol(moduleA_id), Symbol(moduleB_id)]
Symbol 特性:
- 每个 Symbol 值都是唯一的,即使描述相同
- 不是构造函数,不能使用
new
调用 - 不可枚举,常规遍历方法无法获取
- 适合用作对象元数据或私有属性
6.3 对象属性检测方法
JavaScript 提供了多种属性检测方式,各有特点:
方法 | 检测范围 | 是否包含继承属性 | 是否包含不可枚举属性 |
---|---|---|---|
obj.hasOwnProperty(key) |
自有属性 | 否 | 是 |
key in obj |
自有+继承属性 | 是 | 是 |
Object.hasOwn(obj, key) |
自有属性 | 否 | 是 |
Object.keys(obj).includes(key) |
自有可枚举属性 | 否 | 否 |
示例对比:
const parent = { inheritedProp: 'value' };
const obj = Object.create(parent);
obj.ownProp = 'own';
Object.defineProperty(obj, 'nonEnumProp', {
value: 'hidden',
enumerable: false
});
// 检测结果对比
console.log(obj.hasOwnProperty('ownProp')); // true
console.log(obj.hasOwnProperty('inheritedProp')); // false
console.log(obj.hasOwnProperty('nonEnumProp')); // true
console.log('ownProp' in obj); // true
console.log('inheritedProp' in obj); // true
console.log('nonEnumProp' in obj); // true
console.log(Object.keys(obj).includes('ownProp')); // true
console.log(Object.keys(obj).includes('inheritedProp')); // false
console.log(Object.keys(obj).includes('nonEnumProp')); // false
// ES2022新增的Object.hasOwn()方法
console.log(Object.hasOwn(obj, 'ownProp')); // true
console.log(Object.hasOwn(obj, 'inheritedProp')); // false
最佳实践:
- 检查自有属性时,优先使用
Object.hasOwn()
(比hasOwnProperty
更安全) - 需要包含继承属性时使用
in
操作符 - 只关心可枚举属性时使用
Object.keys()
七、对象的实际应用场景
7.1 数据存储与传递(最基础场景)
用于存储结构化数据,如用户信息、接口返回数据等,便于组织和传递。这种形式在前后端交互、本地数据存储等场景中非常常见。
// 存储用户信息
const userInfo = {
id: 1001, // 用户唯一标识
name: "李四", // 用户名
age: 28, // 用户年龄(新增字段)
contact: { // 联系方式对象
phone: "13800138000", // 手机号
email: "lisi@example.com",// 邮箱
address: "北京市朝阳区" // 新增地址字段
},
hobbies: ["reading", "coding", "hiking"], // 爱好数组
isAdmin: false // 新增权限标识
};
// 作为函数参数传递
function printUserInfo(user) {
console.log(`姓名:${user.name},电话:${user.contact.phone},地址:${user.contact.address}`);
}
printUserInfo(userInfo); // 输出:姓名:李四,电话:13800138000,地址:北京市朝阳区
7.2 配置对象(集中管理参数)
在函数或类中,使用对象作为配置参数,避免参数列表过长,提高扩展性。这种模式在HTTP请求库、UI组件库等场景中广泛应用。
// 函数接收配置对象作为参数
function request(url, options = {}) {
// 解构配置对象,设置默认值
const {
method = "GET", // 默认请求方式
headers = { // 默认请求头
"Content-Type": "application/json",
"Accept": "application/json"
},
timeout = 5000, // 默认超时时间
retry = 3, // 新增重试次数
credentials = "include" // 新增跨域凭证
} = options;
console.log(`请求地址:${url},方法:${method},超时:${timeout}ms,重试:${retry}次`);
// 实际请求逻辑...
}
// 调用函数(仅传递必要配置,其余使用默认值)
request("https://api.example.com/data", {
method: "POST",
timeout: 10000,
headers: { // 覆盖部分默认请求头
"X-Requested-With": "XMLHttpRequest"
}
});
7.3 面向对象编程(模拟类与实例)
在 ES6 之前,通过构造函数和原型模拟类的概念,实现代码复用和继承(ES6 的class本质仍是基于原型的语法糖)。
// 模拟"动物"类(构造函数+原型)
function Animal(name) {
this.name = name; // 实例属性
this.energy = 100; // 新增能量值
}
// 原型方法(所有实例共享)
Animal.prototype.eat = function(food) {
this.energy += 20;
console.log(`${this.name}吃${food},能量+20,当前能量:${this.energy}`);
};
// 模拟"猫"类(继承Animal)
function Cat(name, color) {
// 调用父类构造函数(继承实例属性)
Animal.call(this, name);
this.color = color; // 子类实例属性
this.lives = 9; // 新增猫特有属性
}
// 继承父类原型方法
Cat.prototype = Object.create(Animal.prototype);
// 修复子类构造函数指向
Cat.prototype.constructor = Cat;
// 子类原型方法
Cat.prototype.catchMouse = function() {
console.log(`${this.color}的${this.name}在抓老鼠`);
this.energy -= 10;
};
// 创建子类实例
const cat = new Cat("小花", "橘色");
cat.eat("鱼"); // 输出:小花吃鱼,能量+20,当前能量:120
cat.catchMouse(); // 输出:橘色的小花在抓老鼠
7.4 模块化开发(封装功能与状态)
在前端模块化(如 ES Module、CommonJS)中,使用对象封装模块的功能和状态,对外暴露指定接口。
// 模块:mathUtils.js(CommonJS)
const mathUtils = (function() {
// 私有状态(通过IIFE闭包隐藏)
const _pi = 3.14159;
const _version = "1.0.0";
// 私有方法
function _checkNumber(num) {
return typeof num === "number" && !isNaN(num);
}
// 公开API
return {
// 公开方法
add(a, b) {
if(!_checkNumber(a) || !_checkNumber(b)) {
throw new Error("参数必须是数字");
}
return a + b;
},
circleArea(radius) {
return _pi * radius * radius;
},
// 新增三角函数方法
sin(deg) {
return Math.sin(deg * Math.PI / 180);
},
// 获取版本号
getVersion() {
return _version;
}
};
})();
// 对外暴露模块接口
module.exports = mathUtils;
// 引入模块使用示例
const math = require("./mathUtils");
console.log(math.add(2, 3)); // 输出:5
console.log(math.circleArea(2)); // 输出:12.56636
console.log(math.sin(30)); // 输出:0.49999999999999994
console.log(math.getVersion()); // 输出:1.0.0