JavaScript 原型链与原型对象详解

JavaScript 原型链与原型对象详解

一、为什么需要原型

假设用构造函数创建多个对象。如果给每个对象都准备一份自己的方法,会产生重复的内存开销:

js 复制代码
// 方法定义在构造函数内部:每个实例都复制一份新函数
function Person(name) {
  this.name = name;
  this.sayHello = function () {
    console.log(`你好,我是 ${this.name}`);
  };
}

const p1 = new Person('小明');
const p2 = new Person('小红');

console.log(p1.sayHello === p2.sayHello);   // false,每个实例的方法都是独立的函数

两个实例的 sayHello 逻辑完全相同,却没有共享,白白占用内存。

原型(prototype)要解决的就是这个问题:把需要共享的属性与方法放到一个公共对象上,让所有实例通过原型链引用同一个对象,而不是各自复制一份。原型机制还构成了 JavaScript 实现继承的基础。

二、构造函数、实例、原型对象

以 Person 为例,先认识三个互相联系的对象:

角色 指代 说明
构造函数 Person 一个函数,用 new 调用时负责创建实例
实例对象 p1、p2 new Person() 得到的结果
原型对象 Person.prototype Person 上名为 prototype 的属性所指向的对象

2.1 prototype:显式原型

只有函数才有 prototype 属性 (箭头函数没有,也不能用作构造函数)。每个函数都自带一个 prototype 属性,它指向一个普通对象:

js 复制代码
function Person(name) {
  this.name = name;
}

// 把共享方法挂到原型对象上
Person.prototype.sayHello = function () {
  console.log(`你好,我是 ${this.name}`);
};

console.log(typeof Person.prototype);   // "object"

2.2 proto:隐式原型

任何对象都有 __proto__ 属性 ,它指向该对象的原型对象。这个属性对应的是对象内部的原型槽位 [[Prototype]]。__proto__ 是历史上浏览器实现的访问器,标准写法推荐用 Object.getPrototypeOf() 读取。

js 复制代码
function Person(name) {
  this.name = name;
}

const p1 = new Person('小明');

console.log(p1.__proto__ === Person.prototype);   // true
console.log(Object.getPrototypeOf(p1) === Person.prototype);   // true

2.3 构造函数创建实例时发生了什么

用 new 调用一个函数时,引擎大致执行以下四步:

  1. 创建一个新的空对象。
  2. 把这个新对象的 __proto__ 指向构造函数的 prototype(即建立原型关联)。
  3. 把构造函数体内的 this 绑定到新对象并执行构造函数。
  4. 若构造函数显式返回一个对象,则返回该对象;否则返回第 1 步创建的对象。

因此 new Person() 得到的实例,其隐式原型自动等于 Person.prototype。

2.4 三者关系

三者之间需要记住的关系只有两条:

  • Person.prototype.constructor === Person:原型对象上的 constructor 属性指回构造函数。

  • p1.__proto__ === Person.prototype:new 时把实例的隐式原型连到构造函数的 prototype。

    三、属性查找规则与原型链

    3.1 查找规则

    访问 对象.属性 时,JavaScript 的执行顺序是:

    1. 先查找对象自身的属性,找到就返回。
    2. 找不到,就沿 __proto__ 去原型对象上找。
    3. 还找不到,就继续沿上一级的 __proto__ 往上找。
    4. 直到 Object.prototype,它的 __proto__ 是 null,再往上没有对象,返回 undefined。
    js 复制代码
    function Person(name) {
      this.name = name;            // name 是实例自身属性
    }
    Person.prototype.sayHello = function () {
      console.log(`你好,我是 ${this.name}`);
    };
    
    const p1 = new Person('小明');
    console.log(p1.name);          // "小明",来自自身属性
    p1.sayHello();                 // "你好,我是 小明",自身没有,沿原型链找到
    
    // p1 自身没有 toString,但 Object.prototype 上有,所以仍能调用
    console.log(p1.toString());    // "[object Object]"

一条不断通过 __proto__ 向上串联的链路称为原型链。p1 的原型链为:

text 复制代码
p1 ──► Person.prototype ──► Object.prototype ──► null

