ES5 规范第 9 节中定义了一些“抽象操作”(即“仅供内部使用的操作”)和转 换规则。
ToString
规范的 9.8 节中定义了抽象操作 ToString,它负责处理非字符串到字符串的强制类型转换。
基本类型值的字符串化规则为:
null 转换为 "null"
undefined 转换为 "undefined"
//上述在Chrome91,已报错
true 转换为 "true"
数字的字符串化则遵循通用规则,那些极小和极大的数字使用指数形式:
// 1.07 连续乘以七个 1000
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
// 七个1000一共21位数字
a.toString(); // "1.07e21"
对普通对象来说,除非自行定义,否则 toString()(Object.prototype.toString())返回内部属性 [[Class]] 的值,如 “[object Object]”。
var a = {A: '122'};
a.toString(); // "[object Object]"
如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。
var a = {
a: 123,
toString() {
return '456'
}
}
a.toString() // "456"
数组的默认 toString()
方法经过了重新定义,将所有单元字符串化以后再用 “,” 连接起来:
var a = [1,2,3];
a.toString(); // "1,2,3"
toString()
可以被显式调用,或者在需要字符串化时自动调用。
JSON 字符串化
工具函数 JSON.stringify(..)
在将 JSON 对象序列化为字符串时也用到了 ToString。
对大多数简单值来说,JSON 字符串化和 toString()
的效果基本相同,只不过序列化的结果总是字符串:
JSON.stringify( 42 ); // "42"
JSON.stringify( "42" ); // ""42""(含有双引号的字符串)
"42".toString(); // "42"
JSON.stringify( null ); // "null"
JSON.stringify( true ); // "true"
所有安全的 JSON 值(JSON-safe)都可以使用 JSON.stringify(..)
字符串化。安全的 JSON 值是指能够呈现为有效 JSON 格式的值。
不安全的 JSON 值:undefined、function、symbol (ES6+)和包含循环引用(对象之间相互引用,形成一个无限循环)的对象都不符合 JSON结构标准,支持 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 值的对象做字符串化,或者对象中的某些值无法被序列化时,就需要定义 toJSON()
方法来返回一个安全的 JSON 值。
var o = { };
var a = {
b: 42,
c: o,
d: function(){}
};
// 在a中创建一个循环引用
o.e = a;
// 循环引用在这里会产生错误
// JSON.stringify( a );
// 自定义的JSON序列化
a.toJSON = function() {
// 序列化仅包含b
return { b: this.b };
};
JSON.stringify( a ); // "{"b":42}"
toJSON()
返回的应该是一个适当的值,可以是任何类型,然后再由 JSON.stringify(..)
对其进行字符串化。
也就是说,toJSON()
应该“返回一个能够被字符串化的安全的 JSON 值”,而不是“返回 一个 JSON 字符串”。
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]""
第二个函数是对 toJSON 返回的字符串做字符串化,而非数组本身。
可以向 JSON.stringify(..)
传递一个可选参数 replacer,它可以是数组或者函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除,和 toJSON()
很像。
如果 replacer 是一个数组,那么它必须是一个字符串数组,其中包含序列化要处理的对象的属性名称,除此之外其他的属性则被忽略。
如果 replacer 是一个函数,它会对对象本身调用一次,然后对对象中的每个属性各调用一次,每次传递两个参数,键和值。如果要忽略某个键就返回 undefined,否则返回指定的值。
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。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) 字符串、数字、布尔值的 JSON.stringify(..)
规则与 ToString 基本相同。
(2) 如果传递给 JSON.stringify(..)
的对象中定义了 toJSON()
方法,那么该方法会在字符串化前调用,以便将对象转换为安全的 JSON 值。
ToNumber
有时我们需要将非数字值当作数字来使用,比如数学运算。为此 ES5 规范在 9.3 节定义了 抽象操作 ToNumber。
其中 true
转换为 1
,false
转换为 0
。undefined
转换为 NaN
,null
转换为 0
。
ToNumber 对字符串的处理基本遵循数字常量的相关规则 / 语法。处理失败时返回 NaN(处理数字常量失败时会产生语法错误)。不同之处是 ToNumber 对以 0 开头的十六进制数并不按十六进制处理(而是按十进制)。
数字常量的语法规则与 ToNumber 处理字符串所遵循的规则之间差别不大,可参考 ES5 规范的 9.3.1 节。
对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。
为了将值转换为相应的基本类型值,抽象操作 ToPrimitive(参见 ES5 规范 9.1 节)会首先 (通过内部操作 DefaultValue,参见 ES5 规范 8.12.8 节)检查该值是否有 valueOf() 方法。 如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString()
的返回值(如果存在)来进行强制类型转换。
如果 valueOf()
和 toString()
均不返回基本类型值,会产生 TypeError 错误。
从 ES5 开始,使用 Object.create(null)
创建的对象 [[Prototype]]
属性为 null
,并且没 有 valueOf()
和 toString()
方法,因此无法进行强制类型转换。例如:
var a = {
valueOf: function () {
return "42";
}
};
var b = {
toString: function () {
return "42";
}
};
var c = [4, 2];
c.toString = function () {
return this.join(""); // "42"
};
Number(a); // 42
Number(b); // 42
Number(c); // 42
Number(""); // 0
Number([]); // 0
Number(["abc"]); // NaN
ToBoolean
js 中有两个关键词 true 和 false,分别代表布尔类型中的真和假。我们常误以为数值 1 和 0 分别等同于 true 和 false。在有些语言中可能是这样,但在 js 中布尔值和数字是不一样的。虽然我们可以将 1 强制类型转换为 true, 将 0 强制类型转换为 false,反之亦然,但它们并不是一回事。
假值(falsy value)
js 中的值可以分为以下两类:
(1) 可以被强制类型转换为 false 的值
(2) 其他(被强制类型转换为 true 的值)
以下这些是假值:
- undefined
- null
- false
- +0、-0和NaN
- “”
假值的布尔强制类型转换结果为 false
。
从逻辑上说,假值列表以外的都应该是真值(truthy)。但 js 规范对此并没有明确定义,只是给出了一些示例,例如规定所有的对象都是真值,可以理解为假值列表以外的值都是真值。
假值对象(falsy object)
可能会以为假值对象就是包装了假值的封装对象,其实不是。
var a = new Boolean( false );
var b = new Number( 0 );
var c = new String( "" );
它们都是封装了假值的对象,它们都为true
var d = Boolean( a && b && c );
d; // true
Boolean(..)
对a && b && c
进行了封装,强制转换为布尔类型。没有Boolean(...)
的情况:
var e = a && b && c;
e; // String {""}
假值对象看起来和普通对象并无二致(都有属性,等等),但将它们强制类型转换为布尔值时结果为 false
。
最常见的例子是 document.all
,它是一个类数组对象,包含了页面上的所有元素,由 DOM
(而不是 js 引擎)提供给 js 程序使用。它以前曾是一个真正意义上的对象,布尔强制类型转换结果为 true,不过现在它是一个假值对象。document.all
并不是一个标准用法,早就被废止了。
真值(truthy value)
真值就是假值列表之外的值。
var a = "false";
var b = "0";
var c = "''";
var d = Boolean( a && b && c );
d; // true
var a = []; // 空数组——是真值还是假值?
var b = {}; // 空对象——是真值还是假值?
var c = function(){}; // 空函数——是真值还是假值?
var d = Boolean( a && b && c );
d; // true
因为除了以下假值,其他都是真值。
- undefined
- null
- false
- +0、-0 和 NaN
- “”