原生JavaScript的类型安全

2020-05-30 04:45:57

这是对原文章的更新,更短、更简单、更合乎逻辑、更正确。

许多开发人员工具,如IDE、框架、库和链接器,都试图为JavaScript提供某种级别的类型安全。本文解释了什么是类型安全,为什么需要类型安全,以及如何使用原生JavaScript实现类型安全。

类型安全是编程语言阻止或防止类型错误的程度。当向函数或表达式提供意外或不兼容的类型值(通常作为参数或上下文)时,会发生类型错误。将一个数字除以一个数组就是一个很好的例子,通常这是没有意义的,但是JavaScript支持这个操作,通常会得到令人惊讶和不想要的结果。

让我们看看在Javascript中使用一些边缘邪教中流行的简约风格来创建类型错误是多么容易。这是可怕的代码,所以请不要复制它。我们会边走边改进。

那没花太长时间!当我们向重复调用提供‘-3’参数时,发生第一个类型错误。该值应该是一个整数,但是我们提供了一个字符串。然后所有的地狱都挣脱了。

当调用Repeats时,它会创建一个“无穷无尽”的循环条件,很快就会消耗其执行环境中的所有可用资源。这会导致NodeJS进程、或浏览器选项卡、或整个浏览器进程或主机操作系统的强制终止。

如果我们观察While循环中计数值的变化,我们会看到以下一系列:';-3';,';-31';,';-311';,';-3111';,.。测试条件Counts<;0确实将Counts字符串转换为数字,但为时已晚:该值始终小于0,并且在每次迭代时都是负值。这是因为表达式COUNTS+=1总是将数字1转换成字符串';1';并将其附加到counts变量。

JavaScript几乎缺少所有的类型控件,因为它最初并不是用来运行大型应用程序的。没有类型声明,返回类型不可靠,没有静态类型检查,只有一个自己的动态检查。

JavaScript没有正式声明变量类型的方法。变量可以包含在执行过程中随时可以更改的任何类型。没有标识类型的符号,变量名不受约束。我们可能都见过一两个JavaScript应用程序,在不同的上下文中,单个类似于符号的手表被用作对象、地图、列表、布尔标志、字符串、整数和浮点数。然而,在实践中,JavaScript中使用的大多数变量并不打算更改上下文中的类型,因为这样做会造成不必要的混乱。

其他语言提供“通配符”变量来引用任何类型的值。它们通常很容易识别,因为它们有一个独特的语法。开发人员知道他们需要小心处理这些“通配符”。在JavaScript中,所有变量都是“通配符”。

JavaScript表达式的返回类型通常不可靠。许多JavaScript操作符都是多态的,将根据使用的变量类型返回不同类型的值。例如,JavaScript‘+’运算符可以连接字符串、添加数字或将字符串转换为数字。当混合变量类型时,返回值可能会令人吃惊,如下所示。

//已确认使用Google V8版本6.0.286.52控制台。日志(进程。版本。v8);var x,y;//expression//|返回值|强制|运算x=3;//|3|-|x=3+1;//|4|-|+num x=3+';1';;//|';31';|3=>;';3';|+str x=3+[];//||';3';|[]=。';|+str x=3+[21];//|';321';|[21]=>;';21';|+str x=3+{};//|';3[对象对象]';|{}=>;str|+str x=';3';-2;//|1|';3';=>。3';+2;//|#39;32';|2=>;';2';|+str x=+';3';;//|3|';3';=>;3|cast_num x=0+';3';;//|';03';|0=>;';0';//|nan|-|nan x=y+';3';;//|';unfined3';|undef=>;str|+str。

此行为(使用GoogleV8版本6.0.286.52确认;请参见NodeJavaScript)在四个主要的-e';console.log(process.versions.v8);';)引擎(V8、IonMonkey、Nitro、Chakra)和其他十几个引擎之间可能是一致的。然而,不太受欢迎的运营商可能在发动机之间存在某些差异,这可能会导致头痛。

