关于 JavaScript 数据类型,有哪些你不知道的细节?(下)

简介: 大家好,今天我们再来看点基础知识,看看JavaScript数据类型中的一些细节。数据类型是计算机语言的基础知识,数据类型广泛用于变量、函数参数、表达式、函数返回值等场合。JavaScript 规定了八种数据类型:未定义(Undefined)、空(Null)、数字(Number)、字符串(String)、布尔值(Boolean)、符号(Symbol)、任意大整数(BigInt)、对象(Object)。

4. String


String 用于表示字符串,String 有最大长度是 253 - 1,这个所谓的最大长度并不是指字符数,而是字符串的 UTF16 编码长度。 字符串的 charAt、charCodeAt、length 等方法针对的都是 UTF16 编码。所以,字符串的最大长度,实际上是受字符串的编码长度影响的。


JavaScript 中的字符串是永远无法变更的,一旦构造出来,就无法用任何方式改变其内容,所以字符串具有值类型的特征。

字符串的关键就在于它常用的那些方法了,这里不再多介绍。


5. Number


Number 类型表示数字。JavaScript 中的 Number 类型有 18437736874454810627(即 264-253+3) 个值。JavaScript 中的 Number 类型基本符合 IEEE 754-2008 规定的双精度浮点数规则,但是 JavaScript 为了表达几个额外的语言场景(比如为了不让除以 0 出错,而引入了无穷大的概念),规定了几个例外情况:


  • NaN,占用了 9007199254740990,这原本是符合 IEEE 规则的数字,通常在计算失败时会得到该值。要判断一个变量是否为 NaN,则可以通过 Number.isNaN 函数进行判断。
  • Infinity,无穷大,在某些场景下比较有用,比如通过数值来表示权重或者优先级,Infinity 可以表示最高优先级或最大权重。
  • -Infinity,无穷小。

注意,JavaScript 中有 +0-0 的概念,在加法类运算中它们没有区别,但是除法时需要特别注意。可以使用 1/x 是 Infinity 还是 -Infinity来区分 +0 和 -0。


根据双精度浮点数的定义,Number 类型中有效的整数范围是 -0x1fffffffffffff0x1fffffffffffff,所以 Number 无法精确表示此范围外的整数。根据浮点数的定义,非整数的 Number 类型无法用 == 或者 === 来比较,这也就是在 JavaScript 中为什么 0.1+0.2 !== 0.3


出现这种情况的原因在于计算的时候,JavaScript 引擎会先将十进制数转换为二进制,然后进行加法运算,再将所得结果转换为十进制。在进制转换过程中如果小数位是无限的,就会出现误差。


实际上,这里错误的不是结果,而是比较的方法,正确的比较方法是使用 JavaScript 提供的最小精度值,检查等式左右两边差的绝对值是否小于最小精度:

console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);  // true
复制代码


6. Symbol


Symbol 是 ES6 中引入的新数据类型,它表示一个唯一的常量,通过 Symbol 函数来创建对应的数据类型,创建时可以添加变量描述,该变量描述在传入时会被强行转换成字符串进行存储:

var a = Symbol('1')
var b = Symbol(1)
a.description === b.description // true
var c = Symbol({id: 1})
c.description // [object Object]
var d = Symbol('1')
d == a // false
复制代码


基于以上特性,Symbol 属性类型比较适合用于两类场景中:常量值和对象属性


(1)避免常量值重复

getValue 函数会根据传入字符串参数 key 执行对应代码逻辑:

function getValue(key) {
  switch(key){
    case 'A':
      ...
    case 'B':
      ...
  }
}
getValue('B');
复制代码


这段代码对调用者而言非常不友好,因为代码中使用了魔术字符串(Magic string,指的是在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值),导致调用 getValue 函数时需要查看函数代码才能找到参数 key 的可选值。所以可以将参数 key 的值以常量的方式声明:

const KEY = {
  alibaba: 'A',
  baidu: 'B',
}
function getValue(key) {
  switch(key){
    case KEY.alibaba:
      ...
    case KEY.baidu:
      ...
  }
}
getValue(KEY.baidu);
复制代码


但这样也并非完美,假设现在要在 KEY 常量中加入一个 key,根据对应的规则,很有可能会出现值重复的情况:

const KEY = {
  alibaba: 'A',
  baidu: 'B',
  tencent: 'B'
}
复制代码


这就会出现问题:

getValue(KEY.baidu) // 等同于 getValue(KEY.tencent)
复制代码


所以在这种场景下更适合使用 Symbol,不需要关心值本身,只关心值的唯一性:

const KEY = {
  alibaba: Symbol(),
  baidu: Symbol(),
  tencent: Symbol()
}
复制代码


