深入理解JavaScript中的数据类型转换

目录

  1. 概述
  2. 抽象值操作(Abstract Operations)
    • ToString
    • ToNumber
    • ToBoolean
  3. 显示强制类型转换
    • toString()
    • 一元运算符(+ -)
    • parseInt()和parseFloat()
    • !!
  4. 隐式强制类型转换
    • 二元运算符(+)
    • 隐式转换为布尔值
  5. 转换为Oject
  6. 总结

概述

开篇先来一段神代码:

(!(~+[])+{})[--[~+""][+[]]*[~+[]]+~~!+[]]+({}+[])[[~!+[]*~+[]]] = sb

这里面牵扯的东西很多,容我一一道来。

一般来说,类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型语言的运行时。然而在JavaScript中通常将它们统称为强制类型转换。
在学习过程中,我认为更确切的是“显式强制类型转换”和“隐式强制类型转换”。而关于显隐的界限,我更倾向于《你不知道的JavaScript》中的说法,即它们是相对的,取决于你的理解,比如如果你明白a + ""的原理,那么它对于你来说就是显式的。

Js中有7种数据类型,本文主要涉及到的是转化为Number,String,Boolean的规则和方法,顺带提一下转化为Object。在这之前,先了解下类型转换的底层原理。

抽象值操作(Abstract Operations)

ToPrimitive(input [, PreferredType]) ,由7.1.1定义可知,它是包含两个参数的抽象操作,一个是 input ,一个是可选参数 PreferredType。该操作就是把参数input转化为原始数据类型。如果input可以同时转化为多个原始数据,那么会优先参考PreferredType的值。

转换的原则:

  1. input为原始数据类型:返回input自身
  2. input为对象:在没有改写或自定义toPrimitive方法的条件下,默认为Number(Date默认String)。PreferredType是String,则先调用toString(),结果不是原始值的话再调用valueOf(),还不是原始值的话则抛出TypeError错误;PreferredType是Number,则先调用valueOf()再调用toString()。

1. ToString

ToString负责处理非字符串到字符串的强制类型转换。规则如下图所示:

number的转换规则参考7.1.12.1

对普通对象来说,内部调用ToPrimitive方法实现,除非自行定义,否则用Object.prototype.toString()返回内部属性[[Class]]的值,如"[object Object]"

数组的默认toString()方法经过了重新定义,将所有单元字符串化以后再用","连接起来:

var a = [1, 2, 3]
a.toString()      // "1,2,3"

JSON.stringify()在将JSON对象序列化为字符串时也用到了ToString,但它并不是严格意义上的强制类型转换。

2. ToNumber

ToNumber规则如下图所示:

string的转换规则参考7.1.3.1

object的转换,简单的规则是,Number方法的参数是对象时,将返回NaN,除非是包含单个数值的数组。

Number({}) // NaN
Number({a: 1}) // NaN
Number([1, 2, 3]) // NaN
Number([5]) // 5
Number([])  // 0

默认情况下,对象的valueOf方法返回对象本身,所以一般总是会调用toString方法,而toString方法返回对象的类型字符串(比如[object Object]),而根据字符串的解析规则,返回NaN。

3. ToBoolean

ToBoolean的转换规则如下:

有规则可见,所有的对象都返回true,包括[]{}
但这也涉及到一个问题,即假值对象(falsy object)。例如:

var a = new Boolean(false)
var b = new Number(0)
var c = new String('')

Boolean(a && b && c)   // true  注意此处必须用Boolean来封装

可见,falsy值在被封装为对象后,全都返回了true。

显示强制类型转换

显示强制类型转换是指代码有明确意图的转换,是显而易见的。

显示转换主要指使用Number()String()Boolean()三个函数,手动将各种类型的值,分别转换成数字、字符串或者布尔值。注意,它们前面并没有new,并不会封装为对象。
而上述的三个函数的实现原理,就是使用抽象操作,分别调用ToNumber()ToString()ToBoolean()来实现的。

除了Number()String()Boolean()三个函数外,以下的方法也是显示转换:

toString()

toString()方法能显式地将非字符串类型转换为字符串。一个原始类型能调用toString()方法,是因为在这个过程中,原始类型会自动装箱(boxing),被封装成一个对象,从而有调用其包装对象函数的能力,但是原来那个变量的值不会有任何变化。

(23).toString()      // "23"
(function f(){}).toString()    // "function f(){}"

一元运算符(+ -)

+和 - 0 能显式地将非数字转换为数字。

+'3.14'    // 3.14
+-0    // -0
+[]  // 0
{} - 0    // NaN

一元运算符里还有像~这样的位操作符,也能起到类型转换的作用,这里先不涉及。

parseInt()和parseFloat()

这两个方法是解析字符串中的数字,和将字符串强制类型转换为数字还是有明显区别的。
解析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停止;而转换不允许出现非数字字符,否则会失败并返回NaN。

var a = '42px'
Number(a)    // NaN
parseInt(a)    // 42

