细节决定成败,聊聊JS的类型(下)

DAO组织
• 阅读 395

讲完了基本类型,我们来介绍一个现象:类型转换。

因为 JS 是弱类型语言,所以类型转换发生非常频繁,大部分我们熟悉的运算都会先进行类型转换。大部分类型转换符合人类的直觉,但是如果我们不去理解类型转换的严格定义,很容易造成一些代码中的判断失误。其中最为臭名昭著的是 JavaScript 中的“ == ”运算,因为试图实现跨类型的比较,它的规则复杂到几乎没人可以记住。这里我们当然也不打算讲解 == 的规则,它属于设计失误,并非语言中有价值的部分,很多实践中推荐禁止使用“ ==”,而要求程序员进行显式地类型转换后,用 === 比较。其它运算,如加减乘除大于小于,也都会涉及类型转换。

幸好的是,实际上大部分类型转换规则是非常简单的,如下表所示:
细节决定成败,聊聊JS的类型(下)

在这个里面,较为复杂的部分是 Number 和 String 之间的转换,以及对象跟基本类型之间的转换。我们分别来看一看这几种转换的规则。

StringToNumber

字符串到数字的类型转换,存在一个语法结构,类型转换支持十进制、二进制、八进制和十六进制,比如:

  • 30;
  • 0b111;
  • 0o13;
  • 0xFF。

此外,JavaScript 支持的字符串语法还包括正负号科学计数法,可以使用大写或者小写的 e 来表示:

  • 1e3;
  • -1e-2。

需要注意的是,parseInt 和 parseFloat 并不使用这个转换,所以支持的语法跟这里不尽相同。

在不传入第二个参数的情况下,parseInt 只支持 16 进制前缀“0x”,而且会忽略非数字字符,也不支持科学计数法。

在一些古老的浏览器环境中,parseInt 还支持 0 开头的数字作为 8 进制前缀,这是很多错误的来源。所以在任何环境下,都建议传入 parseInt 的第二个参数,而 parseFloat 则直接把原字符串作为十进制来解析,它不会引入任何的其他进制。

多数情况下,Number 是比 parseInt 和 parseFloat 更好的选择。

NumberToString

在较小的范围内,数字到字符串的转换是完全符合你直觉的十进制表示。当 Number 绝对值较大或者较小时,字符串表示则是使用科学计数法表示的。这个算法细节繁多,我们从感性的角度认识,它其实就是保证了产生的字符串不会过长。

具体的算法,你可以去参考 JavaScript 的语言标准。由于这个部分内容,我觉得在日常开发中很少用到,所以这里我就不去详细地讲解了。
装箱转换
每一种基本类型 Number、String、Boolean、Symbol 在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。

前文提到,全局的 Symbol 函数无法使用 new 来调用,但我们仍可以利用装箱机制来得到一个 Symbol 对象,我们可以利用一个函数的 call 方法来强迫产生装箱。

我们定义一个函数,函数里面只有 return this,然后我们调用函数的 call 方法到一个 Symbol 类型的值上,这样就会产生一个 symbolObject。

我们可以用 console.log 看一下这个东西的 type of,它的值是 object,我们使用 symbolObject instanceof 可以看到,它是 Symbol 这个类的实例,我们找它的 constructor 也是等于 Symbol 的,所以我们无论从哪个角度看,它都是 Symbol 装箱过的对象:

var symbolObject = (function(){ return this; }).call(Symbol("a"));
console.log(typeof symbolObject); //object
console.log(symbolObject instanceof Symbol); //true
console.log(symbolObject.constructor == Symbol); //true

装箱机制会频繁产生临时对象,在一些对性能要求较高的场景下,我们应该尽量避免对基本类型做装箱转换。

使用内置的 Object 函数,我们可以在 JavaScript 代码中显式调用装箱能力。

var symbolObject = Object(Symbol("a"));
console.log(typeof symbolObject); //object
console.log(symbolObject instanceof Symbol); //true
console.log(symbolObject.constructor == Symbol); //true

每一类装箱对象皆有私有的 Class 属性,这些属性可以用 Object.prototype.toString 获取:

var symbolObject = Object(Symbol("a"));
console.log(Object.prototype.toString.call(symbolObject)); //[object Symbol]

在 JavaScript 中,没有任何方法可以更改私有的 Class 属性,因此 Object.prototype.toString 是可以准确识别对象对应的基本类型的方法,它比 instanceof 更加准确。

但需要注意的是,call 本身会产生装箱操作,所以需要配合 typeof 来区分基本类型还是对象类型。

拆箱转换

在 JavaScript 标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换(即,拆箱转换)。

对象到 String 和 Number 的转换都遵循“先拆箱再转换”的规则。通过拆箱转换,把对象变成基本类型,再从基本类型转换为对应的 String 或者 Number。

拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError。

var o = {
    valueOf : () => {console.log("valueOf"); return {}},
    toString : () => {console.log("toString"); return {}}
}
o * 2
// valueOf
// toString
// TypeError

我们定义了一个对象 o,o 有 valueOf 和 toString 两个方法,这两个方法都返回一个对象,然后我们进行 o*2 这个运算的时候,你会看见先执行了 valueOf,接下来是 toString,最后抛出了一个 TypeError,这就说明了这个拆箱转换失败了。

到 String 的拆箱转换会优先调用 toString。我们把刚才的运算从 o*2 换成 String(o),那么你会看到调用顺序就变了。

var o = {
    valueOf : () => {console.log("valueOf"); return {}},
    toString : () => {console.log("toString"); return {}}
}
String(o)
// toString
// valueOf
// TypeError

在 ES6 之后,还允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为。

