ES6 知识总结【ES6】

210 阅读9分钟

ES6

ES6模块化

模块化的好处

回顾nodejs中模块化的使用

node.js 遵循了 CommonJS 的模块化规范。其中:

  • 导入其它模块使用 require() 方法
  • 模块对外共享成员使用module.exports 对象

ES6模块化中定义

  • 每个 js 文件都是一个独立的模块
  • 导入其它模块成员使用 import 关键字
  • 向外共享模块成员使用 export 关键字

使用方式

node.js 中默认仅支持 CommonJS 模块化规范,若想基于 node.js 体验与学习 ES6 的模块化语法,可以按照 如下两个步骤进行配置:

  1. 确保安装了 v13.0.0 或更高版本的 node.js

  2. package.json 的根节点中添加 "type": "module" 节点

    package.png

ES6模块语法

ES6 的模块化主要包含如下 3 种用法:

  • 默认导出与默认导入
  • 按需导出与按需导入
  • 直接导入并执行模块中的代码

默认导出与默认导入

默认导出的语法: export default 默认导出的成员

const a = 10
const b = 20

const fn = () => {
    console.log('这是一个函数')
}

// 默认导出
// export default a  // 导出一个值
export default {
    a,
    b,
    fn
}

默认导入的语法: import 接收名称 from '模块路径'

import result from './默认导出.js'
console.log(result)

注意:

  1. 每个模块中,只允许使用唯一的一次 export default

  2. 这里的路径必须补全后缀名,不然会报错。

    不不全.png

按需导出与按需导入

按需导出的语法: export const 变量 = 值

export const dao = 'dao'

按需导入的语法: import { 按需导入的名称 } from '模块标识符'

import { name as dao } from "./common/02-按需导出.js";
let love = '刀'
console.log(love, name);

注意:

  • 每个模块中可以有多次按需导出
  • 按需导入的成员名称必须和按需导出的名称保持一致
  • 按需导入时,如果目前模块的变量与导入的变量重名,可以使用 as 关键字进行重命名
  • 按需导入可以和默认导入一起使用
    export const that = 'dao'
    
    let miss = 'ze'
    let think = 'tie'
    
    export default {
        love,
        miss,
        think,
        look: 'zhan'
    }
    
    import obj, { that } from "./common/03-按需导出与默认导出.js";
    console.log(obj, that); // obj拿到全部的值,that拿到按需导出的值
    

直接导入模块(无导出)

如果只想单纯地执行某个模块中的代码,并不需要得到模块中向外共享的成员。

//xxx.js
for (let i = 0; i < 10; i++) {
  console.log(i)
}

// 导入该模块
import './xxx.js'

Promise

Promise 能够处理异步程序。

