为强制类型转换正名

强制类型转换

引子

强制类型转换是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 除外(不同的对象在底层都表示为二进制, 在JavaScript中二进制前三位都为 0 会被判断为 Object 类型, null 的二进制表示为0, 前三位自然为0, 所以执行 typeof 时会返回 'object'.), null是假值, 也是唯一一个typeof检测会返回 'object' 的基本数据类型值.

1
2
3
4
typeof null // "object"

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

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

1
typeof function(){} // "function"

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

1
2
3
4
5
6
7
8
9
10
11
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是什么鬼.

1
2
3
4
5
6
7
8
9
10
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

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

typeresult
null“null”
undefined“undefined”
booleantrue => “true”; false => “false”
string不转换
numberToString Applied to the Number Type
Object先经ToPrimitive返回基本类型值, 再遵循上述规则

ToNumber

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

typeresult
null+0
undefinedNaN
booleantrue => 1; false => 0
stringToNumber Applied to the String Type
number不转换
Object先经ToPrimitive返回基本类型值, 再遵循上述规则

常见的字符串转换数字:

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

ToBoolean

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

typeresult
nullfalse
undefinedfalse
boolean不转换
string“” => false; 其它 => true
number+0, −0, NaN => false; 其它 => true
Objecttrue

真值 & 假值
假值(强制类型转换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

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

抽象相等

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

  1. 同类型的比较.

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

    1
    2
    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 => 同理 [] == ![].

1
2
3
[] == ![]   // true
false == [] // true
"0" == false // true

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

------------- The End -------------
显示评论