第四章:强制转换1

特别说明,为便于查阅,文章转自https://github.com/getify/You-Dont-Know-JS

现在我们更全面地了解了JavaScript的类型和值,我们将注意力转向一个极具争议的话题:强制转换。

正如我们在第一章中提到的,关于强制转换到底是一个有用的特性,还是一个语言设计上的缺陷(或位于两者之间!),早就开始就争论不休了。如果你读过关于JS的其他书籍,你就会知道流行在世面上那种淹没一切的 声音:强制转换是魔法,是邪恶的,令人困惑的,而且就是彻头彻尾的坏主意。

本着这个系列丛书的总体精神,我认为你应当直面你不理解的东西并设法更全面地 搞懂它。而不是因为大家都这样做,或是你曾经被一些怪东西咬到就逃避强制转换。

我们的目标是全面地探索强制转换的优点和缺点(是的,它们 优点!),这样你就能在程序中对它是否合适做出明智的决定。

转换值

将一个值从一个类型明确地转换到另一个类型通常称为“类型转换(type casting)”,当这个操作隐含地完成时称为“强制转换(coercion)”(根据一个值如何被使用的规则来强制它变换类型)。

注意: 这可能不明显,但是JavaScript强制转换总是得到基本标量值的一种,比如stringnumber,或boolean。没有强制转换可以得到像objectfunction这样的复杂值。第三章讲解了“封箱”,它将一个基本类型标量值包装在它们相应的object中,但在准确的意义上这不是真正的强制转换。

另一种区别这些术语的常见方法是:“类型转换(type casting/conversion)”发生在静态类型语言的编译时,而“类型强制转换(type coercion)”是动态类型语言的运行时转换。

然而,在JavaScript中,大多数人将所有这些类型的转换都称为 强制转换(coercion),所以我偏好的区别方式是使用“隐含强制转换(implicit coercion)”与“明确强制转换(explicit coercion)”。

其中的区别应当是很明显的:在观察代码时如果一个类型转换明显是有意为之的,那么它就是“明确强制转换”,而如果这个类型转换是做为其他操作的不那么明显的副作用发生的,那么它就是“隐含强制转换”。

例如,考虑这两种强制转换的方式:

var a = 42;

var b = a + "";         // 隐含强制转换

var c = String( a );    // 明确强制转换

对于b来说,强制转换是隐含地发生的,因为如果与+操作符组合的操作数之一是一个string值(""),这将使+操作成为一个string连接(将两个字符串加在一起),而string连接的 一个(隐藏的)副作用a中的值42强制转换为它的string等价物:"42"

相比之下,String(..)函数使一切相当明显,它明确地取得a中的值,并把它强制转换为一个string表现形式。

两种方式都能达到相同的效果:从42变成"42"。但它们 如何 达到这种效果,才是关于JavaScript强制转换的热烈争论的核心。

注意: 技术上讲,这里有一些在语法形式区别之上的,行为上的微妙区别。我们将在本章稍后,“隐含:Strings <--> Numbers”一节中仔细讲解。

“明确地”,“隐含地”,或“明显地”和“隐藏的副作用”这些术语,是 相对的

如果你确切地知道a + ""是在做什么,并且你有意地这么做来强制转换一个string,你可能感觉这个操作已经足够“明确”了。相反,如果你从没见过String(..)函数被用于string强制转换,那么对你来说它的行为可能看起来太过隐蔽而让你感到“隐含”。

但我们是基于一个 大众的,充分了解,但不是专家或JS规范爱好者的 开发者的观点来讨论“明确”与“隐含”的。无论你的程度如何,或是没有在这个范畴内准确地找到自己,你都需要根据我们在这里的观察方式,相应地调整你的角度。

记住:我们自己写代码而也只有我们自己会读它,通常是很少见的。即便你是一个精通JS里里外外的专家,也要考虑一个经验没那么丰富的队友在读你的代码时感受如何。对于他们和对于你来说,“明确”或“隐含”的意义相同吗?

抽象值操作

在我们可以探究 明确隐含 强制转换之前,我们需要学习一些基本规则,是它们控制着值如何 变成 一个stringnumber,或boolean的。ES5语言规范的第9部分用值的变形规则定义了几种“抽象操作”(“仅供内部使用的操作”的高大上说法)。我们将特别关注于:ToStringToNumber,和ToBoolean,并稍稍关注一下ToPrimitive

ToString

当任何一个非string值被强制转换为一个string表现形式时,这个转换的过程是由语言规范的9.8部分的ToString抽象操作处理的。

内建的基本类型值拥有自然的字符串化形式:null变为"null"undefined变为"undefined"true变为"true"number一般会以你期望的自然方式表达,但正如我们在第二章中讨论的,非常小或非常大的number将会以指数形式表达:

// `1.07`乘以`1000`,7次
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;

// 7次乘以3位 => 21位
a.toString(); // "1.07e21"

对于普通的对象,除非你指定你自己的,默认的toString()(可以在Object.prototype.toString()找到)将返回 内部[[Class]](见第三章),例如"[object Object]"

