0%

1.主文件 babelParse.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const { getOptions } = require('loader-utils')
const { validate } = require('schema-utils')
const { transform} = require('@babel/core')
const { promisify } = require('util')

// 将回调函数转换为基于promise的函数
const transformAsync = promisify(transform)
// 校验配置
const babelSchema = require('./schemas/babelSchema.js')

/*
* Loader通常是一个函数
* source: 对于第一个执行的loader为资源的内容,非第一个执行的loader为上一个loader的执行结果
*
module.exports = function (source) {
// 获取loader的配置
const options = getOptions(this)
// 校验配置:成功时往下运行,失败时停止运行报出错误
validate(babelSchema, options, {
name: 'babelLoader'
})
// 异步返回结果
const callBack = this.async()
// 调用transform方法进行转码并返回
transformAsync(source, options)
.then(({ code }) => callBack(null, code))
.catch(err => {callBack(err)})
}

2.校验文件 babelSchema.js

1
2
3
4
5
6
7
8
9
10
module.exports = {
type: 'object',
properties: {
presets: {
type: 'array'
}
},
// 是否可以有未定义的附加属性
addtionalProperty: true
}

3.webpack配置文件 webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babelParse.js',
// 配置参数
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
resolveLoader: {
// 寻找loader所在位置
modules: ['node_modules', path.resolve(__dirname, 'loaders/')]
},
...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const express = require('express')
const path = require('path')
const proxy = require('http-proxy-middleware')

const app = express()

// 解决跨域:开发环境前端服务通过proxy代理转发请求。
// 跨域问题是由于浏览器的同源策略,在node环境没有跨域问题。
app.use('*', proxy({ target: 'http://xx.xx.xx.xx:xxxx', changeOrigin: true }));

function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}

var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;

// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}

function onListening() {
var addr = app.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
console.log('Listening on ' + bind);
}

app.on('error', onError);
app.on('listening', onListening);
// 开发环境前端请求自己启动的服务端口,在本地express服务中做代理转发
app.listen(8080)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const express = require('express')
// superagent是一个轻量的Ajax API,服务器端(Node.js)客户端(浏览器端)均可使用
const superagent = require('superagent')
// nodejs的抓取页面模块,实现了核心jQuery的子集
const cheerio = require('cheerio')
const app = express()

app.get('/', (req, res, next) => {
superagent.get('https://www.zhipin.com/wuhan/')
.end((err, data) => {
// 错误优先处理
if (err) return next(err)
// 将获取到dom绑定到$上,后面可以直接用$以jQuery的方式操作dom
const $ = cheerio.load(data.text);
const conpanys = []
$('.conpany-text').each((index, element) => {
conpanys.push($(element).find('h4').text())
});
res.send(conpanys)
})
})

app.listen('9999', () => {
console.log('http://localhost:9999');
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
class Promise {
constructor (execute) {
/**
* Promise实例状态值(3种): 1.pending; 2.fulfilled; 3.rejected。
* 状态改变方式(2种):1.pending -> fulfilled; 2.pending -> fulfilled。
* 注意:一旦状态改变,不可逆。
*/
this.PromiseState = 'pending'
// Promise实例结果: 存储成功时返回的终值(eventual value)和失败时返回的拒因(reason)。
this.PromiseResult = null
/**
* 应用场景:当Promise执行函数为异步时,存储then方法的回调函数,
* 等异步执行完后,根据状态在resolve或reject函数中在去执行。
* 因为每一个Promise实例可以有多个then方法,所以存储回调的对象属性值是一个数组。
* 因为异步执行的结果未知,所以成功回调和失败回调都要存储起来,
* 数组的每一项都是一个对象:{ onResolve: 成功回调, onReject: 失败回调 }。
*/
this.callBacks = []

/**
* 使用方式:在Promise执行函数中,将成功的值作为参数调用resolve函数
* 执行函数里的内置方法,主要作用有三个:
* 1.改变Promise实例状态: pending -> fulfilled;
* 2.改变Promise实例结果: this.PromiseState = data;
* 3.实现Promise的状态不可逆,具体做法是只当状态为pending时才改变状态和结果。
*/
const resolve = (data) => {
if (this.PromiseState !== 'pending') return
this.PromiseState = 'fulfilled'
this.PromiseResult = data
// Promise的then方法是异步执行的,回调函数执行放在定时器里面来实现。
setTimeout(() => { this.callBacks.forEach(item => item.onResolve())}, 0)
}

