原标题:JavaScript 强制类型转换

祝鑫奔

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

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

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

1 var timestamp = +new Date(); // timestamp 就是当前的系统时间戳,单位是 ms

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

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

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

比如:

1 var a = 21;

2

3 var b = a + '';

4

5 var c = String(a);

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

二、抽象值操作

在介绍强制类型转换之前,我们需要先了解一下字符串、数字和布尔值之间类型转换的基本规则。在 ES5 规范中定义了一些“抽象操作”和转换规则,在这我们介绍一下 ToPrimitive、 ToString、 ToNumber和 ToBoolean。注意,这些操作仅供引擎内部使用,和平时 Java 代码中的 .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 var num = 3.912 * Math.pow(10, 50);

2

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

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

1 var arr = [1, 2, 3];

2

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

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

true 转换为 1;

false 转换为 0;

undefined 转换为 NaN;

null 转换为 0;

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

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

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

1 var timestamp = +new Date(); // timestamp 就是当前的系统时间戳,单位是 ms

其执行步骤如下:

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

1 var a = {

2 valueOf: function() {

3 return 18;

4 }

5 };

6

7 var b = 20;

8

9 +a; // 18

10 Number(a); // 18

11 a + b; // 38

12 a - b; // -2

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

1 var a = {};

2 var b = Object.create(null);

3

4 +a; // NaN

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

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

7 b + ''; // Uncaught TypeError: Cannot convert object to primitive value2.4 ToBoolean

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

2.4.1 假值

在 Java 中值可以分为两类:

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

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

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

undefined

null

false

+0、 -0 和 NaN

""

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

2.4.2 例外

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

1 Boolean(document.all);

2 typeof document.all;

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

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

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

1 if (document.all) {

2 // Internet Explorer

3 }

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

2.4.3 真值

除了假值以外都是真值。

比如:

1 var a = 'false';

2 var b = '0';

3 var c = "''";

4

5 var d = Boolean(a && b && c);

6

7 d; // ?

d是 true还是 false呢?

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

1 var a = [];

2 var b = {};

3 var c = function() {};

三、显式强制类型转换

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

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

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

1 var a = 21;

2 var b = '2.71828';

3

4 var c = String(a);

5 var d = Number(b);

6

7 c; // "21"

8 d; // 2.71828

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

1 var a = 21;

2 var b = '2.71828';

3

4 var c = a.toString();

5 var d = +b;

6

7 c; // "21"

8 d; // 2.71828

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

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

3.2 显式转换为布尔值

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

1 var a = '';

2 var b = 0;

3 var c = null;

4 var d = undefined;

5

6 var e = '0';

7 var f = [];

8 var g = {};

9

10 Boolean(a); // false

11 Boolean(b); // false

12 Boolean(c); // false

13 Boolean(d); // false

14

15 Boolean(e); // true

16 Boolean(f); // true

17 Boolean(g); // true

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

1 var a = '';

2 var b = 0;

3 var c = null;

4 var d = undefined;

5

6 var e = '0';

7 var f = [];

8 var g = {};

9

10 !!a; // false

11 !!b; // false

12 !!c; // false

13 !!d; // false

14

15 !!e; // true

16 !!f; // true

17 !!g; // true

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

1 var a = 21;

2 var b = 'hello';

3 var c = false;

4

5 var d = a ? b : c;

6

7 d; // "hello"

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

四、隐式强制类型转换

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

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

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

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

1 var a = 21;

2 var b = 4;

3

4 var c = '21';

5 var d = '4';

6

7 a + b; // 25

8 c + d; // "214"

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

1 var arr0 = [1, 2];

2 var arr1 = [3, 4];

3

4 arr0 + arr1; // ???

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

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

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

ToPrimitive 抽象操作会调用 [[DefaultValue]](hint),其中 hint 为 Number。

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

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

1 var a = 21;

2

3 a + ''; // "21"

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

比如:

1 var a = {

2 valueOf: function() {

3 return '21';

4 },

5 toString: function() {

6 return '6';

7 }

8 };

9

10 a + ''; // "42"

11 String(a); // "6"

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

1 var a = '2.718';

2 var b = a - 0;

3

4 b; // 2.718

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

1 var a = [3];

2 var b = [1];

3

4 a - b; // 2

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

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

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

1 function onlyOne(x, y, z) {

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

3 }

4

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

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

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

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

1 function onlyOne(...args) {

2 return (

3 args.reduce(

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

5 0

6 ) === 1

7 );

8 }

9

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

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

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

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

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

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

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

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

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

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

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

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

4.4 ||和 &&

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

根据 ES5 规范11.11节:

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

比如:

1 var a = 21;

2 var b = 'xyz';

3 var c = null;

4

5 a || b; // 21

6 a && b; // "xyz"

7

8 c || b; // "xyz"

9 c && b; // null

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

1 function foo(x, y) {

2 x = x || 'x';

3 y = y || 'y';

4

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

6 }

7

