彻底理解js中的数据类型与类型转换

从类型说起

js只有7种类型:

  • 原始类型(primitives types)
    • boolean
    • number
      • 包括Infinity和NaN,你可以通过typeof Infinity;来验证
    • string
    • null
    • undefined
    • Symbol (ECMAScript 6 新定义,暂时用不上,这篇文章不讨论)
  • Object 类型
    • js内置了很多对象供你使用,MDN文档将它们全部列举了出来(当然,我们经常使用的只是其中的一部分)。

注意,上面这个MDN链接中给出的“值属性”那一栏中的值并不是对象。

原始类型的值是不会改变的。你可以给变量赋予不同的原始值,只不过是让变量指向了内存中的另外一个原始值,但是原本的那个原始值在内存中并没有变化。

Object类型就不一样,通过obj.k = '另一个值',在内存中存储obj的一些字节确确实实地被改变了。

有关原始类型和对象类型在变量中是如何存储的,可以看我之前写的一篇文章

js的自动装箱

虽然string是原始类型,但为什么我们好像可以调用“string的函数”呢?原始类型不应该有函数啊!

var str = 'I am str';
str.toUpperCase();  // "I AM STR"

原因是js标准库给boolean、number、string分别提供了一个包装对象:Boolean
Number
String
。在需要的时候,原始类型会自动转换成相应的包装对象(这个过程叫自动装箱)。上例的toUpperCase就是String标准对象定义的一个函数。

自动装箱就是临时创建一个包装对象,将原始类型的值封装起来,以便调用包装对象的函数。但是原来那个变量的值不会有任何变化!执行完上面例子的代码之后,str指向的依然是那个原始值:

typeof str;  // "string"

当然,你可以将Boolean 、Number 、String 这三个函数当作构造函数来使用通过手动new一个包装类来装箱

var str_object = new String('I am str_object');  //  手动装箱
str_object.toUpperCase();  //  "I AM STR_OBJECT"
typeof str_object;  //  "object"

在文章的后面,我们还会将这三个函数当作普通的函数使用,实现强制类型转换。

两个与类型转换有关的函数:valueOf()和toString()

  • valueOf()的意义是,返回这个对象逻辑上对应的原始类型的值。比如说,String包装对象的valueOf(),应该返回这个对象所包装的字符串。
  • toString()的意义是,返回这个对象的字符串表示。用一个字符串来描述这个对象的内容。

valueOf()和toString()是定义在Object.prototype上的方法,也就是说,所有的对象都会继承到这两个方法。但是在Object.prototype上定义的这两个方法往往不能满足我们的需求(Object.prototype.valueOf()仅仅返回对象本身),因此js的许多内置对象都重写了这两个函数,以实现更适合自身的功能需要(比如说,String.prototype.valueOf就覆盖了在Object.prototype中定义的valueOf)。当我们自定义对象的时候,最好也重写这个方法

以下是部分内置对象调用valueOf()的行为:

对象 返回值
Array 数组本身(对象类型)。
Boolean 布尔值(原始类型)。
Date 从 UTC 1970 年 1 月 1 日午夜开始计算,到所封装的日期所经过的毫秒数(原始类型)。
Function 函数本身(对象类型)。
Number 数字值(原始类型)。
Object 对象本身(对象类型)。如果自定义对象没有重写valueOf方法,就会使用它。
String 字符串值(原始类型)。

由上表可见,valueOf()虽然期望返回原始类型的值,但是实际上有一些对象在逻辑上无法找到与之对应的原始值,因此只能返回对象本身。

toString()则不一样,因为不管什么对象,我们总有办法“描述”它,因此js内置对象的toString()总能返回一个原始string类型的值。

var d = new Date();
d.toString()
// "Fri Apr 21 2017 14:54:04 GMT+0800 (中国标准时间)"

我们自己在重写toString()的时候也应该返回合理的string。

valueOf()和toString()经常会在类型转换的时候被js内部调用,比如说我们后文会谈到的ToPrimitive。在自定义对象上合理地覆盖valueOf()和toString(),可以控制自定义对象的类型转换。

js内部用于实现类型转换的4个函数

