笔记:JavaScript ES6
前言
和前面写的那篇文章一个背景,最近看 Nest.js 后端的东西发现挺多 JS 的东西,ES6 里面除了几个常用的import
和export
以及箭头函数其它约等于不知道,这里再整理一下。
ECMAScript 6
ES6 算是 JS 史上更新最重磅的一次,这里简单列一下 ES6 新增的东西,后面挑几个自己不熟的出来说。
变量
在ES6之前,变量是用 var
声明的,但 var
有一些问题,比如变量提升(变量在声明之前可以访问)。ES6引入了 let
和 const
,用来解决这些问题。
let
声明的变量只能在它所在的代码块中使用,const
用来声明常量,一旦赋值不能再修改。
模板字符串
以前我们拼接字符串需要用加号,但 ES6 里引入了模板字符串,让我们可以通过 ${}
的形式直接在字符串中插入变量,如果还是以前那种方法还要担心各种符号。
let name = "114514";
console.log(`你好,${name}`); // 输出 你好,114514
解构赋值
这个解构赋值简单来说就是允许我们直接从数组或对象中提取值,用来简化代码的。
let [a, b] = [1, 2]; // 从数组中提取 a=1, b=2
let {name, age} = {name: "Lemon", age: 23}; // 从对象中提取 name 和 age
类
面向对象编程,看这里。
箭头函数、模块化和 Promise
这三个都后面说,箭头函数是 ES6 新增的函数简洁的写法,模块化是可以通过 import
和 export
来管理代码,Promise 是让处理异步更方便的一种办法。
箭头函数
箭头函数是 ES6 的一种简洁的函数写法。传统的函数是这样写的:
function add(a, b) {
return a + b;
}
箭头函数可以写成:
const add = (a, b) => a + b;
如果函数只有一个参数,括号也可以省略:
const square = x => x * x;
箭头函数不仅语法简洁,还有一个特别重要的特性:**它不会创建自己的 this
。而 this 这个东西又有的说了。
this 关键字
在 JavaScript 中,this
是一个非常重要的概念,虽然他不是 ES6 新增的,老早就有了。this 指向的是当前执行上下文的对象。
在普通函数中,this
通常是指向调用该函数的对象;在箭头函数中,this
不会像普通函数一样根据调用方式改变,而是继承自定义该箭头函数时的上下文。简单来说,箭头函数里的 this
是固定的,跟它在哪定义有关,而不是在哪调用。
function Person() {
this.age = 0;
setInterval(() => {
this.age++;
console.log(this.age); // 这里的 `this` 指向 Person 对象
}, 1000);
}
let p = new Person();
在 Vue 选项式 API 或者 Nest 的控制器中,this 都是非常常见的,但这里的 this 不是简单的普通函数或箭头函数中的。这里的 this 是根据类或对象实例的上下文来的。
export default {
data() {
return {
message: 'Hello'
};
},
methods: {
greet() {
console.log(this.message); // 这里的 this 指向 Vue 组件实例
}
}
};
在这段 Vue 的选项式 API 中,this.message 就是访问当前组件实例的 data 中的 message。这个 this 的行为类似于类的实例,不会根据函数调用方式改变,而是始终指向当前的组件实例(那 Vue 中的 this 是如何访问到 data、methods 等的?这个就是 Vue 的内部机制了,Vue 内部叫代理)。
methods: {
greet: () => {
console.log(this.message); // 箭头函数会继承外层的 this,而不是 Vue 实例
}
}
另外,Vue 中一般不用箭头函数也是因为 this 上下文不同,箭头函数的 this 会从定义时的上下文中继承,通常是外层作用域的 this,而不是 Vue 组件的实例。通常不推荐在 Vue 的 methods 中使用箭头函数,因为它会导致 this 绑定到错误的上下文。
Nest 中的 this 也和 Vue 类似,都是依据类或对象实例的上下文。它的 this 是指向类实例的,它的行为类似于面向对象编程中的 this。每个控制器或服务通常是通过类的实例化来创建的,而类中的 this 总是指向该类的当前实例。
@Injectable()
export class AppService {
private readonly data: string = 'Hello';
getData(): string {
return this.data; // 这里的 this 指向 AppService 类的实例
}
}
在这个例子中,this.data
访问的是当前类实例的 data 属性,这与面向对象编程中的类实例一致。this 在 Nest 中的行为类似于普通类的 this,与 Vue 类似,它指向当前实例,并不会因为调用方式不同而变化。
模块化
ES6 引入了 export
和 import
语法,使得 JavaScript 支持模块化编程。模块化编程的优势在于,可以将代码分成多个文件和模块,方便管理、维护、复用代码,避免全局变量污染。通过 export
可以在一个文件中导出变量、函数或类,而其他文件可以使用 import
语法来引入这些导出的内容。
命名导出
首先来看如何使用 export
导出模块中的内容:
// math.js
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export class Calculator {
multiply(a, b) {
return a * b;
}
}
在这个例子中,文件 math.js
中使用了三种 export
语法:
export const PI
:导出一个常量PI
,可以在其他模块中使用。export function add
:导出一个函数add
,可以在其他地方调用。export class Calculator
:导出一个类Calculator
,这个类可以被实例化并使用它的方法。
接下来,在另一个文件中,我们可以通过 import
引入这些导出的内容:
// app.js
import { PI, add } from './math.js'; // 导入 PI 常量和 add 函数
import { Calculator } from './math.js'; // 导入 Calculator 类
console.log(PI); // 打印 3.14159
console.log(add(2, 3)); // 打印 5
const calc = new Calculator();
console.log(calc.multiply(2, 3)); // 打印 6
这里使用了 import { ... } from '...'
语法来从 math.js
文件中导入 PI
、add
和 Calculator
。导入的名称必须与导出时的名称一致。
默认导出
除了上面的命名导出,ES6 还支持默认导出,即一个模块中可以标记某个变量、函数或类为默认导出。在导入时,默认导出可以用任意名字来引用,不必与导出时的名字一致。
// greeting.js
export default function greeting(name) {
return `Hello, ${name}!`;
}
在这个例子中,greeting
函数被默认导出。我们在其他文件中导入时,可以用任意名字:
// app.js
import greet from './greeting.js'; // 任意命名为 greet
console.log(greet('John')); // 打印 "Hello, John!"
默认导出的内容不需要使用 {}
括号来导入,导入时可以随意命名。
混合导出
一个模块可以同时包含命名导出和默认导出。例子如下:
// user.js
export const userName = 'Alice';
export const userAge = 25;
export default function greetUser() {
return `Hello, ${userName}!`;
}
导入时既可以使用默认导出,也可以使用命名导出:
// app.js
import greetUser, { userName, userAge } from './user.js';
console.log(greetUser()); // 打印 "Hello, Alice!"
console.log(userName); // 打印 "Alice"
console.log(userAge); // 打印 25
重新导出
ES6 还支持从一个模块中重新导出内容,这在模块拆分和代码组织时非常有用。通过 export ... from '...'
语法可以直接从另一个模块导出内容,而不需要显式地导入后再导出。
// utils.js
export { PI, add } from './math.js'; // 从 math.js 重新导出 PI 和 add
现在在其他文件中可以像直接从 math.js
导出一样使用这些重新导出的内容:
// app.js
import { PI, add } from './utils.js';
console.log(PI); // 打印 3.14159
console.log(add(4, 5)); // 打印 9
异步
JavaScript 是单线程的,意味着它一次只能执行一件事。但是很多操作,比如网络请求、读写文件,需要时间,所以 JavaScript 提供了一个叫异步的东西来避免程序“卡住”,与之相对的词语叫“同步”。
常见的异步有回调函数、Promise 和 async/await。
回调函数
这个简单懒得讲,因为我天天写,网络请求很多 callback,有时候封装函数也要加 callback。就是执行完某个操作后再执行另一个函数。
setTimeout(function() {
console.log('Hello');
}, 1000); // 一秒后执行回调函数
Promise
Promise 是 ES6 新增的一种机制,用来更优雅地处理异步操作。这个机制的出现解决了回调地狱的问题,也让代码更加简洁和易于维护。在 Promise 出现之前,处理异步操作往往需要使用嵌套回调(如 setTimeout、ajax 等),这会导致代码层层嵌套,难以维护,俗称“回调地狱”,看着都头晕。
Promise 的核心思想是:它代表一个未来的值,这个值在操作完成后要么成功(resolved
),要么失败(rejected
)。Promise 提供了 then
和 catch
方法,分别用于处理成功和失败的结果。
工作流程
- Pending(待定):Promise 对象刚创建时,处于“待定”状态,还没有最终结果。
- Resolved(已成功):异步操作成功完成,Promise 被标记为“已成功”,并返回一个结果。
- Rejected(已失败):异步操作失败,Promise 被标记为“已失败”,并返回一个错误原因。
用法示例
let promise = new Promise((resolve, reject) => {
let success = true;
// 模拟异步操作
setTimeout(() => {
if (success) {
resolve("成功"); // 调用 resolve 表示操作成功
} else {
reject("失败"); // 调用 reject 表示操作失败
}
}, 1000);
});
// 处理 Promise 的结果
promise
.then(result => {
console.log(result); // 如果操作成功,打印 "成功"
})
.catch(error => {
console.log(error); // 如果操作失败,打印 "失败"
});
这段代码中,new Promise() 创建了一个新的 Promise 对象。构造函数接受一个函数作为参数,这个函数有两个参数:resolve 和 reject,分别表示操作成功和失败时的回调。随后,setTimeout() 模拟了一个异步操作,异步操作结束后根据条件调用 resolve() 或 reject()。
链式调用
Promise 还支持链式调用。也就是你可以在 then() 中返回另一个 Promise,来进行更复杂的异步操作,不用写成嵌套套娃那样恶心。每个 then() 返回的值会被传递到下一个 then()。
let promise = new Promise((resolve, reject) => {
let success = true;
setTimeout(() => {
if (success) {
resolve("Step 1 成功");
} else {
reject("Step 1 失败");
}
}, 1000);
});
promise
.then(result => {
console.log(result); // 打印 "Step 1 成功"
// 返回新的 Promise
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Step 2 成功");
}, 1000);
});
})
.then(result => {
console.log(result); // 打印 "Step 2 成功"
// 再返回一个新的 Promise
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Step 3 成功");
}, 1000);
});
})
.then(result => {
console.log(result); // 打印 "Step 3 成功"
})
.catch(error => {
console.log(error); // 如果任何一个步骤失败,捕获错误
});
这段代码是这样子的:
-
第一个 Promise:
第一个 Promise 模拟一个 1 秒延迟的异步操作,成功后 resolve("Step 1 成功")。
then() 方法接收第一个 Promise 的结果并打印 "Step 1 成功"。 -
在 then() 中返回新的 Promise:
第一个 then() 处理完后,返回了一个新的 Promise,它再次模拟了一个 1 秒延迟的异步操作。
第二个 then() 接收并处理新的 Promise,打印 "Step 2 成功"。 -
再返回一个 Promise:
第二个 then() 处理完后,再次返回一个新的 Promise,继续模拟异步操作。
第三个 then() 接收并处理该 Promise,打印 "Step 3 成功"。
如果在任何一个 Promise 链的步骤中发生错误(调用 reject()),catch() 会捕获错误并进行处理。
为什么 Promise 更优雅?
避免回调地狱。在没有 Promise 之前,处理多个异步操作时往往要嵌套多个回调函数,导致代码结构复杂、难以理解。而 Promise 通过 then
链式调用,减少了嵌套。
传统回调地狱的写法:
setTimeout(() => {
console.log("Step 1");
setTimeout(() => {
console.log("Step 2");
setTimeout(() => {
console.log("Step 3");
}, 1000);
}, 1000);
}, 1000);
使用 Promise 后的写法:
new Promise((resolve) => {
setTimeout(() => {
console.log("Step 1");
resolve();
}, 1000);
}).then(() => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Step 2");
resolve();
}, 1000);
});
}).then(() => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Step 3");
resolve();
}, 1000);
});
});
错误处理机制。使用回调时,如果发生错误,往往需要在每个回调中手动处理错误。而 Promise 通过 catch
统一处理所有的错误,简化了代码逻辑。
new Promise((resolve, reject) => {
let success = false;
if (success) {
resolve("操作成功");
} else {
reject("操作失败");
}
})
.then(result => {
console.log(result);
})
.catch(error => {
console.log("发生错误: " + error);
});
链式调用。Promise 支持链式调用,使得多个异步操作可以顺序执行,每一步的输出会传递给下一步,这样写法更加直观。
new Promise((resolve) => {
resolve(1);
}).then(result => {
console.log(result); // 输出 1
return result + 1;
}).then(result => {
console.log(result); // 输出 2
return result + 1;
}).then(result => {
console.log(result); // 输出 3
});
Promise 的代码可读性和维护性都比以前更好,异步代码看起来像同步代码一样直观。
async/await
async/await
是基于 Promise 的一种简洁的异步处理方式,让代码更易于编写和理解的。
使用 async
关键字声明一个异步函数时,函数的返回值是一个 Promise,即使函数内部返回的不是 Promise,JavaScript 也会自动将其封装为 Promise。而 await
关键字只能在 async
函数内部使用,它会暂停函数的执行,直到 Promise 解决(resolved)或拒绝(rejected),从而让异步代码看起来像同步代码一样顺序执行。
async function fetchData() {
try {
let result = await someAsyncFunction(); // 等待异步操作完成
console.log(result); // 打印操作结果
} catch (error) {
console.error("发生错误:", error); // 处理异常
}
}
在这个示例中,fetchData 函数是一个异步函数,内部的 await 用来等待 someAsyncFunction() 的结果。在异步操作完成之前,代码会暂停,直到 Promise 完成,接着继续执行。如果 Promise 成功,结果会被赋值给 result。如果 Promise 失败,错误会被捕获并处理。
更复杂的情况是多个异步操作需要顺序执行。比如:
async function processData() {
try {
let data1 = await fetchData1(); // 等待第一个异步操作完成
console.log('Data 1:', data1);
let data2 = await fetchData2(); // 等待第二个异步操作完成
console.log('Data 2:', data2);
let data3 = await fetchData3(); // 等待第三个异步操作完成
console.log('Data 3:', data3);
} catch (error) {
console.error("发生错误:", error);
}
}
在这个例子中,每个 await 都等待上一个异步操作完成后才继续执行,确保操作按顺序进行。
如果希望多个异步操作并行执行,可以结合 Promise.all(),这会同时启动所有异步操作,并在所有操作都完成后返回结果。例如:
async function processParallel() {
try {
let [data1, data2, data3] = await Promise.all([fetchData1(), fetchData2(), fetchData3()]);
console.log('Data 1:', data1);
console.log('Data 2:', data2);
console.log('Data 3:', data3);
} catch (error) {
console.error("发生错误:", error);
}
}
这里,Promise.all() 接收一个 Promise 数组,并行执行这三个异步操作。只有当所有 Promise 都完成后,结果才会被返回,并通过解构赋值一次性获取。