随书笔录
Published in:2023-10-28 |

随书笔录

什么是JavaScript

JavaScrip刚问世的主要用途

1995 年,JavaScript 问世。当时,它的主要用途是代替 Perl 等服务器端语言处理输入验证。在此之 前,要验证某个必填字段是否已填写,或者某个输入的值是否有效,需要与服务器的一次往返通信

JaveScript实现

完整的JavaScript实现包含以下三个部分

  1. 核心(ECMAScript)

  2. 文档对象模型(DOM)

  3. 浏览器对象模型(BOM)

ECMAScript

ECMAScript,即 ECMA-262 定义的语言,并不局限于浏览器,这门语言没有输入和输出之类的方法,ECMA-262 将这门语言作为一个基准来定义,以便在它之上再构建更稳健的脚本语言,Web 浏览器只是 ECMAScript 实现可能存在的一种宿主环境,宿主环境提供ECMAScript 的基准实现和与环境自身交互必需的扩展,扩展(比如 DOM)使用 ECMAScript 核心类型和语法,提供特定于环境的额外功能

不涉及浏览器的话,ECMA-262定义了什么,如下

语法,语句,关键字,保留字,操作符,类型,全局对象

ECMAScript 只是对实现这个规范描述的所有方面的一门语言的称呼

DOM

文档对象模型(DOM,Document Object Model)是一个应用编程接口(API),用于在 HTML 中使用扩展的 XML。DOM 将整个页面抽象为一组分层节点。HTML 或 XML 页面的每个组成部分都是一种节点,包含不同的数据。

​ DOM 通过创建表示文档的树,让开发者可以随心所欲地控制网页的内容和结构

为什么DOM是必须的?

在 IE4 和 Netscape Navigator 4 支持不同形式的动态 HTML(DHTML)的情况下,开发者首先可以做到不刷新页面而修改页面外观和内容。

但当时由于网景和微软采用不同的思路开发DHTML,开发者写一个HTML页面就可以在任何浏览器上运行的好日子就GG了,为了保持Web跨平台的本性,避免网景和微软各搞各的导致Web分裂,让开发者面向浏览器开发网页,所以万维网联盟(W3C)开始了制定DOM标准的进程

BOM

浏览器对象模型(BOM) API,用于支持访问和操作浏览器的窗口。BOM独一无二也是问题最多的就是它是唯一一个没有相关标准的JavaScript实现。

BOM主要针对浏览器窗口和子窗口,通常会把任何特定于浏览器的扩展都归在BOM的范畴内,如下所示一部分扩展

1.弹出新浏览器窗口的能力;

2.移动、缩放和关闭浏览器窗口的能力;

3.navigator 对象,提供关于浏览器的详尽信息;

4.location 对象,提供浏览器加载页面的详尽信息;

5.screen 对象,提供关于用户屏幕分辨率的详尽信息;

6.performance 对象,提供浏览器内存占用、导航行为和时间统计的详尽信息;

7.对 cookie 的支持;

8.其他自定义对象,如 XMLHttpRequest 和 IE 的 ActiveXObject

小结

JavaScript 是一门用来与网页交互的脚本语言,包含以下三个组成部分。

1.ECMAScript:由 ECMA-262 定义并提供核心功能。

2.文档对象模型(DOM):提供与网页内容交互的方法和接口。

3.浏览器对象模型(BOM):提供与浏览器交互的方法和接口。

HTML中的JavaScript

<script>元素

将 JavaScript 插入 HTML 的主要方法是使用<script>元素。<script>元素有下列 8 个属性。

1.async:可选。表示应该立即开始下载脚本,但不能阻止其他页面动作,比如下载资源或等待其 他脚本加载,只对外部脚本有效

2.charset:可选。使用 src 属性指定的代码字符集。

3.crossorigin:可选。配置相关请求的CORS(跨源资源共享)设置。默认不使用CORS

4.defer:可选。表示脚本可以延迟到文档完全被解析和显示之后再执行。只对外部脚本有效

5.language:废弃,最初用来表示代码块中的脚本语言,大多数浏览器都会忽略这个属性,不应该再使用它。

6.src:可选。表示包含要执行的代码的外部文件。

7.type:可选。代替 language,表示代码块中脚本语言的内容类型(也称 MIME 类型)。

8.integrity:可选。允许比对接收到的资源和指定的加密签名以验证子资源完整性,如果接收到的资源的签名与这个属性指定的签名不匹配,则页面会报错,脚本不会执行。这个属性可以用于确保内容分发网络,不会提供恶意内容。

使用<script>的方式,通过它直接在网页中嵌入 JavaScript 代码,以及通过它在网页中包含外部 JavaScript 文件

标签位置

过去,所有<script>元素都被放在页面的<head>标签内,这种做法的主要目的是把外部的 CSSJavaScript 文件都集中放到一起。但这样就必须把所有的JavaScript代码都下载,解析和解释完成后才能渲染页面,对于需要很多JavaScript的页面,这会导致页面渲染明显延迟,在此期间页面空白,所以现在都是将JavaScript引用放在<body>元素中的最下面,这样页面会在处理JavaScript代码之前渲染

推迟执行脚本

<Scrippt>元素定义了一个叫 defer 的属性。这个属性表示脚本在执行的时候会被延迟到整个页面都解析完毕后再运行。因此,在 <Scrippt>元素上设置 defer 属性,相当于告诉浏览器立即下载,但延迟执行。

异步执行脚本

