likes
comments
collection
share

你不知道的JavaScript基础类型

作者站长头像
站长
· 阅读数 53

前言

今天聊下js中的数据类型,数据类型是一个语言的基石,那你真的了解我们工作中这些常使用的数据类型吗。可以先看以下几个问题?看看你能直接回答上来几个。本文将从这些问题入手,主要讲解我们在使用中容易模糊和产生歧义的点,加深我们对数据类型的理解。

问题

  • 为什么说undefined是变量?那我们能改变它吗?
  • 为什么 typeof null 是 'object'?null是对象吗?
  • 0.1+0.2 为什么不等于 0.3?
  • 为什么let s = '𠮷'; s.length === 2?
  • 为什么"𠮷".charAt(0) // '\uD842'?

概述

在JavaScript中的类型系统可以分为以下七类,我们称之为语言类型,js不支持自己定义类型,所以在前端代码中所有值都来源于以下七种类型:

  1. Undefined
  2. Null
  3. Boolean
  4. Number
  5. String
  6. Symbol
  7. Object

前六个是简单数据类型(原始值),Object是复杂数据类型,也是js中的大boss,整个js语言都是在对象的基础上建立的,依靠js异常灵活的类型系统,现有的7种类型可满足几乎所有使用场景。

Undefined

Undefined类型的值只有一个就是'undefined',属于js中的特殊类型,自身的含义表示一个未定义的值,这个值在ES3版本以前是不存在的,引入的目的是为了明确空对象指针(null)与未初始化变量的区别。

应用场景:当声明一个变量并没有赋值时,就相当于给变量赋值了一个undefined,所以在任何情况下,我们都没必要给一个变量显式的赋值为 undefined,所以undefind并没有实际的主动使用的场景。

为什么说undefined是变量?那我们能改写它吗?

MDN上对此的描述大概是:它挂载在全局对象,是全局作用域下的一个属性,这个属性的最初值就是原始数据类型undefined

那问题又来了,既然是变量那我们能改变它吗? 这个问题真是毫无意义,我们在任何时候都不会,不应该,不可能去尝试改变undefined的值。

而实际上现代浏览器自ES5标准以来undefined就是一个不能被配置(non-configurable),不能被重写(non-writable)的属性。可是在局部作用域中我们仍然可以声明一个名为undefined的变量,去覆盖全局作用域下的undefined,因为在ES中,undefined既不是关键字也不是保留字(无聊的知识增加了),请看下图:

你不知道的JavaScript基础类型

image.png

为了符合编程规范,有些文章会提到可以使用void操作符合理合法的去获取undefined的值,。。。写起来还蛮耳目一新的(无聊的知识又增加了)。

总之,无论这是设计失误还是有意为之,对我们对于该语言的使用基本没有什么影响,简单了解下就好,这个其实也属于是没什么价值和意义的问题。

Null

Null类型的值只有一个就是'null',与undefined类似,也属于js中的特殊类型,既然null是一个原始类型,那就有了这个问题了:

为什么 typeof null 是 'object'?null是对象吗?

这结果就很奇葩,但仍然表现如此,那多数就是历史原因导致,所以我们无需纠结null到底是原始值还是一个对象。《JavaScript高程4》中对此有比较合乎逻辑的定义:null值表示一个空对象的指针null当然是个原始类型的值,但它也是个空对象的指针,这也解释了为什么typeof null === 'object'

由此我们也应该能理解,null虽然含义与undefined类似都表示空,但null表示的是一个空对象,当我们要声明一个变量准备赋值一个对象,却在当时没有一个具体的对象可保存时,就要使用null来填充该变量,我们永远不会主动的去赋值一个undefined,却经常会主动赋值一个null表示一个对象的初始值。

值得一提的是,当我们去判断undefined == null时,会返回一个true,又是一个迷惑性的操作,背后原因是==操作符让值做了隐式类型转换,这也是js类型系统异常灵活的原因。

Boolean

布尔值,值有两个truefalse

Number

Number类型最常用来表示我们常规意义上的十进制数字,也能使用八或十六进制,除此之外还有一些特殊的值如NaN、Infinity、-Infinity等,相关知识点虽多但大多比较容易理解。这里要专门聊的是js老生常谈的Number浮点值精度不足的问题:

0.1+0.2 为什么不等于 0.3?

前端:王德发?? ES:雨我无瓜。

