【课程笔记】Udemy - The Complete JavaScript Course 2022: From Zero to Expert!
JavaScript 基础
数据、运算与判断
隐式转换
当运算符两端数据类型不同时,如 '1' + 2
,若没有进行显式类型转换,则 JavaScript 解释器将对其做隐式类型转换。JavaScripit 的隐式类型转换常常让初学者感到疑惑,这是因为 JavaScript 不同基本数据类型之间没有明确的优先级关系,不存在所谓的“低级转向高级”,也不存在类似 C/C++ 的重载运算符操作;此外,优先级相同的运算符如 +
和 -
可导致截然不同的隐式转换逻辑。
即便如此,JavaScript 的的隐式转换规则仍然有迹可循。
-
+
运算可将两侧变量隐式转换为String
类型:1
"12" + 34; // -> '1234'
-
-
运算可将两侧变量隐式转换为Number
类型:1
"12" - 34; // -> -22
-
<=, <, >=, >
可将两侧变量隐式转换为Number
类型: -
同级运算与隐式转换从左到右顺次进行:
1
5 + 6 + "4" + 9 - 4 - 2; // -> 1143
函数与对象
函数的三种形式
JavaScript 有三种主要的函数表示形式,假设我们要定义一个函数,传入一个参数 population
,返回传入人口参数占世界的百分比,三种形式如下所示:
-
声明形式(Declaration)
1
2
3
4function percentageOfWorld1(population) {
const totalPopulationOfWorld = 7900;
return `${(population / totalPopulationOfWorld) * 100}%`;
} -
匿名形式(Expressions)
1
2
3
4const percentageOfWorld2 = function (population) {
const totalPopulationOfWorld = 7900;
return `${(population / totalPopulationOfWorld) * 100}%`;
}; -
箭头函数(Arrow Function)
1
2
3
4const percentageOfWorld3 = (population) => {
const totalPopulationOfWorld = 7900;
return `${(population / totalPopulationOfWorld) * 100}%`;
};
设全球总人口为 7900M
,中国人口为 1441M
,调用结果如下所示:
1 | percentageOfWorld1(1441); // -> 18.240506329113924% |
箭头函数的不适用场景
-
函数作为对象属性时,应该使用匿名函数,而不是箭头函数,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19let myCountry = {
country: "China",
capital: "Beijing",
language: "Chinese",
population: 1300,
neighbours: ["Japan", "Russia", "Korea", "India"],
describe: function () {
console.log(
`${this.country} has ${this.population} million ${this.language}-speaking people, ${this.neighbours.length} neighbouring countries and a capital called ${this.capital}`
);
},
// 不应该使用箭头函数
// describe: () => {
// console.log(
// `${this.country} has ${this.population} million ${this.language}-speaking people, ${this.neighbours.length} neighbouring countries and a capital called ${this.capital}.`
// );
// },
};箭头函数作为对象属性时,
this
并非指向对象本身,原型链失效,从而使得用this
访问对象属性将导致意外情况。
数组与循环
DOM 树与事件处理
HTML 文件在浏览器内被解析为 DOM 树。
获取 HTML 元素
浏览器的 Web API 提供了 document
对象,用于对 DOM 树进行操作。
-
document.querySelector()
可通过传入 CSS 选择器,选择符合要求的第一个元素节点,返回对该元素对象的引用:1
2const body = document.querySelector("body"); // 选择 body 元素,
const guess = document.querySelector(".guess"); // 选择第一个 class="guess" 的元素 -
document.querySelectorAll()
返回符合选择器的所有元素节点,即一个元素对象数组。
监听点击事件
-
element.addEventListener()
为element
元素对象注册一个监听器,当元素符合监听行为时,立刻执行传入的函数:1
2
3
4// 为 check 元素注册点击事件的监听器
check.addEventListener("click", () => {
statements...
});注意到传入的第二个参数是一个函数,这里体现了 JavaScript 中函数和其他对象(
object
)地位等同,函数的内容就是其本身object
的值。
编辑元素 CSS 样式
-
element.style.property
返回对元素某个样式的引用:1
2const message = document.querySelector(".message");
message.style.fontSize = "2rem";注意到 CSS 中字体大小属性为
font-size
,但是这在 JavaScript 中不是合法的变量名,需改为驼峰命名法的fontSize
。
处理元素的 class
-
element.classList
返回元素的class
列表,可使用.add()
与.remove()
对其进行增删;使用.toggle()
可实现在没有该类时添加该类,有该类时移除该类;.contains()
判断是否存在某个类:1
2
3
4
5const modal = document.querySelector(".modal");
modal.classList.add("hidden"); //添加 hidden 类
modal.classList.remove("hidden"); //移除 hidden 类
modal.classList.toggle("hidden"); //检查是否存在 hidden 类,若不存在则添加,否则移除
modal.classList.contains("hidden"); //检查是否存在 hidden 类 -
element.className.includes("class")"
同样返回是否存在该类。1
2const modal = document.querySelector(".modal");
modal.className.includes("hidden"); //检查是否存在 hidden 类
JavaScript 幕后解析
总览
现代 JavaScript 语言拥有如下特性
- 是一门高级语言
- 支持内存回收
- 解释型或 JIT 编译型语言
- 支持多种编程范式:面向过程、面向对象、函数式
- 基于原型与面向对象
- 函数与变量同级
- 动态类型
- 单线程
- 非阻塞性事件循环
JavaScript 引擎与运行环境
-
JavaScript 引擎主要由两个部分组成:调用栈和堆。
调用栈主要用于存储函数执行的上下文,堆用于存储引用指向的数据实例。
-
”JavaScript 是一门解释型“语言可能是 JavaScript 普遍存在的最大认知误区之一。初出茅庐的 JavaScript 确实如此,但现代浏览器或形如 Node.js 的运行环境使用的是 JIT(Just-in-time)方法。
在 JIT 流程中,JavaScript 代码会先被转换成抽象语法树(AST),使用解释器执行,而 Baselines compiler 与 Optimization compiler 会在后台评估每行代码的执行命中率,逐渐执行与传统编译型语言类似的工作,提高运行效率。
-
浏览器端的 JavaScript 的运行环境由 JavaScript 引擎,浏览器提供的 Web API ,回调队列构成。JavaScript 引擎和回调队列组成非阻塞性事件循环。 Node.js 中的运行环境则是移除了 Web API ,并添加了一些 Native 层的内容。
执行上下文
JavaScript 的执行上下文是一个很抽象的概念,其相当于代码执行的一个“环境”,存储了代码执行所需的一些信息,通常包括:
- 环境中的变量
- 作用域链
this
关键字对象
这好比享用外卖 Pizza 时,我们不仅需要从包装盒里取出 Pizza 本身,还需要拿出一次性餐具,必要时取出收据进行检查。包装盒就是“上下文”,Pizza 是我们要执行的对象,餐具和收据是环境中的变量。
运行在 JavaScript 最外层的代码所处的是全局上下文(Global execution context),在编译完成后被创建,并执行其中的代码,之后等待事件循环的回调函数。
此外,每个函数的执行都会创建自己的上下文。
调用栈、作用域与作用域链
函数的每一次执行都会创建自己的上下文,并压入调用栈中,执行完后弹出栈顶,并回到调用这个函数的地方。这种设计可以防止上下文丢失。
作用域指的是变量可被获得(access)的区域(scope),JavaScript 的作用域分为全局作用域、函数作用域、块级作用域。内部作用域可以访问外部作用域的变量,反之未然 ,这便是作用域链。
var
变量是函数级作用域的,let
和 const
均是块级。
作用域链和调用栈没有任何关系。
声明提升与临时静默域(TDZ)
函数的声明提升,即运行调用写在声明之前,是高级语言解决函数相互引用问题的常见特性。而 var
类型也变量允许声明则是 JavaScript 早期为支持 first-class function
所产生的历史遗留问题。
var
变量在被声明前引用的值是 undefined
,表面上看是 JavaScript 将声明提前了,其实不然。实际上 JavaScript 在每个执行上下文之前都会扫描整个作用域,检查那些变量需要做声明提升。
是否声明提升 | 初值 | 作用域 | |
---|---|---|---|
函数变量 | 是 | 函数值本身 | 块级 |
var 变量 |
是 | undefined |
函数级 |
let 和 const 变量 |
否 | <uninitialized> ,TDZ |
块级 |
函数表达式和箭头函数 | 取决于是 var ,let 还是 const |
var
类型变量、包括函数在内当前上下文内的对象都是父对象的成员变量
this
关键字
this
是每个上下文创建的特殊变量,最常见的用法是,对象的成员函数作用域内的 this
指向对象本身。
其他情况下 this
的指向如表所示。
类型 | 指向 |
---|---|
方法 | 调用之的对象 |
单独调用函数 | undefined (严格模式下) |
箭头函数 | 外部作用域的 this |
监听器 | 监听到的 DOM 元素 |
正则函数与箭头函数
声明式函数与匿名函数被称为正则函数(regular function),其与箭头函数的区别主要在 this
和 arguments
变量上。
1 | const vonbrank = { |
正则函数拥有 arguments
变量,这是传入参数构成的数组。箭头函数则没有。
1 | const addExpr = function (a, b) { |
原始类型与对象引用
JavaScript 中的原始类型(primitive types)有:
- Number
- String
- Boolean
- Undefined
- Null
- Symbol
- BigInt
对象类型(reference types)有:
- Object 字面量
- 数组
- 函数
等。
原始类型和对象引用的名称都存储在 JavaScript 引擎的 Identifier 中。
原始类型的数据值存储在调用栈(CALL STACK)的上下文中,每次修改时,由于调用栈中的数据一旦创建便不可修改。因此会在调用栈中开辟新的内存,存储修改后的值,同时 Identifier 令名称指向新的内存。这便解释了如下代码。
1 | const age = 30; |
数据引用会将数据实际存储在堆区(HEAP),栈区存储指向堆区实例的地址(引用),多个变量名指向同一个引用,修改时也会改变堆区同一个地址的内容。
1 | const me = { |
如果确实要创建两个值相同,引用不同的对象,可以使用 Object.assign()
1 | const source = { b: 4, c: 5 }; |
但这样只是浅度拷贝,即如果原对象有对象成员,拷贝结果仍然是该成员的引用。实现引用对象的深度拷贝很多时候是一件比较困难的事情,原生 JavaScript 并没有提供深度拷贝方法,可以自己手动实现,或调用外部库。
JavaScript 高级编程
为方便模拟实际项目的情况,本节默认全局上下文已执行以下语句:
1 | ; |
解构赋值
-
数组解构赋值
数组解构赋值可以实现将数组批量赋值给一组变量。
1
2
3const arr = [2, 3, 4];
const [x, y, z] = arr;
console.log(x, y, z); //2 3 4还可以跳过某些元素。
1
2
3const arr = [2, 3, 4];
const [x, , y] = arr;
console.log(x, y); //2 4用于快速交换变量的值。
1
2
3
4let x = 1,
y = 2;
[x, y] = [y, x];
console.log(x, y); //2 1接受函数返回多个变量。
1
2
3
4
5const order = function () {
return [2, 5];
};
let [x, y] = order();
console.log(x, y); //2 5嵌套解构赋值。
1
2
3const nest = [2, 4, [5, 6]];
const [i, , [j, k]] = nest;
console.log(i, j, k);默认解构赋值。
1
2
3
4const [p = 1, q = 1, r] = [8, 9];
console.log(p, q, r); //8 9 undefined
[p = 1, q = 1, r = 1] = [8, 9];
console.log(p, q, r); //8 9 1 -
对象解构赋值
对象解构赋值和数组类似,括号变成花括号。
1
2
3
4
5
6
7
8
9
10
11const { name, openingHours, categories } = restaurant;
console.log(name, openingHours, categories);
/*
Classico
Italiano {
thu: { open: 12, close: 22 },
fri: { open: 11, close: 23 },
sat: { open: 0, close: 24 }
}
[ 'Italian', 'Pizzeria', 'Vegetarian', 'Organic' ]
*/使用不同的变量名。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const {
name: restaurantName,
openingHours: hours,
categories: tags,
} = restaurant;
console.log(restaurantName, hours, tags);
/*
Classico
Italiano {
thu: { open: 12, close: 22 },
fri: { open: 11, close: 23 },
sat: { open: 0, close: 24 }
}
[ 'Italian', 'Pizzeria', 'Vegetarian', 'Organic' ]
*/设置默认值。
1
2const { menu = [], starterMenu: starters = [] } = restaurant;
console.log(menu, starters); //[] [ 'Focaccia', 'Bruschetta', 'Garlic Bread', 'Caprese Salad' ]多个变量重新赋值,注意,由于 JavaScript 会将大括号开头的行视作一个新的作用域,因此需要用括号括起来防止歧义。
1
2
3
4
5
6let a = 111;
let b = 999;
const obj = { a: 23, b: 7, c: 14 };
({ a, b } = obj);
console.log(a, b);子对象解构赋值。
1
2
3
4
5
6
7
8
9
10
11const { openingHours } = restaurant;
const {
fri: { open, close },
} = openingHours;
console.log(open, close); // 11 23
//依然可以使用类似方法重命名,节省大量代码
const {
fri: { open: o, close: c },
} = openingHours;
console.log(o, c); // 11 23作为函数参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14restaurant.orderDelivery = function ({
starterIndex = 1,
mainIndex = 0,
time = "20:00",
address,
}) {
console.log(starterIndex, mainIndex, time, address);
};
restaurant.orderDelivery({
time: "22:30",
address: "Via del sole, 21",
mainIndex: 2,
starterIndex: 2,
}); //2 2 22:30 Via del sole, 21
扩张运算符 ...
与 rest
参数运算符
JavaScript 中的 ...
运算符用于将可迭代对象(包括数组,字符串,map
,set
等)分解为一组单个元素,这些元素是原对象的浅层拷贝。
1 | const name = "vonbrank"; |
合并数组。
1 | const arr = [7, 8, 9]; |
作为函数参数传递。
1 | restaurant.orderPasta = function (ing1, ing2, ing3) { |
rest
参数运算符和 ...
运算符写法几乎一致,但是却是执行相反的操作。
1 | const [a, b, ...others] = [1, 2, 3, 4, 5]; |
与 ...
运算符配合使用:
1 | const [pizza, , risotto, ...otherFoods] = [ |
构造能处理任意参数的函数:
1 | const add = function (...numbers) { |
另一个函数使用 rest
参数的例子:
1 | restaurant.orderPizza = function (mainIngredient, ...otherIngredients) { |
逻辑短路与分配运算符
&&
,||
,??
可以实现逻辑短路。
-
||
划分的语句返回第一个真值语句的值,如果都是假值,则返回最后一个。1
2
3
4
5
6
7console.log(3 || "vonbrank"); //3
console.log(false || 0); //0
console.log(null || undefined); //undefiend
console.log(undefined || null); //null
console.log(0 || null || undefined || "" || "hello" || 23);
// hello这种操作可以检测并初始化变量:
1
2
3
4
5
6
7
8
9
10
11restaurant.numGuests = 20;
//方案1
const guests1 = restaurant.numGuests ? restaurant.numGuests : 10;
console.log(guests1); //20
//方案2
const guests2 = restaurant.numGuests || 10;
console.log(guests2); //20
//相比之下使用短路运算符更方便。 -
&&
与||
相反,其返回第一个为假值的语句的返回值,如果都是真值,则返回最后一个。1
2
3console.log("vonbrank" && false);
console.log(0 && "vonbrank");
console.log(7 && "vonbrank"); -
??
与||
几乎一样,但是它只会跳过null
和undefined
,0
、""
、false
不受影响。1
2
3
4
5restaurant.numGuests = null;
const guests2 = restaurant.numGuests ?? 10;
console.log(guests2); //10
console.log(undefined ?? 0 ?? "hello"); //0
短路运算符不仅仅会返回值,被其隔开的每一个语句在短路之前都将被执行。
1 | if (restaurant.orderPizza) { |
上述 restaurant.orderPizza
为真值,所以 &&
右边的语句被继续执行,实现了 if
的功能,但需要注意的是,我们不提倡滥用短路运算符乃至放弃使用 if
语句,因为滥用会影响代码的可读性。
因此短路运算符的实质是:执行被运算符隔开的每一条语句,直至短路,并返回最后一条语句的返回值。其中, ||
被真值短路,&&
被假值短路,??
被除了 null
、 undefined
外的值短路。
JavaScript ES2020 提供了全新的逻辑分配符: ||=
、&&=
、??=
可用于化简包含短路运算符的语句:
1 | restaurant1.numGuests = restaurant1.numGuests || 10; |
此处需要注意,分配符左侧需要是有效左值,即可被赋值的变量。
高级循环
-
for-of
循环for-of
循环写法如下,可以免去自定义循环变量的麻烦:1
2
3
4
5
6
7
8
9
10
11
12const menu = [...restaurant.starterMenu, ...restaurant.mainMenu];
for (const item of menu) {
console.log(item);
}
// Focaccia
// Bruschetta
// Garlic Bread
// Caprese Salad
// Pizza
// Pasta
// Risottofor-of
依然可以使用索引:1
2
3
4
5
6
7
8
9
10for (const [i, item] of menu.entries()) {
console.log(`${i + 1}. ${item}`);
}
//1. Focaccia
//2. Bruschetta
//3. Garlic Bread
//4. Caprese Salad
//5. Pizza
//6. Pasta
//7. Risottofor-of
循环也适用于对象:Object.keys()
可以返回对象关键字的数组:1
2
3
4
5
6for (const day of Object.keys(openingHours)) {
console.log(day);
}
//Thu
//Fri
//SatObject.values()
可以返回对象值的数组。Object.entries()
返回对象[key, value]
的数组。一个灵活使用对象迭代器、解构赋值与
for-of
的例子:1
2
3
4
5
6for (const [key, { open, close }] of Object.entries(openingHours)) {
console.log(`On ${key}, we open at ${open} and close at ${close}`);
}
//On Thu, we open at 12 and close at 22
//On Fri, we open at 11 and close at 23
//On Sat, we open at 0 and close at 24
增强型对象字面值与可选链
JavaScript 对象成员的名称是可计算的,只需要用 []
包裹即可:
1 | const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; |
实际项目中常常需要操作一个未知对象:
1 | console.log(restaurant.openingHours.mon.open); |
我们不确定 restaurant.openingHours
是否有 mon
这个成员,一旦没有,就会报异常。所以通常这么写:
1 | if (restaurant.openingHours.mon) { |
ES6 提供了可选链简化此类代码。
若不知道某个成员是否存在,可以在其后加一个 ?
,若不存在则直接返回 undefined
,防止访问形如 undefined.open
导致异常。
1 | console.log(restaurant.openingHours.mon?.open); //undefined |
可用来判断方法是否存在:
1 | console.log(restaurant.order1?.(0, 1) ?? "Method does not exist"); //Method does not exist |
判断对象成员是否存在:
1 | const users = [ |
Map
和 Set
和数学上的 集合
完全类似, JavaScript 中的 Set
是一种只允许任何元素出现一次的数据结构。
1 | const orderSet = new Set([ |
Map
可建立其两个元素的配对。
1 | const restaurant3 = new Map(); |
字符串操作
JavaScript 提供许多操作字符串的方法。
-
索引搜索与切片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24const airline = "TAP Air Portugal";
const plane = "A320";
console.log(plane[0]); //A
console.log("B747"[0]); //B
//搜索字符串第一个索引位置,找不到返回 -1
console.log(airline.indexOf("Portugal")); //10
//搜索字符串最后一个索引位置
console.log(airline.lastIndexOf("r")); //8
//.slice 用于字符串切片
console.log(airline.slice(5)); //ir Portugal
console.log(airline.slice(4, 7)); //Air
//字符串切片结合索引位置实现切片第一个单词
console.log(airline.slice(0, airline.indexOf(" "))); //TAP
//切片最后一个单词
console.log(airline.slice(airline.lastIndexOf(" ") + 1)); //Portugal
//负数切片表示倒数位置
console.log(airline.slice(-2)); //al
console.log(airline.slice(1, -1)); //AP Air Portuga实例:判断座位是不是在中间
1
2
3
4
5
6
7
8
9
10
11
12
13const checkMiddleSeat = function (seat) {
//B and E are middle seats
const s = seat.slice(-1);
if (s === "B" || s === "E") {
console.log("You get a middle seat.");
} else {
console.log("You got lucky.");
}
};
checkMiddleSeat("11B"); //You get a middle seat.
checkMiddleSeat("23C"); //You got lucky.
checkMiddleSeat("3E"); //You get a middle seat. -
大小写转换、去除空白字符、替换、前后缀判断
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
42
43
44
45const airline = "TAP Air Portugal";
//大小写转换
console.log(airline.toUpperCase());
console.log(airline.toLowerCase());
//字符串格式化转为首字母大写
const passenger = "vONBrNk";
const passengerLower = passenger.toLowerCase();
const passengerCorrect =
passenger[0].toUpperCase() + passengerLower.slice(1);
console.log(passengerCorrect);
//格式化邮箱,去除末尾回车字符
const email = "hello@vonbrank.com";
const emailLogin = "HelLo@vOnBRnk.CoM \n";
const emailLower = emailLogin.toLowerCase();
const emailTrimmed = emailLower.trim();
console.log(emailTrimmed);
//字符串方法均返回新字符串引用,因此可以连续调用
const emailNormalized = emailLogin.toLowerCase().trim();
console.log(emailNormalized);
console.log(emailTrimmed === emailNormalized);
// .replace(d, t) 可以把第一个 d 替换为 t
const priceGB = "288,97£";
const priceUS = priceGB.replace("£", "$").replace(",", ".");
console.log(priceUS);
const announcements =
"All passengers come to boarding door 23. Boarding door 23!";
console.log(announcements.replace("door", "gate"));
//ReplaceAll 是 ES2021 新特性
console.log(announcements.replaceAll("door", "gate"));
//使用正则表达式可以实现仅用 replace 方法替换所有字符串
console.log(announcements.replace(/door/g, "gate"));
const plane = "Airbus A320neo";
console.log(plane.includes("A320")); //true
console.log(plane.includes("Boeing")); //false
// console.log(plane.startsWith("A3")); //true
console.log(plane.startsWith("Air")); //false一个例子,实现违禁品判断:
1
2
3
4
5
6
7
8
9
10
11
12
13
14if (plane.startsWith("Airbus") && plane.endsWith("neo")) {
console.log("Part of the new Airbus family");
}
const checkBaggage = function (items) {
const baggage = items.toLowerCase();
if (baggage.includes("knife") || baggage.includes("gun")) {
console.log("You are not allowed on board.");
} else console.log("Welcome aboard.");
};
checkBaggage("I have a laptop, some Food and a pocket Knife.");
checkBaggage("Socks and camera");
checkBaggage("Got some snacks and a gun for protection"); -
字符串分割与合并、追加与扩充、倍增
1
2
3
4
5
6
7
8
9
10
11
12
13//split 用来分割字符串,传入需要分割的字符
console.log("a+very+nice+string".split("+"));
//[ 'a', 'very', 'nice', 'string' ]
console.log("von brank".split(" "));
//[ 'von', 'brank' ]
const [firstName, lastName] = "von brank".split(" ");
console.log(firstName, lastName);
//join 可将数组里的字符串用传入字符连接
const newName = ["Mr.", firstName, lastName.toUpperCase()].join(" ");
console.log(newName); // Mr. von BRANK一个将首字母转大写的例子
1
2
3
4
5
6
7
8
9
10
11
12
13const capitalizeName = function (name) {
const names = name.split(" ");
const capitalizedName = [];
for (const n of names) {
// n[0] && capitalizedName.push(n[0].toUpperCase() + n.slice(1));
n[0] && capitalizedName.push(n.replace(n[0], n[0].toUpperCase()));
}
return capitalizedName.join(" ");
};
const passenger = "jassica ann smith davis";
console.log(capitalizeName(passenger));
//Jassica Ann Smith DavispadStart
与padEnd
可以实现在字符串前后或填充字符至指定长度:1
2
3
4
5const message = "Go to gate 23";
console.log(message.padStart(25, "+"));
//++++++++++++Go to gate 23
console.log("vonbrank".padEnd(23, "+"));
//vonbrank+++++++++++++++一个创建掩码的例子:
1
2
3
4
5
6
7
8const maskCreditCard = function (number) {
const str = String(number);
const last = str.slice(-4);
return last.padStart(str.length, "*");
};
console.log(maskCreditCard(14285731415926));
//**********5926string.repeat(n)
可以实现将string
重复n
次1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const message2 = "Bad weather... All departures delayed...\n";
console.log(message2.repeat(5));
/*
Bad weather... All departures delayed...
Bad weather... All departures delayed...
Bad weather... All departures delayed...
Bad weather... All departures delayed...
Bad weather... All departures delayed...
*/
const planesInLine = function (n) {
console.log(`There are ${n} planes in line. ${"✈️".repeat(n)}`);
};
planesInLine(5); //There are 5 planes in line. ✈️✈️✈️✈️✈️
planesInLine(3); //There are 3 planes in line. ✈️✈️✈️
深入理解 JavaScript 函数
默认参数与引用传递
ES6 的新特性之一就是允许函数设置默认参数:
1 | const bookings = []; |
函数参数如果是基本类型,则传入值,对象传入引用,因此在函数内修改对象将使得原对象被修改:
1 | const flight = "CZ6371"; |
高阶函数与回调函数
JavaScript 的重要特性之一就是所谓的 Firt-Class Function ,即函数与普通变量同等看待,有 First-Class Function 便有 High Order Function
1 | const oneWord = function (str) { |
这个例子展示了 transformer
接收一个 string
和一个 fn
,transformer
不在乎 fn
将以怎样的方式处理 string
,只需要 fn
接收 str
处理并返回一个值即可, transformer
内部将调用 fn
传入 string
并执行。
这里的 fn
便是所谓的回调函数(call back function) ,意思是把函数当作值传入,稍后再调用。
JavaScript 总是以回调的形式执行函数;forEach
、addEventListener
也是接受回调函数的高阶函数:
1 | //JavaScript used callback all the time |
call
、apply
与 bind
方法
通常,对对象的成员函数,我们可以用 .
运算符来直接执行:
1 | const nanHang = { |
前文提到过,函数执行上下文中的 this
在不同调用条件下可能是不一样的。
如果我们想要把方法保存到另外的变量里,单独执行将导致 this
变成 defined
,除了手动传入 this
参数,我们没有其他方法指定 this
。
因此 JavaScript 提供了 call
,apply
和bind
三种方法便于开发者手动指定 this
变量的值。
call
方法的第一个参数指定的的是函数执行时的 this
变量,之后依照调用函数的写法用逗号隔开所有传入的参数:
1 | const chuanHang = { |
apply
方法与 call
略有不同,对于函数原本接收的参数,需要以数组的形式传入:
1 | const shenHang = { |
bind
将 this
等参数的绑定与函数的执行分开:
1 | const book3U = book.bind(chuanHang); |
本例中 const book3U = book.bind(chuanHang);
将 book3U
的 this
变量永久钦定为 chuanHang
之后单独执行 book3U
时就像直接执行 chuanHang.book
一样; const bookCZ6371 = book.bind(nanHang, 6371);
则不仅绑定了 this
参数,还绑定了 flightNum
参数,之后 bookCZ6371("Cathy")
相当于原本执行 nanHang.book(6371, "Cathy");
。
bind
还可以用在 addEventListener
中传入的回调函数是某个对象的方法,通过 bind
指定方法的 this
来防止出错。
自执行匿名函数与闭包
想要创建一个独立的作用域,执行某些代码块,是编程中常见的需求, ES5 前常使用 Immediately Invoke Function Expression (IIFE) 实现:
1 | (functino () { |
现代 JavaScript 的做法是使用 let
和 const
变量将作用域限制在块级,然后使用单一的大括号创建代码块实现:
1 | { |
闭包(closure)是 JavaScript 中最容易让初学者感到困惑的东西。
先看一个例子:
1 | const greet = function (greeting) { |
greeting
接收一个参数,返回的却是一个函数;本例中 greeterHey
就是一个从 greet
返回的函数,既然它是函数,就是可执行的。
问题在于, 执行 greeterHey
时需要访问 greet
函数中的 greeting
变量,而 greet
函数的上下文在此时早已结束,被弹出调用栈了,那这里的 greeting
是哪来的? What the hell is it?
实现这一切的便是 JavaScript 的闭包机制!闭包的原理解释如图所示:
如果函数返回的是另外一个函数,那么返回的函数会有一个成员变量,名为 closure
,里面包含了创建这个函数位置所处作用域内的所有变量的引用,当携带闭包的函数被执行时,若任何不在其作用域内的变量被访问,JavaScript 引擎都会先去闭包里搜索该变量,若找不到才接着到作用域链里搜索。
嵌套的箭头函数也是闭包:
1 | const greetArrow = (greeting) => (name) => console.log(`${greeting} ${name}!`); |
数组的使用
数组基础方法
slice
与 splice
用于数组切片,与字符串切片类似,不同之处在于, splice
将直接修改原数组,而 slice
不会:
1 | let arr = ["a", "b", "c", "d", "e", "f", "g"]; |
reverse
用于数组翻转,会直接修改原数组
1 | const arr = ["a", "b", "c", "d", "e", "f", "g"]; |
concat
用于拼接数组, a.concat(b)
表示将 b
拼接在 a
后面
1 | const arr = ["a", "b", "c", "d", "e", "f", "g"]; |
join
可以用指定符号连接数组中所有元素,并连接成字符串:
1 | const arr = ["a", "b", "c", "d", "e", "f", "g"]; |
全新的 .at()
函数可以用于取代数组下标, arr.at(i)
返回第 i
个元素:
1 | const arr = [23, 11, 64]; |
forEach
方法
遍历数组进行某种操作是一种常见的需求,通常使用 for
循环实现, JavaScript 提供了 forEach
函数简化代码:
1 | const arr = ["USD", "EUR", "GBP", "CNY", "USD", "EUR", "USD"]; |
可以看出 forEach
方法传入一个回调函数,接收三个参数:值、索引和数组本身的引用。实现相同的功能 for
可能需要更多代码。
forEach
可以直接用于 Set
和 Map
等可迭代对象,不同之处只是回调函数的参数而已:
Map
:fn : (value, key, map) => {}
Set
:fn : (value, value, set) => {}
注意因为 Set
没有 Array
中 index
或 Map
中 key
之类的东西,所以传入的都是 value
。
node.insertAdjacentHTML()
方法可以往 DOM 树的 node 节点中插入文本,这些内容同时更新为新的 DOM 元素:
1 | <div class="movements__row"></div> |
1 | const movementsRow = document.querySelector(".movements__row"); |
1 | <div class="movements__row"> |
insertAdjacentHTML
本身第一个参数有四种取值,除了 afterbegin
外,其他用法效果如图所示:
1 | <!-- beforebegin --> |
JavaScriip 实现面向对象编程 (OOP)
传统 OOP 中,我们使用 class
定义对象的模板,但是 JavaScript 中没有真正的 class
,即使是 ES6 中的 class
关键字也不过是语法糖而已,JavaScript 使用原型(prototype)来实现面向对象编程。
构造函数与 new
运算符
JavaScript 中的对象构造函数只能用显式声明函数和匿名函数表达式定义:
1 | const Person = function (firstName, birthYear) { |
注意不能使用箭头函数,因为众所周知箭头函数没有 this
变量。
当拥有构造函数后,我们就拥有了创建对象的模板,基于模板创建对象的过程称为“实例化”:
1 | const jonas = new Person("Jonas", 1991); |
可以使用 instanceof
关键字判断一个对象是不是某个构造函数的实例:
1 | console.log(jonas instanceof Person); //true |
对象原型 (Prototype)
有了对象,自然就要定义操作对象的方法,我们虽然可以这样定义:
1 | const Person = function (firstName, birthYear) { |
但是实际项目中千万不要这样做,否则每次实例化都会创建一个新的函数对象,造成极大的性能浪费。
JavaScript 使用原型来定义方法。