非字符串参数会被隐式转换为字符串,会有各种bug,强烈不建议这么做。传参前请进行类型检查。另外,parseInt一般会指定转换的基数,即使几进制。

详见我的另一篇文章

!!

这是显式强制转换为布尔值最常用的方法。例如:

!!''  // false
!!{}  // true

var a
!!a  // false

隐式强制类型转换

隐式强制类型转换是指那些隐蔽的,代码在语义上没有明显表明要转换的类型转换。这也是相对的,比如当你看不懂+[]的意思时,它对你来说就是隐式的。
隐式转换最大的好处是代码简洁。

二元运算符(+)

通过重载,+运算符既能用于数字加法,也能用于字符串拼接。通常的理解是,只要+的一侧有字符串,那么+执行的就是字符串拼接。但是两个数组也是能通过+拼接的,比如:

[1, 2, 3] + [4, 5]    // "1,2,34,5"

根据ES5的规范,如果其中一个操作数是对象,则首先对其调用ToPrimitive抽象操作,该抽象操作再调用[[DefaultValue]],以数字作为上下文。这和ToNumber的处理对象的方式一致。
就上例而言,因为数组的valueOf()操作无法得到简单的基本类型值,于是它转而调用toString(),因此呈现出来就是字符串拼接。
而因为+需要调用valueOf()方法,当一个对象中的valueOf()方法被重写的时候,返回值和直接使用String()来转换是不一样的。例如:

var a = {
  valueOf: function () { return 100 }
  toString: function () { return 1 }
}

a + ''    // '100'
String(a)    // '1'

总之,如果+其中一个操作数是字符串(或者能转换成字符串),则执行拼接,否则执行数字加法。

除了加法运算符(+)有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值。

'5' - '2' // 3
'5' * '2' // 10
false - 1 // -1
'1' - 1   // 0
'5' * []    // 0
false / '5' // 0
'abc' - 1   // NaN
null + 1 // 1
undefined + 1 // NaN

这里有一个坑,常常被提到,就是

[] + {}      // "[object Object]"
{} + []      // 0

它们的结果不一样,因为在第一行代码中,{}被当做一个空对象,转换为了"[object Object]",然后进行字符串拼接;在第二行代码中,{}被当做一个独立的代码块,已经表示了一段代码的结束,后面的+ []被转换为数字0。

更多隐式转换的坑参考:Js面试题大坑——隐式类型转换

隐式转换为布尔值

在以下这些情况中,常常会发生隐式强制类型转换:

  1. if(...)语句中的条件判断表达式
  2. for( .. ; .. ; .. ) 语句中的条件判断表达式
  3. while(..)do..while(..)循环中的条件判断表达式
  4. ? :中的条件判断表达式
  5. 逻辑运算符||&&左边的操作数(作为条件判断表达式)

以上的隐式转换都遵循ToBoolean抽象操作规则。
另外要注意,逻辑运算符||&&的返回值是两个操作数中的一个,并不是true或者false

转换为Object

这个一般出现在面试题中,核心思想是从内存图的角度去考虑。
如下的题目:

var a = {n:1}
var b = a 
a.x = a = {n:2}

alert(a.x)返回什么?
alert(b.x)返回什么?

思路:

  1. 当浏览器读到a.x = a = {n:2}时,会先从左至右确定a在栈内存中的地址值。

可以说,此时整个式子中的a的值都简写为是100。

  1. 然后从右至左进行赋值。a = {n:2}的意思是a中的地址已经指向了堆内存中的另一个对象,假定它的地址是101,那么a作为该地址值得容器,已经变成了101。但式子最左边的a.xa的值是提前确定的,仍然是100。

  2. 然后是a.x = a,将右边的a的值赋值给左边a的属性x,而因为左边a的值是100,它所指向的对象里没有x,所以现生成x

  3. alert(a)中的a此时已经是101了,而101中并没有属性x,所以返回undefinedb.x指向的是一个对象,alert会调用toString()方法,将对象字符串化返回,即[object Object]

会出现以上原因,是因为对于简单类型的数据来说,赋值就是深拷贝。
对于复杂类型的数据(对象)来说,要区分浅拷贝和深拷贝。当指向同一个对象时,浅拷贝的变量之间会互相影响,深拷贝才能做到相互独立。 深拷贝如何实现会在以后的文章里再探讨。

总结

本文从底层的抽象操作入手,分析了各种数据类型转换的原理。然后分为显示和隐式两个角度看如何转换为number、string和boolean类型。总结转换方法如下:

  1. 转为string:
    • String(x)
    • x.toString()
    • x + ''
  2. 转为number:
    • Number(x)
    • parseInt(x, n) n为几进制
    • parseFloat(x)
    • +x
    • x - 0
  3. 转为boolean:
    • Boolean(x)
    • !!x
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,106评论 6 542
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,441评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,211评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,736评论 1 317
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,475评论 6 412
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,834评论 1 328
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,829评论 3 446
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,009评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,559评论 1 335
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,306评论 3 358
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,516评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,038评论 5 363
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,728评论 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,132评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,443评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,249评论 3 399
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,484评论 2 379