作为一名前端小白,不知道大家是否遇到和我一样的问题。看了一道面试题的解析,当时觉得会了,可是过两天以后再看又不会了;盲目追求各种新技术,感觉什么都会点,但是一上手就不行了... 痛定思痛后,我终于认识到了问题所在,开始专注于基本功的修炼。近半年来通读了(其实是囫囵吞枣)《JavaScript高级程序设计》、《你不知道的JavaScript上、中、下》等书籍,本系列文章是我读书过程中对知识点的一些总结。喜欢的同学记得帮我点个赞😁。
懂王系列(一)之彻底搞懂JavaScript函数执行机制
懂王系列(二)之彻底搞懂JavaScript作用域
懂王系列(三)之彻底搞懂JavaScript对象
懂王系列(四)之彻底搞懂JavaScript类
懂王系列(五)之彻底搞懂JavaScript原型
懂王系列(六)之彻底搞懂JavaScript中的this
懂王系列(七)之彻底搞懂JavaScript数据类型
懂王系列(八)之彻底搞懂JavaScript语句
懂王系列(九)之彻底搞定JavaScript类型转换
将值从一种类型转换为另一种类型通常称为类型转换(type casting),这是显式的情况;隐式的情况称为强制类型转换(coercion)。
类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型语言的运行时(runtime)
1. 抽象值操作
1.1 ToString
1.1.1 普通对象
对普通对象来说,除非自行定义,否则 toString() 返回内部属性 [[Class]] 的值,如 [object Object]。
1.1.2 数组对象
数组的默认 toString() 方法经过了重新定义,将所有单元字符串化以后再用 "," 连接起来:
var a = [1,2,3];
a.toString(); // "1,2,3"
1.1.3 JSON字符串化
JSON.stringify(..) 在对象中遇到 undefined、function 和 symbol 时会自动将其忽略,在数组中则会返回 null(以保证单元位置不变)。例如:
JSON.stringify(undefined); // undefined
JSON.stringify(function(){}); // undefined
JSON.stringify([1,undefined,function(){},4]); // "[1,null,null,4]"
JSON.stringify({ a:2, b:function(){} }); // "{"a":2}"
注意:对包含循环引用的对象执行 JSON.stringify(..) 会出错
如果对象中定义了 toJSON() 方法,JSON 字符串化时会首先调用该方法,然后用它的返回值来进行序列化。
// 自定义的JSON序列化
a.toJSON = function() {
// 序列化仅包含b
return {b: this.b};
};
JSON.stringify(a); // "{"b":42}"
toJSON() 返回的应该是一个适当的值,可以是任何类型,然后再由 JSON.stringify(..) 对其进行字符串化。也就是说,toJSON() 应该“返回一个能够被字符串化的安全的 JSON 值”,而不是“返回一个 JSON 字符串”。
JSON.stringify参数
- replacer
我们可以向 JSON.stringify(..) 传递一个可选参数 replacer,它可以是数组或者函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除,和 toJSON() 很像。
如果 replacer 是一个数组,那么它必须是一个字符串数组,其中包含序列化要处理的对象的属性名称,除此之外其他的属性则被忽略。
var a = {
b: 42,
c: "42",
d: [1,2,3]
};
JSON.stringify( a, ["b","c"] ); // "{"b":42,"c":"42"}"
JSON.stringify( a, function(k,v){
if (k !== "c") return v;
});
// "{"b":42,"d":[1,2,3]}"
如果 replacer 是函数,它的参数 k 在第一次调用时为 undefined(就是对对象本身调用的那次)。if 语句将属性 "c" 排除掉。由于字符串化是递归的,因此数组 [1,2,3] 中的每个元素都会通过参数 v 传递给 replacer,即 1、2 和 3,参数 k 是它们的索引值,即 0、1 和 2。
- space
JSON.stringify 还有一个可选参数 space,用来指定输出的缩进格式。space 为正整数时是指定每一级缩进的字符数。它还可以是字符串,此时最前面的十个字符被用于每一级的缩进:
var a = {
b: 42,
c: "42",
d: [1,2,3]
};
JSON.stringify( a, null, 3 );
// "{
// "b": 42,
// "c": "42",
// "d": [
// 1,
// 2,
// 3
// ]
// }"
JSON.stringify( a, null, "-----" );
// "{
// -----"b": 42,
// -----"c": "42",
// -----"d": [
// ----------1,
// ----------2,
// ----------3
// -----]
// }"
JSON.stringify(..) 并不是强制类型转换。在这里介绍是因为它涉及 ToString 强制类型转换,具体表现在以下两点。
(1) 字符串、数字、布尔值和 null 的 JSON.stringify(..) 规则与 ToString 基本相同。
(2) 如果传递给 JSON.stringify(..) 的对象中定义了 toJSON() 方法,那么该方法会在字符 串化前调用,以便将对象转换为安全的 JSON 值。
1.2 ToNumber
true 转换为 1,false 转换为 0。undefined 转换为 NaN,null 转换为 0。对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。
注意: 使用 Object.create(null) 创建的对象 [[Prototype]] 属性为 null,并且没有valueOf() 和 toString() 方法,因此无法进行强制类型转换
1.3 ToBoolean
以下这些是假值:
• undefined
• null
• false
• +0、-0 和 NaN
• ""
假值的布尔强制类型转换结果为 false。
var a = []; // 空数组——是真值还是假值?
var b = {}; // 空对象——是真值还是假值?
var c = function(){}; // 空函数——是真值还是假值?
var d = Boolean(a && b && c); // true
[]、{} 和 function(){} 都不在假值列表中,因此它们都是真值。
2. 显式强制类型转换
2.1 字符串和数字之间的显式转换:
除了 String(..) 和 Number(..) 以外,还有其他方法可以实现字符串和数字之间的显式转换:
var a = 42;
var b = a.toString();
var c = "3.14";
var d = +c;
b; // "42"
d; // 3.14
a.toString() 是显式的(“toString”意为“to a string”),不过其中涉及隐式转换。因为 toString() 对 42 这样的基本类型值不适用,所以 JavaScript 引擎会自动为 42 创建一个封装对象,然后对该对象调用 toString()。这里显式转换中含有隐式转换。
- 日期显式转换为数字
一元运算符+的另一个常见用途是将日期(Date)对象强制类型转换为数字,返回结果为Unix 时间戳,以毫秒为单位
var d = new Date("Mon, 18 Aug 2014 08:53:06 CDT");
+d; // 1408369986000
我们常用下面的方法来获得当前的时间戳,例如:
var timestamp = +new Date();
JavaScript 有一处奇特的语法,即构造函数没有参数时可以不用带 ()。于是我们可能会碰到 var timestamp = +new Date; 这样的写法。这样能否提高代码可读性还存在争议,因为这仅用于 new fn(),对一般的函数调用 fn() 并不适用。
不建议对日期类型使用强制类型转换,应该使用 Date.now() 来获得当前的时间戳,使用 new Date(..).getTime() 来获得指定时间的时间戳。
~运算符
~x 大致等同于 -(x+1)。很奇怪,但相对更容易说明问题:
~42; // -(42+1) ==> -43
在 -(x+1) 中唯一能够得到 0(或者严格说是 -0)的 x 值是 -1。也就是说如果 x 为 -1 时,~和一些数字值在一起会返回假值 0,其他情况则返回真值。
~ 和 indexOf() 一起可以将结果强制类型转换(实际上仅仅是转换)为真 / 假值:
var a = "Hello World";
~a.indexOf( "lo" ); // -4 <-- 真值!
if (~a.indexOf( "lo" )) { // true
// 找到匹配!
}
~a.indexOf( "ol" ); // 0 <-- 假值!
!~a.indexOf( "ol" ); // true
if (!~a.indexOf( "ol" )) { // true
// 没有找到匹配!
}
如果 indexOf(..) 返回 -1,~ 将其转换为假值 0,其他情况一律转换为真值。
2.2 显式解析数字字符串
解析和转换两者之间还是有明显的差别
var a = "42";
var b = "42px";
Number(a); // 42
parseInt(a); // 42
Number(b); // NaN
parseInt(b); // 42
解析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停止。而转换不允许出现非数字字符,否则会失败并返回 NaN。 解析和转换之间不是相互替代的关系。它们虽然类似,但各有各的用途。如果字符串右边的非数字字符不影响结果,就可以使用解析。而转换要求字符串中所有的字符都是数字,像 "42px" 这样的字符串就不行。
parseInt(..) 针对的是字符串值。向 parseInt(..) 传递数字和其他类型的参数是没有用的,比如 true、function(){...} 和 [1,2,3]
非字符串参数会首先被强制类型转换为字符串,依赖这样的隐式强制类型转换并非上策,应该避免向 parseInt(..) 传递非字符串参数。
从 ES5 开始 parseInt(..) 默认转换为十进制数,除非另外指定。如果你的代码需要在 ES5 之前的环境运行,请记得将第二个参数设置为 10。
2.3 显示转换为布尔值
在 if(..).. 这样的布尔值上下文中,如果没有使用 Boolean(..) 和 !!,就会自动隐式地进行 ToBoolean 转换。建议使用 Boolean(..) 和 !! 来进行显式转换以便让代码更清晰易读。
建议使用 Boolean(a) 和 !!a 来进行显式强制类型转换
3. 隐式强制类型转换
3.1 字符串和数字之间的隐式强制类型转换
简单来说就是,如果 + 的其中一个操作数是字符串(或者通过以上步骤可以得到字符串),则执行字符串拼接;否则执行数字加法。
3.2 布尔值到数字的隐式强制类型转换
(1) if (..) 语句中的条件判断表达式。
(2) for ( .. ; .. ; .. ) 语句中的条件判断表达式(第二个)。
(3) while (..) 和 do..while(..) 循环中的条件判断表达式。
(4) ? : 中的条件判断表达式。
(5) 逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操作数(作为条件判断表达式)。
3.3 || 和 &&
|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先进行 ToBoolean 强制类型转换,然后再执行条件判断。
对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为false 就返回第二个操作数的值。
&& 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。
a || b;
// 大致相当于(roughly equivalent to):
a ? a : b;
a && b;
// 大致相当于(roughly equivalent to):
a ? b : a;
之所以说 a || b 与 a ? a : b 大致相当,是因为它们返回结果虽然相同但是却有一个细微的差别。在 a ? a : b 中,如果 a 是一个复杂一些的表达式(比如有副作用的函数调用等),它有可能被执行两次(如果第一次结果为真)。而在 a || b 中 a 只执行一次,其结果用于条件判断和返回结果(如果适用的话)。a && b 和 a ? b : a 也是如此。
function foo() {
console.log( a );
}
var a = 42;
a && foo(); // 42
如果条件判断未通过,a && foo() 就会悄然终止(也叫作“短路”,short circuiting),foo() 不会被调用。
为什么 a && (b || c) 这样的表达式在if 和 for 中没出过问题?
var a = 42;
var b = null;
var c = "foo";
if (a && (b || c)) {
console.log( "yep" );
}
这里 a && (b || c) 的结果实际上是 foo 而非 true,然后再由 if 将 foo 强制类型转换为布尔值,所以最后结果为 true。现在明白了吧,这里发生了隐式强制类型转换。如果要避免隐式强制类型转换就得这样:
if (!!a && (!!b || !!c)) {
console.log( "yep" );
}
3.4 符号的强制类型转换
ES6 允许从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误
var s1 = Symbol( "cool" );
String( s1 ); // "Symbol(cool)"
var s2 = Symbol( "not cool" );
s2 + ""; // TypeError
4. 宽松相等和严格相等
4.1 常规比较
- 字符串和数字之间的相等比较
(1) 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果。
(2) 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果。
- 其他类型和布尔类型之间的相等比较
(1) 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果;
(2) 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。
- null 和 undefined 之间的相等比较
(1) 如果 x 为 null,y 为 undefined,则结果为 true。
(2) 如果 x 为 undefined,y 为 null,则结果为 true。
- 对象和非对象之间的相等比较
(1) 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果;
(2) 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPrimitive(x) == y 的结果。
var a = "abc";
var b = Object(a); // 和new String( a )一样
a === b; // false
a == b; // true
a == b 结果为 true,因为 b 通过 ToPrimitive 进行强制类型转换(也称为“拆封”,英文为 unboxed 或 unwrapped),并返回标量基本类型值 abc,与 a 相等。
但有一些值不这样,原因是 == 算法中其他优先级更高的规则。例如:
var a = null;
var b = Object(a); // 和Object()一样
a == b; // false
var c = undefined;
var d = Object(c); // 和Object()一样
c == d; // false
var e = NaN;
var f = Object(e); // 和new Number( e )一样
e == f; // false
因为没有对应的封装对象,所以 null 和 undefined 不能够被封装(boxed),Object(null) 和 Object() 均返回一个常规对象。
NaN 能够被封装为数字封装对象,但拆封之后 NaN == NaN 返回 false,因为 NaN 不等于 NaN
4.2 比较少见的情况
- 返回其它数字
Number.prototype.valueOf = function() {
return 3;
};
new Number( 2 ) == 3; // true
2 和 3 都是数字基本类型值,不会调用 Number.prototype.valueOf() 方法。而 Number(2) 涉及 ToPrimitive 强制类型转换,因此会调用 valueOf()。
if (a == 2 && a == 3) {
// ..
}
// 方法1
let i = 1;
Number.prototype.valueOf = function() {
return i++;
};
var a = new Number( 42 );
if (a== 1 && a == 2 && a == 3) {
console.log( "Yep, this happened." );
}
// 方法2
let a = [1, 2, 3];
a.valueOf = a.shift;
// a.toString = a.shift;
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
// 方法3
let a = {
i: 0,
toString() {
return ++this.i;
}
};
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
// 方法4
let i = 0;
Object.defineProperty(window, 'a', {
get() {
//=>获取window.a的时候触发
return ++i;
},
set() {
//=>给window.a设置属性值的时候触发
}
});
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
- 假值的相等比较
"0" == null; // false
"0" == undefined; // false
"0" == false; // true -- 晕!
"0" == NaN; // false
"0" == 0; // true
"0" == ""; // false
false == null; // false
false == undefined; // false
false == NaN; // false
false == 0; // true -- 晕!
false == ""; // true -- 晕!
false == []; // true -- 晕!
false == {}; // false
"" == null; // false
"" == undefined; // false
"" == NaN; // false
"" == 0; // true -- 晕!
"" == []; // true -- 晕!
"" == {}; // false
0 == null; // false
0 == undefined; // false
0 == NaN; // false
0 == []; // true -- 晕!
0 == {}; // false
- 极端情况
[] == ![] // true
! 运算符都做了些什么?根据 ToBoolean 规则,它会进行布尔值的显式强制类型转换(同时反转奇偶校验位)。所以 [] == ![] 变成了 [] == false。前面我们讲过 false == [],最后的结果就顺理成章了。
2 == [2]; // true
"" == [null]; // true
第一行中的 [2] 会转换为 2,然后通过 ToNumber 转换为 2。第二行中的 [null] 会直接转换为 ""。
- 完整性检查
"0" == false; // true -- 晕!
false == 0; // true -- 晕! `
false == ""; // true -- 晕!
false == []; // true -- 晕!
"" == 0; // true -- 晕!
"" == []; // true -- 晕!
0 == []; // true -- 晕!
其中有 4 种情况涉及 == false,之前我们说过应该避免,应该不难掌握。
现在剩下 3 种:
"" == 0; // true -- 晕!
"" == []; // true -- 晕!
0 == []; // true -- 晕!
doSomething(0) 和 doSomething([])这样的情况
function doSomething(a,b) {
if (a == b) {
// ..
}
}
doSomething("",0) 和 doSomething([],"")也会如此。
- 安全运用隐式强制类型转换
我们要对
==两边的值认真推敲,以下两个原则可以让我们有效地避免出错。
• 如果两边的值中有 true 或者 false,千万不要使用 ==。
• 如果两边的值中有 []、"" 或者 0,尽量不要使用 ==。
有一种情况下强制类型转换是绝对安全的,那就是 typeof 操作。typeof 总是返回七个字符串之一,其中没有空字符串。所以在类型检查过程中不会发生隐式强制类型转换。typeof x == "function" 是 100% 安全的,
Alex Dorey(GitHub 用户名 @dorey)在 GitHub 上制作了一张图表,列出了各种相等比较的情况,
5. 抽象关系比较
如果比较双方都是字符串,则按字母顺序来进行比较:
var a = ["42"];
var b = ["043"];
a < b; // false
a 和 b 并没有被转换为数字,因为 ToPrimitive 返回的是字符串,所以这里比较的是 "42" 和 "043" 两个字符串,它们分别以 "4" 和 "0" 开头。因为 "0" 在字母顺序上小于 "4",所以最后结果为 false。
同理:
var a = [4, 2];
var b = [0, 4, 3];
a < b; // false
a 转换为 "4, 2",b 转换为 "0, 4, 3",同样是按字母顺序进行比较。
再比如:
var a = { b: 42 };
var b = { b: 43 };
a < b; // ??
结果还是 false,因为 a 是 [object Object],b 也是 [object Object],所以按照字母顺序 a < b 并不成立。
下面的例子就有些奇怪了:
var a = { b: 42 };
var b = { b: 43 };
a < b; // false
a == b; // false
a > b; // false
a <= b; // true
a >= b; // true
为什么 a == b 的结果不是 true ?它们的字符串值相同(同为 "[object Object]"),按道理应该相等才对?实际上不是这样,你可以回忆一下前面讲过的对象的相等比较。
但是如果 a < b 和 a == b 结果为 false,为什么 a <= b 和 a >= b 的结果会是 true 呢?因为根据规范 a <= b 被处理为 b < a,然后将结果反转。因为 b < a 的结果是 false,所以 a <= b 的结果是 true。
这可能与我们设想的大相径庭,即 <= 应该是小于或者等于。实际上 JavaScript 中 <= 是不大于的意思(即 !(a > b),处理为 !(b < a))。同理,a >= b 处理为 b <= a。
相等比较有严格相等,关系比较却没有“严格关系比较”(strict relational comparison)。也
就是说如果要避免 a < b 中发生隐式强制类型转换,我们只能确保 a 和 b 为相同的类型,除此之外别无他法。
与 == 和 === 的完整性检查一样,我们应该在必要和安全的情况下使用强制类型转换,如:42 < "43"。换句话说就是为了保证安全,应该对关系比较中的值进行显式强制类型转换:
var a = [ 42 ];
var b = "043";
a < b; // false -- 字符串比较!
Number( a ) < Number( b ); // true -- 数字比较!