JavaScript 对象操作深入理解

小飞兽 JavaScript 287 次阅读 2026-05-21

JavaScript 对象操作深入理解

Introduction

JavaScript 对象(Object)是这门语言最核心的数据结构之一,几乎所有复杂数据都需要通过对象来组织和管理。从字面量创建、属性访问与修改,到原型链继承、属性描述符,再到 ES6 引入的 Proxy、Reflect、Symbol 等新特性,对象操作贯穿 JavaScript 开发的方方面面。理解对象的底层机制,不仅是写出高质量代码的基础,也是深入学习前端框架(Vue响应式、React状态管理)和 Node.js 开发的必经之路。本文将从基础语法讲起,系统梳理对象创建、属性操作、原型继承、属性描述符、ES6新特性以及常见问题,帮助你建立完整的对象操作知识体系。

一、对象的创建

1. 对象字面量

最直接的对象创建方式,使用花括号 {} 定义键值对。

基础语法:

const obj = { key1: value1, key2: value2 }

代码示例:

const person = {
    name: '张三',
    age: 28,
    isStudent: false,
    hobbies: ['读书', '编程', '旅行'],
    address: {
        city: '北京',
        district: '朝阳区'
    },
    // 方法简写
    greet() {
        return `你好,我是${this.name}!`;
    }
};

console.log(person);
console.log(person.greet());

运行效果:

{ name: '张三', age: 28, isStudent: false, hobbies: ['读书', '编程', '旅行'], address: { city: '北京', district: '朝阳区' }, greet: [Function: greet] }
你好,我是张三!

---

2. 构造函数

使用 new Object() 或自定义构造函数创建对象。

代码示例:

function User(name, age) {
    this.name = name;
    this.age = age;
    this.introduce = function() {
        return `我是${this.name},今年${this.age}岁`;
    };
}

const user1 = new User('李四', 25);
const user2 = new User('王五', 30);

console.log(user1.introduce());
console.log(user2.introduce());
console.log(user1 instanceof User); // true

运行效果:

我是李四,今年25岁
我是王五,今年30岁
true

---

3. Object.create()

Object.create() 创建一个新对象,并显式指定其原型。

代码示例:

const parent = {
    greet() {
        return 'Hello from parent';
    }
};

const child = Object.create(parent);
console.log(child.greet());          // 继承自parent
console.log(Object.getPrototypeOf(child) === parent); // true

// 自身属性
child.name = '小明';
console.log(child.name);              // '小明'(自身属性)

运行效果:

Hello from parent
true
小明

---

4. ES6 Class

ES6 的 class 语法是构造函数的语法糖,使面向对象编程更加清晰。

代码示例:

class Animal {
    constructor(name, sound) {
        this.name = name;
        this.sound = sound;
    }
    
    speak() {
        return `${this.name} 叫: ${this.sound}`;
    }
    
    static create(name, sound) {
        return new Animal(name, sound);
    }
}

class Dog extends Animal {
    constructor(name) {
        super(name, '汪汪');
    }
    
    fetch() {
        return `${this.name} 去捡球了!`;
    }
}

const dog = new Dog('旺财');
console.log(dog.speak());
console.log(dog.fetch());
console.log(Animal.create('猫', '喵喵').speak());

运行效果:

旺财 叫: 汪汪
旺财 去捡球了!
猫 叫: 喵喵

---

二、属性访问与修改

5. 点符号与方括号

JavaScript 提供两种方式访问对象属性:点符号(.)和方括号([])。

代码示例:

const config = {
    'server-name': 'my-server',
    port: 8080,
    'db-name': 'mydb'
};

// 点符号 — 简洁,但不能用于含特殊字符的属性名
console.log(config.port);

// 方括号 — 灵活,支持动态属性名和特殊字符
console.log(config['server-name']);
console.log(config['db-name']);

// 动态属性访问
const key = 'port';
console.log(config[key]); // 8080

// 特殊字符属性必须用方括号
const obj2 = {
    'my-key': 'value',
    '123': 'number key'
};
console.log(obj2['my-key']);
console.log(obj2['123']);

运行效果:

8080
my-server
mydb
8080
value
number key

---

6. 属性的增删改查

代码示例:

const user = { name: '张三' };

// 增加属性
user.age = 25;
user['city'] = '上海';
console.log('添加后:', user);

// 修改属性
user.age = 26;
console.log('修改后:', user);

// 查询属性
console.log('name:', user.name);
console.log('name in user:', 'name' in user);
console.log('hasOwnProperty:', user.hasOwnProperty('name'));

