数据类型系统
前言
数据类型系统分语言类型和规范类型,规范类型有以下分类:
- 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的引用
三、类型转换
强制转换
-
StringToNumberparseInt
parseInt通过第二个参数可以指定把二进制、八进制、十六进制或其他任何进制的字符串转换成整数。还有一些细节情况比如开头不是数字的,基本用不到就不做讨论。parseFloat
parseFloat没有第二个参数;只能解析到第一个小数点;- Number
Number可以强制类型转换,用的比较多。
-
NumberToStringtoString()
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 函数,它是对象类型到基本类型的转换(即拆箱转换)。
拆箱操作会调用valueOf和toString方法,来获得拆箱后的基本类型,如果 valueOf和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 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