<Scrippt>元素定义了一个叫 async的属性。添加这个属性的目的是告诉浏览器不必等到脚本下载和执行完后在加载页面,同样也不必等到该异步脚本下载和执行后再加载其他脚本。

动态加载脚本

因为 JavaScript 可以使用 DOM API,所以通过向 DOM 中动态添加 script 元素同样可以加载指定的脚本。

1
2
3
4
//代码示例
let script = document.createElement('script');
script.src = 'gibberish.js';
document.head.appendChild(script);

XHTML中的变化

可扩展超文本标记语言(XHTML)是将HTML作为XML的应用重新包装的结果。

XHTML 中使用 JavaScript 必须指定 type 属性且值为text/javascript,HTML 中则可以没有这个属性。

在 HTML 中,解析<script>元素会应用特殊规则。XHTML 中则没有这些规则。这意味着 a < b语句中的小于号(<)会被解释成一个标签的开始,并且由于作为标签开始的小于号后面不能有空格,这会导致语法错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//避免这种语法错误的方法有两种

//第一种,把所有的小于号都替换成对应的HTML实体形式,以下是示例
function compare(a, b) {
if (a &lt; b) {
console.log("A is less than B");
} else if (a > b) {
console.log("A is greater than B");
} else {
console.log("A is equal to B");
}
}
</script>

//第二种方法是把所有代码都包含到一个 CDATA 块中。在 XHTML(及 XML)中,CDATA 块表示文档中可以包含任意文本的区块,其内容不作为标签来解析,因此可以在其中包含任意字符,包括小于号,并且不会引发语法错误。以下是示例代码
<script type="text/javascript"><![CDATA[
function compare(a, b) {
if (a < b) {
console.log("A is less than B");
} else if (a > b) {
console.log("A is greater than B");
} else {
console.log("A is equal to B");
}
}
]]></script>

文档模式

IE5.5 发明了文档模式的概念,可以使用 doctype 切换文档模式,最初的文档模式有两种:混杂模式(quirks mode)和标准模式(standards mode)。随着浏览器的普遍实现,又出现了第三种文档模式:准标准模式(almost standards mode)。混杂与标准模式模式的主要区别只体现在通过 CSS 渲染的内容方面,但对JavaScript 也有一些关联影响。

<noscript>元素

针对早期浏览器不支持 JavaScript 的问题,需要一个页面优雅降级的处理方案,最终noscript元素出现,用于给不支持 JavaScript 的浏览器提供替代内容,如下代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html> 
//浏览器不支持脚本的情况会显示noscript元素中的内容
//浏览器对脚本的支持被关闭的情况会显示noscript元素中的内容
<html>
<head>
<title>Example HTML Page</title>
<script defer="defer" src="example1.js"></script>
<script defer="defer" src="example2.js"></script>
</head>
<body>
<noscript>
<p>This page requires a JavaScript-enabled browser.</p>
</noscript>
</body>
</html>

语言基础

语法

ECMAScript 的语法很大程度上借鉴了 C 语言和其他类 C 语言,如 Java 和 Perl。

区分大小写

ECMAScript 中一切都区分大小写。无论是变量、函数名还是操作符,都区分大小写。

标识符

标识符,就是变量、函数、属性或函数参数的名称,标识符可以由一或多个下列字符组成,第一个字符必须是一个字母、下划线(_)或美元符号($);剩下的其他字符可以是字母、下划线、美元符号或数字

语句

ECMAScript 中的语句以分号结尾。省略分号意味着由解析器确定语句在哪里结尾

关键字与保留字

ECMA-262 描述了一组保留的关键字,这些关键字有特殊用途,比如表示控制语句的开始与结束,或者执行特定的操作,保留的关键字不能用作标识符或属性名,ECMA-262 第 6 版规定的所有关键字如下:

1
2
3
4
5
6
7
8
9
break  do  in  typeof 
case else instanceof var
catch export new void
class extends return while
const finally super with
continue for switch yield
debugger function this
default if throw
delete import try

规范字也描述了一组保留字,同样不能作为标识符或属性名,虽然保留字在语言中没有特定用途,但它们是保留给将来作关键字用(相当于关键字预备役),以下是 ECMA-262 第 6 版为将来保留的所有词汇。

1
2
3
4
5
6
7
8
9
10
11
12
//始终保留: 
enum

//严格模式下保留:

implements package public
interface protected static
let private

//模块代码中保留:
await

变量

ECMAScript 变量是松散类型的,意思是变量可以用于保存任何类型的数据。有 3 个关键字可以声明变量:var、const 和 let。var 在ECMAScript 的所有版本中都可以使用,而 const 和 let 只能在 ECMAScript 6 及更晚的版本中使用。

var关键字

使用 var 时,下面的代码不会报错。这是因为使用这个关键字声明的变量会自动提升到函数作用域顶部:

1
2
3
4
5
6
7
8
9
10
11
12
function foo() { 
console.log(age);
var age = 26;
}
foo(); // undefined
//之所以不会报错,是因为 ECMAScript 运行时把它看成等价于如下代码:
function foo() {
var age;
console.log(age);
age = 26;
}
foo(); // undefined

这就是var的声明提升,也就是把所有变量声明都拉到函数作用域的顶部,此外,反复多次使用 var 声明同一个变量也没有问题

let声明

let和var基本差不多,但有着非常重要的区别。最明显的就是let 声明的范围是块作用域,var 声明的范围是函数作用域

暂时性死区