// 删除属性
delete user.city;
console.log('删除后:', user);

// 批量赋值
Object.assign(user, { age: 27, email: 'zhang@example.com' });
console.log('Object.assign后:', user);

运行效果:

添加后: { name: '张三', age: 25, city: '上海' }
修改后: { name: '张三', age: 26, city: '上海' }
name: 张三
name in user: true
hasOwnProperty: true
删除后: { name: '张三', age: 26 }
Object.assign后: { name: '张三', age: 27, email: 'zhang@example.com' }

---

三、属性描述符与属性特性

7. Object.getOwnPropertyDescriptor

每个对象属性都有对应的属性描述符,控制属性的行为特性。

代码示例:

const obj = { prop: 42 };

const descriptor = Object.getOwnPropertyDescriptor(obj, 'prop');
console.log('属性描述符:', descriptor);

// 描述符包含的四个特性:
// value — 属性值
// writable — 是否可写
// enumerable — 是否可枚举
// configurable — 是否可配置(删除或修改特性)

运行效果:

属性描述符: { value: 42, writable: true, enumerable: true, configurable: true }

---

8. Object.defineProperty / defineProperties

代码示例:

const person = {};

// 定义一个只读属性
Object.defineProperty(person, 'PI', {
    value: 3.14159,
    writable: false,
    enumerable: true,
    configurable: false
});

console.log('PI:', person.PI);
// person.PI = 3.14; // ❌ 在严格模式下抛出TypeError
console.log('PI修改失败(严格模式报错):', person.PI);

// 定义一个不可枚举的属性
Object.defineProperty(person, 'secret', {
    value: '隐藏信息',
    enumerable: false  // 不出现在 for...in 和 Object.keys() 中
});

console.log('keys枚举:', Object.keys(person));  // 不含 secret
console.log('secret直接访问:', person.secret); // 可以访问

// 批量定义
Object.defineProperties(person, {
    firstName: { value: '张', writable: true, enumerable: true },
    lastName: { value: '三', writable: true, enumerable: true }
});
console.log('person:', person);

运行效果:

PI: 3.14159
PI修改失败(严格模式报错): 3.14159
keys枚举: ['PI', 'firstName', 'lastName']
secret直接访问: 隐藏信息
person: { PI: 3.14159, firstName: '张', lastName: '三' }

---

9. Object.seal / Object.freeze

代码示例:

const obj = { x: 1, y: 2 };

// Object.seal — 禁止添加/删除属性,但允许修改现有属性
Object.seal(obj);
obj.z = 3;        // ❌ 无效果
delete obj.x;     // ❌ 无效果
obj.x = 10;       // ✅ 可以
console.log('seal后:', obj);
console.log('isSealed:', Object.isSealed(obj));

// Object.freeze — 完全冻结,无法增删改任何属性
const frozen = { a: 1, b: { c: 2 } };
Object.freeze(frozen);
frozen.a = 10;    // ❌ 无效果
frozen.b.c = 20;  // ⚠️ 注意:freeze是浅冻结,嵌套对象仍可修改
console.log('freeze后:', frozen);
console.log('isFrozen:', Object.isFrozen(frozen));

运行效果:

seal后: { x: 10, y: 2 }
isSealed: true
freeze后: { a: 1, b: { c: 20 } }
isFrozen: false

重要提示: Object.freeze() 是浅冻结。如需深度冻结,需要递归处理所有嵌套对象。

---

四、原型与继承

10. prototype 与 \__proto__

代码示例:

function Animal(name) {
    this.name = name;
}

Animal.prototype.speak = function() {
    return `${this.name}发出声音`;
};

const cat = new Animal('猫');
console.log(cat.speak());
console.log(cat.__proto__ === Animal.prototype); // true

// 原型链
console.log('constructor:', cat.constructor);
console.log('toString:', cat.toString());

运行效果:

猫发出声音
true
constructor: [Function: Animal]
[object Object]

---

11. Object.getPrototypeOf / Object.setPrototypeOf

代码示例:

const proto = { greet: 'hello' };
const obj = Object.create(proto);

console.log(Object.getPrototypeOf(obj) === proto); // true

// 更换原型(不推荐,影响性能)
const newProto = { farewell: 'goodbye' };
Object.setPrototypeOf(obj, newProto);
console.log(Object.getPrototypeOf(obj) === newProto); // true

运行效果:

true
true

---

12. hasOwnProperty 与 in 的区别

代码示例:

const parent = { a: 1 };
const child = Object.create(parent);
child.b = 2;

