你不知道的JavaScript之强制类型转换

值类型转换

将值从一种类型转换为另一种类型通常称为类型转换( type casting),这是显式的情况;隐式的情况称为强制类型转换( coercion)。

JavaScript 中的强制类型转换总是返回标量基本类型值,如字符串、数字和布尔值,不会返回对象和函数。

var num = 1
String(num)  // '1'
typeof String(num)  // 'string'

隐式类型转换与显式类型转换

// 将数值类型的值转换成字符串
var a = 42
var b = a + ''  // 隐式类型转换
var b = String(a)  // 显式类型转换

这里的“显式”和“隐式”以及“明显的副作用(sideEffect)”和“隐藏的副作用”,都是相对而言的。要是你明白 a + "" 是怎么回事,它对你来说就是“显式”的。相反,如果你不知道 String(..) 可以用来做字符串强制类型转换,它对你来说可能就是“隐式”的。

抽象值操作

toString

非字符串转换为字符串

var a = 42
a.toString()  // '42'

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

除了 undefined 和 null,其他类型的值都有 toString 方法

let a = 123
a.toString()    // '123'

let b = [1, 2, 3]
b.toString()    // '1, 2, 3'

let c = {}
c.toString()    // '[object Object]'

let d = function () {}
d.toString()    // 'function () {}'

let e = true
e.toString()    // 'true'

Object.prototype.toString()

任何类型的值可以通过 call 的方式调用,返回 '[object /该值的基本类型/]',表示任何类型的值都是由对象构造的

let a = 123
Object.prototype.toString.call(a)    // '[object Number]'

let b = [1, 2, 3]
Object.prototype.toString.call(b)    // '[object Array]'

let c = {}
Object.prototype.toString.call(c)    // '[object Object]'

let d = function () {}
Object.prototype.toString.call(d)    // '[object Function]'

let e = true
Object.prototype.toString.call(e)    // '[object Boolean]'

Object.prototype.toString.call(null)    // '[object Null]'

Object.prototype.toString.call(undefined)   // '[object Undefined]'

不安全的JSON值

  • Function
const foo = function () {}
JSON.stringify(foo)   // undefined
  • RegExp
const reg = /w/
JSON.stringify(reg)   // "{}"

JSON 序列化

所有安全的 JSON 值( JSON-safe)都可以使用 JSON.stringify(..) 字符串化。安全的 JSON 值是指能够呈现为有效 JSON 格式的值。

JSON.stringify(42)  // "42"
JSON.stringify("42")  // ""42""
JSON.stringify(null)  // "null"
JSON.stringify(true)  // "true"
JSON.stringify(undefined)  // "undefined"
JSON.stringify(function() {})  // "undefined"

JSON.stringify() 规则

  • undefined、 function、 symbol( ES6+)和包含循环引用(对象之间相互引用,形成一个无限循环)的对象都不符合 JSON结构标准,支持 JSON 的语言无法处理它们。
  • JSON.stringify(..) 在对象中遇到 undefined、 function 和 symbol 时会自动将其忽略,在数组中则会返回 null(以保证单元位置不变)。
  • 如果对象中定义了 toJSON() 方法, JSON 字符串化时会首先调用该方法,然后用它的返回值来进行序列化。
  • toJSON() 方法存在于日期对象中

我们可以向 JSON.stringify(..) 传递一个可选参数 replacer,它可以是数组或者函数,用
来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除,和 toJSON() 很像。

如果 replacer 是一个数组,那么它必须是一个字符串数组,其中包含序列化要处理的对象的属性名称,除此之外其他的属性则被忽略。

var obj = {
  a: 1,
  b: 2,
  c: 3,
}
JSON.stringify(obj, ['a', 'b'])  // "{"a":"1","b":"2"}"

JSON.string 还有一个可选参数 space,用来指定输出的缩进格式。
JSON.stringify(value, ?replacer, ?space)

JSON.stringify(..) 并不是强制类型转换。

