学习JavaScript“强制类型转换”

266 阅读12分钟

抽象值操作

ES规范中定义了一些抽象操作和类型转换。

抽象操作(ToPrimitive)

将值转换为相应的基本类型值,ToPrimitive会首先检查该值是否有 valueOf() 方法。 如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。如果 valueOf()toString() 均不返回基本类型值,会产生 TypeError 错误。(具体可以参考)

ToString

它负责处理非字符串到字符串的强制类型转换。

抽象操作 ToString(argument) 负责将 argument 转换为一个 String 类型值。

参数类型 结果
Undefined 返回 "undefined".
Null 返回 "null".
Boolean 如果 argument 是 true, 返回 "true".
如果 argument 是 false, 返回 "false".
Number 返回 NumberToString(argument).
String 返回 argument.
Symbol 抛出一个 TypeError 异常.
Object ToPrimitive 抽象操作
String(undefined);    //"undefined"

String(null);        //"null"

true.toString();     //"true"

var number = 2019;
number.toString();   //"2019"

var obj = { name:"tom" };

obj.toString()      //"[object Object]"

var arr = ['js','css','html'];
arr.toString();     //"js,css,html"

以上列出的是基本的转换规则,对普通对象来说,除非自行定义,否则toString()返回内部属性 [[Class]]的值,如 "[object Object]"

ToNumber

抽象操作 ToNumber(argument) 把参数 argument 转换为一个 Number 类型的值

参数类型 结果
Undefined 返回 NaN。
Null 返回 +0。
Boolean 如果 argument 是 true,返回 1。
如果 argument 是 false,返回 +0。
Number 返回 argument (不转换)。
String 返回 ToNumber.
Symbol 抛出一个 TypeError 异常.
Object ToPrimitive 抽象操作
var a = {
    valueOf: function(){
        return "24";
    }
};
var b = {
    toString: function(){
        return "24";
    }
};
var c = [2,4];
c.toString = function(){
    return this.join(""); // "24"
};

Number( a );           //24
Number( b );           //24    
Number( c );           //24
Number( "" );          //0
Number( [] );          //0    
Number( [ "abc" ] );   // NaN

ToBoolean

抽象操作 ToBoolean(argument) 把参数 argument 转换为一个 Boolean 类型的值

参数类型 结果
Undefined 返回 false。
Null 返回 false。
Boolean 返回 argument。
Number 如果 argument 为 +0,-0,or NaN,返回 false; 否则返回 true。
String 如果 argument 是一个空字符串 (长度为0的字符串), 返回 false; 否则返回 true。
Symbol 返回 true。
Object 返回 true。
  1. 假值

JavaScript 中的值可以分为以下两类:

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

以下这些是假值:

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

假值的布尔强制类型转换结果为 false。假值列表以外的值都是真值。

  1. 假值对象
 var a = new Boolean( false );
 var b = new Number( 0 );
 var c = new String( "" );

它们都是封装了假值的对象那它们究竟是 true 还是 false 呢?

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

console.log(d); //true

说明 abc 都为 true

  1. 真值
 var a = "false";
 var b = "0";
 var c = "''";
 
 var d = Boolean( a && b && c );
console.log(d);
var a = [];
var b = {};
var c = function(){};

var d = Boolean( a && b && c );
console.log(d);

上面两段代码看似假值但结果返回的是真值,说明了不在假值列表中的都是真值。

显示强制类型转换

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

字符串和数字之间的转换是通过 String(..)Number(..) 这两个内建函数来实现

下面是两者之间的显式强制类型转换:

var a = 2019;
var b = String(a);
var c = a.toString();
console.log(b);     //"2019"
console.log(c);     //"2019"

var d = "24";
var e = Number(d);
var f = +d;
console.log(e);     //24
console.log(f);     //24
  1. 日期显式转换为数字
var timestamp = +new Date();
console.log(timestamp);

也可以使用内置的方法进行转换

var timestamp1 = new Date().getTime();

var timestamp2 = Date.now();

更推荐使用内置的方法进行处理。

  1. ~ 运算符

~ 还可以有另外一种诠释,源自早期的计算机科学和离散数学:~ 返回 2 的补码

~x 大致等同于 -(x+1)

var a = ~42;
console.log(a);      // -43

~平时可能我们不怎么使用,但是知道其中的原理,或许我们也可以运用它。

我们都知道indexOf(..)方法如果找到就返回子字符串所在的位置(从 0 开始),否则返回 -1。这是我们就可以使用~来代替>= 0!= -1

var string = "JavaScript";

if(string.indexOf('a') >= 0){
    console.log('match');
}

if(string.indexOf('a') != -1){
    console.log('match');
}

