做为 JavaScript 程序员,你必定获取过当前系统的时间戳。在 ES5 引入 Date.now()
静态方法以前,下面这段代码你必定不会陌生:javascript
var timestamp = +new Date(); // timestamp 就是当前的系统时间戳,单位是 ms
你确定据说过 JavaScript 的强制类型转换,你能指出这段代码里哪里用到了强制类型转换吗?java
几乎全部 JavaScript 程序员都接触过强制类型转换 —— 不管是有意的仍是无心的。强制类型转换致使了不少隐蔽的 BUG,可是强制类型转换同时也是一种很是有用的技术,咱们不该该因噎废食。程序员
在本文中咱们来详细探讨一下 JavaScript 的强制类型转换,以便咱们能够在避免踩坑的状况下最大化利用强制类型转换的便捷。数组
类型转换发生在静态类型语言的编译阶段,而强制类型转换发生在动态类型语言的运行时(runtime),所以在 JavaScript 中只有强制类型转换。浏览器
强制类型转换通常还可分为 隐式强制类型转换(implicit coercion)和 _显式强制类型转换(explicit coercion)_。函数
从代码中能够看出转换操做是隐式的仍是显式的,显式强制类型转换很容易就能看出来,而隐式强制类型转换可能就没有这么明显了。工具
好比:this
var a = 21; var b = a + ''; var c = String(a);
对于变量 b
而言,这次强制类型转换是隐式的。+
操做符在其中一个操做数是字符串时进行的是字符串拼接操做,所以数字 21
会被转换为相应的字符串 "21"
。prototype
然而 String(21)
则是很是典型的显式强制类型转换。日志
这两种强制转换类型的操做都是将数字转换为字符串。
不过“显式”仍是“隐式”都是相对而言的。好比若是你知道 a + ""
是怎么回事,那么对你来讲这可能就是“显式”的。反之,若是你不知道 String(a)
能够用来字符串强制类型转换,那么它对你来讲可能就是“隐式”的。
在介绍强制类型转换以前,咱们须要先了解一下字符串、数字和布尔值之间类型转换的基本规则。在 ES5 规范中定义了一些“抽象操做”和转换规则,在这咱们介绍一下 ToPrimitive
、ToString
、ToNumber
和 ToBoolean
。注意,这些操做仅供引擎内部使用,和平时 JavaScript 代码中的 .toString()
等操做不同。
你能够将 ToPrimitive
操做看做是一个函数,它接受一个 input
参数和一个可选的 PreferredType
参数。ToPrimitive
抽象操做会将 input
参数转换成一个原始值。若是一个对象能够转换成不止一种原始值,可使用 PreferredType
指定抽象操做的返回类型。
根据不一样的输入类型,ToPrimitive
的转换操做以下:
输入类型 | 操做 / 返回值 |
---|---|
Undefined | 自身(无转换操做) |
Null | 自身(无转换操做) |
Boolean | 自身(无转换操做) |
Number | 自身(无转换操做) |
String | 自身(无转换操做) |
Object | 返回 Object 的 default value 。Object 的 default value 经过在该对象上传递 PreferredType 参数给内部操做 [[DefaultValue]](hint) 得到。[[DefaultValue]](hint) 的实现请往下看。 |
[[DefaultValue]](hint)
内部操做在对象 O
上调用内部操做 [[DefaultValue]]
时,根据 hint
的不一样,其执行的操做也不一样,简化版(具体可参考 ES5 规范 8.12.8 节)以下:
若是
hint
是String
;
若是
O
的toString
属性是函数;
- 将
O
设置为this
值并调用toString
方法,将返回值赋值给val
;- 若是
val
是原始值类型则返回;若是
O
的valueOf
属性是函数;
- 将
O
设置为this
值并调用valueOf
方法,将返回值赋值给val
;- 若是
val
是原始值类型则返回;- 抛出
TypeError
错误。若是
hint
是Number
;
若是
O
的valueOf
属性是函数;
- 将
O
设置为this
值并调用valueOf
方法,将返回值赋值给val
;- 若是
val
是原始值类型则返回;若是
O
的toString
属性是函数;
- 将
O
设置为this
值并调用toString
方法,将返回值赋值给val
;- 若是
val
是原始值类型则返回;- 抛出
TypeError
错误。若是
hint
参数为空;
- 若是
O
是Date
对象,则和hint
为String
时一致;- 不然和
hint
为Number
时一致。
原始值的字符串化的规则以下:
null
转化为 "null"
;undefined
转化为 "undefined"
;true
转化为 "true"
;false
转化为 "false"
;数字的字符串化遵循通用规则,如 21
转化为 "21"
,极大或者极小的数字使用指数形式,如:
var num = 3.912 * Math.pow(10, 50); num.toString(); // "3.912e50"
对于普通对象,若是对象有自定义的 toString()
方法,字符串化时就会调用该自定义方法并使用其返回值,不然返回的是内部属性 [[Class]]
的值,好比 "object [Object]"
。须要注意的是,数组默认的 toString()
方法通过了从新定义,其会将全部元素字符串化以后再用 ","
链接起来,如:
var arr = [1, 2, 3]; arr.toString(); // "1,2,3"
在 ES5 规范中定义的 ToNumber
操做能够将非数字值转换为数字。其规则以下:
true
转换为 1
;false
转换为 0
;undefined
转换为 NaN
;null
转换为 0
;NaN
。在将某个值转换为原始值的时候,会首先执行抽象操做 ToPrimitive
,若是结果是数字则直接返回,若是是字符串再根据相应规则转换为数字。
参照上述规则,如今咱们能够一步一步来解释本文开头的那行代码了。
var timestamp = +new Date(); // timestamp 就是当前的系统时间戳,单位是 ms
其执行步骤以下:
new
操做符比 +
操做符优先级更高,所以先执行 new Date()
操做,生成一个新的 Date
实例;一元操做符 +
在其操做数为非数字时,会对其进行隐式强制类型转换为数字:
hint
是 Number
;
Date
实例的 valueOf
属性指向的是 Date.prototype.valueOf
,是一个函数;this
指向 Date
实例并调用 valueOf
函数,得到返回值;timestamp
变量。有了以上知识,咱们就能够实现一些比较好玩的东西了,好比将数字和对象相加:
var a = { valueOf: function() { return 18; } }; var b = 20; +a; // 18 Number(a); // 18 a + b; // 38 a - b; // -2
顺带提一下,从 ES5 开始,使用 Object.create(null)
建立的对象,其 [[Prototype]]
属性为 null
所以没有 valueOf()
和 toString()
方法,所以没法进行强制类型转换。请看以下示例:
var a = {}; var b = Object.create(null); +a; // NaN +b; // Uncaught TypeError: Cannot convert object to primitive value a + ''; // "[object Object]" b + ''; // Uncaught TypeError: Cannot convert object to primitive value
JavaScript 中有两个关键字 true
和 false
,分别表示布尔类型的真和假。咱们常常会在 if
语句中将 0
做为假值条件,1
做为真值条件,这也利用了强制类型转换。咱们能够将 true
强制类型转换为 1
,false
强制类型转换为 0
,反之亦然。然而 true
和 1
并非一回事,false
和 0
也同样。
在 JavaScript 中值能够分为两类:
false
的值true
的值)在 ES5 规范中下列值被定义为假值:
undefined
null
false
+0
、-0
和 NaN
""
假值的布尔强制类型转换结果为 false
。
在假值列表之外的值都是真值。
规则不免有例外。刚说了除了假值列表之外的全部其余值都是真值,然而你能够在现代浏览器的控制台中执行下面几行代码试试:
Boolean(document.all); typeof document.all;
获得的结果应该是 false
和 "undefined"
。然而若是你直接执行 document.all
获得的是一个类数组对象,包含了页面中全部的元素。document.all
实际上不能算是 JavaScript 语言的范畴,这是浏览器在特定条件下建立一些外来(exotic)值,这些就是“假值对象”。
假值对象看起来和普通对象并没有二致(都有属性,document.all
甚至能够展为数组),可是其强制类型转换的结果倒是 false
。
在 ES5 规范中,document.all
是惟一一个例外,其缘由主要是为了兼容性。由于老代码可能会这么判断是不是 IE:
if (document.all) { // Internet Explorer }
在老版本的 IE 中,document.all
是一个对象,其强制类型转换结果为 true
,而在现代浏览器中,其强制转换结果为 false
。
除了假值之外都是真值。
好比:
var a = 'false'; var b = '0'; var c = "''"; var d = Boolean(a && b && c); d; // ?
d
是 true
仍是 false
呢?
答案是 true
。这些值都是真值,相信不须要过多分析。
一样,如下几个值同样都是真值:
var a = []; var b = {}; var c = function() {};
显式强制类型转换很是常见,也不会有什么坑,JavaScript 中的显式类型转换和静态语言中的很类似。
字符串和数字之间的相互转换靠 String()
和 Number()
这两个内建函数实现。注意在调用时没有 new
关键字,只是普通函数调用,不会建立一个新的封建对象。
var a = 21; var b = '2.71828'; var c = String(a); var d = Number(b); c; // "21" d; // 2.71828
除了直接调用 String()
或者 Number()
方法以外,还能够经过别的方式显式地进行数字和字符串之间的相互转换:
var a = 21; var b = '2.71828'; var c = a.toString(); var d = +b; c; // "21" d; // 2.71828
虽然 a.toString()
看起来很像显式的,然而其中涉及了隐式转换,由于 21
这样的原始值是没有方法的,JavaScript 自动建立了一个封装对象,并调用了其 toString()
方法。
+b
中的 +
是一元运算符,+
运算符会将其操做数转换为数字。而 +b
是显式仍是隐式就取决于开发者自身了,本文以前也提到过,显式仍是隐式都是相对的。
和字符串与数字之间的相互转换同样,Boolean()
能够将参数显示强制转换为布尔值:
var a = ''; var b = 0; var c = null; var d = undefined; var e = '0'; var f = []; var g = {}; Boolean(a); // false Boolean(b); // false Boolean(c); // false Boolean(d); // false Boolean(e); // true Boolean(f); // true Boolean(g); // true
不过咱们不多会在代码中直接用 Boolean()
函数,更常见的是用 !!
来强制转换为布尔值,由于第一个 !
会将操做数强制转换为布尔值,并反转(真值反转为假值,假值反转为真值),而第二个 !
会将结果反转回原值:
var a = ''; var b = 0; var c = null; var d = undefined; var e = '0'; var f = []; var g = {}; !!a; // false !!b; // false !!c; // false !!d; // false !!e; // true !!f; // true !!g; // true
不过更常见的状况是相似 if(...) {}
这样的代码,在这个上下文中,若是咱们没有使用 Boolean()
或者 !!
转换,就会自动隐式地进行 ToBoolean
转换。
三元运算符也是一个很常见的布尔隐式强制类型转换的例子:
var a = 21; var b = 'hello'; var c = false; var d = a ? b : c; d; // "hello"
在执行三元运算的时候,先对 a
进行布尔强制类型转换,而后根据结果返回 :
先后的值。
大部分被诟病的强制类型转换都是隐式强制类型转换。可是隐式强制类型转换真的一无可取吗?并不必定,引擎在必定程度上简化了强制类型转换的步骤,这对于有些状况来讲并非好事,而对于另外一些状况来讲可能并不必定是坏事。
在上一节咱们已经介绍了字符串和数字之间的显式强制类型转换,在这一节咱们来讲说他们二者之间的隐式强制类型转换。
+
运算符既能够用做数字之间的相加也能够经过重载用于字符串拼接。咱们可能以为若是 +
运算符两边的操做数有一个或以上是字符串就会进行字符串拼接。这种想法并不彻底错误,但也不是彻底正确的。好比如下代码能够验证这句话是正确的:
var a = 21; var b = 4; var c = '21'; var d = '4'; a + b; // 25 c + d; // "214"
可是若是 +
运算符两边的操做数不是字符串呢?
var arr0 = [1, 2]; var arr1 = [3, 4]; arr0 + arr1; // ???
上面这条命令的执行结果是 "1,23,4"
。a
和 b
都不是字符串,为何 JavaScript 会把 a
和 b
都转换为字符串再进行拼接?
根据 ES5 规范 11.6.1 节,若是 +
两边的操做数中,有一个操做数是字符串或者能够经过如下步骤转换为字符串,+
运算符将进行字符串拼接操做:
- 若是一个操做数为对象,则对其调用
ToPrimitive
抽象操做;ToPrimitive
抽象操做会调用[[DefaultValue]](hint)
,其中hint
为Number
。
这个操做和上面所述的 ToNumber
操做一致,再也不重复。
在这个操做中,JavaScript 引擎对其进行 ToPrimitive
抽象操做的时候,先执行 valueOf()
方法,可是因为其 valueOf()
方法返回的是数组,没法获得原始值,转而调用 toString()
方法,toString()
方法返回了以 ,
拼接的全部元素的字符串,即 1,2
和 3,4
,+
运算符再进行字符串拼接,获得结果 1,23,4
。
简单来讲,只要 +
的操做数中有一个是字符串,或者能够经过上述步骤获得字符串,就进行字符串拼接操做;其他状况执行数字加法。
因此如下这段代码可谓随处可见:
var a = 21; a + ''; // "21"
利用隐式强制类型转换将非字符串转换为字符串,这样转换很是方便。不过经过 a + ""
和直接调用 String(a)
之间并非彻底同样,有些细微的差异须要注意一下。a + ""
会对 a
调用 valueOf()
方法,而后再经过上述的 ToString
抽象操做转换为字符串。而 String(a)
则会直接调用 toString()
。
虽然返回值都是字符串,然而若是 a
是对象的话,结果可能出乎意料!
好比:
var a = { valueOf: function() { return '21'; }, toString: function() { return '6'; } }; a + ''; // "42" String(a); // "6"
不过大部分状况下也不会写这么奇怪的代码,若是你真的要扩展 valueOf()
或者 toString()
方法的话,请留意一下,由于你可能无心间影响了强制类型转换的结果。
那么从字符串转换为数字呢?请看下面的例子:
var a = '2.718'; var b = a - 0; b; // 2.718
因为 -
操做符不像 +
操做符有重载,-
只能进行数字减法操做,所以若是操做数不是数字的话会被强制转换为数字。固然,a * 1
和 a / 1
也能够,由于这两个运算符也只能用于数字。
把 -
用于对象会怎么样呢?好比:
var a = [3]; var b = [1]; a - b; // 2
-
只能执行数字减法,所以会对操做数进行强制类型转换为数字,根据前面所述的步骤,数组会调用其 toString()
方法得到字符串,而后再转换为数字。
假设如今你要实现这么一个函数,在它的三个参数中,若是有且只有一个参数为真值则返回 true
,不然返回 false
,你该怎么写?
简单一点的写法:
function onlyOne(x, y, z) { return !!((x && !y && !z) || (!x && y && !z) || (!x && !y && z)); } onlyOne(true, false, false); // true onlyOne(true, true, false); // false onlyOne(false, false, true); // true
三个参数的时候代码好像也不是很复杂,那若是是 20 个呢?这么写确定过于繁琐了。咱们能够用强制类型转换来简化代码:
function onlyOne(...args) { return ( args.reduce( (accumulator, currentValue) => accumulator + !!currentValue, 0 ) === 1 ); } onlyOne(true, false, false, false); // true onlyOne(true, true, false, false); // false onlyOne(false, false, false, true); // true
在上面这个改良版的函数中,咱们使用了数组的 reduce()
方法来计算全部参数中真值的数量,先使用隐式强制类型转换把参数转换成 true
或者 false
,再经过 +
运算符将 true
或者 false
隐式强制类型转换成 1
或者 0
,最后的结果就是参数中真值的个数。
经过这种改良版的代码,咱们能够很简单的写出 onlyTwo()
、onlyThree()
的函数,只须要改一个数字就行了。这无疑是一个很大的提高。
在如下状况中会发生隐式强制类型转换:
if (...)
语句中的条件判断表达式;for (..; ..; ..)
语句中的条件判断表达式,也就是第二个;while (..)
和 do..while(..)
循环中的条件判断表达式;.. ? .. : ..
三元表达式中的条件判断表达式,也就是第一个;||
和逻辑与 &&
左边的操做数,做为条件判断表达式。在这些状况下,非布尔值会经过上述的 ToBoolean
抽象操做被隐式强制类型转换为布尔值。
||
和 &&
JavaScript 中的逻辑或和逻辑与运算符和其余语言中的不太同样。在别的语言中,其返回值类型是布尔值,然而在 JavaScript 中返回值是两个操做数之一。所以在 JavaScript 中,||
和 &&
被称做选择器运算符可能更合适。
根据 ES5 规范 11.11 节:
||
和&&
运算符的返回值不必定是布尔值,而是两个操做数中的其中一个。
好比:
var a = 21; var b = 'xyz'; var c = null; a || b; // 21 a && b; // "xyz" c || b; // "xyz" c && b; // null
若是 ||
或者 &&
左边的操做数不是布尔值类型的话,则会对左边的操做数进行 ToBoolean
操做,根据结果返回运算符左边或者右边的操做数。
对于 ||
来讲,左边操做数的强制类型转换结果若是为 true
则返回运算符左边的操做数,若是是 false
则返回运算符右边的操做数。
对于 &&
来讲则恰好相反,左边的操做数强制类型转换结果若是为 true
则返回运算符右边的操做数,若是是 false
则返回运算符左边的操做数。
||
和 &&
返回的是两个操做数之一,而非布尔值。
在 ES6 的函数默认参数出现以前,咱们常常会看到这样的代码:
function foo(x, y) { x = x || 'x'; y = y || 'y'; console.log(x + ' ' + y); } foo(); // "x y" foo('hello'); // "hello y"
看起来和咱们预想的一致。可是,若是是这样调用呢?
foo('hello world', ''); // ???
上面的执行结果是 hello world y
,为何?
在执行到 y = y || "y"
的时候,JavaScript 对运算符左边的操做数进行了布尔隐式强制类型转换,其结果为 false
,所以运算结果为运算符右边的操做数,即 "y"
,所以最后打印出来到日志是 "hello world y"
而非咱们预想的 hello world
。
因此这种方式须要确保传入的参数不能有假值,不然就可能和咱们预想的不一致。若是参数中可能存在假值,则应该有更加明确的判断。
若是你看过压缩工具处理后的代码的话,你可能常常会看到这样的代码:
function foo() { // 一些代码 } var a = 21; a && foo(); // a 为假值时不会执行 foo()
这时候 &&
就被称为守护运算符(guard operator),即 &&
左边的条件判断表达式结果若是不是 true
则会自动终止,不会判断操做符右边的表达式。
因此在 if
或者 for
语句中咱们使用 ||
和 &&
的时候,if
或者 for
语句会先对 ||
和 &&
操做符返回的值进行布尔隐式强制类型转换,再根据转换结果来判断。
好比:
var a = 21; var b = null; var c = 'hello'; if (a && (b || c)) { console.log('hi'); }
在这段代码中,a && (b || c)
的结果实际是 'hello'
而非 true
,而后 if
再经过隐式类型转换为 true
才执行 console.log('hi')
。
Symbol
的强制类型转换ES6 中引入了新的基本数据类型 —— Symbol
。然而它的强制类型转换有些不同,它支持显式强制类型转换,可是不支持隐式强制类型转换。
好比:
var s = Symbol('hi'); String(s); // 'Symbol(hi)' s + ''; // Uncaught TypeError: Cannot convert a Symbol value to a string
并且 Symbol
不能强制转换为数字,好比:
var s = Symbol('hi'); s - 0; // Uncaught TypeError: Cannot convert a Symbol value to a number
Symbol
的布尔强制类型转换都是 true
。