Promise简介

  • Promise对象可以解决回调地狱的问题
  • Promise 是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大
  • Promise 可以理解为一个容器,里面可以编写异步程序的代码`
  • 从语法上说,Promise 是一个对象,使用的使用需要 new

Promise简单使用

  1. 定义 Promise
    let p = new Promise((resolve, reject) => {
        // 形参resolve,单词意思是 完成
        // 形参reject ,单词意思是 失败
        fs.readFile('./a.txt', 'utf-8', (err, data) => {
    
            if (err) {
                // 失败,就告诉别人,承诺失败了
                reject(err);
            } else {
                // 成功,就告诉别人,承诺实现了
                resolve(data.length);
            }  
        });
    });
    
  2. 获取结果
    // promise的成功回调函数触发then,失败回调函数触发catch
        p.then(res => {
            console.log(res);
        }).catch(res => {
            console.log(res);
        })
    

三种状态

  • 最初状态: pending ,等待中,此时promise的结果为 undefined;
  • resolve(value) 调用时,达到最终状态之一: fulfilled,(成功的)完成,此时可以获取结果 value
  • 当 reject(error) 调用时,达到最终状态之一:rejected,失败,此时可以获取错误信息 error

当达到最终的 fulfilledrejected 时,promise 的状态就不会再改变了。

特点

当调用 resolve 的时候, Promise 将到达最终的状态。 达到最终状态之后, Promise 的状态就不会再改变了。

多次调用 resolve 函数,只有第一次有效,其他的调用都无效。

then方法的链式调用

  • 前一个 then 里面返回的字符串,会被下一个 then 方法接收到。但是没有意义;
  • 前一个 then 里面返回的 Promise 对象,并且调用 resolve 的时候传递了数据,数据会被下一个 then 接收到
  • 前一个 then 里面如果没有调用 resolve ,则后续的then不会接收到任何值,后续的代码不会执行。
const p = new Promise(function(resolve, reject) {
    resolve()
})

p.then(res => {
    console.log(11);
    return 22
}).then(res => {
    console.log(res);
    return 33
}).then(res => {
    console.log(res);
    return 44
}).then(res => {
    console.log(res);
    return 55
}).then(res => {
    console.log(res);
})

结果输出:11 22 33 44 55

错误处理

对于 promise 而言,错误处理的方式为添加 .catch() 方式调用 reject 的方法。

then 链式调用中某个 then 触发了 reject ,后续的 then 不会继续执行。

function sleep(num) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (num == 2) {
                reject('出错啦')
            } else {
                resolve(num)
            }
        }, 1000);
    })
}
sleep(1).then(res => {
    console.log(res);
    return sleep(2)
}).then(res => {
    console.log(res);
    return sleep(3)
}).then(res => {
    console.log(res);
    return sleep(4)
}).then(res => {
    console.log(res);
}).catch(res => {
    console.log(res);
})

结果如下所示:

错误处理.png

作业小练

promise 封装 fs 读文件函数

需求:封装一个函数,可以执行 fs 读文件的功能,以 then 的形式返回结果。

思路

  1. 引入 fs 的模块,封装一个函数,形参为文件路径。
    import fs from "fs";
    const readFile = (url) => {}
    
  2. 返回一个 promise 的对象,去调用 fs 的读文件内置对象。
    const readFile = (url, code) => {
        return new Promise((resolve, reject) => {
        })
    }
    
  3. 失败调用 reject 方法;调用成功后触发 resolve 方法。
    const readFile = (url, code) => {
        return new Promise((resolve, reject) => {
            fs.readFile(url, code, (err, data) => {
                if (err) {
                    reject(err)
                } else {
                    resolve(data)
                }
            })
        })
    }
    

结果只需要调用函数,传递路径,结果通过 then 获取即为成功调用。

readFile('./1.json', 'utf-8').then(res => {
    console.log(res);
})

promise 封装 axios 发请求函数

需求:不引用 axios 官方包,通过自己结合 promise 的方式封装一个 axios 函数,能够实现 axios.get() 获取到真正的数据。

思路

  1. 单看 axios.get() 这段代码,根据所学知识,我们能判断出只有对象才有 obj.key 的属性,因此,要设置 axios 为一个对象。
    const axios = {}
    
  2. 该对象中有一个 get() 函数,传递两个形参,一个是 url 请求地址,一个是 params 传递的数据。
    const axios = {
        get(url,params){}
    }
    
  3. axios.get() 是一个异步操作,结果通过 .then(res=>{console.log(res)} 来返回,而 promise 方法刚好用于处理异步操作,在执行成功后也是通过触发resolve() 方法传递参数给 .then() 方法返回结果,因此返回一个 promise 对象是最好的选择。
    const axios = {
        get(url,params){
            return new Promise((resolve,reject)=>{})
        }
    }
    
  4. 后续操作就很简单了,用 ajax 的知识点即可,创建一个 XMLHttpRequest() 对象,设置请求方式和地址 xhr.open() ,设置请求发送 xhr.send() ,设置请求成功返回的回调函数 xhr.onload=function(){} 即可。
    const axios = {
        get(url, params) {
            return new Promise((resolve, reject) => {
                // 创建xhr异步对象
                const xhr = new XMLHttpRequest();
                // xhr设置请求方式请求地址和发送请求
                if (params) {
                    xhr.open('get', url + `?id=${params.id}`)
                } else {
                    xhr.open('get', url)
                }
                xhr.send();
                // xhr执行完毕触发其回调函数
                xhr.addEventListener('load', function() {
                    resolve(JSON.parse(xhr.responseText))
                })
            })
        }
    }
    

使用的时候 axios.get() 是一个 promise 对象,成功后数据通过 then() 输出。

axios.get('http://www.itcbc.com:3006/api/getbooks', {
    id: 15949
}).then(res => {
    console.log(res);
})

宏任务和微任务、事件循环

宏任务

可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。

任务(代码)宏任务环境
script宏任务浏览器
事件宏任务浏览器
网络请求(Ajax)宏任务浏览器
setTimeout() 定时器宏任务浏览器 / Node
fs.readFile() 读取文件宏任务Node

微任务

微任务(microtask)是宏任务中的一个部分,它的执行时机是在同步代码执行之后,下一个宏任务执行之前。

微任务包含:

Promise.then
process.nextTick(Node.js 环境)

运行机制

  • 从上往下读取代码,把所有异步任务放到任务队列中,解析所有同步任务。
  • 同步任务解读完毕,开始解读异步任务。
  • 从上往下执行,先执行异步任务里的同步任务,完毕后执行异步任务中的微任务,最后执行宏任务。

asyncawait

作用

将异步请求的结果以返回值的方式返回给我们,我们不用关心它是不是异步

  • async 用于修饰一个 function

    • async 修饰的函数,总是返回一个 Promise 对象
    • 函数内的返回值,将自动包装在 resolved 的 promise 中
    async function f1() {
        const res = 33
        console.log(res); // 33
    }
    console.log(f1()); // Promise {<fulfilled>: undefined}
    
  • await 只能出现在 async 函数内

    • await 让 JS 引擎等待直到promise完成并返回结果
    • 语法:let value = await promise对象; // 要先等待promise对象执行完毕,才能得到结果
    • 由于await需要等待promise执行完毕,所以await会暂停函数的执行,但不会影响其他同步任务
    async function f1() {
        console.log(1);
        await f2();
        await f3()
        console.log(3);
    }
    f1() // 结果输出 1 1000 1500 3
    
    function f2() {
        return new Promise(resolve => {
            setTimeout(() => {
                console.log(1000);
                resolve()
            }, 1000)
        })
    }
    
    function f3() {
        return new Promise(resolve => {
            setTimeout(() => {
                console.log(1500);
                resolve(1500)
            }, 1500)
        })
    }
    

    注意:

    使用await 时会等到 promise 对象执行完毕,状态从等待切换为成功或失败时才会往下执行,否则一直等待。

    async function f1() {
        console.log(1);
        await f2();
        await f3()
        console.log(3);
    }
    f1() // 1 1000
    
    function f2() {
        return new Promise(resolve => {
            setTimeout(() => {
                console.log(1000);
            }, 1000)
        })
    }
    
    function f3() {
        return new Promise(resolve => {
            setTimeout(() => {
                console.log(1500);
            }, 1500)
        })
    }
    

    结果只输出 1 1000,由于 f2 没有调用 resolve 方法,切换不了状态,则一直等待。

错误处理

对于promise而言,错误处理的方式为添加.catch()方式调用reject的方法

当then链式调用中某个then触发了reject,后续的then不会继续执行

function sleep(num) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (num == 2) {
                reject('出错啦')
            } else {
                resolve(num)
            }
        }, 1000);
    })
}

/* 对于async和await而言,错误处理为try catch
正常的代码写在try内,如果报错,则执行catch内的代码
同 then链式调用 一样,一旦出现问题后续不再执行 */
async function init() {
    try {
        const p1 = await sleep(1)
        console.log(p1);
        const p2 = await sleep(2)
        console.log(p2);
        const p3 = await sleep(3)
        console.log(p3);
        const p4 = await sleep(4)
        console.log(p4);
    } catch (error) {
        console.log(error);
    }
}
init()

自制接口并实现数据渲染

前置知识

  1. express :一个基于 nodejsWeb 开发框架
    • 引入
      import express from "express";
      
    • 设置对象
      const app = express()
      
  2. 获取参数:
    • getdeletereq.query
    • postputpatchreq.body ,需要先提前设置好 `app.use(express.urlencoded())
  3. 回调函数的参数
    • req:全称 requset,包含请求信息,如获取参数
    • res:全程 respone,包含响应信息,通过 res.send() 发送响应数据
  4. 第三方读写文件 npm 下载第三方读写文件的包 npm i fs-then ,这个包支持 promise,极大方便我们的效率。

