Xiao's Home


  • 首页

  • 归档

装饰器 Decorator

发表于 2018-07-12

许多面向对象的语言都有 装饰器(Decorator) 函数,用来修改类的行为。目前,这个方法已经被引入了 ES7,但是无论是主流浏览器还是 Node.js 对它的兼容性都不是特别友好。

因此要在项目中使用Decorator的话,需要使用 Babel 进行转移,或者使用 Javascript 的超集 Typescript 来进行开发。

如果对这一语法细节还不是很了解的话,可以先进这个传送门:http://es6.ruanyifeng.com/#docs/decorator ,跟着阮一峰老师一起了解一下它的特性。

初衷

使用 装饰器 的初衷来自于不想修改原来接口的情况下,能让一件事表现得更好。就像:

  • 手机可以用,但是加了手机壳就能防摔;
  • 椅子可以坐,但是垫了垫子就能够坐的更舒服;
  • 步枪可以射击,但是加了瞄准镜就可以射的更准;
  • ……

如果要更加抽象地理解,在计算机领域,它就可以被应用到日志收集、错误捕获、安全检查、缓存、调试、持久化等等方面。

常用的装饰器

常用的装饰器一般有 类装饰器 和 方法装饰器,当然也会有属性装饰器,但是用的不多就不多讨论了。

类装饰器

主要应用于类构造函数,其参数是类的构造函数:

1
2
3
4
5
6
7
8
9
function testable(target) {
target.prototype.isTestable = true
}

@testable
class MyTestableClass {}

let obj = new MyTestableClass()
obj.isTestable // true

注意: 这里的target参数如果直接给它添加方法,获得的是一个静态方法,相当于在class的方法前添加static关键字;如果想添加实例属性,可以通过目标类的prototype对象操作。

现在我们就用 类装饰器 实现一个捕获方法执行时间的装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
const sleepTimeClass = (timeHandler?: (time?: number) => void) => (target: any) => {
Object.getOwnPropertyNames(target.prototype).forEach(key => {
const func = target.prototype[key]
target.prototype[key] = async (...args: any[]) => {
const startTime = await +new Date()
await func.apply(this, args)
const endTime = await +new Date()
timeHandler && await timeHandler(endTime - startTime)
}
})
return target
}

之所以还在外面包了一层函数,是为了通过柯里化,让使用者可以再进一步处理得到的执行时间:

1
2
3
4
5
6
7
8
9
10
const sleepTimeClassTimer = sleepTimeClass(time => {
console.log('执行时间', `${time}ms`)
})

@sleepTimeClassTimer
class homepageController {
async get(ctx: any) {
ctx.response.body = await pageService.homeHtml('/page/helloworld', '/page/404')
}
}

这样,每次class中的方法执行完之后就会打印出相应的执行时间。

方法装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function readonly(target, name, descriptor){
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// }
descriptor.writable = false
return descriptor
}

readonly(Person.prototype, 'name', descriptor)
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor)

由于在异步编程的时候,async和await的异常很难捕获,如果强行用try...catch来搞,捕捉不完不说,代码看起来还很难看,使用装饰器就很简单了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const asyncMethod = (errorHandler?: (error?: Error) => void) => (...args: any[]) => {
const func = args[2].value
return {
get() {
return (...args: any[]) => {
return Promise.resolve(func.apply(this, args)).catch(error => {
errorHandler && errorHandler(error)
})
}
},
set(newValue: any) {
return newValue
}
}
}

接着使用方法装饰器:

1
2
3
4
5
6
7
8
9
const errorAsyncMethod = asyncMethod(error => {
console.error('错误警告', error)
})

class homepageController {
@errorAsyncMethod async get(ctx: any) {
ctx.response.body = await pageService.homeHtml('/page/helloworld', '/page/404')
}
}

装饰器加载顺序

一个类或者方法可以嵌套很多个装饰器,所以搞清楚它们的执行顺序也很重要:

  • 有多个参数装饰器时,从最后一个参数依次向前执行;
  • 方法和方法参数中参数装饰器先执行;
  • 类装饰器总是最后执行;
  • 方法和属性装饰器,谁在前面谁先执行;
  • 因为参数属于方法一部分,所以参数会一直紧紧挨着方法执行。

装饰器的应用

在初衷那里就已经提到了,试着想象一下,只需要几个装饰器就可以完成前后端基本的性能和日志监控,是不是很有意思?

Node.js 中间件模式

发表于 2018-06-11

中间件 在 Node.js 中被广泛使用,它泛指一种特定的设计模式、一系列的处理单元、过滤器和处理程序,以函数的形式存在,连接在一起,形成一个异步队列,来完成对任何数据的预处理和后处理。

它的优点在于 灵活性:使用中间件我们用极少的操作就能得到一个插件,用最简单的方法就能将新的过滤器和处理程序扩展到现有的系统上。

常规中间件模式

中间件模式中,最基础的组成部分就是 中间件管理器,我们可以用它来组织和执行中间件的函数,如图所示:
中间件.jpg

要实现中间件模式,最重要的实现细节是:

  • 可以通过调用use()函数来注册新的中间件,通常,新的中间件只能被添加到高压包带的末端,但不是严格要求这么做;
  • 当接收到需要处理的新数据时,注册的中间件在意不执行流程中被依次调用。每个中间件都接受上一个中间件的执行结果作为输入值;
  • 每个中间件都可以停止数据的进一步处理,只需要简单地不调用它的毁掉函数或者将错误传递给回调函数。当发生错误时,通常会触发执行另一个专门处理错误的中间件。

至于怎么处理传递数据,目前没有严格的规则,一般有几种方式

  • 通过添加属性和方法来增强;
  • 使用某种处理的结果来替换 data;
  • 保证原始要处理的数据不变,永远返回新的副本作为处理的结果。

而具体的处理方式取决于 中间件管理器 的实现方式以及中间件本身要完成的任务类型。

举一个来自于 《Node.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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class ZmqMiddlewareManager {
constructor(socket) {
this.socket = socket;
// 两个列表分别保存两类中间件函数:接受到的信息和发送的信息。
this.inboundMiddleware = [];
this.outboundMiddleware = [];
socket.on('message', message => {
this.executeMiddleware(this.inboundMiddleware, {
data: message
});
});
}

send(data) {
const message = { data };

this.excuteMiddleware(this.outboundMiddleware, message, () => {
this.socket.send(message.data);
});
}

use(middleware) {
if(middleware.inbound) {
this.inboundMiddleware.push(middleware.inbound);
}
if(middleware.outbound) {
this.outboundMiddleware.push(middleware.outbound);
}
}

exucuteMiddleware(middleware, arg, finish) {
function iterator(index) {
if(index === middleware.length) {
return finish && finish();
}
middleware[index].call(this, arg, err => {
if(err) {
return console.log('There was an error: ' + err.message);
}
iterator.call(this, ++index);
});
}
iterator.call(this, 0);
}
}

接下来只需要创建中间件,分别在inbound和outbound中写入中间件函数,然后执行完毕调用next()就好了。比如:

1
2
3
4
5
6
7
8
9
10
11
12
const zmqm = new ZmqMiddlewareManager();

zmqm.use({
inbound: function(message, next) {
console.log('input message: ', message.data);
next();
},
outbound: function(message, next) {
console.log('output message: ', message.data);
next();
}
});

Express 所推广的 中间件 概念就与之类似,一个 Express 中间件一般是这样的:

1
function(req, res, next) { ... }

Koa2 中使用的中间件

前面展示的中间件模型使用回调函数实现的,但是现在有一个比较时髦的 Node.js 框架Koa2的中间件实现方式与之前描述的有一些不太相同。Koa2中的中间件模式移除了一开始使用ES2015中的生成器实现的方法,兼容了回调函数、convert后的生成器以及async和await。

在Koa2官方文档中给出了一个关于中间件的 洋葱模型,如下图所示:
koa中间件.jpg

从图中我们可以看到,先进入inbound的中间件函数在outbound中被放到了后面执行,那么究竟是为什么呢?带着这个问题我们去读一下Koa2的源码。

在koa/lib/applications.js中,先看构造函数,其它的都可以不管,关键就是this.middleware,它是一个inbound队列:

1
2
3
4
5
6
7
8
9
10
11
constructor() {
super();

this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}

和上面一样,在Koa2中也是用use()来把中间件放入队列中:

1
2
3
4
5
6
7
8
9
10
11
12
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}

接着我们看框架对端口监听进行了一个简单的封装:

1
2
3
4
5
6
// 封装之前 http.createServer(app.callback()).listen(...)
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}

中间件的管理关键就在于this.callback(),看一下这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
callback() {
const fn = compose(this.middleware);

if (!this.listenerCount('error')) this.on('error', this.onerror);

const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};

return handleRequest;
}

这里的compose方法实际上是Koa2的一个核心模块koa-compose(https://github.com/koajs/compose),在这个模块中封装了中间件执行的方法:

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
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}

/**
* @param {Object} context
* @return {Promise}
* @api public
*/

return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}

可以看到,compose通过递归对中间件队列进行了 反序遍历,生成了一个Promise链,接下来,只需要调用Promise就可以执行中间件函数了:

1
2
3
4
5
6
7
8
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

从源码中可以发现,next()中返回的是一个Promise,所以通用的中间件写法是:

