4.2 抽象值操作
介绍显式和隐式强制类型转换之前,我们需要掌握字符串、数字和布尔值之间类型转换的基本规则。ES5 规范第 9 节中定义了一些“抽象操作”(即“仅供内部使用的操作”)和转换规则。这里我们着重介绍 ToString 、ToNumber 和 ToBoolean ,附带讲一讲 ToPrimitive 。
4.2.1 ToString
规范的 9.8 节中定义了抽象操作 ToString ,它负责处理非字符串到字符串的强制类型转换。
基本类型值的字符串化规则为:null 转换为 "null" ,undefined 转换为 "undefined" ,true 转换为 "true" 。数字的字符串化则遵循通用规则,不过第 2 章中讲过的那些极小和极大的数字使用指数形式:
// 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]] 的值(参见第 3 章),如 "[object Object]" 。
然而前面我们介绍过,如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。
将对象强制类型转换为 string 是通过 ToPrimitive 抽象操作来完成的(ES5 规范,9.1 节),我们在此略过,稍后将在 4.2.2 节中详细介绍。
数组的默认 toString() 方法经过了重新定义,将所有单元字符串化以后再用 "," 连接起来:
var a = [1,2,3]; a.toString(); // "1,2,3"
toString() 可以被显式调用,或者在需要字符串化时自动调用。
JSON 字符串化
工具函数 JSON.stringify(..) 在将 JSON 对象序列化为字符串时也用到了 ToString 。
请注意,JSON 字符串化并非严格意义上的强制类型转换,因为其中也涉及 ToString 的相关规则,所以这里顺带介绍一下。
对大多数简单值来说,JSON 字符串化和 toString() 的效果基本相同,只不过序列化的结果总是字符串:
JSON.stringify( 42 ); // "42" JSON.stringify( "42" ); // ""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 字符串化后的值,其实不然,除非我们确实想要对字符串进行字符串化(通常不会!)。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.string 还有一个可选参数 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 值。
4.2.2 ToNumber
有时我们需要将非数字值当作数字来使用,比如数学运算。为此 ES5 规范在 9.3 节定义了抽象操作 ToNumber 。
其中 true 转换为 1 ,false 转换为 0 。undefined 转换为 NaN ,null 转换为 0 。
ToNumber 对字符串的处理基本遵循数字常量的相关规则 / 语法(参见第 3 章)。处理失败时返回 NaN (处理数字常量失败时会产生语法错误)。不同之处是 ToNumber 对以 0 开头的十六进制数并不按十六进制处理(而是按十进制,参见第 2 章)。
数字常量的语法规则与 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() 方法,因此无法进行强制类型转换。详情请参考本系列的《你不知道的 JavaScript(上卷)》“this 和对象原型”部分中 [[Prototype]] 相关部分。
我们稍后将详细介绍数字的强制类型转换,在下面的示例代码中我们假定 Number(..) 已经实现了此功能。
例如:
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
4.2.3 ToBoolean
下面介绍布尔值,关于这个主题存在许多误解和困惑,需要我们特别注意。
首先也是最重要的一点是,JavaScript 中有两个关键词 true 和 false ,分别代表布尔类型中的真和假。我们常误以为数值 1 和 0 分别等同于 true 和 false 。在有些语言中可能是这样,但在 JavaScript 中布尔值和数字是不一样的。虽然我们可以将 1 强制类型转换为 true ,将 0 强制类型转换为 false ,反之亦然,但它们并不是一回事。
1. 假值(falsy value)
我们再来看看其他值是如何被强制类型转换为布尔值的。
JavaScript 中的值可以分为以下两类:
(1) 可以被强制类型转换为 false 的值
(2) 其他(被强制类型转换为 true 的值)
JavaScript 规范具体定义了一小撮可以被强制类型转换为 false 的值。
ES5 规范 9.2 节中定义了抽象操作 ToBoolean ,列举了布尔强制类型转换所有可能出现的结果。
以下这些是假值:
· undefined
· null
· false
· +0 、-0 和 NaN
· ""
假值的布尔强制类型转换结果为 false 。
从逻辑上说,假值列表以外的都应该是真值(truthy)。但 JavaScript 规范对此并没有明确定义,只是给出了一些示例,例如规定所有的对象都是真值,我们可以理解为假值列表以外的值都是真值 。
2. 假值对象(falsy object)
这个标题似乎有点自相矛盾。前面讲过规范规定所有的对象都是真值,怎么还会有假值对象呢?
有人可能会以为假值对象就是包装了假值的封装对象(如 "" 、0 和 false ,参见第 3 章),实际不然。
这只是规范开的一个小玩笑。
例如:
var a = new Boolean( false ); var b = new Number( 0 ); var c = new String( "" );
它们都是封装了假值的对象(参见第 3 章)。那它们究竟是 true 还是 false 呢?答案很简单:
var d = Boolean( a && b && c ); d; // true
d 为 true ,说明 a 、b 、c 都为 true 。
请注意,这里 Boolean(..) 对 a && b && c 进行了封装,有人可能会问为什么。我们暂且记下,稍后会作说明。你可以试试不用 Boolean(..) 的话 d = a && b && c 会产生什么结果。
如果假值对象并非封装了假值的对象,那它究竟是什么?
值得注意的是,虽然 JavaScript 代码中会出现假值对象,但它实际上并不属于 JavaScript 语言的范畴。
浏览器在某些特定情况下,在常规 JavaScript 语法基础上自己创建了一些外来 (exotic)值,这些就是“假值对象”。
假值对象看起来和普通对象并无二致(都有属性,等等),但将它们强制类型转换为布尔值时结果为 false 。
最常见的例子是 document.all ,它是一个类数组对象,包含了页面上的所有元素,由 DOM(而不是 JavaScript 引擎)提供给 JavaScript 程序使用。它以前曾是一个真正意义上的对象,布尔强制类型转换结果为 true ,不过现在它是一个假值对象。
document.all 并不是一个标准用法,早就被废止了。
有人也许会问:“既然这样的话,浏览器能否将它彻底去掉?”这个想法是好的,只不过仍然有很多 JavaScript 程序在使用它。
那为什么它要是假值呢?因为我们经常通过将 document.all 强制类型转换为布尔值(比如在 if 语句中)来判断浏览器是否是老版本的 IE。IE 自诞生之日起就始终遵循浏览器标准,较其他浏览器更为有力地推动了 Web 的发展。
if(document.all) { /* it's IE */ } 依然存在于许多程序中,也许会一直存在下去,这对 IE 的用户体验来说不是一件好事。
虽然我们无法彻底摆脱 document.all ,但为了让新版本更符合标准,IE 并不打算继续支持 if (document.all) { .. } 。
“那我们应该怎么办?”
“也许可以修改 JavaScript 的类型机制,将 document.all 作为假值来处理!”
这并不是一个好办法。大多数 JavaScript 开发人员对这个坑了解得不多,不过更糟糕的还是对其置若罔闻的态度。
3. 真值(truthy value)
真值就是假值列表之外的值。
例如:
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(){}; // 空函数——是真值还是假值? var d = Boolean( a && b && c ); d;
d 依然是 true 。还是同样的道理,[] 、{} 和 function(){} 都不在假值列表中,因此它们都是真值。
也就是说真值列表可以无限长,无法一一列举,所以我们只能用假值列表作为参考。
你可以花五分钟时间将假值列表写出来贴在显示器上,或者记在脑子里,在需要判断真 / 假值的时候就可以派上用场。
掌握真 / 假值的重点在于理解布尔强制类型转换(显式和隐式),在此基础上我们就能对强制类型转换示例进行深入介绍。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论