[万字长文]js类型转换详解

前言

几乎所有 JavaScript 程序员都接触过强制类型转换 —— 不论是有意的还是无意的。强制类型转换导致了很多隐蔽的 BUG ,但是强制类型转换同时也是一种非常有用的技术,我们不应该因噎废食。在本文中我们来详细探讨一下 JavaScript 的强制类型转换,以便我们可以在避免踩坑的情况下最大化利用强制类型转换的便捷。

首先说一下基本的数据类型有哪些。

ES6 前,JavaScript 共有六种数据类型:Undefined、Null、Boolean、Number、String、Object。其中Undefined、Null、Boolean、Number、String是基础类型,Object是复杂类型。

ES6后,基本类型和复杂类型划分如下。

值类型(基本类型):字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined)、Symbol。

引用数据类型:对象(Object)、数组(Array)、函数(Function)。

**注:**Symbol 是 ES6 引入了一种新的原始数据类型,表示独一无二的值。

文中涉及到的部分文档如下。

ES5文档地址

中文版:中文地址  ECMAScript5.1规范系列英文版:英文地址  英文地址2 github

ES6文档地址

本文主要参考的网址:http://www.ecma-international.org/ecma-262/6.0/#sec-type

中文版:中文地址英文版:英文地址ES6的浏览器兼容性问题:地址

ES7文档地址

英文版:英文地址


一、类型转换和强制类型转换

类型转换发生在静态类型语言的编译阶段

强制类型转换发生在动态类型语言的运行时(runtime),因此在 JavaScript 中只有强制类型转换。

