原型链核心原理:prototype 与 proto 的闭环与查找规则

0 阅读6分钟

深入理解 JavaScript 原型链

1. 为什么需要原型链?

在 JavaScript 中,我们需要一种机制让对象之间共享方法或属性。如果每个实例都复制一份方法,内存会被大量浪费。原型链机制让对象可以“借用”其构造函数的 prototype 上的属性与方法,实现内存共享与行为继承。

简单来说:原型链就是 JavaScript 对象之间“自己找不到就找长辈”的查找规则。

2. 核心三角:prototype、proto、constructor

每一个构造函数(用 function 定义或 class 声明的函数)都拥有一个 prototype 属性,指向一个对象,这个对象就是该构造函数所创建实例的原型。原型对象内会有一个 constructor 属性指回构造函数。而每一个对象(实例)都拥有一个 __proto__ 属性指向其构造函数的 prototype。这三者构成了一个闭环。

属性归属指向作用
prototype函数(构造函数)原型对象存放共享属性和方法
__proto__所有对象(包括函数)该对象的原型构成查找链
constructor原型对象关联的构造函数标识对象的构造来源

速记函数有 prototype,对象有 proto,原型有 constructor

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

const p = new Person('Alice');

// 验证闭环
p.__proto__ === Person.prototype;       // true
Person.prototype.constructor === Person; // true

如图:

image.png

2.1 谁有 prototype?——不是所有函数都有

很多人以为“函数都有 prototype”,其实只有可以作为构造函数调用的函数,JavaScript 引擎才会为它自动创建 prototype 对象

有 prototype 的函数

// 函数声明
function foo() {}
console.log(foo.hasOwnProperty('prototype')); // true

// 匿名函数表达式
const bar = function() {};
console.log(bar.hasOwnProperty('prototype')); // true

// class(本质是函数)
class Person {}
console.log(Person.hasOwnProperty('prototype')); // true

// 内置构造函数
console.log(Object.hasOwnProperty('prototype')); // true
console.log(Array.hasOwnProperty('prototype'));  // true

这些函数的 prototype 都是引擎自动创建的普通对象,内部结构如下:

{
  constructor: 该函数,
  [[Prototype]]: Object.prototype
}

没有 prototype 的函数

函数类型示例为何没有
箭头函数() => {}不能作为构造函数,无 [[Construct]] 内部方法
方法简写{ method() {} }同上,纯粹的执行逻辑
bind 返回的函数fn.bind(null)本质仍是原函数,不具备独立构造能力

验证:

const arrow = () => {};
new arrow(); // TypeError: arrow is not a constructor

记忆口诀new 就有 prototype,不能 new 就没有。

2.2 prototype 与 proto 的方向相反

站在不同主体的视角看,prototype__proto__ 恰好是反方向的:

主体属性指向比喻
构造函数prototype它的原型对象向下(父亲为孩子准备的模板)
实例 / 对象__proto__它的原型向上(孩子去查父亲的模板)

一句话总结prototype 是构造函数“向下分发”的入口,__proto__ 是实例“向上查找”的路径。前者是模板的提供方,后者是模板的使用方。原型链正是靠这一下一上的配合,才实现了共享与继承。

3. 原型链查找机制

当访问对象的属性或方法时:

  1. 先在自身属性中找。
  2. 找不到,则沿着 __proto__ 向上一级原型对象中找。
  3. 若仍未找到,继续沿 __proto__ 向上,直到 Object.prototype
  4. Object.prototype.__proto__null,查找结束,返回 undefined

这就是原型链的实质:一条由 __proto__ 串联起来的对象查找路径。

p.toString(); // p → Person.prototype → Object.prototype 找到
p.abc;        // p → Person.prototype → Object.prototype → null 返回 undefined

4. 内置构造函数与原型链全景

JavaScript 中,所有对象最终都继承自 Object,函数也继承自 Function,它们共同编织起完整的原型网络。