1
2
3
4
5
6
7
app.use((ctx, next) => {
const start = new Date();
return next().then(() => {
const ms = new Date() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
});

当然如果要用async和await也行:

1
2
3
4
5
6
app.use((ctx, next) => {
const start = new Date();
await next();
const ms = new Date() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

由于还有很多Koa1的项目中间件是基于生成器的,需要使用koa-convert来进行平滑升级:

1
2
3
4
5
6
7
8
const convert = require('koa-convert');

app.use(convert(function *(next) {
const start = new Date();
yield next;
const ms = new Date() - start;
console.log(`${this.method} ${this.url} - ${ms}ms`);
}));

最后,如果发现什么错漏也欢迎提出!

js设计模式:工厂模式

发表于 2018-06-07

在 Javascript 中, 因为函数的简单性、可用性和对外暴露少的特性,所以相对比纯粹的面向对象,使用函数式的方式是首选,特别是在创建对象实力的时候。

相比直接创建一个新的对象,工厂方法包装了一个新实例的创建,将对象的创建从实现中分离出来,从而更好也更灵活地控制对象的创建。

举个例子,当我们需要创建一个音频对象audio的时候,使用关键字new是最直接的方式:

1
const audio = new Audio(name);

但是,这种创建方式会限定我们产生的对象,而工厂模式则提供了更多我们创建对象的选项,并且还不用暴露创建对象的构造函数,避免被继承或者修改:

1
2
3
4
5
6
7
8
9
function createAudio(name) {
if(name.match(/\.wav$/)) {
return new WavAudio(name);
} else if(name.match(/\.mp3$/)) {
return new Mp3Audio(name);
} else {
throw new Exception('没有该类型音频!');
}
}

使用闭包封装

很多人都迷惑为什么总会在笔试面试的时候被问到闭包的知识,因为大多数人对于闭包的使用第一反应可能是生成块作用域,然而即使在ES2015引入块作用域之后,闭包依然还有很多方向的应用,比如在工厂模式中实现私有变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createAnimal(species) {
const privateProps = {};
const animal = {
setSpecies: species => {
if(!specis) throw new Error('请输入该动物的物种1');
privateProps.species = species;
}
getSpecies: () => {
return privateProps.species;
}
};
animal.setSpecies(species);
return animal;
}

这样就可以很好地将封装私有变量了,通过闭包可以让私有变量只能通过getter和setter来访问和修改。

使用工厂模式构建 Debugger

工厂模式有一个特别常用的场景,就是根据不同的环境,输出不同的对象,接下来,我们就自己构建一个简单的debugger,可以在开发模式调试代码,并且在生产模式不输出调试结果:

1
2
3
4
5
6
7
8
9
10
11
function createDebugger() {
if(process.env.NODE_ENV === 'development') {
return console;
} else if(process.env.NODE_ENV === 'production') {
const deb = {};
Object.keys(console).forEach(key => {
deb[key] = () => {};
});
return deb;
}
}

这样我们就能在不同的生产环境中初始化不同的对象,从而实现不一样的结果。

可组合的工厂函数

可组合的工厂函数 是一类特殊的工厂函数,它们可以被组合到一起来构建一个功能增强的工厂函数。当我们要创建从多个源继承一些行为和属性的对象,又不想构建复杂类结构的时候,就可以考虑组合工厂函数。

举个例子,我们每个人都有自己的name、sex和age等一些基础属性,然后在我们中有着不同的职业:

  • 歌手
  • 画家
  • 程序猿

再从另外一个维度看,又会有各种爱好者:

  • 篮球
  • 游戏
  • 射击

这样就能组合出:

  • 热爱篮球的歌手
  • 热爱游戏的歌手
  • 热爱射击的歌手
  • 等等等等(还有可能喜欢多种项目或是从事多个职业)

所以就需要我们将这些抽象出来的对象组合起来:

1
2
3
4
5
6
7
8
9
10
11
function compose(objList=[]) {
const composedObj = {};
objList.map(obj => {
Object.keys(obj).map(key => {
composedObj[key] = obj[key];
});
});
return composedObj;
}

const shottingSinger = compose([new Singer, new Shotter]);

当然,这只是个示例,少了很多容错的处理,使用 组合工厂函数 推荐一个比较好的库 stampit:https://www.npmjs.com/package/stampit

Promise 异步控制流

发表于 2018-05-25

Node.js 风格函数的 promise 化

在 Javascript 中, 并非所有的异步控制函数和库都支持开箱即用的promise,所以在大多数情况下都需要吧一个典型的基于回调的函数转换成一个返回promise的函数,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function promisify(callbackBaseApi) {
return function promisified() {
const args = [].slice.call(arguments);
return new Promise((resolve, reject) => {
args.push((err, result) => {
if(err) {
return reject(err);
}
if(arguments.length <= 2) {
resolve(result);
} else {
resolve([].slice.call(arguments, 1));
}
});
callbackBaseApi.apply(null, args);
});
}
}

现在的 Node.js 核心实用工具库util里面已经支持(err, value) => ...回调函数是最后一个参数的函数, 返回一个返回值是一个promise版本的函数。

顺序执行流的迭代模式

在看异步控制流模式之前,先开始分析顺序执行流。按顺序执行一组任务意味着一次运行一个任务,一个接一个地运行。执行顺序很重要,必须保留,因为列表中任务运行的结果可能影响下一个任务的执行,比如:

1
start -> task1 -> task2 -> task3 -> end

这种流程一般都有着几个特点:

  • 按顺序执行一组已知任务,而没有链接或者传播结果;
  • 一个任务的输出作为下一个的输入;
  • 在每个元素上运行异步任务时迭代一个集合,一个接一个。

这种执行流直接用在阻塞的 API 中并没有太多问题,但是,在我们使用非阻塞 API 编程的时候就很容易引起回调地狱。比如:

1
2
3
4
5
6
7
task1(err, (callback) => {
task2(err, (callbakck) => {
task3(err, (callback) => {
callback();
});
});
});

传统的解决方案是进行任务的拆解方法就是把每个任务拆开,通过抽象出一个迭代器,在任务队列中去顺序执行任务:

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
class TaskIterator {
constructor(tasks, callback) {
this.tasks = tasks;
this.index = 0;
this.callback = callback;
}
do() {
if(this.index === this.tasks.length) {
return this.finish();
}
const task = tasks[index];
task(() => {
this.index++;
this.do();
})
}
finish() {
this.callback();
}
}

const tasks = [task1, task2, task3];
const taskIterator = new TaskIterator(tasks, callback);

taskIterator.do();

需要注意的是, 如果task()是一个同步操作的话,这样执行任务就可能变成一个递归算法,可能会有由于不再每一个循环中释放堆栈而达到调用栈最大限制的风险。

顺序迭代 模式是非常强大的,因为它可以适应好几种情况。例如,可以映射数组的值,可以将操作的结果传递给迭代中的下一个,以实现 reduce 算法,如果满足特定条件,可以提前退出循环,甚至可以迭代无线数量的元素。 —— Node.js 设计模式(第二版)

值得注意的是,在 ES6 里,引入了promise之后也可以更简便地抽象出顺序迭代的模式:

1
2
3
4
5
6
7
8
9
10
11
const tasks = [task1, task2, task3];

const didTask = tasks.reduce((prev, task) => {
return prev.then(() => {
return task();
})
}, Promise.resolve());

didTask.then(() => {
// do callback
});

并行执行流的迭代模式

在一些情况下,一组异步任务的执行顺序并不重要,我们需要的仅仅是任务完成的时候得到通知,就可以用并行执行流程来处理,例如:

1
2
3
4
5
      -> task1
/
start -> task2 (allEnd callback)
\
-> task3

不作要求的时候,在 Node.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
class AsyncTaskIterator {
constructor(tasks, callback) {
this.tasks = tasks;
this.callback = callback;
this.done = 0;
}
do() {
this.tasks.forEach(task => {
task(() => {
if(++this.done === this.tasks.length) {
this.finish();
}
});
});
}
finish() {
this.callback();
}
}

const tasks = [task1, task2, task3];
const asyncTaskIterator = new AsyncTaskIterator(tasks, callback);

asyncTaskIterator.do();

使用promise也就可以通过Promise.all()来接受所有的任务并且执行:

1
2
3
4
5
6
7
const tasks = [task1, task2, task3];

const didTask = tasks.map(task => task());

Promise.all(didTask).then(() => {
callback();
})

限制并行执行流的迭代模式

并行编程放飞自我自然是很爽,但是在很多情况下我们需要对并行队列的数量做限制,从而减少资源消耗,比如我们限制并行队列最大数为2:

1
2
3
4
5
      -> task1 -> task2 
/
start (allEnd callback)
\
-> task3

这时候,就需要抽象出一个并行队列,在使用的时候对其实例化:

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
class TaskQueque {
constructor(max) {
this.max = max;
this.running = 0;
this.queue = [];
}
push(task) {
this.queue.push(task);
this.next();
}
next() {
while(this.running < this.max && this.queue.length) {
const task = this.queue.shift();
task(() => {
this.running--;
this.next();
});
this.running++;
}
}
}

const tasks = [task1, task2, task3];
const taskQueue = new TaskQueue(2);

let done = 0, hasErrors = false;
tasks.forEach(task => {
taskQueue.push(() => {
task((err) => {
if(err) {
hasErrors = true;
return callback(err);
}
if(++done === tasks.length && !hasError) {
callback();
}
});
});
});

而用promise的处理方式也与之相似:

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
class TaskQueque {
constructor(max) {
this.max = max;
this.running = 0;
this.queue = [];
}
push(task) {
this.queue.push(task);
this.next();
}
next() {
while(this.running < this.max && this.queue.length) {
const task = this.queue.shift();
task.then(() => {
this.running--;
this.next();
});
this.running++;
}
}
}

const tasks = [task1, task2, task3];
const taskQueue = new TaskQueue(2);

const didTask = new Promise((resolve, reject) => {
let done = 0, hasErrors = true;
tasks.forEach(task => {
taskQueue.push(() => {
return task().then(() => {
if(++done === task.length) {
resolve();
}
}).catch(err => {
if(!hasErrors) {
hasErrors = true;
reject();
}
});
});
});
});

didTask.then(() => {
callback();
}).then(err => {
callback(err);
});

同时暴露两种类型的 API

那么问题来了,如果我需要封装一个库,使用者需要在callback和promise中灵活切换怎么办呢?让别人一直自己切换就会显得很难用,所以就需要同时暴露callback和promise的 API ,让使用者传入callback的时候使用callback,没有传入的时候返回一个promise:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function asyncDemo(args, callback) {
return new Promise((resolve, reject) => {
precess.nextTick(() => {
// do something
// 报错产出 err, 没有则产出 result
if(err) {
if(callback) {
callback(err);
}
return resolve(err);
}
if(callback){
callback(null, result);
}
resolve(result);
});
});
}

Node.js 模块上下文会被污染吗?

发表于 2018-05-11

很久之前在知乎上看到一个问题:

为什么 Node.js 不给每一个.js 文件以独立的上下文来避免作用域被污染?

后来在饿了么的分享中也碰到了相应的问题,就来整理一下,希望回顾的同时也能帮到需要的同学,如果这个没弄懂确实不应该说自己熟悉 Node.js 了。

关于这个问题,首先可以想到的关键词就是模块机制了,所以就先来整理一下 Node.js 的模块机制,最重要的就是require的原理。先看一下require的实现原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function require(...) {
var module = { exports: {} };
((module, exports) => {
// Your module code here. In this example, define a function.
function some_func() {};
exports = some_func;
// At this point, exports is no longer a shortcut to module.exports, and
// this module will still export an empty default object.
module.exports = some_func;
// At this point, the module will now export some_func, instead of the
// default object.
})(module, module.exports);
return module.exports;
}

由于这样的机制,我们再关注一下 Node.js 对 CommonJS 的实现:

1
2
3
4
5
6
7
8
9
10
11
var wrapper = Module.wrap(content);

var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
});

// ...

var result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);

在源码中,content实际上就是模块中的代码,比如你传如一个'console.log(t);',在Module.wrap完之后就会得到一个字符串wrapper:

1
'(function (exports, require, module, __filename, __dirname) { console.log(t);\n});'

然后用vm.runInThisContext将字符串转化成:

1
2
3
(function(exports, require, module, __filename, __dirname) {
console.log(t);
})();

在vm.runInThisContext的官方文档中对于编译之后的作用域解释为:

在指定的global对象的上下文中执行vm.Script对象里被编译的代码并返回其结果。被执行的代码虽然无法获取本地作用域,但是能获取global对象。

这解释了以下两个情况:

  • vm.runInThisContext使得包裹函数执行时无法影响本地作用域;
  • global对象是可以访问的。

也解释了饿了么提出的问题:

如果 a.js require了 b.js, 那么在 b 中定义全局变量t = 111能否在 a 中直接打印出来?

答案是可以的,因为global对象是可以访问的,因此t = 1等价于 global.t = 111,但是如果声明了t,上下文就不会被污染。

回到最初的问题,由于只有在 .js 文件中没有声明过才会被挂载在global对象上,每个模块就是独立的作用域,所以要避免上下文污染,只需要在写代码的时候添加'use strict':

1
2
'use strict'
t = 111 // 报错 t 未定义。

最后有人问就可以总结成一句话:

Node.js 模块正常情况对作用域不会造成污染,意外创建全局变量是一种例外,可以采用严格模式来避免。

浏览器和Node事件循环的区别

发表于 2018-05-10

事件循环,是 js 中老生常谈的一个话题了,而在浏览器和 Node 中的事件循环执行机制也不相同,浏览器的事件循环是在 HTML5 中定义的规范,而 Node 中则是由 libuv 库实现,不可以混为一谈。

先看一个简单的事件循环笔试题:

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
function sleep(time) {
let startTime = new Date();
while (new Date() - startTime < time) {}
console.log('<--Next Loop-->');
}

setTimeout(() => {
console.log('timeout1');
setTimeout(() => {
console.log('timeout3');
sleep(1000);
});
new Promise((resolve) => {
console.log('timeout1_promise');
resolve();
}).then(() => {
console.log('timeout1_then');
});
sleep(1000);
});

setTimeout(() => {
console.log('timeout2');
setTimeout(() => {
console.log('timeout4');
sleep(1000);
});
new Promise((resolve) => {
console.log('timeout2_promise');
resolve();
}).then(() => {
console.log('timeout2_then');
});
sleep(1000);
});

在不同的环境中,输出的结果也是不同的:

  • 浏览器中的输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    timeout1
    timeout1_promise
    <--Next Loop-->
    timeout1_then
    timeout2
    timeout2_promise
    <--Next Loop-->
    timeout2_then
    timeout3
    <--Next Loop-->
    timeout4
    <--Next Loop-->
  • Node 环境中的输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    timeout1
    timeout1_promise
    <--Next Loop-->
    timeout2
    timeout2_promise
    <--Next Loop-->
    timeout1_then
    timeout2_then
    timeout3
    <--Next Loop-->
    timeout4
    <--Next Loop-->

接下来我们就看看浏览器和 Node 中时间循环的区别是什么。

1. 任务队列

浏览器环境

浏览器环境下的 异步任务 分为 宏任务(macroTask) 和 微任务(microTask):

  • 宏任务(macroTask):script 中代码、setTimeout、setInterval、I/O、UI render;
  • 微任务(microTask): Promise、Object.observe、MutationObserver。

当满足执行条件时,宏任务(macroTask) 和 微任务(microTask) 会各自被放入对应的队列:宏队列(Macrotask Queue) 和 微队列(Microtask Queue) 中等待执行。

Node 环境

在 Node 环境中 任务类型 相对就比浏览器环境下要复杂一些:

  • microTask:微任务;
  • nextTick:process.nextTick;
  • timers:执行满足条件的 setTimeout 、setInterval 回调;
  • I/O callbacks:是否有已完成的 I/O 操作的回调函数,来自上一轮的 poll 残留;
  • poll:等待还没完成的 I/O 事件,会因 timers 和超时时间等结束等待;
  • check:执行 setImmediate 的回调;
  • close callbacks:关闭所有的 closing handles ,一些 onclose 事件;
  • idle/prepare 等等:可忽略。

因此,也就产生了执行事件循环相应的任务队列 Timers Queue、I/O Queue、Check Queue 和 Close Queue。

2.执行过程

浏览器环境

先执行<script>中的同步任务,然后所有微任务,一个宏任务,所有微任务,一个宏任务……

    1. 执行完主执行线程中的任务;
    1. 取出 Microtask Queue 中任务执行直到清空;
    1. 取出 Macrotask Queue 中一个任务执行;
    1. 重复 2 和 3 。

需要 注意 的是:

  • 在浏览器页面中可以认为初始执行线程中没有代码,每一个<script>中的代码是一个独立的 task ,即会执行完前面的<script>中创建的 microTask 再执行后面的<script>中的同步代码;
  • 如果 microTask 一直被添加,则会继续执行 microTask ,“卡死” macroTask;
  • 部分版本浏览器有执行顺序与上述不符的情况,可能是不符合标准或 js 与 html 部分标准冲突;
  • Promise 的then和catch才是 microTask ,本身的内部代码不是;
  • 个别浏览器独有API未列出。

Node 环境

循环之前

在进入第一次循环之前,会先进行如下操作:

  • 同步任务;
  • 发出异步请求;
  • 规划定时器生效的时间;
  • 执行process.nextTick()。

开始循环

循环中进行的操作:

  • 清空当前循环内的 Timers Queue,清空 NextTick Queue,清空 Microtask Queue;
  • 清空当前循环内的 I/O Queue,清空 NextTick Queue,清空 Microtask Queue;
  • 清空当前循环内的 Check Queue,清空 NextTick Queue,清空 Microtask Queue;
  • 清空当前循环内的 Close Queue,清空 NextTick Queue,清空 Microtask Queue;
  • 进入下轮循环。

可以看出,nextTick 优先级比 Promise 等 microTask 高,setTimeout和setInterval优先级比setImmediate高。

注意

在整个过程中,需要 注意 的是:

  • 如果在 timers 阶段执行时创建了setImmediate 则会在此轮循环的 check 阶段执行,如果在 timers 阶段创建了setTimeout,由于 timers 已取出完毕,则会进入下轮循环,check 阶段创建 timers 任务同理;
  • setTimeout优先级比setImmediate高,但是由于setTimeout(fn,0)的真正延迟不可能完全为 0 秒,可能出现先创建的setTimeout(fn,0)而比setImmediate的回调后执行的情况。

总结

事件循环在 浏览器 和 Node 中的区别很容易被人忽视,执行顺序整理如下:

浏览器环境下:

1
2
3
4
while (true) {
宏任务队列.shift();
微任务队列全部任务();
}

Node 环境下:

1
2
3
4
5
6
7
8
while (true) {
loop.forEach((阶段) => {
阶段全部任务();
nextTick全部任务();
microTask全部任务();
});
loop = loop.next;
}

个人觉得比较清晰了,有什么问题可以私信讨论。

文章部分理论参考自:https://segmentfault.com/a/1190000013660033?utm_source=channel-hottest

Javascript 的最佳实现方法总结(二)

发表于 2018-05-10

Javascript 的数组遍历方法总结

这是第二篇关于 Javascript 实现方法的总结文章,主要是总结从 ES5 到 ES6 中的各种数组遍历方式,以及使用这些方法可能会遇到的坑。当然了,刚开始总结的一定不会很全面,也希望有更多想法和问题的小伙伴可以大家一起交流,文章也会一直完善下去,虽然不一定会有人看,哈哈,那开始吧!

Javascript 有很多数组遍历的方法,我们先从传统的遍历方法开始,然后再引入ES6的新方法:

1. while

传统方法while理解起来很简单,给个index然后每次让index ++就ok了。然后我们引入了一个新的概念,就是ES6中的新语法...,它可以用于但不仅限于数组和对象的的扩展和解构,从一定程度讲,把它用好了在某种程度上就基本可以抛弃ES5里面像concat、slice等等的数组拼接方法了(只是说基本,目前还不敢全扔)。
关于ES6的用法已经基本整理好了,在未来两三个星期会陆续发,希望到时候可以关注。这里扔一个权威的传送门,阮一峰老师的《ECMAScript 6入门》:http://es6.ruanyifeng.com/

1
2
3
4
5
6
7
8
9
10
11
12
13
function whileLooping(...args) {
let index = 0
while(index < args.length){
console.log(args[index])
index ++
}
}

const flag = [1, 2, 3, 4]
whileLooping('java', 'python', 'c', 'c++', 'ruby')
// java python c c++ ruby
whileLooping('java', 'python', 'c', 'c++', 'ruby', flag)
// java python c c++ ruby [1, 2, 3, 4]

很简单,主要是为了做一个关于...语法的引子,让大家多使用更便捷的方法(虽然这里这么写好像有些鸡肋)。

2. for(传统方法里用的最多的)

这个我觉得不管是写什么的都比我熟,所以先象征性地扔一波看起来鸡肋又尴尬的代码:

1
2
3
4
5
6
7
8
9
10
11
function forLooping(array = [a = 10, b = 20, ...args], nothing = 20){
for(let i = 0; i < array.length; i++) {
console.log(array[i])
}
console.log(nothing)
}

forLooping([1, 2, 3, 4, 5])
// 1 2 3 4 5 20
forLooping([1, 2, 3, 4, 5], 10)
// 1 2 3 4 5 10

虽然这里涉及了ES6里的默认参数值以及...的另一种用法,但看起来都容易理解,就不多哔哔。接下来要分享的是一个我曾经在用for循环遇到的一个坑,关于作用域的。
大家可以尝试把let变成var,看看输出结果有什么变化吗?并没有。
但是在接下来这段代码里好像就不太一样了:

1
2
3
4
5
6
7
8
9
10
11
12
13
const array = []

for (var i = 0; i < 5; i++) {
array.push(function() {
console.log(i)
})
}
array[0]() // 5
array[1]() // 5
array[2]() // 5
array[3]() // 5
array[4]() // 5
console.log(array) // [ [Function], [Function], [Function], [Function], [Function] ]

明明push到数组里的看起来都是不一样的值,可是最后一跑就发现输出了没有一个值是对的。这是在var声明的时候,被引用的外部作用域中只有一个i,而不是为每次迭代的函数都有一个i被引用。如果你用let声明i,输出的值就会是0 1 2 3 4。这关系到var和let的区别,上面给的传送门里有介绍到,而我之后的ES6系列也会解释到,这里也不多哔哔了。
还有一件事就是,在某些博客上有人说const是用来声明常量的,可我们这里的array还能改变,这是因为用const声明值类型变量之后不能继续赋值,但是声明的引用类型变量还是可以被改变的。

3. forEach

这个是新方法里最基础的一个,唯一要注意的是IE 9以下浏览器不兼容。用的时候要向里面扔三个参数:

  • currentValue(必需):当前元素;
  • index(可选):当前元素索引值;
  • arr(可选):当前元素所属的数组对象。
1
2
3
4
5
6
7
8
9
10
11
12
function forEachLooping(...args) {
args.forEach((current_value, index, arr) => {
console.log(current_value, index, arr)
})
}

forEachLooping('java', 'python', 'c', 'c++', 'ruby')
// java 0 [ 'java', 'python', 'c', 'c++', 'ruby' ]
// python 1 [ 'java', 'python', 'c', 'c++', 'ruby' ]
// c 2 [ 'java', 'python', 'c', 'c++', 'ruby' ]
// c++ 3 [ 'java', 'python', 'c', 'c++', 'ruby' ]
// ruby 4 [ 'java', 'python', 'c', 'c++', 'ruby' ]

箭头函数=>用法值得关注,很好用,会在ES6系列里面具体介绍到。其它暂时没发现什么值得特别提的点,如果有小伙伴遇到这个东西的坑,希望可以跟我说一下,我们一起进步哈!

4. map

这里的map不是地图的意思,而是指映射,很好理解,就是原数组被映射成新的数组。在forEach方法中,并不会返回一个新的数组,而map则是对数组的每个元素使用一个函数,然后返回一个全新的数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function mapLooping(information) {
const formatInfo = information.map(info => {
return `姓名:${info.name},年龄:${info.age}`
})
console.log(formatInfo)
}
const information = [
{name: 'zixiang.xiao', age: 22},
{name: 'tong.li', age: 22},
{name: 'meimei.han', age: 30}
]
mapLooping(information)
// [ '姓名:zixiang.xiao,年龄:22',
// '姓名:tong.li,年龄:22',
// '姓名:meimei.han,年龄:30' ]

上面的代码也引入了ES6模板字符串的概念,书写起来相当方便,没什么特别的概念,有兴趣可以去了解一下。
关于map的兼容性,它不兼容IE 9以下版本的浏览器,如果非要在IE 6-8之间使用的话,也可以在Array.prototype里面进行扩展:

1
2
3
4
5
6
7
8
9
10
11
if (typeof Array.prototype.map != "function") {
Array.prototype.map = function (fn, context) {
var arr = []
if (typeof fn === "function") {
for (var k = 0, length = this.length; k < length; k++) {
arr.push(fn.call(context, this[k], k, this))
}
}
return arr
}
}

5. reduce

从某种程度上说reduce像是一个累加器,它会在数组中从左到右依次遍历,最终返回一个经过函数处理后的累积值。

1
2
3
4
5
6
7
8
9
function reduceLooping(...args) {
const array_sum = args.reduce((x, y) => {
return x + y
}, 10)
console.log(`初始值为10的array_sum累加之后结果为:${array_sum}`)
}

reduceLooping(1, 2, 3, 4, 5)
// 初始值为10的array_sum累加之后结果为:25

还有一个值得关注的点,如果数组处理之后需要返回一个累积值的时候,推荐使用reduce,从一个最直接的角度来说,据统计reduce的运算速度比for快几十倍。同样的,reduce只兼容IE 9及其以上的浏览器。

6. filter

跟名字一样,filter方法就是用来对一个数组进行过滤的。和之前的方法不一样的一点在于,filter的callback函数返回的是一个Boolean值。

1
2
3
4
5
6
7
8
function filterLooping(array) {
const after = array.filter(name => name.indexOf('D') >= 0)
console.log(after)
}

const name = ['Durant','James','Bryant','Duncan','Curry']
filterLooping(name)
// [ 'Durant', 'Duncan' ]

filter只兼容IE 9及其以上的浏览器。

7. every

关于every方法,用于检测数组所有元素是否都符合指定条件,使用指定函数检测数组中的所有元素:

  • 如果数组中检测到有一个元素不满足,则整个表达式返回false,且剩余的元素不会再进行检测;
  • 如果所有元素都满足条件,则返回true。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function everyLooping(array) {
    let truth = array.every(user => user.indexOf('cool') >= 0)
    if(truth) {
    console.log('都很帅!');
    }else {
    console.log('并不是所有人都很帅!')
    }
    }

    const user = ['zixiang.xiao, cool','lei.li, 9','meimei.han, 9','mou.liu, pdd','benwei.lu, ugly']
    everyLooping(user)
    // 并不是所有人都很帅!

可解释的不多,every只兼容IE 9及其以上的浏览器。希望看完代码你不会觉得我厚颜无耻,就算你真这样觉得也没有什么用!

8. some

some的用法和every其实差不多,只不过some的含义是判断一个数组上的元素是否至少有一个符合某种条件, 方法会依次执行数组的每个元素:

  • 如果有一个元素满足条件,则表达式返回true, 剩余的元素不会再执行检测;
  • 如果没有满足条件的元素,则返回false。
1
2
3
4
5
6
7
8
9
10
11
12
function someLooping(array) {
let truth = array.some(user => user.indexOf('benwei.lu') >= 0)
if(truth) {
console.log('有人长得丑!');
}else {
console.log('并没有特别丑的人!')
}
}

const user = ['zixiang.xiao, cool','lei.li, 9','meimei.han, 9','mou.liu, pdd','benwei.lu, ugly']
someLooping(user)
// 有人长得丑!

这个代码看完我相信你会觉得我说的真对!

总结

其实每一种方法都有它特定的场景可以作为一个优秀的方案使用,作为一个前端小菜鸡,我也会多去尝试这些东西的使用,在一定的累积之后会继续写它们的应用。关于低版本浏览器不兼容的问题,之后我会在原文章上边添加关于低版本浏览器的兼容方法,不过目前我只能做到兼容到IE6,我相信也没多少人用更低的版本了,毕竟长得帅的人用浏览器都是与时俱进的!
最后最后,如果你觉得我写的还行的话,就快给我点赞并且关注我吧!我会继续努力的!谢谢各位大佬!

Javascript 的最佳实现方法总结(一)

发表于 2018-05-10

Javascript 的最佳实现方法总结

本文将主要针对 Javascript 来总结一些常用的实现方法,有的是根据自身实践总结出来的经验,有的是在逛别人的博客和技术分享时候得到的启发,全部都会慢慢搬到这里,也欢迎有更多的人来提供你们的实现方式,大家互相交流。作为一个刚刚毕业的前端小白,也希望的各位老司机能为我指出不足。本文也将不定期更新!

1. 生成随机字符串的实现

还记得校招的时候,以为自己面的是前端岗,应该主要就是考察布局基础、Javascript基础、常用框架理解、Node.js、前端性能优化等等知识,但是没想到坐下来面试官就让我写一个随机字符串生成器,当时也写出来了,不过显得十分笨重,下面是我在阅读别人代码里面看到的比较不错的实现方式,希望可以对工作和面试的人有用。

1
2
3
4
5
6
7
8
const random_str = function(length) {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
let str = ''
for(let i = 0; i <length; i++) {
str += chars.charAt(Math.floor(Math.random() * chars.length))
}
return str
}

直接通过对输出字符串增加字符,要比使用数组方法好一些,而且这样生成一个指定位数的随机字符串,重复几率也会很低,并且随着length增大,重复几率降低。

2. 前端获取服务端时间

这个是在逛博客的时候看到的黑科技,简单粗暴!

1
2
3
4
const request = new XMLHttpRequest()
request.get('GET', location, false)
request.send(null)
console.log(req.getResponseHeader('Date'))

3. 检测类型

还记得以前去面试的时候,面试官问我怎么判断一个变量的类型,我张口就是typeof,现在想想真想给自己一个大耳刮子,实际上,typeof只能用来检测基本类型,我们举个例子:

1
2
3
4
5
6
7
8
typeof 20 //'number'
typeof 'hey man' //'string'
typeof true //'boolean'
typeof undefined //'undefined'
typeof Symbol() //'symbol'
typeof {} //'object'
typeof [] //'object'
typeof Function() //'function'

我们可以看到,基本类型使用typeof就可以检测出变量类型,可是在引用类型里,对象和数组输出的值都是object,函数则是输出function。对于自定义类型和原生的引用类型,一般使用instanceof进行判断。比如:

1
2
3
4
5
let obj = {}
let arr = []
obj instanceof Object //true
obj instanceof Array //false
arr instanceof Array // true

但是有时候情况往往不是这么简单。比如如果你想检测 iframe 里面的属性值的话,基本上是不可能的。因为检测的前提要求是在同一个全局作用于下。所以为了能够正常检测一些值的类型(排除在 iframe 的情况)。那有没有什么万能的方法呢?确实有,你可以使用调用toString()的方法,得到相关的类型。就像这样:

1
2
3
let value = new FormData()
console.log(Object.prototype.toString.call(value))
//'[object FormData]'

由此,也就可以得出一个通用的检测类型方法,基本上可以返回所有的类型。代码如下:

1
2
3
const getType = function(value) {
return Object.prototype.toString.call(value).match(/\s{1}(\w+)/)[1]
}

4. 前端压缩图片

一年前找实习的时候被问到前端性能优化,就有提到前端压缩图片,那么要怎么压缩?为什么要压缩?对于压缩图片的需求一般会出现在上传图片(尤其是移动端)功能里,比如微信发朋友圈、QQ空间发说说(现在多了一个发送原图的按钮)的时候,就对上传的图片进行过压缩。

毕竟现在的手机硬件越来越流弊,随便一拍就是好几M,无论是用3G/4G还是Wifi上传起来都比较费劲,而且也比较浪费流量,一般只需要100+M就已经差不多了。因此,在用户选择好需要上传的图片之后,在客户端需要进行处理,将图片压缩好再上传到服务器进行下一步处理。

图片压缩的原理大同小异,自己也试着写过一些,都不理想,后来看到一篇博客分享了关于前端压缩图片的一个插件localResizeIMG,觉得很好用,就拿来分享了。引用localResizeIMG官方文档的原话:

基本原理是通过canvas渲染图片,再通过 toDataURL 方法压缩保存为 base64 字符串(能够编译为jpg格式的图片)。

那么怎么用呢?也很简单,传入照片(可以是file对象也可以直接传入图片路径),然后设置好自己想要的分辨率(就是最大width和height是多少px),接着设置个图片质量,再来一个Promise风格的callback差不多就行了,最后把压缩完的base64作为参数传进callback。作者还很贴心的把照片base64编码的长度也传参进来了,方便后端校验图片是否上传完整。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<input onchange="upload().bind(this)" type="file" accept="image/*" />
function upload() {
lrz(this.files[0])
.then(rst => {
// 处理成功会执行
})
.catch(err => {
// 处理失败会执行
})
.always(() => {
// 不管是成功失败,都会执行
})
})