var o = {
    valueOf : () => {console.log("valueOf"); return {}},
    toString : () => {console.log("toString"); return {}}
}
o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}
console.log(o + "")
// toPrimitive
// hello

结语

在本篇文章中,我们介绍了 JavaScript 运行时的类型系统。这里回顾一下今天讲解的知识点。
除了这七种语言类型,还有一些语言的实现者更关心的规范类型。

  • List 和 Record: 用于描述函数传参过程。
  • Set:主要用于解释字符集等。
  • Completion Record:用于描述异常、跳出等语句执行过程。
  • Reference:用于描述对象属性访问、delete 等。
  • Property Descriptor:用于描述对象的属性。
  • Lexical Environment 和 Environment Record:用于描述变量和作用域。
  • Data Block:用于描述二进制数据。

有一个说法是:程序 = 算法 + 数据结构,运行时类型包含了所有 JavaScript 执行时所需要的数据结构的定义,所以我们要对它格外重视。

最后我们留一个实践问题,如果我们不用原生的 Number 和 parseInt,用 JavaScript 代码实现 String 到 Number 的转换,该怎么做呢?请你把自己的代码留言给我吧!

更多补充内容请看:开发者网站

点赞
收藏
评论区
推荐文章
凯特林 凯特林
4年前
如何避免JavaScript类型转换
你是否经历过JavaScript中的某些值比较没有得到预期结果的情况?看下面的情况:即使0结果为真,if条件也没有根据结果执行。有没有想过为什么会这样?本文主要说明这些值比较的工作原理以及影响它们的因素。在深入解释之前,大家要熟悉一个概念:类型转换。什么是JavaScript类型转换?这也称为类型强制。对于不熟悉此概念的人来说,它只是将值从一种
Wesley13 Wesley13
3年前
java 类型转换的原理
最近在看JDK的源码,在看源码的时候看到了0xff这么个东东,从这里引出了类型转换。因此在此记录下。在写原理之前先看几个例子。byteb1;intab;然后打印a得出的结果是1.intb1;bytea(byte)b;打印a得出来的是1。inta255;byteb(byte)255;打印b得出的结果也是1;而把这个强制转出
Jacquelyn38 Jacquelyn38
4年前
面试官:JavaScript的数据类型你了解多少?
前言作为JavaScript的入门知识点,Js数据类型在整个JavaScript的学习过程中其实尤为重要。最常见的是边界数据类型条件判断问题。我们将通过这几个方面来了解数据类型:概念检测方法转换方法概念undefined、Null、Boolean、String、Number、Symbol、BigInt为基础类型;Ob
Easter79 Easter79
3年前
struts2基本类型转换出错
Struts2中自带一些基本类型的转换,所以不用我们自定了,但是有时候还是会碰到莫名其妙的基本类型转换问题,比如Caused by: java.lang.NoSuchMethodException: cn.smart3.mdu2.config.entity.MibMap.setModbusPort(Ljava.lang.String;)
Stella981 Stella981
3年前
BigDecimal与Long、int之间的相互转换
在实际开发过程中BigDecimal是一个经常用到的数据类型,它和int、Long之间可以相互转换。转换关系如下代码展示:一、int转换成BigDecimal数据类型//int转换成bigDecimal类型publicstaticBigDecimalintToBigDecim
Stella981 Stella981
3年前
Python将字符串转换成ObjectId类型
MongoDB自动生成的_id是ObjectId类型的。我需要将MongoDB的_id存到ElasticSearch中,而ElasticSearch又只能存String类型的_id,所以就涉及到两种类型的转换。ObjectId类型—→String类型这个非常简单
Wesley13 Wesley13
3年前
C++类型转换
隐式转换在赋值给一个兼容类型会出现隐式类型转换.比如下面这个例子.shorta2000;intb;ba;在以上例子中.值从short自动提升到int,这是标准转换。标准转换影响基本数据类型,它在类型数字类型之间(short to int, int to float, double t
Stella981 Stella981
3年前
ECMAScript——JavaScript的核心
   JavaScript(简称:JS)是一种动态类型、弱类型的直译式脚本语言。也就是说它的数据类型不需要声明,不同类型之间会隐式转换为被赋值的类型。它不需要编译,直接由浏览器解释执行。JavaScript由ECMAScript(简称:ES)、DOM、BOM三大部分组成:ECMAScript规定了语言的语法和基本对象;DOM(文本对象模型)处理网页的节
Stella981 Stella981
3年前
Js——第一课
数据类型:要特别注意相等运算符。JavaScript在设计时,有两种比较运算符:第一种是比较,它会自动转换数据类型再比较,很多时候,会得到非常诡异的结果;第二种是比较,它不会自动转换数据类型,如果数据类型不一致,返回false,如果一致,再比较。由于JavaScript这个设计缺陷,_不要_使用比较,始终
Wesley13 Wesley13
3年前
C++重载双目运算符(2)(对象与数之间)
有两种方法:(1)采用重载双目运算符方式(2)1.类型转换函数(将类的对象转换为其他数据类型)2.转换构造函数(将其他类型转换为类的对象)(仍然要配合重载双目运算符的方式,因为最终实现的是类的两个对象相加)(注意:类型转换函数和转换构造函数不可同时使用,会出现二
小万哥 小万哥
1年前
C 语言:类型转换与常量的细致理解
C语言中的类型转换有时,您必须将一种数据类型的值转换为另一种类型。这称为类型转换隐式转换当您将一种类型的值分配给另一种类型的变量时,编译器会自动进行隐式转换。例如,如果您将一个int值分配给一个float类型:c//自动转换:inttofloatfloat