背景与收益
在实际开发中,我们经常遇到这样的场景:需要批量创建结构相似的对象,或者需要精确控制对象属性的行为(可写、可枚举、可配置等)。如果只用最基础的对象字面量和 Object.defineProperty,代码会变得冗长且难以维护。
本文将带你深入理解:
- 如何高效地批量定义对象属性及其描述符
- JavaScript 提供的对象限制方法及其实战应用场景
- 创建多个同类对象的最佳实践:工厂模式 vs 构造函数
适合已掌握 JavaScript 基础语法、希望提升对象操作能力的开发者。
一、批量定义对象属性
1.1 问题场景
在上一章节中,我们学习了 Object.defineProperty 来定义单个属性的描述符。但实际开发中,一个对象往往有多个属性需要配置。如果每个属性都调用一次 defineProperty,代码会非常冗余:
js
let obj = { JS: 1 };
Object.defineProperty(obj, 'name', {
value: 'XiaoWu',
writable: true,
enumerable: true,
configurable: true
});
Object.defineProperty(obj, 'age', {
value: 18,
writable: false,
enumerable: true,
configurable: true
});
能否通过遍历来优化?当然可以。
1.2 手动实现批量定义
我们可以将多个属性的描述符封装成对象,然后遍历处理:
js
let obj = {
JS: 1
};
let props = {
name: {
value: 'XiaoWu',
writable: true,
enumerable: true,
configurable: true
},
age: {
value: 18,
writable: false,
enumerable: true,
configurable: true
}
};
function defineProperties(obj, properties) {
for (let prop in properties) {
// hasOwnProperty 用于判断是否为对象自有属性(非继承属性)
if (properties.hasOwnProperty(prop)) {
Object.defineProperty(obj, prop, properties[prop]);
}
}
return obj;
}
defineProperties(obj, props);
console.log(obj.name); // XiaoWu
console.log(obj.age); // 18
1.3 原生方法:Object.defineProperties
JavaScript 原生提供了 Object.defineProperties 方法,功能与我们手动实现的一致,但处理了更多边界情况:
js
Object.defineProperties(obj, props);
实战案例:私有属性的访问控制
在实际开发中,我们常用 _ 前缀标识私有属性,并通过 getter/setter 控制访问:
js
var obj = {
_age: 20 // 私有属性,存储真实数据
};
Object.defineProperties(obj, {
name: {
configurable: true,
enumerable: true,
value: "小吴",
writable: true
},
age: {
configurable: false,
enumerable: false, // 不可枚举,for-in 遍历时不会出现
get: function() {
return this._age;
},
set: function(value) {
this._age = value;
}
}
});
console.log(obj.age); // 20
console.log(obj); // { _age: 20, name: '小吴' } 注意:age 不可枚举
obj.age = 18;
console.log(obj.age); // 18
设计思想:
_age是真实数据存储,外部不应直接访问age是对外暴露的接口,通过 getter/setter 控制访问逻辑- 这种"马甲模式"可以在 setter 中加入校验、日志等逻辑,保证数据安全
1.4 对象字面量中的 getter/setter
除了使用 defineProperties,我们也可以直接在对象字面量中定义 getter/setter:
js
var obj = {
_age: 20,
set age(value) {
this._age = value;
},
get age() {
return this._age;
}
};
两种写法的差异:
| 写法 | 控制台输出 | 精细控制 |
|---|---|---|
| 对象字面量 | { _age: 20, age: [Getter/Setter] } |
无法配置 configurable/enumerable |
| defineProperties | { _age: 20 } |
可精确控制所有描述符 |

图 1:getter/setter 在终端的表达形式
选择建议:
- 简单场景:直接在对象字面量中定义,代码更简洁
- 需要精细控制(如设置不可枚举):使用
defineProperties
二、对象方法补充
2.1 获取属性描述符
之前我们提到,[[]] 标记的内部属性无法直接访问,需要通过特定 API 获取:
js
// 获取单个属性的描述符
Object.getOwnPropertyDescriptor(obj, prop);
// 获取所有自有属性的描述符
Object.getOwnPropertyDescriptors(obj);
示例:
js
var obj = {
names: "小吴",
age: 18
};
console.log(Object.getOwnPropertyDescriptor(obj, 'names'));
// { value: '小吴', writable: true, enumerable: true, configurable: true }
console.log(Object.getOwnPropertyDescriptors(obj));
// {
// names: { value: '小吴', writable: true, enumerable: true, configurable: true },
// age: { value: 18, writable: true, enumerable: true, configurable: true }
// }

