banner
NEWS LETTER

为什么构造函数离不开原型?JS 对象世界藏着“共享密码”

Scroll down

理解 JavaScript 构造函数与原型:关系与最佳实践

在 JavaScript 中,构造函数和原型是面向对象编程的核心概念。它们共同定义了对象的行为和继承机制。本文将逐步解释两者的关系,并通过代码示例展示如何高效使用它们。理解这些概念能帮助您编写更高效、可维护的代码,尤其是在处理共享方法和内存优化时。


1. 构造函数:创建对象的蓝图

构造函数是用于创建对象实例的特殊函数。它以大写字母开头(约定),通过 new 操作符调用。当使用 new 时,JavaScript 会:

  • 在内存中创建一个新对象。
  • 将该对象的内部 [[Prototype]] 属性指向构造函数的 prototype 对象(后文详述)。
  • this 关键字绑定到这个新对象,执行构造函数内部的代码。
  • 返回这个新对象(除非构造函数显式返回其他值)。

示例代码:

1
2
3
4
5
6
7
8
9
function Dog(name) {
this.bark = bark; // 共享外部函数 bark
this.name = name; // 实例特有属性
}

const dog1 = new Dog("可乐");
const dog2 = new Dog("雪碧");
dog1.bark(); // 输出 "bark"
console.log(dog1.name); // 输出 "可乐"

在这个例子中:

  • Dog 是构造函数,定义了每个实例的初始属性(如 name)。
  • bark 是一个外部函数,被所有实例共享(节省内存)。
  • new Dog() 创建了独立的实例(dog1dog2),每个实例有自己的 name,但共享 bark 方法。

如果方法定义在构造函数内部,每次 new 都会创建新函数,浪费内存:

1
2
3
4
5
6
7
// 不推荐:每次 new 都创建新函数
function BadDog() {
this.bark = function() { console.log("汪汪汪"); };
}
const badDog1 = new BadDog();
const badDog2 = new BadDog();
console.log(badDog1.bark === badDog2.bark); // false,浪费内存

2. 原型:共享方法的机制

每个函数都有一个 prototype 属性(箭头函数除外),它指向一个对象。当使用 new 创建实例时,实例的内部 [[Prototype]](可通过非标准 __proto__ 访问)会指向这个 prototype 对象。这形成了原型链,允许实例共享方法和属性。
关系图如下所示:
关系图

关键点:

  • 为什么需要原型?
    将方法定义在 prototype 上,所有实例共享同一份函数,避免重复创建,显著节省内存。例如:

    1
    2
    3
    4
    5
    Dog.prototype.swim = function(type) {
    console.log(this.name, type, "swimming"); // this 指向当前实例
    };
    dog1.swim("狗刨"); // 输出 "可乐 狗刨 swimming"
    console.log(dog1.swim === dog2.swim); // true,方法共享
  • 原型链查找
    当访问实例的属性时:

    • 先在实例自身查找(如 name)。
    • 如果未找到,则通过 [[Prototype]] 向上查找构造函数的 prototype 对象。
    • 继续向上直到 null(原型链终点)。

    示例:

    1
    2
    3
    console.log(dog1.hasOwnProperty("name")); // true,实例自身属性
    console.log(dog1.hasOwnProperty("swim")); // false,来自原型
    console.log(dog1.__proto__ === Dog.prototype); // true(但避免使用 __proto__)
  • 标准替代方案
    __proto__ 是非标准属性,实际开发中建议使用 Object.getPrototypeOf() 获取原型:

    1
    console.log(Object.getPrototypeOf(dog1) === Dog.prototype); // true

3. 构造函数与原型的关系

构造函数和原型是协作的:

  • 构造函数:负责初始化实例特有属性(如 this.name)。
  • 原型:负责存储共享方法(如 swim),所有实例通过原型链访问。

关系图简化表示:

1
2
3
4
5
实例 (dog1) 
├── 自身属性: name = "可乐"
└── [[Prototype]] → Dog.prototype
├── swim 方法
└── [[Prototype]] → Object.prototype (内置)

在这个结构中:

  • 修改 Dog.prototype 会影响所有实例。
  • 实例可以重写原型方法(但不推荐,会破坏共享性)。