8 foo(); // "x y"

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

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

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

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

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

1 function foo() {

2 // 一些代码

3 }

4

5 var a = 21;

6

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

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

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

1 var a = 21;

2 var b = null;

3 var c = 'hello';

4

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

6 console.log('hi');

7 }

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

4.5 Symbol的强制类型转换

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

1 var s = Symbol('hi');

2

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

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

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

1 var s = Symbol('hi');

2

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

Symbol的布尔强制类型转换都是 true。返回搜狐,查看更多

责任编辑:

java toprimitive_JavaScript 强制类型转换相关推荐

  1. java中的强制类型转换注意事项_浅谈Java中强制类型转换的问题

    为了更好的理解我们先看下面的例子: package com.yonyou.test; import java.util.ArrayList; import java.util.Iterator; im ...

  2. JAVA语言强制类型转换要求

    JAVA语言强制类型转换要求 数据类型具有高低性的 顺序由低到高为 byte->short->char->int->long->float->double 1.由低 ...

  3. Java数组 强制类型转换

    数组的强制类型转换 数组的强制类型转换 数组类型转换的问题为什么会出现在我脑海中? 数组的强制类型转换 最重要的是!!!最开始的时候声明的数组类型!!! 最重要的是!!!最开始的时候声明的数组类型!! ...

  4. Java的强制类型转换

    类型转换 Java 作为一个强类型编程语言, 当不同类型之间的变量相互赋值的时候, 会有教严格的校验 int a = 10; long b = 100L; b = a; // 可以通过编译 a = b ...

  5. Java之强制类型转换

    在之前的文章中介绍过,将一个类型强制转换为另一个数据类型的过程称为强制类型转换.本文即将介绍的是继承过程中发生的引用类型转换. 引用类型转换是指对象引用的强制类型转换,在了解对象引用的强制类型转换之前 ...

  6. 【Java】强制类型转换

    文章目录 1. 基本概念 2. 应用场景 3. 使用风险 4. 使用规则 5. 训练 1. 基本概念 强制类型转换,可以理解为自动类型转换的逆过程.是将大容量的数据类型转换为小容量的数据类型. 使用时 ...

  7. java多态强制类型转换_java多态和强制类型转换

    子类可以赋值给超类,称之为向上转型,这个是自动的. 超类不可以赋值给子类,这个是向下转型,需要我们手动实现. 赋值给超类的子类引用在运行期间将表现出不同的特性,这就是多态. 小类型    可转换为   ...

  8. 4、Java概述——强制类型转换

    一.什么是强制类型转换 当进行数据的大小从大转换到小的时候,我们就需要使用到强制类型转换. 二.强制类型转换的条件是什么 使用强制类型转换的关键点在于从数据类型的大范围转换到数据类型的小范围,例如我们 ...

  9. java object强制类型转换_JAVA 强制类型转换

    object对象转换为String的一些总结 在 java项目的实际开发和应用中,常常需要用到将对象转为String这一基本功能.本文将对常用的转换方法进行一个总结.常用的方法有 Object.toS ...

最新文章

  1. 揭秘:深度网络背后的数学奥秘
  2. MODE —— 计算10个分数的平均值(知识点: 数组 变长数组)
  3. 【laravel】laravel的基础学习笔记
  4. oracle 并行提交,如何配置Oracle并行处理(上)
  5. node npm nrm nvm gnvm 相爱相杀
  6. abaqus实例_使用Python在ABAQUS中创建XYData数据
  7. 汉游天下公司的一些感悟
  8. 【HDU5187】contest
  9. 你真的懂ArrayList吗?说说foreach与iterator时remove的区别
  10. android快速点击分析
  11. 暑假周进度总结报告3
  12. eclipse怎么修改java的行高_eclipse皮肤怎么修改 eclipse皮肤修改教程
  13. 做一个计算器_如何设计一个JavaScript插件系统,编程思维比死磕API更重要
  14. Typora 中文字体深度修改
  15. IPv4 + IPv6 = IPv10?是的,IPv10就是IPV4 + IPv6!
  16. Android手电筒案例
  17. 程序员如何避免「温水煮码农」
  18. 微信公众平台开发培训
  19. 用一个uchar 类型表示八个通道的状态
  20. 计算机中丢失swr.dll,win10电脑中模块initpki.dll加载失败提示0x80004005错误代码如何解决...

热门文章

  1. 操作系统---文件的空闲管理
  2. 塑胶模具费用计算与报价视频教程
  3. Secure CRT串口工具log打印时间点
  4. 【idea编译错误】IDE 编译报错: Dangling metacharacter
  5. 有效ip地址_本地连接没有有效IP配置,可尝试这3种方法,轻松解决此问题
  6. 【JavaScript】js对象进行排序(对象转数组,对象转对象)
  7. php防止sql注入代码
  8. 物化旁流综合水处理器
  9. IT行业紧缺职位引领高薪行情
  10. 引入阿里图标库(iconfont)后图标黑白问题