搭建服务器

把端口号和每个请求方式的 url 地址搭建好。

import express from "express";
import fs from "fs-then";
const app = express()

app.get('/api/getbooks', async(req, res) => {

})

app.post('/api/addbook', async(req, res) => {

})

app.delete('/api/delbook', async(req, res) => {

})

app.put('/api/updatebook', async(req, res) => {

})

app.listen(3000, () => {
    console.log('3000 open success');
})

创建接口

get 接口

一般而言,get 接口用于获取数据,比如全部图书列表等,因此非必须返回参数,只需要读取 json 数据文件并返回响应状态、响应信息和请求好的数据即可。

app.get('/api/getbooks', async(req, res) => {
    let data = await fs.readFile('./1.json', 'utf-8')
    res.send({
        status: 0,
        message: '成功~',
        data: JSON.parse(data)
    })
})

get.png

post 接口

post 接口获取到的数据通过 req.body 来获得,需要先提前设置好 app.use(express.urlencoded()) ,否则无法获取到数据。

由于要新增数据,因此要先获取到原数据文件内的数据,运用到的知识点为 fs.readFile ,用一个变量 data 来存储。由于是异步操作,因此用到 asyncawait ,拿到其回调函数的返回值,避免回调地狱的产生。

读取到文件后去获取用户输入的信息,拿到的是一个对象形式,并根据数组最后一个元素的id号自增1,赋给获取到的元素。为对象数据类型添加新属性的方法为 obj.key=value ,最后追加到数组 data 内,数组追加元素的方法为 arr.push()

