====== 第五章 对象 ======
===== 5.1 对象概述 =====
对象是JavaScript中最重要的数据类型之一,是键值对的集合。与原始类型(string、number等)不同,对象是**引用类型**,变量存储的是指向内存中对象的引用而非实际数据。
===== 5.2 创建对象 =====
==== 5.2.1 对象字面量 ====
最常用、最简洁的对象创建方式。
// 空对象
const empty = {};
// 带有属性的对象
const person = {
firstName: "John",
lastName: "Doe",
age: 30,
isEmployed: true,
hobbies: ["reading", "swimming"],
address: {
street: "123 Main St",
city: "New York"
},
// 方法
greet: function() {
return `Hello, I'm ${this.firstName}`;
},
// ES6方法简写
sayGoodbye() {
return "Goodbye!";
},
// 计算属性名
["user" + "Id"]: 12345
};
**属性名规则**:
* 可以是字符串或Symbol
* 有效的标识符可以不用引号
* 包含特殊字符的属性名需要用引号
const obj = {
normal: "value",
"with-dash": "value", // 需要引号
"with space": "value", // 需要引号
"123": "numeric", // 数字属性名自动转字符串
"": "empty" // 空字符串也可以作为属性名
};
==== 5.2.2 new Object() ====
// 使用Object构造函数(不推荐)
const person = new Object();
person.name = "Alice";
person.age = 25;
// 与对象字面量等价
const person2 = {
name: "Alice",
age: 25
};
==== 5.2.3 Object.create() ====
创建一个新对象,使用现有对象作为新对象的原型。
// 创建以null为原型的对象(无原型链)
const dict = Object.create(null);
dict.key = "value";
// 没有toString等方法
// 使用现有对象作为原型
const animal = {
type: "animal",
makeSound() {
console.log("Some sound");
}
};
const dog = Object.create(animal);
dog.breed = "Golden Retriever";
dog.makeSound(); // "Some sound"(继承自animal)
// 指定属性描述符
const person = Object.create(Object.prototype, {
name: {
value: "Alice",
writable: true,
enumerable: true,
configurable: true
},
age: {
value: 25,
writable: false // 只读
}
});
==== 5.2.4 构造函数 ====
// 定义构造函数
function Person(name, age) {
this.name = name;
this.age = age;
this.greet = function() {
return `Hello, I'm ${this.name}`;
};
}
// 使用new创建实例
const person1 = new Person("Alice", 25);
const person2 = new Person("Bob", 30);
console.log(person1.greet()); // "Hello, I'm Alice"
console.log(person2.greet()); // "Hello, I'm Bob"
===== 5.3 访问和修改对象属性 =====
==== 5.3.1 点符号与方括号 ====
const person = {
name: "Alice",
"favorite color": "blue",
123: "numeric key"
};
// 点符号(要求属性名是有效标识符)
person.name; // "Alice"
// person.favorite color; // SyntaxError
// 方括号(可以使用任意表达式)
person["name"]; // "Alice"
person["favorite color"]; // "blue"
person[123]; // "numeric key"
person["123"]; // "numeric key"(数字自动转字符串)
// 动态属性访问
const prop = "name";
person[prop]; // "Alice"
// 计算属性名
let i = 0;
const obj = {
["prop" + ++i]: "value1",
["prop" + ++i]: "value2"
};
// obj: { prop1: "value1", prop2: "value2" }
==== 5.3.2 添加、修改和删除属性 ====
const person = {};
// 添加属性
person.name = "Alice";
person["age"] = 25;
// 修改属性
person.age = 26;
// 使用Object.assign()批量添加/修改
Object.assign(person, {
email: "alice@example.com",
phone: "123-456-7890"
});
// 使用展开运算符(ES2018)
const updatedPerson = {
...person,
age: 27,
country: "USA"
};
// 删除属性
delete person.phone;
delete person["email"];
// delete返回true(即使属性不存在)
const result = delete person.nonExistent; // true
==== 5.3.3 属性存在性检查 ====
const person = {
name: "Alice",
age: undefined
};
// in运算符 - 检查自有属性和继承属性
"name" in person; // true
"age" in person; // true(值为undefined但存在)
"toString" in person; // true(继承自Object.prototype)
// hasOwnProperty() - 只检查自有属性
person.hasOwnProperty("name"); // true
person.hasOwnProperty("age"); // true
person.hasOwnProperty("toString"); // false
// Object.prototype.hasOwnProperty.call() - 更安全
Object.prototype.hasOwnProperty.call(person, "name");
// Object.hasOwn() - ES2022新方法
Object.hasOwn(person, "name"); // true
// undefined检查(不可靠)
person.name !== undefined; // true
person.age !== undefined; // false(存在但值为undefined)
person.gender !== undefined; // false(不存在)
// 可选链操作符(ES2020)
person?.address?.city; // undefined(不报错)
===== 5.4 遍历对象 =====
==== 5.4.1 for...in循环 ====
const person = {
name: "Alice",
age: 25,
city: "New York"
};
// 遍历所有可枚举属性(包括继承的)
for (let key in person) {
console.log(key, person[key]);
}
// 只遍历自有属性
for (let key in person) {
if (person.hasOwnProperty(key)) {
console.log(key, person[key]);
}
}
==== 5.4.2 Object.keys()、Object.values()、Object.entries() ====
const person = {
name: "Alice",
age: 25,
city: "New York"
};
// Object.keys() - 返回属性名数组
Object.keys(person); // ["name", "age", "city"]
// Object.values() - 返回属性值数组(ES2017)
Object.values(person); // ["Alice", 25, "New York"]
// Object.entries() - 返回[key, value]数组(ES2017)
Object.entries(person); // [["name", "Alice"], ["age", 25], ["city", "New York"]]
// 遍历
Object.entries(person).forEach(([key, value]) => {
console.log(`${key}: ${value}`);
});
// 从entries重建对象
const entries = [["a", 1], ["b", 2]];
const obj = Object.fromEntries(entries); // { a: 1, b: 2 }
==== 5.4.3 Object.getOwnPropertyNames()和Object.getOwnPropertySymbols() ====
const sym = Symbol("secret");
const obj = {
normal: "value",
[sym]: "symbol value"
};
Object.keys(obj); // ["normal"]
Object.getOwnPropertyNames(obj); // ["normal"]
Object.getOwnPropertySymbols(obj); // [Symbol(secret)]
// 获取所有属性键(包括Symbol)
Reflect.ownKeys(obj); // ["normal", Symbol(secret)]
===== 5.5 属性描述符 =====
每个属性都有一个描述符对象,包含以下特性:
* **value**:属性值
* **writable**:是否可写
* **enumerable**:是否可枚举
* **configurable**:是否可配置(删除或修改描述符)
==== 5.5.1 获取和设置属性描述符 ====
const person = {
name: "Alice",
age: 25
};
// 获取属性描述符
const descriptor = Object.getOwnPropertyDescriptor(person, "name");
console.log(descriptor);
// {
// value: "Alice",
// writable: true,
// enumerable: true,
// configurable: true
// }
// 获取所有属性的描述符
const descriptors = Object.getOwnPropertyDescriptors(person);
// 定义单个属性
Object.defineProperty(person, "id", {
value: 12345,
writable: false, // 只读
enumerable: false, // 不可枚举
configurable: false // 不可删除或重新配置
});
// 定义多个属性
Object.defineProperties(person, {
email: {
value: "alice@example.com",
writable: true,
enumerable: true
},
createdAt: {
value: new Date(),
writable: false,
enumerable: false
}
});
==== 5.5.2 访问器属性(Getter/Setter) ====
const person = {
firstName: "Alice",
lastName: "Smith",
// getter
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
// setter
set fullName(value) {
[this.firstName, this.lastName] = value.split(" ");
}
};
console.log(person.fullName); // "Alice Smith"
person.fullName = "Bob Jones";
console.log(person.firstName); // "Bob"
console.log(person.lastName); // "Jones"
// 使用defineProperty定义访问器
const user = {
_age: 25
};
Object.defineProperty(user, "age", {
get() {
return this._age;
},
set(value) {
if (value < 0 || value > 150) {
throw new Error("Invalid age");
}
this._age = value;
},
enumerable: true,
configurable: true
});
user.age = 30; // ✓
// user.age = -5; // ✗ Error: Invalid age
===== 5.6 对象方法 =====
==== 5.6.1 定义方法 ====
const calculator = {
// 传统方式
add: function(a, b) {
return a + b;
},
// ES6方法简写(推荐)
subtract(a, b) {
return a - b;
},
// 箭头函数(注意this绑定)
multiply: (a, b) => a * b,
// 使用this
value: 0,
increment() {
this.value++;
return this;
},
decrement() {
this.value--;
return this;
},
// 链式调用
addValue(n) {
this.value += n;
return this;
}
};
// 链式调用示例
calculator.increment().addValue(5).decrement();
console.log(calculator.value); // 5
==== 5.6.2 方法中的this ====
const person = {
name: "Alice",
sayHi() {
console.log(`Hi, I'm ${this.name}`);
},
// 箭头函数不绑定自己的this
sayBye: () => {
console.log(`Bye from ${this.name}`); // this是外层作用域的this
},
// 嵌套函数中的this问题
delayedHi() {
// 方式1:保存this
const self = this;
setTimeout(function() {
console.log(`Hi, I'm ${self.name}`);
}, 100);
// 方式2:使用bind
setTimeout(function() {
console.log(`Hi, I'm ${this.name}`);
}.bind(this), 100);
// 方式3:使用箭头函数(推荐)
setTimeout(() => {
console.log(`Hi, I'm ${this.name}`);
}, 100);
}
};
// 方法调用 vs 函数调用
person.sayHi(); // this = person
const greet = person.sayHi;
greet(); // this = undefined(严格模式)或全局对象
===== 5.7 对象拷贝 =====
==== 5.7.1 浅拷贝 ====
const original = {
name: "Alice",
address: { city: "New York" }
};
// 方式1:展开运算符
const copy1 = { ...original };
// 方式2:Object.assign()
const copy2 = Object.assign({}, original);
// 测试
console.log(copy1 === original); // false(不同对象)
console.log(copy1.address === original.address); // true(相同引用!)
copy1.address.city = "Boston";
console.log(original.address.city); // "Boston"(原对象也被修改)
==== 5.7.2 深拷贝 ====
const original = {
name: "Alice",
address: { city: "New York", zip: "10001" },
hobbies: ["reading", "swimming"],
date: new Date(),
fn: function() {}
};
// 方式1:JSON方法(有限制)
const jsonCopy = JSON.parse(JSON.stringify(original));
// 限制:丢失函数、Date变为字符串、丢失undefined、循环引用报错
// 方式2:递归深拷贝
function deepClone(obj, hash = new WeakMap()) {
// 处理null或基本类型
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理Date
if (obj instanceof Date) {
return new Date(obj);
}
// 处理RegExp
if (obj instanceof RegExp) {
return new RegExp(obj);
}
// 处理循环引用
if (hash.has(obj)) {
return hash.get(obj);
}
// 处理数组
if (Array.isArray(obj)) {
const clone = [];
hash.set(obj, clone);
for (let i = 0; i < obj.length; i++) {
clone[i] = deepClone(obj[i], hash);
}
return clone;
}
// 处理对象
const clone = Object.create(Object.getPrototypeOf(obj));
hash.set(obj, clone);
const descriptors = Object.getOwnPropertyDescriptors(obj);
for (let key of Reflect.ownKeys(descriptors)) {
const descriptor = descriptors[key];
if (descriptor.value !== undefined) {
descriptor.value = deepClone(descriptor.value, hash);
}
Object.defineProperty(clone, key, descriptor);
}
return clone;
}
// 方式3:使用库(推荐生产环境)
// _.cloneDeep(obj) // Lodash
// structuredClone(obj) // 现代浏览器和Node.js 17+
===== 5.8 对象比较 =====
// 引用比较
const a = { x: 1 };
const b = { x: 1 };
const c = a;
a === b; // false(不同引用)
a === c; // true(相同引用)
// 浅比较(比较第一层属性)
function shallowEqual(obj1, obj2) {
if (obj1 === obj2) return true;
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (let key of keys1) {
if (obj1[key] !== obj2[key]) return false;
}
return true;
}
// 深比较
function deepEqual(obj1, obj2) {
if (obj1 === obj2) return true;
if (typeof obj1 !== 'object' || obj1 === null ||
typeof obj2 !== 'object' || obj2 === null) {
return false;
}
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (let key of keys1) {
if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
return false;
}
}
return true;
}
console.log(deepEqual({ a: { b: 1 } }, { a: { b: 1 } })); // true
===== 5.9 练习题 =====
==== 练习1:对象基础 ====
// 1. 创建一个person对象,包含name、age、birthDate属性
// birthDate使用getter计算得出
// 2. 实现一个函数mergeObjects(obj1, obj2),合并两个对象
// 如果属性冲突,obj2的属性优先
// 3. 实现一个函数pick(obj, keys),从对象中选取指定属性
// pick({ a: 1, b: 2, c: 3 }, ['a', 'c']) => { a: 1, c: 3 }
// 4. 实现一个函数omit(obj, keys),从对象中排除指定属性
// omit({ a: 1, b: 2, c: 3 }, ['b']) => { a: 1, c: 3 }
==== 练习2:属性描述符 ====
// 1. 创建一个只读对象,所有属性都不可修改、删除或重新配置
// 2. 实现一个函数freezeDeep(obj),深度冻结对象
// 3. 使用getter/setter实现一个计数器对象
// 只能通过increment()和decrement()修改值
// 通过value获取当前值
==== 练习3:深拷贝 ====
// 1. 测试JSON.parse(JSON.stringify())的局限性
// 2. 完善deepClone函数,支持Map和Set
// 3. 实现一个函数isDeepEqual(obj1, obj2),支持循环引用检测
===== 参考答案 =====
**练习1答案**:
// 1. person对象
const person = {
name: "Alice",
age: 25,
get birthDate() {
const year = new Date().getFullYear() - this.age;
return new Date(year, 0, 1);
}
};
// 2. mergeObjects
function mergeObjects(obj1, obj2) {
return { ...obj1, ...obj2 };
// 或 Object.assign({}, obj1, obj2);
}
// 深合并
function deepMerge(obj1, obj2) {
const result = { ...obj1 };
for (let key in obj2) {
if (obj2[key] instanceof Object && key in obj1) {
result[key] = deepMerge(obj1[key], obj2[key]);
} else {
result[key] = obj2[key];
}
}
return result;
}
// 3. pick
function pick(obj, keys) {
return keys.reduce((result, key) => {
if (key in obj) {
result[key] = obj[key];
}
return result;
}, {});
}
// 4. omit
function omit(obj, keys) {
return Object.keys(obj)
.filter(key => !keys.includes(key))
.reduce((result, key) => {
result[key] = obj[key];
return result;
}, {});
}
**练习2答案**:
// 1. 只读对象
function createReadonlyObject(obj) {
const result = {};
for (let key of Object.keys(obj)) {
Object.defineProperty(result, key, {
value: obj[key],
writable: false,
enumerable: true,
configurable: false
});
}
return result;
}
// 或使用Object.freeze()
const readonly = Object.freeze({ a: 1, b: 2 });
// 2. 深度冻结
function freezeDeep(obj) {
Object.freeze(obj);
Object.keys(obj).forEach(key => {
if (obj[key] && typeof obj[key] === 'object') {
freezeDeep(obj[key]);
}
});
return obj;
}
// 3. 计数器
function createCounter() {
let count = 0;
return {
get value() {
return count;
},
increment() {
count++;
return this;
},
decrement() {
count--;
return this;
}
};
}
**练习3答案**:
// 1. JSON方法局限性测试
const obj = {
fn: () => {},
date: new Date(),
undef: undefined,
inf: Infinity,
nan: NaN,
sym: Symbol('test'),
circ: null
};
obj.circ = obj; // 循环引用
// JSON.stringify(obj); // TypeError: Converting circular structure to JSON
// 2. 支持Map和Set的deepClone
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Map) {
const clone = new Map();
hash.set(obj, clone);
obj.forEach((value, key) => {
clone.set(deepClone(key, hash), deepClone(value, hash));
});
return clone;
}
if (obj instanceof Set) {
const clone = new Set();
hash.set(obj, clone);
obj.forEach(value => {
clone.add(deepClone(value, hash));
});
return clone;
}
if (hash.has(obj)) return hash.get(obj);
const clone = Array.isArray(obj) ? [] : {};
hash.set(obj, clone);
for (let key of Reflect.ownKeys(obj)) {
clone[key] = deepClone(obj[key], hash);
}
return clone;
}
===== 本章小结 =====
本章深入学习了JavaScript对象:
* 多种创建对象的方式
* 属性访问和修改的各种方法
* 遍历对象的技术
* 属性描述符和访问器
* 对象拷贝和比较
对象是JavaScript的核心,理解对象的特性和行为对于掌握这门语言至关重要。
下一章将学习数组,这是JavaScript中最常用的数据结构之一。