使用原型扩展 JavaScript 内建对象






4.90/5 (12投票s)
用原型(prototype)提炼你自己的JavaScript风味。
引言
需要知道的是,你可以利用某些 **JavaScript** 内建对象的 `prototype` 属性来扩展它们的功能。特定对象类的所有新实例都会继承分配给该类的方法。如果你熟悉 C++,可以把扩展对象想象成拥有新专有成员函数的派生类。
暴露 `prototype` 属性的内建 JavaScript 对象包括 `Array`、`Boolean`、`Date`、`Function`、`Number`、`Object`、`RegExp` 和 `String` 对象。请注意,`Global` 和 `Math` 对象被排除在外。
那么,让我们好好利用这一点。记住,扩展内建对象通常会使代码更易读写,最终节省你的打字(和修改)时间;当然,扩展函数本身也应该执行一些涉及内建对象的有意义的操作。它还应该是快速且高效的,但话说回来,每段代码都应该是快速且高效的。
协议(Prototypes)
数字对象是最有趣的部分。字符串也有很多可以做的,但通常大多数字符串操作都归结为连接或子字符串提取,这已经被 `String` 对象本身内建支持了。然而,有些事情可能正是你需要的。
修剪字符串字面量是字符串操作中最常见的问题之一,特别是在处理用户输入时;`String` 对象没有一个内建方法可以将 `' hello world. '` 变成 `'hello world.'`。你不能简单地替换每个空格,否则会得到 `'helloworld'`。所以这里有一种方法可以做到:
// trimming with array ops
String.prototype.trim = function() { return this.split(/\s/).join(' '); }
就像魔法一样,这会去除前导、尾随和中间的空格,并且返回的字符串会将单词用一个且仅一个空格分隔。这是通过 `split` 完成的,`split` 是一个内建的字符串函数,它将字符串转换为数组,使用任何“空白字符”的出现作为分隔符。将数组重新连接成一个字符串对象,使用一个(字面量的)空格作为“连接”字符,可以清除所有前导或尾随的空白字符,以及单词之间的中间空白字符。顺便说一下,`\s` 是一个正则表达式,包括制表符、换行符、垂直制表符或空白字符,因此它对整个段落都有效。
但在继续之前,让我们看看原型化的语法:我们通过 `prototype` 属性扩展内建对象,使用一个唯一的字符串字面量(最好是有意义的名称),我们将其赋值给一个*匿名*函数;该函数可以有一个或零个参数,函数体紧随原型声明之后。除了非常罕见嵌套对象作用域的情况外,`this` 关键字引用调用函数的对象类的实例,也就是说,引用实际的字符串。函数不一定需要是匿名的,但给它一个名称可能有点多余或导致混淆。
// redundant prototype function definition
String.prototype.trim =
function trim() { return this.split(/\s/).join(' '); };
...
// confusing prototype function definition
String.prototype.trim =
function trimblanks() { return this.split(/\s/).join(' '); };
不过,命名函数也有用处,如果你想将对象的某个方法(或函数)取回为字符串;你可以通过 `eval` 函数来实现。
var s = eval(String.trim);
// s == 'function() { return this.split(/\s/).join(' '); }'
如果原型声明给函数命名,如前所示。
var s = eval(String.trim);
// s == 'function trim() { return this.split(/\s/).join(' '); }'
同样,语法需要原型名称,但函数名称是可选的。如果你想知道,如果你想解密内建函数,你会得到这个:
var s = eval(String.substr);
// s == 'function substr() { [native code] }'
关于逆向工程 JavaScript 引擎,就到这里。
当然,还有其他方法可以实现 `trim` 函数;程序员首先知道,一个特定问题通常有不止一种解决方案。好吧,也许是数学家首先,然后是工程师。考虑使用字符串的 `replace` 方法和一个更复杂的正则表达式来拆分字符串对象:
// trimming with replace
String.prototype.trim2 =
function() { return this.replace(/^\s*(\S*(\s+\S+)*)\s*$/, '$1'); }
这并不好看,特别是如果你不了解正则表达式语法。我们正在替换行首(`^`)之前的任何前导空格(`\s`,其中 `*` 表示零个或多个通配符),以及任何尾随空格,即行尾(`$`)之前的那些。正则表达式括号中的内容是以非空格字符(`\S*`)开头,但允许空格穿插其中(第二个括号)。结果(`$1`)正是外部括号求值内容的精确内容,并且替换由函数原型返回。请注意,此方法保留了单词间隔。
`trim2` 实际上是我解决 `trim` 问题的第一种方法;它没有类型转换,它只是一个内建方法的包装器。但是,当用一个 100,000 个字符的字符串(一个相当大的纯文本文件)进行测试时,`replace` 方法平均比 split-join 操作慢 160 倍!对于更大的字符串,`replace` 根本就锁死了。如果你想保留单词间隔,`trim2` 可能仍然有用,但你必须检查长度并编写一些逻辑将其分解成可 `replace` 处理的块,使其比现在更丑陋。无论如何,你很少需要保留单词间隔,因为显示两端对齐的文本可以通过 HTML 属性轻松实现,所以 `trim` 作为首选方法胜出。最终,`trim` 比 `trim2` 更优雅、更容易理解。
好了,修剪完之后,这是一个类似 VB 的字符串复制器。
// VB-like string replicator
String.prototype.times = function(n)
{
var s = '';
for (var i = 0; i < n; i++)
s += this;
return s;
}
...
// using the prototype
var r = 'hey', q;
r = r.times(5); // r == 'heyheyheyheyhey'
q = '0'.times(4) // q == '0000'
这个操作很明显,但为什么有人会想把字符串复制 n 次呢?嗯,它的用途不多,但 **VB**、**VBA** 和 **VBScript** 支持 `String()` 函数,而 JavaScript 不支持。除此之外,我确实有一个好的用途,那就是格式化数字,用于零填充;但在此之前,我只想强调几点。
- 该函数通过连接 n 个副本构造一个新的返回值;最好不要修改输入参数(例如,永远不要对 `this` 赋值);你始终可以使用像示例中那样的重新赋值;
- 该函数需要两个变量,`s`,返回值;`i`,循环计数器;代码将 `s` 初始化为空字符串(否则会失败);
- JavaScript 是弱类型语言,所以 `var` 关键字完全是可选的;
- 函数体只要符合操作需求,可以像需要的那样复杂,拥有任意多的局部变量,并完全访问引擎和其他内建对象;
- 函数可以操作类型变量和该类型的常量(字面量),如最后一行所示;这是与大多数脚本和编程语言相比,JavaScript 独有的特性。
我本来可以将函数命名为 'string',就像 VB 中一样,但这可能会导致混淆,因为 `String` 对象本身就有 `string` 方法;'@ times 5' 对任何其他人来说都 pretty self-explanatory。所以这就引出了零填充函数。
// Zero-Padding
String.prototype.zp =
function(n) { return '0'.times(n - this.length) + this; }
...
// zp usage
var a = '5'.zp(5); // a == '00005'
也许你可以为函数想出一个更好的名字,但我对 `zp` 很满意。有趣的部分是当我们向 `Number` 对象扩展原型时。
// string functions that we want to apply directly to numbers...
Number.prototype.zp = function(n) { return this.toString().zp(n); }
...
// ... so that we can use it on numbers!
var b = (5 * 3).zp(5); // b == '00015'
var c = (b * 10).zp(2); // c == '150'
好了,就是这样。我们声明了一个 `Number` 原型函数,它包装了一个 `String` 原型函数,而该函数又基于另一个 `String` 原型函数;我们还为了格式目的将数字转换为字符串,所以此时你可以看出没有返回值限制,没有参数类型检查(甚至没有参数数量强制),基本上没有限制你可以做什么!顺便说一下,即使示例中的 `b` 是一个字符串,将其乘以 10 也会将其转换为数字,并且由于 150 的字符串长度大于 2,`String times` 函数会返回一个空字符串,这正好满足我们的需求。
那么零尾随函数呢?嗯,这和反转连接参数一样简单。
// Zero-Trailing
String.prototype.zt =
function(n) { return this + '0'.times(n - this.length); }
...
// but be carefull about the results!
var b = (5 * 3).zt(5); // b == '15000'
当然,这并不是零尾随的目的;我们本来可以简单地乘以一千来得到相同的结果。尾随零对于格式化实数(浮点数)最有价值。 **C** 的 `printf` 系列函数以及 **Microsoft Excel** 可能是这方面的佼佼者,但我还没有看到任何原生脚本语言实现这样的函数。顺便说一下,VB 的 `Format` 函数根本无法与 `printf` 相提并论,但公平地说,`printf` 是一个错综复杂的功能,它通过一个格式字符串处理可变数量的参数。
你可以谷歌搜索“JavaScript printf”,会找到无数的实现;我曾经也做过一个,它复制了 C `printf` 函数中的字符状态机;好吧,我只是将其翻译成了 JavaScript。
然后我动了动脑筋。格式化的表格输出只有在实际输出是纯文本文件时才有意义。当显示器还是基于字符的时候,这是很棒的,但我们已经不是当年了。如果还需要通过文本文件进行调试或分析,可能还有一些用途;当调整 Direct3D 的 `.x` 文件或其他纯文本 3D 文件时,我通常是这种情况。但总的来说,如果格式化的输出是表格形式的数值数据,那么逗号分隔值(`.csv`)文件或制表符分隔的文件会更好:Excel 可以导入并格式化它们。特别是,JavaScript 输出很可能会显示在某个网页中,因此我们可以利用 HTML 来帮助格式化,至少是缩进问题。
为了给这个问题降降温,我们不会将目标定在开发一个功能齐全的 JavaScript `printf`;除非你真的需要,否则以下方法将比较优雅地处理你的浮点数格式化需求。让我们从截断到指定的小数位数开始。
// decimal digits truncation
Number.prototype.truncate = function(n)
{
return Math.round(this * Math.pow(10, n)) / Math.pow(10, n);
}
...
var a = 78.53981633974483;
var b = a.truncate(4); // b = 78.5398
var c = (5 / 2).truncate(4); // c = 2.5
var d = (199).truncate(4); // d = 199
这只是一个简单的将小数点向后、向前移动,并通过 `Math` 舍入进行内建截断。它工作得很好,除了对于我们的格式化目的,我们缺少 `c` 和 `d` 中的 2.5 或 199 的尾随零。
有几个 `Global` 方法可以将对象解析为整数和浮点数,但没有方法可以提取实数的整数部分;这听起来像是一个不错的 `Number` 原型候选。
// fractional part of a number
Number.prototype.fractional =
function() { return parseFloat(this) - parseInt(this); }
...
var f = a.fractional(); // f == 0.53981633974483
现在我们准备原型化我们的数字格式化函数。
// format a number with n decimal digits
Number.prototype.format = function(n)
{
// round the fractional part to n digits, skip the '0.' and zero trail
var f = this.fractional().truncate(n).toString().substr(2).zt(n);
// integer part + dot + fractional part, skipping the '0.'
return parseInt(this) + '.' + f;
}
使这能够工作的原因是 '+' 运算符会进行正确的类型转换,所以我们可以将一个整数和一个字符串“相加”(连接)。`substr` 函数需要一个 `String` 对象,因此需要进行转换,但之后没有任何东西可以阻止我们定义 `substr` 为一个 `Number` 原型并调用 `String` 版本,或者有吗?
// substr for numbers!
Number.prototype.substr =
function(n) { return this.toString().substr(n); }
好吧,你可能会说这有点过度,但话说回来,它可能会让你少输入一些 `.toString()`!
如前所述,HTML 标签可以处理缩进;只需将表格单元格右对齐,就可以使你的表格输出更具可读性。尽管如此,我们的原型仍然缺少其他格式元素。考虑一种货币格式,带有千位分隔符,就像 VBScript 的 `FormatNumber` 和 `FormatCurrency` 函数一样。
在这种情况下,我们需要在整数部分每三个数字插入一个数字分组分隔符。这是一个字符串操作而不是数字操作。我们将使用逗号 (,) 作为千位分隔符,因为 JavaScript 始终期望点 (.) 作为小数分隔符。对于非美式英语的其他地区,有一些解决方法;我看到的实现大多涉及混合 JavaScript 和 VBScript,但 VBScript 只在 IE 浏览器下工作。无论如何,分隔符的交换应该在显示输出之前完成,而不是在算术运算之间。
还要记住,小数点分隔符对数字至关重要,但千位分隔符只是提高了数字的可读性,也就是说,它仅仅是装饰性的。我提到这一点是为了强调在对数字进行操作或使用数字之后进行格式化的重要性。最终,我们可能需要一个字符串原型来消除数字格式(即千位分隔符),以获得一个我们可以用于计算的实际 `Number` 对象。
至于为什么世界上的其他地方使用逗号作为小数点分隔符,**国际标准化组织** 认为在手写时更容易避免误输入逗号,而点在复印件中可能是一个污点,在传真传输中可能是一个噪音,或者(你敢相信吗?)在横幅广告中可能是一只苍蝇!但我们关于通识教育的内容到此为止,让我们开始研究代码。
我们将从一个千位分隔符函数开始,该函数会丢弃小数部分。插入分隔符最简单的方法是从右到左遍历数字的字符串表示形式,即反向遍历。奇怪的是,JavaScript 中没有内建的字符串反转方法,所以我们必须原型化自己的。
// String reverse
String.prototype.reverse =
function() { return this.split('').reverse().join(''); }
这相当直接:用零长度分隔符分割字符串,幸运的是,这会得到一个每个元素一个字符的数组对象。`Array` 对象确实有一个内建的 `reverse` 方法,所以我们再次用零长度分隔符将它的输出粘合起来,将 `'Hello'` 变成 `'olleH'`。很巧妙。
一旦我们有了字符串的反转,剩下的就是插入千位分隔符。
// integer thousand separators
Number.prototype.group = function()
{
var s = parseInt(this).toString().reverse(), r = '';
for (var i = 0; i < s.length; i++)
r += (i > 0 && i % 3 == 0 ? ',' : '') + s.charAt(i);
return r.reverse();
}
我们从整数部分的字符串表示形式开始并反转它;然后我们遍历它,并在返回变量 `r` 中每三个数字累积一次数字,以及一个可选的 `,` 前缀。如果你不知道发生了什么,请查看模(余数)`%` 运算符的文档(以及你的数学专业知识)。再次反转结果就完成了这项工作。
所以我们旧的 format 函数可以演变成新改进的 `format2` 函数。
// format a number with n decimal digits and thousands separator
Number.prototype.format2 = function(n)
{
// truncate and zero-trail the fractional part
var f = this.fractional().truncate(n).substr(2).zt(n);
// grouped integer part + dot + fractional part
return this.group() + '.' + f;
}
从这里开始就取决于你的想象力了;你可以使用第二个参数来打开或关闭分组,你可以扩展分组函数来处理实数,你可以使小数部分可选,等等。我认为我已经为你提供了一个很好的基础来开始开发你自己的数字格式化器。我见过从递归正则表达式格式化器到货币符号处理程序,更不用说数字转文本转换器了,就像自动支票书写器使用的那些。请记住,这些代码片段只是实现方法之一!
如前所述,这是消除格式化输入并返回实际浮点数的函数。它当然是一个 `String` 方法;格式化输入更有可能是字符串。
// clear format from a string representation of a number
String.prototype.clean =
function() { return parseFloat(this.replace(/,/g, '')); }
...
var a = 7853981.633974483;
var b = a.format(4); // b == '7,853,981.6340'
var c = b.clean(); // c == 7853981.634
请注意,返回的数字没有尾随零,因为在数值上它们没有意义。示例可能看起来有点乏味;我们一开始就可以在任何计算中使用 `a`,但要考虑到输入(`b`)直接来自格式化文本文件或输入框:除非你清理它,否则 `parseFloat(b)` 会是 7,而不是 7 百万多;试着责怪一个投资者犯这样的打字错误!此外,我们只查找逗号,而没有其他任何东西;*正确*的正则表达式应该处理非数字字符、小数点(点)和负号。
String.prototype.clean =
function() { return parseFloat(this.replace(/[^0-9|.|-]/g, '')); }
顺便说一下,它也会去除货币符号(如果有的话)。请注意,包含混合文本和数字的输入字符串可能会产生意外的结果;换句话说,该函数期望的是类似于格式化数字的内容,而不是文本行或段落。
但是,哎呀,我们的函数仍然有一个致命的缺陷,它在处理负数时会惨败。代码必须记住输入的符号,而你猜怎么着,这又是另一个不错的原型候选。
// number sign 'bit' (boolean)
Number.prototype.sign = function() { return this < 0; }
该函数清楚地回答“n 是负数吗?”,所以它的作用就像符号位一样。 Duh。重点不是这个;重点是我们可以使用它作为字符串的索引,来帮助解决问题。
// format a number with n decimal digits thousands sep and sign
Number.prototype.format3 = function(n)
{
// remember the input sign and cancel it
var a = Math.abs(this);
// truncate and zero-trail the fractional part
var f = a.fractional().truncate(n).substr(2).zt(n);
// sign + grouped integer part + dot + fractional part
return '+-'.substr(this.sign(), 1) + a.group() + '.' + f;
}
符号位选择 + 或 -,然后其余的都一样,一旦我们消除了符号。如果你不想要 + 号,你可以用一个空格替换它,并选择性地修剪返回值。
相比之下,VBScript 的 `FormatCurrency` 和 `FormatNumber` 可以使用括号来表示负数,但它们不能强制加上加号(+)。再说一遍,括号只是装饰品,而且相当过时,因为颜色更能区分正数和负数。
关于数字格式化就到这里。这些原型充分满足了我的显示需求,你可以以此为基础构建你获奖的 JavaScript `printf` 代码。它们可能不那么令人惊叹,但它们完成了任务,它们在一种非常简单但功能强大的语言中进行了非常简单的操作,并且它们扩展了内建对象的功能,让我能够创建自己的 JavaScript 方言,而这本身就比任何竞争性脚本语言都强得多。
Date 对象原型
让我们将注意力转向 `Date` 对象。它拥有每个日期部分的内建方法,就像 VBScript 的 `DatePart` 函数一样,但它缺少 `DateDiff` 和 `DateAdd` 的对应项,所以这里是它们:
// date diff in days
Date.prototype.dateDiff = function(d)
{ return Math.round((d.valueOf() - this.valueOf()) / 86400000); }
// date adds
Date.prototype.add = function(n)
{
var d = new Date(this);
d.setDate(d.getDate() + n);
return d;
}
Date.prototype.addMonth = function(n)
{
var d = new Date(this);
d.setMonth(d.getMonth() + n);
return d;
}
Date.prototype.addYear = function(n)
{
var d = new Date(this);
d.setFullYear(d.getFullYear() + n);
return d;
}
...
var today = new Date();
var tomorrow = today.add(1);
var d = today.dateDiff(tomorrow); // d == 1
`dateDiff` 使用 `valueOf` 内建方法将日期转换为自同一固定基准以来的毫秒数,然后除以一天中的毫秒数,再向上舍入到最接近的整数,得到以天为单位的结果。正结果表示参数在时间上靠后;这在一定程度上帮助了调用语法,但仅此而已;日历运算,如“距离复活节还有多少天?”可能会变得很麻烦,如果你以错误的顺序减去参数。
`Date` 加法构造一个新的返回日期,并使用 `Date` 对象的“set”内建方法对其进行修改。请注意,在 Visual Basic 中,你可以相加日期和数字,数字会被解释为天数。然而,在 JavaScript 中,你很可能是在相加毫秒,所以相加整个天、月或年需要相加每种情况等效的毫秒数。然而,这并不能解决不同月份天数不同的问题,所以我们最好只是用所需的量来移动日期部分。
前面的 `Date` 原型帮助我构建了另外两个用于查找月份第一天和最后一天的函数:
// first and last date of month
Date.prototype.getFirstDate =
function() { var d = new Date(this);
d.setDate(1); return d; }
Date.prototype.getLastDate =
function() { var d = this.addMonth(1);
d.setDate(1); return d.add(-1); }
我知道你会发现它们非常有用,特别是如果你曾经需要构建或编程网页日历。第一天很容易,只需将日期索引部分设置为 1。最后一天比较复杂,移到下个月的第一天然后减去一天。你可能也会发现这个范围检查器很有用:
// date between [d1, d2]
Date.prototype.between =
function(d1, d2) { return 0 <= d1.dateDiff(this) &&
d2.dateDiff(this) <= 0; }
...
var b = today.between(today.getFirstDate(),
today.getLastDate()); // b == true
当然,没有参数检查,所以要小心传递日期,并确保 `d1 < d2`。
在同样的日历编程模式下,我曾经面临一个节假日高亮器。星期日和固定日期节假日很容易处理,但可移动的、基于复活节的节假日就不那么容易了。首先,复活节本身就是一个可移动的日期,所以我需要一个(公历)复活节计算器。
// easter calculator
Date.prototype.getEaster = function(y)
{
if (!y)
y = this.getFullYear();
var c, n, k, i, j, l, m, d;
c = parseInt(y / 100);
n = y - 19 * parseInt(y / 19);
k = parseInt((c - 17) / 25);
i = c - parseInt(c / 4) - parseInt((c - k) / 3) + 19 * n + 15;
i = i - 30 * parseInt(i / 30);
i = i - parseInt(i / 28) *
(1 - parseInt(i / 28) * parseInt(29 / (i + 1)) *
parseInt((21 - n) / 11));
j = y + parseInt(y / 4) + i + 2 - c + parseInt(c / 4);
j = j - 7 * parseInt(j / 7);
l = i - j;
m = 3 + parseInt((l + 40) / 44);
d = l + 28 - 31 * parseInt(m / 4);
return new Date(y, m - 1, d);
}
输入是完整的四位数年份,或者如果未提供参数,则是日期实例的年份。你可以自己检查数学,该数学归功于 J. M. Oudin (1940);我在**美国海军网站**上找到了它。规则是,复活节是发生在 3 月 21 日或之后的第一次教会月圆之后的第一个星期日。听起来有点复古,但嘿,这就是传统。
一旦我得到了复活节的日期,可移动的节假日就是相对于它的,所以我只需要检查日期巧合:
// locale holiday test (Venezuelan)
Date.prototype.isHoliday = function()
{
// sundays
if (this.getDay() == 0)
return true;
var y = this.getFullYear();
var m = this.getMonth();
var d = this.getDate();
var r;
// fixed holidays
switch(m)
{
case 0: case 4: r = d == 1; break;
case 3: r = d == 19; break;
case 5: r = d == 24; break;
case 6: r = d == 24 || d == 5; break;
case 9: r = d == 12; break;
case 11: r = d == 25 || d == 31; break;
default:
}
// moveable easter-based holidays
if (!r)
{
// integer date diff decision
switch (this.dateDiff(this.getEaster(y)))
{
case 2: case 3: case 48: case 47: r = true;
default:
}
}
return r;
}
所以你可以通过更改固定节假日的月份和日期,以及改变基于复活节的节假日的偏移值,来轻松修改本地节假日。示例涵盖了委内瑞拉的节假日。
关于日历就到这里。为了完成日期原型,我邀请你查看我之前发表的**date format prototype**文章,它使用格式字符串来显示您选择的地区日期。您还会在本文包含的 `date.js` 文件中找到它。
Math 对象原型
为了结束这个话题,我将向你展示如何对 `Math` 对象的一些函数进行原型化。
`Math` 对象缺少一组三角函数,这可能会让你的三角学噩梦稍微轻松一些。尽管如此,正如文章开头提到的,`Math` 对象没有 `prototype` 属性;这是因为它的实际定义:`Math` 对象是字面量,意味着它没有构造函数,所以它是由 JavaScript 引擎初始化的,而不是先构造再实例化的。因此,它没有 `prototype` 属性。对我们来说幸运的是,我们仍然可以通过简单地跳过 `prototype` 关键字来扩展它!(感谢 **schephais** 在 web developer archives 的指点)。这里是如何做到:
// math cosecant, secant and cotangent
Math.csc = function(x) { return 1 / Math.sin(x); }
Math.sec = function(x) { return 1 / Math.cos(x); }
Math.cot = function(x) { return 1 / Math.tan(x); }
...
// using the new trigonometric predicates
var a = 45 * Math.PI / 180;
var b = Math.csc(a); // b == 1.4142135623730951
我将让你来定义“arc”函数。我们刚刚用一些相当常见的三角函数扩展了内建的 `Math` 对象;有趣之处在于,它们使我们不必再输入 `'1 / Math.aaa(x)'`,并使代码更容易阅读,如果你是一个三角学爱好者。
对于示例函数,`x` 预计是一个数字;否则你会得到 `NaN`(就像 `Math.sin(x)` 或任何 `Math` 方法一样)。
结论
很少有脚本语言能让你像功能齐全的编程语言那样,通过子类化来构建自己的内建对象。JavaScript 就是其中之一;它允许 Web 程序员提炼出自己特有的 JavaScript 风味,并有望激发他们的想象力,帮助他们编写出更好的代码。
所以,再次检查你的代码,开始原型化你自己的 JavaScript 版本吧!