再给个传送门:https://github.com/think2011/localResizeIMG


暂时先总结这四个,有需要的同学可以先看着,等过两天还会给大家带来更多的分享,有的是来自我自己,有的是我在逛技术圈的时候看到比较不错的实现方式,有什么不对的地方欢迎指出来,有什么更好的实现方式也可以提出来,大家一起交流一起进步!当然点个赞和关注是最好的啦!

ES6 总结(一):常用方法

发表于 2018-05-10

##Tip
最近读完了 Nicholas C. Zakas 的新书 深入理解ES6,根据之前的经验和一些文档梳理的知识脉络,总结一些关于 ES6 的常用方法和技巧。

本文参考文章:https://github.com/DrkSephy/es6-cheatsheet

##1. let/const 特性
在 ES6 标准发布之前,JS 一般都是通过关键字var声明变量,与此同时,并不存在明显的代码块声明,想要形成代码块,一般都是采用闭包的方式,举个十分常见的例子:

1
2
3
4
5
6
7
8
9
10
11
12
var arr = []

for(var i = 0; i < 5; i++) {
arr.push(function() {
console.log(i)
})
}

arr.forEach(function(item) {
item()
})
// 输出5次数字5

关于为什么输出的全是数字5,涉及到了JS的事件循环机制,异步队列里的函数执行的时候,由于关键字var不会生成代码块,所以参数i = 5,最后也就全输出了数字5。用之前的方法我们可以这样修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var arr = []

