原型链(Prototype Chain)与原型继承:JS 面向对象的基石

原型链(Prototype Chain)与原型继承:JS 面向对象的基石 —— 且听老码农娓娓道来

各位观众老爷们,大家好!我是老码农,一个在代码的海洋里摸爬滚打了多年的老家伙。今天呢,咱们不聊那些高大上的框架,也不谈那些玄乎的算法,咱们就聊聊JavaScript里一个非常基础,但又至关重要的概念:原型链(Prototype Chain)和原型继承。

为啥说它重要呢?因为它是JavaScript面向对象编程的基石!没有它,JS的面向对象就像没地基的大厦,看着挺唬人,实则风一吹就倒。

别害怕,我保证用最通俗易懂的语言,最有趣的例子,把这个概念掰开了,揉碎了,喂到你嘴里,保证你消化得干干净净,以后再也不怕面试官问你“什么是原型链”了!

一、 什么是对象?为什么要面向对象?

在开始原型链之旅之前,咱们得先搞清楚什么是对象。在JS的世界里,几乎万物皆对象。你想想,一个按钮,一个文本框,甚至一个数字,都可以被看作一个对象。

对象是什么?简单来说,就是一堆属性(properties)方法(methods)的集合。

  • 属性: 描述对象的状态。比如,一个汽车对象,它的属性可能有颜色、品牌、型号、速度等等。
  • 方法: 描述对象的行为。比如,汽车对象的方法可能有启动、加速、刹车等等。

OK,现在你可能要问了,为什么要面向对象呢?面向对象编程 (OOP) 是一种编程范式,它的核心思想是把数据和操作数据的代码封装在一起,形成一个个独立的对象。

面向对象的好处可太多了,就像下面这张表格展示的:

特性 优点 例子
封装性 将数据和方法捆绑在一起,保护数据不被随意修改,提高了代码的安全性。 汽车对象,只能通过特定的方法启动、加速,不能直接修改速度属性。
继承性 可以基于已有的对象创建新的对象,并继承已有对象的属性和方法,减少代码重复,提高开发效率。 跑车可以继承汽车的启动、加速方法,同时增加自己独特的属性和方法,比如:涡轮增压。
多态性 允许不同类型的对象对同一消息做出不同的响应,提高了代码的灵活性和可扩展性。 汽车和自行车都有“行驶”这个方法,但汽车是烧油行驶,自行车是靠脚蹬行驶。
可维护性 代码结构清晰,模块化程度高,易于维护和修改。 如果需要修改汽车的加速方式,只需要修改汽车对象中的加速方法,而不需要修改其他地方的代码。
可复用性 对象可以被多次使用,减少代码冗余。 汽车对象可以被用于模拟驾驶游戏,也可以被用于车辆管理系统。

简单来说,面向对象就是让我们写出来的代码更清晰、更易于管理、更容易复用,就像搭积木一样,把一个个小的对象组装成一个复杂的系统。

二、 初识原型(Prototype):一切对象的起源

在JS的世界里,每个对象都有一个特殊的属性,叫做 __proto__。这个属性指向的是创建该对象的构造函数的原型对象(Prototype Object)。

啥是构造函数?啥是原型对象?别急,我们慢慢来。

构造函数: 你可以把构造函数想象成一个“模具”,它可以用来创建特定类型的对象。比如,我们可以用一个 Car 构造函数来创建多个汽车对象。

function Car(brand, color) {
  this.brand = brand;
  this.color = color;
  this.start = function() {
    console.log("汽车启动了! 🚗💨");
  }
}

// 使用 Car 构造函数创建两个汽车对象
const car1 = new Car("宝马", "黑色");
const car2 = new Car("奔驰", "白色");

car1.start(); // 输出:汽车启动了! 🚗💨
car2.start(); // 输出:汽车启动了! 🚗💨

在这个例子中,Car 就是一个构造函数,car1car2 都是 Car 的实例对象。

原型对象: 每个构造函数都有一个 prototype 属性,它指向的就是原型对象。原型对象是一个普通对象,它可以包含属性和方法,这些属性和方法可以被该构造函数创建的所有实例对象共享。

console.log(Car.prototype); // 输出:{constructor: ƒ}

Car.prototype.run = function() {
  console.log("汽车正在行驶! 🛣️");
}

car1.run(); // 输出:汽车正在行驶! 🛣️
car2.run(); // 输出:汽车正在行驶! 🛣️

在这个例子中,我们给 Car.prototype 添加了一个 run 方法。 这样,car1car2 都可以访问到这个 run 方法了。

重点: __proto__ 是实例对象指向其构造函数的原型对象的指针,而 prototype 是构造函数的一个属性,指向原型对象。

你可以把 prototype 想象成一个“共享仓库”,所有的实例对象都可以从这个仓库里拿东西用。

三、 深入原型链(Prototype Chain):寻根溯源之旅

现在,我们终于要进入正题了:原型链。

原型链就像一条“寻根溯源”的链条,它连接着对象和它的原型对象,以及原型对象的原型对象,一直到最顶层的 null

