在 JavaScript 中,对象 (Object) 是核心概念之一。它们是数据(属性)和操作数据的方法(行为)的集合。当我们需要创建多个具有相同结构和行为的对象时,手动创建每个对象会变得重复且低效。这时,对象构造器 (Object Constructor) 便应运而生。

对象构造器是一种特殊的函数,它允许我们定义一个蓝图,然后根据这个蓝图创建出许多具有相同属性和方法的对象实例。在 ES6 (ECMAScript 2015) 之前,构造函数是 JavaScript 实现面向对象编程 (OOP) 的主要方式。ES6 引入的 class 关键字,虽然提供了更简洁、更像传统 OOP 语言的语法糖,但其底层机制仍然是基于构造函数和原型链的。

本教程将深入探讨 JavaScript 对象构造器的概念、工作原理、如何定义属性和方法、它与原型链的关系,以及其在现代 JavaScript (class) 中的体现。通过掌握对象构造器,您将能够更有效地组织和管理您的代码,为构建复杂应用程序打下坚实基础。


前置知识

在开始本教程之前,建议您具备以下 JavaScript 基础知识:

  • 变量与数据类型: 了解 JavaScript 的基本数据类型(数字、字符串、布尔值)和对象类型。
  • 函数: 熟悉函数的声明、调用、参数以及 return 语句。
  • 对象字面量: 了解 const obj = { key: 'value' }; 这种创建对象的方式。
  • this 关键字:this 的上下文绑定规则有初步了解(本教程会进一步深入 new 绑定)。

目录

  1. 理解 JavaScript 对象构造器
    • 1.1 什么是对象构造器?
    • 1.2 构造器的基本语法与 new 关键字
    • 1.3 构造器内部的 this 关键字
  2. 构造器的属性与方法
    • 2.1 定义实例属性
    • 2.2 定义实例方法 (在构造器内部)
    • 2.3 优化:通过原型 (Prototype) 共享方法
      • 原型链 ([[Prototype]]) 简介
      • 将方法添加到 prototype
      • 为什么要在 prototype 上定义方法?
  3. new 关键字的工作原理
  4. 对象构造器与工厂函数 (Factory Functions)
    • 4.1 什么是工厂函数?
    • 4.2 构造器 vs. 工厂函数的对比
  5. ES6 class:现代构造器的语法糖
    • 5.1 classconstructor 的关系
    • 5.2 类方法与原型
  6. 对象构造器的优缺点
    • 6.1 优点
    • 6.2 缺点
  7. 最佳实践
  8. 总结
  9. 附录:术语表

1. 理解 JavaScript 对象构造器

1.1 什么是对象构造器?

对象构造器 是一个用于创建和初始化对象的特殊函数。它的主要目的是作为对象的蓝图或模板,以便你可以根据这个蓝图创建多个具有相同结构(属性)和行为(方法)的独立对象实例。

当一个普通函数被用作构造器时,它通常遵循以下约定:

  • 函数名通常以大写字母开头(这是一个约定,而非强制要求,但强烈推荐)。
  • 它必须与 new 关键字一起使用才能发挥构造器的作用。
1.2 构造器的基本语法与 new 关键字

使用构造器创建对象的步骤如下:

  1. 定义构造函数:

    function Person(name, age) {
      this.name = name; // 实例属性
      this.age = age;   // 实例属性
    }
    

    这里,Person 就是一个构造函数。

  2. 使用 new 关键字创建实例:

    const person1 = new Person('Alice', 30);
    const person2 = new Person('Bob', 25);
    
    console.log(person1.name); // Alice
    console.log(person2.age);  // 25
    

    person1person2Person 构造函数创建的两个独立的对象实例。它们各自拥有 nameage 属性。

1.3 构造器内部的 this 关键字

在 JavaScript 中,this 关键字的指向是动态的,它取决于函数的调用方式。当一个函数作为构造器被 new 关键字调用时,this 的行为会发生特殊绑定:

  • new 关键字执行构造函数时,它会将 this 绑定到新创建的空对象上。
  • 因此,在构造函数内部,所有通过 this.propertyName = value 形式定义的属性,都会成为该新创建对象的实例属性
function Car(make, model) {
  console.log('Inside constructor, this:', this); // 这里的 this 指向新创建的 Car 实例
  this.make = make;
  this.model = model;
  this.year = 2023; // 所有实例都会有 year 属性
}