但正如早先所展示的,如果一个对象上拥有它自己的toString()方法,而你又以一种类似string的方式使用这个对象,那么它的toString()将会被自动调用,而且这个调用的string结果将被使用。

注意: 技术上讲,一个对象被强制转换为一个string要通过ToPrimitive抽象操作(ES5语言规范,9.1部分),但是那其中的微妙细节将会在本章稍后的ToNumber部分中讲解,所以我们在这里先跳过它。

数组拥有一个覆盖版本的默认toString(),将数组字符串化为它所有的值(每个都字符串化)的(字符串)连接,并用","分割每个值。

var a = [1,2,3];

a.toString(); // "1,2,3"

重申一次,toString()可以明确地被调用,也可以通过在一个需要string的上下文环境中使用一个非string来自动地被调用。

JSON字符串化

另一种看起来与ToString密切相关的操作是,使用JSON.stringify(..)工具将一个值序列化为一个JSON兼容的string值。

重要的是要注意,这种字符串化与强制转换并不完全是同一种东西。但是因为它与上面讲的ToString规则有关联,我们将在这里稍微转移一下话题,来讲解JSON字符串化行为。

对于最简单的值,JSON字符串化行为基本上和toString()转换是相同的,除了序列化的结果 总是一个string

JSON.stringify( 42 );   // "42"
JSON.stringify( "42" ); // ""42"" (一个包含双引号的字符串)
JSON.stringify( null ); // "null"
JSON.stringify( true ); // "true"

任何 JSON安全 的值都可以被JSON.stringify(..)字符串化。但是什么是 JSON安全的?任何可以用JSON表现形式合法表达的值。

考虑JSON 安全的值可能更容易一些。一些例子是:undefinedfunction,(ES6+)symbol,和带有循环引用的object(一个对象结构中的属性互相引用而造成了一个永不终结的循环)。对于标准的JSON结构来说这些都是非法的值,主要是因为它们不能移植到消费JSON值的其他语言中。

JSON.stringify(..)工具在遇到undefinedfunction,和symbol时将会自动地忽略它们。如果在一个array中遇到这样的值,它会被替换为null(这样数组的位置信息就不会改变)。如果在一个object的属性中遇到这样的值,这个属性会被简单地剔除掉。

考虑下面的代码:

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(..)一个带有循环引用的object,就会抛出一个错误。

JSON字符串化有一个特殊行为,如果一个object值定义了一个toJSON()方法,这个方法将会被首先调用,以取得用于序列化的值。

如果你打算JSON字符串化一个可能含有非法JSON值的对象,或者如果这个对象中正好有不适于序列化的值,那么你就应当为它定义一个toJSON()方法,返回这个object的一个 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字符串化的表现形式。这可能是不正确的,除非你事实上想要字符串化string本身(通常不会!)。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]""

在第二个调用中,我们字符串化了返回的string而不是array本身,这可能不是我们想要做的。

既然我们说到了JSON.stringify(..),那么就让我们来讨论一些不那么广为人知,但是仍然很有用的功能吧。

JSON.stringify(..)的第二个参数值是可选的,它称为 替换器(replacer)。这个参数值既可以是一个array也可以是一个function。与toJSON()为序列化准备一个值的方式类似,它提供一种过滤机制,指出一个object的哪一个属性应该或不应该被包含在序列化形式中,来自定义这个object的递归序列化行为。

如果 替换器 是一个array,那么它应当是一个stringarray,它的每一个元素指定了允许被包含在这个object的序列化形式中的属性名称。如果一个属性不存在于这个列表中,那么它就会被跳过。

如果 替换器 是一个function,那么它会为object本身而被调用一次,并且为这个object中的每个属性都被调用一次,而且每次都被传入两个参数值,keyvalue。要在序列化中跳过一个 key,可以返回undefined。否则,就返回被提供的 value

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]}"

注意:function替换器 的情况下,第一次调用时key参数kundefined(而对象a本身会被传入)。if语句会 过滤掉 名称为c的属性。字符串化是递归的,所以数组[1,2,3]会将它的每一个值(12,和3)都作为v传递给 替换器,并将索引值(01,和2)作为k

JSON.stringify(..)还可以接收第三个可选参数值,称为 填充符(space),在对人类友好的输出中它被用做缩进。填充符 可以是一个正整数,用来指示每一级缩进中应当使用多少个空格字符。或者,填充符 可以是一个string,这时每一级缩进将会使用它的前十个字符。

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. stringnumberboolean,和null值在JSON字符串化时,与它们通过ToString抽象操作的规则强制转换为string值的方式基本上是相同的。
  2. 如果传递一个object值给JSON.stringify(..),而这个object上拥有一个toJSON()方法,那么在字符串化之前,toJSON()就会被自动调用来将这个值(某种意义上)“强制转换”为 JSON安全 的。

ToNumber

如果任何非number值,以一种要求它是number的方式被使用,比如数学操作,就会发生ES5语言规范在9.3部分定义的ToNumber抽象操作。

