JavaScript数据类型

298 阅读9分钟

数据类型系统

前言

数据类型系统分语言类型和规范类型,规范类型有以下分类:

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

本文不对规范类型做重点介绍,这篇文章主要从以下几个方面讨论JavaScript的数据类型:

  • 数据类型
  • 数据类型检测
  • 数据类型转换
  • 装箱和拆箱操作

数据类型中的对象是重点内容,将留在下一篇文章中单独介绍。

一、数据类型

类型分类

基本类型Undefined、Null、Boolean、String、Number、Symbol、BigInt

基本数据类型存储在中,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。

引用类型:统称为Object,主要包括Object、Array、Function、Math、RegExp、Date等,详细类型分类会在下一讲介绍

引用数据类型存储在(heap)中的对象,占据空间大、大小不固定,如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

两种类型的区别:

  • 区别一:存储位置不同

    • 基本类型存储在栈中
    • 引用类型存储在堆、栈中
  • 区别二:访问方式不同

    • 基本类型按值访问
    • 引用类型按索引访问

详细介绍

Undefined、Null

这两种类型主要记住他们的区别就可以了,区别知道了用法自然也就知道了。

Undefined类型表示为定义,只有一个值undefined,但是这里的undefined是用一个全局变量(undefined)表达的,而不是关键字,这个全局变量可以被重写,因此建议用void 0代替直接用undefined。

注意:

这里的被重写指的是局部变量可以被重写,因为作用域链的原因会优先访问局部作用域的变量,而全局变量的undefined不可以被重写,因为全局对象上的undefined属性被设置为只读的(read-only)。

为什么用void 0代替undefined?

void 能对给定的表达式求值,返回undefined,因此void后边跟任何数据类型,都返回undefined,选择void 0只是因为0占据的字节比较少,最优考虑。

function foo() {
    var a  = arguments[0] !== (void 0 ) ? arguments[0] : 2;
    return a; 
}

Boolean

Number

Number这块主要从数值精度和类型转换两方面介绍。

JavaScript 中的 Number 类型有 18437736874454810627(即 2^64-2^53+3) 个值,其有效的整数范围是 -0x1fffffffffffff 至 0x1fffffffffffff,所以 Number 无法精确表示此范围外的整数。有几个特殊的值需要注意:

  • NaN,占用了 9007199254740990;

  • Infinity,无穷大;

  • -Infinity,负无穷大。

关于数值精度,有一段很著名的代码:

0.1 + 0.2 == 0.3 // false

0.3 + 0.2 == 0.5 // true

这是因为数值在计算时要从十进制转换为二进制,而0.1 0.2 0.3 转换为二进制时是无限长的,而数值的精度范围是有限制的,这就导致了固定位数的二进制无法表示无限循环序列(超出部分对进行舍去或者进位,这就是代码中的误差来源)。

十进制转二进制分如下情况:正整数转二进制,负整数转二进制,小数转二进制

正整数转二进制: 正整数转成二进制。要点一定一定要记住哈:除二取余,然后倒序排列,高位补零。

负整数转二进制:先是将对应的正整数转换成二进制后,对二进制取反,然后对结果再加一。

小数转二进制:对小数点以后的数乘以2,有一个结果,取结果的整数部分(不是1就是0),然后再用小数部分再乘以2,再取结果的整数部分,依次类推

关于0.1 0.2 0.3 转换为二进制,以0.1为例:

0.1×2=0.2 .....................0

0.2×2=0.4 ......................0

0.4×2=0.8 .....................0

0.8×2=1.6.......................1

0.6×2=1.2.......................1

0.2×2=0.4.......................0

......是无限循环的

正确的比较方法是使用 JavaScript 提供的最小精度值:

Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON

关于number的类型转换接下来会介绍。

String

JavaScript中的字符串一旦创建是永远无法更改的,是一个值类型。

应用方面主要有几种方法:charAt、charCodeAt、codePointAt、length,需要注意的是,这些方法针对的不是”字符“而是UTF16编码。因为String的意义并非字符串,而是字符串的UTF16编码。一个UTF16编码两个字节,而一个字符正好也两个字节,所以说一个UTF16编码对应一个字符。

Note:现行的字符集国际标准,字符是以 Unicode 的方式表示的,每一个 Unicode 的码点表示一个字符,理论上,Unicode 的范围是无限的。UTF 是 Unicode 的编码方式,规定了码点在计算机中的表示方法,常见的有 UTF16 和 UTF8。 Unicode 的码点通常用 U+??? 来表示,其中 ??? 是十六进制的码点值。 0-65536(U+0000 - U+FFFF)的码点被称为基本字符区域(BMP)。

对于超出U+0000 - U+FFFF 范围的字符,在判断时可以使用上边的codePointAt方法。

Symbol

Symbol是ES6新引入的类型,也是很特别的一个类型,两个原因:一个是具有唯一性,即使相同的描述,Symbol也不相等;第二是相比Number、String 和 Boolean,作为构造器时,Symbol不能跟new搭配。