Person.prototype 本身是一个普通对象,它的 __proto__ 指向 Object.prototype;而 Object.prototype 是原型链的顶端,其 __proto__ 为 null,查找到此终止。

3.2 hasOwnProperty 与 in

  • hasOwnProperty():只检查属性是否存在于对象自身。
  • in:检查属性是否能被访问到,包括沿原型链找到的继承属性。
js 复制代码
function Person(name) {
  this.name = name;
}
Person.prototype.sayHello = function () {};

const p1 = new Person('小明');

console.log(p1.hasOwnProperty('name'));      // true,自身属性
console.log(p1.hasOwnProperty('sayHello'));  // false,继承自原型
console.log('sayHello' in p1);               // true,可以访问到

3.3 instanceof

obj instanceof Constructor 的原理是:沿着 obj 的原型链查找,看 Constructor 的 prototype 是否出现在这条链上。只要出现就返回 true。

js 复制代码
function Person() {}
const p1 = new Person();

console.log(p1 instanceof Person);    // true,Person.prototype 在 p1 的原型链上
console.log(p1 instanceof Object);    // true,Object.prototype 也在链上

3.4 数组等内置对象的原型链

数组方法没有定义在数组实例上,而是定义在 Array.prototype 上,通过原型链共享:

js 复制代码
const arr = [1, 2, 3];
console.log(arr.hasOwnProperty('map'));       // false
console.log('map' in arr);                    // true

// arr 的原型链
// arr ──► Array.prototype ──► Object.prototype ──► null

四、函数的原型链:函数也是对象

函数既可以作为构造器,本身也是对象,因此函数也有 __proto__。所有函数(普通函数、构造函数、Object、Function 本身)都是 Function 的实例,它们的 __proto__ 都指向 Function.prototype。

js 复制代码
function Person() {}
const p1 = new Person();

// 普通函数的隐式原型是 Function.prototype
console.log(Person.__proto__ === Function.prototype);   // true
// 内置构造函数 Object 本身也是函数,同样指向 Function.prototype
console.log(Object.__proto__ === Function.prototype);   // true

// 唯一的特殊情况:Function 的隐式原型也是 Function.prototype
console.log(Function.__proto__ === Function.prototype); // true

这里要注意区分两个对象:

  • Person.prototype:由 new Person() 生成的实例的原型,存共享方法。
  • Person.__proto__:指向 Function.prototype,因为它本身是一个函数对象。

它们不是同一个东西:

js 复制代码
console.log(Person.__proto__ === Person.prototype);   // false

Function.prototype 本身是一个可调用的函数对象,它的隐式原型再指向 Object.prototype:

js 复制代码
console.log(Function.prototype.__proto__ === Object.prototype);   // true

以上各对象的 __proto__ 指向可以列表核对:

节点 该节点的 __proto__ 依据
p(new Person() 的实例) Person.prototype 由 new 建立
Person.prototype Object.prototype 原型对象是普通对象,其原型是 Object.prototype
Person Function.prototype 函数都是 Function 的实例
Object Function.prototype 同上,Object 本身是函数
Function Function.prototype 全局唯一闭环,可作结论记忆
Function.prototype Object.prototype Function.prototype 本身也是一个函数对象
Object.prototype null 原型链顶端

要点归纳:

  • Object.prototype 位于绝大多数原型链的顶端,它的 __proto__ 是 null。
  • Object、Function、所有普通函数都是 Function 的实例,因此它们的 __proto__ 都是 Function.prototype。
  • Function.__proto__ === Function.prototype,这是全局关系中的唯一闭环,可作为结论记住,不必深究成因。

五、获取与设置对象原型

5.1 读取原型

__proto__ 在代码中可以直接访问,但它不是规范推荐的正式 API。读取原型应使用 Object.getPrototypeOf():

js 复制代码
function Person() {}
const p1 = new Person();

const proto1 = p1.__proto__;                        // 不推荐但可读
const proto2 = Object.getPrototypeOf(p1);           // 标准写法
console.log(proto1 === Person.prototype);           // true
console.log(proto2 === Person.prototype);           // true

5.2 改变已有对象的原型

