jTypes:底层原理





5.00/5 (2投票s)
深入分析 jTypes 如何将 C#-like 对象定义编译为基于原型的实例矩阵。
引言
本文将深入探讨 jTypes,以及它如何将 C#-like 对象定义编译为基于原型的实例矩阵。这些矩阵允许 jTypes 通过在实例化期间为每个访问级别构建多个原型链来模拟经典继承的行为。这使得开发人员能够使用封装、继承和多态性来构建 JavaScript 中的应用程序库。
通过观察 jTypes 的内部机制并了解其工作原理,开发人员可以更好地理解 jTypes 的用法、它能提供的优势以及可能带来的障碍。如果使用得当,jTypes 可以为大型 JavaScript 应用程序库提供一个强大、有组织且易于维护的代码库。
背景
jTypes 是最全面、最强大的 JavaScript 库,用于克服基于原型的对象的差异化继承。其轻量级而强大的设计为任何平台或浏览器的 Web 程序员提供了模拟经典继承的能力,其中对象由类定义。
更多信息,请访问 www.jTypes.com ,以及源代码、文档和示例。本文将使用最新实验版本(撰写本文时为 2.1.0b)进行调试日志记录到 Google Chrome 开发者工具的控制台。只有在 jTypes 的实验版本中才提供将编译后的定义对象和实例矩阵记录到控制台的功能。
使用代码
jTypes 将 C#-like 对象定义转换为一组编译后的定义对象。然后在实例化过程中使用这些编译后的定义作为蓝图,在公共原型链之上构建实例矩阵。每个类定义在实例化期间都需要在实例矩阵中占有一行,并为对象实例中的每个可能的访问级别创建列。私有实例通过原型链继承自其各自的受保护实例,而矩阵中的所有其他实例则继承自基类中对应的访问级别实例,如下图所示:
为了直观了解编译后的定义对象和实例矩阵,我们将使用 jTypes 下载中提供的两个示例类,可从 www.jtypes.com 下载。首先,将使用以下代码定义一个 Person
类:
var Person = $$(function($fName, $lName, $age)
{
this.firstName = $fName;
this.lastName = $lName;
this._age = $age;
},
{
'public readonly firstName': '',
'public readonly lastName': '',
'protected _age': 0,
'public getFullName': function()
{
return this.firstName + ' ' + this.lastName;
},
'public virtual triggerOneYearOlder': function()
{
this._age++;
},
'public age':
{
'get': function()
{
return this._age;
},
'set': function($v)
{
if ($v > 0)
this._age = $v;
}
}
});
然后将定义一个派生类 Employee
,以展示更复杂的定义对象和一个具有多行的实例矩阵。将使用以下代码定义该类:
var Employee = $$(Person, function($fName, $lName, $age, $salary)
{
this.__base($fName, $lName, $age);
this._salary = $salary;
},
{
'protected _salary': 0,
'public override triggerOneYearOlder': function()
{
this.__base.triggerOneYearOlder();
this._salary *= 1.03;
},
'public salary':
{
'get': function()
{
return this._salary;
}
}
});
由于两个类定义都在同一个脚本中输入并进行了评估,因此 Person
和 Employee
类的编译定义对象会同时输出到控制台,并按各自的顺序排列。在上一个截图中,这两个定义对象集都已标记。
如果在开发者工具中展开这些定义对象,则会按如下所示列出每个访问级别的成员定义:
展开 Person
类的受保护和公共定义对象后(不展开私有定义对象,因为该类中没有私有定义),每个成员定义都按字母顺序列出。受保护集合包含受保护字段 _age
和特殊方法 ~constructor
的定义。公共集合包含只读字段(如 firstName
和 lastName
)、方法(如 getFullName
和 triggerOneYearOlder
)以及 age
属性的特殊 get/set 访问器方法的定义。这些定义中的任何一个都可以展开以观察附加到每个成员定义的数据,如下图所示:
如果展开上一张截图中所示的受保护字段 _age
的定义,则会显示附加到该成员定义的数据。属性 ~iZd
存储成员名称的引用,~uHy
存储成员类型的引用,~vne
存储成员值的引用,~Woo
存储字段 readonly
标志的引用。在 getFullName
方法的定义中,~AKc
、~XSY
和 ~xTZ
分别存储该方法的 abstract
、final
和 virtual
标志的引用。这些数据属性对于每个 jTypes
实例都是唯一的,并且仅供库内部使用。在此实验版本中,它们仅为调试目的输出到控制台。
派生类的受保护和公共定义对象也通过原型链继承自基类中相应的定义对象。这可以在以下截图中观察到:
在 Employee
类的公共定义对象中,可以观察到对象上只有两个成员定义。第一个是方法重写 triggerOneYearOlder
,第二个是 salary
属性的 get 访问器。Employee
类的公共定义对象中其余的所有成员都是通过原型链继承的。如上一张截图中所示,Employee
类公共定义对象的原型存储了 Person
类公共定义对象的引用。
这些编译后的定义对象用于 jTypes 在实例化过程中在类原型链之上构建实例矩阵。下图显示了使用 new
运算符实例化 Person
和 Employee
对象:
每个实例化的实例矩阵都会在控制台中进行日志记录以供调试。它存储为二维数组,其中索引分别为类级别和访问级别。由于 Person
对象没有基类,因此它在数组中只有一个类级别。然而,Employee
对象有两个类级别,因为它继承自 Person
对象。如果在开发者工具中展开类级别,则可以看到每个访问级别实例,如下图所示:
当展开上一张截图中所示的 Person
实例矩阵中的唯一类级别时,会列出四个访问级别实例。数组中的第一个引用映射到 Person
对象的私有实例,该私有实例作为 this
上下文传递给 Person
实例中的所有方法。私有实例还继承自受保护实例,该受保护实例映射到数组中的第二个引用。第三个引用映射到 Person
对象的公共实例,可以通过私有实例的 __this
访问。最后,最后一个引用映射到 Person
实例的基实例,可以通过任何派生类中的私有实例的 __base
访问。还值得注意的是,矩阵构建在其上的原型实例可以通过所有私有实例的 __self
访问,因此它是**在所有实例中唯一用于唯一引用对象的引用**。如果在开发者工具中展开这些访问级别实例,则会按如下所示列出各自访问级别的每个成员:
在上一张截图中,显示了 Employee
对象在每个类级别的受保护实例。可以在控制台中观察到,实例矩阵以自顶向下的方式存储类级别。因此,数组中的第一个类级别映射到 Employee
实例,而第二个类级别映射到 Person
实例。随后,Employee
的受保护和公共实例通过原型链继承自 Person
实例中的相应实例。这在输出中可以看到,其中 Employee
对象(类级别零,访问级别一)的受保护实例的原型存储了 Person
对象(类级别一,访问级别一)的受保护实例的引用。
通过在公共原型链之上构建此实例矩阵,jTypes 有效地使用基于原型的对象模拟了经典继承的行为。然后,附加到每个实例的 as
方法提供了一种将类类型引用映射到实例矩阵中类级别的方法。如果找到匹配的类级别,则该方法将返回该类级别的公共实例。同样,实例矩阵仅供库内部使用,并且在此 jTypes 实验版本中仅为调试目的输出到控制台。
兴趣点
jTypes 的最新实验版本中提供了一个名为 lazy
的新设置。此标志默认设置为 true
,它在实例化期间启用实例矩阵的差异化继承。但是,如果此标志设置为 false
,则在构造实例矩阵时将不使用差异化继承。这可以在以下截图中观察到,其中 lazy
标志设置为 false
,然后实例化了一个 Employee
对象。
通过将 lazy
标志设置为 false
,在实例化 Employee
对象时构造了一个更紧凑的实例矩阵。由于此实例矩阵是在没有差异化继承的情况下构建的,因此成员访问不再需要通过原型链继承。它还产生了一个更轻量级的实例矩阵,需要更少的对象来构造。这是因为当不使用原型链来最小化成员定义时,只需要 3 个访问级别实例。
然而,这种构建实例矩阵的方法需要 jTypes 对每个类级别进行第二次传递,从而导致实例化期间的性能显著下降。因此,jTypes 默认使用另一种构造实例矩阵的方法。但是,这种方法仍然可以在通常具有某种预加载机制的应用程序中使用,并且将受益于减少的实例矩阵对象以及缺少通过原型链继承的成员。