JavaScript 强制类型转换

前言

关于强制类型转换是一个设计上的缺陷还是有用的特性,这一争论从JavaScript 诞生之日起就开始了。在很多的 JavaScript 书籍中强制类型转换被说成是危险、晦涩和糟糕的设计。

由于这一特性,诞生了有意思的题:

if (a == 1 && a == 2) {
  alert('hello world!');
}

1、类型

JavaScript的类型分为原始类型与引用类型。

  • 原始类型
    • 空值( null )
    • 未定义( undefined )
    • 布尔值( boolean )
    • 数字( number )
    • 字符串( string )
    • 符号( symbol ,ES6 中新增)
  • 引用类型
    • 对象( object )
      • 数组 (array)
      • 方法 (function)

我们可以用 typeof 运算符来查看值的类型,它返回的是类型的字符串值。

typeof undefined === "undefined"; // true
typeof true === "boolean"; // true
typeof 42 === "number"; // true
typeof "42" === "string"; // true
typeof { life: 42 } === "object"; // true
// ES6中新加入的类型
typeof Symbol() === "symbol"; // true

你可能注意到 null 类型不在此列。它比较特殊, typeof 对它的处理有问题:

typeof null === "object"; // true

这个 bug 由来已久,在 JavaScript 中已经存在了将近
二十年,也许永远也不会修复,因为这牵涉到太多的 Web 系统,“修复”它会产生更多的bug,令许多系统无法正常工作。

再来看看数组。JavaScript 支持数组,那么它是否也是一个特殊类型?

typeof [1,2,3] === "object"; // true

不,数组也是对象。确切地说,它也是 object 的一个“子类型”

最后看一组

typeof false; // "boolean"
typeof new Boolean(false); // "object"

typeof 1; // "number"
typeof new Number(1); // "object"

2、类型转换

将值从一种类型转换为另一种类型通常称为类型转换。
类型转换又分为隐式显式,如:

var a = 42;
var b = a + ""; // 隐式强制类型转换
var c = String( a ); // 显式强制类型转换

2.1、类型抽象操作

ES5 规范第 9 节中定义了一些“抽象操作”(即“仅供内部使用的操作”)和转换规则。即:

  • ToString - 将非字符串转为字符串。
  • ToNumber - 将非数字转为数字。
  • ToBoolean - 将非布尔值转为布尔值。
  • ToPrimitive - 将引用类型值转为原始类型值。

这些操作都是由js内部实现,并非实际的函数。接下来我们进行解析其内部实现。

2.1.1、ToString

抽象操作 ToString ,它负责处理非字符串到字符串的强制类型转换。

基本类型值的字符串化规则为: null 转换为 "null" , undefined 转换为 "undefined" , true 转换为 "true" 。数值遵循常规规则。

2.1.2、ToNumber

有时我们需要将非数字值当作数字来使用,比如数学运算。为此 ES5 规范在 9.3 节定义了抽象操作 ToNumber 。

其中 true 转换为 1 , false 转换为 0 。 undefined 转换为 NaN , null 转换为 0 。

ToNumber操作字符串时,会判断该字符是否符合数字值格式。处理失败时返回 NaN。其处理规则与Number函数相似:

  Number( "" ); // 0
  Number( "a1" ); // NaN

将引用类型强制类型转换为 string、number 是通过 ToPrimitive 抽象操作来完成的,我们在此略过,稍后会详细介绍。

2.1.3、 ToBoolean

JavaScript 中有两个关键词 true 和 false ,分别代表布尔类型中的真和假。我们常误以为数值 1 和 0 分别等同于 true 和 false 。在有些语言中可能是这样,但在 JavaScript 中布尔值和数字是不一样的。

假值
  • undefined
  • null
  • ""
  • false
  • +0、-0 和 NaN

假值的布尔强制转换的结果均为 false 。

真值

