原型链(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
就是一个构造函数,car1
和 car2
都是 Car
的实例对象。
原型对象: 每个构造函数都有一个 prototype
属性,它指向的就是原型对象。原型对象是一个普通对象,它可以包含属性和方法,这些属性和方法可以被该构造函数创建的所有实例对象共享。
console.log(Car.prototype); // 输出:{constructor: ƒ}
Car.prototype.run = function() {
console.log("汽车正在行驶! 🛣️");
}
car1.run(); // 输出:汽车正在行驶! 🛣️
car2.run(); // 输出:汽车正在行驶! 🛣️
在这个例子中,我们给 Car.prototype
添加了一个 run
方法。 这样,car1
和 car2
都可以访问到这个 run
方法了。
重点: __proto__
是实例对象指向其构造函数的原型对象的指针,而 prototype
是构造函数的一个属性,指向原型对象。
你可以把 prototype
想象成一个“共享仓库”,所有的实例对象都可以从这个仓库里拿东西用。
三、 深入原型链(Prototype Chain):寻根溯源之旅
现在,我们终于要进入正题了:原型链。
原型链就像一条“寻根溯源”的链条,它连接着对象和它的原型对象,以及原型对象的原型对象,一直到最顶层的 null
。
当你在一个对象上访问一个属性或方法时,JS引擎会按照以下步骤进行查找:
- 首先,在对象自身查找,如果找到了,就返回该属性或方法。
- 如果对象自身没有该属性或方法,就沿着
__proto__
向上查找,到该对象的原型对象中查找。 - 如果原型对象中找到了,就返回该属性或方法。
- 如果原型对象中没有找到,就继续沿着原型对象的
__proto__
向上查找,到原型对象的原型对象中查找。 - 这个过程会一直持续到找到该属性或方法,或者查找到原型链的顶端
null
。 - 如果最终都没有找到,就返回
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
对象自身有 name
和 breed
属性,以及 bark
方法。 dog1
对象可以通过原型链找到 Animal.prototype
中的 sayHello
方法。
重点: 原型链就是一条查找属性和方法的链条,它连接着对象和它的原型对象,以及原型对象的原型对象,一直到 null
。
四、 原型继承(Prototypal Inheritance):传承衣钵的奥秘
原型继承是 JavaScript 实现继承机制的方式。它允许一个对象继承另一个对象的属性和方法。
在上面的例子中,Dog
继承了 Animal
的 name
属性和 sayHello
方法,这就是原型继承。
实现原型继承的关键步骤:
- 借用构造函数: 使用
Animal.call(this, name)
来继承父类的属性。 - 设置原型对象: 使用
Dog.prototype = new Animal()
来让Dog
的原型对象成为Animal
的实例。 - 修正 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!”
好了,今天的分享就到这里,感谢大家的观看! 咱们下期再见! 👋