构造函数模式:JavaScript 对象创建的核心机制
在构建应用程序时,我们经常需要创建多个拥有相同结构但包含不同数据的对象——比如系统中的用户列表或购物车中的商品。如果每次都手动编写对象字面量,不仅效率低下,而且难以维护。构造函数模式正是为了解决这个问题而生,它提供了一种标准化的方式来批量创建对象实例。
构造函数的基本概念
构造函数是 JavaScript 中用于创建特定类型对象的函数。按照约定,构造函数的名称通常以大写字母开头,以区别于普通函数。
基本构造函数
javascript
// 基本构造函数
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
// 在构造函数中定义方法(不推荐)
this.sayName = function () {
console.log(this.name);
};
}
// 使用new操作符创建实例
const person1 = new Person("John", 25, "Developer");
const person2 = new Person("Sarah", 30, "Designer");
console.log(person1.name); // John
console.log(person2.age); // 30
person1.sayName(); // John
person2.sayName(); // Sarahnew 操作符的工作原理
当使用new操作符调用构造函数时,JavaScript 引擎会执行以下四个步骤:
- 创建新对象:创建一个空的普通 JavaScript 对象
- 设置原型链:将新对象的
[[Prototype]]指向构造函数的prototype - 绑定 this:将构造函数中的
this指向新创建的对象 - 返回对象:如果构造函数没有显式返回其他对象,则返回新创建的对象
javascript
function Person(name) {
// 第3步:this指向新创建的对象
this.name = name;
this.sayName = function () {
console.log(this.name);
};
// 演示返回不同的情况
// 如果没有显式return,返回this(新创建的对象)
// 如果返回基本类型,仍然返回this
// 如果返回对象,则返回该对象(而不是this)
}
// 第1步:创建新对象 {}
// 第2步:设置原型链:{}.__proto__ = Person.prototype
// 第3步:绑定this,执行构造函数
// 第4步:返回新对象
const person = new Person("John");模拟 new 操作符的实现
为了更好地理解new操作符,我们可以自己实现一个类似的函数:
javascript
function myNew(constructor, ...args) {
// 1. 创建新对象
const obj = {};
// 2. 设置原型链
obj.__proto__ = constructor.prototype;
// 3. 绑定this并执行构造函数
const result = constructor.apply(obj, args);
// 4. 返回结果(如果构造函数返回对象,则返回该对象,否则返回新创建的对象)
return typeof result === "object" && result !== null ? result : obj;
}
// 使用我们的myNew函数
function Person(name) {
this.name = name;
this.sayName = function () {
console.log(this.name);
};
}
const person = myNew(Person, "John");
console.log(person.name); // John
person.sayName(); // John
console.log(person.__proto__ === Person.prototype); // true构造函数与普通函数的区别
调用方式的区别
javascript
function Person(name) {
this.name = name;
console.log("this:", this);
}
// 1. 作为构造函数调用
const person = new Person("John");
// this: Person { name: 'John' }
// 2. 作为普通函数调用
Person("John");
// this: Window (在严格模式下是 undefined)返回值的处理
javascript
function TestConstructor() {
this.prop = "created";
// 情况1:没有return语句
// const obj1 = new TestConstructor();
// obj1是 { prop: 'created' }
// 情况2:return基本类型
// return 42;
// const obj2 = new TestConstructor();
// obj2仍然是 { prop: 'created' }
// 情况3:return对象
// return { custom: 'object' };
// const obj3 = new TestConstructor();
// obj3是 { custom: 'object' }
}
const obj = new TestConstructor();
console.log(obj.prop); // 'created'原型与构造函数的关系
prototype 属性
每个函数都有一个prototype属性,这个属性是一个对象,包含了由该函数创建的实例可以共享的属性和方法。
javascript
function Car(brand, model) {
this.brand = brand;
this.model = model;
}
// 在原型上定义方法,所有实例共享
Car.prototype.start = function () {
console.log(`${this.brand} ${this.model} is starting`);
};
Car.prototype.stop = function () {
console.log(`${this.brand} ${this.model} is stopping`);
};
const car1 = new Car("Toyota", "Camry");
const car2 = new Car("Honda", "Civic");
car1.start(); // Toyota Camry is starting
car2.start(); // Honda Civic is starting
// 验证方法确实是共享的
console.log(car1.start === car2.start); // true
// 验证原型链关系
console.log(car1.__proto__ === Car.prototype); // true
console.log(car2.__proto__ === Car.prototype); // trueconstructor 属性
默认情况下,原型对象有一个constructor属性,指回构造函数:
javascript
function Person(name) {
this.name = name;
}
console.log(Person.prototype.constructor === Person); // true
const person = new Person("John");
console.log(person.constructor === Person); // true
console.log(person.__proto__.constructor === Person); // true最佳实践:在原型上定义方法
方法定义的位置选择
javascript
// ❌ 不推荐:在构造函数中定义方法
function BadExample(name) {
this.name = name;
// 每次创建实例都会创建新的函数对象
this.sayName = function () {
console.log(this.name);
};
}
const person1 = new BadExample("John");
const person2 = new BadExample("Sarah");
console.log(person1.sayName === person2.sayName); // false
// 每个实例都有自己的sayName函数,浪费内存javascript
// ✅ 推荐:在原型上定义方法
function GoodExample(name) {
this.name = name;
}
GoodExample.prototype.sayName = function () {
console.log(this.name);
};
const person1 = new GoodExample("John");
const person2 = new GoodExample("Sarah");
console.log(person1.sayName === person2.sayName); // true
// 所有实例共享同一个sayName函数构造函数模板
这是一个常用的构造函数模板:
javascript
function Animal(species, habitat) {
// 实例独有的属性
this.species = species;
this.habitat = habitat;
this.age = 0;
// 如果有需要,可以在这里定义私有方法
// 但更推荐使用模块模式
}
// 共享的方法定义在原型上
Animal.prototype.eat = function () {
console.log(`${this.species} is eating`);
this.age++;
};
Animal.prototype.sleep = function () {
console.log(`${this.species} is sleeping`);
};
Animal.prototype.getAge = function () {
return this.age;
};
// 静态属性和方法(属于构造函数本身)
Animal.createHuman = function () {
return new Animal("Human", "Urban");
};
Animal.defaultDiet = "omnivore";
// 使用构造函数
const lion = new Animal("Lion", "Savanna");
lion.eat(); // Lion is eating
lion.sleep(); // Lion is sleeping
// 使用静态方法
const human = Animal.createHuman();
console.log(human.species); // Human
console.log(Animal.defaultDiet); // omnivore构造函数的继承
使用原型链实现继承
javascript
function Animal(species) {
this.species = species;
}
Animal.prototype.move = function () {
console.log(`${this.species} is moving`);
};
function Dog(name, breed) {
// 调用父构造函数(继承属性)
Animal.call(this, "Dog");
this.name = name;
this.breed = breed;
}
// 继承原型(继承方法)
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复constructor指向
// 添加子类特有的方法
Dog.prototype.bark = function () {
console.log(`${this.name} barks: Woof!`);
};
// 重写父类方法
Dog.prototype.move = function () {
console.log(`${this.name} the ${this.breed} is running`);
};
const dog = new Dog("Buddy", "Golden Retriever");
// 调用子类方法
dog.bark(); // Buddy barks: Woof!
// 调用重写的方法
dog.move(); // Buddy the Golden Retriever is running
// 调用父类方法(通过原型链)
Animal.prototype.move.call(dog); // Dog is moving实用的继承辅助函数
javascript
function inherit(child, parent) {
// 继承原型
child.prototype = Object.create(parent.prototype);
// 修复constructor
child.prototype.constructor = child;
// 添加到父类的引用
child.super = parent;
}
function Vehicle(wheels) {
this.wheels = wheels;
}
Vehicle.prototype.start = function () {
console.log("Vehicle started");
};
function Car(brand, model) {
Vehicle.super.call(this, 4); // 调用父构造函数
this.brand = brand;
this.model = model;
}
inherit(Car, Vehicle);
Car.prototype.start = function () {
console.log(`${this.brand} ${this.model} engine started`);
};
const car = new Car("Toyota", "Camry");
car.start(); // Toyota Camry engine started
console.log(car.wheels); // 4模块化构造函数模式
IIFE 模式保护构造函数
javascript
const PersonModule = (function () {
"use strict";
// 私有变量和函数
let personCount = 0;
function validateName(name) {
return typeof name === "string" && name.length > 0;
}
// 构造函数
function Person(name, age) {
if (!validateName(name)) {
throw new Error("Invalid name");
}
if (age < 0) {
throw new Error("Age cannot be negative");
}
this.name = name;
this.age = age;
this.id = ++personCount;
}
// 原型方法
Person.prototype.sayName = function () {
console.log(`My name is ${this.name}`);
};
Person.prototype.sayAge = function () {
console.log(`I am ${this.age} years old`);
};
// 静态方法
Person.getPersonCount = function () {
return personCount;
};
Person.createAdult = function (name) {
return new Person(name, 18);
};
return Person;
})();
// 使用模块化的构造函数
const person1 = new PersonModule("John", 25);
const person2 = new PersonModule("Sarah", 30);
person1.sayName(); // My name is John
person2.sayAge(); // I am 30 years old
console.log(PersonModule.getPersonCount()); // 2
const adult = PersonModule.createAdult("Mike");
console.log(adult.age); // 18构造函数模式的陷阱和注意事项
1. 忘记使用 new 操作符
javascript
function Person(name) {
this.name = name;
}
// ❌ 忘记使用new
const person = Person("John"); // this指向全局对象(非严格模式)或undefined(严格模式)
console.log(window.name); // 'John'(污染全局变量)
// ✅ 正确使用new
const person2 = new Person("John");
console.log(person2.name); // 'John'解决方案:自调用构造函数
javascript
function Person(name) {
// 确保总是使用new调用
if (!(this instanceof Person)) {
return new Person(name);
}
this.name = name;
}
// 现在两种方式都可以工作
const person1 = new Person("John");
const person2 = Person("Sarah");
console.log(person1.name); // John
console.log(person2.name); // Sarah2. 原型被意外覆盖
javascript
function Car(brand) {
this.brand = brand;
}
Car.prototype.drive = function () {
console.log(`${this.brand} is driving`);
};
const car = new Car("Toyota");
// ❌ 覆盖整个原型
Car.prototype = {
fly: function () {
console.log(`${this.brand} is flying`);
},
};
console.log(car.drive); // undefined,因为car的原型没有改变
car.fly(); // TypeError: car.fly is not a function3. 引用类型的原型属性
javascript
function Family() {
// 实例属性
this.familyName = "Smith";
}
// ❌ 在原型上定义引用类型
Family.prototype.members = []; // 所有实例共享同一个数组
const family1 = new Family();
const family2 = new Family();
family1.members.push("John");
console.log(family2.members); // ['John'] - 意外共享!
// ✅ 正确的做法
Family.prototype.getMembers = function () {
// 每个实例都有自己的成员数组
if (!this._members) {
this._members = [];
}
return this._members;
};
family1.getMembers().push("John");
console.log(family2.getMembers()); // [] - 独立的数组现代 JavaScript 中的替代方案
Class 语法
现代 JavaScript 提供了更友好的 Class 语法,底层仍然是基于构造函数和原型:
javascript
// 现代Class语法
class Person {
// 构造函数
constructor(name, age) {
this.name = name;
this.age = age;
}
// 实例方法(相当于原型方法)
sayName() {
console.log(`My name is ${this.name}`);
}
// 静态方法
static createAdult(name) {
return new Person(name, 18);
}
// Getter
get info() {
return `${this.name} (${this.age} years old)`;
}
}
// 使用Class
const person = new Person("John", 25);
person.sayName(); // My name is John
console.log(person.info); // John (25 years old)
const adult = Person.createAdult("Sarah");
console.log(adult.age); // 18工厂函数模式
有时候,工厂函数比构造函数更灵活:
javascript
function createPerson(name, age) {
return {
name,
age,
sayName() {
console.log(`My name is ${this.name}`);
},
sayAge() {
console.log(`I am ${this.age} years old`);
},
};
}
const person = createPerson("John", 25);
person.sayName(); // My name is John
// 优点:不需要使用new,可以返回不同的对象结构
// 缺点:没有明确的类型检查,每个实例都有自己的方法副本总结
构造函数模式是 JavaScript 中创建对象的基础机制,理解它对于掌握 JavaScript 面向对象编程至关重要:
- 构造函数是特殊的函数,用于创建特定类型的对象
- new 操作符负责创建对象、设置原型、绑定 this 和返回对象
- 方法应该定义在原型上以实现内存共享和性能优化
- 继承需要结合原型链和构造函数调用来实现完整的属性和方法继承
- 注意常见陷阱,如忘记使用 new、原型被覆盖、引用类型共享等
虽然现代 JavaScript 提供了 Class 语法等更友好的 API,但底层仍然是基于构造函数和原型机制。理解构造函数的工作原理有助于我们更好地理解 JavaScript 的面向对象特性,编写更高效、更健壮的代码。