const myCar = new Car('Toyota', 'Camry');
console.log(myCar); // Car { make: 'Toyota', model: 'Camry', year: 2023 }

重要提示: 如果调用构造函数时忘记使用 new 关键字,this 将会指向全局对象(在浏览器中是 window,严格模式下是 undefined),这会导致意外的行为和难以调试的错误。因此,使用大写字母作为构造函数名的约定,也是为了提醒开发者在使用时务必加上 new


2. 构造器的属性与方法

2.1 定义实例属性

实例属性是每个由构造器创建的对象实例都拥有的、独立的数据。它们在构造函数内部使用 this.propertyName = value 的形式定义。

function Product(name, price) {
  this.id = Math.random().toString(36).substring(2, 9); // 每个产品都有独立的 ID
  this.name = name;
  this.price = price;
  this.isAvailable = true; // 初始状态为可用
}

const laptop = new Product('Laptop', 1200);
const mouse = new Product('Mouse', 25);

console.log(laptop.id);         // 随机 ID
console.log(laptop.name);       // Laptop
console.log(mouse.price);       // 25
console.log(mouse.isAvailable); // true
2.2 定义实例方法 (在构造器内部)

实例方法是每个由构造器创建的对象实例都拥有的、独立的行为。它们在构造函数内部使用 this.methodName = function() { ... } 的形式定义。

function Dog(name, breed) {
  this.name = name;
  this.breed = breed;

  // 这是一个实例方法
  this.bark = function() {
    console.log(`${this.name} (${this.breed}) says Woof!`);
  };
}

const dog1 = new Dog('Buddy', 'Golden Retriever');
const dog2 = new Dog('Max', 'German Shepherd');

dog1.bark(); // Buddy (Golden Retriever) says Woof!
dog2.bark(); // Max (German Shepherd) says Woof!

// 检查两个实例的 bark 方法是否是同一个函数对象
console.log(dog1.bark === dog2.bark); // false

虽然这种方式可行,但它有一个显著的缺点:每次创建新的 Dog 实例时,bark 方法都会被重新创建一次。 这意味着每个实例都有自己的 bark 函数副本,这会浪费内存,并且在有大量实例时可能影响性能。

2.3 优化:通过原型 (Prototype) 共享方法

为了避免为每个实例都创建方法副本,我们可以利用 JavaScript 的原型链 (Prototype Chain) 机制。将方法定义在构造函数的 prototype 对象上,可以实现所有实例共享这些方法。

原型链 ([[Prototype]]) 简介

每个 JavaScript 对象都有一个内部属性 [[Prototype]](在旧版浏览器中可通过 __proto__ 访问,但更推荐 Object.getPrototypeOf()Object.setPrototypeOf()),它指向其原型对象 (prototype object)
当访问一个对象的属性或方法时,如果该对象本身没有,JavaScript 会沿着 [[Prototype]] 链接向上查找,直到找到该属性或方法,或到达原型链的顶端 (Object.prototype)。

将方法添加到 prototype

构造函数本身也有一个 prototype 属性,它指向一个对象。这个对象就是所有由该构造函数创建的实例的原型。

function Cat(name, color) {
  this.name = name;
  this.color = color;
}

// 将 speak 方法添加到 Cat 构造函数的 prototype 对象上
Cat.prototype.speak = function() {
  console.log(`${this.name} (${this.color}) says Meow!`);
};

Cat.prototype.sleep = function() {
  console.log(`${this.name} is sleeping.`);
};

const cat1 = new Cat('Whiskers', 'ginger');
const cat2 = new Cat('Shadow', 'black');

cat1.speak(); // Whiskers (ginger) says Meow!
cat2.sleep(); // Shadow is sleeping.

// 检查两个实例的 speak 方法是否是同一个函数对象
console.log(cat1.speak === cat2.speak); // true (它们引用的是同一个原型上的函数)
console.log(cat1.speak === Cat.prototype.speak); // true
为什么要在 prototype 上定义方法?
  • 内存效率: 所有实例共享同一个方法的引用,而不是每个实例都有自己的方法副本,这节省了内存。
  • 性能: 由于方法只创建一次,初始化实例的速度可能更快。
  • 继承: 原型链是 JavaScript 实现继承的基石,将方法放在原型上是实现继承的标准做法。

3. new 关键字的工作原理

