2

原文链接, 阅读时长: 10'

引子

强制类型转换是JavaScript开发人员最头疼的问题之一, 它常被诟病为语言设计上的一个缺陷, 太危险, 应该束之高阁.

作为开发人员, 往往会遇到或写过涉及到类型转换的代码, 只是我们从来没有意识到. 因为我们基本碰运气.

猜猜看?:

  1. 作为基本类型值, 为什么我们可以使用相关的属性或方法? eg: 'hello'.charAt(0) (内置类型和内建函数的关系)
  2. a && (b || c) 这波操作我们知道, 那么 if (a && (b || c)), 这里又做了哪些操作? (||和&&)
  3. if (a == 1 && a== 2) { dosomething }, dosomething竟然执行了, 什么鬼? (ToPrimitive)
  4. [] == ![] => true ?; false == [] => true ?; "0" == false => true ?(抽象相等)
  5. if (~indexOf('a')), 这波操作熟悉不? (+/-/!/~)
  6. String, Number, Boolean类型之间比较时, 进行的强制类型转换又遵循了哪些规则? (抽象操作)

下面就要学会用实力碰运气.


类型

内置类型

JavaScript 有七种内置类型. 空值: null, 未定义: undefined, 布尔值: boolean, 数字: number, 字符串: string, 对象: object, 符号: symbol. 除 对象:object, 为复杂数据类型, 其它均为基本数据类型.

内建函数

常用的内建函数: String(), Number(), Boolean(), Array(), Object(), Function(), RegExp(), Date(), Error(), Symbol().

内置类型和内建函数的关系

为了便于操作基本类型值, JavaScript提供了封装对象(内建函数), 它们具有各自的基本类型相应的特殊行为. 当读取一个基本类型值的时候, JavaScript引擎会自动对该值进行封装(创建一个相应类型的对象包装它)从而能够调用一些方法和属性操作数据. 这就解释了 问题 1 .

类型检测

typeof => 基本类型的检测均有同名的与之对应. null 除外, null是假值, 也是唯一一个typeof检测会返回 'object' 的基本数据类型值.

typeof null // "object"

let a = null;
(!a && typeof a === 'object') // true

复杂数据类型typeof检测返回 'object', function(函数)除外. 函数因内部属性[[Call]]使其可被调用, 其实属于可调用对象.

typeof function(){} // "function"

Object.prototype.toString => 通过typeof检测返回'object'的对象中还可以细分为好多种, 从内建函数就可以知道.它们都包含一个内部属性[[Class]], 一般通过Object.prototype.toString(...)来查看.

const str = new String('hello');
const num = new Number(123);
const arr = new Array(1, 2, 3);

console.log(Object.prototype.toString.call(str))
console.log(Object.prototype.toString.call(num))
console.log(Object.prototype.toString.call(arr))

// [object String]
// [object Number]
// [object Array]

抽象操作

在数据类型转换时, 处理不同的数据转换都有对应的抽象操作(仅供内部使用的操作), 在这里用到的包括 ToPrimitive, ToString, ToNumber, ToBoolean. 这些抽象操作定义了一些转换规则, 不论是显式强制类型转换, 还是隐式强制类型转换, 无一例外都遵循了这些规则(显式和隐式的命名叫法来自《你不知道的JavaScript》). 这里就解释了 问题 5问题 6 .

ToPrimitive

该抽象操作是将传入的参数转换为非对象的数据. 当传入的参数为 Object 时, 它会调用内部方法[[DefaultValue]] 遵循一定规则返回非复杂数据类型, 规则详见 DefaultValue. 故 ToString, ToNumber, ToBoolean在处理Object时, 会先经过ToPrimitive处理返回基本类型值.

[[DefaultValue]](hint)语法:

[[DefaultValue]]的规则会依赖于传入的参数hint, ToString传入的 hint 值为 String, ToNumber传入的 hint 值为 Number.

  1. [[DefaultValue]](String) => 若 toString 可调用, 且 toString(Obj) 为基本类型值, 则返回该基本类型值. 否则, 若 valueOf 可调用, 且 valueOf(Obj) 为基本类型值, 则返回该基本类型值. 若以上处理还未得到基本类型值, 则抛出 TypeError.
  2. [[DefaultValue]](Number) => 该规则正好和上规则调用 toString, valueOf 的顺序相反. 若 valueOf 可调用, 且 valueOf(Obj) 为基本类型值, 则返回该基本类型值. 否则, 若 toString 可调用, 且 toString(Obj) 为基本类型值, 则返回该基本类型值. 若以上处理还未得到基本类型值, 则抛出 TypeError.
  3. [[DefaultValue]]() => 未传参时, 按照 hint值为 Number 处理. Date 对象除外, 按照hint值为 String 处理.

现在我们就用以上的知识点来解释 问题 3 是什么鬼.

    let i = 1;
    Number.prototype.valueOf = () => {
        return i++
    };
    let a = new Number("0"); // 字符串强制转换为数字类型是不执行Toprimitive抽象操作的.
    console.log('a_1:', a);
    if(a == 1 && a == 2) {
        console.log('a==1 & a==2', 'i:', i);
    }
    // a==1 & a==2 i: 3

