类型转换??

对于JavaScript的类型转换,一直是为人所诟病的一点,因为它实在太奇怪了,会令人感到十分难受,虽说是拥有“灵活”的动态转换能力,但是会让人感到十分的混乱,而且别人很喜欢用这种类似“脑筋急转弯”的东西来考人。下面给个图你感受下:

wtf.png

是不是很难顶??想摸清楚是什么情况。所以我先说一下最常用的基本类型 (primitive)之间的转换。

基本类型转换 primitive to primitive

在静态语言中,类型转换发生在编译期间 (complie time) ,而像JS这种动态语言则发生在运行时(running time),这都是一些抽象的概念。

实际上,使用基本类型作为操作数时,类型转换最后都是 StringBooleanNumber 这三种类型。而且,转换还分为显式隐式的强制转换。

隐式强制转换

一个典型例子:

let num = 3.14;
let toStr = num + ''; // => "3.14" 

对于上面的通过 + ''将num转换为字符串,这个行为就是隐式的。

隐式强制转换发生的一些场景:

  • (1) if (..)语句中的条件判断表达式。

  • (2) for( .. ; .. ; .. ) 语句中的条件判断表达式(第二个)。

  • (3) while (..) 和 do..while(..) 循环中的条件判断表达式。

  • (4) ? :中的条件判断表达式。

  • (5) 逻辑运算符 || (逻辑或)和 && (逻辑与)左边的操作数(作为条件判断表达式)。

逻辑表达式左边操作数的转换规则

&&

  1. 如果第一个操作数是对象,则返回第二个操作数。

  2. 如果第二个操作数是对象,则只有第一个操作数求值为 true 才会返回该对象。

  3. 如果两个操作数都是对象,则返回第二个操作数。

  4. 如果有一个操作数是 null,则返回 null。

  5. 如果有一个操作数是 NaN,则返回 NaN。

  6. 如果有一个操作数是 undefined,则返回 undefined。

||

  1. 如果第一个操作数是对象,则返回第一个操作数。

  2. 如果第一个操作数求值为 false,则返回第二个操作数。

  3. 如果两个操作数都是对象,则返回第一个操作数。

  4. 如果两个操作数都是 null,则返回 null。

  5. 如果两个操作数都是 NaN,则返回 NaN。

  6. 如果两个操作数都是 undefined,则返回 undefined。

直接用表格来表示一些转换规则:

Value to String to Number to Boolean
undefined "undefined" NaN flase
null "null" 0 false
true "true" 1
false "false" 0
""(空字符串) 0 false
"3.14" 3.14 true
"123word" NaN true
0(+0) "0" false
-0 "0" false
-1(不为0) "-1" true
NaN "NaN" false

显式强制转换

  1. 使用内建构造函数封装

    // String
    String(123); // => "123"
    String(3.14); // => "3.14"
    String(NaN); // => "NaN"
    ...
    // Boolean
    Boolean(1); // => true
    Boolean(-0); // => false
    Boolean(undefined); // => false
    ...
    // Number
    Number(null); // => 0
    Number(undefined);// => NaN
    Number("not a Number"); // => NaN
    Number(true); // => 1
    

    对于 to Number ,还可以使用 Number 内置静态函数 parseInt()parseFloat(),这两个函数期待一个 String 类型的参数。

    Number.parseInt("3.14"); // => 3
    Number.parseFloat("3.14"); // => 3.14
    parseInt(NaN); // => NaN
    parseFloat(NaN); // => NaN
    parseInt(true); // => NaN
    parseInt("123word"); // => 123
    parseInt("s1341"); // => NaN
    
  2. 使用运算符

    对于 Boolean 使用两次取反操作符 !! 可强制转换为布尔值。

    !!NaN; // => false
    !!0; // => false
    !!'0' // => true
    !!null // => flase
    !!'' // => false
    !!null // => false
    !!undefined // => false
    

    对于Number则使用四则运算 + - * / ,一般使用 +- 多。

    +NaN // => NaN
    +'' // => 0
    +undefined // => NaN
    +null // => 0
    +true // => 1
    +false // => 0
    +"3.14" // => 3.14
    +"s123" // => NaN
    +'123s' // => NaN
    

    +二元运算符时比较特殊:

    • 当有一个操作数为String类型时,此时作为字符串连接符。

    • 当有一个操作数为Number类型,另一个为其他基本类型(除String),此时为数学加法,不为Number类型被转换为Number类型。

    • 当有一个操作数为Number类型(或者两个都是引用类型),另一个为对象(引用类型),双方都转为字符串

    123 + "str" => // "123str"
    2 + false // => 2+0=2
    1 + true // => 1+1=2
    1 + NaN // => NaN
    1 + null // => 1+0=1
    1 + undefined // => 1+NaN=NaN
    let obj1 = {};
    let ojb2 = {};
    obj1 + obj2 // => '[object Object][object Object]'
    
    1 + [] // => '1'
    1 + {} // => '1[object Object]'
    1 + function(){} // => '1function(){}'
    

