JavaScript

对着这个教程进行学习,下面的内容主要是本人总结,不一定到位,仅作理解上的记录。

介绍

首页放着对JavaScript的介绍,实则和说明书一般,似乎是不用读的,读了一遍,确实不用读。

最后介绍了一个JS版本的Hello World代码,这是比较新颖的,分析一下,function定义了一个无名函数,"use strict"使用严格模式,紧接着是一个内部函数greetMe,后面调用传入世界后,浏览器弹出显示“你好世界”的提示框。

1
2
3
4
5
6
7
8
9
10
11
(function () {
"use strict";
//官方推荐加入以进行严格的语法检查避免潜在报错,
/* 代码开始 */
function greetMe(yourName) {
alert(`你好${yourName}`);
}

greetMe("世界");
/* 代码结束 */
})();

语法

注释

简单来说,JS的语法区分大小写,同时一条指令后需要加入;作为分隔,虽然在某些情况可以省略,但好习惯当然可以减少报错。

单个语句独占一行则可以省略分号;单行多条语句则需用分号进行分隔。

注释这块没有什么独特的地方就是了,一眼可以扫过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 单行注释

/* 这是一个更长的,
* 多行注释
*/

/* 然而,你不能,/* 嵌套注释 */ 这会提前结束注释 */
/* 但可以通过转义正斜杠 /* 嵌套注释 *\/ */

// hashbang 注释语法,是一种用于指定执行脚本的特定 Javascript 引擎路径的特殊注释
#!/usr/bin/env node
#! 告诉操作系统,后面指定的是运行这个脚本的程序
/usr/bin/env 一个系统工具,会按环境变量PATH查找程序
node 要查找并使用的程序,即 Node.js

变量与作用域

JavaScript 有三种变量声明方式,var,let,const,区别如下示例所示。
首先是let和const,如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
//let 与 const 仅在对应的代码块{ }内可以访问。
//const 与 let 的区别在于是否可以重写赋值,let可以,const不能。
//如下所示 message 用 let 声明在 if 的 { } 内,所以出了这个代码块,就不能访问它了。
function test() {
if (true) {
let message = "你好";
console.log(message); // 可以访问
}

console.log(message); // 报错:这里访问不到 message
}

test();

对于var来说,访问的边界不再是代码块而是当前函数,倘若在函数外定义,则访问边界更广。

1
2
3
4
5
6
7
8
9
10
11
12
13
// var 不以 if 的代码块为作用域边界,因当前在函数内故以函数为边界。

function test() {
if (true) {
var message = "你好";
}

console.log(message); // 可以访问,输出“你好”
}

test();

console.log(message); // 报错:出了 test 函数就访问不到了

注:应该总是在声明变量后使用它们。JavaScript 过去允许给未声明的变量赋值,而这会创建一个未声明的全局变量。这在严格模式下是一个错误,应该彻底避免使用它。

可以使用解构语法声明用于解包值的变量,如’const { bar } = foo’,这会创建一个名为 bar 的变量,并且将 foo 对象中属性名与之相同的属性的值赋给它。

声明和初始化

let x = 42中的let x是声明,= 42是初始化器。
本质上,let x = 42等价于let x; x = 42。也就是说后赋值是允许的,除了const,const声明总是需要初始化器,必须使用const y = 123,否则将会报错。

变量提升

这是一个比较神奇的点,var声明会被提升,也就是说在进入一个作用域(如函数或者全局)时,var声明会被提到该作用域的顶部,先被声明且赋值undefined,如下示例详解。

1
2
3
4
5
6
7
console.log(x === undefined); // true
var x = 3;

(function () {
console.log(x); // undefined
var x = "局部值";
})();

这份代码由于var声明的提升会被等价于下面的代码。

1
2
3
4
5
6
7
8
9
10
11
//因为进入该域后先执行声明,于是变成var在前,但并不是实际代码内容变化,只是处理逻辑的展示。
var x;
console.log(x === undefined); // true
x = 3;

(function () {
//新的作用域也如此
var x;
console.log(x); // undefined
x = "局部值";
})();