当你在一个对象上访问一个属性或方法时,JS引擎会按照以下步骤进行查找:

  1. 首先,在对象自身查找,如果找到了,就返回该属性或方法。
  2. 如果对象自身没有该属性或方法,就沿着 __proto__ 向上查找,到该对象的原型对象中查找。
  3. 如果原型对象中找到了,就返回该属性或方法。
  4. 如果原型对象中没有找到,就继续沿着原型对象的 __proto__ 向上查找,到原型对象的原型对象中查找。
  5. 这个过程会一直持续到找到该属性或方法,或者查找到原型链的顶端 null
  6. 如果最终都没有找到,就返回 undefined

用图来表示,就像这样:

对象 -> __proto__ -> 原型对象 -> __proto__ -> 原型对象的原型对象 -> ... -> null

举个例子:

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

Animal.prototype.sayHello = function() {
  console.log("大家好,我是 " + this.name);
}

function Dog(name, breed) {
  Animal.call(this, name); // 借用 Animal 的构造函数,继承 name 属性
  this.breed = breed;
}

// 设置 Dog 的原型对象为 Animal 的实例
Dog.prototype = new Animal();

// 修正 Dog 的 constructor 属性
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
  console.log("汪汪汪! 🐶");
}

const dog1 = new Dog("旺财", "中华田园犬");

console.log(dog1.name);    // 输出:旺财 (在 Dog 实例自身找到)
console.log(dog1.breed);   // 输出:中华田园犬 (在 Dog 实例自身找到)
dog1.bark();           // 输出:汪汪汪! 🐶 (在 Dog.prototype 找到)
dog1.sayHello();       // 输出:大家好,我是 旺财 (在 Animal.prototype 找到)
console.log(dog1.__proto__.__proto__.constructor); // 输出:function Animal(name) { this.name = name; } (Animal 构造函数)

在这个例子中,dog1 对象自身有 namebreed 属性,以及 bark 方法。 dog1 对象可以通过原型链找到 Animal.prototype 中的 sayHello 方法。

重点: 原型链就是一条查找属性和方法的链条,它连接着对象和它的原型对象,以及原型对象的原型对象,一直到 null

四、 原型继承(Prototypal Inheritance):传承衣钵的奥秘

原型继承是 JavaScript 实现继承机制的方式。它允许一个对象继承另一个对象的属性和方法。

在上面的例子中,Dog 继承了 Animalname 属性和 sayHello 方法,这就是原型继承。

实现原型继承的关键步骤:

  1. 借用构造函数: 使用 Animal.call(this, name) 来继承父类的属性。
  2. 设置原型对象: 使用 Dog.prototype = new Animal() 来让 Dog 的原型对象成为 Animal 的实例。
  3. 修正 constructor 属性: 使用 Dog.prototype.constructor = Dog 来修正 Dog 的原型对象的 constructor 属性。

为什么要修正 constructor 属性呢?因为 Dog.prototype = new Animal() 会覆盖 Dog.prototype 原来的 constructor 属性,导致 Dog.prototype.constructor 指向 Animal,而不是 Dog。 修正 constructor 属性可以确保 Dog.prototype.constructor 指向 Dog,这对于判断对象的类型非常重要。

五、 原型链的顶端:Object.prototype

原型链的顶端是 Object.prototype。所有对象都继承自 Object.prototype,包括数组、函数、甚至字符串。

Object.prototype 提供了一些通用的方法,比如 toString()valueOf()hasOwnProperty() 等等。

const obj = {};

console.log(obj.toString());        // 输出:[object Object] (继承自 Object.prototype)
console.log(obj.hasOwnProperty("name")); // 输出:false (继承自 Object.prototype)

六、 总结:画龙点睛

原型链和原型继承是 JavaScript 面向对象编程的核心概念。理解了原型链和原型继承,你就能更好地理解 JavaScript 的对象模型,写出更优雅、更高效的代码。

让我们用一个表格来总结一下今天的内容:

概念 解释 例子
对象 属性和方法的集合。 一个汽车对象,包含颜色、品牌、型号等属性,以及启动、加速、刹车等方法。
构造函数 用于创建特定类型对象的“模具”。 function Car(brand, color) { this.brand = brand; this.color = color; }
原型对象 每个构造函数都有一个 prototype 属性,它指向的就是原型对象。原型对象可以包含属性和方法,这些属性和方法可以被该构造函数创建的所有实例对象共享。 Car.prototype
__proto__ 实例对象指向其构造函数的原型对象的指针。 car1.__proto__ 指向 Car.prototype
原型链 一条查找属性和方法的链条,它连接着对象和它的原型对象,以及原型对象的原型对象,一直到 null 对象 -> __proto__ -> 原型对象 -> __proto__ -> 原型对象的原型对象 -> … -> null
原型继承 JavaScript 实现继承机制的方式。它允许一个对象继承另一个对象的属性和方法。 Dog 继承 Animal 的属性和方法。
Object.prototype 原型链的顶端,所有对象都继承自 Object.prototype obj.toString() (继承自 Object.prototype)

希望今天的讲解能让你对原型链和原型继承有一个更深入的理解。记住,理解这些基础概念是成为一名优秀的 JavaScript 工程师的关键。

下次面试再被问到“什么是原型链”,你就可以自信地告诉面试官:“原型链就是一条寻根溯源的链条,它连接着对象和它的原型对象,以及原型对象的原型对象,一直到 null!”

好了,今天的分享就到这里,感谢大家的观看! 咱们下期再见! 👋

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注