JavaScript 中的原型、__proto__ 和原型继承
这篇文章最初发表在我的网站上。
如果你接触过一段时间的 JavaScript,很可能已经见过或至少听说过原型(prototype)。如果你对原型及其作用还不了解,那么这篇文章正是为你准备的。在本文中,我将尝试帮助你理解 JavaScript 原型的工作原理,并解释原型继承的概念及其工作方式。
在开始之前,我希望您已经了解 JavaScript 中的一切都是高级对象。这意味着除了 null 和 undefined 之外,JavaScript 中的所有内容都派生自Object.
但是Varun,这怎么可能呢?JavaScript里有很多基本数据类型,比如String、Number、Boolean等等。一个数据类型,比如Number,怎么可能从Object派生出来呢?
原型和__proto__
要回答这个问题,我们首先需要了解什么是原型。JavaScript 中的原型其实就是对象所拥有的一组特殊属性(记住,JavaScript 中几乎所有东西都是派生自 `Prototype` Object)。每个对象都有其自身的一组prototype属性。让我们来看一个非常简单的例子来理解我的意思。打开浏览器开发者工具,边看边尝试以下代码片段。
var fooFunc = function() {
return {
foo: 42
}
};
fooFunc.prototype.bar = 'baz';
var fooVal = fooFunc();
console.log(fooVal); // {foo: 42}
console.log(fooFunc.prototype); // {bar: "baz", constructor: ƒ}
第二条打印语句完美地展示了原型继承的精髓。函数fooFunc继承自Object实例,并拥有自己的一组属性,以及它在实例{bar: baz}化时携带的所有属性。Object{constructor: ƒ}
所以如果
fooFunc是从 派生出来的Object,我可以向上追溯到Object的原型吗?
问得好,当然可以。不过你需要记住一点,除了 JavaScriptfunction类型之外,对象的所有其他原型都存在于其__proto__属性中。我来看看这是什么意思。
console.log('prototype of fooFunc:');
console.log(fooFunc.prototype); // {bar: "baz", constructor: ƒ}
console.log('prototype of Object:');
console.log(fooFunc.prototype.__proto__); // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
你看到我看到的了吗?最后一条控制台语句返回一个带有自身特殊属性的对象。这正是一个原型链Object。这证实了我们确实可以沿着原型链向上遍历,并且我们的函数fooFunc派生自该原型Object。
那么,当我尝试遍历原型链时会发生什么
Object?
让我们看看会发生什么:
console.log(fooFunc.prototype); // {bar: "baz", constructor: ƒ}
console.log(fooFunc.prototype.__proto__);// {constructor: ƒ, __defineSetter__: ƒ, …}
console.log(fooFunc.prototype.__proto__.__proto__); // null
你看,Object在 JavaScript 中,`<div>` 是顶层结构。如果你尝试查看 `<div> Object` 的父级持有哪些属性,你会得到 null,因为 `<div>` 没有父级Object。
此时,我希望你们回到开头,将到目前为止的所有内容与我之前在帖子中所说的内容联系起来。
JavaScript 中的原型只不过是对象所拥有的一组特殊属性。
原型继承
既然你已经了解了原型的工作原理,原型继承应该就很容易理解了。让我们来看下面的例子:
var obj = function(){
this.firstName = 'Varun';
this.lastName = 'Dey'
}
obj.prototype.age = 25;
var nameObj = new obj()
console.log(nameObj.age); // 25
让我们来分析一下这里发生了什么:
- 首先,我们定义一个函数
obj。 - 现在我们还
age直接在obj原型链上分配了另一个属性。 nameObj我们实例化一个名为from 的变量obj。nameObj它是一个对象,并为其附加了两个属性,分别是firstName和lastName。- 当我请求
newObj它的age属性时,它首先会进入它自己的对象并尝试查找。它age在nameObj对象中找到了吗?- 不。所以它会沿着链向上查找,
nameObj.__proto__并在该对象中查找age属性。 - 它
age在这里找到了一个属性,因为nameObj.__proto__它与……完全相同obj.prototype。
- 不。所以它会沿着链向上查找,
这就是 JavaScript 原型继承的精髓所在。每当 JavaScript 请求获取一个键时,它首先会检查自身对象的属性。如果找不到,它会向上追溯到自身的原型链,obj.__proto__尝试在这些属性中查找该键。如果仍然找不到,它会继续向上追溯到上一级原型链,obj.__proto__.__proto__重复上述步骤。如此反复,直到到达目标Object对象的原型链,如果仍然找不到,则返回 undefined。
原型污染
这使得 JavaScript 中的继承成为一个有趣的例子,它与其他基于类的语言(如 Java/C++)截然不同:
function parent(){
return{
foo: 42,
bar: 'baz'
}
}
child = new parent()
仔细观察,你会发现它child是 的一个实例化对象parent。而parent最终它只不过是 的一个实例化方法Object。这意味着child's' 和parent's 原型的原型是Object's 原型。
child.__proto__ === parent.prototype.__proto__ // true
现在我们来看另一个例子:
function parent(){
return{
foo: 42,
bar: 'baz'
}
}
parent.prototype.__proto__.baz = 'I should not belong here'
child = new parent()
console.log(child.__proto__)
这里你可以看到一个典型的原型污染的例子。我通过遍历函数的原型链,baz直接在`s` 的原型上创建了一个属性。现在,这个属性会被所有 `s` 的实例共享,这就是为什么如果你查看控制台输出,你会发现除了其他属性之外,现在还有 `s` 。这是一种糟糕的做法,因为它破坏了封装性,所以应该避免。ObjectbazObjectObjectbaz: "I should not belong here"
同样,我也可以这样做,JavaScript 允许我这样做:
function parent(){
return{
foo: 42,
bar: 'baz'
}
}
delete parent.prototype.constructor
child = new parent()
表现
不言而喻,随着原型链向上遍历,查找时间会增加,从而导致性能下降。当尝试访问整个原型链中不存在的属性时,这一点尤为重要。要检查所需的属性是否在对象本身中定义,可以使用以下方法hasOwnProperty:
child.hasOwnProperty('foo'); // true
parent.hasOwnProperty('baz'); // false
Object.prototype.hasOwnProperty('baz'); // true
完成循环
一开始我就说过,除了 null 和 undefined 之外,一切皆为Object实例化。让我们来证明这一点:
const foo = 42;
const bar = 'fooBar';
const baz = true;
foo.__proto__.__proto__ === bar.__proto__.__proto__; // true
bar.__proto__.__proto__ === baz.__proto__.__proto__; // true
所以你明白我的意思了吧。JavaScript 中的几乎所有东西都源自于Object
结论
原型是 JavaScript 的基础模块。我希望这篇文章能帮助你理解 JavaScript 中原型的工作原理。一旦你掌握了原型,就可以进一步了解thisJavaScript 中其他机制的工作原理。Mozilla 在这方面提供了非常优秀的资源,我建议你也阅读一下:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain
我很想知道这篇文章是否帮助你更好地理解了 JavaScript。:)
文章来源:https://dev.to/varundey/prototype-proto-and-prototypal-inheritance-in-javascript-2inl