目录

第五章 对象

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
};

属性名规则

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 属性描述符

每个属性都有一个描述符对象,包含以下特性:

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中最常用的数据结构之一。