/**
* 使用方式:在Promise执行函数中,将失败的原因作为参数调用reject函数
* 执行函数里的内置方法,主要作用有三个:
* 1.改变Promise实例状态: pending -> rejected;
* 2.改变Promise实例结果: this.PromiseState = data;
* 3.实现Promise的状态不可逆,具体做法是只当状态为pending时才改变状态和结果。
*/
const reject = (data) => {
if (this.PromiseState !== 'pending') return
this.PromiseState = 'rejected'
this.PromiseResult = data
// Promise的then方法是异步执行的,回调函数执行放在定时器里面来实现。
setTimeout(() => { this.callBacks.forEach(item => item.onReject())}, 0)
}

// 用try...catch...来处理执行函数运行抛出错误
try {
execute(resolve, reject)
} catch (e) {
reject(e)
}
}

then (onResolve, onReject) {
/**
* then方法主要做的事情有四个:
* 1.接收两个函数作为参数,分别是成功回调和失败回调;
* 2.返回一个新的Promise实例,可以链式调用;
* 3.当前面的Promise状态改变时,then方法根据其最终状态,选择特定的回调函数执行;
* 4.回调函数返回值不同,分三种情况:
* *抛出异常,状态为失败,结果为失败返回的拒因;
* *返回结果不是Promise实例,状态为成功,结果为成功返回的终值;
* *返回结果是Promise实例,返回为这个Promise的结果。
*/

/**
* then方法支持参数可以不传,当成功回调不传的时候,默认赋值为一个函数,
* 这个函数做的事情就是把上一个Promise的返回结果,传递给下一个then方法。
*/
onResolve = typeof onResolve === 'function' ? onResolve : res => res

/**
* then方法支持参数可以不传,当失败回调不传的时候,默认赋值为一个函数,
* 这个函数做的事情就是抛出错误原因,实现异常穿透。
*/
onReject = typeof onReject === 'function' ? onReject : err => { throw err }

return new Promise((resolve, reject) => {
/**
* 根据回调函数返回的结果,执行相应内置函数来改变then函数返回的Promise实例状态和结果值
*/
const handle = (cbType) => {
// 使用try...catch...来捕获成功回调函数执行中的异常
try {
// 执行成功回调,获取then方法成功回调的返回结果。
const res = cbType(this.PromiseResult)
if (res instanceof Promise) {
res.then(resolve, reject)
} else {
resolve(res)
}
} catch (e) {
reject(e)
}
}

switch (this.PromiseState) {
// Promise实例的执行函数是同步的,状态为fulfilled。
case 'fulfilled':
// Promise的then方法是异步执行的,回调函数执行放在定时器里面来实现。
setTimeout(() => { handle(onResolve) })
break;
// Promise实例的执行函数是同步的,状态为rejected。
case 'rejected':
// Promise的then方法是异步执行的,回调函数执行放在定时器里面来实现。
setTimeout(() => { handle(onReject) })
break;
// Promise实例的执行函数是异步的,状态为pending。
default:
// 往回调里面添加成功回调和失败回调
this.callBacks.push({
/**
* 等上一个Promise状态改变时,来根据key执行相对应的value函数
*/
onResolve: () => handle(onResolve),
onReject: () => handle(onReject)
})
}
})
}

catch (onReject) {
// 只需要调用then方法,成功回调传空即可
return this.then(null, onReject)
}

static resolve (value) {
/**
* 返回结果为Promise实例,有两种情况:
* 1.参数是Promise实例,返回为这个Promise的结果;
* 2.参数不是Promise实例,状态为成功,结果为参数。
*/
return new Promise((resolve, reject) => {
if (value instanceof Promise) {
value.then(resolve, reject)
} else {
resolve(value)
}
})
}

static reject (value) {
/**
* 返回结果为Promise实例,状态为失败,结果为参数
*/
return new Promise((resolve, reject) => {
reject(value)
})
}

static all (promises) {
/**
* 接收的参数为一个由Promise实例组成的数组
* 返回结果有两种情况:
* 1.参数中的Promise实例结果都为成功,状态为成功,并将实例终值按原顺序拼凑成一个数组,作为all返回的Promise实例终值;
* 2.参数中的Promise实例结果有一个失败,状态为失败,并将第一个失败的拒因,作为all返回的Promise实例据因。
*/
return new Promise((resolve, reject) => {
let count = 0
const values = new Array(promises.length)
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then(res => {
count++
values[i] = res
console.log('count', count)
if (count === promises.length) {
resolve(values)
}
}, err => {
reject(err)
})
}
})
}