for(var i = 0; i < 5; i++) {
(function(i) {
arr.push(function() {
console.log(i)
})
})(i)
}

arr.forEach(function(item) {
item()
})
// 输出 0 1 2 3 4

而在引入let和const之后,这两个关键字会自动生成代码块,并且不存在变量提升,因此只需要把var关键字换成let就可以输出数字0到4了:

1
2
3
4
5
6
7
8
9
10
11
12
var arr = []

for(let i = 0; i < 5; i++) {
arr.push(function() {
console.log(i)
})
}

arr.forEach(function(item) {
item()
})
// 输出 0 1 2 3 4

关键字let和const的区别在于,使用const声明的值类型变量不能被重新赋值,而引用类型变量是可以的。

变量提升是因为浏览器在编译执行JS代码的时候,会先对变量和函数进行声明,var关键字声明的变量也会默认为undefined,在声明语句执行时再对该变量进行赋值。

值得注意的是,在重构原先代码的过程中,要十分注意,盲目地使用let来替换var可能会出现出乎意料的情况:

1
2
3
4
5
6
7
8
9
10
11
12
var snack = 'Meow Mix'

function getFood(food) {
if (food) {
var snack = 'Friskies'
return snack
}
return snack
}

getFood(false)
// undefined

替换之后会出现与原先输出不匹配的情况,至于原因,就是上面提到的let不存在变量提升。
使用var虽然没有执行if内的语句,但是在声明变量的时候已经声明了var snack = undefined的局部变量,最后输出的是局部变量里的undefined。

