人人都会AI编程

4.4 构造函数、实例、原型对象的三角关系

更新时间:2026-07-11

在 JavaScript 中,构造函数、实例对象和原型对象三者互相引用,形成一条完整的关系链。理解这个“铁三角”,是真正掌握原型与继承的起点。

一张图看清三角关系

先看一个最简单的例子:

function Person(name) {
  this.name = name;
}

Person.prototype.sayHi = function() {
  console.log('Hi, I am ' + this.name);
};

const p1 = new Person('李四');

执行完以上代码后,内存中存在三个关键对象:

  • 构造函数 Person
  • 实例对象 p1
  • 原型对象 Person.prototype

它们之间的引用关系如下:

         构造函数 Person
         /            \
   .prototype         new 调用
       /                  \
原型对象 Person.prototype ←—— 实例对象 p1
      ↑                      |
      |__p1.__proto__ ______|
      
同时:Person.prototype.constructor === Person

用代码验证这些关系:

// 构造函数 -> 原型对象
console.log(Person.prototype);              // {sayHi: f, constructor: f}
console.log(Object.getOwnPropertyNames(Person.prototype)); // ['constructor', 'sayHi']

// 原型对象 -> 构造函数
console.log(Person.prototype.constructor === Person); // true

// 实例对象 -> 原型对象
console.log(p1.__proto__ === Person.prototype);       // true

// 实例对象 -> 构造函数(间接)
console.log(p1.__proto__.constructor === Person);     // true

每个角色的职责

构造函数(Constructor)

构造函数本质上就是一个普通函数,只是通过 new 运算符调用。它的职责有三:

  1. 创建新的空对象。
  2. 将该对象的 [[Prototype]](即 proto)指向构造函数的 prototype 属性所引用的对象。
  3. 将该对象作为 this 的指向,执行函数体内的代码(通常用来初始化实例属性)。
  4. (隐式)返回这个新对象。

构造函数上的 prototype 属性 是一个对象,它会被自动赋予一个 constructor 属性,指向构造函数自身。这个 prototype 对象就是所有实例共享的“原型池”。

实例对象(Instance)

通过 new 构造函数() 创建的每个实例都是独立的对象。实例上存放的是实例自己的属性和方法(比如 p1.name)。这些属性在不同的实例之间互不影响。实例并不复制原型对象上的属性,而是通过内部的 proto 指针动态查找。

原型对象(Prototype)

原型对象是实例和构造函数之间的“共享空间”。定义在原型对象上的属性和方法,可以被所有实例访问到(因为实例的 proto 指向它)。这正是 JavaScript 实现方法共享的方式,而不需要每个实例都存一份方法代码。

三角关系带来的核心行为

1. 属性查找:自己→原型→更上层原型

访问 p1.name 时,引擎先在 p1 自身上查找,找到了直接返回。访问 p1.sayHi 时,p1 自身上没有这个方法,引擎会沿着 proto 找到 Person.prototype,在上面找到 sayHi 方法并执行。

如果 Person.prototype 上也没有,就会继续向上查找 Person.prototype.proto(也就是 Object.prototype),直到 null。这就是原型链查找的全过程。

2. 原型上的属性是所有实例共享的

这是难点也是重点:

Person.prototype.type = '人类';
console.log(p1.type); // '人类'

const p2 = new Person('王五');
console.log(p2.type); // '人类'

// 通过实例修改共享属性会发什么?
p1.type = '兽人';               // 不是修改原型,而是给 p1 添加了自身属性
console.log(p1.type);           // '兽人' (来自自身)
console.log(p2.type);           // '人类' (来自原型)
delete p1.type;                 // 删除自身属性
console.log(p1.type);           // '人类' (重新走原型链查找)

读取操作会沿着原型链向上搜索,但赋值操作默认只影响实例自身,不会改动原型。除非显式地通过 Person.prototype.type = xxx 去修改原型对象。

3. constructor 的意义

Person.prototype.constructor 指向 Person,通常用来从实例回溯到构造函数。例如在继承场景中,子类的原型需要重置 constructor 指向自身,以保证正确的引用关系。但需要注意 constructor 是一个普通属性,可以被篡改,因此不是 100% 可靠的类型判断手段。

用验证代码加深理解

不妨在浏览器控制台亲手跑一下这些语句,观察输出:

function Dog(breed) {
  this.breed = breed;
}
Dog.prototype.bark = function() { console.log('Woof!'); };

const d = new Dog('Golden');

// 三角关系验证
console.log(d.constructor === Dog);              // true(因为 d.__proto__.constructor === Dog)
console.log(Dog.prototype.isPrototypeOf(d));     // true(原型检测)
console.log(Object.getPrototypeOf(d) === Dog.prototype); // true(ES6推荐的获取方式)

// 遍历自身属性
console.log(d.hasOwnProperty('breed'));          // true
console.log(d.hasOwnProperty('bark'));           // false(bark 在原型上)

// 所有实例共享原型方法
const d2 = new Dog('Husky');
console.log(d.bark === d2.bark);                 // true(同一个函数引用)

三角关系的实用价值

理解这一条关系链之后,以下问题自然有了答案:

  • 为什么在 Array.prototype 上新增一个方法,所有数组实例都能使用?—— 因为所有数组实例的 proto 都指向 Array.prototype
  • 为什么方法要定义在原型上,而数据定义在实例内?—— 原型方法被共享,节省内存;实例数据各自独立,互不影响。
  • 在使用 for...in 遍历对象时,为什么有时会冒出预期之外的属性?—— 因为它会沿着原型链遍历所有的可枚举属性(需要配合 hasOwnProperty 过滤)。

当你遇到任何涉及原型的问题时,冷静地画出“构造函数 → prototype → 原型对象 ← proto ← 实例”这条三角链,答案就会清晰呈现。