这段代码存在两个x,但变量查找遵循作用域规则:先找当前作用域,找不到再向外找。也就是内部变量始终优先。

最后要说的是,let和const变量不提升,同时因为这个处理顺序,所以var声明越靠前越有利于代码易读性提升。

全局变量

前面提到var的作用域取决于申明的作用域,倘若于代码伊始声明则你会获得一个全局变量,同时浏览器提供了一个叫window的全局对象,你也可以通过window.变量名来访问,示例如下。

1
2
3
4
5
6
<script>
var phoneNumber = "123456";

console.log(phoneNumber); // "123456"
console.log(window.phoneNumber); // "123456"
</script>

但是顶层的let和const不会成为window的属性,下同globalThis,但可以全局访问,且需要遵守作用域规则。

1
2
3
4
5
6
let city = "广州";
const country = "中国";

console.log(city); // "广州"
console.log(window.city); // undefined,假设没有其他同名属性
console.log(window.country); // undefined

浏览器使用window,Node.js没有浏览器的window。为了让代码跨环境使用,可以写globalThis,globalThis是不同环境中访问全局对象的统一入口。

1
2
3
4
5
console.log(globalThis === window); // true

globalThis.message = "你好";

console.log(window.message); // "你好"

最后是子页面访问父页面的属性,这里要提到iframe,可以理解为“网页里嵌入的另一张网页”,分别称父页面和子页面,它们各有自己的 window。引用时在父子页面同源、没有额外沙箱限制的情况下,可以实现以下示例的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 父页面 main.html -->
<script>
var phoneNumber = "123456";
</script>

//嵌套子页面
<iframe src="child.html"></iframe>
-------------------------------------------------------------------
<!-- 子页面 child.html,实际上应该是两份文件 -->
<script>
var phoneNumber = "345678";
console.log(phoneNumber); // "345678"
//window,指子页面自己的窗口。
console.log(window.phoneNumber); // "345678"
//parent,指嵌入它的父窗口。
console.log(parent.phoneNumber); // "123456"
</script>

变量使用规范

不能使用与变量名或函数名相同的名字来声明常量,错误的示例如下。

1
2
3
4
5
6
7
8
9
// 这会造成错误
function f() {}
const f = 5;

// 这也会造成错误
function f() {
const g = 5;
var g;
}

用const声明的变量不能重新赋值,但如果它指向一个对象,这个对象的属性仍然可以修改。

1
2
3
4
// 变量 MY_OBJECT:受 const 限制,不能重新赋值。
const MY_OBJECT = { key: "值" };
// 对象的属性 key:不受这个 const 限制,可以修改。
MY_OBJECT.key = "其他值"; // 不报错

这个规则同样适用于数组。

1
2
3
const MY_ARRAY = ["HTML", "CSS"];
MY_ARRAY.push("JAVASCRIPT");
console.log(MY_ARRAY); // ['HTML', 'CSS', 'JAVASCRIPT'];

数据结构和类型

数据类型

最新的 ECMAScript 标准定义了 8 种数据类型:
七种基本数据类型:

  • Boolean。true和false。
  • null。表示空值的特殊关键字。注意大小写。
  • undefined。一个未定义值的顶级属性。
  • Number。整数或浮点数。例如,42 或 3.14159。
  • BigInt。任意精度的整数。例如,9007199254740992n。
  • String。表示文本值的字符序列。例如,”Howdy”。
  • Symbol。其实例是唯一且不可变的数据类型。
    此外还有Object。
    函数也可以算得一个基本元素,总的来说,数据类型决定你的对象放什么数据集,而函数利用这些对象执行任务。

数据类型的转换

JS是一门动态类型语言,所以在申明时不必指定该变量的数据类型,也就是说脚本会自动根据你存入的数据改变数据类型。

1
2
3
let answer = 42;
\\可以这样先定义为数值,再改变为字符串,这并不会报错。
answer = "不客气,感谢所有的鱼!";

数字和“+”运算符

再带上+的运算符表达式中如果涉及到数字和字符串,JS则会把数字转换为字符串,示例如下。

