CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛
CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛
CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛

JavaScript强制类型转换-源码交易平台丞旭猿

祝鑫奔

祝鑫奔,YMFE 工程师,猫奴,沉迷于吸猫无法自拔。负责 QReact、QRN-Web 的开发和维护。

强制类型转换是非常常用的技术,虽然它曾经导致了很多隐蔽的 BUG ,但是我们不应该因噎废食,只有理解它的原理才能享受其带来的便利并减少 BUG 的产生。

作为 JavaScript 程序员,你一定获取过当前系统的时间戳。在 ES5 引入Date.now()静态方法之前,下面这段代码你一定不会陌生:

  1. 1vartimestamp=+newDate();// timestamp 就是当前的系统时间戳,单位是 ms

你肯定听说过 JavaScript 的强制类型转换,你能指出这段代码里哪里用到了强制类型转换吗? 几乎所有 JavaScript 程序员都接触过强制类型转换 —— 不论是有意的还是无意的。强制类型转换导致了很多隐蔽的 BUG ,但是强制类型转换同时也是一种非常有用的技术,我们不应该因噎废食。 在本文中我们来详细探讨一下 JavaScript 的强制类型转换,以便我们可以在避免踩坑的情况下最大化利用强制类型转换的便捷。

一、类型转换和强制类型转换