1
2
3
4
5
6
7
8
9
10
11
12
let snack = 'Meow Mix'

function getFood(food) {
if (food) {
let snack = 'Friskies'
return snack
}
return snack
}

getFood(false)
// 'Meow Mix'

而使用let则在不执行if语句时拿不到代码块中局部的snack变量(在临时死区中),最后输出了全局变量中的snack。

当前使用块级绑定的最佳实践是:默认使用const,只在确实需要改变变量的值时使用let。这样就可以在某种程度上实现代码的不可变,从而防止某些错误的产生。

2. 箭头函数

在 ES6 中箭头函数是其最有趣的新增特性,它是一种使用箭头=>定义函数的新语法,和传统函数的不同主要集中在:

  • 没有this、super、arguments和new.target绑定
  • 不能通过new关键字调用
  • 没有原型
  • 不可以改变this的绑定
  • 不支持arguments对象
  • 不支持重复的命名参数

this的绑定是JS程序中一个常见的错误来源,尤其是在函数内就很容易对this的值是去控制,经常会导致意想不到的行为。

在出现箭头函数之前,在声明构造函数并且修改原型的时候,经常会需要对this的值进行很多处理:

1
2
3
4
5
6
7
8
9
10
11
12
function Phone() {
this.type = type
}

Phone.prototype.fixTips = function(tips) {
// var that = this
return tips.map(function(tip) {
// return that.type + tip
return this.type + tip
// })
},this)
}