static race (promises) {
/**
* 接收的参数为一个由Promise实例组成的数组
* 返回一个Promise实例,结果由第一个完成的Promise实例决定
*/
return new Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then(res => {
resolve(res)
}, err => {
reject(err)
})
}
})
}
}

export default Promise

前言

babel是现在几乎每个项目中必备的一个东西,但是其工作原理避不开对js的解析在生成的过程,babel有引擎babylon,早期fork了项目acron,了解这个之前我们先来看看这种引擎解析出来是什么东西。不光是babel还有webpack等都是通过javascript parser将代码转化成抽象语法树,这棵树定义了代码本身,通过操作这颗树,可以精准的定位到赋值语句、声明语句和运算语句。

什么是抽象语法树

我们可以来看一个简单的例子:

1
2
var a = 1;
var b = a + 1;

我们通过这个网站,他是一个esprima引擎的网站,十分好用.画成流程图如下:

img

而他的json对象格式是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "Literal",
"value": 1,
"raw": "1"
}
}
],
"kind": "var"
},
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "b"
},
"init": {
"type": "BinaryExpression",
"operator": "+",
"left": {
"type": "Identifier",
"name": "a"
},
"right": {
"type": "Literal",
"value": 1,
"raw": "1"
}
}
}
],
"kind": "var"
}
],
"sourceType": "script"
}

AST的三板斧

  • 通过esprima生成AST

  • 通过estraverse遍历和更新AST

  • 通过escodegen将AST重新生成源码

作用

抽象语法树的作用非常的多,比如编译器、IDE、压缩优化代码等。在JavaScript中,虽然我们并不会常常与AST直接打交道,但却也会经常的涉及到它。例如使用UglifyJS来压缩代码,实际这背后就是在对JavaScript的抽象语法树进行操作。

作用域

作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供了‘块级作用域’,可通过新增命令 let 和 const 来体现。

作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行。作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

作用域链

一般情况下,变量取值到创建这个变量的函数的作用域中取值。但是如果在当前作用域中没有查到值,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。

静态作用域与动态作用域

JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。

1
2
3
4
5
6
7
8
9
10
11
12
var value = 1;

function foo() {
console.log(value);
}

function bar() {
var value = 2;
foo();
}

bar(); // 1

假设JavaScript采用静态作用域,让我们分析下执行过程:

执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。

假设JavaScript采用动态作用域,让我们分析下执行过程:

执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。

前面我们已经说了,JavaScript采用的是静态作用域,所以这个例子的结果是 1。

思考题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope(); // local scope
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()(); // local scope

原因也很简单,因为JavaScript采用的是词法作用域,函数的作用域基于函数创建的位置。

JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数 f() 定义在这个作用域链里,其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执行 f() 时依然有效。

原型

每一个javascript对象(null除外)在创建的时候,都会与另外一个对象所关联。而这个与创建对象所关联的对象,就是所创建对象的原型,每一个对象都会从原型中继承属性。

构造函数、原型、实例的关系图

为什么原型没有属性指向实例?

因为一个构造函数可以创建多个实例对象,原型与实例对象是一对多的关系,所以就没法用一个属性指向实例。

原型链

当读取实例的属性时,如果找不到就会去查找与对象关联的原型中的属性,如果还找不到,就去找原型的原型,一直找到最顶层为止,这样由原型组成的链状结构就是原型链。

原型链的尽头

1
console.log(Object.prototype.__proto__ === null) // true

Object.prototype.proto 的值为 null 跟 Object.prototype 没有原型,其实表达了一个意思,所以查找属性到了 Object.prototype就可以停止查找了。

原型链关系图

图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。

几个常见小问题

实例的constructor属性指向构造函数吗?

1
2
3
function Person() {}
var person = new Person();
console.log(person.constructor === Person); // true

当获取person的constructor属性时,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性。

1
person.constructor === Person.prototype.constructor

怎么理解__proto__属性?

绝大多数浏览器都支持这个非标准的方法访问原型,然而它并不存在于Person.prototype中,实际上它是来自于 Object.prototype。与其说是一个属性,不如说是一个getter/setter,当使用 obj.proto 时,可以理解成返回了 Object.getPrototypeOf(obj)。