4. 最佳实践:如何高效使用

为了编写高效代码,遵循以下原则:

  • 将方法放在原型上
    避免在构造函数内部定义方法,除非方法需要实例特有状态。共享方法节省内存:

    1
    2
    3
    4
    5
    6
    7
    // 推荐:共享方法
    function Cat(name) {
    this.name = name;
    }
    Cat.prototype.meow = function() { console.log(this.name + " says meow"); };
    const cat1 = new Cat("咪咪");
    cat1.meow(); // 输出 "咪咪 says meow"
  • 使用原型实现继承
    原型链支持继承。例如,创建子类:

    1
    2
    3
    4
    5
    6
    7
    function Puppy(name) {
    Dog.call(this, name); // 调用父类构造函数
    }
    Puppy.prototype = Object.create(Dog.prototype); // 继承原型
    Puppy.prototype.constructor = Puppy; // 修复 constructor
    const puppy = new Puppy("小布");
    puppy.swim("自由泳"); // 继承自 Dog 的方法
  • 避免常见错误

    • 不要直接使用 __proto__:改用 Object.getPrototypeOf()Object.setPrototypeOf()

    • 谨慎修改内置原型:如 Array.prototype,可能导致兼容性问题。

    • 优先使用类语法(ES6):class 语法糖简化了构造函数和原型的操作:

      1
      2
      3
      4
      5
      6
      class Dog {
      constructor(name) {
      this.name = name;
      }
      bark() { console.log("bark"); } // 自动添加到原型
      }
  • 性能优化
    在创建大量实例时(如游戏或数据处理),原型共享方法能减少内存占用。测试共享 vs 非共享:

    1
    2
    3
    4
    5
    6
    // 测试内存
    const dogs = [];
    for (let i = 0; i < 1000; i++) {
    dogs.push(new Dog("Dog" + i));
    }
    // 所有实例共享 bark 和 swim,内存开销小

5. 完整代码示例

以下代码整合了用户示例,演示构造函数和原型的协作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 共享函数定义
function bark() {
console.log("bark");
}

// 构造函数:初始化实例属性
function Dog(name) {
this.bark = bark; // 共享 bark 方法
this.name = name; // 实例特有属性
}

// 原型:添加共享方法
Dog.prototype.swim = function(type) {
console.log(this.name, type, "swimming"); // this 指向调用实例
};

// 创建实例
const dog1 = new Dog("可乐");
const dog2 = new Dog("雪碧");

// 验证共享性
dog1.swim("狗刨"); // 输出 "可乐 狗刨 swimming"
console.log(dog1.bark === dog2.bark); // true,共享方法
console.log(Object.getPrototypeOf(dog1) === Dog.prototype); // true,原型链

// new 操作符内部行为模拟
function customNew(constructor, ...args) {
const obj = {}; // 创建新对象
Object.setPrototypeOf(obj, constructor.prototype); // 设置 [[Prototype]]
constructor.apply(obj, args); // 绑定 this 并执行构造函数
return obj;
}
const dog3 = customNew(Dog, "小黑");
dog3.bark(); // 输出 "bark"

结论

构造函数和原型是 JavaScript 对象系统的基石:

  • 构造函数初始化对象状态,定义实例特有属性。
  • 原型实现方法共享和继承,通过原型链高效管理内存。
  • 最佳实践:将方法置于原型上,优先使用 class 语法,避免直接操作 __proto__

掌握这些概念后,我们将可以构建更健壮的应用程序。例如,在创建大量相似对象时(如 UI 组件),原型共享能显著提升性能。记住,原型链是 JavaScript 灵活性的核心,合理使用能让代码既高效又易于扩展。

其他文章
目录导航 置顶
  1. 1. 理解 JavaScript 构造函数与原型:关系与最佳实践
    1. 1.1. 1. 构造函数:创建对象的蓝图
    2. 1.2. 2. 原型:共享方法的机制
    3. 1.3. 3. 构造函数与原型的关系
    4. 1.4. 4. 最佳实践:如何高效使用
    5. 1.5. 5. 完整代码示例
    6. 1.6. 结论