当使用 new 关键字调用一个函数时,幕后会发生以下四个步骤:

  1. 创建一个新的空对象: 一个全新的、空的 JavaScript 对象被创建出来。
  2. 链接原型: 新创建的对象的 [[Prototype]] 属性会被链接到构造函数的 prototype 对象。这意味着新对象可以访问构造函数 prototype 上的所有属性和方法。
  3. 绑定 this 并执行构造函数: 构造函数被调用,其内部的 this 关键字被绑定到这个新创建的对象。构造函数中的代码会执行,通常用于初始化新对象的实例属性。
  4. 返回新对象: 如果构造函数没有显式地返回一个对象(如果返回原始值,则忽略),那么这个新创建并初始化过的对象就会被返回。
// 假设有构造函数 Function MyConstructor() { ... }
// 当调用 const instance = new MyConstructor(); 时:

// 1. 创建一个新对象
// let obj = {};

// 2. 链接原型
// obj.__proto__ = MyConstructor.prototype; // 或者 Object.setPrototypeOf(obj, MyConstructor.prototype);

// 3. 绑定 this 并执行构造函数
// let result = MyConstructor.call(obj); // 将 obj 绑定为 this

// 4. 返回新对象
// return typeof result === 'object' && result !== null ? result : obj;

4. 对象构造器与工厂函数 (Factory Functions)

除了构造器,工厂函数 (Factory Functions) 也是创建对象的另一种常见方式。

4.1 什么是工厂函数?

工厂函数是一个普通的函数,它返回一个新的对象,而不需要使用 new 关键字。

function createPerson(name, age) {
  return {
    name: name,
    age: age,
    greet: function() {
      console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
    }
  };
}

const personA = createPerson('Charlie', 40);
const personB = createPerson('Diana', 35);

personA.greet(); // Hello, my name is Charlie and I am 40 years old.
4.2 构造器 vs. 工厂函数的对比
特性 对象构造器 (使用 new) 工厂函数 (不使用 new)
调用方式 必须使用 new 关键字。 作为普通函数调用。
命名约定 函数名首字母大写。 函数名首字母小写。
this 绑定 自动绑定到新创建的实例。 this 绑定规则与普通函数相同(取决于调用方式)。
原型链 自动建立原型链 (instance.__proto__ === Constructor.prototype)。 需要手动设置 (Object.setPrototypeOf) 或不使用原型链。
方法共享 通常通过 prototype 实现方法共享,节省内存。 默认每个对象都有方法副本,除非手动管理。
私有变量 可以通过闭包在构造函数内部创建私有变量。 容易通过函数作用域创建私有变量。
继承 通过原型链 (Object.create()extends) 实现。 更灵活,可以通过闭包实现私有变量,但继承需要更复杂的模式。
代码风格 更接近传统面向对象语言。 更偏向函数式编程风格。

何时使用:

  • 构造器: 当你需要创建大量相似对象,并且希望这些对象共享方法以节省内存,同时希望利用原型继承机制时。这是实现类的标准方式。
  • 工厂函数: 当你不需要原型链、不需要使用 new 关键字,或者想要更灵活地返回不同类型的对象(例如,根据条件返回不同配置的对象)时。它也常用于需要封装私有变量但又不想使用 class 或旧式构造函数的场景。

5. ES6 class:现代构造器的语法糖

ES6 引入的 class 关键字为 JavaScript 提供了更简洁、更符合传统面向对象语言习惯的语法来定义构造器和原型方法。

重要的是:class 仅仅是现有构造函数和原型链机制的语法糖**。**

5.1 classconstructor 的关系

在一个 class 内部,constructor 方法就是这个类的构造函数。当你使用 new ClassName() 创建实例时,实际执行的就是 constructor 中的代码。

class NewPerson {
  constructor(name, age) { // 这就是构造函数
    this.name = name;
    this.age = age;
  }

  // 其他方法...
}

const person3 = new NewPerson('Eve', 28);
console.log(person3.name); // Eve
5.2 类方法与原型

class 中定义的所有方法(除了 static 方法和箭头函数作为实例属性)都会自动添加到类的 prototype 上。

class NewCar {
  constructor(make, model) {
    this.make = make;
    this.model = model;
  }

  // 这个方法会自动添加到 NewCar.prototype 上
  drive() {
    console.log(`${this.make} ${this.model} is driving.`);
  }
}

const carInstance = new NewCar('BMW', 'X5');
carInstance.drive(); // BMW X5 is driving.

// 验证方法是否在原型上
console.log(carInstance.drive === NewCar.prototype.drive); // true

使用 class 语法,我们不再需要手动操作 prototype,这使得代码更加清晰和易于维护,同时保留了基于原型的性能优势。