这4个方法实际上是ECMAScript定义的4个抽象的操作,它们在js内部使用,进行类型转换。我们js的使用者不能直接调用这些函数,但是了解这些函数有利于我们理解js类型转换的原理。

  • ToPrimitive ( input [ , PreferredType ] )
  • ToBoolean ( argument )
  • ToNumber ( argument )
  • ToString ( argument )

请区分这里的ToString()和上文谈到的toString(),一个是js引擎内部使用的函数,另一个是定义在对象上的函数。

ToPrimitive ( input [ , PreferredType ] )

将input转化成一个原始类型的值。PreferredType参数要么不传入,要么是Number 或 String。如果PreferredType参数是Number,ToPrimitive这样执行:

  1. 如果input本身就是原始类型,直接返回input。
  2. 调用input.valueOf(),如果结果是原始类型,则返回这个结果。
  3. 调用input.toString(),如果结果是原始类型,则返回这个结果。
  4. 抛出TypeError异常。

以下是PreferredType不为Number时的执行顺序。

  • 如果PreferredType参数是String,则交换上面这个过程的第2和第3步的顺序,其他执行过程相同。
  • 如果PreferredType参数没有传入
    • 如果input是内置的Date类型,PreferredType 视为String
    • 否则PreferredType 视为 Number

可以看出,ToPrimitive依赖于valueOf和toString的实现。

ToBoolean ( argument )

Argument Type Result
Undefined Return false
Null Return false
Boolean Return argument
Number 仅当argument为 +0, -0, or NaN时, return false; 否则一律 return true
String 仅当argument是空字符串(长度为0)时, return false; 否则一律 return true
Symbol Return true
Object Return true

这些规定都来自ECMA的标准,js内部就是这样实现的。
只需要记忆几种返回false的情况就可以了,其他一律返回true。

ToNumber ( argument )

Argument Type Result
Undefined Return NaN
Null Return +0
Boolean 如果 argument 为 true, return 1. 如果 argument 为 false, return +0
Number 直接返回argument
String 将字符串中的内容转化为数字(比如"23"->23),如果转化失败则返回NaN(比如"23a"->NaN)
Symbol 抛出 TypeError 异常
Object primValue = ToPrimitive(argument, Number),再对primValue 使用 ToNumber(primValue)

由上表可见ToNumber的转化并不总是成功,有时会转化成NaN,有时则直接抛出异常。

ToString ( argument )

Argument Type Result
Undefined Return "undefined"
Null Return "null"
Boolean 如果 argument 为 true, return "true".如果 argument 为 false, return "false"
Number 用字符串来表示这个数字
String 直接返回 argument
Symbol 抛出 TypeError 异常
Object 先primValue = ToPrimitive(argument, hint String),再对primValue使用ToString(primValue)

隐式类型转换(自动类型转换)

当js期望得到某种类型的值,而实际在那里的值是其他的类型,就会发生隐式类型转换。系统内部会自动调用我们前面说ToBoolean ( argument )、ToNumber ( argument )、ToString ( argument ),尝试转换成期望的数据类型。

例子1:

if ( !undefined
  && !null
  && !0
  && !NaN
  && !''
) {
  console.log('true');
} // true

例子1:因为在if的括号中,js期望得到boolean的值,所以对括号中每一个值都使用ToBoolean ( argument ),将它们转化成boolean。

例子2:

3 * { valueOf: function () { return 5 } };  //15

例子2:因为在乘号的两端,js期望得到number类型的值,所以对右边的那个对象使用ToNumber ( argument ),得到结果5,再与乘号左边的3相乘。

例子3:

> function returnObject() { return {} }
> 3 * { valueOf: function () { return {} }, toString: function () { return {} } }
// TypeError: Cannot convert object to primitive value

例子3:调用ToNumber ( argument )的过程中,调用了ToPrimitive ( input , Number ),因为在ToPrimitive中valueOf和toString都没有返回原始类型,所以抛出异常。

符号'+'是一个比较棘手的一个符号,因为它既可以表示“算数加法”,也可以表示“字符串拼接”。
简单理解版本:只要'+'两端的任意一个操作数是字符串,那么这个'+'就表示字符串拼接,否则表示算数加法。

12+3
// 15
12+'3'
// "123"