要输出正确的fixTips,必须要对this的指向存在变量中或者给它找个上下文绑定,而如果使用箭头函数的话,则很容易实现:

1
2
3
4
5
6
7
function Phone() {
this.type = type
}

Phone.prototype.fixTips = function(tips) {
return tips.map(tip => this.type + tip)
}

就像上面的例子一样,在我们写一个函数的时候,箭头函数更加简洁并且可以简单地返回一个值。

当我们需要维护一个this上下文的时候,就可以使用箭头函数。

3. 字符串

我认为 ES6 在对字符串处理这一块,新增的特性是最多的,本文只总结常用的方法,但还是推荐大家有时间去仔细了解一下。

.includes()

之前在需要判断字符串中是否包含某些字符串的时候,基本都是通过indexOf()的返回值来判断的:

1
2
3
4
var str = 'superman'
var subStr = 'super'
console.log(str.indexOf(subStr) > -1)
// true

而现在可以简单地使用includes()来进行判断,会返回一个布尔值:

1
2
3
4
const str = 'superman'
const subStr = 'super'
console.log(str.includes(subStr))
// true

当然除此之外还有两个特殊的方法,它们的用法和includes()一样:

  • startWith():如果在字符串的起始部分检测到指定文本则返回true
  • endsWith():如果在字符串的结束部分检测到指定文本则返回true

.repeat()

在此之前,需要重复字符串,我们需要自己封装一个函数:

1
2
3
4
5
6
7
function repeat(str, count) {
var strs = []
while(str.length < count) {
strs.push(str)
}
return strs.join('')
}

现在则只需要调用repeat()就可以了:

1
2
'superman'.repeat(2)
// supermansuperman

模板字符串

我觉得模板字符串也是ES6最牛逼的特性之一,因为它极大地简化了我们对于字符串的处理,开发过程中也是用得特别爽。
首先它让我们不用进行转义处理了:

1
2
var text = 'my name is \'Branson\'.'
const newText = `my name is 'Branson'.`

然后它还支持插入、换行和表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const name = 'Branson'
console.log(`my name is ${name}.`)
// my name is Branson.

const text = (`
what's
wrong
?
`)
console.log(text)
// what's
// wrong
// ?

const today = new Date()
const anotherText = `The time and date is ${today.toLocaleString()}.`
console.log(anotherText)
// The time and date is 2017-10-23 14:52:00

值得注意的是,之前在改离职同学留下的bug时,也发现了要推荐使用模板字符串的一个点:

1
2
3
4
5
6
7
8
9
10
11
formatDate(date) {
if(!date) return ""
if(!(date instanceof Date)) date = new Date(date)
const y = date.getFullYear()
let m = date.getMonth() + 1
m = m < 10 ? ('0' + m) : m
let d = date.getDate()
d = d < 10 ? ('0' + d) : d
// return y + m + d
return `${y}${m}${d}`
}

之前使用的是注释掉的输出方式,由于这位同学在判断m和d的值的时候,没有把大于10情况下的值类型转换成字符串,所以在我们放了个“十一”假回来输出的值就出现问题了。
而使用模板字符串有一个好处就是,我不管你m和d是不是类型转换了,我最后都输出一个字符串,算是容错率更高了吧。

4. 解构

解构可以让我们用一个更简便的语法从一个数组或者对象(即使是深层的)中分离出来值,并存储他们。
这一块没什么可说的,直接放代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 数组解构
// ES5
var arr = [1, 2, 3, 4]
var a = arr[0]
var b = arr[1]
var c = arr[2]
var d = arr[3]
// ES6
let [a, b, c, d] = [1, 2, 3, 4]

// 对象解构
// ES5
var luke = {occupation: 'jedi', father: 'anakin'}
var occupation = luke.occupation
// 'jedi'
var father = luke.father
// 'anakin'
// ES6
let luke = {occupation: 'jedi', father: 'anakin'}
let {occupation, father} = luke
console.log(occupation)
// 'jedi'
console.log(father)
// 'anakin'

5. 模块

在 ES6 之前,我们使用Browserify这样的库来创建客户端的模块化,在node.js中使用require。在 ES6 中,我们可以直接使用所有类型的模块化(AMD 和 CommonJS)。

CommonJS 模块的出口定义:

1
2
3
4
module.exports = 1
module.exports = { foo: 'bar' }
module.exports = ['foo', 'bar']
module.exports = function bar () {}

ES6 模块的出口定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 暴露单个对象
export let type = 'ios'

// 暴露多个对象
function deDuplication(arr) {
return [...(new Set(arr))]
}
function fix(item) {
return `${item} ok!`
}
export {deDuplication, fix}

// 暴露函数
export function sumThree(a, b, c) {
return a + b + c
}

// 绑定默认输出
let api = {
deDuplication,
fix
}
export default api
// export { api as default }

模块出口最佳实践:总是在模块的最后面使用export default方法,这可以让暴露的东西更加清晰并且可以节省时间去找出暴露出来值的名字。尤其如此,在 CommonJS 中通常的实践就是暴露一个简单的值或者对象。坚持这种模式,可以让我们的代码更加可读,并且在 ES6 和 CommonJS 模块之间更好地兼容。

ES6 模块导入:

1
2
3
4
5
6
7
8
9
10
11
// 导入整个文件
import 'test'

// 整体加载
import * as test from 'test'

// 按需导入
import { deDuplication, fix } from 'test'

// 遇到出口为 export { foo as default, foo1, foo2 }
import foo, { foo1, foo2 } from 'foos'

6. 参数

参数这一块儿在这之前,无论是默认参数、不定参数还是重命名参数都需要我们做很多处理,有了ES6之后相对来说就简洁多了:

默认参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ES5
function add(x, y) {
x = x || 0
y = y || 0
return x + y
}

// ES6
function add(x=0, y=0) {
return x + y
}

add(3, 6) // 9
add(3) // 3
add() // 0

不定参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
// ES5
function logArgs() {
for(var i = 0; i < arguments.length; i++) {
console.log(arguments[i])
}
}

// ES6
function logArgs(...args) {
for(let arg of args) {
console.log(arg)
}
}

命名参数:

1
2
3
4
5
6
7
8
9
10
11
12
// ES5
function Phone(options) {
var type = options.type || 'ios'
var height = options.height || 667
var width = options.width || 375
}

// ES6
function Phone(
{type='ios', height=667, width=375}) {
console.log(height)
}

展开操作:

求一个数组的最大值:

1
2
3
4
5
// ES5
Math.max.apply(null, [-1, 100, 9001, -32])

// ES6
Math.max(...[-1, 100, 9001, -32])

当然这个特性还可以用来进行数组的合并:

1
2
3
4
const player = ['Bryant', 'Durant']
const team = ['Wade', ...player, 'Paul']
console.log(team)
// ['Wade', 'Bryant', 'Durant', 'Paul']

7. 类 class

关于面向对象这个词,大家都不陌生,在这之前,JS要实现面向对象编程都是基于原型链,ES6提供了很多类的语法糖,我们可以通过这些语法糖,在代码上简化很多对prototype的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ES5
// 创造一个类
function Animal(name, age) {
this.name = name
this.age = age
}
Animal.prototype.incrementAge = function() {
this.age += 1
}

// 类继承
function Human(name, age, hobby, occupation) {
Animal.call(this, name, age)
this. hobby = hobby
this.occupation = occupation
}
Human.prototype = Object.create(Animal.prototype)
Human.prototype.constructor = Human
Human.prototype.incrementAge = function() {
Animal.prototype.incrementAge.call(this)
console.log(this.age)
}

在ES6中使用语法糖简化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ES6
// 创建一个类
class Animal {
constructor(name, age) {
this.name = name
this.age = age
}
incrementAge() {
this.age += 1
}
}

// 类继承
class Human extends Animal {
constructor(name, age, hobby, occupation) {
super(name, age)
this.hobby = hobby
this.occupation = occupation
}
incrementAge() {
super.incrementAge()
console.log(this.age)
}
}

注意:尽管类与自定义类型之间有诸多相似之处,我们仍然需要牢记它们之间的这些差异:

  • 函数声明可以被提升,而类声明与let声明类似,不能被提升;真正执行声明语句之前,它们会一直存在于临时死区中。
  • 类声明中的所有代码将自动运行在严格模式下,而且无法强行让代码脱离严格模式进行。
  • 在自定义类型中,需要通过Object.defineProperty()方法手动指定某个方法不可枚举;而在类中,所有方法都是不可枚举的。
  • 每个类都有一个constructor方法,通过关键字new调用那些不包含constructor的方法会导致程序抛出错误。
  • 使用除关键字new以外的方式调用类的构造函数会导致程序抛出错误。
  • 在类中修改类名会导致程序报错。

8. Symbols

Symbols在 ES6 之前就已经存在,我们现在可以直接使用一个开发的接口了。
Symbols是不可改变并且是独一无二的,可以在任意哈希中作一个key。

Symbol():

调用Symbol()或者Symbol(description)可以创造一个独一无二的符号,但是在全局是看不到的。Symbol()的一个使用情况是给一个类或者命名空间打上补丁,但是可以确定的是你不会去更新它。比如,你想给React.Component类添加一个refreshComponent方法,但是可以确定的是你不会在之后更新这个方法:

1
2
3
4
5
const refreshComponent = Symbol()

React.Component.prototype[refreshComponent] = () => {
// do something
}

Symbol.for(key):
Symbol.for(key)同样会创造一个独一无二并且不可改变的 Symbol,但是它可以全局看到,两个相同的调用Symbol.for(key)会返回同一个Symbol类:

