Skip to content

构造函数模式: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(); // Sarah

new 操作符的工作原理

当使用new操作符调用构造函数时,JavaScript 引擎会执行以下四个步骤:

  1. 创建新对象:创建一个空的普通 JavaScript 对象
  2. 设置原型链:将新对象的[[Prototype]]指向构造函数的prototype
  3. 绑定 this:将构造函数中的this指向新创建的对象
  4. 返回对象:如果构造函数没有显式返回其他对象,则返回新创建的对象
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); // true

constructor 属性

默认情况下,原型对象有一个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); // Sarah

2. 原型被意外覆盖

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 function

3. 引用类型的原型属性

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 的面向对象特性,编写更高效、更健壮的代码。