let 与 var 的另一个重要的区别,就是 let 声明的变量不会在作用域中被提升

在解析代码时,JavaScript 引擎也会注意出现在块后面的 let 声明,只不过在此之前不能以任何方 式来引用未声明的变量

在 let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone)

全局声明

与 var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性(var 声明的变量则会)。

for循环中的let

for循环如果用var声明起始变量,那么迭代变量会渗透到循环体外部,用let就没这个问题,因为迭代变量被限制在for循环块内部

用let声明迭代变量时,JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量。

const声明

const 的行为与 let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改 const 声明的变量会导致运行时错误。

const 声明的限制只适用于它指向的变量的引用,如果 const 变量引用的是一个对象,那么修改这个对象内部的属性并不违反 const 的限制

数据类型

ECMAScript 有 7种简单数据类型(也称为原始类型):

1.undefined:表示未定义

2.boolean:表示值为布尔值

3.string:表示值为字符串

4.number:表示值为数值

5.null:空对象指针

6.Symbol:独一无二的值,es6新增的

7.Bigint:大整数,能够表示超过 Number 类型大小限制的整数,ES 2020新增

引用数据类型:

Object:对象,Array/数组 和 function/函数 也属于对象的一种

typeof操作符

因为 ECMAScript 的类型系统是松散的,所以需要一种手段来确定任意变量的数据类型。typeof操作符就是为此而生的。

typeof 操作符的具体实现是由 JavaScript 解释器或引擎完成的,应该是通过类型特征来判断类型然后返回

typeof 是一个操作符而不是函数,所以不需要参数(但可以使用参数)。

Undefined类型

Undefined 类型只有一个值,就是特殊值 undefined。当使用 var 或 let 声明了变量但没有初始化时,就相当于给变量赋予了 undefined

Null类型

Null 类型同样只有一个值,即特殊值 null。逻辑上讲,null 值表示一个空对象指针,这也是给typeof 传一个 null 会返回object的原因

undefined 值是由 null 值派生而来的,因此 ECMA-262 将它们定义为表面上相等,用等于操作符(==)比较 nullundefined 始终返回 true

Boolean类型

Boolean(布尔值)类型是 ECMAScript 中使用最频繁的类型之一,有两个字面值:true 和 false。

虽然布尔值只有两个,但所有其他 ECMAScript 类型的值都有相应布尔值的等价形式。可以调用Boolean()转型函数转换成相应布尔值,下表总结了不同类型与布尔值转换的转换规则

数据类型 true false
Boolean true false
String 非空字符串 “ ”(空字符串)
Number 非零数值(包括无穷值) 0,NaN
Object 任意对象 null
Undefined N/A(不存在) undefined

Number类型

Number 类型使用 IEEE 754 格式表示整数和浮点值,某些语言也叫做双精度值,不同的数值类型相应地也有不同的数值字面量格式。

浮点值

要定义浮点值,数值中必须包含小数点,而且小数点后面必须至少有一个数字。虽然小数点前面不是必须有整数,但推荐加上。

因为存储浮点值使用的内存空间是存储整数值的两倍,所以 ECMAScript 总是想方设法把值转换为整数,在小数点后面没数字的情况,数值就会变成整数,小数后面跟着0(如1.0)也会被转成整数,对于非常大或非常小的数值,浮点值可以用科学记数法来表示

浮点值的精确度最高可达 17 位小数,但在算术计算中远不如整数精确

例如0.1加上0.2得到的并不是0.3.而是0.300 000 000 000 000 04。由于这种微小的舍入错误,导致很难测试特定的浮点值。

之所以存在这种舍入错误,是因为使用了 IEEE 754 数值,这种错误并非 ECMAScript所独有。其他使用相同格式的语言也有这个问题。

值的范围

由于内存限制ECMAScript并不能表示世界上所有数值,它可以表示的最小值保存在Number.MIN_VALUE 中,这个值在多数浏览器中是 5e-324,最大值保存在Number.MAX_VALUE中,这个值在多数浏览器中是 1.797 693 134 862 315 7e+308。如果数值超过了JavaScript表示的范围,那么这个数值会自动转换为一个特殊的Infinity(无穷)值,任何无法表示的负数以-Infinity(负无穷大)表示,任何无法表示的正数以 Infinity(正无穷大)表示。

如果计算返回正或者负Infinity,该值不能再进一步用于计算,因为它没有可用于计算的数值表示形式

NaN

NaN是一个特殊的数值,意思是不是数值(Not a Number),用于表示本来要返回数值的操作失败了(而不是抛出错误)。

NaN 有几个独特的属性,首先任何涉及 NaN 的操作始终返回 NaN(如 NaN/10),其次,NaN 不等于包括 NaN 在内的任何值。

ECMAScript 提供了 isNaN()函数。该函数接收一个参数,可以是任意数据类型,然后判断这个参数是否“不是数值”。

数值转换

有三个函数可以将非数值转换为数值,如下

1
2
3
Number()  //Number()是转型函数,可用于任何数据类型。
parseInt()//主要用于将字符串转换为数值
parseFloat()//主要用于将字符串转换为数值

Number( )基于以下规则执行转换

1.布尔值,true 转换为 1,false 转换为 0。

2.数值,直接返回。

3.null,返回 0。

4.undefined,返回 NaN

5.字符串转换规则如下

