在这里插入图片描述

🤍 前端开发工程师、技术日更博主、已过CET6
🍨 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1
🕠 牛客高级专题作者、打造专栏《前端面试必备》《2024面试高频手撕题》《前端求职突破计划》
🍚 蓝桥云课签约作者、上架课程《Vue.js 和 Egg.js 开发企业级健康管理项目》《带你从入门到实战全面掌握 uni-app》

一、原型链的核心概念:从对象到蓝图的映射

在 JavaScript 中,原型(Prototype)是理解对象继承的核心机制。每个对象(除了 null)都关联着一个原型对象,该原型对象又可能指向另一个原型,形成一条层层嵌套的链条,即原型链(Prototype Chain)。原型链的本质是 JavaScript 实现**基于原型的继承(Prototype-based Inheritance)**的底层结构,它使得对象可以继承另一个对象的属性和方法。

二、原型链的构建基石:三个关键角色

(一)对象(Object)与 [[Prototype]] 内部属性

  • 所有对象的隐式连接:每个对象在创建时,都会自动拥有一个内部属性 [[Prototype]](非标准环境中称为 __proto__),该属性指向其原型对象。
    const obj = { key: 'value' };
    console.log(obj.__proto__ === Object.prototype); // true
    
    • 浏览器环境中可通过 __proto__ 访问,但更推荐使用标准方法 Object.getPrototypeOf()

(二)函数的 prototype 属性

  • 构造函数的显式原型:函数对象(如 FunctionObject、自定义构造函数)拥有一个 prototype 属性,该属性是一个对象,默认包含 constructor 属性(指向函数自身)。
    function Person(name) {
      this.name = name;
    }
    console.log(Person.prototype.constructor === Person); // true
    
    • 当使用 new Person() 创建实例时,实例的 [[Prototype]] 会指向 Person.prototype

(三)原型对象的终极指向:Object.prototype

  • 原型链的顶端:所有内置对象的原型链最终都会指向 Object.prototype,它是原型链的根节点。Object.prototype[[Prototype]]null,标志着原型链的终止。
    console.log(Object.prototype.__proto__); // null
    

三、原型链的工作原理:属性访问与继承

(一)属性查找机制

当访问对象的属性或方法时,JavaScript 会按照以下顺序在原型链中查找:

  1. 当前对象自身属性:首先检查对象本身是否拥有该属性(包括自有属性和可枚举属性)。
  2. 原型对象:若当前对象没有该属性,沿 [[Prototype]] 链向上查找原型对象。
  3. 递归查找直至 null:若沿途所有原型对象都不存在该属性,则返回 undefined

示例:原型链属性查找

// 定义构造函数
function Animal() {
  this.type = 'animal';
}
Animal.prototype.speak = function() {
  return 'sound';
};

function Dog(name) {
  Animal.call(this); // 继承自有属性
  this.name = name;
}
// 建立原型链:Dog.prototype.__proto__ = Animal.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复构造函数引用

// 创建实例
const husky = new Dog('Husky');

// 属性查找路径:
// 1. husky 自身:type(animal)、name(Husky)
// 2. Dog.prototype:constructor(Dog)
// 3. Animal.prototype:speak(函数)
// 4. Object.prototype:toString()、hasOwnProperty() 等
console.log(husky.speak()); // 输出 "sound"(从 Animal.prototype 继承)

(二)属性修改与原型链的关系

  • 自有属性优先:若对象自身已存在某属性,会直接覆盖原型链中的同名属性。
    husky.speak = function() {
      return 'woof!';
    };
    console.log(husky.speak()); // 输出 "woof!"(使用自有方法)
    
  • 原型对象的修改会影响所有实例:若修改原型对象的属性,所有继承该原型的实例都会受到影响。
    Animal.prototype.speak = function() {
      return 'new sound';
    };
    console.log(husky.speak()); // 输出 "new sound"(原型链已更新)
    

四、原型链的应用场景:实现继承与扩展

(一)简单继承模式

