JavaScript高级语法(coderwhy版本)(六)

简介: JavaScript高级语法(coderwhy版本)

用生成器替代迭代器

// 原先用迭代器实现:
function createArrayIterator(arr) {
  let index = 0
  return {
    next: () => {  
      if (index < arr.length) {
         return { done: false, value: arr[index++] }
      } else {
        return { done: true, value: undefined }
      }
    }
  }
}

因为生成器是特殊的迭代器,所以我们可以用生成器去简化代码:

function* createArrayIterator(arr) {
  for (const item of arr) {
    yield item
  }
}

yield*

yield*是一种yield的语法糖,可以用它来生产一个可迭代对象

如下面的 yield* arr ,这个代码会依次迭代 arr 这个可迭代对象,每次迭代其中的一个值

function* createArrayIterator(arr) {
  yield* arr
}

async-await

async异步函数

async 关键字用于声明一个异步函数

// 写法一:
async function foo1() {
}
// 写法二:
const foo2 = async () => {
}
// 写法三:
class Foo {
  async bar() {
  }
}


async异步函数与普通函数的区别

1. 返回值

async 函数一定会返回一个 promise 对象。

如果一个 async 函数的返回值看起来不是 promise,那么它将会被隐式地包装在一个 promise 中。

2. 异常

普通函数:

function foo() {
  console.log("foo function start~")
  console.log("中间代码~")
  throw new Error("error message")
  console.log("foo function end~")
}
foo().catch(err => {
  console.log("error message:", err)
})
console.log("后续还有代码~~~~~")
// 输出抛出异常前面的代码并抛出异常,后面代码不执行

异步函数:

async function foo() {
  console.log("foo function start~")
  console.log("中间代码~")
  // 异步函数中的异常, 会被作为异步函数返回的Promise的reject值的
  throw new Error("error message")
  console.log("foo function end~")
}
foo().catch(err => {
  console.log("error message:", err)
})
console.log("后续还有代码~~~~~")
// 后续代码会被执行,之后打印错误信息

如果我们在async中抛出了异常,那么程序它并不会像普通函数一样报错,而是会作为Promise的reject来传递;

await关键字

await 表达式会暂停整个 async 函数的执行进程并出让其控制权,只有当其等待的基于 promise 的异步操作被兑现或被拒绝之后才会恢复进程。promise 的解决值会被当作该 await 表达式的返回值

注意:await关键字只在 async 函数内有效。如果你在 async 函数体之外使用它,就会抛出语法错误。

async/await搭配使用:

function requestData1() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(1111)
    }, 2000);
  })
}
function requestData2() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(222)
    }, 3000);
  })
}
async function foo1() {
  // 1.await跟上表达式
  const res1 = await requestData1()
  // 2. await跟上其他值
  const res2 = await 123  // "await" 对此表达式的类型没有影响
  console.log("后面的代码1", res1) 
  console.log("后面的代码2", res2)
}
foo1()
// 3.reject值
async function foo2() {
  const res3 = await requestData2()
  console.log("res3:", res3)
}
foo2().catch(err => {
  console.log("err:", err)
  })


当await后面跟着是reject值,则它返回的值会作为整个foo2返回的Promise值,await之后的代码不会执行

总结:

async/await的目的为了简化使用基于 promise 的 API 时所需的语法(promise语法糖)。

async/await的行为就好像搭配使用了生成器和 promise。

事件循环

进程和线程

操作系统的工作方式

浏览器中的JavaScript线程

JavaScript是单线程的,也就是说同一时刻只能运行一行代码

浏览器的事件循环

如果在执行JavaScript代码的过程中,有异步操作呢?

  • 中间我们插入了一个setTimeout的函数调用;
  • 这个函数被放到入调用栈中,执行会立即结束,并不会阻塞(JS线程中)后续代码的执行;

那么,传入的一个函数(比如我们称之为timer函数),会在什么时候被执行呢?

  • 事实上,setTimeout是调用了web api,在合适的时机,会将timer函数加入到一个事件队列中;
  • 事件队列中的函数,会被放入到调用栈中,在调用栈中被执行;

简单图解

浏览器的宏任务和微任务

但是事件循环中并非只维护着一个队列,事实上是有两个队列:

  • 宏任务队列(macrotask queue):ajax、setTimeout、setInterval、DOM监听、UI Rendering等
  • 微任务队列(microtask queue):Promise的then回调、 Mutation Observer API、queueMicrotask()等

那么事件循环对于两个队列的优先级是怎么样的呢?

  1. main script中的代码优先执行(编写的顶层script代码);
  2. 在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行
  • 也就是宏任务执行之前,必须保证微任务队列是空的;
  • 如果不为空,那么就优先执行微任务队列中的任务(回调);

