对象详解
对象是 JavaScript 的七大基本数据类型(undefined、null、boolean、number、string、symbol、object)之一,也是唯一一种"复杂"数据类型。本文系统讲解对象的创建、属性操作、属性描述符、常用方法与拷贝技术。
一、对象的创建方式
1.1 字面量(推荐)
const user = {
name: "张三",
age: 25,
greet() {
console.log(`你好,我是${this.name}`);
},
};1.2 new Object
const obj = new Object(); // 空对象
obj.name = "李四";
console.log(obj); // { name: '李四' }
// new Object 传入不同值时的行为
console.log(new Object(42)); // Number { 42 },包装成对象
console.log(new Object("hi")); // String { 'hi' }
console.log(new Object(null)); // {}1.3 Object.create
Object.create(proto, properties) 以指定对象为原型创建新对象,第二参数可同时定义属性:
const proto = { say() { console.log("我是原型方法"); } };
const obj = Object.create(proto);
obj.name = "王五";
obj.say(); // 我是原型方法
// 用 null 创建无原型对象(没有 toString 等内置方法)
const bare = Object.create(null);
console.log(bare.toString); // undefined
// 第二参数:定义属性
const o = Object.create(proto, {
age: { value: 30, writable: true },
});1.4 构造函数
使用 new 调用函数即可构造对象:
function Person(name, age) {
this.name = name;
this.age = age;
}
const p = new Person("赵六", 28);
console.log(p.name); // 赵六四种方式对比:
| 方式 | 代码 | 适用场景 |
|---|---|---|
| 字面量 | {} | 单个对象,日常首选 |
new Object() | new Object() | 与字面量等价,很少用 |
Object.create(proto) | Object.create(obj) | 指定原型的对象、继承场景 |
构造函数 + new | new Person() | 批量创建同构对象 |
二、属性访问
2.1 点号与方括号
const obj = { name: "张三", "favorite-color": "blue" };
console.log(obj.name); // 张三,点号只支持合法标识符
console.log(obj["favorite-color"]); // blue,方括号支持任意字符串
console.log(obj["name"]); // 张三,方括号同样可用
// 方括号里可以是变量
const key = "age";
obj[key] = 25;
console.log(obj.age); // 252.2 计算属性名
在字面量中用方括号包裹表达式,动态生成键名:
const prefix = "user";
const obj = {
[prefix + "-id"]: 1001,
[1 + 1]: "two",
};
console.log(obj["user-id"]); // 1001
console.log(obj[2]); // two,数字键自动转字符串三、属性简写与方法简写
const name = "张三";
const age = 25;
// 属性简写:变量名即属性名
const user = { name, age };
console.log(user); // { name: '张三', age: 25 }
// 方法简写:省略 function 关键字
const calculator = {
add(a, b) { return a + b; }, // 等价于 add: function(a, b) {...}
sub(a, b) { return a - b; },
};
console.log(calculator.add(3, 4)); // 7四、属性描述符
对象的每个属性背后都有一个描述符对象,描述该属性的行为。
4.1 描述符的四个配置项
| 配置项 | 含义 | 默认值 |
|---|---|---|
writable | 值是否可修改 | false |
enumerable | 是否可被枚举(for...in、Object.keys) | false |
configurable | 是否可删除、可修改描述符 | false |
value | 属性值 | undefined |
4.2 Object.getOwnPropertyDescriptor
const obj = { name: "张三" };
const desc = Object.getOwnPropertyDescriptor(obj, "name");
console.log(desc);
// { value: '张三', writable: true, enumerable: true, configurable: true }字面量创建的属性四个配置项默认全为 true,而 defineProperty 定义的默认全为 false。
4.3 Object.defineProperty / defineProperties
const obj = {};
Object.defineProperty(obj, "id", {
value: 1001,
writable: false, // 不可修改
enumerable: false, // 不可枚举
configurable: false, // 不可删除
});
console.log(obj.id); // 1001
obj.id = 999; // 静默失败(严格模式抛错)
console.log(obj.id); // 1001
console.log(Object.keys(obj)); // [],不可枚举
// 批量定义
Object.defineProperties(obj, {
a: { value: 1, enumerable: true },
b: { value: 2, enumerable: true },
});五、数据属性与访问器属性
属性分为两类:数据属性(直接存值)与访问器属性(含 getter/setter,读取与写入时执行函数)。
const user = {
_age: 0, // 数据属性,约定下划线开头为内部值
get age() { // 访问器属性:读取时执行
return this._age;
},
set age(value) { // 访问器属性:写入时执行
if (value < 0 || value > 150) {
throw new Error("年龄不合法");
}
this._age = value;
},
};
user.age = 30;
console.log(user.age); // 30
user.age = 200; // 抛错:年龄不合法访问器属性的描述符没有 value 和 writable,取而代之的是 get 与 set:
const desc = Object.getOwnPropertyDescriptor(user, "age");
console.log(Object.keys(desc)); // ['get','set','enumerable','configurable']六、对象方法大全
6.1 遍历与取值
const obj = { a: 1, b: 2, c: 3 };
console.log(Object.keys(obj)); // ['a','b','c'],可枚举键
console.log(Object.values(obj)); // [1, 2, 3]
console.log(Object.entries(obj)); // [['a',1],['b',2],['c',3]]
for (const key in obj) { // for...in 会包含原型链上可枚举属性
console.log(key);
}6.2 Object.assign:合并
const target = { a: 1 };
const source = { b: 2, a: 100 }; // 同键覆盖
Object.assign(target, source);
console.log(target); // { a: 100, b: 2 }
// 经典用途:为对象设置默认值
function createUser(options) {
return Object.assign({ name: "默认", age: 18 }, options);
}
console.log(createUser({ age: 20 })); // { name: '默认', age: 20 }6.3 Object.freeze / Object.seal
| 方法 | 禁止新增 | 禁止删除 | 禁止修改值 | 修改描述符 |
|---|---|---|---|---|
Object.freeze | 是 | 是 | 是 | 否 |
Object.seal | 是 | 是 | 否 | 否 |
Object.preventExtensions | 是 | 否 | 否 | 否 |
const frozen = Object.freeze({ a: 1 });
frozen.a = 2; // 静默失败
console.log(frozen.a); // 1
console.log(Object.isFrozen(frozen)); // true
const sealed = Object.seal({ a: 1 });
sealed.b = 2; // 新增失败
sealed.a = 2; // 修改成功
console.log(sealed); // { a: 2 }注意 freeze 只冻结浅层,嵌套对象仍可修改。
6.4 Object.is
Object.is 与 === 基本相同,仅两处差异:
console.log(Object.is(NaN, NaN)); // true,而 NaN === NaN 为 false
console.log(Object.is(0, -0)); // false,而 0 === -0 为 true
console.log(Object.is("a", "a")); // true6.5 Object.fromEntries
fromEntries 是 entries 的逆操作,由键值对数组生成对象:
const entries = [["name", "张三"], ["age", 25]];
const obj = Object.fromEntries(entries);
console.log(obj); // { name: '张三', age: 25 }
// 与 Map 配合
const map = new Map([["x", 1], ["y", 2]]);
console.log(Object.fromEntries(map)); // { x: 1, y: 2 }
// 实用案例:过滤对象
const data = { a: 1, b: 2, c: 3 };
const filtered = Object.fromEntries(
Object.entries(data).filter(([k, v]) => v > 1)
);
console.log(filtered); // { b: 2, c: 3 }6.6 属性检测
const obj = { name: "张三" };
// in 运算符:会沿原型链查找
console.log("name" in obj); // true
console.log("toString" in obj); // true,原型链上的方法也算
// hasOwnProperty:只看自身属性
console.log(obj.hasOwnProperty("name")); // true
console.log(obj.hasOwnProperty("toString")); // false
// Object.hasOwn(ES2022 推荐写法)
console.log(Object.hasOwn(obj, "name")); // true6.7 获取全部自身属性名
const obj = { a: 1 };
Object.defineProperty(obj, "hidden", { value: 2, enumerable: false });
console.log(Object.keys(obj)); // ['a'],只含可枚举
console.log(Object.getOwnPropertyNames(obj)); // ['a','hidden'],含不可枚举
// 获取全部属性的完整描述符
console.log(Object.getOwnPropertyDescriptors(obj));
// { a: {value:1,...}, hidden: {value:2,...} }七、浅拷贝与深拷贝
7.1 浅拷贝
只复制第一层,嵌套对象仍共享引用:
const source = { name: "张三", address: { city: "北京" } };
// 方式一:扩展运算符
const copy1 = { ...source };
// 方式二:Object.assign
const copy2 = Object.assign({}, source);
copy1.name = "李四"; // 第一层互不影响
copy1.address.city = "上海"; // 嵌套对象共享!
console.log(source.address.city); // 上海7.2 JSON 方法与局限
const source = { name: "张三", address: { city: "北京" }, date: new Date() };
const copy = JSON.parse(JSON.stringify(source));
console.log(copy.date); // "2026-08-09T00:00:00.000Z",变成字符串!
// 局限:函数、undefined、Symbol 会被丢弃;Date 变字符串;循环引用抛错
const bad = { fn() {}, u: undefined, sym: Symbol("s") };
console.log(JSON.parse(JSON.stringify(bad))); // {}7.3 structuredClone:原生深拷贝
const source = { name: "张三", address: { city: "北京" }, date: new Date() };
const copy = structuredClone(source);
copy.address.city = "上海";
console.log(source.address.city); // 北京,互不影响
console.log(copy.date instanceof Date); // true,Date 正确保留7.4 手写递归深拷贝
function deepClone(value, map = new WeakMap()) {
if (value === null || typeof value !== "object") return value; // 原始值
if (map.has(value)) return map.get(value); // 处理循环引用
const clone = Array.isArray(value) ? [] : {};
map.set(value, clone);
for (const key of Object.keys(value)) {
clone[key] = deepClone(value[key], map);
}
return clone;
}
const obj = { a: 1, nested: { b: [1, 2] } };
const copy = deepClone(obj);
copy.nested.b.push(3);
console.log(obj.nested.b); // [1, 2],原对象不受影响八、原型属性:proto 与 prototype
__proto__ 与 prototype 是两个极易混淆的概念:
| 属性 | 存在于 | 指向 | 说明 |
|---|---|---|---|
__proto__ | 每个对象上 | 该对象的原型 | 即 Object.getPrototypeOf(obj) 的旧写法 |
prototype | 每个函数上 | 构造出来的实例的原型 | 只有函数才有,普通对象没有 |
function Person() {}
const p = new Person();
// 实例的 __proto__ 指向构造函数的 prototype
console.log(p.__proto__ === Person.prototype); // true
console.log(Object.getPrototypeOf(p) === Person.prototype); // true
// 函数的 __proto__ 指向 Function.prototype
console.log(Person.__proto__ === Function.prototype); // true
console.log(Person.prototype.constructor === Person); // true规范推荐使用 Object.getPrototypeOf / Object.setPrototypeOf 代替 __proto__。注意 Object.create(null) 的对象没有 __proto__ 属性。
九、Symbol 作为属性键
Symbol 值可以作属性键,且默认不可被 Object.keys、for...in 枚举:
const sym = Symbol("secret");
const obj = {
[sym]: "隐藏数据",
visible: 1,
};
console.log(obj[sym]); // 隐藏数据,只能用 Symbol 取到
console.log(Object.keys(obj)); // ['visible']
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(secret)]
// Symbol.for 创建全局注册的 Symbol,可跨作用域复用
const s1 = Symbol.for("id");
const s2 = Symbol.for("id");
console.log(s1 === s2); // true
// 内置 Symbol 定义对象默认行为
const iterable = {
*[Symbol.iterator]() {
yield 1;
yield 2;
},
};
console.log([...iterable]); // [1, 2]对象是 JavaScript 组织数据的核心形态,配合原型机制可以实现复用与继承。关于原型链的完整机制,见《原型与原型链》;ES6 之后更推荐的 class 语法,见《ES6+ 类》。