一、错误处理方案
-
开发中我们会封装一些工具函数,封装之后给别人使用
- 在其他人使用的过程中,可能会传递一些参数
- 对于函数来说,需要对这些参数进行验证,否则可能得到是我们想不到的结果
-
很多时候我们可能验证到不是希望得到的参数时,就会直接return
- 但是return存在很大的弊端:调用者不知道是因为函数内部没有正常执行,还是执行结果就是一个undefined
- 事实上,正确的做法应该是如果没有通过某些验证,那么应该让外界知道
-
如何可以让一个函数告知外界自己内部出现了问题呢?
- 通过throw关键字,抛出了一个异常
-
throw语句:
- throw语句用于抛出用户自定义的异常
- 当遇到throw语句时,当前的函数执行会停止(throw后面的语句不会执行)
1.throw后面跟的类型
- 基本的数据类型
- 对象(可以显示更多的信息)
- 可以封装一个类,然后创建这个类的对象
- 使用js提供的Error类,它可以返回一个更加详细的信息,例如:异常信息、调用栈
- 使用Error的子类
- TypeError
- SyntaxError
- RangeError
1.1 Error类型
Error类包含三个属性:
- message:创建Erro对象传入的message
- name:Error的名称,通常和类的名称一致
- stack:整个Error的错误信息,包含函数的调用栈、当我们直接打印Error对象时,打印的就是调用栈
Error有自己的子类:
- RangeError:下标值越界时使用的错误类型
- SyntaxError:解析语法错误时使用的错误类型
- TypeError:出现类型错误时,使用的错误类型
2. 异常的处理
我们会发现在之前的代码中,一个函数抛出了异常,调用它的时候程序会终止执行。
- 这是因为如果我们在调用一个函数时,如果这个函数发生了异常,但是我们没有对这个异常进行捕获,所以这个异常会继续传递到上一个函数调用中。
- 而如果到了顶层调用,还是没有进行异常的处理,则这个时候会报错,并且程序终止执行。
例如下面的异常:
- 在bar函数调用foo函数时,发生了异常,但是并没有进行异常处理。
- 此时这个异常会被传递到调用bar函数的demo函数,但是demo函数也没有进行异常处理。
- 此时这个异常会被传递到调用demo函数的全局作用域,但是全局作用域也没有进行异常处理,此时会抛出一个错误,并程序终止。
function foo(){
throw new Error("foo函数发生了异常")
}
function bar(){
foo()
}
function demo(){
bar()
}
demo()
3. try...catch
如果代码可能会存在问题,可以使用try...catch进行捕获异常。
- try{}:将可能发生异常的代码放在里面
- catch(err){}:进行捕获异常,在ES10增加了一个新特性,如果在catch语句中不需要err信息,可以将参数省略,直接使用catch{}
- finally{}:最后都会执行的代码
function foo(){
throw new Error("foo函数发生了异常")
}
function bar(){
try{
foo()
}catch(err){
console.log("err:",err);
}finally{
console.log("finally代码执行");
}
}
function demo(){
bar()
}
demo()
catch后面参数可省略:
function foo(){
throw new Error("foo函数发生了异常")
}
function bar(){
try{
foo()
}catch{
// console.log("err:",err);
console.log("发生异常了");
}finally{
console.log("finally代码执行");
}
}
function demo(){
bar()
}
demo()
二、模块化
1.什么是模块化
到底什么是模块化、模块化开发?
- 事实上模块化开发最终的目的是程序划分成一个个小的结构。
- 这个结构中编写属于自己的逻辑代码,有自己的作用域,不会影响到其他的结构。
- 这个结构可以将自己希望暴露的变量、函数、对象等导出给其他结构使用。
- 也可以通过某种方式,导入另外结构中的变量、函数、对象等。
上面提到的结构,就是模块。按照这种结构划分开发程序的过程。就是模块化开发的过程。
1.1 模块化的历史
早期时,js仅仅只是作为一种脚本语言,做一些简单的验证、动画,代码也比较少。
- 一般使用的时候,就被包裹在script标签内部,
- 所以没有必要放到多个文件进行编写。
但是随着前端和js的快速发展,js的代码变的越来越复杂了。
- ajax的出现,前后端开发分离,意味着后端返回数据后,我们需要通过js进行前端页面的渲染。
- SPA(Single Page Application单页面应用程序)的出现,前端页面变得更加复杂:包括前端路由、状态管理等等一系列复杂的需求需要通过Js来实现。
- 包括node的实现,js编写复杂的后端程序,没有模块化是致命的硬伤。
所以,模块化已经是js一个非常迫切的需求:
- 但是javascript本身,直到ES6(2015)才推出了自己的模块化方案。
- 在此之前,为了让javascript支持模块化,涌现出了很多不同的模块化出现:AMD、CMD、CommonJS等。
2.没有模块化的问题
早期没有模块化的时候,会造成命名冲突,多个Js文件没有自己的作用域。
所以我们使用了立即执行函数,将需要向外共享的变量、函数、对象以一个对象的形式返回。
但是,我们其实带来了新的问题:
- 第一,我必须记得每个模块返回对象的命名,才能在其他模块中正确的使用。
- 第二,代码写起来混乱不堪,每个文件中的代码都需要包裹在一个匿名函数中来编写。
- 第三,在没有合适的规范下,每个人,每个公司都可能会任意命名,甚至出现模块名称相同的情况。
所以,我们会发现,虽然实现了模块化,但是我们的实现过于简单,并且是没有规范的。
- 我们需要制定一定的规范来约束每个人都按照这个规范去编写模块化的代码。
- 这个规范中应该包括核心功能:模块本身可以导出暴露的属性,模块又可以导入自己需要的属性
- javascript社区为了解决上面的问题,涌现了一系列好用的规范。
3.CommonJS规范和Node关系
CommonJS是一个规范(只是一个规范),最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了体现它的广泛性,修改为CommonJS,平时我们也会成为CJS。
- Node是CommonJS在服务器端一个具有代表性的实现。
- Browserify是CommonJs在浏览器中的一种实现。
- webpack打包工具具备对CommonJS的支持和转换。
webpack是模块化打包工具
webpack打包的代码最终是要在Node环境下运行的。
所以,Node中对CommonJS进行了支持和实现,让我们在开发Node的过程中可以方便的进行模块化开发。
- 在Node中每一个js文件都是一个单独的模块。
- 这个模块中包括了 CommonJS规范的核心变量:exports、module.exports、require
- 我们可以使用这些变量来方便的进行模块化开发。
前面我们提到过模块化的核心是导出和导入,Node中对其进行了实现:
- exports和module.exports可以负责对模块中的内容进行导出
- require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方模块)中的内容。
4.CommonJS的使用
4.1 CommonJS的基本使用
wjy.js
const name="wjy";
const age=18
function sum(num1,num2){
return num1+num2;
}
// * 导出的方式有两个:一个是exports,一个是module.exports
// * module是这个模块本身的对象
// * 返回将东西放在属性exports中,这些东西都会被导出。
module.exports={
name,
age,
sum
}
// console.log("module:",module);
main.js
// * 使用wjy模块,所以需要导入,导入的时候,会将另外模块的代码执行
/**
* * 如果不加后缀
* * 1.会先在末尾加.js后缀
* * 2.如果还是没找到,会在后面加.json后缀
* * 3.如果还是没找到,会在后面加node
* * 4.最后还是没找到,则会报错
*/
// const wjy=require("./wjy")
// console.log(wjy.name);
// console.log(wjy.age);
// console.log(wjy.sum);
// * 也可以使用解构赋值
const {name,age}=require("./wjy")
console.log(name);
console.log(age);
4.2 内部原理
- module.exports和require()返回的对象指向的是同一片地址。无论修改谁,都会受到影响。
wjy.js
module.exports={
name:"wjy",
age:18,
foo:function(){
console.log("foo函数");
}
}
setTimeout(()=>{
module.exports.name="kobe"
},1000)
main.js
const wjy=require("./wjy");//* 导入的是wjy模块的module.exports对象。
console.log(wjy);
//* 内部的原理:根据id找到这个文件,然后将这个模块的module.exports返回,这个它们指向的是同一片内存空间的地址。
// function require(id){
// return module.exports;
// }
setTimeout(()=>{
console.log("name:",wjy.name);
},2000)
4.3 exports
在node的源码中
module.exports={}
exports=module.exports;
起初module.exports和exports指向的是同一片内存空间。
const name="wjy";
const age=18;
function sum(num1,num2){
return num1+num2
}
// * 第一种导出module.exports
// * 第二种导出
exports.name=name
exports.age=age
exports.sum=sum;
// * 源码
// module.exports={}
// exports=module.exports;
所以上面的exports.name=name,都是这片内存空间添加一个东西。
exports={
name,
age,
sum
}
如果直接让exports={...},这就让module.exports和exports指向的不是同一片内存空间,所以这样的导出是无效的。
因为require取出的module.exports这个对象所指向的内存空间。
最终能导出的只有module.exports
// * 第三种情况 module.exports指向了新的内存地址
exports.name=name;
exports.age=age;
exports.sum=sum
module.exports={}
4.5 require细节
我们现在已经知道,require是一个函数,可以帮助我们引入一个文件(模块)导出的对象
require函数传入的是一个字符串,导入格式为 require(X)
-
情况一:如果X是一个Node核心模块时,比如path、http
- 直接返回核心模块,并且停止查找。
-
情况二:X是以./或../开头或/开头的【是一个路径开头,不是一个简单的名称了。】
-
第一步:将X当做一个文件在对应的目录下查找
- 如果有后缀名,按照后缀名的格式查找对应的文件
- 如果没有后缀名,会按照如下顺序:
- 直接查找文件X
- 查找X.js文件
- 查找X.json文件
- 查找X.node文件
-
第二步:如果没有找到对应的文件,则将X当做目录
- 查找目录下面的index文件
- 查找X/index.js文件
- 查找X/index.json文件
- 查找X/index.node文件
- 查找目录下面的index文件
-
-
情况三:X既不是 路径 ,也不是核心模块
- 会沿着当前module.paths数组中依次取出对应的路径,并且会每次在对应的路径中查找node_modules文件夹是否有X文件夹。如果取出数组中的最后一个路径,还是没有找到,会报错。
// * node的核心模块
const path=require("path")
const fs=require("fs")
console.log(path);
如果X既不是路径,也不是核心模块,会去当前目录下的node_modules文件夹查找X文件夹,如果还是没找到,会上一层目录的node_modules文件夹查找。
image-20220520195654010.png4.6 模块的加载过程
- 结论一:模块在被第一次引入时,模块中的代码会被运行一次。
- 结论二:模块被多次引入时,会缓存,最终只加载(运行)一次。
- 为什么只会加载运行一次呢?
- 这是因为每个模块对象module都有一个属性:loaded
- 为false时表示还未加载,为true表示已经加载。
- 结论三:如果有循环引入,那么加载顺序是什么?
- 如果出现了右图模块的引用关系,那么加载顺序是什么?
- 这个其实是一种数据结构:图结构
- 图结构在遍历的过程中,有深度优先搜索(DFS)和广度优先搜索(BFS)
- Node采用的是深度优先算法:main->aaa->ccc->ddd->bbb->eee
- 如果出现了右图模块的引用关系,那么加载顺序是什么?
4.7 CommonJS规范缺点
-
CommonJS加载模块是同步的:
- 同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行。
- 这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快。
-
如果将它应用于浏览器呢?
- 浏览器加载js文件需要先从服务器将文件下载下来,之后再加载运行。
- 那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作。
-
所以在浏览器中,我们通常不使用CommonJS规范:
- 当然在webpack中使用CommonJS是另外一回事
- 因为它会将我们的代码转成浏览器可以直接执行的代码。
-
在早期为了可以让浏览器中使用模块化,通常会采用AMD或CMD
- 但是目前一方现代的浏览器已经支持ES Modules,另一方面借助于webpack等工具可以实现对CommonJS或者ES Module代码的转换。
- AMD和CMD已经使用非常少了
6.AMD
-
AMD主要是应用于浏览器的一种模块化规范:
- AMD是Asynchoronous Module Definition (异步模块定义)的缩写
- 它采用的是异步加载模块
- 事实上AMD的规范早于CommonJS,但是CommonJS目前依然在被使用,而AMD使用的较少了。
-
我们提到过,规范只是定义代码应该如何编写,只有有了具体的实现才能被应用。
- AMD实现的比较常用的库是require.js和curl.js
6.1 require.js的使用
-
第一步:下载require.js
-
第二步:定义HTML的script标签引入require.js文件和入口文件
- data-main属性的作用是在加载完src指定的文件后会加载执行该文件
<script src="./lib/require.js" data-main="./src/main.js"></script>
main.js
//* paths实现了模块化的注册
require.config({
baseUrl:"./src",
paths:{
foo:"./foo",//这个路径是相当于 index.html的位置来看待的
bar:"./bar"
}
})
// * 加载具体的模块
require(["foo","bar"],function(foo){
console.log("main:",foo);
})
foo.js
define(function(){
const name="wjy";
const age=18;
function sum(num1,num2){
return num1+num1;
}
return {
name,
age,
sum
}
})
bar.js
// define(function(){
// console.log("目前到达bar模块---------");
// // * 如果想使用foo模块
// require(["foo"],function(foo){
// console.log("bar:",foo);
// })
// })
// * 如果想使用foo模块
define(["foo"],function(foo){
console.log("bar:",foo);
})
7.CMD
CMD规范也是应用于浏览器的一种模块化规范:
- CMD是Common Module Definition (通用模块定义)的缩写
- 它也采用了异步加载模块,但是它将CommonJS的优点吸收了进来。
- 但是目前CMD使用也非常少了。
CMD也有自己比较优秀的实现方案:
- SeaJS
index.html
<script src="./lib/sea.js"></script>
<script>
seajs.use("./src/main.js")
</script>
main.js
define(function(require,exports,module){
const foo=require("./foo")
console.log("main:",foo);
})
foo.js
define(function(require,exports,module){
const name="wjy"
const age=18;
function sum(num1,num2){
return num1+num2;
}
// * 导出方式一:
// exports.name=name;
// exports.age=age;
// exports.sum=sum;
// * 导出方式二
module.exports={
name,
age,
sum
}
})
8.认识ES Module
js没有模块化一直是它的痛点,所以才会产生我们前面学习的社区规范:CommonJS、AMD、CMD等。所以在ES推出自己的模块化系统时,大家也是兴奋异常。
ES Module和CommonJS的模块化有一些不同之处:
- 一方面它使用了import和export关键字
- 另一方面,它采用编译期的静态分析,并且也加入了动态引用的方式。
ES Module模块采用 import和export关键字来实现模块化
- export负责将模块内的内容导出。
- import负责从其他模块导入内容。
了解:采用ES Module 将自动采用 严格模式 :use strict
ES Module最终还是应用在浏览器中。
8.1 基本使用
index.html
<!-- *
* 默认情况会将js文件当做一个普通的文件加载进来,然后从上往下执行,默认是不支持import、export
* 所以应该要指定加载这个文件是一个模块,所以需要设置 type=module
* file协议是不允许去加载模块的,所以会报错的。需要使用http协议来加载对应的模块
-->
<!--
* 其实在电脑上是可以直接打开一个index.html文件的:这个相当于浏览器对本地文件来解析的。
* live-server:会开启一个本地服务器,有自己的ip地址、端口号
-->
<script src="./main.js" type="module"></script>
main.js
import {name,age} from "./foo.js";//* 这里必须要加后缀名全称,因为没有webpack的环境
console.log(name);
console.log(age);
foo.js
export const name="wjy"
export const age=18;
8.2 export关键字
8.2.1 export 声明语句
export const name="wjy"
export const age=18;
export function sum(num1,num2){
return num1+num2;
}
export class Person{
}
8.2.2 export导出和声明分开
// * 第二种:export导出和声明分开
const name="wjy"
const age=18;
function foo(){
console.log("foo函数");
}
// * {}是一个固定语法,不要去写什么键值对形式,否则会报错的。
export {
name,
age,
foo
}
8.2.3 export导出和声明分开,导出时取别名
使用as 关键字取别名
const name="wjy"
const age=18;
function foo(){
console.log("foo函数");
}
export {
name as fName,
age as fAge,
foo as fFoo
}
8.3 import关键字
8.3.1 普通的导入
// * 导入方式一:普通的导入
import {fName,fAge} from "./foo.js";//* 这里必须要加后缀名全称,因为没有webpack的环境
8.3.2 导入时起别名
// * 导入方式二:起别名
import {name as fName, age as fAge,foo as fFoo} from "./foo.js"
8.3.3 将导出的所有内容放到一个标识符中
// * 导入方式三:将导出的所有内容放到一个标识符中
import * as foo "./foo.js"
console.log(foo.name);
console.log(foo.age);
console.log(foo.foo());
8.4 综合使用
- 如果在工具包目录下写了很多工具方法,可以将其汇总到index.js,由index.js模块统一导出
index.js
// * 导出方式一:
// import {add,sub} from "./math.js"
// import {timeFormat,priceFormat} from "./format.js"
// export {
// add,
// sub,
// timeFormat,
// priceFormat
// }
// * 导出方式二
// export {add,sub} from "./math.js"
// export {timeFormat,priceFormat} from "./format.js"
// * 导出方式三
export * from "./math.js"
export * from "./format.js"
8.5 default
- 默认导出只能有一个
foo.js
const name="wjy"
const age=18;
const foo="foo value"
export {
name,
age,
// * 默认导出方式一
// foo as default
}
// * 默认导出方式二
export default foo
main.js
import wjy from "./foo.js";//这个拿到的是 foo.js的默认导出
console.log(wjy);
8.6 import 函数
前面我们使用的普通的导入方式,是同步的,必须要等到对应的模块加载完,才能执行当前模块的其他代码,所以会造成后续代码的阻塞、
如果不想模块的导入阻塞后续的代码,可以使用require函数,它的返回值是一个Promise对象
- 在ES11新增了一个特性,import也是一个对象,它有一个属性meta,meta属性也是一个对象,它里面有一个url属性,表示当前模块的所在路径。
// import {name,age,foo} from "./foo.js";//* 默认这种导入模块,要等到对应模块加载完,才会执行模块内的代码,如果模块还未加载完,后续的代码会被阻塞的。
// * 如果不想导入其他模块时,而阻塞当前模块的内容,可以使用import函数
// * 可以使用import函数,返回的是一个Promise对象
import ("./foo.js").then(res=>{
console.log("res:",res);
})
console.log("后续的代码都是不会运行的");
8.7 ESModule的解析流程
-
ES Module是如何被浏览器解析并且让模块之间可以相互引用的呢?
-
ES Module的解析过程可以划分为三个阶段:
-
阶段一:构建(Consruction),根据地址查找js文件,并且下载,将其解析成模块记录(Module Record)
-
阶段二:实例化(Instantiation):对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址
-
阶段三:运行(Evaluation),运行代码、计算值,并且将值填充到内存地址中。
-
8.7.1 构建阶段(Construction)
image-20220521144418698.png image-20220521144748680.png8.7.2 阶段二和阶段三:实例化阶段——求值阶段
image-20220521145159094.png- 导出者可以修改里面的值,导入者不允许去修改里面的值。
9ES Module和CommonJS的关系
如果在一个模块中使用module.exprots方式【CommonJS】导出,能否在另外一个模块使用import【ES Module】方式导入呢?
如果在一个模块中使用export方式导出 【ES Module】导出,能否在另外一模块使用 requrie【CommonJS】方式导入呢?
- 在浏览器中(不能)
- Node环境(部分支持)
- 平时开发(基于Webpack的环境)
- vue-cli是基于webpack的
使用webpack 必须使用webpack-cli
-
打包:
-
npx webpack
-
npx webpack --mode=development
-
网友评论