笔记:JavaScript ES6

前言

和前面写的那篇文章一个背景,最近看 Nest.js 后端的东西发现挺多 JS 的东西,ES6 里面除了几个常用的importexport以及箭头函数其它约等于不知道,这里再整理一下。

ECMAScript 6

ES6 算是 JS 史上更新最重磅的一次,这里简单列一下 ES6 新增的东西,后面挑几个自己不熟的出来说。

变量

在ES6之前,变量是用 var 声明的,但 var 有一些问题,比如变量提升(变量在声明之前可以访问)。ES6引入了 letconst,用来解决这些问题。

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 新增的函数简洁的写法,模块化是可以通过 importexport 来管理代码,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 引入了 exportimport 语法,使得 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 语法:

  1. export const PI:导出一个常量 PI,可以在其他模块中使用。
  2. export function add:导出一个函数 add,可以在其他地方调用。
  3. 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 文件中导入 PIaddCalculator。导入的名称必须与导出时的名称一致。

默认导出

除了上面的命名导出,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 提供了 thencatch 方法,分别用于处理成功和失败的结果。

工作流程

  • 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 都完成后,结果才会被返回,并通过解构赋值一次性获取。

posted @ 2024-09-06 01:34  AurLemon  阅读(8)  评论(0编辑  收藏  举报