1
2
3
x = "答案是 " + 42; // "答案是 42"
y = 42 + " 是答案"; // "42 是答案"
z = "37" + 7; // "377"

但在其他运算符使用时,JS不会把数字转换成字符串,示例如下。

1
2
"37" - 7; // 30
"37" * 7; // 259

+的加法用法将在下面讲到。

字符串转换为数字

首先需要讲到,如果想正确使用+,则需要先确保两边都是数字,字符串形式的数字可以使用Number()转换,示例如下。

1
2
3
4
5
const x = "37";
const y = 7;

console.log(x + y); // "377":字符串拼接
console.log(Number(x) + y); // 44:数值相加

但需要注意顺序为先转换后相加,否则。

1
2
Number("37") + Number("7"); // 44 ✅
Number("37" + "7"); // 377:先拼接成 "377",再转成数字

你也可以使用parseInt()和parseFloat(),它们会提取整个字符串开始的数字,遇到非数字则停止。其中parseInt()只返回整数,因此它在处理小数时用途有限,示例如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Number():转换整个字符串
Number("37"); // 37
Number("37.8"); // 37.8
Number("37.8元"); // NaN
Number("你好37"); // NaN
Number(""); // 0

// parseInt():从开头解析整数,10 表示十进制
parseInt("37", 10); // 37
parseInt("37.8", 10); // 37
parseInt("37.8元", 10); // 37
parseInt("你好37", 10); // NaN
parseInt("", 10); // NaN

// parseFloat():从开头解析数字,可以包含小数
parseFloat("37"); // 37
parseFloat("37.8"); // 37.8
parseFloat("37.8元"); // 37.8
parseFloat("你好37"); // NaN
parseFloat(""); // NaN

字符传转换为数字的另一种方法则是使用+本身,示例如下。

1
2
3
"1.1" + "1.1" = "1.11.1"
(+"1.1") + (+"1.1") = 2.2
// 注意:括号是为了清晰起见而添加的,不是必需的。

字面量

简单来说就是赋值时=后你希望变量应该被赋予的值,如let a = 18,18就是数字字面量。常见的字面量如下。

  • 数组字面量
  • 布尔字面量
  • 数字字面量
  • 对象字面量
  • RegExp 字面量
  • 字符串字面量
    按顺序进行解释如后。

数组字面量

JS的数组满足常见语言中的数组定义形式,所以字面量由一对方括号[]括起来的包含零个或多个表达式的列表,每个表达式代表一个数组元素。使用数组字面量创建数组时,会以你指定的值进行初始化,数组的length指定为数组的元素个数。

1
2
// 这个示例展示了一个含有3个元素的 coffees 数组,其长度为3。
const coffees = ["French Roast", "Colombian", "Kona"];

但需要注意的是,在不同位置执行数组字面量时,会出现读取或新建的不同操作,示例如下。

1
2
3
4
5
6
7
8
9
10
// 对于这个示例来说,因为字面量创建于函数外,↓
// 所以两次show都只是读取字面量没有赋值,所以没有新创建数组。
const numbers = [1, 2, 3];

function show() {
console.log(numbers);
}

show();
show();

第二个示例如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 对于这个示例来说,因为字面量创建于函数内,↓
// 所以每次调用时都会从头执行新建一个数组,分别返回给a和b。
function createArray() {
const numbers = [1, 2, 3];
return numbers;
}

const a = createArray();
const b = createArray();

console.log(a); // [1, 2, 3]
console.log(b); // [1, 2, 3]
console.log(a === b); // false:内容相同,但不是同一个数组

简单理解就是每次只要执行[1,2,3]字面量操作,就是在新建数组,而函数被调用两次则分别新建了两个不同的局部变量numbers,所以也不能简单看作是同一个函数中出现两次对同一个变量的定义。

可由此判断数据是“每次重新开始”,还是“保留并共享之前的修改”,避免程序出现意外结果。

如果你在数组字面量中连续放置两个逗号,数组会为未指定的元素留下一个空槽;而如果在元素列表的尾部添加了一个逗号,这个逗号不会增加元素。如下面示例所展示,但尾部逗号可以保持代码形式整齐。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const fish = ["Lion", , "Angel"];


