4.1 值类型转换
将值从一种类型转换为另一种类型通常称为类型转换。
- 隐式的情况称为强制类型转换
js
//将数字 42 转换为字符串 "42"
var a = 42;
var b = a + ""; // 隐式强制类型转换
var c = String( a ); // 显式强制类型转换
4.2 抽象值操作
4.2.1 ToString
处理非字符串到字符串的强制类型转换
- 基本类型值的字符串化规则为:null 转换为 "null"
- undefined 转换为 "undefined",
- true转换为 "true"
- 数字的字符串化则遵循通用规则
- 对普通对象来说,除非自行定义,否则 toString()(Object.prototype.toString())返回内部属性
[[Class]]
的值,如[object Object]
- 如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值
- 数组的默认 toString() 方法经过了重新定义,将所有单元字符串化以后再用 "," 连接起来
- toString() 可以被显式调用,或者在需要字符串化时自动调用。
js
// 1.07 连续乘以七个 1000
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
// 七个1000一共21位数字
a.toString(); // "1.07e21"
var a = [1,2,3];
a.toString(); // "1,2,3"
JSON 字符串化
函数 JSON.stringify(..) 在将 JSON 对象序列化为字符串时也用到了 ToString
- 对大多数简单值来说,JSON 字符串化和 toString() 的效果基本相同,只不过序列化的结果总是字符串
- 不安全的 JSON 值。undefined、function、symbol(ES6+)和包含循环引用(对象之间相互引用,形成一个无限循环)的对象都不符合 JSON结构标准,支持 JSON 的语言无法处理它们
- JSON.stringify(..) 在对象中遇到 undefined、function 和 symbol 时会自动将其忽略,在数组中则会返回 null
- 对包含循环引用的对象执行 JSON.stringify(..) 会出错
js
JSON.stringify( 42 ); // "42"
JSON.stringify( "42" ); // ""42"" (含有双引号的字符串)
JSON.stringify( null ); // "null"
JSON.stringify( true ); // "true"
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}"
//
var o = { };
var a = {
b: 42,
c: o,
d: function(){}
};
// 在a中创建一个循环引用
o.e = a;
// 循环引用在这里会产生错误
// JSON.stringify( a );
toJSON 如果对象中定义了 toJSON() 方法,JSON 字符串化时会首先调用该方法,然后用它的返回值来进行序列化 - 如果要对含有非法 JSON 值的对象做字符串化,或者对象中的某些值无法被序列化时,就需要定义 toJSON() 方法来返回一个安全的 JSON 值。 - toJSON() 返回不是JSON字符串,而是一个对象之类的值,再由 JSON.stringify(..) 对其进行字符串化
js
var o = { };
var a = {
b: 42,
c: o,
d: function(){}
};
// 在a中创建一个循环引用
o.e = a;
// 自定义的JSON序列化
a.toJSON = function() {
// 序列化仅包含b
return { b: this.b };
};
JSON.stringify( a ); // "{"b":42}"
//
var a = {
val: [1,2,3],
// 可能是我们想要的结果!
toJSON: function(){
return this.val.slice( 1 );
}
};
var b = {
val: [1,2,3],
// 可能不是我们想要的结果!
toJSON: function(){
return "[" +this.val.slice( 1 ).join() +"]";
}
};
JSON.stringify( a ); // "[2,3]"
JSON.stringify( b ); // ""[2,3]""
不太为人所知但却非常有用的功能
可以向 JSON.stringify(..) 传递一个可选参数 replacer,它可以是数组或者函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除,和 toJSON() 很像
- 如果 replacer 是一个数组,那么它必须是一个字符串数组,其中包含序列化要处理的对象的属性名称,除此之外其他的属性则被忽略
- 如果 replacer 是一个函数,它会对对象本身调用一次,然后对对象中的每个属性各调用一次,每次传递两个参数,键和值。如果要忽略某个键就返回 undefined,否则返回指定的值
js
var a = {
b: 42,
c: "42",
d: [1,2,3]
};
//只处理属性b和c
JSON.stringify( a, ["b","c"] ); // "{"b":42,"c":"42"}"
//所有的属性都需要经过函数处理,不需要的属性需要返回undefined
JSON.stringify( a, function(k,v){
if (k !== "c") return v;
} );// "{"b":42,"d":[1,2,3]}"
JSON.stringify 还有一个可选参数 space,用来指定输出的缩进格式。
- space 为正整数时是指定每一级缩进的字符数
- 它还可以是字符串,此时最前面的十个字符被用于每一级的缩进
js
var a = {
b: 42,
c: "42",
d: [1, 2, 3]
};
var aJson = JSON.stringify(a, null, 1);
console.log(aJson);
// {
// "b": 42,
// "c": "42",
// "d": [
// 1,
// 2,
// 3
// ]
// }
var aJson = JSON.stringify(a, null, "----");
console.log(aJson);
// {
// ----"b": 42,
// ----"c": "42",
// ----"d": [
// --------1,
// --------2,
// --------3
// ----]
// }
4.2.2 ToNumber
将非数字值当作数字来使用
- 其中 true 转换为 1,false 转换为 0。
- undefined 转换为 NaN
- null 转换为 0
- 对字符串的处理基本遵循数字常量的相关规则/语法,处理失败时返回 NaN
- 对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字
为了将值转换为相应的基本类型值,抽象操作 ToPrimitive
- 会首先检查该值是否有 valueOf() 方法,如果有并且返回基本类型值,就使用该值进行强制类型转换
- 如果没有就使用 toString()的返回值(如果存在)来进行强制类型转换
- 如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误
js
var a = {
valueOf: function () {
return "42";
}
};
var b = {
toString: function () {
return "42";
}
};
var c = [4, 2];
c.toString = function () {
return this.join(""); // "42"
};
console.log(Number(a)); // 42
console.log(Number(b)); // 42
console.log(Number(c)); // 42
console.log(Number("")); // 0
console.log(Number([])); // 0
console.log(Number(["abc"])); // NaN
4.2.3 ToBoolean
假值 false
- undefined
- null
- false
- +0、-0 和 NaN
- ""
- 假值对象
- 将它们强制类型转换为布尔值时结果为 false的外来对象
- document.all
需要注意的是,包装了假值的对象,并不为false
js
var a = new Boolean( false );
var b = new Number( 0 );
var c = new String( "" );
var d = Boolean( a && b && c );
d; // true 说明 a、b、c 都为 true。
真值 true 真值就是假值列表之外的值
js
var a = "false";
var b = "0";
var c = "''";
var d = Boolean( a && b && c );
d; //true 因为 a,b,c都是字符串,都有内容
var a = []; // 空数组------是真值还是假值? 真值
var b = {}; // 空对象------是真值还是假值? 真值
var c = function(){}; // 空函数------是真值还是假值? 真值
var d = Boolean( a && b && c );
d;//true
4.3 显式强制类型转换
显式强制类型转换是那些显而易见的类型转换
4.3.1 字符串和数字之间的显式转
- 通过 String(..) 和 Number(..) 这两个内建函数
js
var a = 42;
var b = String( a );
var c = "3.14";
var d = Number( c );
b; // "42"
d; // 3.14
4.3.2 显式解析数字字符串
解析字符串中的数字和将字符串强制类型转换为数字的返回结果都是数字
- 解析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停止。
- 转换不允许出现非数字字符,否则会失败并返回 NaN。
js
var a = "42";
var b = "42px";
Number( a ); // 42
parseInt( a ); // 42
Number( b ); // NaN
parseInt( b ); // 42
parseInt(..) 针对的是字符串值,ES开始后,默认是10进制数,第二个参数为进制
js
var hour = parseInt( selectedHour.value, 10 );
var minute = parseInt( selectedMiniute.value, 10 );
parseInt 接收非字符串时,会转成字符串,然后再解析,有时候会引起奇怪的问题
js
console.log(parseInt(1 / 0, 19)); //18
console.log(parseInt("Infinity", 19)); //18
- parseInt(1/0, 19) 实际上是 parseInt("Infinity", 19)。
- 基数 19时,有效数字字符范围是 0-9 和 a-i
- 由于第一个字符是 "I",所以 19 为基数时值为 18
4.3.3 显式转换为布尔值
- Boolean(..)
js
var a = "0";
var b = [];
var c = {};
var d = "";
var e = 0;
var f = null;
var g;
Boolean( a ); // true
Boolean( b ); // true
Boolean( c ); // true
Boolean( d ); // false
Boolean( e ); // false
Boolean( f ); // false
Boolean( g ); // false
一元运算符 ! 显式地将值强制类型转换为布尔值,它同时还将真值反转为假值,所以显式强制类型转换为布尔值最常用的方强制类型转换法是 !!
js
var a = "0";
var b = [];
var c = {};
var d = "";
var e = 0;
var f = null;
var g;
!!a; // true
!!b; // true
!!c; // true
!!d; // false
!!e; // false
!!f; // false
!!g; // false
4.4 隐式强制类型转换
隐式强制类型转换指的是那些隐蔽的强制类型转换,副作用也不是很明显
4.4.2 字符串和数字之间的隐式强制类型转换
如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话,+ 将进行拼接操作
- 如果其中一个操作数是对象(包括数组),则首先对其调用ToPrimitive 抽象操作,该抽象操作再调用
[[DefaultValue]]
,以数字作为上下文 - 如果 + 的其中一个操作数是字符串,则执行字符串拼接;否则执行数字加法
js
var a = "42";
var b = "0";
var c = 42;
var d = 0;
a + b; // "420"
c + d; // 42
var a = [1,2];
var b = [3,4];
a + b; // "1,23,4"
var a = 42;
var b = a + "";
b; // "42"
4.4.3 布尔值到数字的隐式强制类型转换
js
function onlyOne() {
var sum = 0;
for (var i = 0; i < arguments.length; i++) {
// 跳过假值,和处理0一样,但是避免了NaN
if (arguments[i]) {
// 的隐式强制类型转换,将真值(true/truthy)转换为 1 并进行累加。
sum += arguments[i];
}
}
return sum == 1;
}
var a = true;
var b = false;
onlyOne(b, a); // true
onlyOne(b, a, b, b, b); // true
onlyOne(b, b); // false
onlyOne(b, a, b, b, b, a); // false
或
js
function onlyOne() {
var sum = 0;
for (var i = 0; i < arguments.length; i++) {
// !!arguments[i] 首先将参数转换为 true 或 false。
// 转换为布尔值以后,再通过 Number(..) 显式强制类型转换为 0 或 1
sum += Number(!!arguments[i]);
}
return sum === 1;
}
4.4.4 隐式强制类型转换为布尔值
下面的情况会发生布尔值隐式强制类型转换
(1) if (..) 语句中的条件判断表达式。
(2) for ( .. ; .. ; .. ) 语句中的条件判断表达式(第二个)。
(3) while (..) 和 do..while(..) 循环中的条件判断表达式。
(4) ? : 中的条件判断表达式。
(5) 逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操作数(作为条件判断表达式)。
js
var a = 42;
var b = "abc";
var c;
var d = null;
if (a) {
console.log("yep"); // yep
}
while (c) {
console.log("nope, never runs");
}
c = d ? a : b;
c; // "abc"
if ((a && d) || c) {
console.log("yep"); // yep
}
4.4.5 || 和 &&
在 JavaScript 中它们返回的并不是布尔值,它们的返回值是两个操作数中的一个(且仅一个)。
js
var a = 42;
var b = "abc";
var c = null;
console.log(a || b); // 42
console.log(a && b); // "abc"
console.log(c || b); // "abc"
console.log(c && b); // null
- 对于 || 来说,如果条件判断结果为 true 就返回第一个操作数(a 和 c)的值,如果为false 就返回第二个操作数(b)的值
- && 则相反,如果条件判断结果为 true 就返回第二个操作数(b)的值,如果为 false 就返回第一个操作数(a 和 c)的值。
js
a || b;
// 大致相当于(roughly equivalent to):
a ? a : b;
a && b;
// 大致相当于(roughly equivalent to):
a ? b : a;
十分常见的 || 的用法
js
function foo(a, b) {
a = a || "hello";
b = b || "world";
console.log(a + " " + b);
}
foo(); // "hello world"
foo("yeah", "yeah!"); // "yeah yeah!"
//需要注意
foo( "That's it!", "" ); // "That's it! world" <-- 晕!
//第二个参数 "" 是一个假值,因此 b = b || "world" 条件不成立,返回默认值 "world"
JavaScript 代码压缩工具常用 &&
如果第一个操作数为真值,则 && 运算符"选择"第二个操作数作为返回值,这也叫作"守护运算符"
js
function foo() {
console.log(a);
}
var a = 42;
a && foo(); // 42
- foo() 只有在条件判断 a 通过时才会被调用。
- 如果条件判断未通过,a && foo() 就会悄然终止(也叫作"短路",short circuiting),foo() 不会被调用
- 通常使用 if (a) { foo(); }, 但 JavaScript代码压缩工具用的是 a && foo(),因为更简洁
既然返回的不是 true 和 false,为什么 a && (b || c) 这样的表达式在if 和 for 中没出过问题?
- 问题在于你可能不知道这些条件判断表达式最后还会执行布尔值的隐式强制类型转换
js
var a = 42;
var b = null;
var c = "foo";
if (a && (b || c)) {
console.log( "yep" );
}
- 这里 a && (b || c) 的结果实际上是 "foo" 而非 true
- 然后再由 if 将 foo 强制类型转换为布尔值,所以最后结果为 true
这里发生了隐式强制类型转换,如果要避免隐式强制类型转换就得这样
js
if (!!a && (!!b || !!c)) {
console.log( "yep" );
}
4.4.6 符号的强制类型转换
ES6 允许从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误
js
var s1 = Symbol("cool");
String(s1); // "Symbol(cool)"
var s2 = Symbol("not cool");
s2 + ""; // TypeError
符号不能够被强制类型转换为数字
可以被强制类型转换为布尔值(显式和隐式结果都是 true)
4.5 宽松相等和严格相等
- 宽松相等(loose equals)==
- 严格相等(strict equals)===
"== 允许在相等比较中进行强制类型转换,而 === 不允许。"
4.5.2 抽象相等
- 字符串和数字之间的相等比较
js
var a = 42;
var b = "42";
a === b; // false
a == b; // true
(1) 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果。
(2) 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果。
因此,"42" 应该被强制类型转换为数字以便进行相等比较
- 其他类型和布尔类型之间的相等比较
js
var a = "42";
var b = true;
a == b; // false
(1) 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果;
(2) 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。
因此,是 b 转成了数字1,所以 1=="42" 的结果是false
- null 和 undefined 之间的相等比较
(1) 如果 x 为 null,y 为 undefined,则结果为 true。
(2) 如果 x 为 undefined,y 为 null,则结果为 true。
在 == 中 null 和 undefined 相等(它们也与其自身相等),除此之外其他值都不存在这种情况。
js
var a = null;
var b;
a == b; // true
a == null; // true
b == null; // true
a == false; // false
b == false; // false
a == ""; // false
b == ""; // false
a == 0; // false
b == 0; // false
- 对象和非对象之间的相等比较
(1) 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果;
(2) 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPromitive(x) == y 的结果。
js
var a = 42;
var b = [ 42 ];
a == b; // true
//[ 42 ] 首先调用 ToPromitive 抽象操作(参见 4.2 节),返回 "42",变成 "42" == 42,然后又变成 42 == 42,最后二者相等
4.5.3 比较少见的情况
- 返回其他数字
js
Number.prototype.valueOf = function() {
return 3;
};
new Number( 2 ) == 3; // true
更离谱的
js
var i = 2;
Number.prototype.valueOf = function () {
return i++;
};
var a = new Number(42);
if (a == 2 && a == 3) {
console.log("Yep, this happened.");
}
- 假值的相等比较
== 中的隐式强制类型转换最为人诟病的地方是假值的相等比较
js
"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
- 极端情况
js
[] == ![] // true
根据 ToBoolean 规则,它会进行布尔值的显式强制类型转换(同时反转奇偶校验位)。
- 所以
[] == ![]
了[] == false
。前面我们讲过false == []
,最后的结果就顺理成章了
js
2 == [2]; // true
"" == [null]; // true
- 第一行中的
[2]
会转换为 "2",然后通过 ToNumber 转换为 2。 - 第二行中的
[null]
会直接转换为 ""。
""、"\n"(或者 " " 等其他空格组合)等空字符串被 ToNumber 强制类型转换为 0。
js
0 == "\n"; // true
- 安全运用隐式强制类型转换
- 如果两边的值中有 true 或者 false,千万不要使用 ==。
- 如果两边的值中有
[]
、"" 或者 0,尽量不要使用 ==。
最好用 === 来避免不经意的强制类型转换

4.6 抽象关系比较
a < b 中涉及的隐式强制类型转换不太引人注意
比较双方首先调用 ToPrimitive,如果结果出现非字符串,就根据 ToNumber 规则将双方强制类型转换为数字来进行比较。
js
var a = [ 42 ];
var b = [ "43" ];
a < b; // true
b < a; // false
如果比较双方都是字符串,则按字母顺序来进行比较
js
var a = [ "42" ];
var b = [ "043" ];
a < b; // false