if(~string.indexOf('a')){
    console.log('match');
}

显式解析数字字符串

解析字符串中的数字和将字符串强制类型转换为数字的返回结果都是数字。但解析和转换 两者之间还是有明显的差别。

例如:

 var a = "2019";
 var b = "2019px";
 
 Number(a);     //42
 parseInt(a);   //42    
 
 Number(b);     //NaN
 parseInt(b);   //42

解析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停 止。而转换不允许出现非数字字符,否则会失败并返回 NaN。

parseInt(..) 针对的是字符串值。向 parseInt(..) 传递数字和其他类型的参数是没有用的,比如 truefunction(){...}[..]

处理非字符串

parseInt(..)先将参数强制类型转换为字符串再进行解析

例如:

parseInt(new String( "2019"));

var obj = {
    num: 10,
    toString: function() { 
        return String( this.num * 2 );
    }
 };
parseInt(obj);  // 20

显式转换为布尔值

显示的转换布尔值是使用Boolean(..)方法,但是并不常用。我们通常使用!!来转换。

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

显示转换布尔值还有一个用处是在JSON序列化过程中将值强制类型转换为 truefalse:

var arr = [
    1,
    function(){},
    2,
    function(){},
];

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

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

隐式强制类型转换

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

一般情况下通过+运算符,既能用于数字加法,也能用于字符串拼接。例如:

var a = "2019";
var b = 24;

var c = 24;
var d = 2019;

var e = [1,2];
var f = [3,4];

console.log(a + b);     //"201924"
console.log(c + d);     //"2043"
console.log(e + f);     //"1,23,4"

这里根据ES规范,如果某个操作数是字符串或者能够通过以下步骤转换为字符串 的话,+ 将进行拼接操作。如果其中一个操作数是对象(包括数组),则首先对其调用 ToPrimitive 抽象操作,该抽象操作再调用 [[DefaultValue]]

因为数组的 valueOf() 操作无法得到简单基本类型值,于是它转而调用 toString()。因此上例中的两 个数组变成了 "1,2""3,4"+ 将它们拼接后返回 "1,23,4"

简单来说就是,如果 + 的其中一个操作数是字符串,则执行字符串拼接;否则执行数字加法。

下面再来看一个常见的例如:

var number = 24;
console.log(number + "");

number + ""这样的隐式转换十分常见,但是这里number + ""Sring(number)差别需要注意,根据 ToPrimitive抽象操作规则,number + ""会对number调用valueOf()方法,然后通过ToString抽象操作将返回值转换为字符串。而 String(number)则是直接调用 ToString()

它们最后返回的都是字符串,但是结果可能不同,例如:

var obj = {
    valueOf: function() { return 2019; },
    toString: function() { return 24; }
 };
 
 obj + "";         // "2019"
 String(obj);    // "24"

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

在将某些复杂的布尔逻辑转换为数字加法的时候,隐式强制类型转换能派上大用场,例如:

function onlyOne(a,b,c) {
    return !!((a && !b && !c) || (!a && b && !c) || (!a && !b && c));
}
var a = true;
var b = false;
onlyOne( a, b, b ); // true
onlyOne( b, a, b ); // true
onlyOne( a, b, a ); // false

但如果有多个参数时(4 个、5 个,甚至 20个),用上面的代码就很难处理了。这时就可以使用从布尔值到数字(0 或 1)的强制类型转换,例如:

function onlyOne() {
    var sum = 0;
    for(var i = 0;i < arguments.length; i++) {
        // 跳过假值,和处理0一样,但是避免了NaN
        if(arguments[i]){
            sum += arguments[i];
        }
    }
    return sum == 1;
}

var a = true;
var b = false;
onlyOne( b, a );                // true
onlyOne( b, a, b, b, b, a );    // false

通过sum += arguments[i]中的隐式强制类型转换,将真值转换为1并进行 累加。如果有且仅有一个参数为 true,则结果为 1;否则不等于 1sum == 1 条件不成立。

我们显式强制类型转换来实现,例如:

function onlyOne() {
    var sum = 0;
    for(var i = 0;i < arguments.length; i++) {
       sum += Number(!!arguments[i]);
    }
    return sum == 1;
}

!!arguments[i] 首先将参数转换为 truefalse。因此非布尔值参数在这里也是可以的, 比如:onlyOne("24", 0),转换为布尔值以后,再通过 Number(..) 显式强制类型转换为 0 或 1。

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