1
2
3
4
5
6
//如果字符串包含数值字符,数值字符前面有加减号的情况下则转换为十进制数值
Number('1') // 返回1
Number('011') // 返回11,忽略前面的0
Number('1.1') //浮点数,返回1.1,也会忽略前面的0
Number('') //空字符串返回0
Number('abc') //返回NaN,如果字符串包含除上述情况之外的其他字符,则返回 NaN。

6.对象,调用 valueOf()方法,并按照上述规则转换返回的值。如果转换结果是 NaN,则调用toString方法,再按照转换字符串的规则转换。

由于Number函数转换字符串相对比较复杂,所以一般在需要得到整数的时候可以优先使用parseInt,parseInt函数更专注于字符串是否包含数值,字符串最前面的空格会被忽略,从第一个非空字符开始转换,如果第一个字符不是数值字符或者加号或减号,parseInt会立马返回NaN,空字符串也会返回NaN,这一点和Number函数不一样(Number会返回0),第一个字符是数值字符或加减号,则会依次往下检测,直到字符末尾或者碰到非数值字符示例如下

1
2
parseInt('1234blue') //返回1234
parseInt(1.6) //返回1,小数点不是有效整数字符

不同的数值格式很容易混淆,因此 parseInt()也接收第二个参数,用于指定底数(进制数)。示例如下

1
2
3
4
let num1 = parseInt("10", 2); // 2,按二进制解析
let num2 = parseInt("10", 8); // 8,按八进制解析
let num3 = parseInt("10", 10); // 10,按十进制解析
let num4 = parseInt("10", 16); // 16,按十六进制解析

不传底数参数相当于让 parseInt()自己决定如何解析,所以为避免解析出错,建议始终传给它第二个参数。

parseFloat函数的工作方式跟 parseInt函数类似,都是从第一个非空字符开始转换,但parseFloat函数是解析到字符末尾或者无效的浮点数值字符为止,这意味这第一次出现小数点是可以解析的,第二次出现的小数点就无效,此时字符串的剩余字符都被忽略

parseFloat函数还有一个不同之处就是它会始终忽略字符串开头的0,parseFloat()只解析十进制值,因此不能指定底数。解析十六进制数值始终会返回0,如果字符串表示整数(没有小数点或者小数点后面只有一个零),则 parseFloat()返回整数。parseFloat使用实例如下

1
2
3
4
5
6
let num1 = parseFloat("1234blue"); // 1234,按整数解析
let num2 = parseFloat("0xA"); // 0
let num3 = parseFloat("22.5"); // 22.5
let num4 = parseFloat("22.34.5"); // 22.34
let num5 = parseFloat("0908.5"); // 908.5
let num6 = parseFloat("3.125e7"); // 31250000

String类型

String(字符串)数据类型表示零或多个 16 位 Unicode 字符序列

字符字面量

字符串数据类型包含一些字符字面量,用于表示非打印字符或有其他用途的字符,如下表所示:

字面量 含义
\n 换行
\t 制表
\b 退格
\r 回车
\f 换页
\\ 反斜杠(\)
' 单引号(’),在字符串以单引号标示时使用,例如’He said, 'hey.'‘
" 双引号(”),在字符串以双引号标示时使用,例如”He said, "hey."“
\xnn 以十六进制编码 nn 表示的字符(其中 n 是十六进制数字 0~F),例如\x41 等于”A”
\unnnn 以十六进制编码 nnnn 表示的 Unicode 字符(其中 n 是十六进制数字 0~F),例如\u03a3 等于希腊字

这些字符字面量可以出现在字符串中的任意位置,且可以作为单个字符被解释

字符串的长度可以通过其 length 属性获取

如果字符串中包含双字节字符,那么length 属性返回的值可能不是准确的字符数。

字符串的特点

字符串是不可变的,意思是一旦创建,他们的值就不能改变,要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量

转换为字符串

有两种方式将一个值转为字符串。第一种就是使用几乎所有值都有的toString方法,这个方法唯一的用途就是返回当前值的字符串等价物。实例如下

1
2
3
4
let age = 11; 
let ageAsString = age.toString(); // 字符串"11"
let found = true;
let foundAsString = found.toString(); // 字符串"true"

toString()方法可见于数值、布尔值、对象和字符串值,nullundefined 值没有 toString()方法。

toString函数一般不接收任何参数,在数值调用这个方法的时候,它可以接收一个底数参数,toString方法返回值一般十进制字符串表示,而通过传入参数,可以得到数值的二进制、八进制、十六进制,或者其他任何有效基数的字符串表示,示例如下

1
2
3
4
5
6
7

let num = 10;
console.log(num.toString()); // "10"
console.log(num.toString(2)); // "1010"
console.log(num.toString(8)); // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"

当我们不确定一个值是不是null或者undefined,我们可以使用String转型函数,它始终会返回表示相应类型值的字符串。String函数遵循如下规则。

1.如果值有 toString()方法,则调用该方法(不传参数)并返回结果。

2.如果值是 null,返回”null“。

3.如果值是 undefined,返回”undefined“。

示例如下

1
2
3
4
5
6
7
8
9
10

let value1 = 10;
let value2 = true;
let value3 = null;
let value4;

console.log(String(value1)); // "10"
console.log(String(value2)); // "true"
console.log(String(value3)); // "null"
console.log(String(value4)); // "undefined"

因为 nullundefined 没有 toString方法,所以 String方法就直接返回了这两个值的字面量文本。

模板字面量

ES6新增了使用模板字面量定义字符串的能力,模板字面量保留换行字符,可以跨行定义字符串