Node的事件循环

Node的架构分析(理解)

  • 我们会发现libuv中主要维护了一个EventLoop和worker threads(线程池);
  • EventLoop负责调用系统的一些其他操作:文件的IO、Network、child-processes等
  • libuv是一个多平台的专注于异步IO的库,它最初是为Node开发的,但是现在也被使用到Luvit、Julia、pyuv等其他地方;

Node的事件循环的阶段

事件循环像是一个桥梁,是连接着应用程序的JavaScript和系统调用之间的通道

  • 无论是我们的文件IO、数据库、网络IO、定时器、子进程,在完成对应的操作后,都会将对应的结果和回调函数放到事件循环(任务队列)中;
  • 事件循环会不断的从任务队列中取出对应的事件(回调函数)来执行;

Node相较于浏览器要维护的东西很多,浏览器主要维护两个队列(简单划分为两个队列),而Node事件循环会划分Tick来执行

一次完整的事件循环Tick分成很多个阶段:

  • 定时器(Timers):本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
  • 待定回调(Pending Callback):对某些系统操作(如TCP错误类型)执行回调,比如TCP连接时接收到ECONNREFUSED(连接拒绝错误)。
  • idle, prepare:仅系统内部使用。
  • 轮询(Poll):检索新的 I/O 事件;执行与 I/O 相关的回调; (JS引擎在没有其他任务要执行时会停留在轮询的阶段检查有没有IO,一旦有IO,则下一次就从这里开始执行IO)
  • 检测(check):setImmediate() 回调函数在这里执行。
  • 关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)

Node的事件循环图解:

Node的微任务和宏任务

我们会发现从一次事件循环的Tick来说,Node的事件循环更复杂,它也分为微任务和宏任务:

  • 宏任务(macrotask):setTimeout、setInterval、IO事件、setImmediate、close事件;
  • 微任务(microtask):Promise的then回调、process.nextTick、queueMicrotask;

但是,Node中的事件循环不只是 微任务队列和 宏任务队列:

  • 微任务队列又细分为:
  • next tick queue:process.nextTick;
  • other queue:Promise的then回调、queueMicrotask;
  • 宏任务队列又细分为:
  • timer queue:setTimeout、setInterval;
  • poll queue:IO事件;
  • check queue:setImmediate;
  • close queue:close事件;

所以,在每一次事件循环的tick中,会按照如下顺序来执行代码:

  • next tick microtask queue;
  • other microtask queue;
  • timer queue;
  • poll queue;
  • check queue;
  • close queue;

JavaScript模块化

错误处理方案

函数出现错误处理

通过throw抛出错误信息并终止程序,强制调用者修改错误

/**
 * 如果我们有一个函数, 在调用这个函数时, 如果出现了错误, 那么我们应该是去修复这个错误.
 */
function sum(num1, num2) {
  // 当传入的参数的类型不正确时, 应该告知调用者一个错误
  if (typeof num1 !== "number" || typeof num2 !== "number") {
    // return undefined
    throw "parameters is error type~"
  }
  return num1 + num2
}
// 调用者(如果没有对错误进行处理, 那么程序会直接终止)
// console.log(sum({ name: "why" }, true))
console.log(sum(20, 30))
console.log("后续的代码会继续运行~")

抛出异常的补充

抛出异常的分类有:

  1. 抛出一个字符串类型(基本的数据类型):throw "error"
  2. 比较常见的是抛出一个对象类型:throw { errorCode: -1001, errorMessage: "type不能为0~" }
  3. 创建类, 并且创建这个类对应的对象:throw new HYError(-1001, "type不能为0~")
  4. 提供了一个Error(默认创建出来会有很多信息,了解即可)

Error包含三个属性:

  • messsage:创建Error对象时传入的message;
  • name:Error的名称,通常和类的名称一致;
  • stack:整个Error的错误信息,包括函数的调用栈,当我们直接打印Error对象时,打印的就是stack;

Error有一些自己的子类:

  • RangeError:下标值越界时使用的错误类型;
  • SyntaxError:解析语法错误时使用的错误类型;
  • TypeError:出现类型错误时,使用的错误类型;

const err = new Error("type不能为0")

// 可以修改error的名称和栈,但一般不会修改

err.name = "why"

err.stack = "aaaa"

  1. Error的子类:const err = new TypeError("当前type类型是错误的~")

如果函数中已经抛出了异常, 那么后续的代码都不会继续执行了

处理抛出的异常

两种处理方法:

  1. 第一种是不处理, 那么异常会进一步的抛出, 直到最顶层的调用