我们知道一个对象的key值,其数据类型要么是字符串,要么就是Symbol类型,所以后者代表的是一切非字符串的对象key的集合,这样就比较好理解了。

Symbol一个比较广的应用是和for...of搭配,for...of可以遍历具有可迭代属性的一切对象,比如Array、Map、Set、String、TypeArray、arguments,因为这些对象本身具有Symbol.iterator属性,如果给对象自定义该属性,也可与对其遍历。

Object

JavaScript中对象的定义是”属性的集合“,也就是说不区分属性和方法一切统称为属性(《你不知道的js》里面有说明)。属性分为数据属性和访问器属性,key值就是我们上边说的字符串或者Symbol类型。

对象涉及的知识比较多,包括:对象的分类、类、装箱和拆箱操作。

对象分类主要从宿主对象和内置对象去划分,下一篇文章会展开讨论。

类也是一个单独的话题,因为JavaScript中的”类“和Java、C++中的不一样,JavaScript 中的“类”仅仅是运行时对象的一个私有属性,而 JavaScript 中是无法自定义类型的,后边单独介绍。

JavaScript中的几个基本类型,在对象类型中都有一个”亲戚“,分别是:Number;String;Boolean;Symbol

比如3 与 new Number(3) 是完全不同的值,它们一个是 Number 类型, 一个是对象类型。

有一种现象,基本类型可以调用对象上的方法,比如:

"abc".charAt(0)  // a

基本类型是没有方法的,所有用到的方法都是其对应的”亲戚“(对象)上的,但是前者为什么能调用后者的方法?装箱操作,.运算符提供了装箱操作,能够根据基本类型构建一个临时对象,然后调用对象的方法。

二、类型检测

typeOf

  • 返回的是一个表示数组类型的字符串,都是小写,包括string、boolean、number、undefined、symbol、function、object
  • 不能准确分别数组和对象
  • 对null的检测类型为Object
typeof Symbol(); // symbol 有效
typeof ''; // string 有效
typeof 1; // number 有效
typeof true; //boolean 有效
typeof undefined; //undefined 有效
typeof new Function(); // function 有效
typeof null; //object 无效
typeof [] ; //object 无效
typeof new Date(); //object 无效
typeof new RegExp(); //object 无效

instanceof

  • instanceof原理是检查该对象是否是原型链上某个构造函数的实例,而Null、Undefined类型没有构造函数,故没有实例对象,不能检测null、undefined。

  • 只要在类型原型链上,检测出来的都是true,故检测不准确,比如数组、对象、函数都是Object的实例

  • 对于基本数据类型来说,字面量方式创建出来的结果和实例方式创建的是有一定的区别的

console.log(1 instanceof Number)//false
console.log(new Number(1) instanceof Number)//true

constructor

  • 不能检测null和undefined

  • 把类的原型进行重写,检测就不准确

function Fn(){}
Fn.prototype = new Array()
var f = new Fn
console.log(f.constructor)//Array

Object.prototype.toString.call()

  • toString方法本意上是将对象转换为字符串
  • 对于Number、String,Boolean,Array,RegExp、Date、Function原型上的toString方法都是把当前的数据类型转换为字符串的类型,null和undefined没有toString方法
  • Object上的toString并不是用来转换为字符串的,每一类对象皆有私有的Class属性,这些属性可以通过Object.prototype.call来获取。
Object.prototype.toString.call('') ;   // [object String]
Object.prototype.toString.call(1) ;    // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window是全局对象global的引用

三、类型转换

强制转换

  • StringToNumber

    • parseInt

    parseInt通过第二个参数可以指定把二进制、八进制、十六进制或其他任何进制的字符串转换成整数。还有一些细节情况比如开头不是数字的,基本用不到就不做讨论。

    • parseFloat

    parseFloat没有第二个参数;只能解析到第一个小数点;

    • Number

    Number可以强制类型转换,用的比较多。

  • NumberToString

    • toString()
    var num = 2;
    num.toString() // '2'
    var bol = true;
    bol.toString() // 'true'
    

    除去undefined和null,其余类型都有toString方法

    • String()
    var sym = Symbol(1);
    String(sym) // 'Symbol(1)'
    var num = 10;
    String(num)  // '10'
    var str;
    String(str) // 'undefined'
    

    如果值能调用toString方法(该方法是值对应的对象类型里的‘亲戚’),则调用该方法并返回响应的结果。

    • 隐式转换
    var a = 3;
    a + ''  // '3'
    var obj = {a: 3};
    
    obj + '' // [object object]
    

    转换规则和 String 方法一致

自动转换

  • if判断会转换为布尔

  • 除了加法之外的运算符

  • 加法运算会转化为字符串

装箱和拆箱操作

每一种基本类型 Number、String、Boolean、Symbol 在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。

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

拆箱操作会调用valueOftoString方法,来获得拆箱后的基本类型,如果 valueOftoString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError

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

o * 2
// valueOf
// toString
// TypeError

o * 2这个运算会先执行valueOf再执行toString。到String的拆箱会优先调用toString

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