假值列表以外的值都是真值。即布尔强制转换后均为true。
所以空数组[]、空对象{}、字符串"null"、字符串"undefined" 等都为真值。

2.1.4、ToPrimitive

抽象操作 ToPrimitive ,它负责将引用类型值转化为基本类型值。其具体规则为:
1、检查该值是否有 valueOf() 方法
2、如果有并且返回基本类型值,就使用该值进行强制类型转换
3、如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。
4、如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。

var a = {
  valueOf: function(){
    return "42";
  }
};
Number( a ); // a.valueOf() -> "42" -> 42
String( a ); // a.toString() -> "[object Object]"

var b = {
  toString: function(){
    return "42";
  }
};
Number( b ); // b.valueOf() -> {};   b.toString() -> "42" -> 42
String( b ); // b.toString() -> "42"

var c = [4,2];
c.toString = function(){
  return this.join( "" ); // "42"
};
Number( c ); // c.toString() -> "42" -> 42

Number( [4, 2] ); // [4,2].toString() -> "4,2" -> NaN

a + "" (隐式)和前面的 String(a) (显式)之间有一个细微的差别需要注意:

var a = {
  valueOf: function() { return 42; },
  toString: function() { return 4; }
};
a + ""; // a.valueOf() -> 42 -> "42"
String( a ); // a.toString() -> 4 -> "4"

2.2、显式类型转换

函数:

  • Number 用于转换为数值
  • String 用于转换为字符串
  • Boolean 用于转换为布尔值
  • Object.prototype.toString 用于将对象转换为字符串
  • Array.prototype.toString 用于将数组转换为字符串
  • Number.prototype.toString 用于将数值转换为字符串
  • ...

操作符:

  • !x 用于将x转为非布尔值,非。
  • (+x) 用于将x转为数值。

2.3、隐式类型转换

隐式强制类型转换指的是那些隐蔽的强制类型转换,副作用也不是很明显。换句话说,你自己觉得不够明显的强制类型转换都可以算作隐式强制类型转换。

主要分为:

  • 部分类型值之间的加减乘除等(算式)
  • 部分类型值之间比较大小
  • 抽象相等 ==
  • 条件语句 (其他类型值转布尔值)

这里主要讲算式与弱相等。

2.3.1、算式

原始类型

数字 + 字符串
number + string = ToString(number) + string

1 + '2'; // 

数字 + 布尔
number + boolean = number + ToNumber(boolean)

1 + true; // 2 + ToNumber()

字符串 + 布尔
string + boolean = string + ToString( boolean )

'1' + true; // "1true"

原始类型 -x÷ 原始类型
primitive -x÷ primitive = ToNumber(primitive) -x÷ ToNumber(primitive)

引用类型

引用类型在参与计算时,会先经过ToPrimitive转换成原始类型,然后按照上述规则参与计算。

1 + {}; // "1[object Object]"
({toString(){return 1}}) + ({toString(){return 1}}); // 2
({toString(){return 1}}) + ({toString(){return '1'}}); // "11"

2.3.2、抽象相等

抽象相等也称为宽松相等,它的规则正是隐式强制类型转换被诟病的原因,很容易导致 bug,实际上他的规则非常简单。

首先,有几个非常规的情况需要注意。

  • NaN 不等于 NaN
  • +0 等于 -0
定义

== 在比较两个不同类型的值时会发生隐式强制类型转换,会将其中之一或两者都转换为相同的类型后再进行比较。

1、字符串和数字之间的相等比较

这里用字符串和数字的例子来解释 == 中的强制类型转换:

var a = 42;
var b = "42";
a === b; // false
a == b; // true

因为没有强制类型转换,所以 a === b 为 false , 42 和 "42" 不相等。

而 a == b 是宽松相等,即如果两个值的类型不同,则对其中之一或两者都进行强制类型转换。

具体怎么转换?是 a 从 42 转换为字符串,还是 b 从 "42" 转换为数字?

