前言

非常深入地讲解了包含隐式转换时js计算过程,全篇干货。本文由@keenjaan授权分享。

本文由@仙人掌推荐分享

正文从这里开始

你有没有在面试中遇到特别奇葩的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被设置为String2、否则,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 = newNumber('123');
num.valueOf(); // 123var str = newString('12df');
str.valueOf(); // '12df'var bool = newBoolean('fd');
bool.valueOf(); // true

2、Date这种特殊的对象,其原型Date.prototype上内置的valueOf函数将日期转换为日期的毫秒的形式的数值。

var a = newDate();
a.valueOf(); // 1515143895500

3、除此之外返回的都为this,即对象本身:(有问题欢迎告知)

var a = newArray();
a.valueOf() === a; // truevar b = newObject({});
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'); // trueBoolean.prototype.hasOwnProperty('toString'); // trueString.prototype.hasOwnProperty('toString'); // trueArray.prototype.hasOwnProperty('toString'); // trueDate.prototype.hasOwnProperty('toString'); // trueRegExp.prototype.hasOwnProperty('toString'); // trueFunction.prototype.hasOwnProperty('toString'); // truevar num = newNumber('123sd');
num.toString(); // 'NaN'var str = newString('12df');
str.toString(); // '12df'var bool = newBoolean('fd');
bool.toString(); // 'true'var arr = newArray(1,2);
arr.toString(); // '1,2'var d = newDate();
d.toString(); // "Wed Oct 11 2017 08:00:00 GMT+0800 (中国标准时间)"var func = function () {}
func.toString(); // "function () {}"

除这些对象及其实例化对象之外,其他对象返回的都是该对象的类型,(有问题欢迎告知),都是继承的Object.prototype.toString方法。

var obj = newObject({});
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将值转换为数字

根据参数类型进行下面转换:
参数结果undefinedNaNnull+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 () {
    return1;
 },
 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

其实了解了以上隐形转换的原理,你有没有发现这些隐式转换并没有想象中那么难。

最后,为你推荐

【第1175期】2017年 JavaScript 明星项目

【第1047期】重新认识JS的this、作用域、闭包、对象

关于本文
作者:@keenjaan
原文:https://juejin.im/post/5a7172d9f265da3e3245cbca

js 1+'2' == '1'+'2'的更多相关文章

  1. Vue.js 和 MVVM 小细节

    MVVM 是Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对View 和 ViewModel 的双向数据绑定,这使得ViewModel 的状态改变可以自 ...

  2. js学习笔记:操作iframe

    iframe可以说是比较老得话题了,而且网上也基本上在说少用iframe,其原因大致为:堵塞页面加载.安全问题.兼容性问题.搜索引擎抓取不到等等,不过相对于这些缺点,iframe的优点更牛,跨域请求. ...

  3. js学习笔记:webpack基础入门(一)

    之前听说过webpack,今天想正式的接触一下,先跟着webpack的官方用户指南走: 在这里有: 如何安装webpack 如何使用webpack 如何使用loader 如何使用webpack的开发者 ...

  4. JS调用Android、Ios原生控件

    在上一篇博客中已经和大家聊了,关于JS与Android.Ios原生控件之间相互通信的详细代码实现,今天我们一起聊一下JS调用Android.Ios通信的相同点和不同点,以便帮助我们在进行混合式开发时, ...

  5. jquery和Js的区别和基础操作

    jqery的语法和js的语法一样,算是把js升级了一下,这两种语法可以一起使用,只不过是用jqery更加方便 一个页面想要使用jqery的话,先要引入一下jqery包,jqery包从网上下一个就可以, ...

  6. 利用snowfall.jquery.js实现爱心满屏飞

    小颖在上一篇一步一步教你用CSS画爱心中已经分享一种画爱心的方法,这次再分享一种方法用css画爱心,并利用snowfall.jquery.js实现爱心满屏飞的效果. 第一步: 利用伪元素before和 ...

  7. node.js学习(三)简单的node程序&&模块简单使用&&commonJS规范&&深入理解模块原理

    一.一个简单的node程序 1.新建一个txt文件 2.修改后缀 修改之后会弹出这个,点击"是" 3.运行test.js 源文件 使用node.js运行之后的. 如果该路径下没有该 ...

  8. JS正则表达式常用总结

    正则表达式的创建 JS正则表达式的创建有两种方式: new RegExp() 和 直接字面量. //使用RegExp对象创建 var regObj = new RegExp("(^\\s+) ...

  9. 干货分享:让你分分钟学会 JS 闭包

    闭包,是 Javascript 比较重要的一个概念,对于初学者来讲,闭包是一个特别抽象的概念,特别是ECMA规范给的定义,如果没有实战经验,很难从定义去理解它.因此,本文不会对闭包的概念进行大篇幅描述 ...

  10. JS核心系列:理解 new 的运行机制

    和其他高级语言一样 javascript 中也有 new 运算符,我们知道 new 运算符是用来实例化一个类,从而在内存中分配一个实例对象. 但在 javascript 中,万物皆对象,为什么还要通过 ...

随机推荐

  1. JavaScript高级 面向对象(11)--对象的动态特性-关联数组用法

    说明(2017.4.2): 1. 对象的动态特性: (1)在js中,一个对象需要属性,就可以利用“对象名.属性 = 值”的方式为其添加,只要赋值成功,对象就新增这个属性. (2)对象属性的访问形式: ...

  2. MySQL5.7 添加用户、删除用户与授权

    mysql -uroot -proot MySQL5.7 mysql.user表没有password字段改 authentication_string: 一. 创建用户: 命令:CREATE USER ...

  3. # Writing your-first Django-app-part 4-simple-form

    简单的表单 处理表单提交-跳转/错误信息 处理表单提交--结果显示 通用view (generic view) 设计:越少代码越好? 1.修改DemoAppPoll/urls.py 2.修改DemoA ...

  4. python 3 操作mysql数据库的方法

    参考:http://www.cnblogs.com/txw1958/archive/2012/07/22/python3-mysql.html http://www.jb51.net/article/ ...

  5. [ADC]TI am4378 ADC采样设置问题(am335x类似)

    这段时间在调试AM4378的ADC问题,发现采样到的数据和真实输入波形有所出入,比如输入是1ms的周期,50%占空比的信号,但是采样的数据描点总是偏差较大,数据如下 iio device number ...

  6. PostgreSQL Table Partitioning<转>

    原创文章,转载请务必将下面这段话置于文章开头处(保留超链接).本文转发自Jason’s Blog,原文链接 http://www.jasongj.com/2015/12/13/SQL3_partiti ...

  7. CAS (13) —— CAS 使用Maven Profile支持多环境编译

    CAS (13) -- CAS 使用Maven Profile支持多环境编译 摘要 CAS 使用Maven Profile支持多环境编译 版本 tomcat版本: tomcat-8.0.29 jdk版 ...

  8. Redis (1) —— 安装

    Redis (1) -- 安装 摘要 介绍Mac OS X安装Redis基本方法 版本 Redis版本: 2.8.24 内容 下载Redis包 地址:http://download.redis.io/ ...

  9. 十大要避免的Ext JS开发方法

    原文地址:http://www.sencha.com/blog/top-10-ext-js-development-practices-to-avoid/ 作者:Sean LanktreeSean i ...

  10. SpringMVC接受JSON参数详解及常见错误总结

    SpringMVC接受JSON参数详解及常见错误总结 SpringMVC接受JSON参数详解及常见错误总结 最近一段时间不想使用Session了,想感受一下Token这样比较安全,稳健的方式,顺便写一 ...