例如,true变为1false变为0undefined变为NaN,而(奇怪的是)null变为0

对于一个string值来说,ToNumber工作起来很大程度上与数字字面量的规则/语法很相似(见第三章)。如果它失败了,结果将是NaN(而不是number字面量中会出现的语法错误)。一个不同之处的例子是,在这个操作中0前缀的八进制数不会被作为八进制数来处理(而仅作为普通的十进制小数),虽然这样的八进制数作为number字面量是合法的。

注意: number字面量文法与用于string值的ToNumber间的区别极其微妙,在这里就不进一步讲解了。更多的信息可以参考ES语言规范的9.3.1部分。

对象(以及数组)将会首先被转换为它们的基本类型值的等价物,而后这个结果值(如果它还不是一个number基本类型)会根据刚才提到的ToNumber规则被强制转换为一个number

为了转换为基本类型值的等价物,ToPrimitive抽象操作(ES5语言规范,9.1部分)将会查询这个值(使用内部的DefaultValue操作 —— ES5语言规范,8.12.8部分),看它有没有valueOf()方法。如果valueOf()可用并且它返回一个基本类型值,那么 这个 值就将用于强制转换。如果不是这样,但toString()可用,那么就由它来提供用于强制转换的值。

如果这两种操作都没提供一个基本类型值,就会抛出一个TypeError

在ES5中,你可以创建这样一个不可强制转换的对象 —— 没有valueOf()toString() —— 如果它的[[Prototype]]的值为null,这通常是通过Object.create(null)来创建的。关于[[Prototype]]的详细信息参见本系列的 this与对象原型

注意: 我们会在本章稍后讲解如何强制转换至number,但对于下面的代码段,想象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

ToBoolean

下面,让我们聊一聊在JS中boolean如何动作。世面上关于这个话题有 许多的困惑和误解,所以集中注意力!

首先而且最重要的是,JS实际上拥有truefalse关键字,而且它们的行为正如你所期望的boolean值一样。一个常见的误解是,值10true/false是相同的。虽然这可能在其他语言中是成立的,但在JS中number就是number,而boolean就是boolean。你可以将1强制转换为true(或反之),或将0强制转换为false(或反之)。但它们不是相同的。

Falsy值

但这还不是故事的结尾。我们需要讨论一下,除了这两个boolean值以外,当你把其他值强制转换为它们的boolean等价物时如何动作。

