🧬 深入理解 JavaScript 原型链:从对象创建到继承机制的全景透视

63 阅读4分钟

🚀 前言

JavaScript 中的对象与类系统建立在 原型链(Prototype Chain) 上。所有对象的属性查找和继承机制都离不开原型链的支持。虽然这个概念在大多数教程中被简单地理解为“对象继承”的基础,但在实际应用中,原型链的背后隐藏着复杂的内存管理、执行上下文和性能优化问题。

理解原型链不仅仅是理解“继承”——它是整个 JavaScript 对象机制、作用域链和闭包等重要概念的基础。本文将深入探讨原型链的工作原理,从基础定义到复杂的继承体系,再到实际性能优化。


1️⃣ 原型链的定义与本质:从对象创建到查找过程

在 JavaScript 中,每一个对象都有一个内部属性 [[Prototype]],它指向该对象的原型对象。这个原型对象也可能有它自己的原型,形成一条链式结构。当你访问一个对象的属性时,JavaScript 引擎会沿着这个原型链逐层查找,直到找到该属性或达到链的末尾(null)。这个过程被称为 原型链查找

创建一个对象的原型链

const obj = { a: 1 };
console.log(obj.toString);  // [Function: toString] 来自 Object.prototype
  • obj 没有 toString 方法,JavaScript 会沿着原型链查找到 Object.prototype,并使用该方法。
  • 每个对象都至少有一个原型对象,如果没有显式指定,它会继承自 Object.prototype

对象原型链查找示意

  • obj 会查找它自己的属性。
  • 如果找不到,它会去 obj.__proto__(即 Object.prototype)查找。
  • 如果还找不到,它会继续沿着更高层的原型查找,直到 null

2️⃣ __proto__prototype:从原型链中跳出

__proto__prototype 的关系

__proto__ 是每个对象实例的内部属性,指向该对象的原型,而 prototype 是构造函数的属性,用于创建该类实例时为实例对象指定的原型。

名称类型用途
__proto__实例对象的属性指向构造函数的 prototype
prototype构造函数的属性每个函数对象的原型对象,供实例继承
function Person() {}
const p = new Person();

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

3️⃣ new 操作符底层机制:原型链的起点

理解 new 操作符的底层机制是深入原型链的关键。调用 new 时,实际上是在做以下几件事:

  1. 创建一个空对象 obj
  2. 设置 obj.__proto__ 为构造函数的 prototype
  3. 执行构造函数 fn,将 obj 作为 this 绑定。
  4. 如果构造函数没有返回对象,则返回 obj
function Animal(name) {
  this.name = name;
}
Animal.prototype.say = function () {
  console.log(`I am ${this.name}`);
};

const a = new Animal('Dog');
a.say();  // I am Dog

手动实现 new 操作符

function myNew(fn, ...args) {
  const obj = Object.create(fn.prototype);  // 建立原型链
  const result = fn.apply(obj, args);
  return result instanceof Object ? result : obj;  // 如果构造函数返回对象,则返回该对象
}

Object.create(fn.prototype) 这一操作是建立原型链的关键所在。通过 Object.create() 创建的新对象,其原型会指向构造函数的 prototype


4️⃣ 原型链与继承:多层继承与经典模式

原型链实现继承

JavaScript 的继承是通过原型链来实现的。构造函数的原型对象会被实例对象共享。因此,可以通过设置构造函数的 prototype 来实现继承。

function Parent() {
  this.name = 'parent';
}
Parent.prototype.say = function () {
  console.log('parent');
};

function Child() {
  Parent.call(this);  // 继承 Parent 的属性
}
Child.prototype = Object.create(Parent.prototype);  // 继承 Parent 的方法
Child.prototype.constructor = Child;  // 修正 constructor 指向

const child = new Child();
child.say();  // parent
  • 关键:Child.prototype = Object.create(Parent.prototype) 创建了继承链。

ES6 class 和原型链

ES6 引入了 class 语法糖,但底层原理依然基于原型链:

class Parent {
  constructor() {
    this.name = 'parent';
  }
  say() {
    console.log('parent');
  }
}

class Child extends Parent {
  constructor() {
    super();
    this.name = 'child';
  }
}

const child = new Child();
child.say();  // parent
  • class 实际上是基于原型链的包装语法。
  • extendssuper 关键字用于设置原型链和调用父类方法。

5️⃣ 性能影响与优化

原型链查找的性能

虽然 JavaScript 引擎优化了原型链的查找过程,但原型链过长时,性能仍然会受到影响,尤其是在深度继承或频繁属性查找的场景中。

hasOwnPropertyin 操作符

  • hasOwnProperty() 用于判断一个属性是否是对象自身的属性,而不是继承来的。
  • in 操作符会查找整个原型链,直到找到该属性。
obj.hasOwnProperty('a');  // 判断是否是 obj 自有属性
'a' in obj;               // 判断是否存在于原型链上

优化策略:

  • 尽量避免使用过长的原型链,合理划分继承结构。
  • 使用 hasOwnProperty 判断属性是否来自对象本身。

✅ 总结

  • 原型链 是 JavaScript 中对象机制的基础,所有对象的属性查找和继承都离不开它。
  • __proto__prototype 分别指代实例和构造函数的原型,理解两者的区别是理解原型链的第一步。
  • new 操作符 底层实现了原型链的建立,掌握其机制能够更好地理解构造函数和继承的实现。
  • 原型链继承 是 JavaScript 中实现继承的基础,通过修改构造函数的 prototype 来创建多层继承。
  • 在实际应用中,需要考虑原型链的性能问题,避免不必要的原型链查找。

通过掌握原型链的工作机制,你将能够更好地理解 JavaScript 中的对象创建、继承和性能优化技巧,提升编写高效、可维护代码的能力。