认识 ES Module
JavaScript没有模块化一直是它的痛点,所以才会产生我们前面学习的社区规范:CommonJS、AMD、CMD等,
所以在ES推出自己的模块化系统时,大家也是兴奋异常。
ES Module和CommonJS的模块化有一些不同之处:
- 一方面它使用了import和export关键字;
- 另一方面它采用编译期的静态分析,并且也加入了动态引用的方式;
ES Module模块采用export和import关键字来实现模块化:
- export负责将模块内的内容导出;
- import负责从其他模块导入内容;
了解:采用ES Module将自动采用严格模式:use strict
如果你不熟悉严格模式可以简单看一下MDN上的解析;
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode
1.案例代码结构组件
如果直接在浏览器中运行代码,会报如下错误:
这个在MDN上面有给出解释:
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules
- 你需要注意本地测试 — 如果你通过本地加载Html 文件 (比如一个 file://路径的文件), 你将会遇到 CORS 错误,因
- 为Javascript 模块安全性需要。
- 你需要通过一个服务器来测试。
这里可以用VSCode中有一个插件:Live Server 来解决
2. export关键字
export关键字将一个模块中的变量、函数、类等导出;
我们希望将其他中内容全部导出,它可以有如下的方式:
方式一:在语句声明的前面直接加上export关键字
方式二:将所有需要导出的标识符,放到export后面的 {}中
注意:这里的 {}里面不是ES6的对象字面量的增强写法,{}也不是表示一个对象的;
所以: export {name: name},是错误的写法;
方式三:导出时给标识符起一个别名
3.import关键字
import关键字负责从另外一个模块中导入内容
导入内容的方式也有多种:
- 方式一:import {标识符列表} from '模块';
注意:这里的{}也不是一个对象,里面只是存放导入的标识符列表内容;
- 方式二:导入时给标识符起别名
- 方式三:通过 * 将模块功能放到一个模块功能对象(a module object)上
4. Export和import结合使用
补充:export和import可以结合使用
为什么要这样做呢?
- 在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中;
- 这样方便指定统一的接口规范,也方便阅读;
- 这个时候,我们就可以使用export和import结合使用;
5. default用法
前面我们学习的导出功能都是有名字的导出(named exports):
- 在导出export时指定了名字;
- 在导入import时需要知道具体的名字;
还有一种导出叫做默认导出(default export)
- 默认导出export时可以不需要指定名字;
- 在导入时不需要使用 {},并且可以自己来指定名字;
- 它也方便我们和现有的CommonJS等规范相互操作;
注意:在一个模块中,只能有一个默认导出(default export);
6. import函数
通过import加载一个模块,是不可以在其放到逻辑代码中的:
因为:
将属于Parse阶段的放入了运行阶段会导致JS引擎识别不了,会出错
而解决这个问题则要用‘import函数’:使用 import() 函数来动态加载(import() 函数是异步加载的,import函数返回的结果是一个Promise);
ES Module解析流程
三个阶段:
- 阶段一:构建(Construction),根据地址查找js文件,并且下载,将其解析成模块记录(Module Record);
- 阶段二:实例化(Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址。
- 阶段三:运行(Evaluation),运行代码,计算值,并且将值填充到内存地址中;
阶段一:构建阶段
解析的过程是一个静态分析(不会运行代码)的过程,且不会请求重复的js文件
阶段二和三:实例化阶段 – 求值阶段
包管理工具深入解析
共享你的代码
方式一:上传到GitHub上、其他程序员通过GitHub下载我们的代码手动的引用;
- 缺点是大家必须知道你的代码GitHub的地址,并且从GitHub上手动下载;
- 需要在自己的项目中手动的引用,并且管理相关的依赖;
- 不需要使用的时候,需要手动来删除相关的依赖;
- 当遇到版本升级或者切换时,需要重复上面的操作;
- 显然,上面的方式是有效的,但是这种传统的方式非常麻烦,并且容易出错;
方式二:使用一个专业的工具来管理我们的代码
- 我们通过工具将代码发布到特定的位置;
- 其他程序员直接通过工具来安装、升级、删除我们的工具代码;
包管理工具npm
- 显然,通过第二种方式我们可以更好的管理自己的工具包,其他人也可以更好的使用我们的工具包。
- 包管理工具npm:
- Node Package Manager,也就是Node包管理器;
- 但是目前已经不仅仅是Node包管理器了,在前端项目中我们也在使用它来管理依赖的包;
- 比如express、koa、react、react-dom、axios、babel、webpack等等;
- npm管理的包可以在哪里查看、搜索呢?
https://www.npmjs.com/这是我们安装相关的npm包的官网;
- npm管理的包存放在哪里呢?
- 我们发布自己的包其实是发布到registry上面的; (registry相当于一个仓库)
- 当我们安装一个包时其实是从registry上面下载的包;
项目配置文件
事实上,我们每一个项目都会有一个对应的配置文件,无论是前端项目还是后端项目:
这个配置文件会记录着你项目的名称、版本号、项目描述等;
也会记录着你项目所依赖的其他库的信息和依赖库的版本号;
这个配置文件在Node环境下面(无论是前端还是后端)就是package.json
npm init #创建时填写信息
npm init -y # 所有信息使用默认的
常见的属性
必须填写的属性:name、version
- name是项目的名称;
- version是当前项目的版本号;
- description是描述信息,很多时候是作为项目的基本描述;
- author是作者相关信息(发布时用到);
- license是开源协议(发布时用到);
private属性:
- private属性记录当前的项目是否是私有的;
- 当值为true时,npm是不能发布它的,这是防止私有项目或模块发布出去的方式;
main属性:
- 设置程序的入口。
- 很多人会有疑惑,webpack不是会自动找到程序的入口吗?
- 这个入口和webpack打包的入口并不冲突;
- 它是在你发布一个模块的时候会用到的;
- 比如我们使用axios模块 const axios = require('axios');
- 实际上是找到对应的main属性查找文件的;
scripts属性
- scripts属性用于配置一些脚本命令,以键值对的形式存在;
- 配置后我们可以通过 npm run 命令的key来执行这个命令;
- npm start和npm run start的区别是什么?
- 它们是等价的;
- 对于常用的 start、 test、stop、restart可以省略掉run直接通过npm start等方式运行;
dependencies属性
- dependencies属性是指定无论开发环境还是生成环境都需要依赖的包;
- 通常是我们项目实际开发用到的一些库模块;
- 与之对应的是devDependencies;
devDependencies属性
- 一些包在生成环境是不需要的,比如webpack、babel等;
- 这个时候我们会通过 npm install webpack --save-dev,将它安装到devDependencies属性中;
- 疑问:那么在生成环境如何保证不安装这些包呢?
生成环境不需要安装时,我们需要通过npm install --production 来安装文件的依赖;
版本管理的问题
我们会发现安装的依赖版本出现:^2.0.3或~2.0.3,这是什么意思呢?
npm的包通常需要遵从semver版本规范:
- semver:https://semver.org/lang/zh-CN/
- npm semver:https://docs.npmjs.com/misc/semver
semver版本规范是X.Y.Z:
- X主版本号(major):当你做了不兼容的 API 修改(可能不兼容之前的版本);
- Y次版本号(minor):当你做了向下兼容的功能性新增(新功能增加,但是兼容之前的版本);
- Z修订号(patch):当你做了向下兼容的问题修正(没有新功能,修复了之前版本的bug);
我们这里解释一下 ^和~的区别:
- ^x.y.z:表示x是保持不变的,y和z永远安装最新的版本;
- ~x.y.z:表示x和y保持不变的,z永远安装最新的版本;
engines属性 (了解)
engines属性用于指定Node和NPM的版本号;
在安装的过程中,会先检查对应的引擎版本,如果不符合就会报错;
事实上也可以指定所在的操作系统 "os" : [ "darwin", "linux"],只是很少用到;
browserslist属性 (了解)
用于配置打包后的JavaScript浏览器的兼容情况,参考;
否则我们需要手动的添加polyfills来让支持某些语法;
也就是说它是为webpack等打包工具服务的一个属性(这里不是详细讲解webpack等工具的工作原理,所以不再给出详情)
npm install 命令
安装npm包分两种情况:
- 全局安装(global install): npm install yarn -g;
- 项目(局部)安装(local install): npm install
全局安装
- 全局安装是直接将某个包安装到全局:
- 比如yarn的全局安装: npm install yarn -g
但是很多人对全局安装有一些误会:
- 通常使用npm全局安装的包都是一些工具包:yarn、webpack等;
- 并不是类似于 axios、express、koa等库文件;
- 所以全局安装了之后并不能让我们在所有的项目中使用 axios等库;
项目安装
项目安装会在当前目录下生产一个 node_modules 文件夹,我们之前讲解require查找顺序时有讲解过这个包在什 么情况下被查找;
局部安装分为开发时依赖和生产时依赖:
# 安装开发和生产依赖 npm install axios npm i axios # 开发依赖 npm install webpack --save-dev npm install webpack -D npm i webpack –D # 根据package.json中的依赖包 npm install
npm install 原理
package-lock.json
- package-lock.json文件解析:
- name:项目的名称;
- version:项目的版本;
- lockfileVersion:lock文件的版本;
- requires:使用requires来跟着模块的依赖关系;
- dependencies:项目的依赖
- 当前项目依赖axios,但是axios依赖follow-redireacts;
- axios中的属性如下:
- version表示实际安装的axios的版本;
- resolved用来记录下载的地址,registry仓库中的位置;
- requires记录当前模块的依赖;
- integrity用来从缓存中获取索引,再通过索引去获取压缩包文件;
npm其他命令
我们这里再介绍几个比较常用的:
- 卸载某个依赖包:
npm uninstall package
npm uninstall package --save-dev
npm uninstall package -D
- 强制重新build
npm rebuild
- 清除缓存
npm cache clean
npm的命令其实是非常多的:
- https://docs.npmjs.com/cli-documentation/cli
- 更多的命令,可以根据需要查阅官方文档
Yarn工具
另一个node包管理工具yarn:
- yarn是由Facebook、Google、Exponent 和 Tilde 联合推出了一个新的 JS 包管理工具;
- yarn 是为了弥补 npm 的一些缺陷而出现的;
- 早期的npm存在很多的缺陷,比如安装依赖速度很慢、版本依赖混乱等等一系列的问题;
- 虽然从npm5版本开始,进行了很多的升级和改进,但是依然很多人喜欢使用yarn;
补充:node会自带npm和npx,而yarn需要另外下载才能使用
cnpm工具
- 由于一些特殊的原因,某些情况下我们没办法很好的从https://registry.npmjs.org下载下来一些需要的包。
- 这个时候,我们可以使用cnpm,并且将cnpm设置为淘宝的镜像:
npm install -g cnpm --registry=https://registry.npm.taobao.org
cnpm config get registry # https://r.npm.taobao.org/
npx工具
- npx是npm5.2之后自带的一个命令。
npx的作用非常多,但是比较常见的是使用它来调用项目中的某个模块的指令。
- 我们以webpack为例:
全局安装的是webpack5.1.3
项目安装的是webpack3.6.0
- 如果我在终端执行 webpack --version使用的是哪一个命令呢?
显示结果会是 webpack 5.1.3,事实上使用的是全局的,为什么呢?
原因非常简单,在当前目录下找不到webpack时,就会去全局找,并且执行命令;
如何解决这个问题呢?解决局部命令执行
那么如何使用项目(局部)的webpack,常见的是两种方式:
- 方式一:明确查找到node_module下面的webpack
- 方式二:在 scripts定义脚本,来执行webpack;
- 方式一:在终端中使用如下命令(在项目根目录下)
./node_modules/.bin/webpack --version
- 方式二:修改package.json中的scripts
"scripts": {
"webpack": "webpack --version"
}
- 方式三:使用npx
npx webpack --version
npx的原理非常简单,它会到当前目录的node_modules/.bin目录下查找对应的命令;
补充:npm 发布自己的包
JSON和数据存储
JSON
🤔在开发的时候我们会经常遇到JSON,JSON到底是什么?
JSON是一种非常重要的数据格式(用来保存一部分数据),它并不是编程语言,而是一种可以在服务器和客户端之间传输的数据格式。
应用场景:
- 网络数据的传输JSON数据;
- 项目的某些配置文件;
- 非关系型数据库(NoSQL)将json作为存储格式;
基本语法:
JSON的顶层支持三种类型的值:
- 简单值:数字(Number)、字符串(String,不支持单引号)、布尔类型(Boolean)、null类型;
123
- 对象值:由key、value组成,key是字符串类型,并且必须添加双引号,值可以是简单值、对象值、数组值;
{ "name":"eureka", "age": 19 }
- 数组值:数组的值可以是简单值、对象值、数组值;
[ 123, { "name":"Eureka" } ]
🚨 注意:JSON不能加注释,而且属性名称必须是双引号括起来的字符串;最后一个属性后不能有逗号。
JSON序列化
在MDN上提到:JSON 是一种语法,用来序列化对象、数组、数值、字符串、布尔值和 null 。它基于 JavaScript 语法,但与之不同:JavaScript 不是 JSON,JSON 也不是 JavaScript。
🤔为什么需要JSON序列化呢?
场景:将对象数据存储到localStorage
const obj = { name: 'eureka', age: 19, hobby:['滑板'] } localStorage.setItem("obj",obj)
如果直接将obj对象存储到localStorage,setItem会将对象类型转成string,而obj对象转成string类型,则会转成"[object object]"
,我们要存储的内容根本存不到localStorage里面。
所以我们需要将对象JSON序列化:
const obj = { name: 'eureka', age: 19, hobby:['滑板'] } // 将obj转成JSON格式的字符串 const objString = JSON.stringify(obj) // 将对象数据存储localStorage localStorage.setItem("obj", objString)
这样就可以将对象数据存储到localStorage里面了
而我们之后要用到JSON序列化之后的数据则要将其解析才能使用:
const jsonString = localStorage.getItem("obj") // 将JSON格式的字符串转回对象 const info = JSON.parse(jsonString) console.log(info)
JSON方法
序列化方法 JSON.stringify()
返回与指定值对应的 JSON 字符串,可以通过额外的参数,控制仅包含某些属性,或者以自定义方法来替换某些 key 对应的属性值。
- 上面代码一样直接转化
- stringfy第二个参数:replacer
- 如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。
- 如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;
// 对key和value起到一个拦截作用 const jsonString3 = JSON.stringify(obj, (key, value) => { if (key === "age") { return value + 1 } return value }) console.log(jsonString3)
- 如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;
// 相当于起到了过滤的作用 const jsonString2 = JSON.stringify(obj, ["name", "hobby"]) console.log(jsonString2)
- stringfy第三个参数space
指定缩进用的空白字符串,用于美化输出(pretty-print);
- 如果该参数没有提供(或者为 null),将没有空格。
- 如果参数是个数字,它代表有多少的空格;上限为 10。该值若小于 1,则意味着没有空格;
- 如果该参数为字符串(当字符串长度超过 10 个字母,取其前 10 个字母),该字符串将被作为空格;
const jsonString4 = JSON.stringify(obj, null, "---") console.log(jsonString4)
- 如果obj对象中有toJSON方法,则转化JSON字符串时会转化成toJSON方法内容的字符串
const obj = { name: 'eureka', age: 19, hobby:['滑板'], toJSON: function() { return "123456" } }
解析JSON字符串方法 JSON.parse()
解析 JSON 字符串并返回对应的值,可以额外传入一个转换函数,用来将生成的值和其属性,在返回之前进行某些修改。
- 上面代码一样直接转化
- parse第二个参数:reviver
const info = JSON.parse(JSONString, (key, value) => { if (key === "age") { return value - 1 } return value }) console.log(info)
应用
了解引用赋值和浅拷贝,主要是理解利用JSON序列化深拷贝
// obj 对象 const obj = { name: "why", age: 18, friends: { name: "kobe" }, hobbies: ["篮球", "足球"], foo: function() { console.log("foo function") } }
- 引用赋值
const info = obj
- 浅拷贝(相当于将原对象赋值一份到新对象中,但对象里面的属性对应的是另一个对象(属性是引用类型),则原对象和新对象里面的引用类型属性对应的是同一个)
const info2 = { ...obj } obj.age = 1000 console.log(info2.age) // 改变obj的属性值,info2中的属性值不变 obj.friends.name = "james" console.log(info2.friends.name) // 会改变,因为obj和info2中的friends用的是同一个
- 利用stringify和parse来实现深拷贝(让原对象和新对象完全没有关系)
const jsonString = JSON.stringify(obj) console.log(jsonString) const info3 = JSON.parse(jsonString) obj.friends.name = "curry" console.log(info3.friends.name) console.log(info3)
数据存储
Storage
WebStorage主要提供了一种机制,可以让浏览器提供一种比cookie更直观的key、value存储方式:
- localStorage:本地存储,提供的是一种永久性的存储方法,在关闭掉网页重新打开时,存储的内容依然保留;
- sessionStorage:会话存储,提供的是本次会话的存储,在关闭掉会话时,存储的内容会被清除;
localStorage.setItem("obj", "localStorage") // (key value)
sessionStorage.setItem("name", "sessionStorage")
localStorage和sessionStorage的区别
- 区别一:关闭网页后重新打开,localStorage会保留,而sessionStorage会被删除;
- 区别二:在页面内实现跳转,localStorage会保留,sessionStorage也会保留;
- 区别三:在页面外实现跳转(打开新的网页),localStorage会保留,sessionStorage不会被保留;
Storage常见方法和属性
适用于localStorage和sessionStorage
属性:
Storage.length:只读属性
返回一个整数,表示存储在Storage对象中的数据项数量;
方法:
- Storage.key():该方法接受一个数值n作为参数,返回存储中的第n个key名称;
- Storage.getItem():该方法接受一个key作为参数,并且返回key对应的value;
- Storage.setItem():该方法接受一个key和value,并且将会把key和value添加到存储中。 (如果key存储,则更新其对应的值; )
- Storage.removeItem():该方法接受一个key作为参数,并把该key从存储中删除;
- Storage.clear():该方法的作用是清空存储中的所有key;
// 1.setItem localStorage.setItem("name", "coderwhy") localStorage.setItem("age", 18) // 2.length console.log(localStorage.length) // 利用length属性对localStorage进行遍历 for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i) console.log(localStorage.getItem(key)) } // 3.key方法 console.log(localStorage.key(0)) // 4.getItem(key) console.log(localStorage.getItem("age")) // 5.removeItem localStorage.removeItem("age") // 6.clear方法 localStorage.clear()
因为我们从Srorage存入或取出都要对其内容做一个转化(JSON序列化/JSON解析),为了更好的使用,所以要对其进行封装
Storage的工具类的封装
class HYCache { constructor(isLocal = true) { this.storage = isLocal ? localStorage: sessionStorage } setItem(key, value) { if (value) { this.storage.setItem(key, JSON.stringify(value)) } } getItem(key) { let value = this.storage.getItem(key) if (value) { value = JSON.parse(value) return value } } removeItem(key) { this.storage.removeItem(key) } clear() { this.storage.clear() } key(index) { return this.storage.key(index) } length() { return this.storage.length } } const localCache = new HYCache() const sessionCache = new HYCache(false) export { localCache, sessionCache }
IndexedDB
IndexedDB是一种底层的API,用于在客户端存储大量的结构化数据。
- 它是一种事务型数据库系统,是一种基于JavaScript面向对象数据库,有点类似于NoSQL(非关系型数据库);
- IndexedDB本身就是基于事务的,我们只需要指定数据库模式,打开与数据库的连接,然后检索和更新一系列事务即可;
相对于Storage存储字符串的方式,IndexedDB以数据库的方式来存储数据更加方便我们检索数据(数据库操作效率更高)
IndexedDB数据库的使用
IndexedDB数据库增删改查
前置工作——连接数据库
- 打开indexedDB的某一个数据库
通过indexDB.open(数据库名称, 数据库版本)方法;
// 打开数据(和数据库建立连接)
const dbRequest = indexedDB.open("why", 3)
- 如果数据库不存在,那么会创建这个数据;
- 如果数据库已经存在,那么会打开这个数据库;
- 通过监听回调得到数据库连接结果:
数据库的open方法会得到一个IDBOpenDBRequest类型
拿到这个对象之后可以使用一些回调函数去监听连接数据库的状态:
- onerror:当数据库连接失败时;
dbRequest.onerror = function(err) { console.log("打开数据库失败~") }
- onsuccess:当数据库连接成功时回调; (可以拿到db对象了)
let db = null dbRequest.onsuccess = function(event) { db = event.target.result }
- onupgradeneeded:当数据库的version发生变化并且高于之前版本时回调;
- 通常我们在这里会创建具体的存储对象:db.createObjectStore(存储对象名称, { keypath: 存储的主键 })
// 第一次打开/或者版本发生升级 dbRequest.onupgradeneeded = function(event) { const db = event.target.result console.log(db) // 创建一些存储对象,keyPath 指的是主键 db.createObjectStore("users", { keyPath: "id" }) }
使用数据库
我们对数据库的操作要通过事务对象来完成,所以我们要先创建一个事务对象
- 第一步:通过db获取对应存储的事务 db.transaction(想要操作的表名称, 操作);
- 第二步:通过事务获取对应的存储对象 transaction.objectStore(想要操作的表名称,);
const transaction = db.transaction("users", "readwrite") // readwrite 即可以读又可以写 const store = transaction.objectStore("users") 想要进行操作的对象: class User { constructor(id, name, age) { this.id = id this.name = name this.age = age } } const users = [ new User(100, "why", 18), new User(101, "kobe", 40), new User(102, "james", 30), ]
新增操作
for (const user of users) { const request = store.add(user) // 通过onsuccess来监听本次操作是否成功 request.onsuccess = function() { console.log(`${user.name}插入成功`) } } // 通过oncomplete来监听本次事务所有操作是否完成 transaction.oncomplete = function() { console.log("添加操作全部完成") }
查询操作
- 查询方式一(知道主键, 根据主键查询)
const request = store.get(102) request.onsuccess = function(event) { console.log(event.target.result) }
- 查询方式二:
通过openCursor(游标)来查询 const request = store.openCursor() request.onsuccess = function (event) { const cursor = event.target.result // 先判断有没有值 if (cursor) { // 101是我们想要查询的值 if (cursor.key === 101) { console.log(cursor.key, cursor.value) } else { cursor.continue() } } else { console.log("查询完成") } }
修改操作
通过update方法进行修改 const updateRequest = store.openCursor() updateRequest.onsuccess = function (event) { const cursor = event.target.result if (cursor) { if (cursor.key === 101) { const value = cursor.value; value.name = "curry" cursor.update(value) } else { cursor.continue() } } else { console.log("修改完成") } }
删除操作
const deleteRequest = store.openCursor() deleteRequest.onsuccess = function (event) { const cursor = event.target.result if (cursor) { if (cursor.key === 101) { cursor.delete() } else { cursor.continue() } } else { console.log("删除完成") } }
Cookie
类型为“小型文本文件,某些网站为了辨别用户身份而存储在用户本地终端
(Client Side)上的数据
浏览器会在特定的情况下携带上cookie来发送请求,我们可以通过cookie来获取一些信息;
Cookie分类
按在客户端中的存储位置,Cookie可以分为内存Cookie和硬盘Cookie。
- 内存Cookie由浏览器维护,保存在内存中,浏览器关闭时Cookie就会消失,其存在时间是短暂的;
- 硬盘Cookie保存在硬盘中,有一个过期时间,用户手动清理或者过期时间到时,才会被清理;
如果判断一个cookie是内存cookie还是硬盘cookie呢?
- 没有设置过期时间,默认情况下cookie是内存cookie,在关闭浏览器时会自动删除;
- 有设置过期时间,并且过期时间不为0或者负数的cookie,是硬盘cookie,需要手动或者到期时,才会删除;
通过KOA来实现服务器设置Cookie并在下次请求中携带Cookie的过程:
const Koa = require('koa'); const Router = require('koa-router'); const app = new Koa(); const testRouter = new Router(); // 登录接口 testRouter.get('/test', (ctx, next) => { // maxAge对应毫秒 ctx.cookies.set("name", "why", { maxAge: 60 * 1000, // 设置过期时间 httpOnly: false }) ctx.body = "test"; }); testRouter.get('/demo', (ctx, next) => { // 读取cookie const value = ctx.cookies.get('name'); ctx.body = "你的cookie是" + value; }); app.use(testRouter.routes()); app.use(testRouter.allowedMethods()); app.listen(8000, () => { console.log("服务器启动成功~"); })
cookie常见属性
cookie的生命周期:
默认情况下的cookie是内存cookie,也称之为会话cookie,也就是在浏览器关闭时会自动被删除;
我们可以通过设置expires或者max-age来设置过期的时间;
- expires:设置的是Date.toUTCString(),设置格式是;expires=date-in-GMTString-format;
- max-age:设置过期的秒钟,;max-age=max-age-in-seconds (例如一年为60*60*24*365);
cookie的作用域:(允许cookie发送给哪些URL)
- Domain:指定哪些主机可以接受cookie
- 如果不指定,那么默认是 origin,不包括子域名。
- 如果指定Domain,则包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如developer.mozilla.org)。
- Path:指定主机下哪些路径可以接受cookie
例如,设置 Path=/docs,则以下地址都会匹配:
- /docs
- /docs/Web/
- /docs/Web/HTTP
目前cookie使用的越来越少了,原因如下:
取而代之的是token
BOM-DOM
BOM
JavaScript有一个非常重要的运行环境就是浏览器,而且浏览器本身又作为一个应用程序需要对其本身进行操作,所以通常浏览器会有对应的对象模型(BOM,Browser Object Model)。
我们可以将BOM看成是连接JavaScript脚本与浏览器窗口的桥梁
通过BOM可以操作浏览器窗口,比如弹出框、控制浏览器跳转、获取分辨率等。
BOM主要包括的对象模型:
- window:包括全局属性、方法,控制浏览器窗口相关的属性、方法;
- location:浏览器连接到的对象的位置(URL);
- history:操作浏览器的历史;
- document:当前窗口操作文档的对象;(html/div...)
Window全局对象
- 第一:包含大量的属性,localStorage、console、location、history、screenX、scrollX等等(大概60+个属性);
- 第二:包含大量的方法,alert、close、scrollTo、open等等(大概40+个方法);
- 第三:包含大量的事件,focus、blur、load、hashchange等等(大概30+个事件);
- 第四:包含从EventTarget继承过来的方法,addEventListener、removeEventListener、dispatchEvent方法;
对于这么多的内容,去背肯定是不现实的,而且有些内容还会改变,所以建议记几个常用的,剩下的用到再去MDN查找:
查看MDN文档时,我们会发现有很多不同的符号:
coderwhy老师对它们的解释是:
删除符号:表示这个API已经废弃,不推荐继续使用了;
点踩符号:表示这个API不属于W3C规范,某些浏览器有实现(所以兼容性的问题);
实验符号:该API是实验性特性,以后可能会修改,并且存在兼容性问题;
Window常见属性-方法-事件
- 常见的属性
console.log(window.screenX) console.log(window.screenY) window.addEventListener("scroll", () => { console.log(window.scrollX, window.scrollY) }) console.log(window.outerHeight) console.log(window.innerHeight)
- 常见的方法
const scrollBtn = document.querySelector("#scroll") scrollBtn.onclick = function() { // 1.scrollTo window.scrollTo({ top: 2000 }) // 2.close window.close() // 3.open window.open("http://www.baidu.com", "_self") // _self不另开页面 }
- 常见事件
window.onload = function() { console.log("window窗口加载完毕~") } window.onfocus = function() { console.log("window窗口获取焦点~") } window.onblur = function() { console.log("window窗口失去焦点~") } // 一般vue的路由会有 const hashChangeBtn = document.querySelector("#hashchange") hashChangeBtn.onclick = function() { location.hash = "aaaa" } window.onhashchange = function() { console.log("hash发生了改变") } 前面我们提到window继承自EventTarget,其中包含的事件也是会经常用到的: const clickHandler = () => { console.log("window发生了点击") } // 由浏览器派发click事件,所以上面的函数才会被调用 window.addEventListener("click", clickHandler) window.removeEventListener("click", clickHandler) // 监听coderwhy事件,有这个事件就会进行处理 window.addEventListener("coderwhy", () => { console.log("监听到了coderwhy事件") }) // 派发coderwhy事件(浏览器没有的事件) window.dispatchEvent(new Event("coderwhy"))
location对象
主要是对URL进行操作
location常见属性-方法
- 常见属性
- Location.href:包含整个 URL
- Location.protocol:包含 URL 对应协议,最后有一个":"。
- Location.host:包含了域名,可能在该串最后带有一个":"并跟上 URL 的端口号。
- Location.hostname:包含 URL 域名。
- Location.port:包含端口号。
- Location.pathname:包含 URL 中路径部分,开头有一个 /。
- Location.hash:URL的哈希值,开头有一个 #。
- 常见方法
- Location.assign():加载给定 URL 的内容资源到这个 Location 对象所关联的对象上。(跳转+可以回退)
- Location.reload():重新加载来自当前 URL 的资源。
- 该参数为 true 时会导致该方法引发的刷新一定会从服务器上加载数据。
- 如果是 false或没有制定这个参数,浏览器可能从缓存当中加载页面。
- Location.replace():打开一个新的URL,并且跳转到该URL中(不同的是不会在浏览记录中留下之前的记录)
history常见的属性-方法
- 常见属性
- length:会话中的记录条数;
- state:当前保留的状态值;
- 常见方法
- back():返回上一页,等价于history.go(-1);
- forward():前进下一页,等价于history.go(1);
- go():加载历史中的某一页;
- pushState():打开一个指定的地址; (跳转(不刷新网页))
- replaceState():打开一个新的地址,并且使用replace
DOM
浏览器是用来展示网页的,而网页中最重要的就是里面各种的标签元素,JavaScript很多时候是需要操作这些元素的。
JavaScript如何操作元素呢?
通过Document Object Model(DOM,文档对象模型)。
文档对象模型(Document Object Model,简称DOM),是W3C组织推荐的处理可扩展标记语言的标准编程接口。通过 DOM 提供的接口可以对页面上的各种元素进行操作(大小、位置、颜色等)。
DOM给我们提供了一系列的模型和对象,让我们可以方便的来操作Web页面,架构图如下:
把尽可能都有的属性放在父类(EventTarget和Node),有根据属性的不同来划分成其他类型,这样我们就能根据不同的需求来获取不同的内容,再针对不同的内容进行不同的操作
EventTarget
跟上面讲过的用法是一样的
const divEl = document.querySelector("#box") const spanEl = document.querySelector(".content") divEl.addEventListener("click", () => { console.log("div元素被点击") }) spanEl.addEventListener("click", () => { console.log("span元素被点击") })
Node
Node是对应节点的名称
- 常见属性
- Node.nodeName: 返回当前节点的节点名称
- Node.nodeType: 只读属性,表示的是该节点的类型。
- nodeType 属性可用来区分不同类型的节点,比如 元素, 文本 和 注释。
- Node.nodeValue: 属性返回或设置当前节点的值。
- 对于文档节点来说,nodeValue返回null. 对于 text, comment,和 CDATA 节点来说,nodeValue 返回该节点的文本内容. 对于 attribute 节点来说,返回该属性的属性值。
- childNodes:所有的子节点;
- 常见方法:
- Node.cloneNode:克隆一个 Node,并且可以选择是否克隆这个节点下的所有内容。默认情况下,节点下的内容会被克隆。
- textContent: 文本内容
- appendChild: 添加子节点
// 常见的方法 const strongEl = document.createElement("strong") strongEl.textContent = "我是strong元素" const cloneStrongEl = strongEl.cloneNode(true) //true,则该节点的所有后代节点也都会被克隆,如果为 false,则只克隆该节点本身。 divEl.appendChild(strongEl)
Document
- 常见属性
- 常见方法
// 常见的方法 // 创建元素 const imageEl = document.createElement("img") const imageEl2 = new HTMLImageElement() // 获取元素 const divEl1 = document.getElementById("box") const divEl2 = document.getElementsByTagName("div") const divEl3 = document.getElementsByName("title") const divEl4 = document.querySelector(".content") const divEl5 = document.querySelectorAll(".content") // 删除元素 document.body.removeChild(imageEl)
Element
我们平时创建的div、p、span等元素在DOM中表示为Element元素
- 常见属性
const divEl = document.querySelector("#box") // 常见的属性 console.log(divEl.id) console.log(divEl.tagName) console.log(divEl.children) console.log(divEl.className) console.log(divEl.classList) console.log(divEl.clientWidth) // 元素内部的宽度,该属性包括内边距(padding) console.log(divEl.clientHeight)// 元素内部的高度(以像素为单位),包含内边距 console.log(divEl.offsetLeft) console.log(divEl.offsetTop)
- 常见方法
- Element.getAttribute():返回元素上一个指定的属性值。如果指定的属性不存在,则返回 null 或 "" (空字符串)
- Element.setAttribute(): 设置指定元素上的某个属性值。如果属性已经存在,则更新该值;否则,使用指定的名称和值添加一个新的属性。
// 常见的方法 const value = divEl.getAttribute("age") console.log(value) divEl.setAttribute("height", 1.88)
事件监听
浏览器在某个时刻可能会发生一些事件,比如鼠标点击、移动、滚动、获取、失去焦点、输入内容等等一系列的事件,我们需要以某种方式(代码)来对其进行响应,进行一些事件的处理;
方法1:在script中直接监听;
方法2:通过元素的on来监听事件;
方法3:通过EventTarget中的addEventListener来监听;(更常用)
<div class="box" onclick="console.log('div元素被点击')"></div> --> <div class="box" onclick="divClick()"></div> function divClick() { console.log("div元素被点击2") } divEl.addEventListener("click", () => { console.log("div元素被点击3") })
事件流
如果我们同时有事件冒泡和事件捕获的监听,那么会优先监听到事件捕获的:
const spanEl = document.querySelector(".span") const divEl = document.querySelector(".container") spanEl.addEventListener("click", () => { console.log("事件冒泡:span元素被点击了") }) divEl.addEventListener("click", () => { console.log("事件冒泡:div元素被点击了") }) document.body.addEventListener("click", () => { console.log("事件冒泡:body元素被点击了") }) // 再次监听 spanEl.addEventListener("click", (event) => { console.log("事件捕获:span元素被点击了") event.stopPropagation() }, true) // 在后面添加个true就变成事件捕获了 divEl.addEventListener("click", () => { console.log("事件捕获:div元素被点击了") }, true) document.body.addEventListener("click", (event) => { console.log("事件捕获:body元素被点击了") }, true)
捕获阶段由 DOM 最顶层节点开始,然后逐级向下传播到到最具体的元素接收
冒泡阶段开始时由最具体的元素接收,然后逐级向上传播到到 DOM 最顶层节点
小结:
- JS 代码中只能执行捕获或者冒泡其中的一个阶段
onclick
和attachEvent
只能得到冒泡阶段addEventListener(type,listener[,useCapture])
第三个参数如果是 true,表示在事件捕获阶段调用事件处理程序;如果是 false (不写默认就是false),表示在事件冒泡阶段调用事件处理程序- 实际开发中我们很少使用事件捕获,我们更关注事件冒泡。
- 有些事件是没有冒泡的,比如 onblur、onfocus、onmouseenter、onmouseleave
事件对象
当一个事件发生时,就会有和这个事件相关的很多信息: 比如事件的类型是什么,你点击的是哪一个元素,点击的位置是哪里等等相关的信息;
那么这些信息会被封装到一个Event对象中; 该对象给我们提供了想要的一些属性,以及可以通过该对象进行某些操作;
- 常见属性:
- type:事件的类型;
- target:当前事件发生的元素;
- currentTarget:当前处理事件的元素;
- offsetX、offsetY:点击元素的位置;
const spanEl = document.querySelector(".span") spanEl.addEventListener("click", (event) => { console.log("span元素被点击:", event) console.log("事件的类型:", event.type) console.log("事件的元素:", event.target, event.currentTarget) console.log("事件发生的位置:", event.offsetX, event.offsetY) })
- 常见方法:
- preventDefault:取消事件的默认行为;
const aEl = document.querySelector("a") aEl.addEventListener("click", (event) => { event.preventDefault() // 阻止链接跳转 })
- stopPropagation:阻止事件的进一步传递;(可以用到事件冒泡和事件捕获)
补充:参数作用域
😅这种东西只会存在于面试题:
var x = 0 // 当函数的参数有默认值时, 会形成一个新的作用域, 这个作用域用于保存参数的值 function foo(x, y = function() { x = 3; console.log(x) }) { console.log(x) var x = 2 console.log(x) y() console.log(x) } foo() console.log(x) // undefined 执行的是第5行的console.log(x) 在作用域里没找到,且没传参数,所以有undefined // 2 执行的是第7行的console.log(x) 在函数作用域里找到2 // 3 调用了y函数,发现参数有默认值,形成新的作用域 创建一个新值x // 2 执行的是第9行的console.log(x) 在函数作用域里找到2 // 0 执行最外侧的console.log(x) 找的是全局的x,最外面的值不会跑里面去找
认识防抖节流函数
前置——认识防抖节流函数
JavaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理。
对于某些频繁的事件处理会造成性能的损耗,我们就可以通过防抖和节流来限制事件频繁的发生;
防抖函数(debounce)
防抖的过程:
- 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;
- 当事件密集触发时,函数的触发会被频繁的推迟;
- 只有等待了一段时间也没有事件触发,才会真正的执行响应函数;
应用场景:
- 输入框中频繁的输入内容,搜索或者提交信息;
- 频繁的点击按钮,触发某个事件;
- 监听浏览器滚动事件,完成某些特定操作;
- 用户缩放浏览器的resize事件;
节流函数(throttle)
节流的过程 :
- 当事件触发时,会执行这个事件的响应函数;
- 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数;
- 不管在这个中间有多少次触发这个事件,执行函数的频率总是固定的;
应用场景:
- 监听页面的滚动事件;
- 鼠标移动事件;
- 用户频繁点击按钮操作;
- 游戏中的一些设计;
生活中防抖的例子
比如说有一天我上完课,我说大家有什么问题来问我,我会等待五分钟的时间。
如果在五分钟的时间内,没有同学问我问题,那么我就下课了;
- 在此期间,a同学过来问问题,并且帮他解答,解答完后,我会再次等待五分钟的时间看有没有其他同学问问题;
- 如果我等待超过了5分钟,就点击了下课(才真正执行这个时间);
防抖就相当于游戏里的回城,假设回城需要5s的时间,被打断之后就只能再等5s
生活中节流的例子
比如说有一天我上完课,我说大家有什么问题来问我,但是在一个5分钟之内,不管有多少同学来问问题,我只会解答一个问题; (相当于5分钟回答一个问题,有固定频率)
如果在解答完一个问题后,5分钟之后还没有同学问问题,那么就下课;
使用第三方库实现防抖节流操作
使用的是underscore第三方库
Underscore的官网: https://underscorejs.org/
<input type="text"> <script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script> <script> const inputEl = document.querySelector("input") let counter = 0 const inputChange = function(event) { console.log(`发送了第${++counter}次网络请求`) } // 防抖处理 inputEl.oninput = _.debounce(inputChange, 2000) // 节流处理 inputEl.oninput = _.throttle(inputChange, 2000) </script>
手写防抖函数
简单实现
<input type="text"> <script src="./01_debounce-v1-基本实现.js"></script> <script> const inputEl = document.querySelector("input") let counter = 0 const inputChange = function(event) { console.log(`发送了第${++counter}次网络请求`) } // 防抖处理 inputEl.oninput = debounce(inputChange, 2000) </script> function debounce(fn, delay) { // 1.定义一个定时器, 保存上一次的定时器 let timer = null // 2.真正执行的函数 const _debounce = function() { // 取消上一次的定时器 // (不取消就会在最后一次性发送前面堆积的请求,而我们要实现的是到最后才请求一次) if (timer) clearTimeout(timer) // 延迟执行 timer = setTimeout(() => { // 外部传入的真正要执行的函数 fn() }, delay) } return _debounce }
功能优化——this、参数改进
在原来的基础之上将inputChange修改了一下:
<script> const inputChange = function(event) { console.log(`发送了第${++counter}次网络请求`,this,event) } </script>
直接用inputEl.oninput = inputChange
触发得到:
用第三方库触发得到:
用我们上面手写的防抖函数得到:
可见的我们的函数对this和event的处理是有问题的。
改进:
function debounce(fn, delay) { let timer = null const _debounce = function(...args) { if (timer) clearTimeout(timer) timer = setTimeout(() => { // 用apply进行绑定this、arg fn.apply(this, args) }, delay) } return _debounce }
功能优化——立即执行
功能实现:希望在第一次执行的时候就能给我立刻请求一次(可以选择立即执行/不立即执行)
改进:
function debounce(fn, delay, immediate = false) { let timer = null let isInvoke = false const _debounce = function(...args) { if (timer) clearTimeout(timer) // 判断是否需要立即执行 if (immediate && !isInvoke) { fn.apply(this, args) isInvoke = true } else { timer = setTimeout(() => { fn.apply(this, args) isInvoke = false }, delay) } } return _debounce }
功能优化——取消功能
场景:用户在输入东西之后防抖函数发送请求之前按到退出键退出了界面,这时我们就不用再发送请求
<script> // 取消功能 const cancelBtn = document.querySelector("#cancel") cancelBtn.onclick = function() { debounceChange.cancel() } <script> // 封装取消功能 _debounce.cancel = function() { if (timer) clearTimeout(timer) // 好习惯:取消或执行完毕后将默认值重置 timer = null isInvoke = false }
功能优化——函数返回值
<script> const inputChange = function(event) { console.log(`发送了第${++counter}次网络请求`, this, event) // 返回值 return "aaaaaaaaaaaa" } // 防抖处理 const debounceChange = debounce(inputChange, 3000, false, (res) => { console.log("拿到真正执行函数的返回值:", res) }) </script> function debounce(fn, delay, immediate = false, resultCallback) { let timer = null let isInvoke = false const _debounce = function(...args) { if (timer) clearTimeout(timer) if (immediate && !isInvoke) { const result = fn.apply(this, args) if (resultCallback) resultCallback(result) isInvoke = true } else { timer = setTimeout(() => { const result = fn.apply(this, args) if (resultCallback) resultCallback(result) isInvoke = false timer = null }, delay) } }) } // 封装取消功能 _debounce.cancel = function() { if (timer) clearTimeout(timer) timer = null isInvoke = false } return _debounce }
手写节流函数
节流的基本实现逻辑
基本实现
<input type="text"> <script src="./01_throttle-v1-基本实现.js"></script> <script> const inputEl = document.querySelector("input") let counter = 0 const inputChange = function(event) { console.log(`发送了第${++counter}次网络请求`) } // 节流处理 inputEl.oninput = throttle(inputChange, 2000) </script> function throttle(fn, interval, options) { // 1.记录上一次的开始时间 let lastTime = 0 // 2.事件触发时, 真正执行的函数 const _throttle = function() { // 2.1.获取当前事件触发时的时间 const nowTime = new Date().getTime() // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长时间需要去触发函数 const remainTime = interval - (nowTime - lastTime) if (remainTime <= 0) { // 2.3.真正触发函数 fn() // 2.4.保留上次触发的时间 lastTime = nowTime } } return _throttle }
功能优化——leading实现
有时候我们不想第一次输入的时候就发送请求,这时候我们最好做一个可选项供用户选择第一次的时候是否发送请求(leading: true/false)
function throttle(fn, interval, options = { leading: true }) { const { leading } = options let lastTime = 0 const _throttle = function() { const nowTime = new Date().getTime() if (!lastTime && !leading) lastTime = nowTime const remainTime = interval - (nowTime - lastTime) if (remainTime <= 0) { fn() lastTime = nowTime } } return _throttle }
功能优化——trailing 实现
用户在10s内输出了一些内容后停止输出,因为 “ 与上一次发送请求时的间隔不为10s“ 而没有发送请求
用户需求:想要在停止输出的10s后发送请求,即使最后一次输出没有达到与上一次发送请求的间隔为10s的要求
function throttle(fn, interval, options = { leading: true, trailing: false }) { const { leading, trailing } = options let lastTime = 0 let timer = null const _throttle = function() { const nowTime = new Date().getTime() if (!lastTime && !leading) lastTime = nowTime const remainTime = interval - (nowTime - lastTime) if (remainTime <= 0) { // 在最后一次达到要求的情况下是不需要加上定时器的,要取消掉 if (timer) { clearTimeout(timer) timer = null } fn() lastTime = nowTime return // return是为了防止被加上下面的定时器 } if (trailing && !timer) { timer = setTimeout(() => { timer = null lastTime = !leading ? 0: new Date().getTime() fn() }, remainTime) } } return _throttle }
功能优化——this、参数改进
与防抖中this、参数改进使用的方法是一样的,用apply进行绑定this、arg,用剩余函数...arg接收参数
function throttle(fn, interval, options = { leading: true, trailing: false }) { const { leading, trailing } = options let lastTime = 0 let timer = null // 用剩余函数...arg接收参数 const _throttle = function(...args) { const nowTime = new Date().getTime() if (!lastTime && !leading) lastTime = nowTime const remainTime = interval - (nowTime - lastTime) if (remainTime <= 0) { if (timer) { clearTimeout(timer) timer = null } //用apply进行绑定this、arg fn.apply(this, args) lastTime = nowTime return } if (trailing && !timer) { timer = setTimeout(() => { timer = null lastTime = !leading ? 0: new Date().getTime() fn.apply(this, args) }, remainTime) } } return _throttle }
功能优化——取消功能
场景:用户在输入东西之后防抖函数发送请求之前按到退出键退出了界面或按了取消按钮,这时我们就不用再发送请求
<script> // 取消功能 const cancelBtn = document.querySelector("#cancel") cancelBtn.onclick = function() { _throttle.cancel() } </script> // 在原来的外部script代码中添加取消功能的代码 _throttle.cancel = function() { if(timer) clearTimeout(timer) timer = null lastTime = 0 }
功能优化——函数返回值
使用Promise来实现,resolve( ) 将结果进行回调出去 function throttle(fn, interval, options = { leading: true, trailing: false }) { const { leading, trailing, resultCallback } = options let lastTime = 0 let timer = null const _throttle = function(...args) { return new Promise((resolve, reject) => { const nowTime = new Date().getTime() if (!lastTime && !leading) lastTime = nowTime const remainTime = interval - (nowTime - lastTime) if (remainTime <= 0) { if (timer) { clearTimeout(timer) timer = null } const result = fn.apply(this, args) if (resultCallback) resultCallback(result) resolve(result) lastTime = nowTime return } if (trailing && !timer) { timer = setTimeout(() => { timer = null lastTime = !leading ? 0: new Date().getTime() const result = fn.apply(this, args) if (resultCallback) resultCallback(result) resolve(result) }, remainTime) } }) } _throttle.cancel = function() { if(timer) clearTimeout(timer) timer = null lastTime = 0 } return _throttle }
自定义深拷贝函数
前情回顾
对象的深拷贝:在对象相互赋值时,两个对象不再有任何关系,不会互相影响
在前面我们就有使用过JSON.parse来实现深拷贝
简单实现——JSON.parse
使用过JSON.parse来简单实现深拷贝的功能
const obj = { name: "why", friend: { name: "kobe" }, foo: function() { console.log("foo function") } } const info = JSON.parse(JSON.stringify(obj)) console.log(info === obj) obj.friend.name = "james" console.log(info)
弊端:
- 这种深拷贝的方式其实对于函数、Symbol等是无法处理的;
- 并且如果存在对象的循环引用,也会报错的;
自定义函数对深拷贝函数简单实现
function isObject(value) { const valueType = typeof value return (value !== null) && (valueType === "object" || valueType === "function") } function deepClone(originValue) { // 判断传入的originValue是否是一个对象类型 if (!isObject(originValue)) { return originValue } const newObject = {} for (const key in originValue) { newObject[key] = deepClone(originValue[key]) // 对象嵌套要递归调用直到传入的值不是一个对象 } return newObject } // 测试代码 const obj = { name: "why", age: 18, friend: { name: "james", address: { city: "广州" } } } const newObj = deepClone(obj)
弊端:
功能优化——对象中其他类型的深拷贝
数组/函数 类型/Symbol/Map/Set function isObject(value) { const valueType = typeof value return (value !== null) && (valueType === "object" || valueType === "function") } function deepClone(originValue) { // 判断是否是一个Set类型(实际上少见) if (originValue instanceof Set) { return new Set([...originValue]) } // 判断是否是一个Map类型(实际上少见) if (originValue instanceof Map) { return new Map([...originValue]) } // 判断如果是Symbol的value, 那么创建一个新的Symbol if (typeof originValue === "symbol") { return Symbol(originValue.description) } // 判断如果是函数类型, 那么直接使用同一个函数 if (typeof originValue === "function") { return originValue } // 判断传入的originValue是否是一个对象类型 if (!isObject(originValue)) { return originValue } // 判断传入的对象是数组, 还是对象 const newObject = Array.isArray(originValue) ? [] : {} for (const key in originValue) { newObject[key] = deepClone(originValue[key]) } // 对Symbol的key进行特殊的处理 const symbolKeys = Object.getOwnPropertySymbols(originValue) for (const sKey of symbolKeys) { newObject[sKey] = deepClone(originValue[sKey]) // 在不同对象里面使用同一个key值是不会冲突的 } return newObject } // 测试代码 let s1 = Symbol("aaa") let s2 = Symbol("bbb") const obj = { name: "why", age: 18, friend: { name: "james", address: { city: "广州" } }, // 数组类型 hobbies: ["abc", "cba", "nba"], // 函数类型 foo: function (m, n) { console.log("foo function") console.log("100代码逻辑") return 123 }, // Symbol作为key和value [s1]: "abc", s2: s2, // Set/Map set: new Set(["aaa", "bbb", "ccc"]), map: new Map([["aaa", "abc"], ["bbb", "cba"]]) }
功能优化——循环引用
obj.info = obj 这种就叫做循环引用
问题:将 obj.info = obj 传入函数中,函数会不断地去调用,最后栈溢出报错
方案:使用WeakMap
图解:
思路:
创建一个map对象,在循环引用第二次拷贝函数的时候可以拿到map对象,拿到之后判断原来有没有设置过newObject :
- 如果没有则创建新的newObject
- 如果已经有了就不再创建新的newObject而是将已有的newObject 返回
// 在参数里面创建新的map对象 // 并且当传入参数有map对象时候不会创建新的map对象, // 可以保证 同一次循环引用时map对象唯一 function deepClone(originValue, map = new WeakMap()) { ... if (!isObject(originValue)) { return originValue } // 判断原来有没有设置过newObject if (map.has(originValue)) { return map.get(originValue) } const newObject = Array.isArray(originValue) ? []: {} // 将传入的originValue和newObject进行映射(设置newObject) map.set(originValue, newObject) for (const key in originValue) { newObject[key] = deepClone(originValue[key], map) } const symbolKeys = Object.getOwnPropertySymbols(originValue) for (const sKey of symbolKeys) { newObject[sKey] = deepClone(originValue[sKey], map) } return newObject }
自定义事件总线
知识回顾
事件总线属于一种观察者模式,其中包括三个角色:
- 发布者(Publisher):发出事件(Event);
- 订阅者(Subscriber):订阅事件(Event),并且会进行响应(Handler);
- 事件总线(EventBus):无论是发布者还是订阅者都是通过事件总线作为中台的;
当然我们可以选择一些第三方的库:
- Vue2默认是带有事件总线的功能;
- Vue3中推荐一些第三方库,比如mitt;
自定义事件总线
实现功能:事件的监听方法on; 事件的发射方法emit; 事件的取消监听off;
大体框架
class HYEventBus { constructor() { this.eventBus = {} } // 监听 on() { } // 取消 off() { } // 发送 emit() { } }
事件的监听方法on
on(eventName, eventCallback, thisArg) { let handlers = this.eventBus[eventName] if (!handlers) { handlers = [] this.eventBus[eventName] = handlers } handlers.push({ eventCallback, thisArg }) }
事件的取消监听off
off(eventName, eventCallback) { const handlers = this.eventBus[eventName] if (!handlers) return const newHandlers = [...handlers] for (let i = 0; i < newHandlers.length; i++) { const handler = newHandlers[i] if (handler.eventCallback === eventCallback) { const index = handlers.indexOf(handler) handlers.splice(index, 1) } } }
事件的发射方法emit
emit(eventName, ...payload) { const handlers = this.eventBus[eventName] if (!handlers) return handlers.forEach(handler => { handler.eventCallback.apply(handler.thisArg, payload) }) }
使用
const eventBus = new HYEventBus() // main.js eventBus.on("abc", function() { console.log("监听abc1", this) }, {name: "玛卡巴卡"}) const handleCallback = function() { console.log("监听abc2", this) } eventBus.on("abc", handleCallback, {name: "玛卡巴卡"}) // utils.js eventBus.emit("abc", 123) // 移除监听 eventBus.off("abc", handleCallback) eventBus.emit("abc", 123)