正文之前,先抛出几组问题:
// 第一组
[] == [] //false
// 第二组
[] == ![] //true
{} == !{} //false
{} == ![] // false
[] == !{} //true
{} == 1 // false
// 第三组
{} < 1 // false
{} > 1 // false
看到这几个问题,是不是一脸懵逼?
稍微有点基础的同学,应该一眼就能看出 [] == []
输出 false
,因为 Object
是引用类型,两个引用类型做 ==
比较,如果它们引用的是同一个地址,输出 true
,否则输出 false
。但是后面几道题可能会有一点点麻烦。
后面几道题都涉及到 JavaScript
中的一个难点:隐式转换。本文将会带领大家深入了解 JavaScript
中的类型转换机制。
1,JavaScript 数据类型
js数据类型分为两大类:
- 基本类型(原始值):
Undefined,Null,Boolean,Number,String, Symbol
; - 对象类型:
Object
;
2,ECMAScript 规范中的抽象操作
2.1 ToPrimitive ( input [, PreferredType] ) 转换为原始值
抽象操作 ToPrimitive(input[, PreferredType])
,将input
参数转换为一个非对象类型的值,即原始值类型。转换规则如下:
-
Undefined
:返回原始值,不转换 -
Null
:返回原始值,不转换 -
Boolean
:返回原始值,不转换 -
Number
:返回原始值,不转换 -
String
:返回原始值,不转换 -
Symbol
:返回原始值,不转换 -
Object
:见下文
当 Type(input)
为 Object
时,可以将抽象操作 ToPrimitive(input[, PreferredType])
的执行过程用如下代码解释:
// 仿抽象操作 ToPrimitive(input[, PreferredType]) 的执行过程
function ToPrimitive(input: Object, PreferredType: undefined | 'String' | 'Number') {
let hint;
// 如果没有送 PreferredType ,hint 为 'default'
// 如果 PreferredType 为 'String', hint 为 'string'
// 如果 PreferredType 为 'Number', hint 为 'number'
if(!PreferredType) {
hint = 'default';
} else if (PreferredType === 'String') {
hint = 'string';
} else if(PreferredType === 'Number') {
hint = 'number';
}
// 获取对象的 @@toPrimitive 方法,如果对象自身没有,会一直顺着原型链查找
let exoticToPrim = GetMethod(input, Symbol.toPrimitive);
// 如果该方法不为undefined,用对象调用该方法,赋值给result
// 如果 result 不是 Object 类型,返回 result;否则抛出 TypeError 异常
if(exoticToPrim !== undefined) {
let result = exoticToPrim.call(input, hint);
if (typeof result !== 'object') {
return result;
}
throw new TypeError();
}
// 如果该方法为undefined
// 如果 hint 为 'default',令 hint 为 'number'
// 返回 抽象操作 OrdinaryToPrimitive 的调用结果
if(hint === 'default'){
hint = 'number';
}
return OrdinaryToPrimitive(input,hint);
}
这里涉及到另一个抽象操作 GetMethod,我们先看一下ECMAScript 6 规范中对 GetMethod 的定义:
简单说来就是: 抽象操作 GetMethod(O,P)
获取对象 O 的 P 属性,如果 该属性是 undefined
,返回 undefined
;如果该属性是一个函数,返回此函数;否则抛出一个 TypeError
异常。
下面我们看一下 抽象操作 OrdinaryToPrimitive
的过程:
// 仿抽象操作 OrdinaryToPrimitive(O, hint) 的执行过程
function OrdinaryToPrimitive(O: Object, hint: 'string' | 'number') {
// 假定 hint 是一个字符串,并且其值只能是'string' 或 'number'
// 如果 hint === 'string',令 methodNames = ['toString', 'valueOf']
// 如果 hint === 'number',令 methodNames = ['valueOf', 'toString']
let methodNames;
if(hint === 'string') {
methodNames = ['toString', 'valueOf'];
} else {
methodNames = ['valueOf', 'toString'];
}
// 遍历 methodNames,获取对象的方法,赋值给 result
// 如果 result 不是 Object,终止遍历,并返回 result
// 抛出一个 TypeError 异常
for (let item of methodNames) {
let method = O[item];
let result = method.call(O);
if(typeof(result) !== 'object') {
return result;
}
}
throw new TypeError();
}
Date
对象和 Symbol
对象的原型上已经部署了 [@@toPrimitive]
方法,这个方法是不可枚举(enumerable: false
),不可改写的(writable: false
)。对于Date
对象原型上的[@@toPrimitive]
方法,如果没有送hint
,会将hint
当作'string'
。
我们可以使用 Symbol.toPrimitive
来给 Object
添加 [@@toPrimitive]
方法:
Object.prototype[Symbol.toPrimitive] = function(hint) {
if(hint === 'default') {
let thisType = Object.prototype.toString.call(this);
if(thisType === '[object Date]') {
hint = 'string';
} else {
hint = 'number';
}
}
let methodNames;
if(hint === 'string') {
methodNames = ['toString', 'valueOf'];
} else if(hint === 'number') {
methodNames = ['valueOf', 'toString'];
} else {
throw new TypeError('Invalid hint: ' + hint);
}
for (let key of methodNames) {
let method = this[key];
let result = method.call(this);
if(typeof(result) !== 'object') {
return result;
}
}
throw new TypeError();
}
2.1.1 对象的 valueOf() 方法和 toString() 方法
对象在执行 ToPrimitive 转换时,需要用到对象的valueOf()
和toString()
方法。我们可以在Object.prototype
上找到这两个方法。在JavaScript
中,Object.prototype
是所有对象原型链的顶层原型,因此,任何对象都有valueOf()
和toString()
方法。
JavaScript
的许多内置对象都重写了这两个方法,以实现更适合自身的功能需要。
不同类型对象的valueOf()
方法的返回值:
-
Array
: 返回数组对象本身。 -
Boolean
: 布尔值。 -
Date
: 存储的时间是从 1970 年 1 月 1 日午夜开始计的毫秒数 UTC。 -
Function
: 函数本身。 -
Number
: 数字值。 -
Object
: 对象本身。这是默认情况。 -
String
: 字符串值。 -
Symbol
:Symbol值本身。
不同类型对象的toString()
方法的返回值:
-
Array
:连接数组并返回一个字符串,其中包含用逗号分隔的每个数组元素。 -
Boolean
:返回字符串"true"
或"false"
。 -
Date
:返回一个美式英语日期格式的字符串。 -
Function
:返回一个字符串,其中包含用于定义函数的源文本段。 -
Number
: 返回指定Number
对象的字符串表示形式。 -
Object
: 返回"[object type]"
,其中type
是对象的类型。 -
String
: 字符串值。 -
Symbol
:返回当前Symbol
对象的字符串表示。
2.2 ToBoolean 转换为布尔值类型
抽象操作 ToBoolean
根据下列规则将其参数转换为布尔值类型的值:
-
Undefined
:false
-
Null
:false
-
Boolean
:结果等于输入的参数(不转换)。 -
Number
:如果参数是+0, -0,
或NaN
,结果为false
;否则结果为true
。 -
String
:如果参数是空字符串(其长度为零),结果为false
,否则结果为true
。 -
Symbol
:true
。 -
Object
:true
。
2.3 ToNumber 转换为数值类型
抽象操作 ToNumber
根据下列规则将其参数转换为数值类型的值:
-
Undefined
:NaN -
Null
:+0 -
Boolean
:如果参数是 true,结果为1
。如果参数是 false,此结果为+0
。 -
Number
:结果等于输入的参数(不转换)。 -
String
:参见下文 -
Symbol
:抛出TypeError
异常 -
Object
:先进行ToPrimitive
转换,得到原始值,再进行ToNumber
转换
2.3.1 对字符串类型应用 ToNumber
对字符串应用 ToNumber
时,如果符合如下规则,转为数值:
- 十进制的字符串数值常量,可有任意位数的0在前面,如
'000123'
和'123'
都会被转为123
- 指数形式的字符串数值常量,如
'1e2'
转为100
- 带符号的十进制字符串数值常量或指数字符串数值常量,如
'-100', '-1e2'
都会转为-100
- 二进制,八进制,十六进制的字符串数值常量,如
'0b11', '0o11', '0x11'
分别转为3, 9, 17
- 符合上述条件的字符串数值常量开头或结尾,可以包含任意多个空格。如
' 0b11 '
转为3
- 空字符串(长度为零的字符串)或只有空格的字符串,转为
0
如果字符串不符合上述规则,将转为NaN
。
2.4 ToString 转为字符串类型
抽象操作 ToString
根据下列规则将其参数转换为字符串类型的值:
-
Undefined
:"undefined"
-
Null
:"null"
-
Boolean
:如果参数是true
,那么结果为"true"
。 如果参数是false
,那么结果为"false"
。 -
String
:结果等于输入的参数(不转换)。 -
Number
:参见下文。 -
Symbol
:抛出TypeError
异常 -
Object
:先进行ToPrimitive
转换,hint
为'string'
,得到原始值,再进行ToString
转换
2.4.1 对数值类型应用 ToString
抽象操作 ToString
运算符将数字 m 转换为字符串格式的给出如下所示:
- 如果 m 是
NaN
,返回字符串"NaN"
。 - 如果 m 是
+0
或-0
,返回字符串"0"
。 - 如果 m 小于零,返回
"-m"
。 - 如果 m 正无限大,返回字符串
"Infinity"
。如果 m 负无限大,返回字符串"-Infinity"
。 - 否则,返回 "m" 或 m 的指数形式的字符串数值常量
2.5,抽象操作 GetValue
先看一下 ECMAScript 规范中定义的 GetValue
方法:
注意区分这一句:2. If Type(V) is not Reference, return V.
中的 Reference
和我们平时说的 引用类型
的区别。
我们平时说的 引用类型 指的是 ECMAScript 规范中 语言类型的 Object 类型(例如 Object, Array, Date 等);而这里的 Reference 指的是ECMAScript 规范中 规范类型的 Reference 类型 ,是一个抽象的概念。
按规范的描述,Reference 是一个 name binding,由三部分组成:
- base:一个 undefined,Object, Boolean, String, Symbol, Number 或者 环境记录(Environment Record)
- referreference name:一个字符串 或 Symbol 值
- strict mode flag:严格引用标志
举个例子:赋值语句 let obj.a = 1
中的 obj.a
产生的 Reference,base 是 obj,referreference name 是 'b',至于 strict mode flag 是用来检测是否处于严格模式。
Reference 和 环境记录(Environment Record) 这些概念是为了更好地描述语言的底层行为逻辑才存在的,并不存在于我们实际的 js 代码中。
4,逻辑非运算符(!)
逻辑非运算符(!) 按下列过程将表达式转换为布尔值
- 令
expr
为表达式求值的结果 - 令
oldValue = ToBoolean(GetValue(expr))
- 如果
oldValue === true
,返回false
;否则返回true
因此,逻辑非运算符(!)可以当作是:对 ToBoolean 操作的结果取反。
5,== 运算符
比较运算 x==y,按如下规则进行:
1,若 Type(x) 与 Type(y) 相同, 则
1) 若 Type(x) 为 Undefined, 返回 true。
2) 若 Type(x) 为 Null, 返回 true。
3) 若 Type(x) 为 Number, 则
(1)、若 x 为 NaN, 返回 false。
(2)、若 y 为 NaN, 返回 false。
(3)、若 x 与 y 为相等数值, 返回 true。
(4)、若 x 为 +0 且 y 为 −0, 返回 true。
(5)、若 x 为 −0 且 y 为 +0, 返回 true。
(6)、返回 false。
4) 若 Type(x) 为 String,则当 x 和 y 为完全相同的字符序列时返回 true。 否则,返回 false。
5) 若 Type(x) 为 Boolean,当 x 和 y 为同为 true 或者同为 false 时返回 true。 否则, 返回 false。
6) 若 Type(x) 为 Symbol,如果 x 和 y 是同一个 Symbol,返回true。否则,返回 false。
7) 若 Type(x) 为 Object,当 x 和 y 是对同一对象的引用时返回 true。否则,返回 false。
2,若 x 为 null 且 y 为 undefined,返回 true。
3,若 x 为 undefined 且 y 为 null,返回 true。
4,若 Type(x) 为 Number 且 Type(y) 为 String,返回 x == ToNumber(y)的结果。
5,若 Type(x) 为 String 且 Type(y) 为 Number,返回 ToNumber(x) == y的结果。
6,若 Type(x) 为 Boolean, 返回 ToNumber(x) == y 的结果。
7,若 Type(y) 为 Boolean, 返回 x == ToNumber(y) 的结果。
8、若 Type(x) 为 String 或 Number 或 Symbol,且 Type(y) 为 Object,返回 x == ToPrimitive(y) 的结果。
9、若 Type(x) 为 Object 且 Type(y) 为 String 或 Number 或 Symbol, 返回 ToPrimitive(x) == y 的结果。
10、返回 false。
现在,我们来分析一下文章开头提出的问题:[] == ![] // true
- 根据 上面 逻辑非运算符 和
ToPrimitive
的规则,![]
返回false
,因此,我们接下来需要比较的是[] == false
; -
[] == false
符合上面规则中的第 7 条,需要对false
执行ToNumber
转换,得到 0,接下来要比较[] == 0
; -
[] == 0
符合上面规则中的第 9 条,对[]
进行ToPrimitive
转换,得到空字符串''
,接下来要比较'' == 0
; -
'' == 0
符合上面规则中的第 5 条,对''
进行ToNumber
转换,得到 0 - 接下来比较
0 == 0
,得到true
其他几道题我就不一一分析了,有兴趣的同学们可以自己分析验证。提示一下,需要注意 Object.prototype.toString
和 Array.prototype.toString
的区别
6,比较运算符
比较运算 x < y,按照如下规则执行
1,令 px = ToPrimitive(x),令 py = ToPrimitive(y)。
2,如果 Type(px) 和 Type(py) 都是 String,则
1)、如果 py 是 px 的前缀,返回 false。
2)、如果 px 是 py 的前缀,返回 true。
3)、找出 px 和 py 中 相同下标处第一个不同的字符串单元,将其 词典排序 分别记为 m 和 n。
4)、如果 m < n,返回 true,否则,返回 false。
3,令 nx = ToNumber(px),令 ny = ToNumber(py)
1)、如果 Type(nx) 是 NaN,返回 false。
2)、如果 Type(ny) 是 NaN,返回 false。
3)、如果 nx 是 +0,ny 是 -0,返回 true。
4)、如果 nx 是 -0,ny 是 +0,返回 true。
5)、如果 nx 是 +Infinity,返回 false。
6)、如果 ny 是 +Infinity,返回 true 。
7)、如果 ny 是 -Infinity,返回 false。
8)、如果 nx 是 -Infinity,返回 true。
9)、如果 nx 的数学值小于 ny 的数学值,返回 true,否则返回 false。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。