Number浮点值表现出来的这种特性来源于ES采用的IEEE754二进位浮点数算术标准,该标准运用广泛,很多常见的编程语言(如C++、C#、Java)都使用该标准来处理数据的存储与计算,实际上任何采用此标准的语言,都会有以上特性(有些语言通过内部封装帮助解决该问题)。而ES正是采用其中的双精度浮点数规则。

双精度浮点数是计算机中常见的一种数据格式,在内存中占64位。计算机对 Number 类型做存储时,需将其转化为二进制做存储,十进制小数转为二进制时,会出现二进制位数超出处理范围的问题,如0.1(0.000110011... 0011死循环),计算机会通过0舍1入来存储处理范围内的位数,此时误差就出现了,但是由于保留位数很多,误差将非常小,可忽略不计。

但当我们需要测试某个特定的浮点值时,可能就会产生错误,所以我们在程序中尽量不去验证某个特定的浮点值。ES6之后新增了Number.EPSILON属性,表示数字最小间隔,也可用它来比较判断,是正常误差值还是个错误。

(0.1+0.2)-0.3<=Number.EPSILON //true

ES 也为我们处理了其他一些场景,如1/0并不会抛出错误,而是定义了Infinity的无穷值,非数值会表示为特殊的NaN值等。

String

本节探讨下字符串在 ES 中是如何做存储的,字符串有个非常常用的length属性,表示字符数量,凭借我们程序员最朴素的情感,通常认为length的值会与我们眼睛看到的结果是一致的,但偶尔会突然发现一些不一样的情况,如下:

let s = '𠮷'; console.log(s.length) // 2

栓Q,又被骗到

实际上字符串的存储要比我们看到的复杂的多,如何将字符串真正存储到计算机中,这里涉及到两个多数人都听过,但可能又不是特别了解的概念,字符集字符编码。相关知识点很多可以单开一篇文章,下面简单讲解下。

字符集

字符集相当于一个密码本,在一个字符集中每个字符会对应一个固定的编号(码点),编号可以使用数字代替,而字符则可能是各种各样的文字表情、字母符号、图形图像等一切人们使用的符号。如果我们把全世界所有文字都放到一个字符集中,那就在计算机中实现了世界文字的统一。

而现在也正有这样的一个字符集那就是大名鼎鼎Unicode,这个字符集囊括了迄今为止世界上所有的文字,到今天发布到了15.0版本,收录了 149,186 个文字,已经成为计算机中使用最广泛的字符集标准。

Unicode使用数字给字符做唯一编号,通常使用十六进制表示,会在U+0000~U+10FFFF范围定义字符,能使用的总数大概是一百多万个,目前只有十分之一被定义了字符。比如U+597D代表中文字

Unicode将字符集范围分为了17个平面,前面的65536个字符位,称为基本多语言平面(BMP),它的码点范围就是U+0000~U+FFFF。所有最常见的字符都放在这个平面,是Unicode最先定义的一个平面,其他字符放在其他16个平面,称为增补平面(SMP),

字符编码

而字符编码是指计算机要如何将Unicode中的字符编号存入计算机中,是一种编码方式,每个字符集都有其对应的编码方式,而Unicode对应的编码方式就是我们常听到的,UTF8、UTF16、UTF32。特点如下:

  • UTF8:1-4字节,变长
  • UTF16:2或4字节,变长
  • UTF32:4字节,定长

编码方式

我们知道计算机只能存储二进制数,所以当我们知道一个字符的十六进制码点数(字符编号),只要把它转成二进制,存到计算机中即可,而编码就是如何转换和存储的过程。

虽然以上三种编码方式目的都是将码点转成二进制数,但转换的方式、转换后存储的二进制数、计算机读取二进制的方法都是不同的,这是因为在U+0000~U+10FFFF范围的码点,转换成二进制存储,最小的只需要1个字节,最多需要3个字节

  • 如果我们采用UTF32编码方式,分配给每个字符的内存都将是固定的4个字节/32位,这是最直观的存储方式,Unicode中所有码点转成的二进制数都是32位以内,所以计算机只需要直接将码点转成二进制存入计算机即可,读取时计算机固定按4个字节为一个字符的规则去读取。简单明了,但该方法将造成大量的空间浪费,比如排在前面的ASCII码的字符,只需要1个字节/8位去存储,将浪费3个字节。这种编码方式很少使用,互联网中普遍采用UTF8变长的方式去编码。
  • 如果我们采用UTF16编码方式,分配给每个字符的内存将会分为2或4字节的定长,对于U+0000~U+FFFF范围的基本平面码点,使用2字节/16位直接转换存储。对于U+010000~U+10FFFF辅助平面码点,转换成二进制将大于16位,UTF16会将其拆成两半,使用两个2字节/16位去存储,该方法称为代理对。如𠮷(0xD842,0xDFB7)

UTF-16将这20位拆成两半,前10位映射在U+D800到U+DBFF(空间大小210),称为高位(H),后10位映射在U+DC00到U+DFFF(空间大小210),称为低位(L)。这意味着,一个辅助平面的字符,被拆成两个基本平面的字符表示。

JS中的字符编码

说了这么多,我们再聊回js,js中到底采用哪种编码方式是UTF8还是UTF16呢,重点来了,都不是!js采用的是UTF16UCS-2的编码混合策略,从今天看来UTF16可以算是UCS-2的升级版。为什么js不直接采用UTF16呢,因为js首次面世时UTF16还未推出,两者混用也算是一个历史遗留问题。

对于U+0000~U+FFFF基本平面的字符UCS-2UTF16是完全没区别的,我们日常使用的绝大部分字符都来源于这个平面,所以js开发者在一般情况下对此无感。

UCS-2是固定的将2字节/16位认为是一个字符(因为Unicode早期只有一个平面,16位已经完全足够,后期进行了扩容)。当字符中出现基本平面之外的字符,因为上文说的代理对策略,该字符会用两个2字节/16位去存储,而UCS-2固定的认为2字节就是一个单独的字符,所以此时使用字符类的操作时,会出现错误,所以才会出现上述问题'𠮷'.length = 2

当然这些都算是历史问题,自ES6推出,Unicode相关的编码问题已经得到解决,ES也完全有能力自动识别字符是2个字节还是4个字节,但开发者对于.length的使用习惯由来已久,为了保证兼容性,并未对其结果做出修正。

结尾

由于众所周知的历史原因和复杂多样的执行环境,JavaScript在使用过程中可能会遇到各种奇怪难理解的现象,有一些是因为更深层的底层原理,也有很多只是因为设计失误或历史包袱,在这里作者建议我们在学习中只聚焦少部分有价值的问题,而忽略无意义的探究。

参考文献

转载自:https://juejin.cn/post/7164311846432800798
评论
请登录