6. 对象构造器的优缺点

6.1 优点
  • 创建多个相似对象: 提供一个蓝图,高效创建具有相同属性和方法的多个实例。
  • 内存效率(通过原型): 将方法放在原型上,所有实例共享,节省内存。
  • 结构清晰: 对象实例的属性在构造函数中初始化,方法在原型上定义,结构一目了然。
  • 实现继承: 构造函数和原型是 JavaScript 实现面向对象继承的基础(无论是 ES5 模式还是 ES6 extends 语法糖)。
  • 类型识别: 通过 instanceof 运算符可以检查一个对象是否是某个构造函数的实例(myCar instanceof Car 返回 true)。
6.2 缺点
  • this 绑定复杂: this 关键字的动态性质,特别是在忘记 new 关键字时,容易导致错误。
  • 私有成员不直观(ES6 之前): 在 ES6 的私有字段 (#) 出现之前,实现真正的私有成员需要依赖闭包,增加了复杂性。
  • 继承语法冗长(ES6 之前):class 语法出现之前,实现复杂的原型链继承代码相对冗长和复杂。
  • 强制使用 new 开发者必须记住使用 new,否则会引发问题。虽然有约定俗成的命名规则,但语言层面没有强制检查。

7. 最佳实践

  • 始终使用 new 关键字: 确保构造函数正确地初始化新对象并绑定 this
  • 构造函数名首字母大写: 这是行业标准约定,用于区分普通函数和构造函数,提醒开发者使用 new
  • 将方法添加到 prototype 避免内存浪费,并确保所有实例共享方法。现代 JavaScript 中,使用 class 语法会自动处理这一点。
  • 优先使用 ES6 class 语法: 对于新的项目和代码库,class 提供了更清晰、更现代的语法来创建对象和实现继承,它将构造函数和原型方法更好地组织在一起。
  • 考虑使用工厂函数: 在某些情况下,例如需要返回不同类型的对象,或者不需要复杂的继承层次时,工厂函数可能是一个更简单、更灵活的选择。

8. 总结

JavaScript 对象构造器是其面向对象编程的核心基石。它允许我们通过定义一个函数作为蓝图,来创建多个具有相似属性和共享方法的对象实例。

本教程详细阐述了:

  • 构造器的定义和使用方式,强调 new 关键字的重要性。
  • this 在构造函数内部的行为。
  • 通过构造函数内部和原型来定义实例属性和共享方法,并强调了将方法置于原型上的内存和性能优势。
  • new 关键字在创建对象实例时的幕后工作原理。
  • 构造器与工厂函数的区别和各自适用场景。
  • ES6 class 作为构造器和原型模式的语法糖,提供了更简洁的现代 OOP 编程方式。
  • 对象构造器的优缺点,以及使用时的最佳实践。

掌握对象构造器是理解 JavaScript 对象模型和面向对象特性的关键一步,无论您是使用传统的构造函数模式还是现代的 class 语法,其底层原理都是相通的。


9. 附录:术语表

  • 对象构造器 (Object Constructor): 一个用于创建和初始化对象的特殊函数,用作对象的蓝图。
  • new 关键字: 用于调用构造函数并创建新的对象实例。
  • this 关键字: 在构造函数内部,this 指向当前正在创建的新对象实例。
  • 实例属性 (Instance Property): 每个对象实例独有的属性,通过 this 在构造函数内部定义。
  • 实例方法 (Instance Method): 每个对象实例独有的方法,如果定义在构造函数内部。
  • 原型 (Prototype): 每个 JavaScript 对象都链接到的另一个对象,允许对象继承属性和方法。
  • 原型链 (Prototype Chain): 对象通过 [[Prototype]] 链接起来的链条,用于实现继承和属性查找。
  • prototype 属性: 构造函数独有的属性,指向其原型对象,所有由该构造函数创建的实例都会继承这个原型对象上的属性和方法。
  • 工厂函数 (Factory Function): 一个普通函数,返回一个新的对象,无需使用 new 关键字。
  • class 关键字 (ES6): JavaScript 中用于定义类的语法糖,底层仍然是构造函数和原型继承。
  • constructor 方法: class 内部用于初始化实例的方法,相当于传统的构造函数。
  • 语法糖 (Syntactic Sugar): 一种编程语言的特性,它提供了一种更简洁、更易读的语法,但其底层功能与现有、更复杂的语法相同。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