强制类型转换一般还可分为 *隐式强制类型转换(implicit coercion)*和 显式强制类型转换(explicit coercion

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

比如:

var a = 2;
var b = a + '';  // "2"  隐式转换
var c = String(a);  // "2"  显式转换

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

String(2) 则是非常典型的显式强制类型转换。这两种强制转换类型的操作都是将数字转换为字符串。

1.1 常见的隐式类型转换场景

1、  +   例子:200 + '3'  //2003 数字转字符串

2、 - *  %   例子:200 - '3'  //197   字符串转数字

3、++ --    //字符串变数字

4、><     10 >' 9 '  //  true   数字跟字符串比较,字符串转数字
         '10' > '9' //false    字符串的比较,一位一位进行比较

5、!      !'ok'    //false   把右边的数据类型转换成布尔值

6、==      '2' == 2  //true    没有进行数据类型的比较,建议用‘===’


1.2 诡异的==号

在正常的开发中,js编码规范都会要求你使用”===”而不是”==“,因为”a===b“要求a和b不仅在类型上相等,而且值也相等的情况下,才会返回true,而”==“会触发各种诡异的隐式类型转换,有时候水平不够完全看不出bug,特别让人恼火,例如下面几个例子。

null == undefined // true
null == {}  // false
undefined == {}  // false
{} == {}   // false
[] == {}  // false
[] == []   // false
true == {}   // false
false == {}  //  false
true == []   // false
false == []  // true
0 == []    // true
0 == {}   // false

上面几个看下来,是不是已经开始想吐了?别怕,接下来我就来带你探究一下==操作符其中的原理。

根据es6规范 7.2.12 Abstract Equality Comparison ,==号的判定如下。

The comparison x == y, where x and y are values, produces true or false. 
#  判定流程
    ReturnIfAbrupt(x).
    ReturnIfAbrupt(y).
    If Type(x) is the same as Type(y)
        return the result of performing Strict Equality Comparison x === y.
    If x is null and y is undefined
        return true.
    If x is undefined and y is null
        return true.
    If Type(x) is Number and Type(y) is String,
        return the result of the comparison x == ToNumber(y).
    If Type(x) is String and Type(y) is Number,
        return the result of the comparison ToNumber(x) == y.
    If Type(x) is Boolean
        return the result of the comparison ToNumber(x) == y.
    If Type(y) is Boolean
        return the result of the comparison x == ToNumber(y).
    If Type(x) is either String, Number, or Symbol and Type(y) is Object
        return the result of the comparison x == ToPrimitive(y).
    If Type(x) is Object and Type(y) is either String, Number, or Symbol
        return the result of the comparison ToPrimitive(x) == y.
    return false.

英文好的相信已经可以看懂了,其实大概意思就是下面这些。

例如==的左右分别为X和Y,有如下规则:

  若X是null,Y是undefined,则输出结果为True

  若x是number,Y是string,则将字符串转换为数字再进行判断,

  若x是boolean,Y是其他类型,则先将Boolean转换为数字再进行判断

  若x是Object,Y是Number/String/Symbol,则先将x转化为原始值,再进行判断

  除了上述几种类型,其余的输出结果都是False

在上面可以明显地看到,只要x和y不是同一个类型的,==将会是一个递归的过程,直到x和y被转化为相同类型,最终输出===的结果。可能你又会开始疑惑,object转换成string的时候,编译器遵循着什么规则呢?别怕,下面的章节就是专门来介绍es6中类型转换的原则的,请抓稳上车!

二、抽象值操作

在介绍强制类型转换之前,需要先了解一下字符串、数字和布尔值之间类型转换的基本规则。

在 ES5、ES6以及之后的规范中定义了一些“抽象操作”和转换规则,在这以es6为准,主要讲一下 ToPrimitiveToStringToNumberToBoolean

tip:注意,这些操作仅供引擎内部使用,和平时 JavaScript 代码中的 .toString() 等操作不一样, 我们是调用不到的。

[万字长文]js类型转换详解
img


2.1 ToPrimitive

根据es6规范的第7.1.1,该函数语法表示如下:

ToPrimitive(input[, PreferredType])
  • 第一个参数是 input,表示要处理的输入值。

  • 第二个参数是 PreferredType,非必填,表示希望转换成的类型,有两个值可以选,Number 或者 String。

可以将 ToPrimitive 操作看作是一个函数,它接受一个 input 参数和一个可选的 PreferredType 参数。

ToPrimitive 抽象操作会将 input 参数转换成一个原始值。如果一个对象可以转换成不止一种原始值,可以使用 PreferredType 指定抽象操作的返回类型。

该抽象操作接受一个参数input和一个可选的参数PreferredType,目的是把参数input转化为非对象数据类型,也就是原始数据类型。

如果input可以同时转化为多个原始数据,那么会优先参考PreferredType的值。转化过程参照下表:

参数input的数据类型 结果
Undefined 返回input自身
Boolean 返回input自身
Number 返回input自身
String 返回input自身
Symbol 返回input自身
Object 执行下面的步骤

如果input的数据类型是对象Object,执行下述步骤:

  • 如果没有传入PreferredType参数,让hint等于”default”;

  • 如果PreferredType是hint String,让hint等于”string”;

  • 如果PreferredType是hint Number,让hint等于”number”;

  • 让exoticToPrim等于GetMethod(input, @@toPrimitive),大概语义就是获取参数input的@@toPrimitive方法;

  • 如果exoticToPrim不是Undefined,那么:

    • 让result等于Call(exoticToPrim, input, « hint »),大概语义就是执行exoticToPrim(hint);
    • 如果result是原始数据类型,返回result;
    • else,抛出类型错误的异常;
  • 如果hint是”default”,让hint等于”number”;

  • 返回OrdinaryToPrimitive(input, hint)抽象操作的结果。

  • 如果是 ToPrimitive(obj, Number),处理步骤如下:

    • 如果 obj 为 基本类型,直接返回
    • 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
    • 否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。
    • 否则,JavaScript 抛出一个类型错误异常。
  • 如果是 ToPrimitive(obj, String),处理步骤如下:

    • 如果 obj为 基本类型,直接返回
    • 否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。
    • 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
    • 否则,JavaScript 抛出一个类型错误异常。


2.1.1 [[DefaultValue]](hint) 内部操作

在对象 O 上调用内部操作 [[DefaultValue]] 时,根据 hint 的不同,其执行的操作也不同,简化版(es6规范的第7.1.1)如下:

[万字长文]js类型转换详解
img


2.2 ToString

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

  • null 转化为 "null"
  • undefined 转化为 "undefined"
  • true 转化为 "true"
  • false 转化为 "false";
  • 数字的字符串化遵循通用规则,如 21 转化为 "21",极大或者极小的数字使用指数形式,如:
var num = 3.912 * Math.pow(1050);
num.toString(); // "3.912e50"
  • 对于普通对象,如果对象有自定义的 toString() 方法,字符串化时就会调用该自定义方法并使用其返回值,否则返回的是内部属性 [[Class]] 的值,比如 "object [Object]"。需要注意的是,数组默认的 toString() 方法经过了重新定义,其会将所有元素字符串化之后再用 "," 连接起来,如:
var arr = [123];
arr.toString(); // "1,2,3"

2.3 ToNumber

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

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

在将某个值转换为原始值的时候,会首先执行抽象操作 ToPrimitive,如果结果是数字则直接返回,如果是字符串再根据相应规则转换为数字。参照上述规则,现在我们可以一步一步来解释本文开头的那行代码了。

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

其执行步骤如下:

[万字长文]js类型转换详解
图片

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

var a = {
    valueOffunction({
        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


2.4 ToBoolean

参考es6规范的7.1.2 ToBoolean ( argument )的定义。

JavaScript 中有两个关键字 truefalse,分别表示布尔类型的真和假。

通常地,在 if 语句中将 0 作为假值条件, 1 作为真值条件,这也利用了强制类型转换。

我们可以将 true 强制类型转换为 1false强制类型转换为 0,反之亦然。

然而 true1 并不是一回事, false0 也一样。

2.4.1 假值

在 JavaScript 中,按照转换成bool值的真假情况,值可以分为两类:

  • 可以被强制类型转换为 false 的值
  • 其他(被强制类型转换为 true 的值)

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

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

假值的布尔强制类型转换结果为 false。在假值列表以外的值都是真值,[]、{}这些都是真值,并且笔试题很喜欢考。

2.4.2 例外

规则难免有例外。刚说了除了假值列表以外的所有其他值都是真值,然而你可以在现代浏览器的控制台中执行下面几行代码试试:

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。不得不说,IE真是反前端的存在…


2.4.3 真值

除了假值以外都是真值。

比如:

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({};


三、显式强制类型转换

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

字符串和数字之间的相互转换靠 String()Number() 这两个内建函数实现。

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 是显式还是隐式就取决于开发者自身了,显式还是隐式都是相对的。

3.2 显式转换为布尔值

和字符串与数字之间的相互转换一样, 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 进行布尔强制类型转换,然后根据结果返回 : 前后的值。

四、隐式强制类型转换

大部分被诟病的强制类型转换都是隐式强制类型转换。但实际上,js引擎在一定程度上简化了强制类型转换的步骤,这对于有些情况来说并不是好事,而对于另一些情况来说可能并不一定是坏事。

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

+ 运算符既可以用作数字之间的相加也可以通过重载用于字符串拼接。

我们可能觉得如果 + 运算符两边的操作数有一个或以上是字符串就会进行字符串拼接。这种想法并不完全错误,但也不是完全正确的。比如以下代码可以验证这句话是正确的:

var a = 21;
var b = 4;
var c = '21';
var d = '4';
a + b; // 25
c + d; // "214"
a + d; // "214"
b + c; // "421"

但是如果 + 运算符两边的操作数不是字符串呢?

var arr0 = [12];
var arr1 = [34];
arr0 + arr1; // ???

上面这条命令的执行结果是 "1,23,4"

ab 都不是字符串,为什么 JavaScript 会把 a 和 b 都转换为字符串再进行拼接?

根据 ES6 规范7.1.3.1 ToNumber Applied to the String Type,如果 + 两边的操作数中,有一个操作数是字符串或者可以通过以下步骤转换为字符串, + 运算符将进行字符串拼接操作:

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

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

在这个操作中,JavaScript 引擎对其进行 ToPrimitive 抽象操作的时候,先执行 valueOf() 方法,但是由于其 valueOf() 方法返回的是数组,无法得到原始值,转而调用 toString() 方法, toString() 方法返回了以 , 拼接的所有元素的字符串,即 1,23,4+ 运算符再进行字符串拼接,得到结果 1,23,4

// 
[1,2].valueOf()  // [1, 2]  不属于原始值类型啊 即不在6个基本类型之中 
[1,2].toString()  // "1,2"
[1,2] + [3,4] -> [1,2].toString() + [3,4].toString() 

综上所述,可以概括为

if( + 的操作数中有一个是字符串 || 可以通过valueOf、toString方法得到字符串){
  进行字符串拼接操作;
}else{
 执行数字加法。
}

下面看几个例子加深一下理解:

var a = 2;
a + ''// "2"

let c = {
    valueOf : function(){
        return 10
    }
}
c + 2  // 12

let d = {
    valueOf : function(){
        return 'kj'
    }
}
d + 1 // "kj1"

利用隐式强制类型转换将非字符串转换为字符串,这样转换非常方便。

不过通过 a +"" 和直接调用 String(a) 之间并不是完全一样,有些细微的差别需要注意一下。

  • a+"" 会对 a 调用 valueOf() 方法,然后再通过上述的 ToString 抽象操作转换为字符串。

  • String(a) 则会直接调用 toString()

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

比如:

var a = {
    valueOffunction({
        return '21';
    },
    toStringfunction({
        return '6';
    }
};
a + ''// "21"
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
a.toString() // "3"

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


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

假设现在你要实现这么一个函数,在它的三个参数中,如果有且只有一个参数为真值则返回 true,否则返回 false,你该怎么写?简单一点的写法:

function onlyOne(x, y, z{
    return !!((x && !y && !z) || (!x && y && !z) || (!x && !y && z));
}
onlyOne(truefalsefalse); // true
onlyOne(truetruefalse); // false
onlyOne(falsefalsetrue); // true

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

function onlyOne(...args{
    return ( args.reduce(
        (accumulator, currentValue) => accumulator + !!currentValue,
        0
    ) === 1);
}
onlyOne(truefalsefalsefalse); // true
onlyOne(truetruefalsefalse); // false
onlyOne(falsefalsefalsetrue); // true

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

通过这种改良版的代码,我们可以很简单的写出 onlyTwo()onlyThree() 的函数,只需要改一个数字就好了。这无疑是一个很大的提升。


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

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

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

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


4.4 ||&&

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

根据 ES6 规范12.12 Binary Logical Operators:

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

比如:

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);6  
}
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')


4.5 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

五、原始值转换详解

5.1 原始值转布尔

使用 Boolean 函数将类型转换成布尔类型。

在 JavaScript 中,只有 6 种值可以被转换成 false,其他都会被转换成 true。

console.log(Boolean()) // false,注意,当 Boolean 函数不传任何参数时,会返回 false 

console.log(Boolean(false)) // false

console.log(Boolean(undefined)) // false

console.log(Boolean(null)) // false

console.log(Boolean(+0)) // false
console.log(Boolean(-0)) // false
console.log(Boolean(NaN)) // false

console.log(Boolean("")) // false


5.2 原始值转数字

可以使用 Number 函数将类型转换成数字类型,如果参数无法被转换为数字,则返回 NaN。

ES6 规范 20.1.1.1 中关于 Number函数的介绍如下:

20.1.1.1Number ( [ value ] )

When Number is called with argument number, the following steps are taken:

  1. If no arguments were passed to this function invocation, let n be +0.
  2. Else, let n be ToNumber(value).
  3. ReturnIfAbrupt(n).
  4. If NewTarget is undefined, return n.
  5. Let O be OrdinaryCreateFromConstructor(NewTarget, "%NumberPrototype%", «[[NumberData]]» ).
  6. ReturnIfAbrupt(O).
  7. Set the value of O’s [[NumberData]] internal slot to n.
  8. Return O.

根据规范,如果 Number 函数不传参数,返回 +0,如果有参数,调用 ToNumber(value)。后面几种情况主要是对前面的补充处理,涉及到的函数比较少见,这里就不介绍了,感兴趣的可以直接进入官网查看。

注意这个 ToNumber 表示的是一个底层规范实现上的方法,并没有直接暴露出来。

7.1.3ToNumber ( argument )直接给了一个对应的结果表,如下:

Argument Type Result
Completion Record If argument is an abrupt completion, return argument. Otherwise return ToNumber(argument.[[value]])。意思是,如果参数是[突然完成],则返回参数。返回[ToNumber]参数。
Undefined Return NaN.
Null Return +0.
Boolean Return 1 if argument is true. Return +0 if argument is false.
Number Return argument (no conversion).
String 这段比较复杂,看例子
Symbol Throw a TypeError exception.
Object Apply the following steps: Let primValue be ToPrimitive(argument, hint Number).Return ToNumber(primValue). 意思是,调用ToPrimitive方法,拿到返回值后,再调用ToNumber并返回结果。

让我们写几个例子验证一下:

console.log(Number()) // +0

console.log(Number(undefined)) // NaN
console.log(Number(null)) // +0

console.log(Number(false)) // +0
console.log(Number(true)) // 1

console.log(Number("123")) // 123
console.log(Number("-123")) // -123
console.log(Number("1.2")) // 1.2
console.log(Number("000123")) // 123
console.log(Number("-000123")) // -123

console.log(Number("0x11")) // 17

console.log(Number("")) // 0
console.log(Number(" ")) // 0

console.log(Number("123 123")) // NaN
console.log(Number("foo")) // NaN
console.log(Number("100a")) // NaN

console.log(Number({
    name:"hzs",
    age:18
}))   // NaN

如果通过 Number 转换函数传入一个字符串,它会试图将其转换成一个整数或浮点数,而且会忽略所有前导的 0,如果有一个字符不是数字,结果都会返回 NaN,鉴于这种严格的判断,一般会使用更加灵活的 parseInt 和 parseFloat 进行转换。

parseInt 只解析整数,parseFloat 则可以解析整数和浮点数,如果字符串前缀是 “0x” 或者”0X”,parseInt 将其解释为十六进制数,parseInt 和 parseFloat 都会跳过任意数量的前导空格,尽可能解析更多数值字符,并忽略后面的内容。如果第一个非空格字符是非法的数字直接量,将最终返回 NaN:

console.log(parseInt("3 abc")) // 3
console.log(parseFloat("3.14 abc")) // 3.14
console.log(parseInt("-12.34")) // -12
console.log(parseInt("0xFF")) // 255
console.log(parseFloat(".1")) // 0.1
console.log(parseInt("0.1")) // 0

5.3 原始值转字符

使用 String 函数将类型转换成字符串类型。

es6规范第21.1 String ObjectsString Objects中有关 String 函数的介绍:

如果 String 函数不传参数,返回空字符串,如果有参数,调用 ToString(value),而 ToString 也给了一个对应的结果表。表如下:

参数类型 结果
Undefined “undefined”
Null “null”
Boolean 如果参数是 true,返回 “true”。参数为 false,返回 “false”
Number 又是比较复杂,可以看例子
String 返回与之相等的值

让我们写几个例子验证一下:

console.log(String()) // 空字符串

console.log(String(undefined)) // undefined
console.log(String(null)) // null

console.log(String(false)) // false
console.log(String(true)) // true

console.log(String(0)) // 0
console.log(String(-0)) // 0
console.log(String(NaN)) // NaN
console.log(String(Infinity)) // Infinity
console.log(String(-Infinity)) // -Infinity
console.log(String(1)) // 1

注意这里的 ToString 和上一节的 ToNumber 都是底层规范实现的方法,并没有直接暴露出来。

5.4 原始值转对象

原始值到对象的转换非常简单,原始值通过调用 String()、Number() 或者 Boolean() 构造函数,转换为它们各自的包装对象。

null 和 undefined 属于例外,当将它们用在期望是一个对象的地方都会造成一个类型错误 (TypeError) 异常,而不会执行正常的转换。

var a = 1;
console.log(typeof a); // number
var b = new Number(a);
console.log(typeof b); // object

5.5 对象转布尔值

对象到布尔值的转换非常简单:所有对象(包括数组和函数)都转换为 true。对于包装对象也是这样,举个例子:

console.log(Boolean(new Boolean(false))) // true

5.6 对象转字符串和数字

对象到字符串和对象到数字的转换都是通过调用待转换对象的一个方法来完成的。

而 JavaScript 对象有两个不同的方法来执行转换,一个是 toString,一个是 valueOf。注意这个跟上面所说的 ToStringToNumber 是不同的,这两个方法是真实暴露出来的方法。

所有的对象除了 null 和 undefined 之外的任何值都具有 toString 方法,通常情况下,它和使用 String 方法返回的结果一致。toString 方法的作用在于返回一个反映这个对象的字符串。Object.prototype.toString 方法会根据这个对象的[[class]]内部属性,返回由 “[object ” 和 class 和 “]” 三个部分组成的字符串。

举个例子:

Object.prototype.toString.call({a1}) // "[object Object]"
({a1}).toString() // "[object Object]"
({a1}).toString === Object.prototype.toString // true

我们可以看出当调用对象的 toString 方法时,其实调用的是 Object.prototype 上的 toString 方法。

然而 JavaScript 下的很多类根据各自的特点,覆写自己的 toString 方法。例如:

  • 数组的 toString 方法将每个数组元素转换成一个字符串,并在元素之间添加逗号后合并成结果字符串。

  • 函数的 toString 方法返回源代码字符串。

  • 日期的 toString 方法返回一个可读的日期和时间字符串。

  • RegExp 的 toString 方法返回一个表示正则表达式直接量的字符串。

console.log(({}).toString()) // [object Object]

console.log([].toString()) // ""
console.log([0].toString()) // 0
console.log([123].toString()) // 1,2,3
console.log((function(){var a = 1;}).toString()) // function (){var a = 1;}
console.log((/d+/g).toString()) // /d+/g
console.log((new Date(201001)).toString()) // Fri Jan 01 2010 00:00:00 GMT+0800 (CST)

而另一个转换对象的函数是 valueOf,表示对象的原始值。

默认的 valueOf 方法返回这个对象本身,数组、函数、正则简单的继承了这个默认方法,也会返回对象本身。

日期是一个例外,它会返回它的一个内容表示: 1970 年 1 月 1 日以来的毫秒数。

var date = new Date(2017421);
console.log(date.valueOf()) // 1495296000000


5.7 对象接着转字符串和数字

了解了 toString 方法和 valueOf 方法,我们分析下从对象到字符串是如何转换的。看规范 ES6 7.1.12 ToString(argument),其实就是 ToString 方法的对应表, Object 的转换规则如下:

参数类型 结果
Object 1. Let primValue be ToPrimitive(argument, hint String).  2. Return ToString(primValue).

所谓的 ToPrimitive 方法,其实就是输入一个值,然后返回一个一定是基本类型的值。

我们总结一下,当我们用 String 方法转化一个值的时候,如果是基本类型,就参照 “原始值转字符” 这一节的对应表,如果不是基本类型,我们会将调用一个 ToPrimitive 方法,将其转为基本类型,然后再参照“原始值转字符” 这一节的对应表进行转换。

其实,从对象到数字的转换也是一样:

参数类型 结果
Object 1. primValue = ToPrimitive(input, Number)  2. Return ToNumber(primValue)。

虽然转换成基本值都会使用 ToPrimitive 方法,但传参有不同,最后的处理也有不同,转字符串调用的是 ToString,转数字调用 ToNumber


5.8 对象转字符串

所以总结下,对象转字符串(就是 Number() 函数)可以概括为:

  1. 如果对象具有 toString 方法,则调用这个方法。如果他返回一个原始值,JavaScript 将这个值转换为字符串,并返回这个字符串结果。
  2. 如果对象没有 toString 方法,或者这个方法并不返回一个原始值,那么 JavaScript 会调用 valueOf 方法。如果存在这个方法,则 JavaScript 调用它。如果返回值是原始值,JavaScript 将这个值转换为字符串,并返回这个字符串的结果。
  3. 否则,JavaScript 无法从 toString 或者 valueOf 获得一个原始值,这时它将抛出一个类型错误异常。


5.9 对象转数字

对象转数字的过程中,JavaScript 做了同样的事情,只是它会首先尝试 valueOf 方法

  1. 如果对象具有 valueOf 方法,且返回一个原始值,则 JavaScript 将这个原始值转换为数字并返回这个数字
  2. 否则,如果对象具有 toString 方法,且返回一个原始值,则 JavaScript 将其转换并返回。
  3. 否则,JavaScript 抛出一个类型错误异常。

举个例子:

console.log(Number({})) // NaN
console.log(Number({a : 1})) // NaN

console.log(Number([])) // 0
console.log(Number([0])) // 0
console.log(Number([123])) // NaN
console.log(Number(function(){var a = 1;})) // NaN
console.log(Number(/d+/g)) // NaN
console.log(Number(new Date(201001))) // 1262275200000
console.log(Number(new Error('a'))) // NaN

注意,在这个例子中,[][0] 都返回了 0,而 [1, 2, 3] 却返回了一个 NaN。

分析原因:

当调用Number([]) 的时候,先调用 []valueOf 方法,此时返回 [],因为返回了一个对象而不是原始值,所以又调用了 toString 方法,此时返回一个空字符串,接下来调用 ToNumber 这个规范上的方法,参照对应表,转换为 0, 所以最后的结果为 0

而当调用 Number([1, 2, 3]) 的时候,先调用 [1, 2, 3]valueOf 方法,此时返回 [1, 2, 3],再调用 toString 方法,此时返回 1,2,3,接下来调用 ToNumber,参照对应表,因为无法转换为数字,所以最后的结果为 NaN

"1,2,3"  - 1  // NaN
"12" - 1  // 11


5.10 JSON.stringify

参考es6规范第24.3.2  JSON.stringify 。

JSON.stringify() 方法可以将一个 JavaScript 值转换为一个 JSON 字符串,实现上也是调用了 toString 方法,也算是一种类型转换的方法。

JSON.stringify 的注意要点主要有几个:

  • 处理基本类型时,与使用toString基本相同,结果都是字符串,除了 undefined
console.log(JSON.stringify(null)) // null
console.log(JSON.stringify(undefined)) // undefined,注意这个undefined不是字符串的undefined
console.log(JSON.stringify(true)) // true
console.log(JSON.stringify(42)) // 42
console.log(JSON.stringify("42")) // "42"
  • 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
JSON.stringify([new Number(1), new String("false"), new Boolean(false)]); // "[1,"false",false]"
  • undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。这里是面试的时候,经常问到的,对obj和数组用JSON.stringify实现深拷贝时,会有哪些坑?
JSON.stringify({xundefinedyObjectzSymbol("")}); 
// "{}"

JSON.stringify([undefinedObjectSymbol("")]);          
// "[null,null,null]" 
  • JSON.stringify 有第二个参数 replacer,它可以是数组或者函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除。
function replacer(key, value{
  if (typeof value === "string") {
    return undefined;
  }
  return value;
}

var foo = {foundation"Mozilla"model"box"week45transport"car"month7};
var jsonString = JSON.stringify(foo, replacer);

console.log(jsonString)
// {"week":45,"month":7}
var foo = {foundation"Mozilla"model"box"week45transport"car"month7};
console.log(JSON.stringify(foo, ['week''month']));
// {"week":45,"month":7}
  • 如果一个被序列化的对象拥有 toJSON 方法,那么该 toJSON 方法就会覆盖该对象默认的序列化行为:不是那个对象被序列化,而是调用 toJSON 方法后的返回值会被序列化,例如:
var obj = {
  foo'foo',
  toJSONfunction ({
    return 'bar';
  }
};
JSON.stringify(obj);      // '"bar"'
JSON.stringify({x: obj}); // '{"x":"bar"}'


小结

稍稍小结一下,总的来说,我们最常遇到的类型转换就是下面几种情况了。分别为数字、字符串和布尔值这三种情况,遇上复杂类型(Object、Array)等情况,主要考虑valueOf和toString方法,看他们是如何尝试往原始值过渡的(ToPrimititive),然后在根据情境来判断这个原始值会往哪个基本类型转,最后得出结果。

一、转为字符串:使用 .toString或者String

  • **.**toString()方法:注意,不可以转null和underfined
//转为字符串-->toString方法
var bool=true;
console.log(bool.toString());
//注意,toString不能转null和underfined.
  • String()方法:都能转
console.log(String(null));
  • 隐式转换:num  +  “”,当 + 两边一个操作符是字符串类型,一个操作符是其它类型的时候,会先把其它类型转换成字符串再进行字符串拼接,返回字符串。
var a=true;
var str= a+"";
str // "true"

二、转为数值类型

  • Number():Number()可以把任意值转换成数值,如果要转换的字符串中有一个不是数值的字符,返回NaN
console.log(Number(true));
  • parseInt():
var a="12.3px";
parseInt(a)  //结果:12.3.  如果第一个字符是数字会解析直到遇到非数字结束.

var a="abc2.3";
parseInt(a) // 结果:返回NaN,如果第一个字符不是数字或者符号就返回NaN.

var a="0.3";
parseInt(a) // 结果:返回0,如果第一个字符不是数字或者符号就返回NaN.
  • parseFloat():  parseFloat()把字符串转换成浮点数,parseFloat()和parseInt非常相似,不同之处在与parseFloat会解析第一个. 遇到第二个.或者非数字结束如果解析的内容里只有整数,解析成整数。

  • 隐式转换:

var str="123";
var num=str-0;
num  // 123  结果为数值型;

三、转换为Boolean():

可以概括为:0 ''(空字符串) null undefined NaN 会转换成false  其它都会转换成true

  • Boolean():
console.log(Boolean(2)); // true
  • if 判断
var message = "";
if(message){
   console.log(""// a未被打印 因为 ""转为bool后为false 
};
  • 隐式转换:!!
var str="123";
var bool=!!str;
str  // true

下表展示了使用不同的数值转换为数字(Number), 字符串(String), 布尔值(Boolean)。

原始值 转换为数字 转换为字符串 转换为布尔值
false 0 “false” false
true 1 “true” true
0 0 “0” false
1 1 “1” true
“0” 0 “0” true
“000” 0 “000” true
“1” 1 “1” true
NaN NaN “NaN” false
Infinity Infinity “Infinity” true
-Infinity -Infinity “-Infinity” true
“” 0 “” false
“20” 20 “20” true
“Runoob” NaN “Runoob” true
[ ] 0 “” true
[20] 20 “20” true
[10,20] NaN “10,20” true
[“Runoob”] NaN “Runoob” true
[“Runoob”,”Google”] NaN “Runoob,Google” true
function(){} NaN “function(){}” true
{ } NaN “[object Object]” true
null 0 “null” false
undefined NaN “undefined” false

嘿嘿,把文本看完,面试官在类型转换这块就问不倒你了!


原文始发于微信公众号(豆子前端):[万字长文]js类型转换详解

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/56730.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
半码博客——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!