JavaScript 强制类型转换

JavaScript 强制类型转换

做为 JavaScript 程序员,你必定获取过当前系统的时间戳。在 ES5 引入 Date.now() 静态方法以前,下面这段代码你必定不会陌生:javascript

var timestamp = +new Date(); // timestamp 就是当前的系统时间戳,单位是 ms

你确定据说过 JavaScript 的强制类型转换,你能指出这段代码里哪里用到了强制类型转换吗?java

几乎全部 JavaScript 程序员都接触过强制类型转换 —— 不管是有意的仍是无心的。强制类型转换致使了不少隐蔽的 BUG,可是强制类型转换同时也是一种很是有用的技术,咱们不该该因噎废食。程序员

在本文中咱们来详细探讨一下 JavaScript 的强制类型转换,以便咱们能够在避免踩坑的状况下最大化利用强制类型转换的便捷。数组

类型转换和强制类型转换

类型转换发生在静态类型语言的编译阶段,而强制类型转换发生在动态类型语言的运行时(runtime),所以在 JavaScript 中只有强制类型转换。浏览器

强制类型转换通常还可分为 隐式强制类型转换(implicit coercion和 _显式强制类型转换(explicit coercion)_。函数

从代码中能够看出转换操做是隐式的仍是显式的,显式强制类型转换很容易就能看出来,而隐式强制类型转换可能就没有这么明显了。工具

好比:this

var a = 21;

var b = a + '';

var c = String(a);

对于变量 b 而言,这次强制类型转换是隐式的。+ 操做符在其中一个操做数是字符串时进行的是字符串拼接操做,所以数字 21 会被转换为相应的字符串 "21"prototype

然而 String(21) 则是很是典型的显式强制类型转换。日志

这两种强制转换类型的操做都是将数字转换为字符串。

不过“显式”仍是“隐式”都是相对而言的。好比若是你知道 a + "" 是怎么回事,那么对你来讲这可能就是“显式”的。反之,若是你不知道 String(a) 能够用来字符串强制类型转换,那么它对你来讲可能就是“隐式”的。

抽象值操做

在介绍强制类型转换以前,咱们须要先了解一下字符串、数字和布尔值之间类型转换的基本规则。在 ES5 规范中定义了一些“抽象操做”和转换规则,在这咱们介绍一下 ToPrimitiveToStringToNumberToBoolean。注意,这些操做仅供引擎内部使用,和平时 JavaScript 代码中的 .toString() 等操做不同。

ToPrimitive

你能够将 ToPrimitive 操做看做是一个函数,它接受一个 input 参数和一个可选的 PreferredType 参数。ToPrimitive 抽象操做会将 input 参数转换成一个原始值。若是一个对象能够转换成不止一种原始值,可使用 PreferredType 指定抽象操做的返回类型。

根据不一样的输入类型,ToPrimitive 的转换操做以下:

输入类型 操做 / 返回值
Undefined 自身(无转换操做)
Null 自身(无转换操做)
Boolean 自身(无转换操做)
Number 自身(无转换操做)
String 自身(无转换操做)
Object 返回 Objectdefault valueObjectdefault value 经过在该对象上传递 PreferredType 参数给内部操做 [[DefaultValue]](hint) 得到。[[DefaultValue]](hint) 的实现请往下看。

[[DefaultValue]](hint) 内部操做

在对象 O 上调用内部操做 [[DefaultValue]] 时,根据 hint 的不一样,其执行的操做也不一样,简化版(具体可参考 ES5 规范 8.12.8 节)以下:

  • 若是 hintString

    • 若是 OtoString 属性是函数;

      • O 设置为 this 值并调用 toString 方法,将返回值赋值给 val
      • 若是 val 是原始值类型则返回;
    • 若是 OvalueOf 属性是函数;

      • O 设置为 this 值并调用 valueOf 方法,将返回值赋值给 val
      • 若是 val 是原始值类型则返回;
    • 抛出 TypeError 错误。
  • 若是 hintNumber

    • 若是 OvalueOf 属性是函数;

      • O 设置为 this 值并调用 valueOf 方法,将返回值赋值给 val
      • 若是 val 是原始值类型则返回;
    • 若是 OtoString 属性是函数;

      • O 设置为 this 值并调用 toString 方法,将返回值赋值给 val
      • 若是 val 是原始值类型则返回;
    • 抛出 TypeError 错误。
  • 若是 hint 参数为空;

    • 若是 ODate 对象,则和 hintString 时一致;
    • 不然和 hintNumber 时一致。

ToString

原始值的字符串化的规则以下:

  • null 转化为 "null"
  • undefined 转化为 "undefined"
  • true 转化为 "true"
  • false 转化为 "false";
  • 数字的字符串化遵循通用规则,如 21 转化为 "21",极大或者极小的数字使用指数形式,如:

    var num = 3.912 * Math.pow(10, 50);
    
    num.toString(); // "3.912e50"
  • 对于普通对象,若是对象有自定义的 toString() 方法,字符串化时就会调用该自定义方法并使用其返回值,不然返回的是内部属性 [[Class]] 的值,好比 "object [Object]"。须要注意的是,数组默认的 toString() 方法通过了从新定义,其会将全部元素字符串化以后再用 "," 链接起来,如:

    var arr = [1, 2, 3];
    
    arr.toString(); // "1,2,3"

ToNumber

在 ES5 规范中定义的 ToNumber 操做能够将非数字值转换为数字。其规则以下:

  • true 转换为 1
  • false 转换为 0
  • undefined 转换为 NaN
  • null 转换为 0
  • 针对字符串的转换基本遵循数字常量的相关规则。处理失败则返回 NaN
  • 对象会先被转换为原始值,若是返回的是非数字的原始值,则再遵循上述规则将其强制转换为数字。

在将某个值转换为原始值的时候,会首先执行抽象操做 ToPrimitive,若是结果是数字则直接返回,若是是字符串再根据相应规则转换为数字。

参照上述规则,如今咱们能够一步一步来解释本文开头的那行代码了。

var timestamp = +new Date(); // timestamp 就是当前的系统时间戳,单位是 ms

其执行步骤以下:

  • new 操做符比 + 操做符优先级更高,所以先执行 new Date() 操做,生成一个新的 Date 实例;
  • 一元操做符 + 在其操做数为非数字时,会对其进行隐式强制类型转换数字

    • hintNumber

      • Date 实例的 valueOf 属性指向的是 Date.prototype.valueOf,是一个函数;
      • this 指向 Date 实例并调用 valueOf 函数,得到返回值;
      • 返回值是一个数字,直接将其返回;
  • 隐式强制类型转换得到的值赋值给 timestamp 变量。

有了以上知识,咱们就能够实现一些比较好玩的东西了,好比将数字和对象相加:

var a = {
  valueOf: function() {
    return 18;
  }
};

var b = 20;

+a; // 18
Number(a); // 18
a + b; // 38
a - b; // -2

顺带提一下,从 ES5 开始,使用 Object.create(null) 建立的对象,其 [[Prototype]] 属性为 null 所以没有 valueOf()toString() 方法,所以没法进行强制类型转换。请看以下示例:

var a = {};
var b = Object.create(null);

+a; // NaN
+b; // Uncaught TypeError: Cannot convert object to primitive value
a + ''; // "[object Object]"
b + ''; // Uncaught TypeError: Cannot convert object to primitive value

ToBoolean

JavaScript 中有两个关键字 truefalse,分别表示布尔类型的真和假。咱们常常会在 if 语句中将 0 做为假值条件,1 做为真值条件,这也利用了强制类型转换。咱们能够将 true 强制类型转换为 1false 强制类型转换为 0,反之亦然。然而 true1 并非一回事,false0 也同样。

假值

在 JavaScript 中值能够分为两类:

  • 能够被强制类型转换为 false 的值
  • 其余(被强制类型转换为 true 的值)

在 ES5 规范中下列值被定义为假值:

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

假值的布尔强制类型转换结果为 false

在假值列表之外的值都是真值。

⚠️ 例外

规则不免有例外。刚说了除了假值列表之外的全部其余值都是真值,然而你能够在现代浏览器的控制台中执行下面几行代码试试:

Boolean(document.all);
typeof document.all;

获得的结果应该是 false"undefined"。然而若是你直接执行 document.all 获得的是一个类数组对象,包含了页面中全部的元素。document.all 实际上不能算是 JavaScript 语言的范畴,这是浏览器在特定条件下建立一些外来(exotic)值,这些就是“假值对象”。

假值对象看起来和普通对象并没有二致(都有属性,document.all 甚至能够展为数组),可是其强制类型转换的结果倒是 false

在 ES5 规范中,document.all 是惟一一个例外,其缘由主要是为了兼容性。由于老代码可能会这么判断是不是 IE:

if (document.all) {
  // Internet Explorer
}

在老版本的 IE 中,document.all 是一个对象,其强制类型转换结果为 true,而在现代浏览器中,其强制转换结果为 false

真值

除了假值之外都是真值。

好比:

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

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

d; // ?

dtrue 仍是 false 呢?

答案是 true。这些值都是真值,相信不须要过多分析。

一样,如下几个值同样都是真值:

var a = [];
var b = {};
var c = function() {};

显式强制类型转换

显式强制类型转换很是常见,也不会有什么坑,JavaScript 中的显式类型转换和静态语言中的很类似。

字符串和数字之间的显式转换

字符串和数字之间的相互转换靠 String()Number() 这两个内建函数实现。注意在调用时没有 new 关键字,只是普通函数调用,不会建立一个新的封建对象。

var a = 21;
var b = '2.71828';

var c = String(a);
var d = Number(b);

c; // "21"
d; // 2.71828

除了直接调用 String() 或者 Number() 方法以外,还能够经过别的方式显式地进行数字和字符串之间的相互转换:

var a = 21;
var b = '2.71828';

var c = a.toString();
var d = +b;

c; // "21"
d; // 2.71828

虽然 a.toString() 看起来很像显式的,然而其中涉及了隐式转换,由于 21 这样的原始值是没有方法的,JavaScript 自动建立了一个封装对象,并调用了其 toString() 方法。

+b 中的 + 是一元运算符,+ 运算符会将其操做数转换为数字。而 +b 是显式仍是隐式就取决于开发者自身了,本文以前也提到过,显式仍是隐式都是相对的。

显式转换为布尔值

和字符串与数字之间的相互转换同样,Boolean() 能够将参数显示强制转换为布尔值:

var a = '';
var b = 0;
var c = null;
var d = undefined;

var e = '0';
var f = [];
var g = {};

Boolean(a); // false
Boolean(b); // false
Boolean(c); // false
Boolean(d); // false

Boolean(e); // true
Boolean(f); // true
Boolean(g); // true

不过咱们不多会在代码中直接用 Boolean() 函数,更常见的是用 !! 来强制转换为布尔值,由于第一个 ! 会将操做数强制转换为布尔值,并反转(真值反转为假值,假值反转为真值),而第二个 ! 会将结果反转回原值:

var a = '';
var b = 0;
var c = null;
var d = undefined;

var e = '0';
var f = [];
var g = {};

!!a; // false
!!b; // false
!!c; // false
!!d; // false

!!e; // true
!!f; // true
!!g; // true

不过更常见的状况是相似 if(...) {} 这样的代码,在这个上下文中,若是咱们没有使用 Boolean() 或者 !! 转换,就会自动隐式地进行 ToBoolean 转换。

三元运算符也是一个很常见的布尔隐式强制类型转换的例子:

var a = 21;
var b = 'hello';
var c = false;

var d = a ? b : c;

d; // "hello"

在执行三元运算的时候,先对 a 进行布尔强制类型转换,而后根据结果返回 : 先后的值。

隐式强制类型转换

大部分被诟病的强制类型转换都是隐式强制类型转换。可是隐式强制类型转换真的一无可取吗?并不必定,引擎在必定程度上简化了强制类型转换的步骤,这对于有些状况来讲并非好事,而对于另外一些状况来讲可能并不必定是坏事。

字符串和数字之间的隐式强制类型转换

在上一节咱们已经介绍了字符串和数字之间的显式强制类型转换,在这一节咱们来讲说他们二者之间的隐式强制类型转换。

+ 运算符既能够用做数字之间的相加也能够经过重载用于字符串拼接。咱们可能以为若是 + 运算符两边的操做数有一个或以上是字符串就会进行字符串拼接。这种想法并不彻底错误,但也不是彻底正确的。好比如下代码能够验证这句话是正确的:

var a = 21;
var b = 4;

var c = '21';
var d = '4';

a + b; // 25
c + d; // "214"

可是若是 + 运算符两边的操做数不是字符串呢?

var arr0 = [1, 2];
var arr1 = [3, 4];

arr0 + arr1; // ???

上面这条命令的执行结果是 "1,23,4"ab 都不是字符串,为何 JavaScript 会把 ab 都转换为字符串再进行拼接?

根据 ES5 规范 11.6.1 节,若是 + 两边的操做数中,有一个操做数是字符串或者能够经过如下步骤转换为字符串,+ 运算符将进行字符串拼接操做:

  • 若是一个操做数为对象,则对其调用 ToPrimitive 抽象操做;
  • ToPrimitive 抽象操做会调用 [[DefaultValue]](hint),其中 hintNumber

这个操做和上面所述的 ToNumber 操做一致,再也不重复。

在这个操做中,JavaScript 引擎对其进行 ToPrimitive 抽象操做的时候,先执行 valueOf() 方法,可是因为其 valueOf() 方法返回的是数组,没法获得原始值,转而调用 toString() 方法,toString() 方法返回了以 , 拼接的全部元素的字符串,即 1,23,4+ 运算符再进行字符串拼接,获得结果 1,23,4

简单来讲,只要 + 的操做数中有一个是字符串,或者能够经过上述步骤获得字符串,就进行字符串拼接操做;其他状况执行数字加法。

因此如下这段代码可谓随处可见:

var a = 21;

a + ''; // "21"

利用隐式强制类型转换将非字符串转换为字符串,这样转换很是方便。不过经过 a + "" 和直接调用 String(a) 之间并非彻底同样,有些细微的差异须要注意一下。a + "" 会对 a 调用 valueOf() 方法,而后再经过上述的 ToString 抽象操做转换为字符串。而 String(a) 则会直接调用 toString()

虽然返回值都是字符串,然而若是 a 是对象的话,结果可能出乎意料!

好比:

var a = {
  valueOf: function() {
    return '21';
  },
  toString: function() {
    return '6';
  }
};

a + ''; // "42"
String(a); // "6"

不过大部分状况下也不会写这么奇怪的代码,若是你真的要扩展 valueOf() 或者 toString() 方法的话,请留意一下,由于你可能无心间影响了强制类型转换的结果。

那么从字符串转换为数字呢?请看下面的例子:

var a = '2.718';
var b = a - 0;

b; // 2.718

因为 - 操做符不像 + 操做符有重载,- 只能进行数字减法操做,所以若是操做数不是数字的话会被强制转换为数字。固然,a * 1a / 1 也能够,由于这两个运算符也只能用于数字。

- 用于对象会怎么样呢?好比:

var a = [3];
var b = [1];

a - b; // 2

- 只能执行数字减法,所以会对操做数进行强制类型转换为数字,根据前面所述的步骤,数组会调用其 toString() 方法得到字符串,而后再转换为数字。

布尔值到数字的隐式强制类型转换

假设如今你要实现这么一个函数,在它的三个参数中,若是有且只有一个参数为真值则返回 true,不然返回 false,你该怎么写?

简单一点的写法:

function onlyOne(x, y, z) {
  return !!((x && !y && !z) || (!x && y && !z) || (!x && !y && z));
}

onlyOne(true, false, false); // true
onlyOne(true, true, false); // false
onlyOne(false, false, true); // true

三个参数的时候代码好像也不是很复杂,那若是是 20 个呢?这么写确定过于繁琐了。咱们能够用强制类型转换来简化代码:

function onlyOne(...args) {
  return (
    args.reduce(
      (accumulator, currentValue) => accumulator + !!currentValue,
      0
    ) === 1
  );
}

onlyOne(true, false, false, false); // true
onlyOne(true, true, false, false); // false
onlyOne(false, false, false, true); // true

在上面这个改良版的函数中,咱们使用了数组的 reduce() 方法来计算全部参数中真值的数量,先使用隐式强制类型转换把参数转换成 true 或者 false,再经过 + 运算符将 true 或者 false 隐式强制类型转换成 1 或者 0,最后的结果就是参数中真值的个数。

经过这种改良版的代码,咱们能够很简单的写出 onlyTwo()onlyThree() 的函数,只须要改一个数字就行了。这无疑是一个很大的提高。

隐式强制类型转换为布尔值

在如下状况中会发生隐式强制类型转换:

  • if (...) 语句中的条件判断表达式;
  • for (..; ..; ..) 语句中的条件判断表达式,也就是第二个;
  • while (..)do..while(..) 循环中的条件判断表达式;
  • .. ? .. : .. 三元表达式中的条件判断表达式,也就是第一个;
  • 逻辑或 || 和逻辑与 && 左边的操做数,做为条件判断表达式。

在这些状况下,非布尔值会经过上述的 ToBoolean 抽象操做被隐式强制类型转换为布尔值。

||&&

JavaScript 中的逻辑或和逻辑与运算符和其余语言中的不太同样。在别的语言中,其返回值类型是布尔值,然而在 JavaScript 中返回值是两个操做数之一。所以在 JavaScript 中,||&& 被称做选择器运算符可能更合适。

根据 ES5 规范 11.11 节:

||&& 运算符的返回值不必定是布尔值,而是两个操做数中的其中一个。

好比:

var a = 21;
var b = 'xyz';
var c = null;

a || b; // 21
a && b; // "xyz"

c || b; // "xyz"
c && b; // null

若是 || 或者 && 左边的操做数不是布尔值类型的话,则会对左边的操做数进行 ToBoolean 操做,根据结果返回运算符左边或者右边的操做数。

对于 || 来讲,左边操做数的强制类型转换结果若是为 true 则返回运算符左边的操做数,若是是 false 则返回运算符右边的操做数。

对于 && 来讲则恰好相反,左边的操做数强制类型转换结果若是为 true 则返回运算符右边的操做数,若是是 false 则返回运算符左边的操做数。

||&& 返回的是两个操做数之一,而非布尔值。

在 ES6 的函数默认参数出现以前,咱们常常会看到这样的代码:

function foo(x, y) {
  x = x || 'x';
  y = y || 'y';

  console.log(x + ' ' + y);
}

foo(); // "x y"
foo('hello'); // "hello y"

看起来和咱们预想的一致。可是,若是是这样调用呢?

foo('hello world', ''); // ???

上面的执行结果是 hello world y,为何?

在执行到 y = y || "y" 的时候,JavaScript 对运算符左边的操做数进行了布尔隐式强制类型转换,其结果为 false,所以运算结果为运算符右边的操做数,即 "y",所以最后打印出来到日志是 "hello world y" 而非咱们预想的 hello world

因此这种方式须要确保传入的参数不能有假值,不然就可能和咱们预想的不一致。若是参数中可能存在假值,则应该有更加明确的判断。

若是你看过压缩工具处理后的代码的话,你可能常常会看到这样的代码:

function foo() {
  // 一些代码
}

var a = 21;

a && foo(); // a 为假值时不会执行 foo()

这时候 && 就被称为守护运算符(guard operator),即 && 左边的条件判断表达式结果若是不是 true 则会自动终止,不会判断操做符右边的表达式。

因此在 if 或者 for 语句中咱们使用 ||&& 的时候,if 或者 for 语句会先对 ||&& 操做符返回的值进行布尔隐式强制类型转换,再根据转换结果来判断。

好比:

var a = 21;
var b = null;
var c = 'hello';

if (a && (b || c)) {
  console.log('hi');
}

在这段代码中,a && (b || c) 的结果实际是 'hello' 而非 true,而后 if 再经过隐式类型转换为 true 才执行 console.log('hi')

Symbol 的强制类型转换

ES6 中引入了新的基本数据类型 —— Symbol。然而它的强制类型转换有些不同,它支持显式强制类型转换,可是不支持隐式强制类型转换。

好比:

var s = Symbol('hi');

String(s); // 'Symbol(hi)'
s + ''; // Uncaught TypeError: Cannot convert a Symbol value to a string

并且 Symbol 不能强制转换为数字,好比:

var s = Symbol('hi');

s - 0; // Uncaught TypeError: Cannot convert a Symbol value to a number

Symbol 的布尔强制类型转换都是 true

相关文章
相关标签/搜索