构造类型实例举例原型链路径
自定义构造函数new Person()实例 → Person.prototypeObject.prototypenull
数组[1, 2, 3]实例 → Array.prototypeObject.prototypenull
函数function foo(){}实例 → Function.prototypeObject.prototypenull
普通对象{a:1}实例 → Object.prototypenull

全景图(含实例):

4.1 特例:Function 的自引用——既是鸡又是蛋

在 JavaScript 中,Function 构造函数是所有函数的“母亲”,但 Function 自己也是一个函数。这就产生了一个奇特的闭环:

typeof Function;                            // "function"
Function.__proto__ === Function.prototype;   // true —— 它就是自己的实例!

这意味着 Function.prototype 既是所有函数的原型,也是 Function 自身的原型。在整个原型链图谱中,这是一个自引用环

这个环为什么成立?

  • 向下(prototype)Function 作为构造函数,为所有函数实例提供共享方法(callapplybind 等),所以它有一个 prototype
  • 向上(protoFunction 自身也是函数,需要沿着原型链去获取自己的方法,所以它的 __proto__ 只能指向 Function.prototype
普通构造函数(如 PersonFunction 构造函数
Person.__proto__ === Function.prototype(因为它是一个函数)Function.__proto__ === Function.prototype(它也是函数,且只能指回自己)
Person.prototype 是一个普通对象,与 Person 不是同一个引用Function.prototype 也是 Function 的原型,两者形成闭环

速记:普通函数是 Function 的实例;Function 自己是自己的实例。

理解了这个自引用,你就能明白为什么所有函数都能用 callapply,因为它们最终都通过 __proto__ 找到了 Function.prototype,包括 Function 自己。

5. 原型链与继承(ES5 方案演进)

JavaScript 早期没有 class,只能通过原型链实现继承。不同方案不断弥补前一代的缺陷。

继承方式核心思路典型缺陷
原型链继承Child.prototype = new Parent()父类引用值被所有子实例共享
构造函数继承在 Child 内执行 Parent.call(this)父类原型上的方法无法继承
组合继承结合上面两种方式父类构造函数被执行两次
寄生组合继承Child.prototype = Object.create(Parent.prototype)写法略复杂(但最优)

寄生组合继承(最优 ES5 方案)示例

function Parent(name) { this.name = name; }
Parent.prototype.getName = function() { return this.name; };

function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}
Child.prototype = Object.create(Parent.prototype); // 取用父原型副本
Child.prototype.constructor = Child;                // 修正 constructor

6. ES6 class 与原型链

class 本质上仍然是原型链的语法糖。定义在 class 内部的方法会被挂载到 类.prototype 上,实例通过 __proto__ 调用。

class Person {
  constructor(name) { this.name = name; }
  sayHi() {} // 等价于 Person.prototype.sayHi
}
class Student extends Person {
  constructor(name, grade) {
    super(name);
    this.grade = grade;
  }
}

const s = new Student('Bob', 3);
s.__proto__ === Student.prototype;              // true
Student.prototype.__proto__ === Person.prototype; // true(extends 设置)

extends 背后就做了两件事:

  • Student.prototype.__proto__ 指向 Person.prototype(原型继承)。
  • Student.__proto__ 指向 Person(构造函数的静态继承)。

整套机制与寄生组合继承完全一致,只是语法更直观。

7. 常见误区与速记

误区真相
“所有对象都有 prototype”普通函数才有 prototype,对象没有(但都有 __proto__
__proto__ 可以随意使用”__proto__ 是非标准访问器,应优先使用 Object.getPrototypeOf()
instanceof 检查自身构造”instanceof 是沿着原型链查找,可能误判(如跨 iframe)
Object.create(null) 和普通对象一样”用 null 做原型的对象没有 __proto__,无任何内置方法

8. 总结

原型链是 JavaScript 对象之间共享数据与行为的根本机制。它通过 __proto__ 指针建立从实例、构造函数的 prototype、再到 Object.prototype 的层层查找路径。早年的继承方案为弥补原型链的缺陷不断演进,最终沉淀为 ES6 class 的底层实现。理解原型链,你就掌握了对象关系、继承原理,以及 class 真正在做的是什么。