对象转换为基本类型

Boolean

由上面知道除了''undefinednull0NaN为假值,其他所有值都是真值,所以对象的布尔值都会true

对象转基本类型有三种变体(variant),发生在各种情况下。这些情况被称为hint(ECMAScript规范中这样定义:当一个对象被用在需要原始值的上下文中时,例如,在 alert 或数学运算中,对象会被转换为原始值)。

Hint

"string"

对象到字符串的转换,当我们对期望一个字符串的对象执行操作,如作为alert的参数:

// 输出
alert(obj); // 此时hint的值为"string"

// 将对象作为属性键
anotherObj[obj] = 123;

"number"

对象到数字的转换,例如当我们进行数学运算时:

// 显式转换
let num = Number(obj);

// 数学运算(除了二元加法)
let n = +obj; // 一元加法
let delta = date1 - date2;

// 小于/大于的比较
let greater = user1 > user2;

"default"

二元加法+,不严格相等 ==<>操作符。

// 二元加法使用默认 hint
let total = obj1 + obj2;

// obj == number 使用默认 hint
if (user == 1) { ... };

为了实现转换,JavaScript会遵循以下规则(按1-3顺序):

  1. 调用 obj[Symbol.toPrimitive](hint) —— 带有 symbol 键 Symbol.toPrimitive(系统 symbol)的方法,如果这个方法存在的话

  2. 否则,如果 hint 是 "string" —— 尝试 obj.toString()obj.valueOf(),无论哪个存在。

  3. 否则,如果 hint 是 "number""default" —— 尝试 obj.valueOf()obj.toString(),无论哪个存在。

Symbol.toPrimitive

这是个有趣的特性,来点代码~ 顺便理解下hint:

let obj = {};
obj[Symbol.toPrimitive] = function(hint){
  console.log(`hint: ${hint}`);
}

+obj; // => hint: number
alert(obj) // => hint: string
if(obj == 3.14){} // => hint: default
1 + obj; // => hint: default

Symbol.toPrimitive必须返回一个原始值,否则就按上面不存在对待。

由上面可知Symbol.toPrimitive的优先级较高(后面的toString()和valueOf()也可),我们可以用它来实现一些特别的操作!

如:让 a==1 && a==2 && a==3返回true ?

let a = {value: 0};
a[Symbol.toPrimitive] = function(hint) { // 此时 hint为default
    return this.value++;
}
a==1 && a==2 && a==3; // => true

toString()valueof()

如果没有 Symbol.toPrimitive(这也是目前默认的),那么 JavaScript 将尝试寻找 toStringvalueOf 方法:

  • 对于 “string” hint:toString,如果它不存在,则 valueOf(因此,对于字符串转换,优先 toString)。

  • 对于其他 hint:valueOf,如果它不存在,则 toString(因此,对于数学运算,优先 valueOf)。

这些方法必须返回一个原始值。如果 toStringvalueOf 返回了一个对象,那么返回值会被忽略(和这里没有方法的时候相同)。