1
2
3
4
5
6
Symbol('foo') === Symbol('foo') 
// false
Symbol.for('foo') === Symbol('foo')
// false
Symbol.for('foo') === Symbol.for('foo')
// true

对于 Symbols 的普遍用法(尤其是Symbol.for(key))是为了协同性。它可以通过在一个第三方插件中已知的接口中对象中的参数中寻找用 Symbol 成员来实现,比如:

1
2
3
4
5
6
7
8
9
function reader(obj) {
const specialRead = Symbol.for('specialRead')
if (obj[specialRead]) {
const reader = obj[specialRead]()
// do something with reader
} else {
throw new TypeError('object cannot be read')
}
}

在另一个库中:

1
2
3
4
5
6
7
8
const specialRead = Symbol.for('specialRead')

class SomeReadableType {
[specialRead]() {
const reader = createSomeReaderFrom(this)
return reader
}
}

9. Set/Map

在此之前,开发者都是用对象属性来模拟set和map两种集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// set
var set = Object.create(null)

set.foo = true

if(set.foo) {
// do something
}

// map
var map = Object.create(null)
map.foo = 'bar'

var value = map.foo
console.log(value)
// 'bar'

由于在 ES6 中set和map的操作与其它语言类似,本文就不过多介绍这些,主要通过几个例子来说说它们的应用。

在 ES6 中新增了有序列表set,其中含有一些相互独立的非重复值,通过set集合可以快速访问其中的数据,更有效地追踪各种离散值。

关于set运用得最多的应该就是去重了:

1
2
3
4
5
6
7
8
const arr = [1, 1, 2, 11, 32, 1, 2, 3, 11]

const deDuplication = function(arr) {
return [...(new Set(arr))]
}

console.log(deDuplication(arr))
// [1, 2, 11, 32, 3]

map是一个非常必需的数据结构,在 ES6 之前,我们通过对象来实现哈希表:

1
2
3
var map = new Object()
map[key1] = 'value1'
map[key2] = 'value2'

但是它并不能防止我们偶然地用一些特殊的属性名重写函数:

1
2
getOwnProperty({ hasOwnProperty: 'Hah, overwritten'}, 'Pwned')
// TypeError: Property 'hasOwnProperty' is not a function

在 ES6 中map允许我们队值进行get、set和search操作:

1
2
3
4
5
6
7
8
let map = new Map()

map.set('name', 'david')

map.get('name')
// david
map.has('name')
// true

而map更令人惊奇的部分就是它不仅限于使用字符串作为 key,还可以用其他任何类型的数据作为 key:

1
2
3
4
5
6
7
8
9
10
11
12
let map = new Map([
['name', 'david'],
[true, 'false'],
[1, 'one'],
[{}, 'object'],
[function () {}, 'function']
])

for(let key of map.keys()) {
console.log(typeof key)
}
// string, boolean, number, object, function

注意:我们使用map.get()方法去测试相等时,如果在map中使用函数或者对象等非原始类型值的时候测试将不起作用,所以我们应该使用 Strings, Booleans 和 Numbers 这样的原始类型的值。

我们还可以使用 .entries() 来遍历迭代:

1
2
3
for(let [key, value] of map.entries()) {
console.log(key, value);
}

10. Weak Set/Weak Map

对于set和WeakSet来说,它们之间最大的区别就是,WeakSet保存的是对象值得弱引用,下面这个实例会展示它们的差异:

1
2
3
4
5
6
7
8
9
let set = new WeakSet(),
key = {}

set.add(key)
console.log(set.has(key))
// true

// 移除对象key的最后一个强引用( WeakSet 中的引用也自动移除 )
key = null

这段代码执行过后,就无法访问WeakSet中 key 的引用了。除了这个,它们还有以下几个差别:

  • 在WeakSet的实例中,如果向add()、has()和delete()这三个方法传入非对象参数都会导致程序报错。
  • WeakSet集合不可迭代,所以不能被用于for-of循环。
  • WeakSet集合不暴露任何迭代器(例如keys()和values()方法),所以无法通过程序本身来检测其中的内容。
  • WeakSet集合不支持forEach()方法。
  • WeakSet集合不支持size属性。

总之,如果你只需要跟踪对象引用,你更应该使用WeakSet集合而不是普通的set集合。

在 ES6 之前,为了存储私有变量,我们有各种各样的方法去实现,其中一种方法就是用命名约定:

1
2
3
4
5
6
7
8
9
class Person {
constructor(age) {
this._age = age
}

_incrementAge() {
this._age += 1
}
}

但是命名约定在代码中仍然会令人混淆并且并不会真正的保持私有变量不被访问。现在,我们可以使用WeakMap来存储变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let _age = new WeakMap()
class Person {
constructor(age) {
_age.set(this, age)
}

incrementAge() {
let age = _age.get(this) + 1
_age.set(this, age)
if (age > 50) {
console.log('Midlife crisis')
}
}
}

在WeakMap存储变量很酷的一件事是它的 key 他不需要属性名称,可以使用Reflect.ownKeys()来查看这一点:

1
2
3
4
5
const person = new Person(50)
person.incrementAge()
// 'Midlife crisis'
Reflect.ownKeys(person)
// []

一个更实际的实践就是可以WeakMap储存 DOM 元素,而不会污染元素本身:

1
2
3
4
5
6
7
8
9
10
11
12
13
let map = new WeakMap()
let el = document.getElementById('someElement');

// Store a weak reference to the element with a key
map.set(el, 'reference')

// Access the value of the element
let value = map.get(el)
// 'reference'

// Remove the reference
el.parentNode.removeChild(el)
el = null

如上所示,当一个对象被垃圾回收机制销毁的时候,WeakMap将会自动地一处关于这个对象地键值对。

注意:为了进一步说明这个例子的实用性,可以考虑 jQuery 是如何实现缓存一个对象相关于对引用地 DOM 元素对象。使用 jQuery ,当一个特定地元素一旦在 document 中移除的时候,jQuery 会自动地释放内存。总体来说,jQuery 在任何 dom 库中都是很有用的。

11. Promise

在 ES6 出现之前,处理异步函数主要是通过回调函数,虽然看起来也挺不错,但是用多之后就会发现嵌套太多回调函数回引起回调地狱:

1
2
3
4
5
6
7
8
9
10
11
func1(function (value1) {
func2(value1, function (value2) {
func3(value2, function (value3) {
func4(value3, function (value4) {
func5(value4, function (value5) {
// Do something with value 5
})
})
})
})
})

当我们有了 Promise 之后,就可以将这些转化成垂直代码:

1
2
3
4
5
6
7
func1(value1)
.then(func2)
.then(func3)
.then(func4)
.then(func5, value5 => {
// Do something with value 5
})

原生的 Promise 有两个处理器:resolve(当 Promise 是fulfilled时的回调)和reject(当 Promise 是rejected时的回调):

1
2
3
new Promise((resolve, reject) =>
reject(new Error('Failed to fulfill Promise')))
.catch(reason => console.log(reason))

Promise的好处:对错误的处理使用一些列回调会使代码很混乱,使用 Promise,我看可以清晰的让错误冒泡并且在合适的时候处理它,甚至,在 Promise 确定了resolved/rejected之后,他的值是不可改变的——它从来不会变化。

这是使用 Promise 的一个实际的例子:

1
2
3
4
5
6
7
8
9
10
11
const request = require('request')

return new Promise((resolve, reject) => {
request.get(url, (error, response, body) => {
if (body) {
resolve(JSON.parse(body))
} else {
resolve({})
}
})
})

我们还可以使用 Promise.all() 来并行处理多个异步函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let urls = [
'/api/commits',
'/api/issues/opened',
'/api/issues/assigned',
'/api/issues/completed',
'/api/issues/comments',
'/api/pullrequests'
]


let promises = urls.map((url) => {
return new Promise((resolve, reject) => {
$.ajax({ url: url })
.done((data) => {
resolve(data);
})
})
})

Promise.all(promises)
.then((results) => {
// Do something with results of all our promises
})

12. Generators 生成器

就像 Promise 可以帮我们避免回调地狱,Generator 可以帮助我们让代码风格更整洁——用同步的代码风格来写异步代码,它本质上是一个可以暂停计算并且可以随后返回表达式的值的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function* sillyGenerator() {
yield 1
yield 2
yield 3
yield 4
}

var generator = sillyGenerator();
console.log(generator.next())
// { value: 1, done: false }
console.log(generator.next())
// { value: 2, done: false }
console.log(generator.next())
// { value: 3, done: false }
console.log(generator.next())
// { value: 4, done: false }

next可以回去到下一个yield返回的值,当然上面的代码是非常不自然的,我们可以利用 Generator 来用同步的方式来写异步操作:

1
2
3
4
5
function request(url) {
getJSON(url, function(response) {
generator.next(response)
})
}

这里的 generator 函数将会返回需要的数据:

1
2
3
4
5
6
function* getData() {
var entry1 = yield request('http://some_api/item1')
var data1 = JSON.parse(entry1)
var entry2 = yield request('http://some_api/item2')
var data2 = JSON.parse(entry2)
}

通过yield,我们可以保证entry1有data1中我们需要解析并储存的数据。

虽然我们可以利用 Generator 来用同步的方式来写异步操作,但是确认错误的传播变得不再清晰,我们可以在 Generator 中加上 Promise:

1
2
3
4
5
function request(url) {
return new Promise((resolve, reject) => {
getJSON(url, resolve)
})
}

