探秘JavaScript原型链:对象继承的奇妙之旅

30 阅读3分钟

欢迎使用我的小程序👇👇👇👇

small.png


想象一下,你有一个工具库,每次需要新工具时,不是从零开始制造,而是基于现有工具改进——这就是JavaScript原型继承的核心思想。

从简单对象说起

在JavaScript中,万物皆对象。当我们创建一个对象时,它并不是孤立存在的:

// 创建一个简单的对象
let person = {
  name: '小明',
  age: 25,
  greet() {
    console.log(`你好,我是${this.name}`);
  }
};

person.greet(); // 你好,我是小明

但如果有多个相似的对象,每个都这样创建会非常低效。这时候构造函数就派上用场了。

构造函数与原型初探

// 构造函数(首字母大写是约定)
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 通过构造函数的prototype属性添加共享方法
Person.prototype.greet = function() {
  console.log(`你好,我是${this.name}`);
};

// 创建实例
let person1 = new Person('小明', 25);
let person2 = new Person('小红', 23);

person1.greet(); // 你好,我是小明
person2.greet(); // 你好,我是小红

console.log(person1.greet === person2.greet); // true - 同一个方法!

神奇的事情发生了:两个实例共享同一个greet方法!这是如何实现的?

原型对象:共享的工具箱

每个JavaScript函数都有一个特殊的prototype属性(箭头函数除外)。当我们使用new关键字调用函数时:

  1. 创建一个新对象
  2. 将这个对象的__proto__指向构造函数的prototype
  3. this绑定到这个新对象
  4. 执行构造函数
  5. 返回这个新对象
// 查看原型关系
console.log(person1.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true

// 实例的原型链
console.log(person1.__proto__); // Person.prototype
console.log(person1.__proto__.__proto__); // Object.prototype
console.log(person1.__proto__.__proto__.__proto__); // null

原型链:逐级查找的机制

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

  1. 先在对象自身查找
  2. 如果找不到,通过__proto__到原型对象中查找
  3. 继续沿原型链向上查找,直到找到或到达null
// 原型链查找示例
person1.greet(); // 1. person1自身没有greet方法
                 // 2. 去person1.__proto__(Person.prototype)找
                 // 3. 找到了!执行

// 添加自身属性会覆盖原型属性
person1.greet = function() {
  console.log(`嗨,我是${this.name}`);
};

person1.greet(); // 嗨,我是小明 (使用自身方法)
person2.greet(); // 你好,我是小红 (仍使用原型方法)

// 删除自身属性后,又会使用原型的方法
delete person1.greet;
person1.greet(); // 你好,我是小明

完整的原型链图示

让我们通过一个更复杂的例子理解完整的原型链:

function Animal(name) {
  this.name = name;
}

Animal.prototype.eat = function() {
  console.log(`${this.name}在吃东西`);
};

function Dog(name, breed) {
  Animal.call(this, name); // 调用父类构造函数
  this.breed = breed;
}

// 关键步骤:建立原型链
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复constructor指向

Dog.prototype.bark = function() {
  console.log(`${this.name}在汪汪叫`);
};

let myDog = new Dog('旺财', '金毛');

// 完整的原型链查找
myDog.bark(); // 1. myDog自身查找 → 2. Dog.prototype找到
myDog.eat();  // 1. myDog自身没有 → 2. Dog.prototype没有 → 
              // 3. Animal.prototype找到

console.log(myDog instanceof Dog);    // true
console.log(myDog instanceof Animal); // true
console.log(myDog instanceof Object); // true

现代JavaScript中的原型继承

ES6引入了class语法糖,让原型继承更加直观:

class Animal {
  constructor(name) {
    this.name = name;
  }
  
  eat() {
    console.log(`${this.name}在吃东西`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 调用父类构造函数
    this.breed = breed;
  }
  
  bark() {
    console.log(`${this.name}在汪汪叫`);
  }
}

// 底层仍然是原型继承
console.log(Dog.prototype.__proto__ === Animal.prototype); // true

实用技巧与注意事项

  1. 检查原型关系
// 检查对象是否是构造函数的实例
console.log(myDog instanceof Dog); // true

// 检查属性是在自身还是原型上
console.log(myDog.hasOwnProperty('name')); // true
console.log(myDog.hasOwnProperty('eat'));  // false

// 获取对象的原型
console.log(Object.getPrototypeOf(myDog) === Dog.prototype); // true
  1. 避免常见的原型陷阱
// 错误:直接修改内置对象的原型
Array.prototype.customMethod = function() { /* ... */ }; // 不推荐!

// 正确:通过继承扩展功能
class MyArray extends Array {
  customMethod() { /* ... */ }
}

// 性能注意:原型链过长会影响查找速度
  1. 实现安全的原型继承
// 使用Object.create实现纯净的原型链
function createObject(proto) {
  function F() {}
  F.prototype = proto;
  return new F();
}

// ES5标准方式
let child = Object.create(parent);

总结

JavaScript的原型继承机制就像一条工具传递链:

  • 每个对象都有一个隐式的__proto__链接指向它的原型
  • 每个函数都有一个显式的prototype属性,用于实例共享方法
  • 查找属性时沿着原型链向上,形成了一种优雅的继承机制

理解原型链不仅有助于编写更好的JavaScript代码,还能帮助调试、理解第三方库,以及真正掌握JavaScript这门语言的核心。

记住:在JavaScript的世界里,对象不是孤岛,它们通过原型链相互连接,共享智慧与能力。