我是三钻,一个在《技术银河》中等你们一起来终生漂泊学习。点赞是力量,关注是认可,评论是关爱!下期再见 ?!
这个笔记是基于 Winter 老师的 《重学前端》的内容总结而得。
JavaScript 中最小的结构,同学们已知的有什么呢?我想同学们都应该会想到一些东西,比如一些关键字,数字 123,或者 String 字符等等。这里我们从最小的单位,字面值和运行时类型开始讲起。
原子(Atom)
这里分为语法(Grammer)和运行时(Runtime)两个部分。
语法(Grammer)
- 直接量/字面值(Literal)
- 变量(Variable)
- 关键字(Keywords)
- 空格/换行符(Whitespace)
- 行结束符(Line Terminator)
这些都是我们用来组成 JavaScript 语言的最小元素/单位,这是通过我们的字面值,比如一个数字类型的字面值
123
、1.1
、2.2
,然后配合上我们的变量和if
、else
关键字,以及一些符号、空白符、换行符等。它们虽然不会产生一些语言上的作用,但是可以让我们整个语言的格式更好看一些。
运行时(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 独特有的特性。
Null
和Undefined
经常被我们的前端工程师被混起来使用, 所以说我们不会把 Undefined 的值用来赋值,我们只会检查一个变量的值是否是 Undefined。但是客观上来说 JavaScript 是允许进行 Undefined 赋值的。建议大家一定要克制,凡是我们进行过赋值的我们尽量都用 Null,而不是用 Undefined。
真正编程中会有5种比较常用的基本类型,Number
、String
、Boolean
、Object
、Null
。
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)
0
、0.
、.2
、1e3
- 二进制(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
,那么用十进制来表示呢?
数学中的表示就是:
换成计算机的十进制就是:
上面两个公式得出的结果都是:200 0 5 0.7 0.05 = 205.75
二进制案例
在上面的
IEEE754
中的每一位数都是二进制的,而一共是有64 位
。那二进制的5.75
又是怎么表示呢?
计算机中的二进制表示:
这里有一个浮点数中的老生常谈的
0.1
的浮点数的精度丢失问题,到底是怎么回事呢?我们一起来用二进制来表示看一下是怎么回事吧!
首先我们把二进制中一些精度位数的值先列出来,这些会在后面表示我们 0.1
的时候用到。
使用二进制的时候,其实我们是用所有 2 次方的结果值相加得到我们的数字的。因为这里是从,小数点开始所以我们从
(也就是
)开始。然后我们用上面的 2 次方表来找到可相加的数值,让相加的数值可以等于,或者最接近 0.1
。
- 这里我们会发现头三个的数值都大于
0.1
所以都是
,直到
开始是可以相加的。这里
,所以加法的结果与 0.1
还差 0.0375。
- 所以我们需要继续往后找数值相加后结果是小于或者等于
0.1
的。 - 这里我们发现下一位
是可以的,最后相加后是 0.09375
- 如果我们想再接近
0.1
,我们就需要继续往下找,首先
,所以我们需要继续往下找小于或者等于这个数的2的次方。
- 我们发现下两位
和
都是大于 0,00625,直到
是可以的。
如果我们把上面的公式换成二进制就是: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(国标)—— 国标经历了几个年代
- 国标有几个版本
GB2312
、GBK(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 不存在编码问题,但是 GB
、Unicode
都存在编码问题。因为 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 编码呢?
- 首先 “中” 的 charCode 是
200013
- 200013 位于
2048 - 0xFFFF
的区间,所以 “中” 是占用三个字节的 - UTF-8 的 3 个字节补位规则是 :
1110xxxx 10xxxxxx 10xxxxxx
- 首先把 200013 转二进制,那就是
01001110 00101101
- 然后就将 200013 的二进制以前到后的顺序依次放到 3 个字节的部位空间(也就是
x
的位置) - 放入部位中之后,我们获得 11100100 10111000 10101101 ,这里面加粗的部分就是原本 200013 二进制的部分。
- 最后我们把转换出来的
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
{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
2028
和2029
就是对应的分段
和分页
x
和u
两种转义方法- 当然这个我们是不需要死记住的,只要知道
bfnrtv
这几种特殊的字符,还有上面考虑到的因素即可。
布尔类型
其实就是 true
和 false
, 这个类型真的是非常简单的类型,如果没有和计算联合起来用,就真的是一个很简单的类型。
Null 和 Undefined
这两个类型都是大家日常会接触的,其实都表示空值。不同的是:
Null
表示有值,但是是空Undefined
语义上就表示根本没有人去设置过这个值,所以就是没有定义
我们要注意 Null
其实是关键字 ,但是 Undefined
其实并不是关键字。
Undefined 是一个全局变量,在早期的 JavaScript 版本里全局的变量我们还可以给他重新赋值的。比如我们把 Undefined
赋值成 true,最后造成了一大堆地方出问题了。但是大家一般都没有那么顽皮,这么顽皮的人一般都被公司开掉了哈。
代码语言:javascript复制虽然说新版本的 JavaScript 无法改变全局的
Undefined
的值,但是在局部函数领域中,我们还是可以改变Undefined
的值的。例如以下例子:
function foo() {
var undefined = 1;
console.log(undefined);
}
那么 null
是一个关键字,所以它就没有这一类的问题,如果我们给 Null 赋值它就会报错了。
function foo() {
var null = 0;
console.log(null);
}
这里就说一下,我们怎么去表示
Undefined
是最安全的呢?在开发的过程中我们一般不用全局变量,我们会用void 0
来产生Undefined
,因为void
运算符是一个关键字,void
后面不管跟着什么,他都会把后面的表达式的值变成Undefined
这个值。那么void 0
、void 1
、void
的一切都是可以的,一般我们都会写void 0
,因为大家都这么写,大家说一样的话比较能够接受。
小总结
我们还有一个 Symbol 和 Object 还没有讲到,这个就结合在一起在一篇文章中一起讲了。敬请期待!