其他语言的行为没有那么复杂,因为它们通常具有更严格的类型检查、更严格的强制规则、更少的多态运算符和更少的供应商。例如,Perl使用点(.)。用于连接字符串的运算符和用于添加数字的加号(+)运算符。Perl还使用$、@和%之类的信号(前缀)来指示类型,并使用特殊语法来标识“通配符”(type-glob)引用。

许多语言都提供了某种级别的静态类型检查。例如,Java在编译期间解析大多数变量类型。如果JavaScript有类似的机制,我们将无法运行我们的应用程序,直到我们解决了编译错误。在这个虚构世界中,我们的JavaScript编译输出可能如下所示:

00:OK|x=3;|01:OK|x=3+1;|02:COMPILE_ERROR:TYPE_MISMATCH|x=3+';1';|03:COMPILE_ERROR:TYPE_MISMATCH|x=3+[];|04:COMPILE_ERROR:TYPE_MISMATCH|x=3+[21];|06:COMPILE_ERROR:TYPE_MISMATCH|x=3+{};|07:COMPILE_ERROR:TYPE_MISMATCH|x='。|08:COMPILE_ERROR:TYPE_MISMATCH|x=';3';+2;|09:COMPILE_ERROR:TYPE_MISMATCH|x=+';3';|10:COMPILE_ERROR:TYPE_MISMATCH|x=0+';3';;|11:COMPILE_ERROR:TYPE_MISMATCH|x=y++3;|12:COMPILE_ERROR:TYPE_MISMATCH|x=y+';3';

静态(编译时)类型检查的最大优点可能是它可以提高性能:在编译过程中可以解析的每个类型检查都会删除每次调用函数或方法时需要调用的类型检查。这可以在应用程序运行时删除大量调用,从而提高性能。

静态类型检查并非在所有情况下都有效,尤其是在处理来自未知或不可信来源的数据时。在这些情况下,我们求助于运行时的动态类型检查。用于此目的的原生JavaScript工具是有限的。例如,typeof方法不区分对象和数组。

类型错误可能很难识别和调试。当一个例程未能检查类型时,错误的结果可能会向上传播到调用堆栈,从而导致一连串的错误。如果变量没有命名以指示其预期类型,则很难发现原始缺陷,如下所示:

//此赋值中的类型错误非常明显,let total_str=watch_list/use_bool;//明显的问题//-从除法运算返回的应该是数字,而不是字符串//-在大多数情况下除以列表将导致NaN//-布尔值将强制为0或1//这里是通过使用正确的类型let total_num=watch_list修复的问题。长度/USE_COUNT;

是的,预期的变量类型就是那么重要。我们不得不维护大量的第三方模块,在这些模块中,变量名没有提供类型或用途的提示,或者更糟糕的是,它们明显具有误导性。我们宁愿以可变的方式命名我们的变量,并利用节省下来的时间专注于新的挑战。

正如我们已经展示的,类型错误可能导致严重的应用程序故障和安全漏洞。设想一些NodeJS代码没有正确键入-检查其JSON API。只需在API请求中发送字符串而不是数字,就可以实现拒绝服务(DOS)攻击并关闭整个群集。这种事情时有发生。

有几种方法可以提高JavaScript类型安全性。一种方法涉及使用库或框架,例如需要转换代码或以其他方式预处理代码的流或打字脚本。在这里,我们提出一个更简单的解决方案,它可能特别适合较小的项目,并且只需要三个步骤。

就本文而言,类型转换是使用一组非常严格的规则将值转换为所需数据类型的过程。我们的类型转换函数要么返回请求的值类型,要么返回默认情况下未定义的故障值。

我们可以从hi_core项目中获得易于安装的类型转换方法(在终端中输入npm install hi_core)。如果编辑exampleapplication,则可以使用xhi.util.js中的所有强制转换方法。

npm install hi_Score CD hi_Score bin/xhi设置google-chrome./index.html#打开JavaScript控制台访问xhi._util_Functions。

您不必使用整个库;如果需要,只需从xhi.util.js复制方法即可。去吧,你不会伤害任何人的感情的。这些类型转换方法如下所示,并且在项目中都经过了彻底的测试和良好的文档编制。

所有类型转换方法都有一个或两个参数。只有当转换明确时,才会在类型之间转换数字、字符串和整数。示例如下所示。