如果在最顶层也没有对这个异常进行处理, 那么我们的程序就会终止执行, 并且报错

  1. 使用try catch来捕获异常
function foo(type) {
  if (type === 0) {
    throw new Error("foo error message~")
  }
}
// 1.不处理, bar函数会继续将收到的异常直接抛出去
function bar() {
  foo(0)
}
// test 拿到 bar 抛出的异常
function test() {
// 2. 在上层使用try catch来捕获异常也是可以的
  try {
    bar()
  } catch (error) {
    console.log("error:", error)
  }
}
function demo() {
  test()
}
// 有对异常进行处理就会继续执行后续代码
console.log("后续的代码执行~")
补充(finally语法解释):不管有没有发生异常,finally里面的代码都会执行
try {
} catch (err) {
}finally {
}

什么是模块化

那么,到底什么是模块化开发呢?

  • 事实上模块化开发最终的目的是将程序划分成一个个小的结构
  • 这个结构中编写属于自己的逻辑代码,自己的作用域,不会影响到其他的结构;

  • 这个结构可以将自己希望暴露的变量、函数、对象等导出给其结构使用;
  • 也可以通过某种方式,导入另外结构中的变量、函数、对象等;
// 导出
exports = {sum,add,sub} 
// 导入
const {sum,add} = require('abc.js')

上面说提到的结构,就是模块;按照这种结构划分开发程序的过程,就是模块化开发的过程;

CommonJS规范和Node关系

我们需要知道CommonJS是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了 体现它的广泛性,修改为CommonJS,平时我们也会简称为CJS。

  • Node是CommonJS在服务器端一个具有代表性的实现;
  • Browserify是CommonJS在浏览器中的一种实现; (很少用了)
  • webpack打包工具具备对CommonJS的支持和转换;


所以,Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:

  • 在Node中每一个js文件都是一个单独的模块
  • 这个模块中包括CommonJS规范的核心变量:exports、module.exports、require;
  • 我们可以使用这些变量来方便的进行模块化开发


前面我们提到过模块化的核心是导出和导入,Node中对其进行了实现:

  • exports和module.exports可以负责对模块中的内容进行导出
  • require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;

案例时间:

1. exports导出:

注意:exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出;

另外一个文件中可以导入:

上面这行完成了什么操作呢?理解下面这句话,Node中的模块化一目了然

  • 意味着main中的bar变量等于exports对象;
  • 也就是require通过各种查找方式,最终找到了exports这个对象;
  • 并且将这个exports对象赋值给了bar变量;
  • bar变量就是exports对象了;


Node中实现CommonJS的本质就是对象的引用赋值

深入:module.exports

其实导出的真正实现者的是module.exports,只是官方默认module.exports=exports

一旦module.exports修改了对象,则导出的对象就与exports无关了


2. require细节

require是一个函数,可以帮助我们引入一个文件(模块)中导入的对象。

比较常见的查找规则

导入格式如下:require(X)

情况一:X是一个核心模块,比如path、http

  • 直接返回核心模块,并且停止查找

情况二:X是以 ./ 或 ../ 或 /(根目录)开头的 (在本地目录中查找)

第一步:将X当做一个文件在对应的目录下查找;

  • 1.如果有后缀名,按照后缀名的格式查找对应的文件
  • 2.如果没有后缀名,会按照如下顺序:
  • 1> 直接查找文件X
  • 2> 查找X.js文件
  • 3> 查找X.json文件
  • 4> 查找X.node文件

第二步:没有找到对应的文件,将X作为一个目录

查找目录下面的index文件

  • 1> 查找X/index.js文件
  • 2> 查找X/index.json文件
  • 3> 查找X/index.node文件

如果没有找到,那么报错:not found

情况三:直接是一个X(没有路径),并且X不是一个核心模块