字符串插值

模板字面量最常用的一个特性是支持字符串插值,示例如下

1
2
3
4
5
6
7
8
9
10
11
let value = 5; 
let exponent = 'second';
// 以前,字符串插值是这样实现的:
let interpolatedString =
value + ' to the ' + exponent + ' power is ' + (value * value);
// 现在,可以用模板字面量这样实现:
let interpolatedTemplateLiteral =
`${ value } to the ${ exponent } power is ${ value * value }`;

console.log(interpolatedString); // 5 to the second power is 25
console.log(interpolatedTemplateLiteral); // 5 to the second power is 25

所有插入的值都会使用 toString()强制转型为字符串,而且任何 JavaScript 表达式都可以用于插值

将表达式转换为字符串时会调用 toString(),在插值表达式中可以调用函数和方法

模板字面量标签函数

通过标签函数可以自定义插值行为。标签函数会接收被插值记号分隔后的模板和对每个表达式求值的结果。

代码实例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let a = 1;
let b = 2;
// 定义函数simpleTag,接收四个参数strings、aValExpression、bValExpression和sumExpression
function simpleTag(strings, aValExpression, bValExpression, sumExpression) {
// 打印strings的值
//strings打印出来为["", " + ", " = ", ""] 为表达式`${ a }${ a }之前和 + ${ b } = ${ a + b }`插值之间的分隔符组成的数组,有符号就取符号,没有就去空字符串,所以数组第一个和最后一个是空字符串,因为${ a }之前和${ a + b }之后没有符号
console.log(strings);
// 打印a的值
console.log(aValExpression);
// 打印b的值
console.log(bValExpression);
// 打印a和b的和的值
console.log(sumExpression);
// 返回字符串'foobar'
return 'foobar';
}

// 定义变量untaggedResult,使用模板字符串的方式将a、b和a+b的值拼接成字符串
let untaggedResult = `${ a } + ${ b } = ${ a + b }`;
// 定义变量taggedResult,使用函数simpleTag将a、b和a+b的值拼接成字符串
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;
// 打印untaggedResult的值
// 预期输出: "1 + 2 = 3"
console.log(untaggedResult);
// 打印taggedResult的值
// 预期输出: "foobar"
console.log(taggedResult);

原始字符串

Symbol类型

SymbolES6新增的数据类型,符号是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

符号的基本用法

符号需要使用 Symbol()函数初始化。因为符号本身是原始类型,所以 typeof 操作符对符号返回symbol

1
2
let sym = Symbol(); 
console.log(typeof sym); // symbol

调用 Symbol()函数时,也可以传入一个字符串参数作为对符号的描述(description),将来可以通过这个字符串来调试代码,,这个字符串参数与符号定义或标识完全无关:

1
2
3
4
5
6
7
let genericSymbol = Symbol(); 
let otherGenericSymbol = Symbol();

let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');

console.log(genericSymbol == otherGenericSymbol); // false

Symbol()函数不能与 new 关键字一起作为构造函数使用

使用全局符号注册表

如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局符号注册表中创建并重用符号。

Symbol.for()对每个字符串键都执行幂等操作(获取全局注册表中与指定字符串键的symbol值,同一个字符串键多次调用symbol.for,返回相同的值,这个过程被称为幂等操作)。

第一次使用某个字符串调用时,它会检查全局运行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。

1
2
let fooGlobalSymbol = Symbol.for('foo'); // 创建新符号
let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号

即使符号描述相同,在全局注册表中定义的符号与使用symbol定义的符号也不相同:

1
2
3
let localSymbol = Symbol('foo'); 
let globalSymbol = Symbol.for('foo');
console.log(localSymbol === globalSymbol); // false

全局注册表中的符号必须使用字符串键来创建,因此作为参数传给 Symbol.for()的任何值都会被转换为字符串。

使用 Symbol.keyFor()来查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询的不是全局符号,则返回 undefined

1
2
3
4
5
6
// 创建全局符号
let s = Symbol.for('foo');
console.log(Symbol.keyFor(s)); // foo
// 创建普通符号
let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined
使用符号作为属性

凡是可以使用字符串或数值作为属性的地方,都可以使用符号。对象字面量只能在计算属性语法中使用符号作为属性。

1
2
3
4
5
6
7
8
9
10
let s1 = Symbol('foo'), 
s2 = Symbol('bar'),
s3 = Symbol('baz'),
s4 = Symbol('qux');
let o = {
[s1]: 'foo val'
};
// 这样也可以:o[s1] = 'foo val';
console.log(o);
// {Symbol(foo): foo val}

Object类型

ECMAScript 中的对象其实就是一组数据和功能的集合。对象通过 new 操作符后跟对象类型的名称来创建。开发者可以通过创建 Object 类型的实例来创建自己的对象,然后再给对象添加属性和方法

每个Object实例都有如下属性和方法:

1.constructor:用于创建当前对象的函数

2.hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串(如 o.hasOwnProperty("name"))或符号。

3.isPrototypeOf(*object*):用于判断当前对象是否为另一个对象的原型。

4.propertyIsEnumerable(*propertyName*):用于判断给定的属性是否可以使用for-in 语句枚举,与 hasOwnProperty()一样,属性名必须是字符串。

5.toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地化执行环境。

6.toString():返回对象的字符串表示。

7.valueOf():返回对象对应的字符串、数值或布尔值表示。通常与 toString()的返回值相同。