然后我们写一个函数逐步调用next并且利用 request 方法产生一个 Promise:

1
2
3
4
5
6
7
8
9
function iterateGenerator(gen) {
var generator = gen()
(function iterate(val) {
var ret = generator.next()
if(!ret.done) {
ret.value.then(iterate)
}
})()
}

在 Generators 中加上 Promise 之后我们可以更清晰的使用 Promise 中的.catch和reject来捕捉错误,让我们使用新的 Generator,和之前的还是蛮相似的:

1
2
3
4
5
6
iterateGenerator(function* getData() {
var entry1 = yield request('http://some_api/item1')
var data1 = JSON.parse(entry1)
var entry2 = yield request('http://some_api/item2')
var data2 = JSON.parse(entry2)
})

13. Async Await

当 ES7 真正到来的时候,async await可以用更少的处理实现 Promise 和 Generators 所实现的异步处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var request = require('request')

function getJSON(url) {
return new Promise(function(resolve, reject) {
request(url, function(error, response, body) {
resolve(body)
})
})
}

async function main() {
var data = await getJSON()
console.log(data)
// NOT undefined!
}

main()

14. Getter/Setter 函数

ES6 已经开始实现了 getter 和 setter 函数:

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
class Employee {

constructor(name) {
this._name = name
}


get name() {
if(this._name) {
return 'Mr. ' + this._name.toUpperCase()
} else {
return undefined
}
}

set name(newName) {
if (newName == this._name) {
console.log('I already have this name.')
} else if (newName) {
this._name = newName
} else {
return false
}
}
}

var emp = new Employee("James Bond")

// uses the get method in the background
if (emp.name) {
console.log(emp.name)
// Mr. JAMES BOND
}

// uses the setter in the background
emp.name = "Bond 007"
console.log(emp.name)
// Mr. BOND 007

最新版本的浏览器也在对象中实现了getter和setter函数,我们可以使用它们来实现 计算属性,在设置和获取一个属性之前加上监听器和处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var person = {
firstName: 'James',
lastName: 'Bond',
get fullName() {
console.log('Getting FullName')
return this.firstName + ' ' + this.lastName
},
set fullName (name) {
console.log('Setting FullName')
var words = name.toString().split(' ')
this.firstName = words[0] || ''
this.lastName = words[1] || ''
}
}

person.fullName
// James Bond
person.fullName = 'Bond 007'
person.fullName
// Bond 007

总结

本文部分代码参考自:https://github.com/DrkSephy/es6-cheatsheet
主要是为了分享一些 ES6 的用法技巧以及最佳实践,欢迎交流!

Javascript 实现基础数据结构

发表于 2018-05-10

##Tip
计算机基础不管是什么语言、什么开发岗位都是必须要了解的知识,本文主要是通过 JS 来实现一些常用的数据结构,很简单。

本文插图引用:啊哈!算法 ,由于是总结性文章,所以会不定时地修改并更新,欢迎关注!

Stack 栈

堆栈(stack),也可以直接称为栈,它是一种后进先出的数据结构,并且限定了只能在一段进行插入和删除操作。

如上图所示,假设我们有一个球桶,并且桶的直径只能允许一个球通过。如果按照 2 1 3 的顺序把球放入桶中,然后要将球从桶中取出,取出的顺序就是 3 1 2,先放入桶中的球被后取出,这就是 栈 插入和删除的原理。

我们接触过的算法书基本也都是用c语言实现这个数据结构,它的特殊之处在于只能允许链接串列或者阵列的一端(top)进行 插入数据(push)和 输出数据(pop)的运算。那么如果要用 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
29
30
31
32
33
34
35
36
37
38
39
40
// 栈可以用一维数组或连结串列的形式来完成

class Stack {
constructor() {
this.data = []
this.top = 0
}
// 入栈
push() {
const args = [...arguments]
args.forEach(arg => this.data[this.top++] = arg)
return this.top
}
// 出栈
pop() {
if(this.top === 0) {
throw new Error('The stack is already empty!')
}
const popData = this.data[--this.top]
this.data = this.data.slice(0, -1)
return popData
}
// 获取栈内元素的个数
length() {
return this.top
}
// 判断栈是否为空
isEmpty() {
return this.top === 0
}
// 获取栈顶元素
goTop() {
return this.data[this.top-1]
}
// 清空栈
clear() {
this.top = 0
return this.data = []
}
}

通过 面向对象 的思想就可以抽象出一个 栈 的对象,实例化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const stack = new Stack()

stack.push(1, 2)
stack.push(3, 4, 5)
console.log(stack.data)
// [ 1, 2, 3, 4, 5 ]
console.log(stack.length())
// 5
console.log(stack.goTop())
// 5
console.log(stack.pop())
// 5
console.log(stack.pop())
// 4
console.log(stack.goTop())
// 3
console.log(stack.data)
// [ 1, 2, 3 ]

stack.clear()
console.log(stack.data)
// []

在 啊哈!算法 描述 栈 的应用时,举了一个求回文数的例子,我们也用 JS 来把它实现一波:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const isPalindrome = function(words) {
const stack = new Stack()
let copy = ''
words = words.toString()

words.split('').forEach(word => stack.push(word))

while(stack.length() > 0) {
copy += stack.pop()
}
return copy === words
}

console.log(isPalindrome('123ada321'))
// true
console.log(isPalindrome(1234321))
// true
console.log(isPalindrome('addaw'))
// false

Linked List 链表

在存储一大波数的时候,我们通常使用的是数组,但有时候数组显得不够灵活,比如:有一串已经从小到大排好序的数 2 3 5 8 9 10 18 26 32。现需要往这串数中插入 6 使其得到的新序列仍符合从小到大排列。

如上图所示,如果使用数组来实现的话,则需要将 8 和 8 后面的 数都依次往后挪一位,这样操作显然很耽误时间,如果使用 链表 则会快很多:

链表(Linked List)是一种常见的数据结构。它是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针。和图中一样,如果需要在 8 前面插入一个 6,直接插入就行了,而无需再将 8 及后面的数都依次往后挪一位。

那么问题来了,如何使用 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
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
// 抽象节点类
function Node(el) {
this.el = el
this.next = null
}

// 抽象链表类
class LinkedList {
constructor() {
this.head = new Node('head')
}
// 查找数据
find(item) {
let curr = this.head
while(curr.el !== item) {
curr = curr.next
}
return curr
}
// 查找前一个节点
findPre(item) {
if(item === 'head') {
throw new Error('Now is head!')
}
let curr = this.head
while(curr.next && curr.next.el !== item) {
curr = curr.next
}
return curr
}
// 在 item 后插入节点
insert(newEl, item) {
let newNode = new Node(newEl)
let curr = this.find(item)
newNode.next = curr.next
curr.next = newNode
}
// 删除节点
remove(item) {
let pre = this.findPre(item)
if(pre.next !== null) {
pre.next = pre.next.next
}
}
// 显示链表中所有元素
display() {
let curr = this.head
while(curr.next !== null) {
console.log(curr.next.el)
curr = curr.next
}
}
}

实例化:

1
2
3
4
5
6
7
8
9
10
11
12
const list = new LinkedList()
list.insert('0', 'head')
list.insert('1', '0')
list.insert('2', '1')
list.insert('3', '2')
console.log(list.findPre('1'))
// Node { el: '0', next: Node { el: '1', next: Node { el: '2', next: [Object] } } }
list.remove('1')
console.log(list)
// LinkedList { head: Node { el: 'head', next: Node { el: '0', next: [Object] } } }
console.log(list.display())
// 0 2 3

Binary tree 二叉树

二叉树(binary tree)是一种特殊的树。二叉树 的特点是每个结点最多有两个儿子,左边的叫做左儿子,右边的叫做右儿子,或者说每个结点最多有两棵子树。更加严格的递归定义是:二叉树 要么为空,要么由根结点、左子树和右子树组成,而左子树和右子树分别是一棵 二叉树。

和前两个例子一样,通过类的抽象来实现 二叉树:

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
// 抽象节点类
function Node(key) {
this.key = key
this.left = null
this.right = null
}
// 插入节点的方法
const insertNode = function(node, newNode) {
if (newNode.key < node.key) {
if (node.left === null) {
node.left = newNode
} else {
insertNode(node.left, newNode)
}
} else {
if (node.right === null) {
node.right = newNode
} else {
insertNode(node.right, newNode)
}
}
}
// 抽象二叉树
class BinaryTree {
constructor() {
this.root = null
}
insert(key) {
let newNode = new Node(key)
if(this.root === null) {
this.root = newNode
} else {
insertNode(this.root, newNode)
}
}
}

实例化测试一波:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const data = [8, 3, 10, 1, 6, 14, 4, 7, 13]

// 实例化二叉树的数据结构
const binaryTree = new BinaryTree()
// 遍历数据并插入
data.forEach(item => binaryTree.insert(item))
// 打印结果
console.log(binaryTree.root)
/*
Node {
key: 8,
left:
Node {
key: 3,
left: Node { key: 1, left: null, right: null },
right: Node { key: 6, left: [Object], right: [Object] } },
right:
Node {
key: 10,
left: null,
right: Node { key: 14, left: [Object], right: null } } }
*/

总结

算法和数据结构是非常重要的一环,之后也会慢慢总结更新,欢迎关注!

Xiao's Home

VIPKID 长期招聘前端工程师,简历可以发送到 xiaozixiang@vipkid.com.cn

10 日志
GitHub 微博 简书
© 2018 Xiao's Home