使用 Object.setPrototypeOf() 可以修改一个对象的原型。该操作会触发引擎重排继承关系,性能开销较大,正式代码中应尽量少用:

js 复制代码
const a = { x: 1 };
const b = { y: 2 };

console.log(b.x);   // undefined,b 的原型上没有 x

Object.setPrototypeOf(b, a);
console.log(b.x);   // 1,通过原型链从 a 上找到
console.log(Object.getPrototypeOf(b) === a);   // true

5.3 Object.create:创建一个指定原型的对象

Object.create(proto) 创建一个新对象,并把它的原型直接设为 proto。这是创建"继承自某个对象"的对象最直接的方式:

js 复制代码
const personProto = {
  sayHello() {
    console.log(`你好,我是 ${this.name}`);
  },
};

// 创建的原型为 personProto,自身再添加 name
const p1 = Object.create(personProto);
p1.name = '小明';

p1.sayHello();   // "你好,我是 小明"
console.log(Object.getPrototypeOf(p1) === personProto);   // true

Object.create(null) 可以创建没有任何原型 的对象,它无法访问 toString 等方法:

js 复制代码
const empty = Object.create(null);
console.log(empty.toString);   // undefined,Object.prototype 不在其原型链上

六、用原型实现继承

6.1 原型链继承

把子类构造函数的 prototype 替换为父类的一个实例,子类的实例就能通过链式查找访问父类原型上的方法:

js 复制代码
function Animal(name) {
  this.name = name;
}
Animal.prototype.say = function () {
  console.log(`${this.name} 发出叫声`);
};

function Dog(name) {
  this.name = name;
}
// 让 Dog.prototype 成为一个 Animal 实例,从而继承 Animal.prototype
Dog.prototype = new Animal('占位');

const d = new Dog('旺财');
d.say();   // "旺财 发出叫声",沿原型链找到 Animal.prototype.say

纯原型链继承的缺点:父类的实例属性会被所有子类实例共享,属性写在同一处会互相影响,所以实际中通常配合下面的"借用构造函数"使用。

6.2 组合继承:借用构造函数 + 原型

先用 call 在子类构造函数里执行父类构造函数,让每个子实例有各自的实例属性;再把子类的原型指向父类原型的副本,继承方法:

js 复制代码
function Animal(name) {
  this.name = name;
}
Animal.prototype.say = function () {
  console.log(`${this.name} 发出叫声`);
};

function Dog(name, breed) {
  Animal.call(this, name);   // 借用父构造函数,name 成为每个实例的自身属性
  this.breed = breed;
}

// 把子类原型替换为父类原型的副本,替换后 constructor 会丢失,需要补回
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;   // 补回 constructor 指向(见易错点 7.2)

const d = new Dog('旺财', '金毛');
console.log(d.name);                  // "旺财",自身属性
d.say();                              // "旺财 发出叫声",继承的方法
console.log(d instanceof Dog);        // true
console.log(d instanceof Animal);     // true
console.log(Dog.prototype.constructor === Dog);   // true

七、常见易错点

7.1 混淆 proto 与 prototype

  • __proto__ 存在于所有对象,指"我这个对象从哪里继承"。
  • prototype 只存在于函数,指"用我 new 出来的实例从哪里继承"。

Person.__proto__(函数作为对象的原型)与 Person.prototype(作为构造器时为实例准备的原型)是两个不同的东西,见第四节。

7.2 重写 prototype 后 constructor 丢失

constructor 是原型对象上的属性,本身不是自动维护的。用新对象整体替换 prototype 后,新对象上没有 constructor,此时沿原型链会找到 Object.prototype,导致 constructor 指向 Object:

js 复制代码
function Person() {}
const p = new Person();

console.log(p.constructor === Person);   // true

// 用新对象整体替换 prototype,新对象没有 constructor 属性
Person.prototype = {
  sayHello() {},
};

console.log(p.constructor === Person);   // false,指向了 Object
console.log(p.constructor === Object);   // true

解决办法是替换后手动补回:

js 复制代码
Person.prototype.sayHello = function () {};
// 或者
Person.prototype = {
  constructor: Person,
  sayHello() {},
};