通过原型链实现类的继承,是 JavaScript 中最经典的继承方式。
步骤解析

  1. 定义父类:封装通用属性和方法(如 Animaltypespeak)。
  2. 定义子类:通过 Object.create() 建立子类原型与父类原型的连接。
  3. 构造函数绑定:在子类构造函数中调用 父类.call(this),继承父类的自有属性。

ES5 继承示例

function Shape(color) {
  this.color = color;
}
Shape.prototype.getArea = function() { /* 通用逻辑 */ };

function Circle(radius, color) {
  Shape.call(this, color); // 继承自有属性
  this.radius = radius;
}
Circle.prototype = Object.create(Shape.prototype); // 继承原型方法
Circle.prototype.constructor = Circle;

const redCircle = new Circle(5, 'red');
console.log(redCircle.color); // "red"(继承自 Shape)

(二)扩展内置对象

利用原型链为内置对象(如 ArrayString)添加自定义方法,但需谨慎避免污染原生对象。

// 为数组添加去重方法
Array.prototype.unique = function() {
  return [...new Set(this)];
};
const arr = [1, 2, 2, 3];
console.log(arr.unique()); // [1, 2, 3]

五、原型链的局限性与替代方案

(一)原型链的常见问题

  1. 引用类型属性共享:原型对象的引用类型属性会被所有实例共享,可能导致意外修改。
    function Car() {}
    Car.prototype.parts = ['engine', 'wheel'];
    const car1 = new Car();
    const car2 = new Car();
    car1.parts.push('seat');
    console.log(car2.parts); // 输出 ["engine", "wheel", "seat"](共享问题)
    
  2. 子类构造函数无法直接访问父类原型方法:需通过 super 关键字(ES6 语法)或显式调用父类方法。

(二)ES6 类语法:语法糖下的原型链

ES6 的 classextends 关键字本质上仍是基于原型链实现继承,只是语法更简洁且避免了部分坑点:

class Animal {
  constructor(type) {
    this.type = type;
  }
  speak() {
    return 'sound';
  }
}

class Dog extends Animal {
  constructor(name, type) {
    super(type); // 必须先调用 super()
    this.name = name;
  }
}

const husky = new Dog('Husky', 'dog');
console.log(husky.speak()); // "sound"(原型链继承)
  • super 关键字用于访问父类原型方法,内部处理了原型链的绑定。

(三)替代继承模式

  • 对象关联继承(Object Composition):通过组合对象而非继承实现代码复用,避免原型链的复杂性。
    const animal = {
      type: 'animal',
      speak() { return 'sound' }
    };
    const dog = {
      ...animal,
      name: 'Husky',
      speak() { return 'woof!' }
    };
    
  • ES6 Object.setPrototypeOf():显式设置对象的原型,但性能低于构造函数继承。

六、原型链的性能与最佳实践

  1. 避免深层原型链:过长的原型链会导致属性查找变慢,建议继承层级不超过3层。
  2. 优先使用对象组合:对于复杂逻辑,组合模式比继承更灵活且性能更优。
  3. 谨慎扩展内置原型:避免覆盖原生方法(如 Array.prototype.push),防止全局作用域污染。
  4. 使用 hasOwnProperty 判断自有属性:在遍历对象属性时,通过 obj.hasOwnProperty(key) 避免访问原型链属性。
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        // 处理自有属性
      }
    }
    

七、总结:原型链的本质与延伸

原型链是 JavaScript 区别于传统类式语言(如 Java、C#)的核心特性,其本质是通过对象间的隐式连接实现动态继承。理解原型链不仅能掌握 thiscall/apply/bind 等关键机制,还能深入理解 JavaScript 的“弱类型”“动态性”设计哲学。尽管 ES6 引入了类语法,但原型链仍是 JavaScript 引擎实现继承的底层逻辑。在实际开发中,需根据场景选择合适的继承模式,平衡代码简洁性与性能,避免滥用原型链导致的维护难题。

更多推荐