JavaScript 基础
数据、运算与判断
隐式转换
当运算符两端数据类型不同时,如 '1' + 2
,若没有进行显式类型转换,则 JavaScript 解释器将对其做隐式类型转换。JavaScripit 的隐式类型转换常常让初学者感到疑惑,这是因为 JavaScript 不同基本数据类型之间没有明确的优先级关系,不存在所谓的“低级转向高级”,也不存在类似 C/C++ 的重载运算符操作;此外,优先级相同的运算符如 +
和 -
可导致截然不同的隐式转换逻辑。
即便如此,JavaScript 的的隐式转换规则仍然有迹可循。
+
运算可将两侧变量隐式转换为String
类型:"12" + 34; // -> '1234'
-
运算可将两侧变量隐式转换为Number
类型:"12" - 34; // -> -22
<=, <, >=, >
可将两侧变量隐式转换为Number
类型:同级运算与隐式转换从左到右顺次进行:
5 + 6 + "4" + 9 - 4 - 2; // -> 1143
函数与对象
函数的三种形式
JavaScript 有三种主要的函数表示形式,假设我们要定义一个函数,传入一个参数 population
,返回传入人口参数占世界的百分比,三种形式如下所示:
声明形式(Declaration)
function percentageOfWorld1(population) { const totalPopulationOfWorld = 7900; return `${(population / totalPopulationOfWorld) * 100}%`; }
匿名形式(Expressions)
const percentageOfWorld2 = function (population) { const totalPopulationOfWorld = 7900; return `${(population / totalPopulationOfWorld) * 100}%`; };
箭头函数(Arrow Function)
const percentageOfWorld3 = (population) => { const totalPopulationOfWorld = 7900; return `${(population / totalPopulationOfWorld) * 100}%`; };
设全球总人口为 7900M
,中国人口为 1441M
,调用结果如下所示:
percentageOfWorld1(1441); // -> 18.240506329113924%
percentageOfWorld2(1441); // -> 18.240506329113924%
percentageOfWorld3(1441); // -> 18.240506329113924%
箭头函数的不适用场景
函数作为对象属性时,应该使用匿名函数,而不是箭头函数,如:
let 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 选择器,选择符合要求的第一个元素节点,返回对该元素对象的引用:const body = document.querySelector("body"); // 选择 body 元素, const guess = document.querySelector(".guess"); // 选择第一个 class="guess" 的元素
document.querySelectorAll()
返回符合选择器的所有元素节点,即一个元素对象数组。
监听点击事件
element.addEventListener()
为element
元素对象注册一个监听器,当元素符合监听行为时,立刻执行传入的函数:// 为 check 元素注册点击事件的监听器 check.addEventListener("click", () => { statements... });
注意到传入的第二个参数是一个函数,这里体现了 JavaScript 中函数和其他对象(
object
)地位等同,函数的内容就是其本身object
的值。
编辑元素 CSS 样式
element.style.property
返回对元素某个样式的引用:const message = document.querySelector(".message"); message.style.fontSize = "2rem";
注意到 CSS 中字体大小属性为
font-size
,但是这在 JavaScript 中不是合法的变量名,需改为驼峰命名法的fontSize
。
处理元素的 class
element.classList
返回元素的class
列表,可使用.add()
与.remove()
对其进行增删;使用.toggle()
可实现在没有该类时添加该类,有该类时移除该类;.contains()
判断是否存在某个类:const 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")"
同样返回是否存在该类。const 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
变量上。
const vonbrank = {
year: 2001,
calcAge: function () {
console.log(this);
console.log(this.year);
const foo1 = function () {
console.log(this.year);
// this == undefined,会报 reference error
};
foo1();
const self = this;
const foo1 = function () {
this = self;
console.log(this.year);
// 将外部的 this 保存为 self 传进来,可以正常运行
};
foo2();
const foo3 = () => {
console.log(this);
console.log(this.year);
// 箭头函数本身不不包含 this 对象,沿着作用域链网上找能找到 calcAge 的 this ,即 vonbrank
};
foo3();
},
greeting: () => console.log(this),
//同样的 greeting 内没有 this 参数,沿作用域链向上找到全局作用域,而全局作用域也没有 this 对象,所以输出 undefined
};
vonbrank.calcAge();
vonbrank.greeting();
正则函数拥有 arguments
变量,这是传入参数构成的数组。箭头函数则没有。
const addExpr = function (a, b) {
console.log(arguments);
};
const addArrow = (a, b) => console.log(arguments);
addExpr(2, 5); //[2, 5]
addArrow(2, 5); //reference error
原始类型与对象引用
JavaScript 中的原始类型(primitive types)有:
- Number
- String
- Boolean
- Undefined
- Null
- Symbol
- BigInt
对象类型(reference types)有:
- Object 字面量
- 数组
- 函数
等。
原始类型和对象引用的名称都存储在 JavaScript 引擎的 Identifier 中。
原始类型的数据值存储在调用栈(CALL STACK)的上下文中,每次修改时,由于调用栈中的数据一旦创建便不可修改。因此会在调用栈中开辟新的内存,存储修改后的值,同时 Identifier 令名称指向新的内存。这便解释了如下代码。
const age = 30;
const oldAge = age;
age = 27;
console.log(age); //27
console.log(oldAge); //30
数据引用会将数据实际存储在堆区(HEAP),栈区存储指向堆区实例的地址(引用),多个变量名指向同一个引用,修改时也会改变堆区同一个地址的内容。
const me = {
name: "Jonas",
age: 30,
};
const friend = me;
friend.age = 27;
console.log(friend.age); //27
console.log(me.age); //27
如果确实要创建两个值相同,引用不同的对象,可以使用 Object.assign()
const source = { b: 4, c: 5 };
const newSource = Object.assign({}, source);
console.log(newSource); //{ b: 4, c: 5 }
但这样只是浅度拷贝,即如果原对象有对象成员,拷贝结果仍然是该成员的引用。实现引用对象的深度拷贝很多时候是一件比较困难的事情,原生 JavaScript 并没有提供深度拷贝方法,可以自己手动实现,或调用外部库。
JavaScript 高级编程
为方便模拟实际项目的情况,本节默认全局上下文已执行以下语句:
"use strict";
// Data needed for a later exercise
const flights =
"_Delayed_Departure;fao93766109;txl2133758440;11:25+_Arrival;bru0943384722;fao93766109;11:45+_Delayed_Arrival;hel7439299980;fao93766109;12:05+_Departure;fao93766109;lis2323639855;12:30";
// Data needed for first part of the section
const restaurant = {
name: "Classico Italiano",
location: "Via Angelo Tavanti 23, Firenze, Italy",
categories: ["Italian", "Pizzeria", "Vegetarian", "Organic"],
starterMenu: ["Focaccia", "Bruschetta", "Garlic Bread", "Caprese Salad"],
mainMenu: ["Pizza", "Pasta", "Risotto"],
openingHours: {
thu: {
open: 12,
close: 22,
},
fri: {
open: 11,
close: 23,
},
sat: {
open: 0, // Open 24 hours
close: 24,
},
},
};
解构赋值
数组解构赋值
数组解构赋值可以实现将数组批量赋值给一组变量。
const arr = [2, 3, 4]; const [x, y, z] = arr; console.log(x, y, z); //2 3 4
还可以跳过某些元素。
const arr = [2, 3, 4]; const [x, , y] = arr; console.log(x, y); //2 4
用于快速交换变量的值。
let x = 1, y = 2; [x, y] = [y, x]; console.log(x, y); //2 1
接受函数返回多个变量。
const order = function () { return [2, 5]; }; let [x, y] = order(); console.log(x, y); //2 5
嵌套解构赋值。
const nest = [2, 4, [5, 6]]; const [i, , [j, k]] = nest; console.log(i, j, k);
默认解构赋值。
const [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
对象解构赋值
对象解构赋值和数组类似,括号变成花括号。
const { 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' ] */
使用不同的变量名。
const { 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' ] */
设置默认值。
const { menu = [], starterMenu: starters = [] } = restaurant; console.log(menu, starters); //[] [ 'Focaccia', 'Bruschetta', 'Garlic Bread', 'Caprese Salad' ]
多个变量重新赋值,注意,由于 JavaScript 会将大括号开头的行视作一个新的作用域,因此需要用括号括起来防止歧义。
let a = 111; let b = 999; const obj = { a: 23, b: 7, c: 14 }; ({ a, b } = obj); console.log(a, b);
子对象解构赋值。
const { 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
作为函数参数。
restaurant.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
等)分解为一组单个元素,这些元素是原对象的浅层拷贝。
const name = "vonbrank";
const letters = [...name, " ", ".s"];
console.log(letters);
//['v', 'o', 'n', 'b', 'r', 'a', 'n', 'k', ' ', '.s']
合并数组。
const arr = [7, 8, 9];
const newArr = [1, 2, ...arr];
console.log(newArr); //[1, 2, 7, 8, 9]
console.log(...arr); //[7, 8, 9]
作为函数参数传递。
restaurant.orderPasta = function (ing1, ing2, ing3) {
console.log(ing1, ing2, ing3);
};
const ingredients = ["a", "b", "c"];
restaurant.orderPasta(...ingredients); // a b c
rest
参数运算符和 ...
运算符写法几乎一致,但是却是执行相反的操作。
const [a, b, ...others] = [1, 2, 3, 4, 5];
console.log(a, b, others); //1 2 [ 3, 4, 5 ]
与 ...
运算符配合使用:
const [pizza, , risotto, ...otherFoods] = [
...restaurant.mainMenu,
...restaurant.starterMenu,
];
console.log(pizza, risotto, otherFoods); //Pizza Risotto [ 'Focaccia', 'Bruschetta', 'Garlic Bread', 'Caprese Salad' ]
构造能处理任意参数的函数:
const add = function (...numbers) {
let sum = 0;
for (let number of numbers) {
sum += number;
}
console.log(sum);
};
add(2, 4); //6
add(2, 5, 8, 10); //25
add(1, 2, 3, 5, 7, 13, 17); //48
另一个函数使用 rest
参数的例子:
restaurant.orderPizza = function (mainIngredient, ...otherIngredients) {
console.log(mainIngredient);
console.log(otherIngredients);
};
restaurant.orderPizza("mushrooms", "onion", "olives", "spinach"); //[ 'onion', 'olives', 'spinach' ]
逻辑短路与分配运算符
&&
,||
,??
可以实现逻辑短路。
||
划分的语句返回第一个真值语句的值,如果都是假值,则返回最后一个。console.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
这种操作可以检测并初始化变量:
restaurant.numGuests = 20; //方案1 const guests1 = restaurant.numGuests ? restaurant.numGuests : 10; console.log(guests1); //20 //方案2 const guests2 = restaurant.numGuests || 10; console.log(guests2); //20 //相比之下使用短路运算符更方便。
&&
与||
相反,其返回第一个为假值的语句的返回值,如果都是真值,则返回最后一个。console.log("vonbrank" && false); console.log(0 && "vonbrank"); console.log(7 && "vonbrank");
??
与||
几乎一样,但是它只会跳过null
和undefined
,0
、""
、false
不受影响。restaurant.numGuests = null; const guests2 = restaurant.numGuests ?? 10; console.log(guests2); //10 console.log(undefined ?? 0 ?? "hello"); //0
短路运算符不仅仅会返回值,被其隔开的每一个语句在短路之前都将被执行。
if (restaurant.orderPizza) {
restaurant.orderPizza("mushrooms", "spinach");
}
restaurant.orderPizza && restaurant.orderPizza("mushrooms", "spinach");
上述 restaurant.orderPizza
为真值,所以 &&
右边的语句被继续执行,实现了 if
的功能,但需要注意的是,我们不提倡滥用短路运算符乃至放弃使用 if
语句,因为滥用会影响代码的可读性。
因此短路运算符的实质是:执行被运算符隔开的每一条语句,直至短路,并返回最后一条语句的返回值。其中, ||
被真值短路,&&
被假值短路,??
被除了 null
、 undefined
外的值短路。
JavaScript ES2020 提供了全新的逻辑分配符: ||=
、&&=
、??=
可用于化简包含短路运算符的语句:
restaurant1.numGuests = restaurant1.numGuests || 10;
restaurant1.numGuests ||= 10;
//等价于 restaurant1.numGuests = restaurant1.numGuests || 10;
restaurant2.numGuests ||= 10;
//等价于 restaurant2.numGuests = restaurant2.numGuests || 10;
console.log(restaurant1.numGuests); //10
console.log(restaurant2.numGuests); //10
restaurant2.owner &&= "<ANONYMOUS>";
//等价于 restaurant2.owner = restaurant2.owner && "<ANONYMOUS>"
console.log(restaurant2.owner);
此处需要注意,分配符左侧需要是有效左值,即可被赋值的变量。
高级循环
for-of
循环for-of
循环写法如下,可以免去自定义循环变量的麻烦:const menu = [...restaurant.starterMenu, ...restaurant.mainMenu]; for (const item of menu) { console.log(item); } // Focaccia // Bruschetta // Garlic Bread // Caprese Salad // Pizza // Pasta // Risotto
for-of
依然可以使用索引:for (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. Risotto
for-of
循环也适用于对象:Object.keys()
可以返回对象关键字的数组:for (const day of Object.keys(openingHours)) { console.log(day); } //Thu //Fri //Sat
Object.values()
可以返回对象值的数组。Object.entries()
返回对象[key, value]
的数组。一个灵活使用对象迭代器、解构赋值与
for-of
的例子:for (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 对象成员的名称是可计算的,只需要用 []
包裹即可:
const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const openingHours = {
[`${weekdays[4]}`]: {
open: 12,
close: 22,
},
Fri: {
open: 11,
close: 23,
},
Sat: {
open: 0, // Open 24 hours
close: 24,
},
};
console.log(openingHours);
/*
{
Thu: { open: 12, close: 22 },
Fri: { open: 11, close: 23 },
Sat: { open: 0, close: 24 }
}
*/
实际项目中常常需要操作一个未知对象:
console.log(restaurant.openingHours.mon.open);
我们不确定 restaurant.openingHours
是否有 mon
这个成员,一旦没有,就会报异常。所以通常这么写:
if (restaurant.openingHours.mon) {
console.log(restaurant.openingHours.mon.open);
}
ES6 提供了可选链简化此类代码。
若不知道某个成员是否存在,可以在其后加一个 ?
,若不存在则直接返回 undefined
,防止访问形如 undefined.open
导致异常。
console.log(restaurant.openingHours.mon?.open); //undefined
可用来判断方法是否存在:
console.log(restaurant.order1?.(0, 1) ?? "Method does not exist"); //Method does not exist
判断对象成员是否存在:
const users = [
{ name: "vonbrank", job: "student" },
{ name: "alice", job: "Engineer" },
];
console.log(users[0]?.name || "user property does not exist"); ///vonbrank
console.log(users[2]?.name || "user property does not exist"); //user property does not exist
Map
和 Set
和数学上的 集合
完全类似, JavaScript 中的 Set
是一种只允许任何元素出现一次的数据结构。
const orderSet = new Set([
"Pizza",
"Pasta",
"Pizza",
"Pizza",
"Risotto",
"Pasta",
"Pizza",
]);
console.log(orderSet); //Set(3) { 'Pizza', 'Pasta', 'Risotto' }
console.log(orderSet.size); //3
console.log(orderSet.has("Pizza")); //true
console.log(orderSet.has("Bread")); //false
orderSet.add("Garlic Bread");
console.log(orderSet); //Set(4) { 'Pizza', 'Pasta', 'Risotto', 'Garlic Bread' }
orderSet.delete("Risotto");
// orderSet.clear();
console.log(orderSet); //Set(3) { 'Pizza', 'Pasta', 'Garlic Bread' }
//Set 是可迭代对象,可使用 for-of 循环
for (const order of orderSet) {
console.log(order);
}
//Set 拆解字符串
console.log(new Set("hello, world!"));
//Set(10) { 'h', 'e', 'l', 'o', ',', ' ', 'w', 'r', 'd', '!' }
Map
可建立其两个元素的配对。
const restaurant3 = new Map();
restaurant3.set("name", "origus");
restaurant3.set(1, "Italy");
restaurant3.set(2, "Franch");
restaurant3
.set("categories", ["Italian", "Pizzeria", "Vegetarian", "Organic"])
.set("open", "11")
.set("close", "23")
.set(true, "we are open")
.set(false, "we are closed");
console.log(restaurant3);
/*
Map(8) {
'name' => 'origus',
1 => 'Italy',
2 => 'Franch',
'categories' => [ 'Italian', 'Pizzeria', 'Vegetarian', 'Organic' ],
'open' => '11',
'close' => '23',
true => 'we are open',
false => 'we are closed'
}
*/
console.log(restaurant3.get("name")); //origus
restaurant3.delete(2);
console.log(restaurant3);
console.log([...restaurant3.entries()]);
/*
[
[ 'name', 'origus' ],
[ 1, 'Italy' ],
[ 'categories', [ 'Italian', 'Pizzeria', 'Vegetarian', 'Organic' ] ],
[ 'open', '11' ],
[ 'close', '23' ],
[ true, 'we are open' ],
[ false, 'we are closed' ]
]
*/
console.log(restaurant3.has("categories")); //true
console.log(restaurant3.size); //7
字符串操作
JavaScript 提供许多操作字符串的方法。
索引搜索与切片
const 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
实例:判断座位是不是在中间
const 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.
大小写转换、去除空白字符、替换、前后缀判断
const 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
一个例子,实现违禁品判断:
if (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");
字符串分割与合并、追加与扩充、倍增
//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
一个将首字母转大写的例子
const 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 Davis
padStart
与padEnd
可以实现在字符串前后或填充字符至指定长度:const message = "Go to gate 23"; console.log(message.padStart(25, "+")); //++++++++++++Go to gate 23 console.log("vonbrank".padEnd(23, "+")); //vonbrank+++++++++++++++
一个创建掩码的例子:
const maskCreditCard = function (number) { const str = String(number); const last = str.slice(-4); return last.padStart(str.length, "*"); }; console.log(maskCreditCard(14285731415926)); //**********5926
string.repeat(n)
可以实现将string
重复n
次const 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 的新特性之一就是允许函数设置默认参数:
const bookings = [];
const createBooking = function (
flightNum,
numPassengers = 1,
price = 100 * numPassengers
) {
//ES5 way
// numPassengers ||= 1;
// price ||= 100;
const booking = {
flightNum,
numPassengers,
price,
};
console.log(booking);
bookings.push(booking);
};
createBooking("CZ6369");
createBooking("CZ6371", undefined, 200);
createBooking("CZ6371", 10, 399);
/*
{ flightNum: 'CZ6369', numPassengers: 1, price: 100 }
{ flightNum: 'CZ6371', numPassengers: 1, price: 200 }
{ flightNum: 'CZ6371', numPassengers: 10, price: 399 }
*/
函数参数如果是基本类型,则传入值,对象传入引用,因此在函数内修改对象将使得原对象被修改:
const flight = "CZ6371";
const vonbrank = {
name: "Von Brank",
passport: 1234567,
};
const checkIn = function (flightNum, passenger) {
flightNum = "CZ6666";
passenger.name = "Mr." + passenger.name;
if (passenger.passport === 1234567) {
console.log("check in");
} else console.log("wrong passport");
};
checkIn(flight, vonbrank);
console.log(flight, vonbrank);
// CZ6371 { name: 'Mr.Von Brank', passport: 1234567 }
高阶函数与回调函数
JavaScript 的重要特性之一就是所谓的 Firt-Class Function ,即函数与普通变量同等看待,有 First-Class Function 便有 High Order Function
const oneWord = function (str) {
return str.replace(/ /g, "").toLowerCase();
};
const upperFirstWord = function (str) {
const [first, ...other] = str.split(" ");
return [first.toUpperCase(), ...other].join(" ");
};
const transformer = function (str, fn) {
console.log(`Original string: ${str}`);
console.log(`Transformed string: ${fn(str)}`);
console.log(`Transformed by: ${fn.name}`);
};
transformer("JavaScript is the best language!", upperFirstWord);
/*
Original string: JavaScript is the best language!
Transformed string: JAVASCRIPT is the best language!
Transformed by: upperFirstWord
*/
transformer("JavaScript is the best language!", oneWord);
/*
Original string: JavaScript is the best language!
Transformed string: javascriptisthebestlanguage!
Transformed by: oneWord
*/
这个例子展示了 transformer
接收一个 string
和一个 fn
,transformer
不在乎 fn
将以怎样的方式处理 string
,只需要 fn
接收 str
处理并返回一个值即可, transformer
内部将调用 fn
传入 string
并执行。
这里的 fn
便是所谓的回调函数(call back function) ,意思是把函数当作值传入,稍后再调用。
JavaScript 总是以回调的形式执行函数;forEach
、addEventListener
也是接受回调函数的高阶函数:
//JavaScript used callback all the time
const high5 = function (item) {
console.log("Hooray!");
console.log(item);
};
document.body.addEventListener("click", high5);
["VonBrank", "Alice", "Bob", "Cathy"].forEach(high5);
call
、apply
与 bind
方法
通常,对对象的成员函数,我们可以用 .
运算符来直接执行:
const nanHang = {
airline: "nanHang",
iataCode: "CZ",
bookings: [],
book: function (flightNum, passengerName) {
console.log(
`${passengerName} booked a seat on ${this.airline} flight ${this.iataCode}${flightNum}.`
);
this.bookings.push({
flight: `${this.iataCode}${flightNum}`,
name: passengerName,
});
},
};
nanHang.book("6371", "VonBrank");
nanHang.book("6317", "Smith");
console.log(nanHang);
/*
VonBrank booked a seat on nanHang flight CZ6371.
Smith booked a seat on nanHang flight CZ6317.
{
airline: 'nanHang',
iataCode: 'CZ',
bookings: [
{ flight: 'CZ6371', name: 'VonBrank' },
{ flight: 'CZ6317', name: 'Smith' }
],
book: [Function: book]
}
*/
前文提到过,函数执行上下文中的 this
在不同调用条件下可能是不一样的。
如果我们想要把方法保存到另外的变量里,单独执行将导致 this
变成 defined
,除了手动传入 this
参数,我们没有其他方法指定 this
。
因此 JavaScript 提供了 call
,apply
和bind
三种方法便于开发者手动指定 this
变量的值。
call
方法的第一个参数指定的的是函数执行时的 this
变量,之后依照调用函数的写法用逗号隔开所有传入的参数:
const chuanHang = {
airline: "chuangHang",
iataCode: "3U",
bookings: [],
};
const book = nanHang.book;
book.call(chuanHang, "3456", "Steven");
console.log(chuanHang);
/*
Steven booked a seat on chuangHang flight 3U3456.
{
airline: 'chuangHang',
iataCode: '3U',
bookings: [ { flight: '3U3456', name: 'Steven' } ]
}
*/
apply
方法与 call
略有不同,对于函数原本接收的参数,需要以数组的形式传入:
const shenHang = {
airline: "Shenzhen Airline",
iataCode: "ZH",
bookings: [],
};
book.call(shenHang, 538, "Alice");
console.log(shenHang);
const flightData = [583, "Cooper"];
book.apply(shenHang, flightData);
console.log(shenHang);
/*
Alice booked a seat on Shenzhen Airline flight ZH538.
Cooper booked a seat on Shenzhen Airline flight ZH583.
{
airline: 'Shenzhen Airline',
iataCode: 'ZH',
bookings: [
{ flight: 'ZH538', name: 'Alice' },
{ flight: 'ZH583', name: 'Cooper' }
]
}
*/
bind
将 this
等参数的绑定与函数的执行分开:
const book3U = book.bind(chuanHang);
book3U(1234, "Bob"); //Bob booked a seat on chuangHang flight 3U1234.
const bookCZ6371 = book.bind(nanHang, 6371);
bookCZ6371("Cathy"); //Cathy booked a seat on nanHang flight CZ6371.
本例中 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) 实现:
(functino () {
console.log(`This function executes only once.`);
})();
现代 JavaScript 的做法是使用 let
和 const
变量将作用域限制在块级,然后使用单一的大括号创建代码块实现:
{
console.log(`This function executes only once.`);
}
闭包(closure)是 JavaScript 中最容易让初学者感到困惑的东西。
先看一个例子:
const greet = function (greeting) {
return function (name) {
console.log(`${greeting} ${name}!`);
};
};
const greeterHey = greet("Hey");
greeterHey("Jonas"); //Hey Jonas
greeterHey("Von Brank"); //Hey Von Brank
greet("Hello")("Alice"); //Hello Alice
const greetArrow = (greeting) => (name) => console.log(`${greeting} ${name}!`);
const greeterArrowHey = greetArrow("Hey");
greeterArrowHey("Jonas");
greeting
接收一个参数,返回的却是一个函数;本例中 greeterHey
就是一个从 greet
返回的函数,既然它是函数,就是可执行的。
问题在于, 执行 greeterHey
时需要访问 greet
函数中的 greeting
变量,而 greet
函数的上下文在此时早已结束,被弹出调用栈了,那这里的 greeting
是哪来的? What the hell is it?
实现这一切的便是 JavaScript 的闭包机制!闭包的原理解释如图所示:
如果函数返回的是另外一个函数,那么返回的函数会有一个成员变量,名为 closure
,里面包含了创建这个函数位置所处作用域内的所有变量的引用,当携带闭包的函数被执行时,若任何不在其作用域内的变量被访问,JavaScript 引擎都会先去闭包里搜索该变量,若找不到才接着到作用域链里搜索。
嵌套的箭头函数也是闭包:
const greetArrow = (greeting) => (name) => console.log(`${greeting} ${name}!`);
数组的使用
数组基础方法
slice
与 splice
用于数组切片,与字符串切片类似,不同之处在于, splice
将直接修改原数组,而 slice
不会:
let arr = ["a", "b", "c", "d", "e", "f", "g"];
console.log(arr.slice(2, 4)); //[ 'c', 'd' ]
console.log(arr.slice(3)); //[ 'd', 'e', 'f', 'g' ]
console.log(arr.slice(-1)); //[ 'g' ]
console.log(arr.slice(1, -2)); //[ 'b', 'c', 'd', 'e' ]
console.log(arr.slice());
/*
[
'a', 'b', 'c',
'd', 'e', 'f',
'g'
]
*/
arr = ["a", "b", "c", "d", "e", "f", "g"];
console.log(arr.splice(2)); // ["c", "d", "e", "f", "g"]
console.log(arr); // ["a", "b"]
//splice 会移除元素中中使用 splice 切片的部分
reverse
用于数组翻转,会直接修改原数组
const arr = ["a", "b", "c", "d", "e", "f", "g"];
const arr2 = ["h", "i", "j", "k", "l"];
console.log(arr2.reverse()); //[ 'l', 'k', 'j', 'i', 'h' ]
console.log(arr2); //[ 'l', 'k', 'j', 'i', 'h' ]
concat
用于拼接数组, a.concat(b)
表示将 b
拼接在 a
后面
const arr = ["a", "b", "c", "d", "e", "f", "g"];
const arr2 = ["h", "i", "j", "k", "l"];
const letters = arr.concat(arr2);
console.log(letters);
/*
[
'a', 'b', 'c', 'd',
'e', 'f', 'g', 'h',
'i', 'j', 'k', 'l'
]
*/
console.log([...arr, ...arr2]);
/*
[
'a', 'b', 'c', 'd',
'e', 'f', 'g', 'h',
'i', 'j', 'k', 'l'
]
*/
join
可以用指定符号连接数组中所有元素,并连接成字符串:
const arr = ["a", "b", "c", "d", "e", "f", "g"];
console.log(arr.join("-")); //a-b-c-d-e-f-g
全新的 .at()
函数可以用于取代数组下标, arr.at(i)
返回第 i
个元素:
const arr = [23, 11, 64];
console.log(arr[0]); //23
console.log(arr.at(0)); //23
console.log(arr[arr.length - 1]); //64
console.log(arr.slice(-1)[0]); //64
console.log(arr.at(-1)); //64
//字符串也可以用
console.log("vonbrank".at(-1)); // 'k'
forEach
方法
遍历数组进行某种操作是一种常见的需求,通常使用 for
循环实现, JavaScript 提供了 forEach
函数简化代码:
const arr = ["USD", "EUR", "GBP", "CNY", "USD", "EUR", "USD"];
arr.forEach((value, i, arr) => {
console.log(value, i);
});
for (let i = 0; i < arr.length; i++) console.log(arr[i], i);
可以看出 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 元素:
<div class="movements__row"></div>
const movementsRow = document.querySelector(".movements__row");
const type = "abc";
const i = 0;
const mov = 1000;
const html = `
<div class="movements__type movements__type--${type}">
${i + 1} ${type.toUpperCase()}
</div>
<div class="movements__value">${mov}€</div>
`;
movementsRow.insertAdjacentHTML(`afterbegin`, html);
<div class="movements__row">
<div class="movements__type movements__type--ABC">1 ABC</div>
<div class="movements__value">1000€</div>
</div>
insertAdjacentHTML
本身第一个参数有四种取值,除了 afterbegin
外,其他用法效果如图所示:
<!-- beforebegin -->
<p>
<!-- afterbegin -->
foo
<!-- beforeend -->
</p>
<!-- afterend -->
JavaScriip 实现面向对象编程 (OOP)
传统 OOP 中,我们使用 class
定义对象的模板,但是 JavaScript 中没有真正的 class
,即使是 ES6 中的 class
关键字也不过是语法糖而已,JavaScript 使用原型(prototype)来实现面向对象编程。
构造函数与 new
运算符
JavaScript 中的对象构造函数只能用显式声明函数和匿名函数表达式定义:
const Person = function (firstName, birthYear) {
this.firstName = firstName;
this.birthYear = birthYear;
};
注意不能使用箭头函数,因为众所周知箭头函数没有 this
变量。
当拥有构造函数后,我们就拥有了创建对象的模板,基于模板创建对象的过程称为“实例化”:
const jonas = new Person("Jonas", 1991);
console.log(jonas); //Person { firstName: 'Jonas', birthYear: 1991 }
const jassica = new Person("Jassica", 2017);
const jack = new Person("Jack", 1975);
console.log(jassica, jack); //Person { firstName: 'Jassica', birthYear: 2017 } Person { firstName: 'Jack', birthYear: 1975 }
可以使用 instanceof
关键字判断一个对象是不是某个构造函数的实例:
console.log(jonas instanceof Person); //true
对象原型 (Prototype)
有了对象,自然就要定义操作对象的方法,我们虽然可以这样定义:
const Person = function (firstName, birthYear) {
this.firstName = firstName;
this.birthYear = birthYear;
this.calcAge = function () {
return 2037 - birthYear;
};
};
但是实际项目中千万不要这样做,否则每次实例化都会创建一个新的函数对象,造成极大的性能浪费。
JavaScript 使用原型来定义方法。