console.log('child.hasOwnProperty("a"):', child.hasOwnProperty('a')); // false
console.log('child.hasOwnProperty("b"):', child.hasOwnProperty('b')); // true
console.log('"a" in child:', 'a' in child);  // true(含原型链)
console.log('"b" in child:', 'b' in child);  // true

运行效果:

child.hasOwnProperty('a'): false
child.hasOwnProperty('b'): true
'a' in child: true
'b' in child: true

---

五、对象方法

13. Object.keys / values / entries

代码示例:

const obj = { a: 1, b: 2, c: 3 };

console.log('keys:', Object.keys(obj));
console.log('values:', Object.values(obj));
console.log('entries:', Object.entries(obj));

// entries用于迭代
for (const [key, value] of Object.entries(obj)) {
    console.log(`${key}: ${value}`);
}

// entries转Map
const map = new Map(Object.entries(obj));
console.log('转为Map:', map);

运行效果:

keys: ['a', 'b', 'c']
values: [1, 2, 3]
entries: [['a', 1], ['b', 2], ['c', 3]]
a: 1
b: 2
c: 3
转为Map: Map(3) { 'a' => 1, 'b' => 2, 'c' => 3 }

---

14. Object.assign — 浅拷贝合并

代码示例:

const target = { a: 1, b: { x: 10 } };
const source = { b: { y: 20 }, c: 3 };

const result = Object.assign({}, target, source);
console.log('合并结果:', result);

// 注意:浅拷贝,嵌套对象会合并而非替换
console.log('result.b:', result.b); // { y: 20 },而非 { x: 10, y: 20 }

运行效果:

合并结果: { a: 1, b: { y: 20 }, c: 3 }
result.b: { y: 20 }

---

15. Object.fromEntries — 从键值对创建对象

代码示例:

// 从entries创建对象
const entries = [['a', 1], ['b', 2], ['c', 3]];
const obj = Object.fromEntries(entries);
console.log('fromEntries:', obj);

// 实战:对象属性值翻倍
const nums = { x: 1, y: 2, z: 3 };
const doubled = Object.fromEntries(
    Object.entries(nums).map(([k, v]) => [k, v * 2])
);
console.log('翻倍后:', doubled);

// 实战:去除空值
const withEmpty = { a: 1, b: null, c: undefined, d: 'hello' };
const cleaned = Object.fromEntries(
    Object.entries(withEmpty).filter(([_, v]) => v != null)
);
console.log('去空值后:', cleaned);

运行效果:

fromEntries: { a: 1, b: 2, c: 3 }
翻倍后: { x: 2, y: 4, z: 6 }
去空值后: { a: 1, d: 'hello' }

---

六、ES6+ 新特性

16. 扩展运算符 ...obj

代码示例:

const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };

// 浅拷贝 + 合并
const merged = { ...obj1, ...obj2 };
console.log('合并:', merged); // { a: 1, b: 3, c: 4 }

// 排除属性
const user = { name: '张三', age: 25, password: 'secret', email: 'a@b.com' };
const { password, ...safeUser } = user;
console.log('排除password:', safeUser);

运行效果:

合并: { a: 1, b: 3, c: 4 }
排除password: { name: '张三', age: 25, email: 'a@b.com' }

---

17. 可选链 ?. 与 空值合并 ??

代码示例:

const user = {
    profile: {
        name: '张三',
        social: {
            twitter: '@zhangsan'
        }
    }
};

// 可选链 — 防止深层属性访问报错
console.log('name:', user.profile?.name);
console.log('twitter:', user.profile?.social?.twitter);
console.log('facebook:', user.profile?.social?.facebook); // undefined,不报错
console.log('avatar:', user.avatar?.url);                 // undefined,不报错

// 空值合并 ?? — 只在 null/undefined 时使用默认值
const value = null;
console.log('?? 运算符:', value ?? '默认值');    // '默认值'
console.log('|| 运算符:', value || '默认值');    // '默认值'(同样效果)

const zero = 0;
console.log('?? 0:', zero ?? '默认值');          // 0(|| 会返回默认值)
console.log('|| 0:', zero || '默认值');         // '默认值'

运行效果:

name: 张三
twitter: @zhangsan
facebook: undefined
avatar: undefined
?? 运算符: 默认值
|| 运算符: 默认值
?? 0: 0
|| 0: 默认值

---

18. Symbol 在对象中的应用

代码示例:

const sym = Symbol('description');
const obj = {
    [sym]: 'symbol value',
    regular: 'normal value'
};