注意:替换 prototype 只影响之后 new 出来的实例。上面代码中旧实例 p 的隐式原型仍是旧的 Person.prototype 对象。

7.3 重写 prototype 之后才创建的实例才受新原型影响

js 复制代码
function Person() {}
const p1 = new Person();

Person.prototype.sayHi = function () { console.log('hi'); };
console.log(p1.sayHi);   // 函数存在,原型对象被修改,实例立刻能访问到

Person.prototype = { other() {} };
const p2 = new Person();

console.log(p1.other);   // undefined,p1 的原型仍是旧的 Person.prototype
console.log(p2.other);   // 函数存在,p2 使用新的 prototype

给已有原型追加 方法,对所有实例即时生效;整体替换原型,只影响替换之后创建的实例。

7.4 不要修改内置对象的原型

往 Array.prototype、Object.prototype 上挂自定义方法是许多旧代码的常见写法,会污染全局,可能与第三方库冲突,也可能破坏依赖枚举的代码。除非做 Polyfill 且有规范约束,否则应避免。

7.5 for...in 会遍历继承来的可枚举属性

js 复制代码
const proto = { inherited: 1 };
const obj = Object.create(proto);
obj.own = 2;

for (const key in obj) {
  console.log(key);   // "own"、"inherited",继承属性也会被遍历
}

// 只想要自身属性时用 hasOwnProperty 过滤
for (const key in obj) {
  if (obj.hasOwnProperty(key)) {
    console.log(key);   // 只输出 "own"
  }
}

八、总结

概念 说明
prototype 函数才有的显式属性,指向该函数作为构造函数时给实例准备的原型对象
__proto__ 所有对象都有的隐式原型,指向它的原型对象;标准读取用 Object.getPrototypeOf()
constructor 原型对象上的属性,默认指回该函数;整体替换 prototype 后需要手动补回
Object.getPrototypeOf / setPrototypeOf 读取 / 修改一个对象的原型
Object.create(proto) 创建指定原型的对象;Object.create(null) 创建无原型对象
原型链 通过 __proto__ 逐级上溯的链路,终点是 null
属性查找 先查自身,再沿原型链向上找,hasOwnProperty 只判断自身
instanceof 判断右侧构造函数的 prototype 是否在左侧对象的原型链上
class extends 语法糖,底层仍通过原型链实现继承

使用建议:

  • 共享方法挂到 prototype 上,实例状态放在构造函数内部,不要给每个实例复制方法。
  • 读取原型用 Object.getPrototypeOf(),不要依赖 __proto__。
  • 整体替换 prototype 后记得补回 constructor,并注意它只影响替换之后创建的实例。
  • 不要修改 Object.prototype、Array.prototype 等内置原型。
  • 现代代码做继承优先使用 class extends。
相关推荐
Mr_Mao30 分钟前
告别选型困难!集 VueUse、ahooks、Mantine 于一身:286 个 Hook 的 ReaUse 来了
前端·javascript·react.js
默_笙2 小时前
🚙 一条腿走路会摔:ES 关键词检索、混合召回与 ReRank 裁判
前端·javascript
dsyyyyy11012 小时前
Vue 3 Watch 监视完全指南
前端·javascript·vue.js
u0111026754 小时前
Vue 3 实现图片裁剪框:拖动、缩放与固定宽高比
前端·javascript·vue.js
0nirvana05 小时前
从普通 script 到 ESM:一文搞懂 JavaScript 模块、IIFE 和 window
javascript
wshzd6 小时前
LLM之Agent(104)|DeepSeek-Harness(十三)ReactLoopAgent 总览:kick → turn → step
开发语言·前端·javascript
天天喝旺仔7 小时前
浏览器渲染原理:从解析 HTML/CSS、构建渲染树到重排重绘与首屏优化
前端·javascript·css·性能优化·html
Days20508 小时前
时间胶囊项目正式发布
javascript·gpt·php
朱 欢 庆8 小时前
el-table-v2 虚拟表格组件
前端·javascript·vue.js
OpsEye9 小时前
企业大模型用量成本该如何监控和管控?
javascript·ai编程