图 2:obj 对象的属性描述符详情
2.2 对象限制方法
JavaScript 提供了三个方法来限制对象的可变性,它们的限制程度逐级递增:
2.2.1 Object.preventExtensions - 禁止扩展
禁止给对象添加新属性,但可以修改和删除现有属性:
js
var obj = {
names: "小吴",
age: 18
};
Object.preventExtensions(obj);
obj.newProperty = 'new'; // 添加失败(严格模式下报错)
console.log(obj.newProperty); // undefined
2.2.2 Object.seal - 密封对象
在 preventExtensions 基础上,将所有现有属性的 configurable 设为 false,禁止删除和重新配置属性:
js
Object.seal(obj);
delete obj.age; // 删除失败
console.log(obj.age); // 18
obj.names = "JS高级"; // 可以修改值
console.log(obj.names); // JS高级
2.2.3 Object.freeze - 冻结对象
在 seal 基础上,将所有现有属性的 writable 设为 false,完全冻结对象:
js
Object.freeze(obj);
obj.names = "why"; // 修改失败
console.log(obj.names); // JS高级
实战应用:Vue 性能优化
在 Vue 中,响应式系统会劫持对象的 getter/setter。如果有大量静态数据(如几十万条配置数据)不需要响应式,可以用 Object.freeze 冻结,避免 Vue 进行响应式处理,显著提升性能:
js
// 大量静态数据
const staticData = Object.freeze([
{ id: 1, name: '数据1' },
{ id: 2, name: '数据2' },
// ... 几十万条
]);
export default {
data() {
return {
list: staticData // 不会被 Vue 响应式处理
};
}
};
三种方法对比:
| 方法 | 禁止新增 | 禁止删除 | 禁止修改值 | 禁止重新配置 |
|---|---|---|---|---|
| preventExtensions | ✅ | ❌ | ❌ | ❌ |
| seal | ✅ | ✅ | ❌ | ✅ |
| freeze | ✅ | ✅ | ✅ | ✅ |
三、创建多个对象的方案
3.1 问题场景
假设我们需要创建多个 Person 对象,每个对象都有 name、age、sex、address 等属性,以及 eating、running 等方法。如果用对象字面量:
js
var p1 = {
name: "小吴",
age: 20,
sex: "男",
address: "福建",
eating: function() {
console.log(this.name + "在吃烧烤");
},
running: function() {
console.log(this.name + "在跑步做运动");
}
};
var p2 = {
name: "why",
age: 35,
sex: "男",
address: "广州",
eating: function() {
console.log(this.name + "在吃烧烤");
},
running: function() {
console.log(this.name + "在跑步做运动");
}
};
问题:代码重复率极高,难以维护。
解决方案:
- 工厂模式
- 构造函数
- ES6 Class(后续章节)
- 原型 + Object.create(后续章节)
本文重点讲解前两种。
3.2 方案一:工厂模式
3.2.1 基本实现
工厂模式的核心思想:抽离共性,参数化差异,流水线生产。
js
function createPerson(name, age, sex, occupation, address) {
var p = new Object();
p.name = name;
p.age = age;
p.sex = sex;
p.occupation = occupation;
p.address = address;
p.eating = function() {
console.log(this.name + "在吃满汉全席");
};
return p;
}
var p1 = createPerson("小吴", 20, "男", "大三学生", "福建");
var p2 = createPerson("why", 35, "男", "全栈工程师兼教师", "广州");
console.log(p1, p2);

图 3:new 调用所产生的结构共性
3.2.2 工厂模式的缺点
- 类型信息丢失 :所有对象的类型都是
Object,无法区分是 Person 还是其他类型 - 无法利用原型链:每个对象都有自己的方法副本,无法共享,浪费内存
- 调试困难:堆栈跟踪中难以定位对象的创建源
js
console.log(p1); // Object { name: '小吴', age: 20, ... }
// 无法看出这是一个 Person 对象
适用场景:
- 简单的对象创建,不需要类型区分
- 临时性的数据结构封装
3.3 方案二:构造函数
3.3.1 什么是构造函数
构造函数本质上是普通函数,但通过 new 关键字调用时,会执行特殊的对象创建流程:
js
function foo() {
console.log("foo~");
}
// 普通调用
foo();
// 构造函数调用
new foo(); // 或 new foo
3.3.2 new 操作符的执行流程
当使用 new 调用函数时,会自动执行以下步骤:
- 在内存中创建一个新的空对象
- 将这个对象的
[[Prototype]]指向构造函数的prototype属性 - 将构造函数内部的
this指向这个新对象 - 执行构造函数的代码(给
this添加属性) - 如果构造函数返回一个对象,则返回该对象;否则返回步骤 1 创建的对象
js
function foo() {
// 内部隐式执行:
// var obj = {};
// this = obj;
console.log("foo~");
// 隐式返回 this
}
var f1 = new foo(); // foo~
console.log(f1); // foo {}
类型验证:
js
function XiaoWu(name) {
this.name = name;
console.log("我是小吴");
}
var f1 = new XiaoWu("小吴"); // 我是小吴
console.log(f1); // XiaoWu { name: '小吴' }
console.log(f1.__proto__.constructor.name); // XiaoWu
3.3.3 构造函数实现
js
function Person(name, age, sex, address) {
this.name = name;
this.age = age;
this.sex = sex;
this.address = address;
this.eating = function() {
console.log(this.name + "在吃鱿鱼须");
};
this.running = function() {
console.log(this.name + "在跟坤坤打篮球");
};
}
var f1 = new Person("小吴同学", 20, "男", "福建");
console.log(f1);
// Person {
// name: '小吴同学',
// age: 20,
// sex: '男',
// address: '福建',
// eating: [Function (anonymous)],
// running: [Function (anonymous)]
// }
var f2 = new Person("小满zs", 23, "男", "北京");
var f3 = new Person("洛洛", 20, "萌妹子", "福建");