console.log('Symbol属性:', obj[sym]);
console.log('Object.getOwnPropertySymbols:', Object.getOwnPropertySymbols(obj));

// 常用Symbol:Symbol.iterator
const iterable = {
    *[Symbol.iterator]() {
        yield 1; yield 2; yield 3;
    }
};
console.log('可迭代对象:', [...iterable]);

// Symbol.toStringTag
const custom = {
    [Symbol.toStringTag]: 'MyCustomClass'
};
console.log('toString:', Object.prototype.toString.call(custom));

运行效果:

Symbol属性: symbol value
Object.getOwnPropertySymbols: [Symbol(description)]
可迭代对象: [1, 2, 3]
toString: [object MyCustomClass]

---

19. Proxy 与 Reflect

代码示例:

const target = { name: '张三', age: 25 };

const handler = {
    get(target, prop, receiver) {
        console.log(`获取属性: ${String(prop)}`);
        return Reflect.get(target, prop, receiver);
    },
    set(target, prop, value, receiver) {
        console.log(`设置属性: ${String(prop)} = ${value}`);
        return Reflect.set(target, prop, value, receiver);
    },
    has(target, prop) {
        console.log(`检查属性: ${String(prop)}`);
        return Reflect.has(target, prop);
    }
};

const proxy = new Proxy(target, handler);

console.log('---');
console.log('访问name:', proxy.name);
console.log('设置新属性:', proxy.city = '北京');
console.log('检查属性:', 'age' in proxy);

运行效果:

---
获取属性: name
访问name: 张三
设置属性: city = 北京
检查属性: age
true

---

七、常见问题(FAQ)

Q1:Object.assign 和扩展运算符 {...obj} 的区别?
两者都是浅拷贝。Object.assign(target, ...sources) 会修改并返回目标对象,而 {...obj} 创建新对象。两者都不能完整处理嵌套对象的深拷贝。

Q2:如何深拷贝一个对象?

// 方法1:JSON序列化(无法处理函数、undefined、循环引用)
const deep1 = JSON.parse(JSON.stringify(obj));

// 方法2:递归深拷贝
function deepClone(obj, seen = new WeakMap()) {
    if (obj === null || typeof obj !== 'object') return obj;
    if (seen.has(obj)) return seen.get(obj);
    const clone = Array.isArray(obj) ? [] : {};
    seen.set(obj, clone);
    for (const [k, v] of Object.entries(obj)) {
        clone[k] = deepClone(v, seen);
    }
    return clone;
}

// 方法3:structuredClone(现代浏览器原生支持)
const deep2 = structuredClone(obj);

Q3:Object.freeze 和 Object.seal 的区别?
Object.seal() 禁止添加/删除属性,但可以修改现有属性的值。Object.freeze() 完全冻结,既不能增删属性也不能修改任何属性的值。但两者都是浅操作。

Q4:为什么需要 hasOwnProperty?
in 运算符会遍历整个原型链,可能误判继承属性。hasOwnProperty 只检查对象自身的属性,是更精确的存在性判断。不过更推荐使用 Object.hasOwn()(ES2022),因为它避免了对象重写 hasOwnProperty 的风险。

Q5:如何安全地检查对象属性?

const obj = { name: '张三' };
// ❌ 不安全:obj可能为null/undefined
// ✅ 安全方案1:可选链
obj?.name;
// ✅ 安全方案2:默认空对象
(obj || {}).name;
// ✅ 安全方案3:Object.hasOwn
Object.hasOwn(obj, 'name');

Q6:Map 和普通对象(Object)的区别?
| 特性 | Map | Object |
|------|-----|--------|
| 键类型 | 任意值 | 仅字符串或Symbol |
| 键有序 | 是(插入顺序) | ES2017+有序(非数值键) |
| 大小 | Map.size | Object.keys().length |
| 性能 | 大量键值对时更优 | 小规模数据更优 |
| 原型 | 无默认键 | 有默认原型链键 |

---

八、延伸阅读

1. MDN Web Docs — Objectshttps://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object 完整的 Object API 文档。
2. 你不知道的 JavaScript(上卷):深入讲解 JavaScript 的对象、原型链和作用域机制。
3. ES6 入门教程 — Class:阮一峰 ES6 教程中关于 class 语法的章节。
4. JavaScript 权威指南:经典参考书,系统讲解 JavaScript 语言特性。
5. Proxy 和 Reflect 深入理解:MDN 官方文档中关于 ES6 Proxy 和 Reflect 的高级用法。
6. Vue 3 响应式原理:学习 Proxy 在现代前端框架中的实际应用。