类型转换发生在静态类型语言的编译阶段,而强制类型转换发生在动态类型语言的运行时(runtime),因此在 JavaScript 中只有强制类型转换。 强制类型转换一般还可分为隐式强制类型转换(implicit coercion显式强制类型转换(explicit coercion。 从代码中可以看出转换操作是隐式的还是显式的,显式强制类型转换很容易就能看出来,而隐式强制类型转换可能就没有这么明显了。

比如:

  1. 1vara=21;

  2. 2

  3. 3varb=a+;

  4. 4

  5. 5varc=String(a);

对于变量b而言,此次强制类型转换是隐式的。+操作符在其中一个操作数是字符串时进行的是字符串拼接操作,因此数字21会被转换为相应的字符串"21"。 然而String(21)则是非常典型的显式强制类型转换。 这两种强制转换类型的操作都是将数字转换为字符串。 不过显式还是隐式都是相对而言的。比如如果你知道a+""是怎么回事,那么对你来说这可能就是显式的。反之,如果你不知道String(a)可以用来字符串强制类型转换,那么它对你来说可能就是隐式的。

二、抽象值操作

在介绍强制类型转换之前,我们需要先了解一下字符串、数字和布尔值之间类型转换的基本规则。在 ES5 规范中定义了一些抽象操作和转换规则,在这我们介绍一下ToPrimitiveToStringToNumberToBoolean。注意,这些操作仅供引擎内部使用,和平时 JavaScript 代码中的.toString()等操作不一样。

2.1 ToPrimitive

你可以将ToPrimitive操作看作是一个函数,它接受一个input参数和一个可选的PreferredType参数。ToPrimitive抽象操作会将input参数转换成一个原始值。如果一个对象可以转换成不止一种原始值,可以使用PreferredType指定抽象操作的返回类型。

根据不同的输入类型,ToPrimitive的转换操作如下:

2.1.1[[DefaultValue]](hint)内部操作

在对象O上调用内部操作[[DefaultValue]]时,根据hint的不同,其执行的操作也不同,简化版(具体可参考 ES5 规范 8.12.8 节 https://www.ecma-international.org/ecma-262/5.1/sec-8.12.8)如下:

2.2 ToString

原始值的字符串化的规则如下:

  • null转化为"null"

  • undefined转化为"undefined"

  • true转化为"true"

  • false转化为"false";

  • 数字的字符串化遵循通用规则,如21转化为"21",极大或者极小的数字使用指数形式,如:

  1. 1varnum=3.912*Math.pow(10,50);

  2. 2

  3. 3num.toString();// "3.912e50"

  • 对于普通对象,如果对象有自定义的toString()方法,字符串化时就会调用该自定义方法并使用其返回值,否则返回的是内部属性[[Class]]的值,比如"object [Object]"。需要注意的是,数组默认的toString()方法经过了重新定义,其会将所有元素字符串化之后再用","连接起来,如:

  1. 1vararr=[1,2,3];

  2. 2

  3. 3arr.toString();// "1,2,3"

2.3 ToNumber

在 ES5 规范中定义的ToNumber操作可以将非数字值转换为数字。其规则如下:

  • true转换为1

  • false转换为0

  • undefined转换为NaN

  • null转换为0

  • 针对字符串的转换基本遵循数字常量的相关规则。处理失败则返回NaN

  • 对象会先被转换为原始值,如果返回的是非数字的原始值,则再遵循上述规则将其强制转换为数字。

在将某个值转换为原始值的时候,会首先执行抽象操作ToPrimitive,如果结果是数字则直接返回,如果是字符串再根据相应规则转换为数字。 参照上述规则,现在我们可以一步一步来解释本文开头的那行代码了。

  1. 1vartimestamp=+newDate();// timestamp 就是当前的系统时间戳,单位是 ms

其执行步骤如下:

有了以上知识,我们就可以实现一些比较好玩的东西了,比如将数字和对象相加:

  1. 1vara={

  2. 2valueOf:function(){

  3. 3return18;

  4. 4}

  5. 5};

  6. 6

  7. 7varb=20;

  8. 8

  9. 9+a;// 18

  10. 10Number(a);// 18

  11. 11a+b;// 38

  12. 12a-b;// -2

顺带提一下,从 ES5 开始,使用Object.create(null)创建的对象,其[[Prototype]]属性为null因此没有valueOf()toString()方法,因此无法进行强制类型转换。请看如下示例:

  1. 1vara={};

  2. 2varb=Object.create(null);

  3. 3

  4. 4+a;// NaN

  5. 5+b;// Uncaught TypeError: Cannot convert object to primitive value

  6. 6a+;// "[object Object]"

  7. 7b+;// Uncaught TypeError: Cannot convert object to primitive value

2.4 ToBoolean

JavaScript 中有两个关键字truefalse,分别表示布尔类型的真和假。我们经常会在if语句中将0作为假值条件,1作为真值条件,这也利用了强制类型转换。我们可以将true强制类型转换为1false强制类型转换为0,反之亦然。然而true1并不是一回事,false0也一样。

2.4.1 假值

在 JavaScript 中值可以分为两类:

  • 可以被强制类型转换为false的值

  • 其他(被强制类型转换为true的值)

在 ES5 规范中下列值被定义为假值:

  • undefined

  • null

  • false

  • +0-0NaN

  • ""

假值的布尔强制类型转换结果为false。 在假值列表以外的值都是真值。

2.4.2 例外

规则难免有例外。刚说了除了假值列表以外的所有其他值都是真值,然而你可以在现代浏览器的控制台中执行下面几行代码试试:

  1. 1Boolean(document.all);

  2. 2typeofdocument.all;

得到的结果应该是false"undefined"。然而如果你直接执行document.all得到的是一个类数组对象,包含了页面中所有的元素。document.all实际上不能算是 JavaScript 语言的范畴,这是浏览器在特定条件下创建一些外来(exotic)值,这些就是假值对象。

假值对象看起来和普通对象并无二致(都有属性,document.all甚至可以展为数组),但是其强制类型转换的结果却是false

在 ES5 规范中,document.all是唯一一个例外,其原因主要是为了兼容性。因为老代码可能会这么判断是否是 IE:

  1. 1if(document.all){

  2. 2// Internet Explorer

  3. 3}

在老版本的 IE 中,document.all是一个对象,其强制类型转换结果为true,而在现代浏览器中,其强制转换结果为false

2.4.3 真值

除了假值以外都是真值。

比如:

  1. 1vara=false;

  2. 2varb=0;

  3. 3varc="";

  4. 4

  5. 5vard=Boolean(a&&b&&c);

  6. 6

  7. 7d;// ?

dtrue还是false呢?

答案是true。这些值都是真值,相信不需要过多分析。 同样,以下几个值一样都是真值:

  1. 1vara=[];

  2. 2varb={};

  3. 3varc=function(){};

三、显式强制类型转换

显式强制类型转换非常常见,也不会有什么坑,JavaScript 中的显式类型转换和静态语言中的很相似。

3.1 字符串和数字之间的显式转换

字符串和数字之间的相互转换靠String()Number()这两个内建函数实现。注意在调用时没有new关键字,只是普通函数调用,不会创建一个新的封建对象。

  1. 1vara=21;

  2. 2varb=2.71828;

  3. 3

  4. 4varc=String(a);

  5. 5vard=Number(b);

  6. 6

  7. 7c;// "21"

  8. 8d;// 2.71828

除了直接调用String()或者Number()方法之外,还可以通过别的方式显式地进行数字和字符串之间的相互转换:

  1. 1vara=21;

  2. 2varb=2.71828;

  3. 3

  4. 4varc=a.toString();

  5. 5vard=+b;

  6. 6

  7. 7c;// "21"

  8. 8d;// 2.71828

虽然a.toString()看起来很像显式的,然而其中涉及了隐式转换,因为21这样的原始值是没有方法的,JavaScript 自动创建了一个封装对象,并调用了其toString()方法。

+b中的+是一元运算符,+运算符会将其操作数转换为数字。而+b是显式还是隐式就取决于开发者自身了,本文之前也提到过,显式还是隐式都是相对的。

3.2 显式转换为布尔值

和字符串与数字之间的相互转换一样,Boolean()可以将参数显示强制转换为布尔值:

  1. 1vara=;

  2. 2varb=0;

  3. 3varc=null;

  4. 4vard=undefined;

  5. 5

  6. 6vare=0;

  7. 7varf=[];

  8. 8varg={};

  9. 9

  10. 10Boolean(a);// false

  11. 11Boolean(b);// false

  12. 12Boolean(c);// false

  13. 13Boolean(d);// false

  14. 14

  15. 15Boolean(e);// true

  16. 16Boolean(f);// true

  17. 17Boolean(g);// true

不过我们很少会在代码中直接用Boolean()函数,更常见的是用!!来强制转换为布尔值,因为第一!将操作数强制转换为布尔值,并反转(真值反转为假值,假值反转为真值),而第二个!会将结果反转回原值:

  1. 1vara=;

  2. 2varb=0;

  3. 3varc=null;

  4. 4vard=undefined;

  5. 5

  6. 6vare=0;

  7. 7varf=[];

  8. 8varg={};

  9. 9

  10. 10!!a;// false

  11. 11!!b;// false

  12. 12!!c;// false

  13. 13!!d;// false

  14. 14

  15. 15!!e;// true

  16. 16!!f;// true

  17. 17!!g;// true

不过更常见的情况是类似if(...){}这样的代码,在这个上下文中,如果我们没有使用Boolean()或者!!转换,就会自动隐式地进行ToBoolean转换。 三元运算符也是一个很常见的布尔隐式强制类型转换的例子:

  1. 1vara=21;

  2. 2varb=hello;

  3. 3varc=false;

  4. 4

  5. 5vard=a?b:c;

  6. 6

  7. 7d;// "hello"

在执行三元运算的时候,先对a进行布尔强制类型转换,然后根据结果返回:前后的值。

四、隐式强制类型转换

大部分被诟病的强制类型转换都是隐式强制类型转换。但是隐式强制类型转换真的一无是处吗?并不一定,引擎在一定程度上简化了强制类型转换的步骤,这对于有些情况来说并不是好事,而对于另一些情况来说可能并不一定是坏事。

4.1 字符串和数字之间的隐式强制类型转换

在上一节我们已经介绍了字符串和数字之间的显式强制类型转换,在这一节我们来说说他们两者之间的隐式强制类型转换。

+运算符既可以用作数字之间的相加也可以通过重载用于字符串拼接。我们可能觉得如果+运算符两边的操作数有一个或以上是字符串就会进行字符串拼接。这种想法并不完全错误,但也不是完全正确的。比如以下代码可以验证这句话是正确的:

  1. 1vara=21;

  2. 2varb=4;

  3. 3

  4. 4varc=21;

  5. 5vard=4;

  6. 6

  7. 7a+b;// 25

  8. 8c+d;// "214"

但是如果+运算符两边的操作数不是字符串呢?

  1. 1vararr0=[1,2];

  2. 2vararr1=[3,4];

  3. 3

  4. 4arr0+arr1;// ???

上面这条命令的执行结果是"1,23,4"ab都不是字符串,为什么 JavaScript 会把 a 和 b 都转换为字符串再进行拼接?

根据 ES5 规范11.6.1节,如果+两边的操作数中,有一个操作数是字符串或者可以通过以下步骤转换为字符串,+运算符将进行字符串拼接操作:

  • 如果一个操作数为对象,则对其调用ToPrimitive抽象操作;

  • ToPrimitive抽象操作会调用[[DefaultValue]](hint),其中hintNumber

这个操作和上面所述的ToNumber操作一致,不再重复。 在这个操作中,JavaScript 引擎对其进行ToPrimitive抽象操作的时候,先执行valueOf()方法,但是由于其valueOf()方法返回的是数组,无法得到原始值,转而调用toString()方法,toString()方法返回了以,拼接的所有元素的字符串,即1,23,4+运算符再进行字符串拼接,得到结果1,23,4

简单来说,只要 `+ 的操作数中有一个是字符串,或者可以通过上述步骤得到字符串,就进行字符串拼接操作;其余情况执行数字加法。 所以以下这段代码可谓随处可见:

  1. 1vara=21;

  2. 2

  3. 3a+;// "21"

利用隐式强制类型转换将非字符串转换为字符串,这样转换非常方便。不过通过a+""和直接调用String(a)之间并不是完全一样,有些细微的差别需要注意一下。a+""会对a调用valueOf()方法,然后再通过上述的ToString抽象操作转换为字符串。而String(a)则会直接调用toString()。 虽然返回值都是字符串,然而如果a是对象的话,结果可能出乎意料!

比如:

  1. 1vara={

  2. 2valueOf:function(){

  3. 3return21;

  4. 4},

  5. 5toString:function(){

  6. 6return6;

  7. 7}

  8. 8};

  9. 9

  10. 10a+;// "42"

  11. 11String(a);// "6"

不过大部分情况下也不会写这么奇怪的代码,如果你真的要扩展valueOf()或者toString()方法的话,请留意一下,因为你可能无意间影响了强制类型转换的结果。 那么从字符串转换为数字呢?请看下面的例子:

  1. 1vara=2.718;

  2. 2varb=a-0;

  3. 3

  4. 4b;// 2.718

由于-操作符不像+操作符有重载,-只能进行数字减法操作,因此如果操作数不是数字的话会被强制转换为数字。当然,a*1a/1也可以,因为这两个运算符也只能用于数字。 把-用于对象会怎么样呢?比如:

  1. 1vara=[3];

  2. 2varb=[1];

  3. 3

  4. 4a-b;// 2

-只能执行数字减法,因此会对操作数进行强制类型转换为数字,根据前面所述的步骤,数组会调用其toString()方法获得字符串,然后再转换为数字。

4.2 布尔值到数字的隐式强制类型转换

假设现在你要实现这么一个函数,在它的三个参数中,如果有且只有一个参数为真值则返回true,否则返回false,你该怎么写? 简单一点的写法:

  1. 1functiononlyOne(x,y,z){

  2. 2return!!((x&&!y&&!z)||(!x&&y&&!z)||(!x&&!y&&z));

  3. 3}

  4. 4

  5. 5onlyOne(true,false,false);// true

  6. 6onlyOne(true,true,false);// false

  7. 7onlyOne(false,false,true);// true

三个参数的时候代码好像也不是很复杂,那如果是 20 个呢?这么写肯定过于繁琐了。我们可以用强制类型转换来简化代码:

  1. 1functiononlyOne(...args){

  2. 2return(

  3. 3args.reduce(

  4. 4(accumulator,currentValue)=>accumulator+!!currentValue,

  5. 50

  6. 6)===1

  7. 7);

  8. 8}

  9. 9

  10. 10onlyOne(true,false,false,false);// true

  11. 11onlyOne(true,true,false,false);// false

  12. 12onlyOne(false,false,false,true);// true

在上面这个改良版的函数中,我们使用了数组的reduce()方法来计算所有参数中真值的数量,先使用隐式强制类型转换把参数转换成true或者false,再通过+运算符将true或者false隐式强制类型转换成1或者0,最后的结果就是参数中真值的个数。 通过这种改良版的代码,我们可以很简单的写出onlyTwo()onlyThree()的函数,只需要改一个数字就好了。这无疑是一个很大的提升。

4.3 隐式强制类型转换为布尔值

在以下情况中会发生隐式强制类型转换:

  • if(...)语句中的条件判断表达式;

  • for(..;..;..)语句中的条件判断表达式,也就是第二个;

  • while(..)do..while(..)循环中的条件判断表达式;

  • ..?..:..三元表达式中的条件判断表达式,也就是第一个;

  • 逻辑或||和逻辑与&&左边的操作数,作为条件判断表达式。

在这些情况下,非布尔值会通过上述的ToBoolean抽象操作被隐式强制类型转换为布尔值。

4.4||&&

JavaScript 中的逻辑或和逻辑与运算符和其他语言中的不太一样。在别的语言中,其返回值类型是布尔值,然而在 JavaScript 中返回值是两个操作数之一。因此在 JavaScript 中,||&&被称作选择器运算符可能更合适。

根据 ES5 规范11.11节:

||&&运算符的返回值不一定是布尔值,而是两个操作数中的其中一个。

比如:

  1. 1vara=21;

  2. 2varb=xyz;

  3. 3varc=null;

  4. 4

  5. 5a||b;// 21

  6. 6a&&b;// "xyz"

  7. 7

  8. 8c||b;// "xyz"

  9. 9c&&b;// null

如果||或者&&左边的操作数不是布尔值类型的话,则会对左边的操作数进行ToBoolean操作,根据结果返回运算符左边或者右边的操作数。 对于||来说,左边操作数的强制类型转换结果如果为true则返回运算符左边的操作数,如果是false则返回运算符右边的操作数。 对于&&来说则刚好相反,左边的操作数强制类型转换结果如果为true则返回运算符右边的操作数,如果是false则返回运算符左边的操作数。||&&返回的是两个操作数之一,而非布尔值。 在 ES6 的函数默认参数出现之前,我们经常会看到这样的代码:

  1. 1functionfoo(x,y){

  2. 2x=x||x;

  3. 3y=y||y;

  4. 4

  5. 5console.log(x++y);

  6. 6}

  7. 7

  8. 8foo();// "x y"

  9. 9foo(hello);// "hello y"

看起来和我们预想的一致。但是,如果是这样调用呢?

  1. 1foo(hello world,);// ???

上面的执行结果是hello world y,为什么?

在执行到y=y||"y"的时候, JavaScript 对运算符左边的操作数进行了布尔隐式强制类型转换,其结果为false,因此运算结果为运算符右边的操作数,即"y",因此最后打印出来到日志是"hello world y"而非我们预想的hello world。 所以这种方式需要确保传入的参数不能有假值,否则就可能和我们预想的不一致。如果参数中可能存在假值,则应该有更加明确的判断。 如果你看过压缩工具处理后的代码的话,你可能经常会看到这样的代码:

  1. 1functionfoo(){

  2. 2// 一些代码

  3. 3}

  4. 4

  5. 5vara=21;

  6. 6

  7. 7a&&foo();// a 为假值时不会执行 foo()

这时候&&就被称为守护运算符(guard operator),即&&左边的条件判断表达式结果如果不是true则会自动终止,不会判断操作符右边的表达式。

所以在if或者for语句中我们使用||&&的时候,if或者for语句会先对||&&操作符返回的值进行布尔隐式强制类型转换,再根据转换结果来判断。 比如:

  1. 1vara=21;

  2. 2varb=null;

  3. 3varc=hello;

  4. 4

  5. 5if(a&&(b||c)){

  6. 6console.log(hi);

  7. 7}

在这段代码中,a&&(b||c)的结果实际是hello而非true,然后if再通过隐式类型转换为true才执行console.log(hi)

4.5Symbol的强制类型转换

ES6 中引入了新的基本数据类型 ——Symbol。然而它的强制类型转换有些不一样,它支持显式强制类型转换,但是不支持隐式强制类型转换。 比如:

  1. 1vars=Symbol(hi);

  2. 2

  3. 3String(s);// Symbol(hi)

  4. 4s+;// Uncaught TypeError: Cannot convert a Symbol value to a string

而且Symbol不能强制转换为数字,比如:

  1. 1vars=Symbol(hi);

  2. 2

  3. 3s-0;// Uncaught TypeError: Cannot convert a Symbol value to a number

Symbol的布尔强制类型转换都是true

声明:本文部分素材转载自互联网,如有侵权立即删除 。

© 版权声明
THE END
喜欢就支持一下吧
点赞0赞赏 分享
相关推荐
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容