与 toString() 的渊源

  • 字符串、数字、布尔值和 null 的 JSON.stringify(..) 规则与 ToString 基本相同。
  • 如果传递给 JSON.stringify(..) 的对象中定义了 toJSON() 方法,那么该方法会在字符串化前调用,以便将对象转换为安全的 JSON 值。

toNumber

有时我们需要将非数字值当作数字来使用,比如数学运算。为此 ES5 规范在 9.3 节定义了抽象操作 ToNumber。

ToNumber 对字符串的处理基本遵循数字常量的相关规则 / 语法。
处理失败时返回 NaN(处理数字常量失败时会产生语法错误)。

对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。

var obj = {
  a: 1
}
obj.valueOf()  // {a:1}
Number(obj)  // NaN  处理失败的情况
obj.toString()  // "[object Object]"

var arr = [1, 2]
arr.valueOf()  // [1, 2]
Number(arr)  // NaN
arr.toString()  // "1,2"

为了将值转换为相应的基本类型值,抽象操作 ToPrimitive(参见 ES5 规范 9.1 节)会首先(通过内部操作 DefaultValue,参见 ES5 规范 8.12.8 节)检查该值是否有 valueOf() 方法。
如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString()的返回值(如果存在)来进行强制类型转换。
如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。

[[PrimitiveValue]]

var num = new Number(42) || Object(42)  // 两种写法效果一样
num
Number {42} => [[PrimitiveValue: 42]]
num.valueOf()  // 42
Number(num)  // 42
+ num  // 类似 Number
/**
 * 这里执行了三步
 * 1、寻找该值的 valueOf() 方法,发现有
 * 2、调用该方法,返回 42
 * 3、对该值进行强制类型转换,转换为 42
 */

从 ES5 开始,使用 Object.create(null) 创建的对象 [[Prototype]] 属性为 null,并且没有 valueOf() 和 toString() 方法,因此无法进行强制类型转换。

var nullValue = Object.create(null)
// 没有 valueOf() 和 toString() 方法
// 无法进行强制类型转换
Boolean(nullValue)  // Uncaught TypeError: Cannot convert object to primitive value
// 模拟 Number 的运作过程
var a = {
  valueOf: function () {
    return 42
  }
}
var b = {
  toString: function () {
    return '42'
  }
}
var c = {
  toString: function () {
    return {}
  }
}

Number(a)  // 42
Number(b)  // 42
Number(c)  // Uncaught TypeError: Cannot convert object to primitive value

ToBoolean

首先也是最重要的一点是, JavaScript 中有两个关键词 true 和 false,分别代表布尔类型中的真和假。我们常误以为数值 1 和 0 分别等同于 true 和 false。在有些语言中可能是这样,但在 JavaScript 中布尔值和数字是不一样的。虽然我们可以将 1 强制类型转换为 true,将 0 强制类型转换为 false,反之亦然,但它们并不是一回事。

假值(falsy value):可以强制转换为 false 的值

假值:

  • false
  • ''
  • 0
  • undefined
  • null
  • NaN

eq: 假值以外的都是真值

特殊的假值:假值对象(falsy object)

  • Boolean({}) // true
  • Boolean([]) // true
  • new Boolean(false) // true
  • new Boolean(0) // true

真值(truthy value): 除假值外的所有值

var a = []
var b = {}
var c = function () {}
Boolean(a) && Boolean(b) && Boolean(c)  // true

显示强制类型转换

显式强制类型转换是那些显而易见的类型转换,很多类型转换都属于此列。我们在编码时应尽可能地将类型转换表达清楚,以免给别人留坑。类型转换越清晰,代码可读性越高,更容易理解。

字符串与数字之间的类型转换

字符串和数字之间的转换是通过 String(..) 和 Number(..) 这两个内建函数来实现的。
它们前面没有 new 关键字,并不创建封装对象。

var a = 42
String(a)  // "a"
var b = '3.14'
Number(b)  // 3.14