因为在 ECMAScript 中 Object 是所有对象的基类,所以任何对象都有这些属性和方法。

操作符

ECMA-262 描述了一组可用于操作数据值的操作符,包括数学操作符(如加、减)、位操作符、关系操作符和相等操作符等。在应用给对象时,操作符通常会调用 valueOf()/toString()方法来取得可以计算的值。

一元操作符

只操作一个值的操作符叫一元操作符

递增/递减操作符

递增和递减操作符直接照搬自 C 语言,但有两个版本:前缀版和后缀版。

前缀版就是把操作符放变量前面,后缀版则相反

前缀版无论是递增还是递减,变量的值都会在语句被求值之前改变,示例如下

1
2
3
4
5
6
7
8
let age = 29; 
let anotherAge = --age + 2;

console.log(age); // 28
console.log(anotherAge); // 30

//变量 anotherAge 以 age 减 1 后的值再加 2 进行初始化。因为递减操作先发生,
//所以 age 的值先变成 28,然后再加 2,结果是 30。

而后缀版递增和递减是在语句被求值后才发生,示例如下

1
2
3
4
5
let age = 29;
let anotherAge = age-- + 2; //此时age仍是29

console.log(age); // 28
console.log(anotherAge); // 31

4 个操作符可以作用于任何值,意思是不限于整数——字符串、布尔值、浮点值,甚至对象都可以。递增和递减操作符遵循如下规则。

1.对于字符串,如果是有效的数值形式,则转换为数值再应用改变。变量类型从字符串变成数值。

2.对于字符串,如果不是有效的数值形式,则将变量的值设置为 NaN 。变量类型从字符串变成数值。

3.对于布尔值,如果是 false,则转换为 0 再应用改变。变量类型从布尔值变成数值。

4.对于布尔值,如果是 true,则转换为 1 再应用改变。变量类型从布尔值变成数值。

5.对于浮点值,加 1 或减 1。

6.如果是对象,则调用其valueOf()方法取得可以操作的值。对得到的值应用上述规则。如果是 NaN,则调用 toString()并再次应用其他规则。变量类型从对象变成数值。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let s1 = "2"; 
let s2 = "z";
let b = false;
let f = 1.1;
let o = {
valueOf() {
return -1;
}
};
s1++; // 值变成数值 3
s2++; // 值变成 NaN
b++; // 值变成数值 1
f--; // 值变成 0.10000000000000009(因为浮点数不精确)
o--; // 值变成-2
一元加和减

一元加由一个加号(+)表示,放在变量前头,对数值没有任何影响

如果将一元加应用到非数值,则会执行与使用 Number()转型函数一样的类型转换:布尔值 false和 true 转换为 0 和 1,字符串根据特殊规则进行解析,对象会调用它们的 valueOf()和/toString()方法以得到可以转换的值。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let s1 = "01"; 
let s2 = "1.1";
let s3 = "z";
let b = false;
let f = 1.1;
let o = {
valueOf() {
return -1;
}
};
s1 = +s1; // 值变成数值 1
s2 = +s2; // 值变成数值 1.1
s3 = +s3; // 值变成 NaN
b = +b; // 值变成数值 0
f = +f; // 不变,还是 1.1
o = +o; // 值变成数值-1

一元减由一个减号(-)表示,放在变量前头,主要用于把数值变成负值,如把 1 转换为-1。对数值使用一元减会将其变成相应的负值,当应用到非数值时,一元减会遵循和一元加同样的规则,先进行转换然后再取负值,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let s1 = "01"; 
let s2 = "1.1";
let s3 = "z";
let b = false;
let f = 1.1;
let o = {
valueOf() {
return -1;
}
};
s1 = -s1; // 值变成数值-1
s2 = -s2; // 值变成数值-1.1
s3 = -s3; // 值变成 NaN
b = -b; // 值变成数值 0
f = -f; // 变成-1.1
o = -o; // 值变成数值 1

一元加和减操作符主要用于基本的算术,但也可以像上面的例子那样,用于数据类型转换。

位操作符

用于数值的底层操作,操作内存中表示数据的比特(位)。

ECMAScript中所有数值都是64位格式存储,但位操作并不直接应用到64位表示,而是先转成32位整数,在进行位操作,之后再把结果转成64位,所以我们只需要考虑32位整数即可。

有符号整数使用32位的前31位表示整数值,第32位表示数值的符号。如0表示正,1表示负,这一位被称为符号位,它的值决定数值其余部分的格式:

正值以真正的二进制格式存储,即 31位中的每一位都代表 2 的幂。

负值以一种称为二补数(或补码)的二进制编码存储。

一个数值的二补数通过如下 3 个步骤计算得到:

1.确定绝对值的二进制表示(如,对于-18,先确定 18 的二进制表示)

2.找到数值的一补数(或反码),换句话说,就是每个 0 都变成 1,每个 1 都变成 0;

3.给结果加 1。

默认情况下,ECMAScript 中的所有整数都表示为有符号数。不过,确实存在无符号整数。对无符号整数来说,第 2 位不表示符号,因为只有正值。无符号整数比有符号整数的范围更大,因为符号位被用来表示数值了。

ECMAScript中数值应用位操作符时,后台会进行转换,64位转32位然后执行位操作,在把结果转回64位存储起来,在这个转换过程中有个缺点,特殊值NaNInfinity在位操作中都会被当成 0 处理。所以如果将位操作符应用到非数值,那么首先会使用 Number()函数将该值转换为数值,再应用位操作

