JavaScript 类型 — 重学 JavaScript

2020-10-29 10:18:51 浏览数 (1)

我是三钻,一个在《技术银河》中等你们一起来终生漂泊学习。点赞是力量,关注是认可,评论是关爱!下期再见 ?!

这个笔记是基于 Winter 老师的 《重学前端》的内容总结而得。

JavaScript 中最小的结构,同学们已知的有什么呢?我想同学们都应该会想到一些东西,比如一些关键字,数字 123,或者 String 字符等等。这里我们从最小的单位,字面值和运行时类型开始讲起。

原子(Atom)

这里分为语法(Grammer)和运行时(Runtime)两个部分。

语法(Grammer)

  • 直接量/字面值(Literal)
  • 变量(Variable)
  • 关键字(Keywords)
  • 空格/换行符(Whitespace)
  • 行结束符(Line Terminator)

这些都是我们用来组成 JavaScript 语言的最小元素/单位,这是通过我们的字面值,比如一个数字类型的字面值 1231.12.2,然后配合上我们的变量和 ifelse 关键字,以及一些符号、空白符、换行符等。它们虽然不会产生一些语言上的作用,但是可以让我们整个语言的格式更好看一些。

运行时(Runtime)

  • 类型(Types)
  • 执行上下文(Execution Context)

语法中的元素实际上最终反映到运行时(Runtime)中,字面值一共有五六种写法,对应到 JavaScript 的 7 种基本类型中的几种。另外我们的变量实际上是对应到运行时的 Execution Context 的一些存储变化。最终这些语法都会造成运行时的改变。

JavaScript 中的类型

  • 数字类型(Number)
    • 这个在小学的时候就认识了
    • 但是到了 JavaScript 当中就不是小学时候理解的那个概念了
  • 字符类型(String)
    • 这个到了学编程的时候都会知道的概念
  • 布尔类型(Boolean)
    • 表示真值
    • 计算机领域的 true 和 false,是把日常生活中真假的概念做了一个抽象
  • 对象(Object)
    • Object 历史渊源比较久
  • Null
    • 代表的是有值,但是是空
    • 有一个设计 bug,typeof null 值的变量时会出来 Object,这个只能忍耐了,因为官方也声明过不会修复的。
  • Undefined
    • 本没有没有定义过这个值
  • Symbol
    • 新加的基本类型
    • 它一定程度上代替了 String 的作用
    • 可以用于 Object 里的索引
    • 与 String 最大的区别就是,String 全天下都一样,只要你能猜出 String 的内容是什么,无论前面后面加了多少个符号,只要别人想用,对象的属性总是能取出来。
    • Symbol 就不一样了,如果你取不到 Symbol,那里面的内容是不可能被取得的。这个也是 JavaScript 独特有的特性。

NullUndefined 经常被我们的前端工程师被混起来使用, 所以说我们不会把 Undefined 的值用来赋值,我们只会检查一个变量的值是否是 Undefined。但是客观上来说 JavaScript 是允许进行 Undefined 赋值的。建议大家一定要克制,凡是我们进行过赋值的我们尽量都用 Null,而不是用 Undefined。

真正编程中会有5种比较常用的基本类型,NumberStringBooleanObjectNull

Number 类型

在我们的概念里面 Number 就是一个数字,准确的说 JavaScript 中的 Number 对应到我们的概念里面的有限位数的一个小数。

Number 按照它的定义是 double float,双精度浮点数类型。很多时候我们对 Number 的理解都在表面,所以我们要理解 IEEE754 定义的 Float 标准,我们才能真正理解 JavaScript 里面的 Number。

Float 表示浮点数,意思是它的小数点是可以来回浮动的。他的基本思想就是把一个数字拆成它的 “指数” 和 “有效位数”。这个数的有效位数决定了浮点数表示的精度,而指数决定浮点数表的范围。

浮点数还有一个可以表示的符号,它可以是正负,总共占一位。0 为正数,1 为负数。

IEEE754 定义的浮点数中有以下:

  • 符号位|Sign(1位)—— 用于表示正负数
  • 指数位|Exponent(11位)
  • 精度位|Fraction(52位)

Number 的语法 在 2018 年的标准里面有 4 个部分:

  • 十进制(Decimal Literal)
    • 00..21e3
  • 二进制(Binary Integeral Literal)
    • 0b111 —— 以 0b 开头,可以用 0 或者 1
  • 八进制(Octal Integral Literal)
    • 0o10 —— 以 0o 开头,可以用 0-7
  • 十六进制(Hex Integer Literal)
    • 0xFF —— 0x 开头,可以用 0-9,然后 A-F