追加完数据后还不能结束,现在只是改变了变量内的值,需要我们把追加好数据的数组重新写回文件中。写文件的操作为 fs.writeFile

app.post('/api/addbook', async(req, res) => {
    let data = JSON.parse(await fs.readFile('./1.json', 'utf-8'))
    let row = req.body;
    row.id = data[data.length - 1].id + 1
    data.push(row)
    await fs.writeFile('./1.json', JSON.stringify(data))
    res.send({
        status: 0,
        message: '添加成功'
    })
})

post.png

delete 接口

delete 方法一般用于删除数据,数据一般由 paramsurl 拼接的方式传递,因此用 req.query 的方式获取到数据。

首先先读取数据文件的内容,然后获取到传递的数据,打印后发现是对象的形式,由于我们需要的是 id ,因此需要 req.query.id

获取到 id 值后对数据数组进行遍历,对数组内某个值做删除处理我们之前做过很多次了,使用 filter 方法是较合适的选择,返回所有 id 值不等于获取到的 id 即可,赋值给变量 list

最后把新数组 list 写回文件内即可,还可以再做点细节处理。

app.delete('/api/delbook', async(req, res) => {
    let data = JSON.parse(await fs.readFile('./1.json', 'utf-8'))
    if (data.length != 0) {
        let id = req.query.id
        let list = data.filter(item => {
            return item.id != id
        })
        fs.writeFile('./1.json', JSON.stringify(list))
        res.send({
            status: 0,
            message: '删除图书成功'
        })
    } else {
        res.send({
            status: 1,
            message: '没有数据'
        })
    }
})

put 接口

put 接口用于数据修改,需要拿到 id 号和 用户想要修改的值 name 。由于传递的方式是放在对象 data 内,因此用 req.body 来获取传递的参数。

读取数据文件,分别拿到 idname ,对数据遍历,把数据内的 name 属性的值改为获取到的值,再写文件即可。

app.put('/api/updatebook', async(req, res) => {
    let data = JSON.parse(await fs.readFile('./1.json', 'utf-8'))
    let id = req.body.id
    let newname = req.body.name
    data.forEach(item => {
        if (item.id == id) {
            item.name = newname
        }
    });
    fs.writeFile('./1.json', JSON.stringify(data))
    res.send({
        status: 0,
        message: '修改成功'
    })
})

前端数据渲染

页面初始化

首先先初始化页面,设置一个基础的表格并引入 axios 包,后续的数据都拼接在 tbody 内。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>前端页面</title>
</head>

<body>
    <div class="main">
        <input type="button" class="btnadd" value="添加">
        <input type="text" id="add">
        <table>
            <thead>
                <tr>
                    <th>ID</th>
                    <th>名字</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody>
            </tbody>
        </table>
    </div>
    <script src="./js/axios.js"></script>
</body>

</html>

获取数据做页面渲染

调用 get 接口发送 get 请求获取数据,并对数据遍历拼接到 tbody 中。由于发送 axios 请求是异步操作,因此用到了 asyncawait 的知识点,等待异步处理结束。

async function getbook() {
    let data = await axios('http://127.0.0.1:3000/api/getbooks')
    console.log(data);
    let res = data.data.data
    let html = ''
    res.forEach(item => {
        html += `
            <tr>
                <td>${item.id}</td>
                <td>${item.name}</td>
                <td><button class="btndel" data-id="${item.id}">删除</button></td>
                <td><button class="btnedit" data-id="${item.id}"><a href="./11-图书编辑页面.html?id=${item.id}">编辑</a></button></td>
            </tr>
        `
    });
    document.querySelector('tbody').innerHTML = html
    delbook()
}
getbook()

删除图书

删除图书需要传递 id ,根据前面的知识我们很轻松就能想出方法:在数据渲染的时候为删除按钮绑定自定义属性id即可,点击按钮后获取自定义属性。