我们改写了内建函数 Number 原型上的 valueOf 方法, 并使得一个字符串转换成 Number 对象, 第一次 Object 类型和 Number 类型做比较时, Object 类型将进行 ToPrimitive 处理(抽象相等), 内部调用了 valueOf, 返回 2. 第二次同样的处理方式, 返回 3.

ToString

该抽象操作负责处理非字符串到字符串的转换.

type result
null "null"
undefined "undefined"
boolean true => "true"; false => "false"
string 不转换
number ToString Applied to the Number Type
Object 先经ToPrimitive返回基本类型值, 再遵循上述规则

ToNumber

该抽象操作负责处理非数字到数字的转换.

type result
null +0
undefined NaN
boolean true => 1; false => 0
string ToNumber Applied to the String Type
number 不转换
Object 先经ToPrimitive返回基本类型值, 再遵循上述规则

常见的字符串转换数字:

  1. 字符串是空的 => 转换为0.
  2. 字符串只包含数字 => 转换为十进制数值.
  3. 字符串包含有效的浮点格式 => 转换为对应的浮点数值.
  4. 字符串中包含有效的十六进制格式 => 转换为相同大小的十进制整数值.
  5. 字符串中包含除以上格式之外的符号 => 转换为 NaN.

ToBoolean

该抽象操作负责处理非布尔值到布尔值转换.

type result
null false
undefined false
boolean 不转换
string "" => false; 其它 => true
number +0, −0, NaN => false; 其它 => true
Object true

真值 & 假值

假值(强制类型转换false的值) => undefined, null, false, +0, -0, NaN, "".
真值(强制类型转换true的值) => 除了假值, 都是真值.

特殊的存在

假值对象 => documen.all 等. eg: Boolean(window.all) // false


隐式强制类型转换

+/-/!/~

  1. +/- 一元运算符 => 运算符会将操作数进行ToNumber处理.
  2. ! => 会将操作数进行ToBoolean处理.
  3. ~ => (~x)相当于 -(x + 1) eg: ~(-1) ==> 0; ~(0) ==> 1; 在if (...)中作类型转换时, 只有-1时, 才为假值.
  4. +加号运算符 => 若操作数有String类型, 则都进行ToString处理, 字符串拼接. 否则进行ToNumber处理, 数字加法.

条件判断

  1. if (...), for(;;;), while(...), do...while(...)中的条件判断表达式.
  2. ? : 中的条件判断表达式.
  3. ||&& 中的中的条件判断表达式.

以上遵循ToBoolean规则.

||和&&

  1. 返回值是两个操作数的中的一个(且仅一个). 首先对第一个操作数条件判断, 若为非布尔值则进行ToBoolean强制类型转换.再条件判断.
  2. || => 条件判断为true, 则返回第一个操作数; 否则, 返回第二个操作数. 相当于 a ? a : b;
  3. && => 条件判断为true, 则返回第二个操作数; 否则, 返回第一个操作数, 相当于 a ? b : a;

结合条件判断, 解释下问题 2

    let a = true;
    let b = undefined;
    let c = 'hello';
    if (a && (b || c)) {
        dosomething()
    }
    a && (b || c) 返回 'hello', if语句中经Toboolean处理强制类型转换为true.

抽象相等

这里的知识点是用来解释 问题 4 的, 也是考验人品的地方. 这下我们要靠实力拼运气.

  1. 同类型的比较.

        +0 == -0 // true
        null == null // true
        undefined == undefined // true
        NaN == NaN // false, 唯一一个非自反的值
  2. nullundefined 的比较.

        null == undefined // true
        undefined == null // true
  3. Number 类型和 String 类型的比较. => String 类型要强制类型转换为 Number 类型, 即 ToNumber(String) .(参见ToNumber)
  4. Boolean 类型和其它类型的比较. => Boolean 类型要强制类型转换为 Number 类型, 即 ToNumber(Boolean) .(参见ToNumber)
  5. Object 类型和 String 类型或 Number 类型. => Object 类型要强制转换为基本类型值, 即 ToPrimitive(Object) .(参见ToPrimitive)
  6. 其它情况, false.

回头看看 问题 4 中的等式. [] == ![], false == [], "0" == false.
[] == ![] => ! 操作符会对操作数进行 ToBoolean 处理, [] 是真值, !true 则为 false. 再遵循第 4 点, Boolean 类型经过 ToNumber 转换为 Number 类型, 则为数值 0. 再遵循第 5 点, 对 [] 进行 ToPrimitive 操作, 先后调用 valueOf(), toString()直到返回基本类型, 直到返回 "". (先[].valueOf() => [], 非基本类型值; 再[].toString() => "", 基本类型值, 返回该基本类型值.). 再遵循第 3 点, 对 "" 进行 ToNumber 处理, 则为数值 0. 到此, 0 == 0, 再遵循第 1 点(其实没写全?, 详见The Abstract Equality Comparison Algorithm), return true, 完美!?.
false == [] => 同理 [] == ![].
"0" == false => 同理 [] == ![].

[] == ![]   // true
false == [] // true
"0" == false    // true

运气是留给有准备的人, 所以呢, 我要准备买彩票了.?


至棣
109 声望3 粉丝