十进制案例

十进制 来表示 Number,比如说我们现在有一个浮点数 205.75,那么用十进制来表示呢?

数学中的表示就是:

205.75 = 2times100 0times10 5times1 7times0.1 5times0.01

换成计算机的十进制就是:

205.75 = 2times10^2 0times10^1 5times10^0 7times10^{-1} 5times10^{-2}

上面两个公式得出的结果都是:200 0 5 0.7 0.05 = 205.75

二进制案例

在上面的 IEEE754 中的每一位数都是二进制的,而一共是有 64 位。那二进制的 5.75 又是怎么表示呢?

计算机中的二进制表示:

5.75 = (1times2^2 0times2^1 1times2^0) (1times2^{-1} 1times2^{-2})

这里有一个浮点数中的老生常谈的 0.1 的浮点数的精度丢失问题,到底是怎么回事呢?我们一起来用二进制来表示看一下是怎么回事吧!

首先我们把二进制中一些精度位数的值先列出来,这些会在后面表示我们 0.1 的时候用到。

frac12 = 0.5 \ frac14 = 0.25 \ frac18 = 0.125 \ frac1{16} = 0.0625 \ frac1{32} = 0.03125 \ frac1{64} = 0.015625 \ frac1{128} = 0.0078125 \ frac1{256} = 0.00390625 \ frac1{512} = 0.001953125 \ frac1{1024} = 0.000976562 \

使用二进制的时候,其实我们是用所有 2 次方的结果值相加得到我们的数字的。因为这里是从,小数点开始所以我们从

2^{-1}

(也就是

frac12

)开始。然后我们用上面的 2 次方表来找到可相加的数值,让相加的数值可以等于,或者最接近 0.1

  • 这里我们会发现头三个的数值都大于 0.1 所以都是
0times二次方的数值

,直到

frac1{16}

开始是可以相加的。这里

0.01 - frac1{16} = 0.0375

,所以加法的结果与 0.1 还差 0.0375。

  • 所以我们需要继续往后找数值相加后结果是小于或者等于 0.1 的。
  • 这里我们发现下一位
frac1{32}

是可以的,最后相加后是 0.09375

0.1 = 0timesfrac12 0timesfrac14 0*frac18 1timesfrac1{16} 1timesfrac1{32} = 0.09375
  • 如果我们想再接近 0.1,我们就需要继续往下找,首先
0.1 - 0.09375 = 0.00625

,所以我们需要继续往下找小于或者等于这个数的2的次方。

  • 我们发现下两位
frac1{64}

frac1{128}

都是大于 0,00625,直到

frac1{256}

是可以的。

0.1 = 0timesfrac12 0timesfrac14 0*frac18 1timesfrac1{16} 1timesfrac1{32} 0timesfrac1{64} 0timesfrac1{128} 1timesfrac1{256} 1timesfrac1{512} = 0.999609375

如果我们把上面的公式换成二进制就是:00110011

如果我们一直往下寻找,并且相加,我们会发现二进制会一直循环 0011 这个规律。但是因为 IEEE754 里面双精度的精度位最多只有 52 位。所以就算我们一直放满 52 位,也无法相加得到 0.1 这个数值,只能越来越接近。所以最后再二进制中 0.1 是一定会有至少一个 epsilon 的精度丢失的。(这里的解说,是通过 "代码会说话" UP 主的视频学习所得。想看视频解说的可以 点这里观看)

String 类型

String 对大家来说就是一个文本,写字读字大家都会,表示字我们在代码中加上 '(引号)就是 String了。但是 String 还是有一些知识需要我们去理解透测的。

首先我们要介绍的是 Character (字符) 相关的知识。String 在英文里的意思是串成一串的意思,在计算机领域里面这个字符串,就是把字符串在一起,那串着的就当然是字符了。

那字符在英文里面就是 Character ,但是字符在计算机里面是没有办法表示的。比如我们看到的字母 A、中文的 等这些字符都是一个形状,其实这些都是字形,我们认为字符其实是一个抽象的表达。然后结合字体才会变成一个可见的形象。

那计算机里怎么表示 Character 呢?它是用一个叫 Cold Point(码点) 来表示 Character 的。Code Point 其实也不是什么复杂的东西,就是一个数字。比如说我们规定 97 就代表 A,只要我们结合一定的类型信息,我们只要用 97 和字体里面的信息,就可以把 A 找出来并且画到屏幕上。