由于删除按钮是一个动态元素,一般用事件冒泡来处理,但这次我们换一个方法。声明一个删除函数,获取所有删除按钮,循环遍历添加点击事件,调用函数方法的代码写在拼接成功的代码后面,这样就能确保渲染完后再获取到所有按钮。

async function getbook() {
    document.querySelector('tbody').innerHTML = html
    delbook()
}
getbook()

把拿到的自定义属性 id 传递给后台, delete 传递数据的方式是 params ,异步处理结束后重新调用一次 getbook() 方法局部刷新数据。

function delbook() {
    let btndel = document.querySelectorAll('.btndel')
    btndel.forEach((ele, index) => {
        ele.addEventListener('click', async function() {
            let id = this.dataset.id
            let res = await axios.delete('http://127.0.0.1:3000/api/delbook', {
                params: {
                    id
                }
            })
            getbook()
        })
    })
}

新增图书

新增数据调用 post 接口来发送请求,对象的方式传递数据。

function addbook() {
    btnadd.addEventListener('click', async function() {
        let name = add.value
        let data = await axios.post('http://127.0.0.1:3000/api/addbook', {
            name
        })
        console.log(data);
        getbook()
    })
}
addbook()

但是我们点击后发现,数据发送成功,但是后台接收不到,显示为空数据对象{} ,返回的是 undefined ,用 APIfox 软件测试发现能够发送数据,点开调试工具查看发送的请求,查看响应标头,如下图所示。

响应头.png

联想之前要获取到 urlencoded 类型数据时,我们设置了一条 app.use(express.urlencoded()); 代码,因此我们可以合理怀疑,我们还需要再加一条类似的代码来识别 json 格式的数据。最后经过百度查询资料,我们确定了要下载 body-parser 第三方包,语法格式如下。

import body_parser from "body-parser";
app.use(body_parser.json())

这样就能接收到用户的数据了。

查询单个图书

想要实现编辑图书,首先需要拿到所有被编辑的图书数据,即获取到单个图书信息,因此需要后台写一个新的接口。

获取数据,一般为 get 请求,params 传递 id 参数,因此用 req.query.id 拿到 id ,读取文件,遍历数组对象,找到 id 号相等的数据,如果没有值数组长度为0的时候返回错误信息,否则输出返回数据。

app.get('/api/getOne', async(req, res) => {
    let data = JSON.parse(await fs.readFile('./1.json', 'utf-8'))
    let id = req.query.id
    let list = data.filter(item => item.id == id)
    if (list.length != 0) {
        res.send({
            status: 0,
            message: '成功',
            data: list
        })
    } else {
        res.send({
            status: 1,
            message: '失败'
        })
    }
})

点击编辑超链接后跳转到编辑页面,此时我们需要在新的页面拿到 id 参数,有两个解决方案:

  1. 利用本地存储先存储再获取
  2. url 拼接数据的方式在跳转的时候拼接。

我们使用第二种方式,在数据渲染的时候就拼接 id 数据。

<button class="btnedit" data-id="${item.id}"><a href="./11-图书编辑页面.html?id=${item.id}">编辑</a></button>

在新的页面获取数据。

let id = location.search.split('=')[1]

跳转页面后立刻发送查询单个数据的请求,把结果渲染到页面的表单中。

let ipt = document.querySelector('input')
async function editbook() {
    let res = await axios('http://127.0.0.1:3000/api/getOne', {
        params: {
            id
        }
    })
    console.log(res);
    ipt.value = res.data.data[0].name
}
editbook()

修改完点击按钮获取最新的表单内容,发送修改请求,传递 idname 即可,最后返回原页面。

btn.addEventListener('click', async function() {
    let name = ipt.value
    let res = await axios.put('http://127.0.0.1:3000/api/updatebook', {
        id,
        name
    })

    location.href = './10-显示图书.html'
})

出现的小问题

自动刷新与跨域

在处理业务实现功能时,每当数据被修改删除添加,页面总会自动刷新,这是因为我们使用了 VS Code 的一个插件,该插件会监听整个文件夹的全部文件,只要有一个文件内容发生改变,则自动刷新一次页面。

因此为了达到局部刷新页面,我们要用最基本的页面打开方式,但是打开控制台发现报错,跨域错误。

同源的条件是:协议、端口号、域名/ip地址一致,只要有一个不符合要求即为跨域。直接打开html页面采用的是 file协议,与http协议不一致,因此跨域。

解决跨域的问题很简单,后端引入第三方包 cors ,设置 app.use(cors()) 即可解决。