(2)避免对象属性覆盖

函数 fn 需要对传入的对象参数添加一个临时属性 user,但可能该对象参数中已经有这个属性了,如果直接赋值就会覆盖之前的值。此时就可以使用 Symbol 来避免这个问题。创建一个 Symbol 数据类型的变量,然后将该变量作为对象参数的属性进行赋值和读取,这样就能避免覆盖的情况:


function fn(o) { // {user: {id: xx, name: yy}}
  const s = Symbol()
  o[s] = 'zzz'
}
复制代码


7. BigInt


BigInt 可以表示任意大的整数。其语法如下:

BigInt(value);
复制代码


其中 value 是创建对象的数值。可以是字符串或者整数。

在 JavaScript 中,Number 基本类型可以精确表示的最大整数是253。因此早期会有这样的问题:

let max = Number.MAX_SAFE_INTEGER;    // 最大安全整数
let max1 = max + 1
let max2 = max + 2
max1 === max2   // true
复制代码


有了BigInt之后,这个问题就不复存在了:

let max = BigInt(Number.MAX_SAFE_INTEGER);
let max1 = max + 1n
let max2 = max + 2n
max1 === max2   // false
复制代码


可以通过typeof操作符来判断变量是否为BigInt类型(返回字符串"bigint"):

typeof 1n === 'bigint'; // true 
typeof BigInt('1') === 'bigint'; // true 
复制代码


还可以通过Object.prototype.toString方法来判断变量是否为BigInt类型(返回字符串"[object BigInt]"):

Object.prototype.toString.call(10n) === '[object BigInt]';    // true
复制代码


注意,BigInt 和 Number 不是严格相等的,但是宽松相等:

10n === 10 // false 
10n == 10  // true 
复制代码


Number 和 BigInt 可以进行比较:

1n < 2;    // true 
2n > 1;    // true 
2 > 2;     // false 
2n > 2;    // false 
2n >= 2;   // true
复制代码


8. Object


Object 是 JavaScript 中最复杂的类型,它表示对象。在 JavaScript 中,对象的定义是属性的集合。简单地说,Object 类型数据就是键值对的集合,键是一个字符串(或者 Symbol) ,值可以是任意类型的值; 复杂地说,Object 又包括很多子类型,比如 Date、Array、Set、RegExp。

其实,JavaScript的几个基本数据类型在对象类型中都有一个对应的类:

  • Number;
  • String;
  • Boolean;
  • Symbol。

对于 Number 类,1 与 new Number(1) 是完全不同的值,一个是 Number 类型, 一个是对象类型。Number、String 和 Boolean 构造器是两用的:当跟 new 搭配时,它们产生对象;当直接调用时,它们表示强制类型转换。Symbol 函数比较特殊,直接用 new 调用它会抛出错误,但它仍然是 Symbol 对象的构造器。


对于Object类型,有一种很常见的操作,那就是深拷贝, 这里不再多介绍,可以参考文章:《如何实现一个深浅拷贝?》


三、类型转换


上面提到了,JavaScript是一门弱类型语言,在执行时可能会发生类型转化,那最后我们就来看看数据之间的类型准换规则。


1. StringToNumber


Number 和 String 之间的相互转换应该是比较复杂的,将字符串转化为数字的方法很多,比如Number()、parseInt()、parseFloat()。

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

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


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

  • 1e3;
  • -1e-2。

需要注意,parseInt 和 parseFloat 并不使用这个转换:


  • 在不传入第二个参数的情况下,parseInt 只支持 16 进制前缀“0x”,而且会忽略非数字字符,也不支持科学计数法。所以在任何环境下,都建议传入 parseInt 的第二个参数,
  • parseFloat 则直接把原字符串作为十进制来解析,它不会引入任何的其他进制。

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


2. 装箱转换


上面提到,Number、String、Boolean、Symbol 基本类型在对象中都有对应的类,所谓装箱转换,就是把基本类型转换为对应的对象


我们知道,全局的 Symbol 函数无法使用 new 来调用,但仍可以利用装箱机制来得到一个 Symbol 对象,可以利用一个函数的 call 方法来强迫产生装箱。定义一个函数,函数里面只有 return this,然后调用函数的 call 方法到一个 Symbol 类型的值上,这样就会产生一个 symbolObject


let symbolObject = (function(){ return this }).call(Symbol("a"));
    console.log(typeof symbolObject); //object
    console.log(symbolObject instanceof Symbol); //true
    console.log(symbolObject.constructor == Symbol); //true
复制代码


