该专栏总结自《你不知道的JavaScript》丛书的知识,易错易混点。
类型装换
类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型怨言的运行时。在js中统称为强制类型转换。
抽象值操作
ToString
:他负责处理非字符串到字符串的强制类型转换。
- 如果转化的是极大极小值时,转化后的字符串是指数形式。
const a = 100000000000000000000000000000000000000000000; console.log(a.toString()) //1e+44
- 对于普通对象来说,除非自定义toString方法,否则都调用
Object.prototype.toString()
,然后返回"[object Object]"
- JSON字符串化在对象中遇到undefined, function, symbol时会自动将其忽略,在数组中则会返回null,用来保证数组中元素对应的位置不变。并且对包含循环引用的对象JSON字符串化时报错。
console.log(JSON.stringify([1, undefined, function() {}, 4])) //[1,null,null,4] console.log(JSON.stringify({ name: "zh", age: undefined, foo: function() {} })) // {"name":"zh"}
如果对象中定义了toJSON方法,JSON字符串化时会首先调用该方法,然后用他的返回值来进行序列化。
ToNumber
: 将非非数字值转化为数字值。
- 注意这两个值的转换。undefined转换为NaN, null转换为0。
console.log(Number(undefined)) //NaN console.log(Number(NaN)) //NaN console.log(Number(null)) // 0 console.log(Number("")) // 0 console.log(Number([])) // 0 console.log(Number({})) // NaN
- 为了将值转换为相应的基本类型值,抽象操作ToPrimitive会首先检查该值是否有valueOf方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用toStrin()的返回值来进行强制类型转换。如果valueOf(), toString()均不返回基本类型值,会产生TypeError错误。
ToBoolean
: 将非boolean值转换为boolean类型。
- undefined, null, false, +0, -0, NaN, ""会被强制转化为false, 其他任何值都将被转化为true。
显示强制类型转换
显示强制类型转换是那些显而易见的类型转换。
字符串和数字之间的显示转换
- String,Number两个内建函数来完成上面的操作。并且遵守ToString,ToNumber规则。
- 还有很多方法可以实现二者之间的显示转换。
const a = 42; const b = a.toString(); // "42" +"3.14" // 3.14
- 一元运算符
+
可以将日期对象强制转换为数字。返回结果为Unix时间戳。但是一般通过Date.now()
来获取当前时间戳,使用new Date().getTime()
来获取指定时间时间戳。
console.log(+new Date())
字符串转换为数字的解析和转换
- 解析(parseInt, parseFloat)允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停止。
- 转换(Number)不允许出现非数字字符,否则会失败并返回NaN。
- 注意:parseInt,parseFloat传入的是字符串,所以我们将其他类型的数据传入后,他们会先被转换为字符串,然后再按照ToPrimitive规则来进行转化,然后再转换为数字。
console.log(parseInt([1,2,3])) // 1
上面的方法在es5之前会有一个bug,如果不指定第二个参数,那么它将根据第一个参数第一个字符来确定,将要将字符串数字转化为几进制数字。但是在es5之后,第二个参数默认为10。所以在es5之前环境下,我们需要手动指定第二个参数为10。
- parseInt解析非字符串类型。很可能会出现意想不到的结果。但是都是先将该类型计算后转换为字符串,然后再根据第二个参数来转化。
console.log(parseInt(1 / 0, 19)) // parseInt("Infinity", 19) => 18 console.log(parseInt(0.000008)) // 0 ("0" 来自于"0.000008") console.log(parseInt(0.0000008)) // 8 ("8" 来自于"8e-7" ) console.log(parseInt(false, 16)) // 250 ("fa" 来自于 "false") console.log(parseInt("0x10")) // 16 console.log(parseInt("103", 2)) // 2
显示转换为布尔值
- 通过调用Boolean,并且遵循ToBoolean规则。
- 或者通过
!!
。
隐式强制类型转换
隐式强制类型转换指的是那些隐蔽的强制类型转换,副作用也不是很明显。
数字转化为字符串
+
操作符。 如果操作符两边有一个可以转换为字符串,那么就是字符串拼接操作。如果都是数字,则为数字加法操作。
const a = [1,2]; const b = [3,4]; console.log(a + b) // 1,23,4
上面的结果就是数组通过ToPrimitive后转化为字符串,然后再进行字符串拼接。
(3) + ""
和String(3)
的差别。前者是通过ToPrimitive规则转换的,后者通过ToString规则转换的。
const a = { valueOf(){ return 3 }, toString() { return 5 } } console.log(a +''); // "3" console.log(String(a)) // "5"
字符串转换为数字
- 通过
-, *, /
等等数字运算符,即可。
- 由于算术运算符两边都是数字,所以需要先将两边操作数转换为数字。如果是对象类型需要通过ToPrimitive规则转换为字符串,然后再转化为数字。 在编辑器中输出
console.log({} + []) // [object Object] console.log([] + {}) // [object Object]
在浏览器控制台中直接输出
{} + [] // 0 [] + {} // [object Object]
为什么上面会产生不同的结果呢?[] + {}
: 他先将[]
转化为"" => 0
, {}
转换为[object Object]
{} + []
: 他将{}
看作是一个空代码块会被忽略。所以最后转换为+ []
,然后被转换为0。
隐式强制类型转换为布尔值
下面的情况会发生布尔值隐式强制类型转换:
- if()语句中的条件判断表达式
- for()语句中的条件判断表达式(第二个)
- while()和do...while()循环中的条件判断表达式
- ? :中的条件判断表达式
- 逻辑运算符 ||, &&。 转换的时候,遵循ToBoolean抽象操作规则。
- &&, ||运算符的返回值并不一定是布尔值,而是两个操作数其中一个值。
const a = 42; const b = "abc"; const c = null; console.log(a || b); // 42 console.log(a && b); // abc console.log(c || b); // abc console.log(c && b); // null
Symbol的强制类型转换
- 不能被强制类型转换为数字(显示隐式都报错),但是可以强制类型转换为布尔值(显式和隐式都可以)。可以显式强制类型转换为字符串,但是不能隐式强制类型转换为字符串。
console.log(Symbol("cool") + "") // TypeError: Cannot convert a Symbol value to a string console.log(String(Symbol("cool"))) // Symbol(cool)
宽松相等和严格相等
- 一般对于二者的理解就是:
==
检查值是否相等,===
检查值和类型是否相等。
- 但是正确的理解应该是:
==
允许在相等比较中进行强制类型转换,而===
不允许。
==
在比较两个不同类型的值时会发生隐式强制类型转换,会将其中之一或二者都转换为相同的类型后在进行比较。
- 字符串和数字做
==
比较时,会将字符串做ToNumber
规则转换。
"2n" == 2 // false => Number("2n") = NaN "2" == 2 // true => Number("2") = 2
- 布尔值和数字做
==
比较时,会将boolean值做ToNumber
规则转换
false == 0 // true => Number(false) = 0 false == 2 // false
- 字符串和boolean做
==
比较时,会将二者都做ToNumber
规则转换。
"n" == true // false "n" == false // false "2" == true // false "2" == false // false "1" == false // false "1" == true // true
比较时并没有做ToBoolean操作。所以尽量不要使用== true, == false
。
- null和undefined两者永远相等,二者和其他任何值做
==
比较时都为false。
- 对象和字符串,数字,布尔值做
==
表示时,会将对象做ToPrimitive规则转换,然后==
操作符两边的操作数转换为数字。
console.log(42 == [42]) // true 42 == "42" => 42 == 42 => true
- 基本类型数据通过对应的基本包装类型封装后与原基本类型做
==
操作时,依旧是相等的。但是null, undefined, NaN
则不符合。
const a = "abc"; const b = Object(a); console.log(a == b) // true console.log(null == Object(null)) // false console.log(undefined == Object(undefined)) //false console.log(NaN == Object(NaN)) // false
[null], [undefined], [NaN]
通过ToPromitive规则转换后的值。
console.log([undefined].toString()) // "" console.log([null].toString()) // "" console.log([NaN].toString()) // NaN
- 安全运用隐式强制类型转换的原则:
- 如果两边值其中有
true
,false
,千万不要使用==
。
- 如果两边的值有
[]
,""
,0
,千万不要使用==
。
- 以上请使用
===
。注意:使用==
来做比较时,两边的操作数最终都转为数字来判断的。即调用Number()。
语句和表达式
- 语句想等于句子,表达式相当于短语,运算符则相当于标点符号和连接词。
- 由单个表达式构成的语句被称为"表达式语句"。
- 每一条语句都有结果值。我们在代码中是没办法获得这个结果值的。在控制台中输入
var a = 1;
会返回一个undefined。 代码块的结果值就如同一个隐式的返回,即返回最后一个语句的结果值。我们可以通过eval
获取一个代码块的返回值。
var a, b; a = eval("if(true) { b = 4 + 10 }"); console.log(a) // 14
es7有一个do表达式
提案。
a = do { if(true) { b = 4 + 10; } } console.log(a) // 14
其目的是将语句当做表达式来处理,从而不需要将语句封装为函数再调用return来返回值。现在测试,会报错,应该是没有被采纳。
- 语句系列逗号运算符:将多个独立的表达式语句串联成一个语句。表示将最后一个表达式的值,赋值给变量。
var a = 42, b; b = (a++, a); a // 43 b // 43
标签语句
{ foo: bar() }
上面代码就是一个普通的代码块,但是内部是一个标签语句。我们可以在循环中使用continue 标签
, break 标签
来实现跳转执行。并且标签也能用于非循环代码块,但是只有break才可以。
function foo() { bar: { console.log("Hello"); break bar; console.log("never runs"); } console.log("World") } foo() // Hello World
运算符的优先级
- 用
,
来连接一系列语句的时候,他的优先级最低,其他操作数的优先级都比他高。
const a = 42, b; b = a++, a; a // 43 b // 42
&&
运算符的优先级高于||
。
console.log(true || false && false) // true
||
的优先级高于?:
。
- 关联:右关联不是指从右往左执行,而是指从右往左组合。但是执行顺序在任何时候都是从左往右。
&&, ?:, =
都是右关联。
var a, b, c; a = b = c = 42; // (a = (b = (c = 42))) var d = a && b || c ? c || b ? a : c && b : a; // ((a || b) || c) ? ((c || b) ? a : (c && b)) : a;
函数参数
arguments.length
计算的是实参的个数。
- 在非严格模式下,向函数传递参数时,arguments数组中的对应单元会和命名参数建立关联以得到相同的值,相反不传递参数就不会建立关联。
function foo(a) { a = 42; console.log(arguments[0]); } foo(2); // 42 foo() // undefined
但是在严格模式下,传不传递参数都不会建立关联。
function foo(a) { "use strict" a = 42; console.log(arguments[0]); } foo(2); // 2 foo() // undefined
因此,在开发中不要依赖这种关联机制。实际上,他是js语言引擎底层实现的一个抽象泄漏,并不是语言本身的特性。
这种关联机制与es没有关系,只是语言的一种缺陷,只有在严格模式下,才不会被关联。所以我们在使用只需要遵守一个原则:不要同时访问命名参数和其对应的arguments数组单元。
try...finally
- finally中的return会覆盖try和catch中return的返回值。
function foo() { try { return 42; } finally { // 无返回值,所以没有覆盖 } } function bar() { try { return 42; } finally { return ; } } function baz() { try { return 42; } finally { return "hello"; } } console.log(foo()) // 42 console.log(bar()) // undefined console.log(baz()) // hello
switch
- 参数 和 case 表达式的匹配算法与
===
相同。 通常case语句中的switch都是简单值,所以并没有问题。
- 然而,有时可能会需要通过强制类型转换来进行相等比较(即 == ),这是就需要做一些特殊处理。
const a = "42"; switch (true) { case a == 10: console.log("10 or '10'") break; case a == 42: console.log("42 or '42'") break; default: break; }
- default不一定要写在最后面,可以写在中间位置。效果都一样。