那问题来了,计算机怎么存储这个 97 这个数字呢?我们都知道计算机当中存储的基本单位是 字节(Bytes)。数字和英文只需要一个字节就能存了,但是中文一个字节就不够用了。所以要理解透彻 String 呢,我们就需要理解 字符(Character)码点(Code Point)编译(Encoding) ,这三个概念了。

字符集(String)

这里我们就讲讲这些字符集的来由和各自的特征。

  • ASCII
    • 大部分的同学都知道 ASCII 这个概念的
    • 早年确实是因为字符数数量比较少,所以我们都把字符的编码都叫 ASCII 码
    • 但是其实是不对的,ASCII 只规定了 127 个字符
    • 这 127 个字符就是计算机里最常用的 127 个字符,包括26个大写,26个小写英文字母,0-9数字,以及各种制表符、特殊符号、换行、控制字符,总共用了127个,所以用了 0-127 来表示
    • 但是这个显然就没有办法表示中文了,ASCII 字符集最早是美国计算机先发明出来的一种编码方式,所以只照顾到英文
  • Unicode
    • Unicode 是后来建立的标准,把全世界的各种字符都给放在一起了,形成一个大合集
    • 所以也叫 “联合的编码集”
    • Unicode 的字符的数量非常庞大,然后还划成了各种的片区,每个片区分给不同国家的字符和字体
    • 早年的时候大家觉得 Unicode 中 0000 到 FFFF 就已经够了,也就是相等于两个字节,后来发现还不够用
    • 所以这个也造成了一些设计上的问题
  • UCS
    • Unicode 和另外一个标准化组织发生结合的时候产生了 UCS
    • UCS也是只有 0000 到 FFFF 一个范围的字符集
  • GB(国标)—— 国标经历了几个年代
    • 国标有几个版本 GB2312GBK(GB3000)GB18030
    • GB2312 是国标的第一个版本,也是大家广泛使用的一个版本。
    • GBK 是后来推出的扩充版本,GBK 本来也是以为够用了
    • 后来又出了一个大全的版本叫 GB18030, 这个就补上了所有的缺失的字符了
    • 国标里的字符码点跟 Unicode 里面的码点不一致
    • 但是这个几乎与世界所有的编码都会去兼容 ASCII
    • 国标范围比较小,与 Unicode 相比同样的一组中文,用 GB 编码肯定要比用 Unicode 要省空间
  • ISO-8859
    • 与国标类似,一些东欧国家把自己国家的语言设计成了类似 GB 一样的 ASCII 扩展
    • 8859 系列都是跟 ASCII 兼容,但是互不兼容,所以它不是一个统一的标准
    • 然后我们国家也没有往 ISO 里面去推,所以 ISO 里面是没有中文的版本的
  • BIG5
    • BIG5 与国标类似,是台湾一般用的就是 BIG5,俗称大五码
    • 我们小时候一般大家不用 Unicode 的时候,就会发现台湾游戏玩不了,所有文字都是乱码
    • 这个就是因为他们用的是大五码来表示字符的
    • ISO-8859系列和 BIG5 系列的性质特别的像,都是属于一定的国家地区语言的特定的编码格式
    • 但是他们的码点都是重复的,所以是不兼容的,所以会出现乱码,需要去切换编码才能正常看到文字

字符编码(Encoding)

因为 ASCII 字符集本身最多就占一个字节,所以说它的编码和码点是一模一样的,我们是没有办法做出比一个字节更小的编码单位。所以 ASCII 不存在编码问题,但是 GBUnicode 都存在编码问题。因为 Unicode 结合了各个国家的字符,所以它存在一些各种不同的编码方式。

UTF-8 (全称:Unicode Transformation Format 8-bit)是一种针对 Unicode 的可变长字符编码,也是一种续码。里面的 8 是代表 8 个字节。

我们一起来通过理解 String 是怎么编译 UTF-8 的,从而来深入认识 UTF-8 背后的原理。

我们要转换 String 之前,我们要知道 UTF-8 的编码结构长度,它是根据某单个字符的大小来决定的。

在 JavaScript 中,我们可以使用 charCodeAt 来查看一下字符大小,我们会发现:英文占的是 1 个字符,汉字占的是 2 个字符。