默认情况下,普通对象具有 toStringvalueOf 方法:

  • toString 方法返回一个字符串 "[object Object]"

  • valueOf 方法返回对象自身。

一般地,toString()作为“全能”的方法来处理转换。

看代码:

let obj = {};
if(obj.hasOwnProperty(Symbol.toPrimitive)){
  alert(obj); // => "[object Object]"
}
obj === obj.valueOf() // => true

讨论一些让人觉得怪(weird)的。

+[]; // => 此时hint明显为number, [].valueOf()返回自身(对象),直接当不存在,因此调用toString() => '' => 0
         // 原始类型转换 '' => 0
alert([]); // => hint: string => [].toString() => ''
+{}; // => hint: number => {}.valueOf() => {} => {}.toString() => '[object Object]' => NaN
alert({}); // => hint: string => toString() => '[object Object]'

hint"default",更是怪异:

[] == {}; // => '' == '[object Object]' 明显为false
[] + {}; // => '' + '[object Object]' => '[object Object]'
{} + []; // => 0 ??? 这是为什么?
                 // 因为{}在这里是代码块(什么也不执行), 而 + 则作为显示强制转换,hint为number

有了这些知识后,我们可以将开头的打印 'nb'解出来了:

([][[]] + [])[+!![]] + ([] + {})[!+[] + !![]]; // => 'nb'
 [][[]] //=> 这是一个数组访问表达式,hint为number => [].toString() => '' => 0,而[][0]明显为undefined
 [][[]] + [] // => undefined + [] hint为default => "undefined" + '' => "undefined"
             [+!![]] // => +true => 1 即 "undefined"[1] === 'n'
                        [] + {} // => '[object Object]'
                                 !+[] + !![] // => !0 + true => true + true => 2

说实话我是挺烦这种脑筋急转弯的。。就像耍小聪明!在最后说一下不严格相等操作符 ==和严格相等操作符 ===

==

等于运算符(==)检查其两个操作数是否相等,并返回Boolean结果。与严格相等运算符(===)不同,它会尝试强制类型转换并且比较不同类型的操作数。

相等运算符(==!=)使用抽象相等比较算法比较两个操作数。可以大致概括如下:

  • 如果两个操作数都是对象,则仅当两个操作数都引用同一个对象时才返回true

  • 如果一个操作数是null,另一个操作数是undefined,则返回true

  • 如果两个操作数是不同类型的,就会尝试在比较之前将它们转换为相同类型:

    • 当数字与字符串进行比较时,会尝试将字符串转换为数字值。

    • 如果操作数之一是Boolean,则将布尔操作数转换为1或0。

      • 如果是true,则转换为1

      • 如果是 false,则转换为0

    • 如果操作数之一是对象,另一个是数字或字符串,会尝试使用对象的valueOf()toString()方法将对象转换为原始值。

  • 如果操作数具有相同的类型,则将它们进行如下比较:

    • Stringtrue仅当两个操作数具有相同顺序的相同字符时才返回。

    • Numbertrue仅当两个操作数具有相同的值时才返回。+0并被-0视为相同的值。如果任一操作数为NaN,则返回false

    • Booleantrue仅当操作数为两个true或两个false时才返回true

列举一些Weird的例子:

NaN != NaN; // => true
NaN == NaN; // => false
null == undefined; // true
1 == '1'; // => true
true == 1; // => true
String('string') == 'string'; // => true

===

全等运算符(===!==)使用全等比较算法来比较两个操作数。

  • 如果操作数的类型不同,则返回 false

  • 如果两个操作数都是对象,只有当它们指向同一个对象时才返回 true

  • 如果两个操作数都为 null,或者两个操作数都为 undefined,返回 true

  • 如果两个操作数有任意一个为 NaN,返回 false

  • 否则,比较两个操作数的值:

    • 数字类型必须拥有相同的数值。+0-0 会被认为是相同的值。

    • 字符串类型必须拥有相同顺序的相同字符。

    • 布尔运算符必须同时为 true 或同时为 false

代码就不带了,大同小异。

Reference参考: