返回介绍

第一部分 类型和语法

第二部分 异步和性能

4.2 抽象值操作

发布于 2023-05-24 16:38:21 字数 9474 浏览 0 评论 0 收藏 0

介绍显式和隐式强制类型转换之前,我们需要掌握字符串、数字和布尔值之间类型转换的基本规则。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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文