String(...) 将值转换为字符串基本类型, Number(...) 将值转换为数值基本类型。

var a = 42
a.toString()  // "42"

var b = '3.14'
+b  // 3.14

a.toString() 是显式的,不过其中涉及隐式转换。
因为 toString() 对 42 这样的基本类型值不适用,所以 JavaScript 引擎会自动为 42 创建一个封装对象(参见第 3 章),然后对该对象调用 toString()。这里显式转换中含有隐式转换。
+c 是 + 运算符的一元( unary)形式(即只有一个操作数)。 + 运算符显式地将 c 转换为数字,而非数字加法运算

var a = '2'
var b = 1 + a  // '12' 字符串拼接
var c = 1 + +a  // 3 显式强制类型转换

经典:+c 是显式还是隐式,取决于你自己的理解和经验。如果你已然知道一元运算符 + 会将操作数显式强制类型转换为数字,那它就是显式的。如果不明就里的话,它就是隐式强制类型转换,让你摸不着头脑。

  • 操作符
var a = '42'
- -a  // 可以达到与 +a 同样的效果

为何用 - -a 这种略显怪异的写法,是因为 --a 有递减的意思

操作符的应用

  • 日期显示转换为数字
var date = new Date()
var timeStamp = +date  // 这里使用的是,显式强制类型转换

var timeStamp = date.getTime()  // 更为显式的写法,调用方法,更容易让人理解
var timeStamp = Date.now()  // es5 提供的更为合理的写法

想起一句话:评判一个代码的好坏,并不是看他语法用的多高级,不是看他用了多少鲜为人知的代码,而是那种能让新人一看就能懂的代码。

  • 奇特的 ~ 操作符(字位操作“非”)

它首先将值强制类型转换为 32 位数字,然后执行字位操作“非”(对每一个字位进行反转)。

~42  // -43
-(42+1)  // -43
~(-42) => -(-42+1) => 41

在 -(x+1) 中唯一能够得到 0(或者严格说是 -0)的 x 值是 -1。也就是说如果 x 为 -1 时, ~和一些数字值在一起会返回假值 0,其他情况则返回真值。

~ 与 indexOf() 配合使用
indexOf() -- 返回一个字符串在另一个字符串中的位置,如果没找到,则返回 -1
该方法在字符串中搜索指定的子字符串,如果找到就返回子字符串所在的位置(从 0 开始),否则返回 -1。

var str = 'hello world'
if (str.indexOf('o') !== -1) {  // true
  ...
}

if (str.indexOf('w') >= 0) {  // true
  ...
}

1>= 0 和 == -1 这样的写法不是很好,称为“抽象渗漏”,意思是在代码中暴露了底层的实现细节,这里是指用 -1 作为失败时的返回值,这些细节应该被屏蔽掉。

// 更简便的写法,看起来简便,但是对于不懂的人来说就是耐人寻味的代码
var str = 'hello world'
if (~str.indexOf('lo')) {   // -4 true
  ...
}

if (~str.indexOf('lolo')) {  // 0 false
  ...
}

如果 indexOf(..) 返回 -1, ~ 将其转换为假值 0,其他情况一律转换为真值。
从技术角度来说, if (~a.indexOf(..)) 仍然是对 indexOf(..) 的返回结果进行隐式强制类型转换, 0 转换为 false,其他情况转换为 true。但我觉得 ~ 更像显式强制类型转换,前提是我对它有充分的理解。

  • 字位截除
    Math.floor(x) => 找到小于 x,而且离 x 最近的整数

~~ 后面接正数的时候与 Math.floor() 相同, 负数时是找到离 x 最近的大于 x 的整数

Math.floor(4.5)  // 4
Math.floor(-4.5)  // -5

~~4.5  // 4
~~-4.5  // -4

显式解析数字字符串

解析字符串中的数字和将字符串强制类型转换为数字的返回结果都是数字。但解析和转换两者之间还是有明显的差别。

析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停止。而转换不允许出现非数字字符,否则会失败并返回 NaN。