然后单个 Unicode 字符编码之后最大的长度是 6 个字节,以下就是每个字符大小占用多少个字节的一个换算:

  • 1个字节:Unicode 码为 0 - 127
  • 2个字节:Unicode 码为 128 - 2047
  • 3个字节:Unicode 码为 2048 - 0xFFFF
  • 4个字节:Unicode 码为 65536 - 0x1FFFFF
  • 5个字节:Unicode 码为 0x200000 - 0x3FFFFFF
  • 6个字节:Unicode 码为 0x4000000 - 0x7FFFFFFF

这里呢,英文和英文字符的 Unicode 码点是 0 - 127,所以英文在 Unicode 和 UTF-8 中的长度和字节是一致的。都是只占用一个字节。但是中文汉字的 Unicode 码点范围是 0x2e80 - 0x9fff,所以汉字在 UTF-8 中的最长长度是 3 个字节。

字符转换 UTF-8 编码

1、获取字符 Unicode 值大小

代码语言:javascript复制
let string = '中';
let charCode = str.charCodeAt(0);
console.log(charCode); // 返回:20013

所以这里我们获取到,汉字字符 的字符大小是 20013

2、判断字符 UTF-8 长度

上一步我们获得字符的大小,根据 Unicode 的长度区间换算出这个字符占用多少个字节。根据我们的上面的表格,我们可以看出字符 落在 2048 - 0xFFFF 这个区间,那就是占 3 个字节。

3、补码

在转换成 UTF-8 时,我们就需要用补码的规则进行转换。首先我们看看 UTF-8 中的补码规则:

  • 1 个字节:0xxxxxxx
  • 2 个字节:110xxxxx 10xxxxxx
  • 3 个字节:1110xxxx 10xxxxxx 10xxxxxx
  • 4 个字节:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
  • 5 个字节:111110xx 10xxxxxx 10xxxxxx 10xxxxxx
  • 6 个字节:1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

这里面的 x 代码补位的位置。在一个字节的时候是特殊的,直接用 0 控制位开头,然后后面补位有 7 个。其他都是 n 个 1 0 开头。这里有一个规律。从 2 个字节开始,头一个字节中的 1 的个数就是字节的个数,比如 2 个字节的,就是 2 个 1 0 开头,3 个字节的就是 3 个 1 0 开头。然后后面的字节都是 10 开头,接着的都是补位。

现在知道补位的规则,那如果是一个字符 "A" 我们应该怎么填写这些补位,从而获得 UTF-8 编码呢?

  1. 首先 “中” 的 charCode 是 200013
  2. 200013 位于 2048 - 0xFFFF 的区间,所以 “中” 是占用三个字节的
  3. UTF-8 的 3 个字节补位规则是 :1110xxxx 10xxxxxx 10xxxxxx
  4. 首先把 200013 转二进制,那就是 01001110 00101101
  5. 然后就将 200013 的二进制以前到后的顺序依次放到 3 个字节的部位空间(也就是 x 的位置)
  6. 放入部位中之后,我们获得 11100100 10111000 10101101 ,这里面加粗的部分就是原本 200013 二进制的部分。
  7. 最后我们把转换出来的 11100100 10111000 10101101 的 3 个字节的 UTF-8 编码,转换成 十六进制,得到 0xE4 0xB8 0xAD

为了证明我们转换的结果是正确的,我们可以用 node.js 中的 Buffer 来验证一下。

代码语言:javascript复制
var buffer = new Buffer('中'); 
console.log(buffer.length); // => 3
console.log(buffer); // => <Buffer e4 b8 ad>
// 最终得到三个字节 0xe4 0xb8 0xad

这部分内容的参考了 “张亚涛” 的 《通过javascript进行UTF-8编码》

字符串语法(Grammer)

早年 JavaScript 支持两种写法:

  • 双引号字符串 —— “abc”
  • 单引号字符串 —— 'abc'

双引号和单引号字符串其实没有什么区别,它们之间的区别仅仅是在单双引号的使用下,双引号里面可以加单引号作为普通字符,而单引号中可以加双引号作为普通字符。

引号中会有一些特殊字符,比如说 “回车” 就需要用 n、“Tab” 符就是 t。在双引号当中如果我们想使用双引号这个字符的时候,同样我们可以在前面加上反斜杠:"。没有特殊含义的字符,就是在它们前面加上反斜杠。(然后反斜杠自身也是 \ 就可以了)

这些就是字符串里面的 “微语法”。

