用五岁小孩都能听懂的方式解释 JavaScript 的原型链
最初发表于 codburst.io,标题为《通过了解 JavaScript 的原型链来通过前端面试》。
当你刚开始学习编程时,你可能接触过“面向对象编程”这个术语。你查阅了它的含义,发现它只不过是一个用来描述将数据分组为具有属性的“对象”的流行词。
在许多编程语言中,用于创建这些对象的关键字是类(class)。定义一个类时,需要包含构造函数以及若干公共和私有函数。如果想要一个类继承自另一个类,只需编写简单的继承语法,(瞧!)你就创建了一条继承链。
当然,如果你不是 JavaScript 开发者,这一切听起来都很棒。在 ES2015 之前,JavaScript 语言并没有实现类(class)的概念。它使用的是原型链(prototype chain),而且现在仍然如此。新的 ES6 “类”只不过是一种华丽的语法糖,它隐藏了原型链的内部运作机制。如果你想在使用 JavaScript 的面向对象编程(OOP)范式时开发高性能代码,那么理解原型链的工作原理至关重要。
对于熟悉(或不太熟悉)计算机科学的人来说,原型链就是一个链表。这是一种非常简化的描述,但保持简单是理解的关键。以下是 Mozilla 对原型链的定义:
在 JavaScript 中,继承只有一种结构:对象。每个对象都有一个私有属性,其中包含指向另一个对象的链接,称为其原型。该原型对象本身也有一个原型,依此类推,直到遇到原型为 null 的对象。根据定义,null 没有原型,并且是此原型链中的最后一个环节。
听起来很像一个列表,对吧?这个列表中的每个元素都包含一个名为原型(prototype)的对象字面量。
上面的图片有点误导性,因为原型链中的最后一个元素始终是 Object,所有实例(例如函数和数组)都派生自它。将原型链视为链表有什么帮助呢?
嗯,关于列表,我们需要了解一个重要的特性。当我们想要遍历一个列表时,必须从列表的头部开始。因此,用大O表示法来说,从原型链中检索一个元素需要O(n)的时间复杂度。你可以想象,如果原型链过长,将会面临怎样的性能问题。
我们如何初始化我们的链?
首先,我们需要创建一个构造函数。按照 ES5 之前的标准编写代码,没有“优雅”的方式来实现这一点。我们唯一能做的就是将构造函数的首字母大写,以此来区分它与其他函数。然后,我们使用 `new` 关键字来创建一个对象。
注意:目前使用 ES5 之前的示例是为了避免使用类语法。我们稍后会回到 ES5 的框架。
function Bat(name){
this.name = name;
}
let bob = new Bat('bob');
我们初始化的每个构造函数都会获得一个免费的原型对象作为其属性之一。我们可以给它命名一个键并为其赋值。
Bat.prototype.fly = function(){
console.log('Im flying. Weeee');
};
Bat.prototype.detect = function(){
console.log('I found a mouse!');
};
现在,您已经可以看到原型继承的优势。我们可以在不修改类定义的情况下向类中添加方法,让继承链来处理属性继承。以下是 Bat 对象一个实例中的继承链示例:
Bob{name: bob}=>Prototype{fly: [Function], detect: [Function]} => Object {} => null
现在,如果我们输入 bob.name,我们会得到 'bob'。如果我们输入bob.fly(),我们会得到‘Im flying. Weeee’。嗯。如何在fly()不调用 的情况下访问 呢bob.prototype.fly()?
其实这并非魔法。JavaScript 引擎首先会在对象本身中查找该属性。如果找不到,它会遍历第一个原型,然后是下一个……以此类推,直到找到目标属性或遇到 null 值为止。
综合起来
我们可以利用原型链来实现类继承。这种面向对象编程方法也称为子类化。
function Mammal(){
this.bloodTemp = 'warm';
}
function Carnivore(){
}
function Lion(name){
Mammal.call(this); //super. Inherit constructor
this.name = name;
}
我们将创建一个超类和两个子类。食肉动物类应继承自哺乳动物类,而狮子类应同时继承自食肉动物类和哺乳动物类。
Mammal.prototype.growHair = function(){
console.log('my hair is growing');
}
Carnivore.prototype = Object.create(Mammal.prototype);
Carnivore.prototype.eatMeat = function(){
console.log('Mmm.Meat');
};
Lion.prototype = Object.create(Carnivore.prototype);
Lion.prototype.pride = function(){
console.log('im king of the jungle');
};
我们又开始使用 ES5 了。`__init__`Object.create()将原型转换为一个独立的对象字面量,然后我们可以将其赋值为另一个对象的原型。这意味着我们在继承时会忽略父类的构造函数。
注意:这就是为什么我们在 Lion 的构造函数中调用了 `__init__` Mammal.call(this),以便我们可以借用 Mammal 的构造函数。
了解了原型链的工作原理后,您就能明白从链式调用方法到链式调用对象是多么容易。
以下是预期输出:
var charlie = new Lion(‘charlie’)
charlie.growHair() // my hair is growing
charlie.eatMeat() // Mmm.Meat
charlie.pride() //im king of the jungle
charlie.bloodTemp // warm
注意:要达到同样的效果,你也可以使用 mixin Object.assign()。
//classical inheritance
Lion.prototype = Object.create(Mammal.prototype);
//This is a mixin
Object.assign(Lion.prototype, Carnivore.prototype);
结论
与简洁的类实现相比,使用原型链创建方法和继承可能显得繁琐。然而,你会逐渐体会到 JavaScript 语言的动态性和模块化特性。需要注意的是,不要过度依赖原型继承。想想看:`charlie.growHair()` 函数需要沿着继承链向上传递很长一段距离才能执行。较短的继承链意味着更好的性能。
作者:
Raji Ayinla | https://dev.to/realrayinla | rajiayinla858@gmail.com


