Typescript中的继承如何在幕后起作用?
Typescript和JavaScript都是令人印象深刻的语言。 Typescript对广泛的开发人员来说更加熟悉,因为它提供了我们通常在传统编程语言中遇到的概念。 继承就是其中之一。
但是TypeScript不会在浏览器中运行。 我们需要将其编译为JavaScript 。 主要是ES5,它不包含类或扩展关键字。
那么,这如何工作? Typescript使用语法糖来“模仿”类和继承行为。 它给这些概念带来一种错觉。 让我们看看我的意思。
在这篇博文中,我们将深入探讨。 在我们的路上,我们将遇到很多概念。 重要的是要把这些概念弄清楚。 花些时间,如有必要,请稍作休息。 您不必一口气就能理解所有内容。
让我们建立一个新的TypeScript项目,以说明幕后发生的事情。 要创建一个新的空的Typescript项目,我们运行以下命令:
tsc --init
这将为我们建立一个tsconfig.json
,TypeScript编译器将使用它来编译我们的代码。 当前,它什么也不会做,因为我们还没有编写任何代码。 让我们继续添加一个index.ts:
Foo
类充当Bar
派生的基类。 Bar
接受名称和喜欢的食物作为构造函数参数。 它通过对Foo
的超级调用传递名称,并将喜欢的食物参数分配给私有财产。
然后,我们实例化bar1
和bar2
并调用greet and talk。 Bar
之所以只能打招呼,是因为它扩展了Foo
。 这里没什么好看的。
上面的代码显示了我一开始所谈论的语法糖。 它允许我们以一种经典的方式来编写继承,而这种继承是从其他语言(如Java或C#)中使用的。
但是,这如何工作呢? 让我们运行tsc
命令看看“ unsugared”版本。
不要花太多时间来研究此代码-我们将在以后介绍。
哇! 那是很多额外的代码。 乍一看,我们可以看到所有的糖都消失了-不再有类别。 但是至少我们认识到我们之前写过的一些东西。
我们仍然可以看到Foo和Bar现在变成了“ iffe's”。 IFFE代表“立即调用的函数表达式” 。 顾名思义,它表示立即执行的功能😉
但是,这里添加了很多额外的东西。 __extends
方法的目的是什么?
为了理解此代码,我们首先退后一步,用普通的旧JavaScript编写相同的功能。 扎根! ❤️
原始JavaScript中的继承
此代码与Typescript版本相同。 即使代码看起来很简单,也有很多事情要理解。 重要的是我们要获得背后的思想观念。
为了使这些概念形象化,我们将使用其“您不知道JS”系列中使用的相同图形表示形式Kyle Simpson( Kyle )。
顺便说说。 如果您还没有阅读它们,我强烈建议您阅读。 他们都是伟大的!
凯尔(Kyle)在有关“此原型和对象原型”的书中用圆表示函数,用方形表示对象。 因此,让我们看看如何以这种图形表示形式表示上面的JS代码。
👀 在Twitter或媒体上关注我,以获取有关最新博客文章和有趣的前端内容的通知!
- 当第一行执行时,它将创建一个函数
Foo
。 注意,它还会创建一个对象,该对象是Foo
函数的原型。
function Foo(name) {
this .name = name;
}
函数Foo
包含.prototype
对象的.prototype
链接。 如链接所示,该对象充当Foo的原型。 但是.constructor
属性呢?
不要被它的名字弄糊涂了。 它与建筑无关。 这只是从原型到功能的内部联系。
2.让我们转到第5行。在这一行中,我们将greet函数添加到原型中。
Foo.prototype.greet = function () {
console.log(`Hi I am ${ this .name}`);
};
3.然后,第9行创建Bar
函数:
function Bar(name, favouriteFood) {
this .favouriteFood = favouriteFood;
Foo.call( this , name);
}
4.第14行是使事情变得有趣的地方。
Bar.prototype = Object.create(Foo.prototype);
自ES5以来, Object.create
是一种存在的方法。 它创建一个全新的对象并将其链接到我们作为参数传递的原型。 换句话说,我们创建了一个与Foo原型链接的全新对象。
然后,我们将该对象分配给Bar的原型。 结果如下图所示。
注意, Bar.prototype
和Foo.prototype
之间的链接由[[Prototype]]
。 [[Prototype]]
只是从一个对象到另一个对象的内部链接。
也许您已经听说过“原型链”? 原型链是bar实例可以调用greet函数的原因。 它允许对象将方法调用委派给其他对象。 [[Prototype]]
是此链的一部分。
因此,基本上,如果有人在bar上打招呼,则会检查Bar.prototype
是否存在Bar.prototype
。 如果不是,则遵循内部[[Prototype]]
链接,直到找到一个问候方法或链结束。 如果链结束,则返回未定义。
尽管[[Prototype]]
是一个内部链接,但也有一个称为__proto__
的公共链接。 这种链接是Mozilla发明的,但从未标准化。 但是,除IE之外,每个人都采用了它。 🤪
5.目前, bar
非常友好,它与Foo
关联,因此可以打招呼。 但是它仍然很害羞。 第16至18行将其更改。 Bar
现在将告诉您他最喜欢的食物是什么,这确实非常有用;)
Bar.prototype.talk = function () {
console.log(`${ this .name}: I love ${ this .favouriteFood}!`);
};
6.好。 凉。 我们创建了整个概念,但是当前代码没有执行任何操作。 因此,让我们创建对象并在其上调用一些方法。
var bar1 = new Bar('bar one');
var bar2 = new Bar('bar two');
现在图片已完成👨🎨(实际上还有更多内容在进行中,但这超出了本文的范围)
单击此处发布有关此文章的推文🐥
bar1
和bar2
也链接到talk
。 由于有了这些链接,我们现在有了正确的原型链,可以在bar1
和bar2
上greet
和talk
了。
现在,我们清楚地了解了原始JS代码的作用以及代码背后的心理概念。
很酷,但是Typescript代码呢?
是的,此博客文章的目标是深入研究Typescript继承。 我们将在一秒钟内这样做。 到目前为止,我们所做的事情只是更好地理解下一部分的必要步骤。 现在我们准备就绪,让我们开始潜水吧! 🐠
让我们从仔细看15到36行开始。我们的Typescript类消失了,几乎变成了我们之前编写的ES5代码。
Foo
和Bar
现在包装在IIFE(立即调用的函数表达式)中。 IIFE允许我们将基础对象捕获为变量。
如果将其与编写的普通JS代码进行比较,可以看到Object.create
方法已消失。 相反,我们现在看到__extends
方法,该方法在Bar
内部调用。 了解这种方法很重要,因为它负责“继承”魔术。 因此,让我们仔细看看。
__扩展
我们将__extends
方法分为两部分。 首先,我们看一下extendStatics
方法,然后转到返回的函数。
extendStatics
extendStatics
是一个带有两个参数d
和b
调用的函数。 d
表示派生类( Bar
), b
表示基类( Foo
)。
首先,检查Object.setPrototypeof
存在。 如果没有
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; })
将被应用。 如果两个检查都返回false,我们将应用第三个函数
function (d, b) { for ( var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
所有这些功能均实现相同。 由于向后兼容,因此必须进行检查。
如果不支持前两种方法,则使用最后一种方法。 它也是最易读和最好地解释了extendStatic
功能。
它检查基类上是否存在属性。 如果是这样,此属性将被复制到派生类。 通常说来,它将基类的静态成员复制到子类。
好。 这是容易的部分。 让我们看一看较难的部分。 我们做得到! 💪
重建原型继承
这部分非常棘手,可能需要阅读一些内容才能理解。
很明显,我们跳过第一行。 我们称上述的extendStatics
方法。
在第3行中,我们创建了一个名为__
的命名函数。 我们将很快讨论它的含义。 我认为理解此代码的最佳方法是首先查看最后一行。
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
多次仔细阅读此方法。 如果您不知道此方法的用途,请不要担心。 这行代码包含很多逻辑。
为了更好地解释此方法,我更愿意将此行重构为更易读的版本。 让我们重构吧!
if (b === null ) {
d.prototype = Object.create(b);
} else {
__.prototype = b.prototype;
d.prototype = new __();
}
好。 众所周知, b
等于Foo
, d
等于Bar
。 到我们输入该函数时, b
为Foo
并因此被定义,这将导致else部分的执行。
为了使事物可视化,我将再次使用功能的表示法作为圆,将对象的表示为正方形。 在我们输入函数时,已经定义了b
和d
。
接下来,我们执行以下行:
function __() { this.constructor = d }
这将导致另一个函数__
及其原型。 我们还获得了已经介绍的.constructor
和.prototype
链接。
然后,将调用重构代码的else语句内的第一行。
__.prototype = b.prototype;
我们将__.prototype
的链接更改为b.prototype
。
大! 差不多好了。 仅剩一行。 要了解最后一行,还需要一个先决条件。 我们需要了解new关键字对函数调用的影响。
JavaScript中的new关键字不同于传统语言中的new关键字。 在JavaScript中,构造函数只是使用新关键字调用的函数。
每个常规函数都可以使用new
关键字来调用。 当您使用new
关键字调用函数时,会发生四件事。
- 创建了一个全新的对象。
- 该对象链接了
[[Prototype]]
[[Prototype]]。 - 对象被设置为此绑定。
- 对象被返回。
在最后一行,我们用新的__
d.prototype = new __();
通过在__
上调用new,我们凭空创建了一个全新的对象。 然后,该对象链接到__
函数的原型,即Foo.prototpye
。 然后,我们将该对象分配给Bar.prototype
。
哇,看起来对ES5图表相当熟悉,不是吗? 但是谈话方法发生了什么? 不应该在Bar
的原型上吗?
它不在Bar
上的原因很简单。 尚未添加。
我们可以看到在执行__
extend方法之后添加了Bar.prototype
get。 最后,我们创建bar1
和bar2
。 完整的图如下所示:
您可能已经注意到,它与普通的ES5图相同。
结论
Typescript是一种出色的语言,与Javascript相比,它为我们提供了很多好处。 尽管如此,我已经看到太多使用TypeScript但不了解JavaScript的前端开发人员。
永远记住,在运行时执行的是JavaScript。 因此,我认为,对JavaScript具有扎实的理解至关重要。
🙏顺便说一句,如果您喜欢这篇文章,请单击左侧的👏🏻拍击👏🏻按钮。
拍手帮助其他人找到它并鼓励我写更多帖子😜
随时查看我的其他一些博客:
From: https://hackernoon.com/typescript-inheritance-deep-dive-9a53989af5a6