到了后面比较新的 JavaScript 版本就加了 “反引号” —— `abc`,也就是我们键盘上 1 键左边的按键。目前来说反引号这个符号是不太常用,也正因为这个字符不常用,所以它非常适合做语法的结构。

反引号要比早年的双单引号更加强大,里面可以解析出回车、空格、Tab等字符。特别是可以在里面插入 ${变量名} ,直接就可以在字符串内插入变量拼接。只要我们在里面不用反引号,我们可以随便加什么都行。

那么 JavaScript 引擎是怎么编译反引号和分解里面的变量的呢?

这里我们举个例子 `ab

{x}abc

{y}abc`

在这个反引号中,JavaScript 引擎会把它拆成 3 份,`ab${、`}abc%{、}ab`

  • 所以我们看起来这个反引号是一个整体
  • 但是其实在我们的 JavaScript 的引擎看来,一个反引号后面跟着一个字符串,然后后边一个 $ 符号左大括号,这才是一对的括号关系,它们引入了字符串
  • 中间的结构都是一个右打括号,后面跟着一串字符,最后是 $ 符号左打括号,这一个整个也是一对括号关系
  • 所以说其实一个反引号,造成了事实上 4 种不同的新的 token,分别是 开始中间结束,当然还有前后的反引号,但是中间我们是不插变量的这样形式。这里其实就是用 4 种 token 形成了一种 String 模版的语法结构(String Template)。
  • 如果我们按照 JavaScript 引擎的角度,它其实是反过来的,被括起来的是一些裸的 JavaScript 语法,被括起来以外的部分才是字符串的本体。
  • 这种格式

案例 —— 这里我们尝试使用正则表达式,来匹配一个单引号/双引号的字符串:

代码语言:javascript复制
// 双引号字符正则表达式
"(?:[^"n\ru2028u2029]|\(?:[''\bfnrtvnru2029u2029]|rn)|\x[0-9a-fA-F]{2}|\u[0-9a-fA-F]{4}|\[^0-9ux'"\bfnrtvn\ru2028u2029])*"

// 单引号字符正则表达式
'(?:[^'n\ru2028u2029]|\(?:[''\bfnrtvnru2029u2029]|rn)|\x[0-9a-fA-F]{2}|\u[0-9a-fA-F]{4}|\[^0-9ux'"\bfnrtvn\ru2028u2029])*'
  • 看首先是空白定义,包含回车斜杠nr
  • 20282029 就是对应的分段分页
  • xu 两种转义方法
  • 当然这个我们是不需要死记住的,只要知道 bfnrtv 这几种特殊的字符,还有上面考虑到的因素即可。

布尔类型

其实就是 truefalse, 这个类型真的是非常简单的类型,如果没有和计算联合起来用,就真的是一个很简单的类型。

Null 和 Undefined

这两个类型都是大家日常会接触的,其实都表示空值。不同的是:

  • Null 表示有值,但是是空
  • Undefined 语义上就表示根本没有人去设置过这个值,所以就是没有定义

我们要注意 Null 其实是关键字 ,但是 Undefined 其实并不是关键字。

Undefined 是一个全局变量,在早期的 JavaScript 版本里全局的变量我们还可以给他重新赋值的。比如我们把 Undefined 赋值成 true,最后造成了一大堆地方出问题了。但是大家一般都没有那么顽皮,这么顽皮的人一般都被公司开掉了哈。

虽然说新版本的 JavaScript 无法改变全局的 Undefined 的值,但是在局部函数领域中,我们还是可以改变 Undefined 的值的。例如以下例子:

代码语言:javascript复制
function foo() {
  var undefined = 1;
  console.log(undefined);
}

那么 null 是一个关键字,所以它就没有这一类的问题,如果我们给 Null 赋值它就会报错了。

代码语言:javascript复制
function foo() {
  var null = 0;
  console.log(null);
}

这里就说一下,我们怎么去表示 Undefined 是最安全的呢?在开发的过程中我们一般不用全局变量,我们会用 void 0 来产生 Undefined ,因为 void 运算符是一个关键字,void 后面不管跟着什么,他都会把后面的表达式的值变成 Undefined 这个值。那么 void 0void 1void 的一切都是可以的,一般我们都会写 void 0,因为大家都这么写,大家说一样的话比较能够接受。

小总结

我们还有一个 Symbol 和 Object 还没有讲到,这个就结合在一起在一篇文章中一起讲了。敬请期待!

0 人点赞