图 4:构造函数 Person 调用结果
3.3.4 如何识别构造函数
构造函数与普通函数在语法上没有区别,社区约定了以下规范:
- 命名规范:首字母大写,使用大驼峰命名(PascalCase)
- 编辑器提示 :当函数内使用
this赋值时,编辑器会提示"此构造函数可能会转换为类声明"
js
function XiaoWu(name) {
this.name = name; // 使用 this 赋值,编辑器识别为构造函数
}

图 5:如何区分是否为构造函数(编辑器中的构造函数)
注意 :只有通过 new 调用时,函数才真正成为构造函数。
3.3.5 构造函数的缺点
每次创建对象时,方法都会被重新创建,导致内存浪费:
js
function foo() {
function bar() {
console.log("你猜一不一样");
}
return bar;
}
var f1 = foo();
var f2 = foo();
console.log(f1 === f2); // false 每次调用都创建新的函数对象
应用到构造函数:
js
function XiaoWu(name, age, sex, address) {
this.name = name;
this.age = age;
this.sex = sex;
this.address = address;
// 每次 new 都会创建新的函数对象
this.eating = function() {
console.log(this.name + "在吃鱿鱼须");
};
this.running = function() {
console.log(this.name + "在跟坤坤打篮球");
};
}
var f1 = new XiaoWu("小吴同学", 20, "男", "福建");
var f2 = new XiaoWu("小吴同学", 20, "男", "福建");
console.log(f1.eating === f2.eating); // false
console.log(f1.running === f2.running); // false
问题分析:
- 虽然
f1和f2的eating方法功能完全相同,但它们是两个不同的函数对象 - 当创建大量实例时,会造成内存浪费
解决方案:使用原型(Prototype),将方法定义在原型上,所有实例共享。这将在下一章节详细讲解。
四、工厂模式 vs 构造函数
| 对比维度 | 工厂模式 | 构造函数 |
|---|---|---|
| 调用方式 | 普通函数调用 | 使用 new 关键字 |
| 类型识别 | 所有对象都是 Object |
可以识别具体类型(如 Person) |
| 原型链 | 无法利用 | 可以利用原型共享方法 |
| 内存占用 | 每个对象独立方法 | 每个对象独立方法(未优化时) |
| 代码复杂度 | 简单直观 | 需要理解 new 和 this |
| 适用场景 | 简单对象创建 | 需要类型区分和原型链的场景 |
选择建议:
- 简单场景、不需要类型区分:工厂模式
- 需要类型识别、后续会用到原型链:构造函数
- 现代开发:优先使用 ES6 Class(本质是构造函数的语法糖)
五、实战建议
5.1 属性描述符使用场景
- 配置对象保护:将配置对象冻结,防止被意外修改
- 私有属性模拟:通过不可枚举 + getter/setter 实现访问控制
- 数据校验:在 setter 中加入校验逻辑
5.2 对象创建模式选择
- 单个对象:对象字面量
- 少量同类对象:工厂模式或构造函数
- 大量同类对象:构造函数 + 原型(下一章)
- 现代项目:ES6 Class
5.3 性能优化要点
- 避免在构造函数中定义方法:应该定义在原型上(下一章详解)
- 大量静态数据使用
Object.freeze:特别是在 Vue 等响应式框架中 - 合理使用属性描述符:不要过度使用,会增加代码复杂度
六、总结与下一步
6.1 核心要点
Object.defineProperties可以批量定义属性描述符,比多次调用defineProperty更高效preventExtensions、seal、freeze三个方法提供了不同级别的对象保护- 工厂模式简单直观,但无法识别对象类型
- 构造函数通过
new调用,可以创建具有特定类型的对象 - 构造函数的缺点是方法无法共享,需要通过原型解决
6.2 遗留问题
在本文中,我们多次提到"原型"(Prototype),并且发现构造函数存在方法无法共享的问题。在控制台查看对象时,总能看到神秘的 [[Prototype]] 属性:

图 6:对象中的原型世界
6.3 下一章预告
在下一章节中,我们将深入学习:
- 什么是原型(Prototype)和原型链
- 如何通过原型实现方法共享,解决构造函数的内存浪费问题
- 原型链的查找机制和继承原理
- 大量内存图帮助理解原型的指向关系
原型是 JavaScript 中最重要的概念之一,理解原型是掌握 JavaScript 面向对象编程的关键。