console.log(fish);
// 打印时显示[ 'Lion', <1 empty item>, 'Angel' ]
// 但需要注意:这个位置没有元素”和“读取这个位置得到什么值”时两件事 ↓
// 1,打印数组时这个位置没有元素会显示“empty”。
// 2. 但索引访问 fish[1] 时会返回 undefined。
// 3. 当使用数组遍历方法(例如,Array.prototype.map)时,空槽会被跳过。

const a = ["Lion", "Angel"];
const b = ["Lion", "Angel",];
const c = ["Lion", "Angel",,];

console.log(a.length); // 2
console.log(b.length); // 2
console.log(c.length); // 3

布尔字面量

很常见的true和false.

数字字面量

JavaScript 数字字面量包括多种进制的整数字面量和仅十进制的浮点数字面量。

  • 十进制整数字面量由数字序列组成,且没有前缀 0(零)。
  • 八进制的整数字面量以 0(或 0O、0o)开头,只能包括数字 0-7。
  • 十六进制整数字面量以 0x(或 0X)开头,可以包含数字(0-9)和字母 a-f 或 A-F。(字符的大小写不影响数值。因此,0xa = 0xA = 10 和 0xf = 0xF = 15。)
  • 二进制整数字面量以 0b(或 0B)开头,只能包含数字 0 和 1。
  • BigInt 由整数字面量和 n 后缀组成。BigInt 也可以使用上面的基数。注意前缀零的八进制语法(例如,0123n)是不允许的,但 0o123n 是可以的。

浮点数字面量