真的是从原型上“继承”属性吗?

继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性。相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。

typeof和instanceof都是用来判断变量类型的,两者的区别在于:

  • typeof判断所有变量的类型,返回值有number,boolean,string,function,object,undefined。(这里需要注意一下,js中基本数据类型常用的有六种,其中简单一点的有五个:String、Number、Boolean、Undefined、Null,一个复杂的数据类型:Object。ES6中新增了一个Symbol用于生成唯一标识符,ES10中新增了BigInt可以表示任意大的整数)。

  • typeof对于丰富的对象实例,只能返回”object”字符串。

  • instanceof用来判断对象,代码形式为obj1 instanceof obj2(obj1是否是obj2的实例),obj2必须为对象,否则会报错!其返回值为布尔值。

语法: object instanceof constructor

object(要检测的对象),constructor(某个构造函数),instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。

简而言之,A instanceof B , 是判断对象实例A是否是构造函数B的实例。更准确一点的说法是,构造函数B的原型,是否存在与对象实例A的原型链上。

防抖节流应用场景?

按照需求来确定是用防抖还是节流:

tips: 在连续频繁操作的时间区域内,要能执行函数的情况用节流。

  1. 监听浏览器resize事件;
  2. 文本编辑器实时保存;
  3. 输入框的模糊查询功能;

防抖函数

防抖的原理是什么?

防抖的原理就是:不管怎么触发事件,但是一定在事件触发 n 秒后才执行,如果一个事件触发的 n 秒内又触发了这个事件,那就以新事件的时间为准,n 秒后才执行,总之就是要等触发完事件 n 秒内不再触发事件才执行。

实现一个防抖函数?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const debounce = (fn, delay) => {
let timer = null
/**
* 返回值也是一个函数,函数内部的变量就是局部变量,可以避免造成全局污染;
* 注意:这里不能使用箭头函数,因为后面会用到this。
*/
return function () {
// 存储参数
let args = arguments
/**
* 除了第一次timers为null,后续函数的调用timer都有值;
* 这一步要做的是清除目前正在进行的定时器,即使时间到了也不能执行回调函数;
* 注意:clearTimeout(timer)执行之后,timer的值没变。
*/
if (timer) clearTimeout(timer)
// 将新的定时器赋值给timer
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}

节流函数

节流的原理是什么?

节流的原理是:一个函数执行一次后,只有大于设定的执行周期,才会执行第二次。也就是说:在规定的时间内,只让函数触发的第一次生效,后面的不生效。

实现一个节流函数?

1.使用定时器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const throttle = (fn, delay) => {
let timer = null
/**
* 返回值也是一个函数,函数内部的变量就是局部变量,可以避免造成全局污染;
* 注意:这里不能使用箭头函数,因为后面会用到this。
*/
return function () {
// 存储参数
let args = arguments
/**
* timer为null的两种情况:
* 1.第一次执行函数timer初始值为空;
* 2.每次定时器到时间了,执行回调时会将timer赋值为空。
*/
if (!timer) {
timer = setTimeout(() => {
/**
* 注意:这里直接将timer是赋值为null,而不是使用clearTimeout,
* 还是因为clearTimeout(timer)执行之后,timer的值没变
*/
timer = null
fn.apply(this, args)
}, delay)
}
}
}

2.使用时间戳

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function throttle_2(fn, delay) {
let previous = 0;
return function () {
// 存储参数
let args = arguments
const nowTime = Date.now()
/**
* 执行的两种情况:
* 1.第一次执行函数previous为0,nowTime-0>delay;
* 2.距离上一次执行时间超过了delay。
*/
if (nowTime - previous > delay) {
fn.apply(this, args)
previous = nowTime
}
}
}

发布订阅模式属于广义上的观察者模式
发布订阅模式是最常用的一种观察者模式的实现,并且从解耦和重用角度来看,更优于典型的观察者模式。

发布订阅模式多了个事件通道
在观察者模式中,观察者需要直接订阅目标事件;在目标发出内容改变的事件后,直接接收事件并作出响应。

在发布订阅模式中,发布者和订阅者之间多了一个发布通道;一方面从发布者接收事件,另一方面向订阅者发布事件;订阅者需要从事件通道订阅事件,以此避免发布者和订阅者之间产生依赖关系。