ES5 规范 11.9.3.4-5 这样定义:
(1) 如果 Type(x) 是数字, Type(y) 是字符串,则返回 x == ToNumber(y)
的结果。
(2) 如果 Type(x) 是字符串, Type(y) 是数字,则返回 ToNumber(x) == y
的结果。

其中ToNumber 为抽象操作的规则前面已经介绍过。

2、其他类型和布尔类型之间的相等比较

== 最容易出错的一个地方是 true 和 false 与其他类型之间的相等比较。
例如:

var a = "42";
var b = true;
a == b; // false

我们都知道 "42" 是一个真值(见本章前面部分),为什么 == 的结果不是 true 呢?原因既简单又复杂,让人很容易掉坑里,很多 JavaScript 开发人员对这个地方并未引起足够的重视。

规范 11.9.3.6-7 是这样说的:
(1) 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果
(2) 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。

根据规则
"42" == true ,经过ToNumber(true) -> 1;
"42" == 1,再经过ToNumber("42") -> 42;
42 == 1,所以输出false。

3、null 和 undefined 之间的相等比较

null 和 undefined 之间的 == 也涉及隐式强制类型转换。

ES5 规范 11.9.3.2-3 规定:
(1) 如果 x 为 null , y 为 undefined ,则结果为 true 。
(2) 如果 x 为 undefined , y 为 null ,则结果为 true 。

在 == 中 null 和 undefined 相等(它们也与其自身相等),除此之外其他值都不存在这种情况。
也就是说除null和undefined外的所有类型值都不与他们抽象相等。

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
4、对象和非对象之间的相等比较

关于引用类型(对象 / 函数 / 数组)和原始类型值(字符串 / 数字 / 布尔值)之间的相等比较。

ES5 规范 11.9.3.8-9 做如下规定:
(1) 如果 Type(x) 是字符串或数字, Type(y) 是对象,则返回 x == ToPrimitive(y)
的结果;
(2) 如果 Type(x) 是对象, Type(y) 是字符串或数字,则返回 ToPromitive(x) == y
的结果。

例如

var a = 42;
var b = [ 42 ];
a == b; // true

[ 42 ] == 42, 经过ToPrimitive([ 42 ]) -> [ 42 ].toString() -> "42";
"42" == 42, 再经过ToNumber("42") -> 42;
42 == 42; 返回true

5、其他少见的情况
Number.prototype.valueOf = function() {
  return 3;
};
new Number( 2 ) == 3; // true

而 2 == 3 不会有这种问题,因为 2 和 3 都是数字基本类型值,不会调用
Number.prototype.valueOf() 方法。而 Number(2) 涉及 ToPrimitive 强制类型
转换,因此会调用 valueOf() 。

还有文章开头的题:

if (a == 1 && a == 2) {
  alert('hello world!');
}

你也许觉得这不可能,因为 a 不会同时等于 2 和 3 。但“同时”一词并不准确,因为 a == 2 在 a == 3 之前执行。
如果让 a.valueOf() 每次调用都产生副作用,比如第一次返回 2 ,第二次返回 3 ,就会出现这样的情况。

var i = 1;
Number.prototype.valueOf = function() {
  return i++;
};
var a = new Number( 42 );
if (a == 1 && a == 2) {
  console.log( "Yep, this happened." );
  alert('hello world!');
}

再次强调,千万不要这样,也不要因此而抱怨强制类型转换。对一种机制的滥用并不能成为诟病它的借口。我们应该正确合理地运用强制类型转换,避免这些极端的情况。

总结

本文介绍了 JavaScript 的数据类型之间的转换:包括显式和隐式。

在处理强制类型转换的时候要十分小心,尤其是隐式强制类型转换。在编码的时候,要知其然,还要知其所以然,并努力让代码清晰易读。

本文大部分例子与描述来自 《你不知道的JavaScript》。

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

推荐阅读更多精彩内容