由这些部分组成:

  • 一个无符号的十进制整数
  • 小数点(.)
  • 小数部分(另一个十进制数)
  • 指数部分(指数部分以 e 或 E 开头,后面跟着一个整数,这个整数可以有正负号(即前缀 + 或 -)

浮点数字面量至少有一位数字,而且必须带小数点或者 e(E),示例如下。

1
2
3
4
3.1415926
.123456789
3.1E+12
.1e-23

对象字面量

对象字面量是由一对花括号({})括起来的包含零个或多个属性名和相关值的列表。

但建议不要在一条语句的开头使用对象字面量,这时{可能会被识别为一个语句块的起始。

下面是简单的对象字面量示例。

1
2
3
4
5
6
7
8
9
10
11
12
const sales = "Toyota";

function carTypes(name) {
//常见三元表达式
return name === "Honda" ? name : `对不起,我们不售卖 ${name}。`;
}

const car = { myCar: "Saturn", getCar: carTypes("Honda"), special: sales };

console.log(car.myCar); // Saturn
console.log(car.getCar); // Honda
console.log(car.special); // Toyota

更进一步,我们还可以使用数字或字符串字面量作为属性的名字,或者在另一个对象字面量内嵌套一个对象字面量。如下所示。

1
2
3
4
const car = { manyCars: { a: "Saab", b: "Jeep" }, 7: "Mazda" };

console.log(car.manyCars.b); // Jeep
console.log(car[7]); // Mazda

从合法命名的角度出发,如果我们的对象属性名字不是合法的 JavaScript 标识符或数字,它必须用引号包裹。此时我们的属性名不合法,不能用(.)来访问该属性值,而是通过方括号表示法([])来访问。

1
2
3
4
5
6
7
8
9
const unusualPropertyNames = {
'': '空字符串',
'!': '砰!'
}
console.log(unusualPropertyNames.''); // SyntaxError: Unexpected string
console.log(unusualPropertyNames.!); // SyntaxError: Unexpected token !

console.log(unusualPropertyNames[""]); // 空字符串
console.log(unusualPropertyNames["!"]); // 砰!

增强的对象字面量

对象字面量支持一些很新潮的用法,示例如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = {
// 等价于 Object.setPrototypeOf(obj, theProtoObj);
__proto__: theProtoObj,
// 等价于 handler: handler,同名属性+同名值简写
handler,
// 方法
toString() {
// Super 调用访问父类/原型对象上的成员,也就是 theProtoObj 上的 toString 方法。
return "d " + super.toString();
},
// 计算(动态的)属性名,也就是用一个表达式的值来当属性名。
["prop_" + (() => 42)()]: 42,
};

RegExp 字面量

正则式字面量是字符被正斜杠围成的表达式,如下示例所示。

正则式,用特殊符号表示一类字符或重复次数,主要用语字符串匹配。

1
const re = /ab+c/;

字符串字面量

字符串字面量是由一对双引号(")或单引号(')括起来的零个或多个字符。字符串被限定在同种引号之间(也即,必须是成对单引号或成对双引号)。

1
2
3
4
5
'foo'
"bar"
'1234'
'一行\n另一行'
"Joyo 的猫"

你可以在字符串字面量值上使用 String 对象的所有方法,JavaScript 会自动将字符串字面量转换为一个临时字符串对象,调用该方法,然后废弃掉那个临时的字符串对象。你也可以使用字符串字面量的 length 属性。(但需要区分字符串字面量与 String 对象)

1
2
// 将打印字符串中的字符个数(包括空格)
console.log("John 的猫".length); // 结果为:7

模板字面量

模板字面量(Template Literal)是 JavaScript 里用反引号 ` 包裹字符串的写法。它和普通字符串最大的区别是:可以插值、可以换行、还能加标签函数。

1
2
3
4
5
6
7
8
9
10
11
//常规字符串
const name = "Tom";
const msg = "Hello, " + name + "!";

//模板字面量
const name = "Tom";
const msg = `Hello, ${name}!`;

const a = 1;
const b = 2;
console.log(`sum = ${a + b}`); // sum = 3

有时未定义的toString用法会导致讨厌的 [object Object],示例如下。

1
2
3
4
5
6
7
8
9
10
11
const age = 18;
const hobbies = ["reading", "coding"];

console.log(`Age: ${age}, Hobbies: ${hobbies}`);
// 输出 Name: 18, Hobbies: reading,coding
// 数值和字符串可以被转化为字符串

const info = { city: "Beijing" };
console.log(`Info: ${info}`);
//输出 Info: [object Object],等价于info.toString()
//因为info是对象,自己没有toString(),所以继承原型Object.prototype的方法,返回[object 类型],info的类型是Object

示例二,这里我们通过自定print函数来避免讨厌的 [object Object]。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
const formatArg = (arg) => {
if (Array.isArray(arg)) {
// map遍历成新的值,这里正展示模板字面量的特点,可以插值,最后\n再换行
return arg.map((part) => `- ${part}`).join("\n");
}
if (arg.toString === Object.prototype.toString) {
// 检查对象是不是没有toString方法,也就是会不会输出“[object Object]”。
// JSON.stringify(arg) 是 JavaScript 的内置方法,用于把 JavaScript 数据转换成 JSON 格式的字符串。
return JSON.stringify(arg);
}
return arg;
};

const print = (segments, ...args) => {
// 结构是segments[0] args[0] segments[1] args[1] segments[2]
// 保存第一个文本片段
let message = segments[0];
// 开始拼接,forEach两次循环,每次的回调会接收一次插值 + 一个文本片段
segments.slice(1).forEach((segment, index) => {
message += formatArg(args[index]) + segment;
});
console.log(message);
};

const todos = ["学习 JavaScript", "学习 Web API", "构建网站", "利润!"];

const progress = { javascript: 20, html: 50, css: 10 };

// JavaScript 会自动把模板字符串拆成“固定文本片段”和“插值”,然后传给 print。
// 这里包含两个${},一次插值对应两个片段,则这句话被插值分成三个片段。
print`我需要做:
${todos}
当前进度为:${progress}
`;

// 我需要做:
// - 学习 JavaScript
// - 学习 Web API
// - 构建网站
// - 利润!
// 当前进度为:{"javascript":20,"html":50,"css":10}

控制流与错误处理

块语句

块由一对花括号{}界定,常用来组合语句,如控制流语句(if、for、while)。

1
2
3
4
5
6
7
while (x < 10) {
x++;
statement1;
statement2;
// …
statementN;
}

前面已经提过,不同变量的作用域不同,其中块就是一个作用域界。