可以看到,它的 type of 值是 object;使用 symbolObject instanceof 可以看到,它是 Symbol 这个类的实例;它的 constructor 也是等于 Symbol 的。所以,它就是 Symbol 装箱过的对象。装箱机制会频繁产生临时对象,在一些对性能要求较高的场景下,应该尽量避免对基本类型做装箱转换。


可以使用JavaScript中内置的 Object 函数显式调用装箱能力。每一类装箱对象皆有私有的 Class 属性,这些属性可以用 Object.prototype.toString 获取:  

var symbolObject = Object(Symbol("a"));
console.log(typeof symbolObject); //object
console.log(symbolObject instanceof Symbol); //true
console.log(symbolObject.constructor == Symbol); //true
console.log(Object.prototype.toString.call(symbolObject)); //[object Symbol]
复制代码


在 JavaScript 中,没有任何方法可以更改私有的 Class 属性,因此 Object.prototype.toString 是可以准确识别对象对应的基本类型的方法,它比 instanceof 更加准确。但需要注意的是,call 本身会产生装箱操作,所以需要配合 typeof 来区分基本类型还是对象类型。


3. 封箱转换


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

对象到 String 和 Number 的转换都遵循“先拆箱再转换”的规则。通过拆箱转换,把对象变成基本类型,再从基本类型转换为对应的 String 或者 Number。拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError:


let obj = {
  valueOf : () => {console.log("valueOf"); return {}},
  toString : () => {console.log("toString"); return {}}
}
obj * 2
// valueOf
// toString
// Uncaught TypeError: Cannot convert object to primitive value
复制代码


这里定义了一个对象 obj,obj 有 valueOf 和 toString 两个方法,这两个方法都返回一个对象,然后进行 obj*2 这个运算时,先执行了 valueOf,接下来是 toString,最后抛出了一个 TypeError,这就说明了这个拆箱转换失败了。到 String 的拆箱转换会优先调用 toString。把刚才的运算从 obj*2 换成 String(obj),调用顺序就变了:

let obj = {
  valueOf : () => {console.log("valueOf"); return {}},
  toString : () => {console.log("toString"); return {}}
}
String(obj)
// toString
// valueOf
// Uncaught TypeError: Cannot convert object to primitive value


相关文章
|
17天前
|
JavaScript 前端开发
js变量的作用域、作用域链、数据类型和转换应用案例
【4月更文挑战第27天】JavaScript 中变量有全局和局部作用域,全局变量在所有地方可访问,局部变量只限其定义的代码块。作用域链允许变量在当前块未定义时向上搜索父级作用域。语言支持多种数据类型,如字符串、数字、布尔值,可通过 `typeof` 检查类型。转换数据类型用 `parseInt` 或 `parseFloat`,将字符串转为数值。
18 1
|
1月前
|
JavaScript
JS 获取对象数据类型的键值对的键与值
JS 获取对象数据类型的键值对的键与值
|
1月前
|
存储 JavaScript 前端开发
JavaScript数据类型详解
【4月更文挑战第4天】JavaScript有七种基本数据类型(Number, String, Boolean, Null, Undefined, Symbol, BigInt)和一种复杂数据类型(Object)。Number用于数字,String是不可变的文本,Boolean表示逻辑值,Null为空,Undefined表示未赋值,Symbol是唯一标识,BigInt处理大整数。Object用于复杂数据结构,如数组和函数。此外,`typeof`操作符检测数据类型,但有特殊行为,如`typeof null === &quot;object&quot;`。
21 2
|
1月前
|
JavaScript
typeof 和 instanceofJS数据类型(js的问题)
typeof 和 instanceofJS数据类型(js的问题)
|
2月前
|
JavaScript 前端开发
JavaScript 中有哪些数据类型?
JavaScript 中有哪些数据类型?
19 3
|
2月前
|
JavaScript
JS常用数据类型转换
JS常用数据类型转换
13 1
|
1月前
|
JavaScript
数据类型转换(js的问题)
数据类型转换(js的问题)
10 0
|
13天前
|
存储 JavaScript 前端开发
【JavaScript技术专栏】JavaScript基础入门:变量、数据类型与运算符
【4月更文挑战第30天】本文介绍了JavaScript的基础知识,包括变量(var、let、const)、数据类型(Number、String、Boolean、Undefined、Null及Object、Array)和运算符(算术、赋值、比较、逻辑)。通过实例展示了如何声明变量、操作数据类型以及使用运算符执行数学和逻辑运算。了解这些基础知识对初学者至关重要,是进阶学习JavaScript的关键。
|
14天前
|
存储 JavaScript 前端开发
JavaScript引用数据类型
JavaScript引用数据类型
|
14天前
|
JavaScript 前端开发
JavaScript 基本数据类型
JavaScript 基本数据类型