//return_data=CastInt(<;Value-to-Cast>;[,<;Failure-Value>;]);return_data=CastInt(0);//0 return_data=CastInt(';0';);//0 return_data=CastInt(';a';);//未定义的return_data=CastInt([]);//未定义的return_data=CastInt。,0);//0 return_data=CastInt([],0);//0。

JavaScript试图强制类型,但结果往往不理想。空白单元格是通常会引发类型错误异常的情况。

强制转换方法是可预测的、显式的和自我记录的,只转换最明确的值。空白单元格是返回失败值(默认情况下未定义)的条件。这些方法不会抛出任何异常。

让我们调整我们的示例函数以使用CastInt和CastFn,以确保提供的参数是正确的类型。如果不是,该函数将返回,不进行任何进一步处理。

函数重复(arg_counts,arg_run){var counts=CastInt(Arg_Count)var run=CastFn(Arg_Run)if(!(counts&;&;run)){return}While(Counts<;){Run(Counts)Counts+=1}}函数报告(INFO){console。log(Info)}重复(';-3';,报告)。

类型转换处理运行时类型检查。静态检查的大多数好处可以通过命名变量来指示它们的预期类型来实现。如果我们遵循这个约定,大多数静态类型错误就会变得不言而喻。让我们使用JS代码标准快速参考中的命名约定重写代码。

函数repeatFn(Arg_Map){var map=CastMap(arg_map,{}),int=CastInt(map.。_int_,0),fn=CastFn(贴图。_fn_),idx;如果(!fn){return;}(idx=int;idx<;0;idx++){fn(Idx);}}函数printToConsole(Idx){console。log(Idx);}repeatFn({_int_:';-3';,_fn_:printToConsole});

此代码现在将传递ESLint。多亏了命名约定,我们可以告诉我们fn应该是一个函数,idx应该立即是一个整数,而不需要阅读其他代码,添加标记注释,或者执行任何转换。

我们使用的完整JS代码标准讨论了为什么简单的命名约定可以极大地减少注释的需要。如果你对这类东西感兴趣,我们认为这是一本有趣而引人入胜的书。

现在我们有了一致的按类型命名的变量和更好的格式,我们可以轻松地阅读代码来创建内联API文档。使用代码标准中的指南,我们得到以下内容:

//BEGIN实用程序方法/REPEATFN/摘要:REPEATFn({_INT_:<;INTEGER>;,_FN_:<;function>;)//目的:只要//COUNTER';INT&39;INT<;为<;0,就重复调用函数';fn';。每次调用后,';int';递增1。如果';int';//的初始值不是<;0,则不会调用函数';fn';。//示例:repeatFn({//_int_:-3,//_fn_:function(Idx){console.log(Idx)}//});//参数:(已命名)//_fn_:要执行的函数。//索引(Idx)的当前值作为其唯一参数提供。//_int_:idx的初始值。idx在执行//_fn_之后递增。因此,值';-1';将导致_fn_(Idx);//单次执行;//返回:未定义//抛出:无//函数repeFn(Arg_Map){var map=CastMap(arg_map,{}),int=CastInt(map.。_int_,0),fn=CastFn(贴图。_fn_),idx;如果(!fn){return;}for(idx=int;idx<;0;idx++){fn(Idx);}}//结束实用程序方法/repeatFn/function printToConsole(Idx){console。log(Idx);}repeatFn({_int_:';-3';,_fn_:printToConsole});

还记得我们在哪里恳求您不要复制我们的第一个代码示例吗?改为复制此代码。它不受大多数类型错误的影响,可读、可测试、可维护、可压缩,并且有很好的文档记录。

我们可以使用内联API文档以及伊斯坦布尔和nodeunit等工具为类型安全的RepeatFn函数创建测试。查看hi_Score的测试套件,看看如何实现这一点。

这项简单的技术与流和打字相比如何?我们打算出版一部续集来回答这个问题。

请记住,只有在处理外部数据和公共方法的输入时才使用类型转换。依靠命名约定在私有方法和变量中传递预期类型的变量。

我们希望您会发现这一点很有用!请在下面的评论中分享您的想法和经验。