下列几种情况会发生布尔值隐式强制类型转换。

  • if (..)语句中的条件判断表达式。
  • for ( .. ; .. ; .. )语句中的条件判断表达式(第二个)。
  • while (..)do..while(..) 循环中的条件判断表达式。
  • ? :中的条件判断表达式。
  • 逻辑运算符 ||(逻辑或)和&&(逻辑与)左边的操作数(作为条件判断表达式)。
var a = 24;
var b = "abc";
var c;
var d = null;

if (a) {
    console.log( "hello" );
}

while (c) {
    console.log( "run" );
}

c = d ? a : b; c;

if ((a && d) || c) {
    console.log( "ok" );
}

|| 和 &&

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

var a = 24;
var b = "hello";
var c = null;

a || b;  //24     
a && b;  //"hello"     
c || b;  //"hello"
c && b;  // null

||&& 首先会对第一个操作数执行条件判断,如果其不是布尔值(如上例)就先进行 ToBoolean 强制类型转换,然后再执行条件判断。

对于 || 来说,如果条件判断结果为 true 就返回第一个操作的值,如果为 false 就返回第二个操作数的值。

&& 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返 回第一个操作数的值。

换一个角度来理解:

a || b;
// 大致相当于 a ? a : b;
a && b;
// 大致相当于 a ? b : a;

宽松相等和严格相等

宽松相等==和严格相等===都用来判断两个值是否“相等”,但是它们之间有一个很重要的区别。

== 允许在相等比较中进行强制类型转换,而 === 不允许

es中有详细的规范说明,这里简单介绍一下。(抽象相等比较)

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

var a = 2019;
var b = "2019";

a === b;    // false
a == b;     // true
  • 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果。
  • 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果。

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

var a = "2019";
var b = true;

a == b; // false
  • 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果。
  • 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。

3. 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
  • 如果 x 为 null,y 为 undefined,则结果为 true
  • 如果 x 为 undefined,y 为 null,则结果为 true
  • ==nullundefined 相等(它们也与其自身相等),除此之外其他值都不存在这种 情况。

4. 对象和非对象之间的相等比较

var a = 2019;
var b = [2019];

a == b; // true
  • 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果。
  • 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPromitive(x) == y 的结果。

之前介绍过的 ToPromitive 抽象操作的所有特性(如 toString()valueOf()) 在这里都适用。

var a = "hello";
var b = Object(a);

a === b;  //false         
a == b;   //true

a == b结果为true,因为b通过ToPromitive进行强制类型转换。

但有一些值不这样,原因是 == 算法中其他优先级更高的规则。例如:

var a = null;
var b = Object(a);  // 和Object()一样
a == b;             // false

var c = undefined;
var d = Object(c);  // 和Object()一样
c == d;             // false

var e = NaN;
var f = Object(e);  // 和new Number(e)一样
e == f;             // false

因为没有对应的封装对象,所以 nullundefined 不能够被封装,Object(null)Object() 均返回一个常规对象。

NaN能够被封装为数字封装对象,但拆封之后NaN == NaN返回false,因为NaN不等于NaN

抽象关系比较

a < b我们在开发者经常遇到,不过还是很有必要深入了解一下。es规范定义了抽象关系比较

比较双方首先调用 ToPrimitive,如果结果出现非字符串,就根据 ToNumber 规则将双方强 制类型转换为数字来进行比较。

例如:

var a = [2019];
var b = ["2020"];

 a < b;  // true
 b < a;  // false

如果比较双方都是字符串,则按字母顺序来进行比较:

var a = ["24"];
var b = ["024"];

a < b; // false

ab 并没有被转换为数字,因为 ToPrimitive 返回的是字符串,所以这里比较的是 "42""043" 两个字符串,它们分别以 "4""0" 开头。因为 "0" 在字母顺序上小于 "4",所以 最后结果为 false

再例如:

var a = { num: 24 };
var b = { num: 25 };

a < b; //false

结果还是false,因为a[object Object]b也是[object Object],所以按照字母顺序 a < b 并不成立。

奇怪的例子:

var a = { num: 24 };
var b = { num: 25 };

a < b;  // false
a == b; // false
a > b;  // false
a <= b; // true
a >= b; // true

为什么a == b的结果不是true?它们的字符串值相同(同为"[object Object]"),按道 理应该相等才对?

a < ba == b结果为false,为什么a <= ba >= b的结果会是true呢?

因为根据规范a <= b被处理为b < a,然后将结果反转。因为b < a的结果是false,所 以 a <= b 的结果是 true

实际上 JavaScript<= 是“不大于”的意思(即 !(a > b),处理为 !(b < a))。同理 a >= b 处理为 b <= a

小结

本小结介绍了JavaScript的数据类型之间的转换,即强制类型转换:包括显式和隐式。让我们在开发中避免不少问题和疑惑。

参考