所有的JavaScript值都可以被划分进两个类别:

  1. 如果被强制转换为boolean,将成为false的值
  2. 其它的一切值(很明显将变为true

我不是在出洋相。JS语言规范给那些在强制转换为boolean值时将会变为false的值定义了一个明确的,小范围的列表。

我们如何才能知道这个列表中的值是什么?在ES5语言规范中,9.2部分定义了一个ToBoolean抽象操作,它讲述了对所有可能的值而言,当你试着强制转换它们为boolean时究竟会发生什么。

从这个表格中,我们得到了下面所谓的“falsy”值列表:

  • undefined
  • null
  • false
  • +0, -0, and NaN
  • ""

就是这些。如果一个值在这个列表中,它就是一个“falsy”值,而且当你在它上面进行boolean强制转换时它会转换为false

通过逻辑上的推论,如果一个值 在这个列表中,那么它一定在 另一个列表 中,也就是我们称为“truthy”值的列表。但是JS没有真正定义一个“truthy”列表。它给出了一些例子,比如它说所有的对象都是truthy,但是语言规范大致上暗示着:任何没有明确地存在于falsy列表中的东西,都是truthy

Falsy对象

等一下,这一节的标题听起来简直是矛盾的。我 刚刚才说过 语言规范将所有对象称为truthy,对吧?应该没有“falsy对象”这样的东西。

这会是什么意思呢?

它可能诱使你认为它意味着一个包装了falsy值(比如""0false)的对象包装器(见第三章)。但别掉到这个 陷阱 中。

注意: 这个可能是一个语言规范的微妙笑话。

考虑下面的代码:

var a = new Boolean( false );
var b = new Number( 0 );
var c = new String( "" );

我们知道这三个值都是包装了明显是falsy值的对象(见第三章)。但这些对象是作为true还是作为false动作呢?这很容易回答:

var d = Boolean( a && b && c );

d; // true

所以,三个都作为true动作,这是唯一能使d得到true的方法。

提示: 注意包在a && b && c表达式外面的Boolean( .. ) —— 你可能想知道为什么它在这儿。我们会在本章稍后回到这个话题,所以先做个心理准备。为了先睹为快,你可以自己试试如果没有Boolean( .. )调用而只有d = a && b && cd是什么。

那么,如果“falsy对象” 不是包装着falsy值的对象,它们是什么鬼东西?

刁钻的地方在于,它们可以出现在你的JS程序中,但它们实际上不是JavaScript本身的一部分。

什么!?

有些特定的情况,在普通的JS语义之上,浏览器已经创建了它们自己的某种 外来 值的行为,也就是这种“falsy对象”的想法。

一个“falsy对象”看起来和动起来都像一个普通对象(属性,等等)的值,但是当你强制转换它为一个boolean时,它会变为一个false值。

为什么!?

最著名的例子是document.all:一个 由DOM(不是JS引擎本身) 给你的JS程序提供的类数组(对象),它向你的JS程序暴露你页面上的元素。它 曾经 像一个普通对象那样动作 —— 是一个truthy。但不再是了。

document.all本身从来就不是“标准的”,而且从很早以前就被废弃/抛弃了。

“那他们就不能删掉它吗?” 对不起,想得不错。但愿它们能。但是世面上有太多的遗产JS代码库依赖于它。

那么,为什么使它像falsy一样动作?因为从document.allboolean的强制转换(比如在if语句中)几乎总是用来检测老的,非标准的IE。

IE从很早以前就开始顺应规范了,而且在许多情况下它在推动web向前发展的作用和其他浏览器一样多,甚至更多。但是所有那些老旧的if (document.all) { /* it's IE */ }代码依然留在世面上,而且大多数可能永远都不会消失。所有这些遗产代码依然假设它们运行在那些给IE用户带来差劲儿的浏览体验的,几十年前的老IE上,

所以,我们不能完全移除document.all,但是IE不再想让if (document.all) { .. }代码继续工作了,这样现代IE的用户就能得到新的,符合标准的代码逻辑。

“我们应当怎么做?” “我知道了!让我们黑进JS的类型系统并假装document.all是falsy!”

呃。这很烂。这是一个大多数JS开发者们都不理解的疯狂的坑。但是其它的替代方案(对上面两败俱伤的问题什么都不做)还要烂得 多那么一点点

所以……这就是我们得到的:由浏览器给JavaScript添加的疯狂,非标准的“falsy对象”。耶!

Truthy值

回到truthy列表。到底什么是truthy值?记住:如果一个值不在falsy列表中,它就是truthy

考虑下面代码:

var a = "false";
var b = "0";
var c = "''";

var d = Boolean( a && b && c );

d;

你期望这里的d是什么值?它要么是true要么是false

它是true。为什么?因为尽管这些string值的内容看起来是falsy值,但是string值本身都是truthy,而这是因为在falsy列表中""是唯一的string值。

那么这些呢?

var a = [];             // 空数组 -- truthy 还是 falsy?
var b = {};             // 空对象 -- truthy 还是 falsy?
var c = function(){};   // 空函数 -- truthy 还是 falsy?

var d = Boolean( a && b && c );

d;

是的,你猜到了,这里的d依然是true。为什么?和前面的原因一样。尽管它们看起来像,但是[]{},和function(){} 不在 falsy列表中,因此它们是truthy值。

换句话说,truthy列表是无限长的。不可能制成一个这样的列表。你只能制造一个falsy列表并查询它。

花五分钟,把falsy列表写在便利贴上,然后粘在你的电脑显示器上,或者如果你愿意就记住它。不管哪种方法,你都可以在自己需要的时候通过简单地查询一个值是否在falsy列表中,来构建一个虚拟的truthy列表。

truthy和falsy的重要性在于,理解如果一个值在被(明确地或隐含地)强制转换为boolean值的话,它将如何动作。现在你的大脑中有了这两个列表,我们可以深入强制转换的例子本身了。

明确的强制转换

明确的 强制转换指的是明显且明确的类型转换。对于大多数开发者来说,有很多类型转换的用法可以清楚地归类于这种 明确的 强制转换。

我们在这里的目标是,在我们的代码中指明一些模式,在这些模式中我们可以清楚明白地将一个值从一种类型转换至另一种类型,以确保不给未来将读到这段代码的开发者留下任何坑。我们越明确,后来的人就越容易读懂我们的代码,也不必费太多的力气去理解我们的意图。

关于 明确的 强制转换可能很难找到什么主要的不同意见,因为它与被广泛接受的静态类型语言中的类型转换的工作方式非常接近。因此,我们理所当然地认为(暂且) 明确的 强制转换可以被认同为不是邪恶的,或没有争议的。虽然我们稍后会回到这个话题。

明确地:Strings <--> Numbers

我们将从最简单,也许是最常见强制转换操作开始:将值在stringnumber表现形式之间进行强制转换。

为了在stringnumber之间进行强制转换,我们使用内建的String(..)Number(..)函数(我们在第三章中所指的“原生构造器”),但 非常重要的是,我们不在它们前面使用new关键字。这样,我们就不是在创建对象包装器。

取而代之的是,我们实际上在两种类型之间进行 明确地强制转换

var a = 42;
var b = String( a );

var c = "3.14";
var d = Number( c );

b; // "42"
d; // 3.14

String(..)使用早先讨论的ToString操作的规则,将任意其它的值强制转换为一个基本类型的string值。Number(..)使用早先讨论过的ToNumber操作的规则,将任意其他的值强制转换为一个基本类型的number值。

我称此为 明确的 强制转换是因为,一般对于大多数开发者来说这是十分明显的:这些操作的最终结果是适当的类型转换。

实际上,这种用法看起来与其他的静态类型语言中的用法非常相像。

举个例子,在C/C++中,你既可以说(int)x也可以说int(x),而且它们都将x中的值转换为一个整数。两种形式都是合法的,但是许多人偏向于后者,它看起来有点儿像一个函数调用。在JavaScript中,当你说Number(x)时,它看起来极其相似。在JS中它实际上是一个函数调用这个事实重要吗?并非如此。

除了String(..)Number(..),还有其他的方法可以把这些值在stringnumber之间进行“明确地”转换:

var a = 42;
var b = a.toString();

var c = "3.14";
var d = +c;

b; // "42"
d; // 3.14

调用a.toString()在表面上是明确的(“toString”意味着“变成一个字符串”是很明白的),但是这里有一些藏起来的隐含性。toString()不能在像42这样的 基本类型 值上调用。所以JS会自动地将42“封箱”在一个对象包装器中(见第三章),这样toString()就可以针对这个对象调用。换句话讲,你可能会叫它“明确的隐含”。

这里的+c+操作符的 一元操作符(操作符只有一个操作数)形式。取代进行数学加法(或字符串连接 —— 见下面的讨论)的是,一元的+明确地将它的操作数(c)强制转换为一个number值。

+c明确的 强制转换吗?这要看你的经验和角度。如果你知道(现在你知道了!)一元+明确地意味着number强制转换,那么它就是相当明确和明显的。但是,如果你以前从没见过它,那么它看起来就极其困惑,晦涩,带有隐含的副作用,等等。

注意: 在开源的JS社区中一般被接受的观点是,一元+是一个 明确的 强制转换形式。

即使你真的喜欢+c这种形式,它绝对会在有的地方看起来非常令人困惑。考虑下面的代码:

var c = "3.14";
var d = 5+ +c;

d; // 8.14

一元-操作符也像+一样进行强制转换,但它还会翻转数字的符号。但是你不能放两个减号--来使符号翻转回来,因为那将被解释为递减操作符。取代它的是,你需要这么做:- -"3.14",在两个减号之间加入空格,这将会使强制转换的结果为3.14

你可能会想到所有种类的可怕组合 —— 一个二元操作符挨着另一个操作符的一元形式。这里有另一个疯狂的例子:

1 + - + + + - + 1;  // 2

当一个一元+(或-)紧邻其他操作符时,你应当强烈地考虑避免使用它。虽然上面的代码可以工作,但几乎全世界都认为它是一个坏主意。即使是d = +c(或者d =+ c!)都太容易与d += c像混淆了,而后者完全是不同的东西!

注意: 一元+的另一个极端使人困惑的地方是,被用于紧挨着另一个将要作为++递增操作符和--递减操作符的操作数。例如:a +++ba + ++b,和a + + +b。更多关于++的信息,参见第五章的“表达式副作用”。

记住,我们正努力变得明确并 减少 困惑,不是把事情弄得更糟!

Datenumber

另一个一元+操作符的常见用法是将一个Date对象强制转换为一个number,其结果是这个日期/时间值的unix时间戳(从世界协调时间的1970年1月1日0点开始计算,经过的毫秒数)表现形式:

var d = new Date( "Mon, 18 Aug 2014 08:53:06 CDT" );

+d; // 1408369986000

这种习惯性用法经常用于取得当前的 现在 时刻的时间戳,比如:

var timestamp = +new Date();

注意: 一些开发者知道一个JavaScript中的特别的语法“技巧”,就是在构造器调用(一个带有new的函数调用)中如果没有参数值要传递的话,()可选的。所以你可能遇到var timestamp = +new Date;形式。然而,不是所有的开发者都同意忽略()可以增强可读性,因为它是一种不寻常的语法特例,只能适用于new fn()调用形式,而不能用于普通的fn()调用形式。

但强制转换不是从Date对象中取得时间戳的唯一方法。一个不使用强制转换的方式可能更好,因为它更加明确:

var timestamp = new Date().getTime();
// var timestamp = (new Date()).getTime();
// var timestamp = (new Date).getTime();

但是一个 更更好的 不使用强制转换的选择是使用ES5加入的Date.now()静态函数:

var timestamp = Date.now();

而且如果你想要为老版本的浏览器填补Date.now()的话,也十分简单:

if (!Date.now) {
    Date.now = function() {
        return +new Date();
    };
}

我推荐跳过与日期有关的强制转换形式。使用Date.now()来取得当前 现在 的时间戳,而使用new Date( .. ).getTime()来取得一个需要你指定的 非现在 日期/时间的时间戳。

奇异的~

一个经常被忽视并通常让人糊涂的JS强制操作符是波浪线~操作符(也叫“按位取反”,“比特非”)。许多理解它在做什么的人也总是想要避开它。但是为了坚持我们在本书和本系列中的精神,让我们深入并找出~是否有一些对我们有用的东西。

在第二章的“32位(有符号)整数”一节,我们讲解了在JS中位操作符是如何仅为32位操作定义的,这意味着我们强制它们的操作数遵循32位值的表现形式。这个规则如何发生是由ToInt32抽象操作(ES5语言规范,9.5部分)控制的。

ToInt32首先进行ToNumber强制转换,这就是说如果值是"123",它在ToInt32规则实施之前会首先变成123

虽然它本身没有 技术上进行 强制转换(因为类型没有改变),但对一些特定的特殊number值使用位操作符(比如|~)会产生一种强制转换效果,这种效果的结果是一个不同的number值。

举例来说,让我们首先考虑惯用的空操作0 | x(在第二种章有展示)中使用的|“比特或”操作符,它实质上仅仅进行ToInt32转换:

0 | -0;         // 0
0 | NaN;        // 0
0 | Infinity;   // 0
0 | -Infinity;  // 0

这些特殊的数字是不可用32位表现的(因为它们源自64位的IEEE 754标准 —— 见第二章),所以ToInt32将这些值的结果指定为0

有争议的是,0 | __是否是一种ToInt32强制转换操作的 明确的 形式,还是更倾向于 隐含。从语言规范的角度来说,毫无疑问是 明确的,但是如果你没有在这样的层次上理解位操作,它就可能看起来有点像 隐含的 魔法。不管怎样,为了与本章中其他的断言保持一致,我们称它为 明确的

那么,让我们把注意力转回~~操作符首先将值“强制转换”为一个32位number值,然后实施按位取反(翻转每一个比特位)。

注意: 这与!不仅强制转换它的值为boolean而且还翻转它的每一位很相似(见后面关于“一元!”的讨论)。

但是……什么!?为什么我们要关心被翻转的比特位?这是一些相当特殊的,微妙的东西。JS开发者需要推理个别比特位是十分少见的。

另一种考虑~定义的方法是,~源自学校中的计算机科学/离散数学:~进行二进制取补操作。太好了,谢谢,我完全明白了!

我们再试一次:~x大致与-(x+1)相同。这很奇怪,但是稍微容易推理一些。所以:

~42;    // -(42+1) ==> -43

你可能还在想~这个鬼东西到底和什么有关,或者对于强制转换的讨论它究竟有什么要紧。让我们快速进入要点。

考虑一下-(x+1)。通过进行这个操作,能够产生结果0(或者从技术上说-0!)的唯一的值是什么?-1。换句话说,~用于一个范围的number值时,将会为输入值-1产生一个falsy(很容易强制转换为false)的0,而为任意其他的输入产生truthy的number

为什么这要紧?

-1通常称为一个“哨兵值”,它基本上意味着一个在同类型值(number)的更大的集合中被赋予了任意的语义。在C语言中许多函数使用哨兵值-1,它们返回>= 0的值表示“成功”,返回-1表示“失败”。

JavaScript在定义string操作indexOf(..)时采纳了这种先例,它搜索一个子字符串,如果找到就返回它从0开始计算的索引位置,没有找到的话就返回-1

这样的情况很常见:不仅仅将indexOf(..)作为取得位置的操作,而且作为检查一个子字符串存在/不存在于另一个string中的boolean值。这就是开发者们通常如何进行这样的检查:

var a = "Hello World";

if (a.indexOf( "lo" ) >= 0) {   // true
    // 找到了!
}
if (a.indexOf( "lo" ) != -1) {  // true
    // 找到了
}

if (a.indexOf( "ol" ) < 0) {    // true
    // 没找到!
}
if (a.indexOf( "ol" ) == -1) {  // true
    // 没找到!
}

我感觉看着>= 0== -1有些恶心。它基本上是一种“抽象泄漏”,这里它将底层的实现行为 —— 使用哨兵值-1表示“失败” —— 泄漏到我的代码中。我倒是乐意隐藏这样的细节。

现在,我们终于看到为什~可以帮到我们了!将~indexOf()一起使用可以将值“强制转换”(实际上只是变形)为 可以适当地强制转换为boolean的值

var a = "Hello World";

~a.indexOf( "lo" );         // -4   <-- truthy!

if (~a.indexOf( "lo" )) {   // true
    // 找到了!
}

~a.indexOf( "ol" );         // 0    <-- falsy!
!~a.indexOf( "ol" );        // true

if (!~a.indexOf( "ol" )) {  // true
    // 没找到!
}

~拿到indexOf(..)的返回值并将它变形:对于“失败”的-1我们得到falsy的0,而其他的值都是truthy。

注意: ~的假想算法-(x+1)暗示着~-1-0,但是实际上它产生0,因为底层的操作其实是按位的,不是数学操作。

技术上讲,if (~a.indexOf(..))仍然依靠 隐含的 强制转换将它的结果0变为false或非零变为true。但总的来说,对我而言~更像一种 明确的 强制转换机制,只要你知道在这种惯用法中它的意图是什么。

我感觉这样的代码要比前面凌乱的>= 0 / == -1更干净。

截断比特位

在你遇到的代码中,还有一个地方可能出现~:一些开发者使用双波浪线~~来截断一个number的小数部分(也就是,将它“强制转换”为一个“整数”)。这通常(虽然是错误的)被说成与调用Math.floor(..)的结果相同。

~ ~的工作方式是,第一个~实施ToInt32“强制转换”并进行按位取反,然后第二个~进行另一次按位取反,将每一个比特位都翻转回原来的状态。于是最终的结果就是ToInt32“强制转换”(也叫截断)。

注意: ~~的按位双翻转,与双否定!!的行为非常相似,它将在稍后的“明确地:* --> Boolean”一节中讲解。

然而,~~需要一些注意/澄清。首先,它仅在32位值上可以可靠地工作。但更重要的是,它在负数上工作的方式与Math.floor(..)不同!

Math.floor( -49.6 );    // -50
~~-49.6;                // -49

Math.floor(..)的不同放在一边,~~x可以将值截断为一个(32位)整数。但是x | 0也可以,而且看起来还(稍微)省事儿 一些。

那么,为什么你可能会选择~~x而不是x | 0?操作符优先权(见第五章):

~~1E20 / 10;        // 166199296

1E20 | 0 / 10;      // 1661992960
(1E20 | 0) / 10;    // 166199296

正如这里给出的其他建议一样,仅在读/写这样的代码的每一个人都知道这些操作符如何工作的情况下,才将~~~作为“强制转换”和将值变形的明确机制。

明确地:解析数字字符串

将一个string强制转换为一个number的类似结果,可以通过从string的字符内容中解析(parsing)出一个number得到。然而在这种解析和我们上面讲解的类型转换之间存在着区别。

考虑下面的代码:

var a = "42";
var b = "42px";

Number( a );    // 42
parseInt( a );  // 42

Number( b );    // NaN
parseInt( b );  // 42

从一个字符串中解析出一个数字是 容忍 非数字字符的 —— 从左到右,如果遇到非数字字符就停止解析 —— 而强制转换是 不容忍 并且会失败而得出值NaN

解析不应当被视为强制转换的替代品。这两种任务虽然相似,但是有着不同的目的。当你不知道/不关心右手边可能有什么其他的非数字字符时,你可以将一个string作为number解析。当只有数字才是可接受的值,而且像"42px"这样的东西作为数字应当被排除时,就强制转换一个string(变为一个number)。

提示: parseInt(..)有一个孪生兄弟,parseFloat(..),它(听起来)从一个字符串中拉出一个浮点数。

不要忘了parseInt(..)工作在string值上。向parseInt(..)传递一个number绝对没有任何意义。传递其他任何类型也都没有意义,比如truefunction(){..}[1,2,3]

如果你传入一个非string,你所传入的值首先将自动地被强制转换为一个string(见早先的“ToString”),这很明显是一种隐藏的 隐含 强制转换。在你的程序中依赖这样的行为真的是一个坏主意,所以永远也不要将parseInt(..)与非string值一起使用。

在ES5之前,parseInt(..)还存在另外一个坑,这曾是许多JS程序的bug的根源。如果你不传递第二个参数来指定使用哪种进制(也叫基数)来翻译数字的string内容,parseInt(..)将会根据开头的字符进行猜测。

如果开头的两个字符是"0x""0X",那么猜测(根据惯例)将是你想要将这个string翻译为一个16进制number。否则,如果第一个字符是"0",那么猜测(也是根据惯例)将是你想要将这个string翻译成8进制number

16进制的string(以0x0X开头)没那么容易搞混。但是事实证明8进制数字的猜测过于常见了。比如:

var hour = parseInt( selectedHour.value );
var minute = parseInt( selectedMinute.value );

console.log( "The time you selected was: " + hour + ":" + minute);

看起来无害,对吧?试着在小时上选择08在分钟上选择09。你会得到0:0。为什么?因为89都不是合法的8进制数。

ES5之前的修改很简单,但是很容易忘:总是在第二个参数值上传递10。这完全是安全的:

var hour = parseInt( selectedHour.value, 10 );
var minute = parseInt( selectedMiniute.value, 10 );

在ES5中,parseInt(..)不再猜测八进制数了。除非你指定,否则它会假定为10进制(或者为"0x"前缀猜测16进制数)。这好多了。只是要小心,如果你的代码不得不运行在前ES5环境中,你仍然需要为基数传递10

解析非字符串

几年以前有一个挖苦JS的玩笑,使一个关于parseInt(..)行为的一个臭名昭著的例子备受关注,它取笑JS的这个行为:

parseInt( 1/0, 19 ); // 18

这里面设想(但完全不合法)的断言是,“如果我传入一个无限大,并从中解析出一个整数的话,我应该得到一个无限大,不是18”。没错,JS一定是疯了才得出这个结果,对吧?

虽然这是个明显故意造成的,不真实的例子,但是让我们放纵这种疯狂一小会儿,来检视一下JS是否真的那么疯狂。

首先,这其中最明显的原罪是将一个非string传入了parseInt(..)。这是不对的。这么做是自找麻烦。但就算你这么做了,JS也会礼貌地将你传入的东西强制转换为它可以解析的string

有些人可能会争论说这是一种不合理的行为,parseInt(..)应当拒绝在一个非string值上操作。它应该抛出一个错误吗?坦白地说,像Java那样。但是一想到JS应当开始在满世界抛出错误,以至于几乎每一行代码都需要用try..catch围起来,我就不寒而栗。

它应当返回NaN吗?也许。但是……要是这样呢:

parseInt( new String( "42") );

这也应当失败吗?它是一个非string值啊。如果你想让String对象包装器被开箱成"42",那么42先变成"42",以使42可以被解析回来就那么不寻常吗?

我会争论说,这种可能发生的半 明确隐含 的强制转换经常可以成为非常有用的东西。比如:

var a = {
    num: 21,
    toString: function() { return String( this.num * 2 ); }
};

parseInt( a ); // 42

事实上parseInt(..)将它的值强制转换为string来实施解析是十分合理的。如果你传垃圾进去,那么你就会得到垃圾,不要责备垃圾桶 —— 它只是忠实地尽自己的责任。

那么,如果你传入像Infinity(很明显是1 / 0的结果)这样的值,对于它的强制转换来说哪种string表现形式最有道理呢?我脑中只有两种合理的选择:"Infinity""∞"。JS选择了"Infinity"。我很高兴它这么选。

我认为在JS中 所有的值 都有某种默认的string表现形式是一件好事,这样它们就不是我们不能调试和推理的神秘黑箱了。

现在,关于19进制呢?很明显,这完全是伪命题和造作。没有真实的JS程序使用19进制。那太荒谬了。但是,让我们再一次放任这种荒谬。在19进制中,合法的数字字符是0 - 9a - i(大小写无关)。

那么,回到我们的parseInt( 1/0, 19 )例子。它实质上是parseInt( "Infinity", 19 )。它如何解析?第一个字符是"I",在愚蠢的19进制中是值18。第二个字符"n"不再合法的数字字符集内,所以这样的解析就礼貌地停止了,就像它在"42px"中遇到"p"那样。

结果呢?18。正如它应该的那样。对JS来说,并非一个错误或者Infinity本身,而是将我们带到这里的一系列的行为才是 非常重要 的,不应当那么简单地被丢弃。

其他关于parseInt(..)行为的,令人吃惊但又十分合理的例子还包括:

parseInt( 0.000008 );       // 0   ("0" from "0.000008")
parseInt( 0.0000008 );      // 8   ("8" from "8e-7")
parseInt( false, 16 );      // 250 ("fa" from "false")
parseInt( parseInt, 16 );   // 15  ("f" from "function..")

parseInt( "0x10" );         // 16
parseInt( "103", 2 );       // 2

其实parseInt(..)在它的行为上是相当可预见和一致的。如果你正确地使用它,你就能得到合理的结果。如果你不正确地使用它,那么你得到的疯狂结果并不是JavaScript的错。

明确地:* --> Boolean

现在,我们来检视从任意的非boolean值到一个boolean值的强制转换。

正如上面的String(..)Number(..)Boolean(..)(当然,不带new!)是强制进行ToBoolean转换的明确方法:

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

虽然Boolean(..)是非常明确的,但是它并不常见也不为人所惯用。

正如一元+操作符将一个值强制转换为一个number(参见上面的讨论),一元的!否定操作符可以将一个值明确地强制转换为一个boolean问题 是它还将值从truthy翻转为falsy,或反之。所以,大多数JS开发者使用!!双否定操作符进行boolean强制转换,因为第二个!将会把它翻转回原本的true或false:

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

没有Boolean(..)!!的话,任何这些ToBoolean强制转换都将 隐含地 发生,比如在一个if (..) ..语句这样使用boolean的上下文中。但这里的目标是,明确地强制一个值成为boolean来使ToBoolean强制转换的意图显得明明白白。

另一个ToBoolean强制转换的用例是,如果你想在数据结构的JSON序列化中强制转换一个true/false

var a = [
    1,
    function(){ /*..*/ },
    2,
    function(){ /*..*/ }
];

JSON.stringify( a ); // "[1,null,2,null]"

JSON.stringify( a, function(key,val){
    if (typeof val == "function") {
        // 强制函数进行 `ToBoolean` 转换
        return !!val;
    }
    else {
        return val;
    }
} );
// "[1,true,2,true]"

如果你是从Java来到JavaScript的话,你可能会认得这个惯用法:

var a = 42;

var b = a ? true : false;

? :三元操作符将会测试a的真假,然后根据这个测试的结果相应地将truefalse赋值给b

表面上,这个惯用法看起来是一种 明确的 ToBoolean类型强制转换形式,因为很明显它操作的结果要么是true要么是false

然而,这里有一个隐藏的 隐含 强制转换,就是表达式a不得不首先被强制转换为boolean来进行真假测试。我称这种惯用法为“明确地隐含”。另外,我建议你在JavaScript中 完全避免这种惯用法。它不会提供真正的好处,而且会让事情变得更糟。

对于 明确的 强制转换Boolean(a)!!a是好得多的选项。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容