/Users/coderwhy/Desktop/Node/TestCode/04_learn_node/05_javascript-module/02_commonjs/main.js中编写require('why’)

path是查找路径(上图可以由console.log(module)得到path)

如果上面的路径中都没有找到,那么报错:not found

3. 模块的加载过程

结论一:模块在被第一次引入时,模块中的js代码会被运行一次

扩: 加载过程是同步的

结论二:模块被多次引入时,会缓存,最终只加载(运行)一次

  • 为什么只会加载运行一次呢?
  • 这是因为每个模块对象module都有一个属性:loaded。
  • 为false表示还没有加载,为true表示已经加载;

结论三:如果有循环引入,那么加载顺序是什么?

如果出现下图模块的引用关系,那么加载顺序是什么呢?

这个其实是一种数据结构:图结构;

图结构在遍历的过程中,有深度优先搜索(DFS, depth first search)和广度优先搜索(BFS, breadth first search);

Node采用的是深度优先算法:main -> aaa -> ccc -> ddd -> eee ->bbb

4. CommonJS缺点


5. CMD规范(了解即可)

CMD规范也是应用于浏览器的一种模块化规范:

  • CMD 是Common Module Definition(通用模块定义)的缩写;
  • 它也采用了异步加载模块,但是它将CommonJS的优点吸收了过来;
  • 但是目前CMD使用也非常少了;

CMD也有自己比较优秀的实现方案:

  • SeaJS

SeaJs

第一步:下载SeaJS

第二步:引入sea.js和使用主入口文件

  • seajs是指定主入口文件的


目录
相关文章
|
3月前
|
JavaScript 测试技术 API
跟随通义灵码一步步升级vue2(js)项目到vue3版本
Vue 3 相较于 Vue 2 在性能、特性和开发体验上都有显著提升。本文介绍了如何利用通义灵码逐步将 Vue 2 项目升级到 Vue 3,包括备份项目、了解新特性、选择升级方式、升级依赖、迁移组件和全局 API、调整测试代码等步骤,并提供了注意事项和常见问题的解决方案。
108 4
|
3月前
|
JavaScript 前端开发
JavaScript 函数语法
JavaScript 函数是使用 `function` 关键词定义的代码块,可在调用时执行特定任务。函数可以无参或带参,参数用于传递值并在函数内部使用。函数调用可在事件触发时进行,如用户点击按钮。JavaScript 对大小写敏感,函数名和关键词必须严格匹配。示例中展示了如何通过不同参数调用函数以生成不同的输出。
|
3月前
|
JavaScript 前端开发 索引
JavaScript ES6及后续版本:新增的常用特性与亮点解析
JavaScript ES6及后续版本:新增的常用特性与亮点解析
78 4
|
4月前
vite.config.js中vite.defineConfig is not defined以及创建最新版本的vite项目
本文讨论了在配置Vite项目时遇到的`vite.defineConfig is not defined`错误,这通常是由于缺少必要的导入语句导致的。文章还涉及了如何创建最新版本的Vite项目以及如何处理`configEnv is not defined`的问题。
241 3
vite.config.js中vite.defineConfig is not defined以及创建最新版本的vite项目
|
2月前
|
JavaScript Linux iOS开发
详解如何实现自由切换Node.js版本
不同的项目中需要使用不同版本的 Node.js,有时旧项目需要旧版本,而新项目则可能依赖最新的 Node.js 版本
86 0
|
4月前
|
移动开发 前端开发 JavaScript
JS配合canvas实现贪吃蛇小游戏_升级_丝滑版本_支持PC端和移动端
本文介绍了一个使用JavaScript和HTML5 Canvas API实现的贪吃蛇游戏的升级版本,该版本支持PC端和移动端,提供了丝滑的转向效果,并允许玩家通过键盘或触摸屏控制蛇的移动。代码中包含了详细的注释,解释了游戏逻辑、食物生成、得分机制以及如何响应不同的输入设备。
91 1
JS配合canvas实现贪吃蛇小游戏_升级_丝滑版本_支持PC端和移动端
|
3月前
|
JavaScript 算法 内存技术
如何降低node.js版本(nvm下载安装与使用)
如何降低node.js版本(nvm下载安装与使用)
|
3月前
|
JavaScript 前端开发 大数据
在JavaScript中,Object.assign()方法或展开语法(...)来合并对象,Object.freeze()方法来冻结对象,防止对象被修改
在JavaScript中,Object.assign()方法或展开语法(...)来合并对象,Object.freeze()方法来冻结对象,防止对象被修改
53 0
|
4月前
|
JavaScript Linux 开发者
一个用于管理多个 Node.js 版本的安装和切换开源工具
【9月更文挑战第14天】nvm(Node Version Manager)是一个开源工具,用于便捷地管理多个 Node.js 版本。其特点包括:版本安装便捷,支持 LTS 和最新版本;版本切换简单,不影响开发流程;多平台支持,包括 Windows、macOS 和 Linux;社区活跃,持续更新。通过 nvm,开发者可以轻松安装、切换和管理不同项目的 Node.js 版本,提高开发效率。
134 4
|
5月前
|
JavaScript 前端开发
JavaScript基础&实战(1)js的基本语法、标识符、数据类型
这篇文章是JavaScript基础与实战教程的第一部分,涵盖了JavaScript的基本语法、标识符、数据类型以及如何进行强制类型转换,通过代码示例介绍了JS的输出语句、编写位置和数据类型转换方法。
JavaScript基础&实战(1)js的基本语法、标识符、数据类型