按位非

按位非操作符用波浪符(~)表示,它的作用是返回数值的一补数。示例如下

1
2
3
let num1 = 25; // 二进制 00000000000000000000000000011001 
let num2 = ~num1; // 二进制 11111111111111111111111111100110
console.log(num2); // -26

按位非操作符作用到了数值 25,得到的结果是-26。由此可以看出,按位非的最终效果是对数值取反并减 1,就像执行如下操作的结果一样:

1
2
3
let num1 = 25; 
let num2 = -num1 - 1;
console.log(num2); // "-26"

虽然结果一样,但按位非得速度快得多,因为位操作是在数值的底层表示上完成的

按位与

按位与操作符用和号(&)表示,有两个操作数。本质上,按位与就是将两个数的每一个位对齐,然后基于真值表中的规则,对每一位执行相应的与操作。

第一个数值的位 第二个数值的位 结果
1 1 1
1 0 0
0 1 0
0 0 0

按位与操作在两个位都是 1 时返回 1,在任何一位是 0 时返回 0,示例如下

1
2
3
4
5
6
7
8
9
let result = 25 & 3; 
console.log(result); // 1
//25 和 3 的按位与操作的结果是 1。让我们看下面的二进制计算过程:
25 = 0000 0000 0000 0000 0000 0000 0001 1001
3 = 0000 0000 0000 0000 0000 0000 0000 0011
---------------------------------------------
AND = 0000 0000 0000 0000 0000 0000 0000 0001
//如上所示,25 和 3 的二进制表示中,只有第 0 位上的两个数都是 1。于是结果数值的所有其他位都
//会以 0 填充,因此结果就是 1。
按位或

按位或操作符用管道符(|)表示,同样有两个操作数。按位或遵循如下真值表:

第一个数值的位 第二个数值的位 结果
1 1 1
1 0 1
0 1 1
0 0 0

按位或操作在至少一位是 1 时返回 1,两位都是 0 时返回 0。示例如下

1
2
3
4
5
6
7
8
9
let result = 25 | 3; 
console.log(result); // 27

//二进制计算过程
25 = 0000 0000 0000 0000 0000 0000 0001 1001
3 = 0000 0000 0000 0000 0000 0000 0000 0011
---------------------------------------------
OR = 0000 0000 0000 0000 0000 0000 0001 1011
//在参与计算的两个数中,有 4 位都是 1,因此它们直接对应到结果上。二进制码 11011 等于 27
按位异或

按位异或用脱字符(^)表示,同样有两个操作数。下面是按位异或的真值表:

第一个数值的位 第二个数值的位 结果
1 1 0
1 0 1
0 1 1
0 0 0

按位异或与按位或的区别是,它只在一位上是 1 的时候返回 1(两位都是 1 或 0,则返回 0)。示例如下

1
2
3
4
5
6
7
8
9
10
let result = 25 ^ 3; 
console.log(result); // 26

//二进制计算过程如下
25 = 0000 0000 0000 0000 0000 0000 0001 1001
3 = 0000 0000 0000 0000 0000 0000 0000 0011
---------------------------------------------
XOR = 0000 0000 0000 0000 0000 0000 0001 1010
//两个数在 4 位上都是 1,但两个数的第 0 位都是 1,因此那一位在结果中就变成了 0。其余位上的 1在另一个数上没有对应的 1,
//因此会直接传递到结果中。二进制码 11010 等于 26。

这比对同样两个值执行按位或操作得到的结果小 1

左移

左移操作符用两个小于号(<<)表示,会按照指定的位数将数值的所有位向左移动。

比如,如果数值 2(二进制 10)向左移 5 位,就会得到 64(二进制 1000000),如下所示:

1
2
let oldValue = 2; // 等于二进制 10 
let newValue = oldValue << 5; // 等于二进制 1000000,即十进制 64

在移位后,数值右端会空出 5 位。左移会以 0 填充这些空位

左移会保留它所操作数值的符号。比如,如果-2 左移 5 位,将得到-64,而不是正 64

右移

有符号右移由两个大于号(>>)表示,会将数值的所有 32 位都向右移,同时保留符号(正或负)。有符号右移实际上是左移的逆运算。比如,如果将 64 右移 5 位,那就是 2,如下所示

1
2
let oldValue = 64; // 等于二进制 1000000 
let newValue = oldValue >> 5; // 等于二进制 10,即十进制 2

在移位后,数值左端会空出 5 位。右移移会以 0 填充这些空位

无符号右移

无符号右移用 3 个大于号表示(>>>),会将数值的所有 32 位都向右移。对于正数,无符号右移与有符号右移结果相同。仍然以前面有符号右移的例子为例,64 向右移动 5 位,会变成 2:

1
2
let oldValue = 64; // 等于二进制 1000000 
let newValue = oldValue >>> 5; // 等于二进制 10,即十进制 2

无符号右移对于正数来说和有符号右移效果相同,但对于负数来说有时候差异会非常大,无符号右移操作符将负数的二进制表示当成正数的二进制表示来处理,。因为负数是其绝对值的二补数,所以右移之后差异会很大,如下所示:

1
2
let oldValue = -64; // 等于二进制 11111111111111111111111111000000 
let newValue = oldValue >>> 5; // 等于十进制 134217726

布尔操作符

布尔操作符一共有 3 个:逻辑非、逻辑与和逻辑或

逻辑非