原理理解版本:根据ECMAScript的定义,对'+'运算的求值按照以下过程:

  1. 令lval = 符号左边的值,rval = 符号右边的值
  2. 令lprim = ToPrimitive(lval),rprim = ToPrimitive(rval)
    • 如果lprim和rprim中有任意一个为string类型,将ToString(lprim)和ToString(rprim)的结果做字符串拼接
  • 否则,将ToNumber(lprim)和ToNumber(rprim)的结果做算数加法

根据这个原理可以解释

[]+[]
//  ""
// 提示:ToPrimitive([])返回空字符串

[] + {}
//  "[object Object]"
//  提示:ToPrimitive({})返回"[object Object]"

123 + { toString: function () { return "def" } }
//  "123def"
//  提示:ToPrimitive(加号右边的对象)返回"def"

{} + []
//  0
// 结果不符合我们的预期:"[object Object]"
// 提示:在Chrome中,符号左边的{}被解释成了一个语句块,而不是一个对象
// 注意在别的执行引擎上可能会将{}解释成对象
//  这一行等价于'+[]'
// '+anyValue'等价于Number(anyValue)

({}) + []
//  "[object Object]"
// 加上括号以后,{}被解释成了一个对象,结果符合我们的预期了

'<'、'>'的情况与'+'类似,但是处理方式与'+'有些不同。如果好奇请自行查阅文档

显式类型转换(强制类型转换)

程序员显式调用Boolean(value)、Number(value)、String(value)完成的类型转换,叫做显示类型转换。
我们在文章的前面说过new Boolean(value)、new Number(value)、new String(value)传入各自对应的原始类型的值,可以实现“装箱”——将原始类型封装成一个对象。其实这三个函数不仅仅可以当作构造函数,它们可以直接当作普通的函数来使用,将任何类型的参数转化成原始类型的值:

Boolean('sdfsd');  //  true
Number("23");  //  23
String({a:24});  //  "[object Object]"

其实这三个函数用于类型转换的时候,调用的就是js内部的ToBoolean ( argument )、ToNumber ( argument )、ToString ( argument )方法!

这里解释一下String({a:24}); // "[object Object]"的过程:

  • 执行String({a:24})
    • 执行js内部函数ToString ( {a:24} )
      • 执行primValue = ToPrimitive({a:24}, hint String)
        1. 因为{a:24}不是原始类型,进入下一步。
        2. 在ToPrimitive内调用({a:24}).toString(),返回了原始值"[object Object]",因此直接返回这个字符串,ToPrimitive后面的步骤不用进行下去了。
      • primValue被赋值为ToPrimitive的返回值:"[object Object]"
      • 执行js内部函数ToString ( "[object Object]" ),返回"[object Object]"
      • 返回"[object Object]"
    • 返回"[object Object]"
  • 返回"[object Object]"

为了防止出现意料之外的结果,最好在不确定的地方使用显式类型转换


参考文章:
ECMAScript类型转换规范
What is {} + {} in JavaScript?
JavaScript quirk 1: implicit conversion of values
阮一峰的js教程
Object.prototype.toString()的原理
改变Object.prototype.toString.call(myClass)的输出

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

推荐阅读更多精彩内容

  • 强制转换 强制转换主要指使用Number、String和Boolean三个构造函数,手动将各种类型的值,转换成数字...
    灯火阑珊Zone阅读 390评论 0 3
  • 官方中文版原文链接 感谢社区中各位的大力支持,译者再次奉上一点点福利:阿里云产品券,享受所有官网优惠,并抽取幸运大...
    HetfieldJoe阅读 2,499评论 1 17
  • 9.正则表达式 首先,js定义了RegExp()构造函数,用来创建表示文本匹配模式的对象。这就是正则表达式。正则表...
    我就是z阅读 633评论 0 5
  • 丁酉仲秋,宴八荒四海客,席上有客闻铃,铃引客去,归途悄隐,客惊,铃现,铃持于一觋,骤止铃,漫光散,客行,失而坠,见...
    辕凤阅读 495评论 0 0
  • 迅速洗漱完毕,走到二楼,突然想要天气预报说今天有中雨,探头看楼下没有雨,就继续下楼。还是
    富思竭虑阅读 163评论 0 0