共计 7173 个字符,预计需要花费 18 分钟才能阅读完成。
你有没有在面试中遇到特地奇葩的 js 隐形转换的面试题,第一反馈是怎么会是这样呢?难以自信,js 到底是怎么去计算失去后果,你是否有深刻去理解其原理呢?上面将深刻解说其实现原理。
其实这篇文章初稿三个月前就写好了,在我读一些源码库时,遇到了这些基础知识,想归档整顿下,就有了这篇文章。因为始终忙没工夫整顿,最近看到了这个比拟热的题,决定把这篇文章整顿下。
const a = {
i: 1,
toString: function () {return a.i++;}
}
if (a == 1 && a == 2 && a == 3) {console.log('hello world!');
}
网上给出了很多不错的解析过程,读了上面内容,你将更深刻的理解其执行过程。
1、js 数据类型
js 中有 7 种数据类型,能够分为两类:原始类型、对象类型:
根底类型(原始值):
Undefined、Null、String、Number、Boolean、Symbol (es6 新出的,本文不探讨这种类型)
简单类型(对象值):
object
2、三种隐式转换类型
js 中一个难点就是 js 隐形转换,因为 js 在一些操作符下其类型会做一些变动,所以 js 灵便,同时造成易出错,并且难以了解。
波及隐式转换最多的两个运算符 + 和 ==。
+ 运算符即可数字相加,也能够字符串相加。所以转换时很麻烦。== 不同于 ===,故也存在隐式转换。- * / 这些运算符只会针对 number 类型,故转换的后果只能是转换成 number 类型。
既然要隐式转换,那到底怎么转换呢,应该有一套转换规则,能力追踪最终转换成什么了。
隐式转换中次要波及到三种转换:
1、将值转为原始值,ToPrimitive()。
2、将值转为数字,ToNumber()。
3、将值转为字符串,ToString()。
2.1、通过 ToPrimitive 将值转换为原始值
js 引擎外部的形象操作 ToPrimitive 有着这样的签名:
ToPrimitive(input, PreferredType?)
input 是要转换的值,PreferredType 是可选参数,能够是 Number 或 String 类型。他只是一个转换标记,转化后的后果并不一定是这个参数所值的类型,然而转换后果肯定是一个原始值(或者报错)。
2.1.1、如果 PreferredType 被标记为 Number,则会进行上面的操作流程来转换输出的值。
1、如果输出的值曾经是一个原始值,则间接返回它
2、否则,如果输出的值是一个对象,则调用该对象的 valueOf()办法,如果 valueOf()办法的返回值是一个原始值,则返回这个原始值。3、否则,调用这个对象的 toString()办法,如果 toString()办法返回的是一个原始值,则返回这个原始值。4、否则,抛出 TypeError 异样。
2.1.2、如果 PreferredType 被标记为 String,则会进行上面的操作流程来转换输出的值。
1、如果输出的值曾经是一个原始值,则间接返回它
2、否则,调用这个对象的 toString()办法,如果 toString()办法返回的是一个原始值,则返回这个原始值。3、否则,如果输出的值是一个对象,则调用该对象的 valueOf()办法,如果 valueOf()办法的返回值是一个原始值,则返回这个原始值。4、否则,抛出 TypeError 异样。
既然 PreferredType 是可选参数,那么如果没有这个参数时,怎么转换呢?PreferredType 的值会依照这样的规定来主动设置:
1、该对象为 Date 类型,则 PreferredType 被设置为 String
2、否则,PreferredType 被设置为 Number
2.1.3、valueOf 办法和 toString 办法解析
下面次要提及到了 valueOf 办法和 toString 办法,那这两个办法在对象里是否肯定存在呢?答案是必定的。在控制台输入 Object.prototype,你会发现其中就有 valueOf 和 toString 办法,而 Object.prototype 是所有对象原型链顶层原型,所有对象都会继承该原型的办法,故任何对象都会有 valueOf 和 toString 办法。
先看看对象的 valueOf 函数,其转换后果是什么?对于 js 的常见内置对象:Date, Array, Math, Number, Boolean, String, Array, RegExp, Function
。
1、Number、Boolean、String 这三种构造函数生成的根底值的对象模式,通过 valueOf 转换后会变成相应的原始值。如:参考 前端进阶面试题具体解答
var num = new Number('123');
num.valueOf(); // 123
var str = new String('12df');
str.valueOf(); // '12df'
var bool = new Boolean('fd');
bool.valueOf(); // true
2、Date 这种非凡的对象,其原型 Date.prototype 上内置的 valueOf 函数将日期转换为日期的毫秒的模式的数值。
var a = new Date();
a.valueOf(); // 1515143895500
3、除此之外返回的都为 this,即对象自身:(有问题欢送告知)
var a = new Array();
a.valueOf() === a; // true
var b = new Object({});
b.valueOf() === b; // true
再来看看 toString 函数,其转换后果是什么?对于 js 的常见内置对象:Date, Array, Math, Number, Boolean, String, Array, RegExp, Function
。
1、Number、Boolean、String、Array、Date、RegExp、Function 这几种构造函数生成的对象,通过 toString 转换后会变成相应的字符串的模式,因为这些构造函数上封装了本人的 toString 办法。如:
Number.prototype.hasOwnProperty('toString'); // true
Boolean.prototype.hasOwnProperty('toString'); // true
String.prototype.hasOwnProperty('toString'); // true
Array.prototype.hasOwnProperty('toString'); // true
Date.prototype.hasOwnProperty('toString'); // true
RegExp.prototype.hasOwnProperty('toString'); // true
Function.prototype.hasOwnProperty('toString'); // true
var num = new Number('123sd');
num.toString(); // 'NaN'
var str = new String('12df');
str.toString(); // '12df'
var bool = new Boolean('fd');
bool.toString(); // 'true'
var arr = new Array(1,2);
arr.toString(); // '1,2'
var d = new Date();
d.toString(); // "Wed Oct 11 2017 08:00:00 GMT+0800 (中国规范工夫)"
var func = function () {}
func.toString(); // "function () {}"
除这些对象及其实例化对象之外,其余对象返回的都是该对象的类型,(有问题欢送告知),都是继承的 Object.prototype.toString 办法。
var obj = new Object({});
obj.toString(); // "[object Object]"
Math.toString(); // "[object Math]"
从下面 valueOf 和 toString 两个函数对对象的转换能够看出为什么对于 ToPrimitive(input, PreferredType?),PreferredType 没有设定的时候,除了 Date 类型,PreferredType 被设置为 String,其它的会设置成 Number。
因为 valueOf 函数会将 Number、String、Boolean 根底类型的对象类型值转换成 根底类型,Date 类型转换为毫秒数,其它的返回对象自身,而 toString 办法会将所有对象转换为字符串。显然对于大部分对象转换,valueOf 转换更正当些,因为并没有规定转换类型,应该尽可能放弃原有值,而不应该想 toString 办法一样,一股脑将其转换为字符串。
所以对于没有指定 PreferredType 类型时,先进行 valueOf 办法转换更好,故将 PreferredType 设置为 Number 类型。
而对于 Date 类型,其进行 valueOf 转换为毫秒数的 number 类型。在进行隐式转换时,没有指定将其转换为 number 类型时,将其转换为那么大的 number 类型的值显然没有多大意义。(不论是在 + 运算符还是 == 运算符)还不如转换为字符串格局的日期,所以默认 Date 类型会优先进行 toString 转换。故有以上的规定:
PreferredType 没有设置时,Date 类型的对象,PreferredType 默认设置为 String,其余类型对象 PreferredType 默认设置为 Number。
2.2、通过 ToNumber 将值转换为数字
依据参数类型进行上面转换:
参数 | 后果 |
---|---|
undefined | NaN |
null | +0 |
布尔值 | true 转换 1,false 转换为 +0 |
数字 | 毋庸转换 |
字符串 | 有字符串解析为数字,例如:‘324’转换为 324,‘qwer’转换为 NaN |
对象(obj) | 先进行 ToPrimitive(obj, Number)转换失去原始值,在进行 ToNumber 转换为数字 |
2.3、通过 ToString 将值转换为字符串
依据参数类型进行上面转换:
参数 | 后果 |
---|---|
undefined | ‘undefined’ |
null | ‘null’ |
布尔值 | 转换为 ’true’ 或 ‘false’ |
数字 | 数字转换字符串,比方:1.765 转为 ’1.765′ |
字符串 | 毋庸转换 |
对象(obj) | 先进行 ToPrimitive(obj, String)转换失去原始值,在进行 ToString 转换为字符串 |
讲了这么多,是不是还不是很清晰,先来看看一个例子:
({} + {}) = ?
两个对象的值进行 + 运算符,必定要先进行隐式转换为原始类型能力进行计算。1、进行 ToPrimitive 转换,因为没有指定 PreferredType 类型,{}会使默认值为 Number,进行 ToPrimitive(input, Number)运算。2、所以会执行 valueOf 办法,({}).valueOf(), 返回的还是 {} 对象,不是原始值。3、继续执行 toString 办法,({}).toString(), 返回 "[object Object]",是原始值。故失去最终的后果,"[object Object]" + "[object Object]" = "[object Object][object Object]"
再来一个指定类型的例子:
2 * {} = ?
1、首先 * 运算符只能对 number 类型进行运算,故第一步就是对 {} 进行 ToNumber 类型转换。2、因为 {} 是对象类型,故先进行原始类型转换,ToPrimitive(input, Number)运算。3、所以会执行 valueOf 办法,({}).valueOf(), 返回的还是 {} 对象,不是原始值。4、继续执行 toString 办法,({}).toString(), 返回 "[object Object]",是原始值。5、转换为原始值后再进行 ToNumber 运算,"[object Object]" 就转换为 NaN。故最终的后果为 2 * NaN = NaN
3、== 运算符隐式转换
== 运算符的规定规律性不是那么强,依照上面流程来执行,es5 文档
比拟运算 x==y, 其中 x 和 y 是值,返回 true 或者 false。这样的比拟按如下形式进行: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* 当 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,且 Type(y) 为 Object,返回比拟 x == ToPrimitive(y) 的后果。9、若 Type(x) 为 Object 且 Type(y) 为 String 或 Number,返回比拟 ToPrimitive(x) == y 的后果。10、返回 false。
下面次要分为两类,x、y 类型雷同时,和类型不雷同时。
类型雷同时,没有类型转换,次要留神 NaN 不与任何值相等,包含它本人,即 NaN !== NaN。
类型不雷同时,
1、x,y 为 null、undefined 两者中一个 // 返回 true
2、x、y 为 Number 和 String 类型时,则转换为 Number 类型比拟。
3、有 Boolean 类型时,Boolean 转化为 Number 类型比拟。
4、一个 Object 类型,一个 String 或 Number 类型,将 Object 类型进行原始转换后,按下面流程进行原始值比拟。
3.1、== 例子解析
所以类型不雷同时,能够会进行下面几条的比拟,比方:
var a = {valueOf: function () {return 1;},
toString: function () {return '123'}
}
true == a // true;
首先,x 与 y 类型不同,x 为 boolean 类型,则进行 ToNumber 转换为 1, 为 number 类型。接着,x 为 number,y 为 object 类型,对 y 进行原始转换,ToPrimitive(a, ?), 没有指定转换类型,默认 number 类型。而后,ToPrimitive(a, Number)首先调用 valueOf 办法,返回 1,失去原始类型 1。最初 1 == 1,返回 true。
咱们再看一段很简单的比拟,如下:
[] == !{}
//
1、! 运算符优先级高于 ==,故先进行!运算。2、!{}运算后果为 false,后果变成 [] == false 比拟。3、依据下面第 7 条,等式左边 y = ToNumber(false) = 0。后果变成 [] == 0。4、依照下面第 9 条,比拟变成 ToPrimitive([]) == 0。依照下面规定进行原始值转换,[]会先调用 valueOf 函数,返回 this。不是原始值,持续调用 toString 办法,x = [].toString() = ''。故后果为 '' == 0 比拟。5、依据下面第 5 条,等式右边 x = ToNumber('') = 0。所以后果变为:0 == 0,返回 true,比拟完结。
最初咱们看看文章结尾说的那道题目:
const a = {
i: 1,
toString: function () {return a.i++;}
}
if (a == 1 && a == 2 && a == 3) {console.log('hello world!');
}
1、当执行 a == 1 && a == 2 && a == 3 时,会从左到右一步一步解析,首先 a == 1,会进行下面第 9 步转换。ToPrimitive(a,Number) == 1。
2、ToPrimitive(a, Number),依照下面原始类型转换规定,会先调用 valueOf 办法,a 的 valueOf 办法继承自 Object.prototype。返回 a 自身,而非原始类型,故会调用 toString 办法。
3、因为 toString 被重写,所以会调用重写的 toString 办法,故返回 1,留神这里是 i ++,而不是 ++i,它会先返回 i,在将 i +1。故 ToPrimitive(a, Number) = 1。也就是 1 == 1,此时 i = 1 + 1 = 2。
4、执行完 a == 1 返回 true,会执行 a == 2,同理,会调用 ToPrimitive(a, Number),同上先调用 valueOf 办法,在调用 toString 办法,因为第一步,i = 2 此时,ToPrimitive(a, Number) = 2,也就是 2 == 2, 此时 i = 2 + 1。
5、同上能够推导 a == 3 也返回 true。故最终后果 a == 1 && a == 2 && a == 3 返回 true
其实理解了以上隐形转换的原理,你有没有发现这些隐式转换并没有设想中那么难。