解析和转换之间不是相互替代的关系。它们虽然类似,但各有各的用途。如果字符串右边的非数字字符不影响结果,就可以使用解析。而转换要求字符串中所有的字符都是数字,像 "42px" 这样的字符串就不行。

var a = '42'
var b = '42px'

parseInt(a)  // 解析字符串 42
Number(a)  // 显式强制类型转换 42

parseInt(b)  // 42
Number(b)  // 转换失败,返回 NaN

parseInt(s, ?radix) -- 第一个参数是要解析的字符串,第二个参数是转变之后的数字基底(2进制、10进制),默认十进制
从 ES5 开始 parseInt(..) 默认转换为十进制数,除非另外指定。如果你的代码需要在 ES5 之前的环境运行,请记得将第二个参数设置为 10。

注:parseInt(..) 先将参数强制类型转换为字符串再进行解析,这样做没有任何问题。因为传递错误的参数而得到错误的结果,并不能归咎于函数本身。

parseFloat() 解析浮点数

显式转换为布尔值

使用 Boolean(x)

Boolean(undefined)  // false
Boolean(null)  // false

Boolean({})  // true
Boolean([])  // true

一元运算符 ! 显式的将值转换为其自身布尔类型的相反的值,根据这个特性,使用 !! 显式的将值转换为对应的布尔值。

if(xxx) {} 背后的原理:

在 if(..).. 这样的布尔值上下文中,如果没有使用 Boolean(..) 和 !!,就会自动隐式地进行 ToBoolean 转换。建议使用 Boolean(..) 和 !! 来进行显式转换以便让代码更清晰易读。

JSON.stringify()

var arr = [1, function () {}, 3]
JSON.stringify(arr)  // "[1,null,3]"

显式 ToBoolean 的另外一个用处,是在 JSON 序列化过程中将值强制类型转换为 true 或false,而不是只显示 null

显式转换布尔值在三元运算符中的应用:

var a = 42
var b = a ? true : false  // true
// 更简便的写法
var b = !!a
var b = Boolean(a)

这里涉及隐式强制类型转换,因为 a 要首先被强制类型转换为布尔值才能进行条件判断。这种情况称为“显式的隐式”,有百害而无一益,我们应彻底杜绝。
所以在使用三元运算符的时候尽量使用显式强制类型转换对待判断值做显式处理,更容易让人理解(!!a Boolean(a))。

隐式强制类型转换

隐式强制类型转换指的是那些隐蔽的强制类型转换,副作用也不是很明显。换句话说,你自己觉得不够明显的强制类型转换都可以算作隐式强制类型转换。
显式强制类型转换旨在让代码更加清晰易读,而隐式强制类型转换看起来就像是它的对立面,会让代码变得晦涩难懂。

字符串与数字之间的隐式强制类型转换

  • 操作符规则
    如果两个操作数都是数字,将执行加法操作;
    如果有一个操作数是字符串(或者说能被转换成字符串),将执行字符串拼接操作;

根据 ES5 规范 11.6.1 节,如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话, + 将进行拼接操作。如果其中一个操作数是对象(包括数组),则首先对其调用 ToPrimitive 抽象操作(规范 9.1 节),该抽象操作再调用 [[DefaultValue]](规范 8.12.8节),以数字作为上下文。

var a = 42
var b = '42'

var c = [4, 2]
var d = [2]

a + 0  // 42
b + 0  // '420'

c + d  // "4,22"

对于两个数组或者一个数组和一个对象相加,会执行以下操作:

  • c.toString() // "4,2"
  • d.toString() // "2"
  • "4,2" + "2" // "4,22"
  • 数字减法运算符
    为了执行减法运算,左右两边的数都要转换成数字,它们首先被转换为字符串(通过强制类型转换toString()),然后再转换为数字。

布尔值到数字的隐式强制类型转换

隐式强制类型转换为布尔值

发生布尔值隐式强制类型转换的情况:

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