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 调用时负责创建实例
实例对象 p1p2 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.prototypenew 时把实例的隐式原型连到构造函数的 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__。所有函数(普通函数、构造函数、ObjectFunction 本身)都是 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__ 依据
pnew 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
  • ObjectFunction、所有普通函数都是 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.prototypeObject.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.prototypeArray.prototype 等内置原型。
  • 现代代码做继承优先使用 class extends
相关推荐
paopaokaka_luck1 小时前
非遗文物数字化系统(AI非遗问答、ONNX图像识别、协同过滤推荐、ECharts数据分析、非遗知识浏览与互动、文创商城订单闭环、文化活动报名签到、社区交流)
前端·javascript·vue.js·人工智能·数据分析·echarts
mONESY2 小时前
让大模型「听话」输出 JSON:LangChain OutputParser 结构化输出实战,从手写正则到 Zod Schema 四层进化
javascript
研☆香2 小时前
js中使用的正则表达式
开发语言·javascript·正则表达式
开开心心就好2 小时前
免费桌签打印工具,支持批量导入名字
前端·javascript·人工智能·docker·jupyter·智能手机·语音识别
CappuccinoRose2 小时前
Web Components 基础
前端·javascript·交互·web component·shadow dom
AgentMaster3 小时前
元数据、血缘、质量、安全四大模块能力拆解,数据治理方案对比:4 种技术路线深度评测
大数据·数据库·数据仓库·人工智能·原型模式
imgsq3 小时前
MapLibre GL JS v6 正式发布:v5 之后八个月的第一个大版本(编译)
开发语言·javascript·ecmascript
lytao1233 小时前
90% 覆盖率不等于没 Bug:用风险配置测试组合
前端·javascript·bug·软件工程
mayaairi3 小时前
Vue2 组件通讯(一):Props传值完全指南
前端·javascript·vue.js