摘要:强制类型转换作为程序员,你一定获取过当前系统的时间戳。比如对于变量而言,此次强制类型转换是隐式的。然而则是非常典型的显式强制类型转换。隐式强制类型转换大部分被诟病的强制类型转换都是隐式强制类型转换。
JavaScript 强制类型转换
作为 JavaScript 程序员,你一定获取过当前系统的时间戳。在 ES5 引入 Date.now() 静态方法之前,下面这段代码你一定不会陌生:
var timestamp = +new Date(); // timestamp 就是当前的系统时间戳,单位是 ms
你肯定听说过 JavaScript 的强制类型转换,你能指出这段代码里哪里用到了强制类型转换吗?
几乎所有 JavaScript 程序员都接触过强制类型转换 —— 不论是有意的还是无意的。强制类型转换导致了很多隐蔽的 BUG,但是强制类型转换同时也是一种非常有用的技术,我们不应该因噎废食。
在本文中我们来详细探讨一下 JavaScript 的强制类型转换,以便我们可以在避免踩坑的情况下最大化利用强制类型转换的便捷。
类型转换和强制类型转换类型转换发生在静态类型语言的编译阶段,而强制类型转换发生在动态类型语言的运行时(runtime),因此在 JavaScript 中只有强制类型转换。
强制类型转换一般还可分为 隐式强制类型转换(implicit coercion)和 _显式强制类型转换(explicit coercion)_。
从代码中可以看出转换操作是隐式的还是显式的,显式强制类型转换很容易就能看出来,而隐式强制类型转换可能就没有这么明显了。
比如:
var a = 21; var b = a + ""; var c = String(a);
对于变量 b 而言,此次强制类型转换是隐式的。+ 操作符在其中一个操作数是字符串时进行的是字符串拼接操作,因此数字 21 会被转换为相应的字符串 "21"。
然而 String(21) 则是非常典型的显式强制类型转换。
这两种强制转换类型的操作都是将数字转换为字符串。
不过“显式”还是“隐式”都是相对而言的。比如如果你知道 a + "" 是怎么回事,那么对你来说这可能就是“显式”的。反之,如果你不知道 String(a) 可以用来字符串强制类型转换,那么它对你来说可能就是“隐式”的。
抽象值操作在介绍强制类型转换之前,我们需要先了解一下字符串、数字和布尔值之间类型转换的基本规则。在 ES5 规范中定义了一些“抽象操作”和转换规则,在这我们介绍一下 ToPrimitive、ToString、ToNumber 和 ToBoolean。注意,这些操作仅供引擎内部使用,和平时 JavaScript 代码中的 .toString() 等操作不一样。
ToPrimitive你可以将 ToPrimitive 操作看作是一个函数,它接受一个 input 参数和一个可选的 PreferredType 参数。ToPrimitive 抽象操作会将 input 参数转换成一个原始值。如果一个对象可以转换成不止一种原始值,可以使用 PreferredType 指定抽象操作的返回类型。
根据不同的输入类型,ToPrimitive 的转换操作如下:
输入类型 | 操作 / 返回值 |
---|---|
Undefined | 自身(无转换操作) |
Null | 自身(无转换操作) |
Boolean | 自身(无转换操作) |
Number | 自身(无转换操作) |
String | 自身(无转换操作) |
Object | 返回 Object 的 default value。Object 的 default value 通过在该对象上传递 PreferredType 参数给内部操作 [[DefaultValue]](hint) 获得。[[DefaultValue]](hint) 的实现请往下看。 |
在对象 O 上调用内部操作 [[DefaultValue]] 时,根据 hint 的不同,其执行的操作也不同,简化版(具体可参考 ES5 规范 8.12.8 节)如下:
ToString
如果 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"ToNumber
在 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 valueToBoolean
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。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/94453.html
摘要:如果有并且返回基本类型值,再使用该值进行强制类型转换。四宽松相等和严格相等允许在相等比较中进行强制类型转换,而不允许。这时最好用来避免不经意的强制类型转换。这两个原则可以让我们避开几乎所有强制类型转换的坑。 一、类型转换规则 1.ToString 对于普通对象来说,除非自行定义toString方法,否则就会调用Object.prototype.toString()方法,如果对象有自己的...
摘要:显示的调用转换过程称为显式强制类型转换,隐式的情况称为隐式强制类型转换。隐式强制类型转换让代码变得晦涩难懂而又便捷而奇妙。事实上,允许在比较中进行强制类型转换,而不允许。如果有并且返回基本类型值,就使用该值进行强制类型转换。 JavaScript是一种非常灵活的现代编程语言,灵活到使用者极其容易被忽视的它那广阔的世界以及它带给我们的无限遐想空间。本文将对JavaScript最最基础也最...
摘要:核心点中的强制类型转换总是返回标量基本类型值。数字化对象在强制转换为数字的时候,会优先调用方法,如果返回基本类型的值,则直接使用该返回值如果返回值不是基本类型,则会继续调用方法,如果返回基本类型的值,则直接使用该返回值,否则报错。 最近在读《你不知道的javascript》系列图书,收获蛮大,感慨也挺多的。是的,关于javascript,你不是不知道,而是真的不知道。?就比如类型转换,...
摘要:拆封想要等到封装对象中基本类型值,我们可以使用方法获取。值类型转换上面两种方式,第一种我们称为显示强制类型转换第二种称之为隐式强制类型转换。介绍强制与隐式类型转换时,我们需要掌握对字符串数字和布尔类型的转换规则。 前面两章介绍了几大数据类型以及值类型,接下来的这个知识点,我觉得它对于javascript程序员来说是很重要的, 认识封装对象 在开始之前,我们先看一个例子,以便之后更轻松的...
摘要:第三章强制类型转换强制类型转换将其他数据类型转换成类型将其他数据类型转换为强制类型转换指将一个数据类型强制转换为其他数据类型类型转换主要是指将其他数据类型转换为将其他数据类型转换为方式一调用被转换数据的方法该方法不会影响到原变量但是注 第三章强制类型转换 Cast type:强制类型转换 将其他数据类...
阅读 2524·2023-04-25 19:47
阅读 3347·2019-08-29 17:18
阅读 835·2019-08-29 15:26
阅读 3344·2019-08-29 14:17
阅读 1050·2019-08-26 13:49
阅读 3282·2019-08-26 13:22
阅读 2984·2019-08-26 10:44
阅读 2678·2019-08-23 16:51