逻辑非操作符由一个叹号(!)表示,这个操作符始终返回布尔值,无论应用到什么数据类型,都会先将操作数转换为布尔值,然后对其取反,逻辑非会遵循如下规则:

1.如果操作数是对象,则返回 false

2.如果操作数是空字符串,则返回 true

3.如果操作数是非空字符串,则返回 false

4.如果操作数是数值 0,则返回 true

5.如果操作数是非 0 数值(包括 Infinity),则返回 false

6.如果操作数是 null,则返回 true

7.如果操作数是 NaN,则返回 true

8.如果操作数是 undefined,则返回 true

逻辑非操作符也可以用于把任意值转换为布尔值。同时使用两个叹号(!!),相当于将操作数转为布尔值取反,之后在取反得到真正对于的布尔值

逻辑与

逻辑与操作符由两个和号(&&)表示,应用到两个值,如下所示:

1
let result = true && false;

逻辑与操作符遵循如下真值表:

第一个操作数 第二个操作数 结果
true true true
true false false
false true false
false false false

逻辑与操作符可用于任何类型的操作数,不限于布尔值。如果有操作数不是布尔值,则逻辑与并不一定会返回布尔值,会遵循如下规则:

1.如果第一个操作数是对象,则返回第二个操作数

2.如果第二个操作数是对象,则只有第一个操作数求值为 true 才会返回该对象

3.如果两个操作数都是对象,则返回第二个操作数

4.如果有一个操作数是 null,则返回 null

5.如果有一个操作数是 NaN,则返回 NaN

6.如果有一个操作数是 undefined,则返回 undefined

逻辑与操作符是一种短路操作符,如果第一个操作数决定了结果,就不会对第二个操作数求值,对逻辑与操作符来说,如果第一个操作数是 false,那么无论第二个操作数是什么值,结果也不可能等于 true

逻辑或

逻辑或操作符由两个管道符(||)表示,示例如下:

1
let result = true || false;

逻辑或操作符遵循如下真值表:

第一个操作数 第二个操作数 结果
true true true
true false true
false true true
false false false

逻辑或与逻辑与有点类似,如果有一个操作数不是布尔值,那么逻辑或操作符也不一定返回布尔值,会遵循如下规则。

1.如果第一个操作数是对象,则返回第一个操作数

2.如果第一个操作数求值为 false,则返回第二个操作数

3.如果两个操作数都是对象,则返回第一个操作数

4.如果两个操作数都是 null,则返回 null

5.如果两个操作数都是 NaN,则返回 NaN

6.如果两个操作数都是 undefined,则返回 undefined

逻辑或操作符也具有短路的特性。只不过对逻辑或而言,第一个操作数求值为true,第二个操作数就不会再被求值了

乘性操作符

ECMAScript 定义了 3 个乘性操作符:乘法、除法和取模。在处理非数值时,它们会包含一些自动的类型转换。如果乘性操作符有不是数值的操作数,操作数在后台会被Number( )转型函数转为数值,这意味这空字符串会被当成0,true会被当成1。

乘法操作符

乘法操作符由一个星号(*)表示,可以用于计算两个数值的乘积。示例如下:

1
let num = 87 * 33

乘法操作符在处理特殊值时也有一些特殊行为:

1.如果操作数都是数值,两个正值相乘是正值,两个负值相乘也是正值,正负符号不同的值相乘得到负值。如果 ECMAScript 不能表示乘积,则返回 Infinity-Infinity

2.如果有任一操作数是 NaN,则返回 NaN

3.如果是 Infinity 乘以 0,则返回 NaN

4.如果是 Infinity 乘以非 0的有限数值,则根据第二个操作数的符号返回 Infinity 或-Infinity

5.如果是 Infinity 乘以 Infinity,则返回 Infinity

6.如果有不是数值的操作数,则先在后台用 Number()将其转换为数值,然后再应用上述规则

除法操作符

除法操作符由一个斜杠(/)表示,用于计算第一个操作数除以第二个操作数的商。示例如下:

1
let num = 88 / 72;

除法操作符在处理特殊值时也有一些特殊行为:

1.如果操作数都是数值,两个正值相除是正值,两个负值相除也是正值,符号不同的值相除得到负值。如果ECMAScript不能表示商,则返回Infinity-Infinity

2.如果有任一操作数是 NaN,则返回 NaN

3.如果是 Infinity 除以 Infinity,则返回 NaN

4.如果是 0 除以 0,则返回 NaN

5.如果是非 0 的有限值除以 0,则根据第一个操作数的符号返回 Infinity-Infinity

6.如果是 Infinity 除以任何数值,则根据第二个操作数的符号返回 Infinity-Infinity

7.如果有不是数值的操作数,则先在后台用 Number()将其转换为数值,然后再应用上述规则

取模操作符

取模(余数)操作符由一个百分比符号(%)表示,比如:

1
let result = 26 % 5; // 等于 1

取模操作符在处理特殊值时也有一些特殊行为:

1.如果操作数是数值,则执行常规除法运算,返回余数

2.如果被除数是无限值,除数是有限值,则返回 NaN

3.如果被除数是有限值,除数是 0,则返回 NaN

4.如果是 Infinity 除以 Infinity,则返回 NaN

5.如果被除数是有限值,除数是无限值,则返回被除数。

6.如果被除数是 0,除数不是 0,则返回 0

7.如果有不是数值的操作数,则先在后台用 Number()将其转换为数值,然后再应用上述规则

Prev:
个人博客搭建
Next:
vue3学习文档