GithubHelp home page GithubHelp logo

koala-coding / goodblog Goto Github PK

View Code? Open in Web Editor NEW
850.0 32.0 134.0 14.33 MB

我是koala, 公众号【程序员成长指北】的作者, 专注Node.js技术栈分享,从前端到Node.js再到后端数据库,帮您成为优秀的Node.js全栈工程师。和我一起进阶全栈吧!

Home Page: http://www.inode.club

JavaScript 0.01% HTML 100.00%
nodejs javascript mysql sequelize

goodblog's Introduction

【程序员成长指北】全栈开发

这里是 koala 的博客, 最近在做一个node高级进阶的学习路线,

文章在线阅读体验更好,点这里

支持一下

博客技术栈目录

技术栈预览图

加入我们

参与贡献

  1. 如果您对本项目有任何建议或发现文中内容有误的,欢迎提交 issues 进行指正。
  2. 对于文中我没有涉及到知识点,欢迎提交 PR。
  3. 如果您有文章推荐请以 markdown 格式到邮箱 [email protected]中文技术文档的写作规范指南

goodblog's People

Contributors

koala-coding avatar sweetxiaoyan avatar ubbcou avatar weblixin avatar x1angli avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

goodblog's Issues

一道面试题引发的node事件循环深入思考

本文涵盖

  • 面试题的引入
  • 笔者对事件循环面试题执行顺序的一些疑问
  • 通过面试题对微任务、事件循环、定时器等对深入理解
  • 结论总计

面试题

面试题如下,大家可以先试着写一下输出结果,然后再看我下面的详细讲解,看看会不会有什么出入,如果把整个顺序弄清楚node.js的执行顺序应该就没问题了。

async function async1(){
    console.log('async1 start')
    await async2()
    console.log('async1 end')
  }
async function async2(){
    console.log('async2')
}
console.log('script start')
setTimeout(function(){
    console.log('setTimeout0') 
},0)  
setTimeout(function(){
    console.log('setTimeout3') 
},3)  
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
async1();
new Promise(function(resolve){
    console.log('promise1')
    resolve();
    console.log('promise2')
}).then(function(){
    console.log('promise3')
})
console.log('script end')

面试题正确的输出结果

script start
async1 start
async2
promise1
promise2
script end
nextTick
async1 end
promise3
setTimeout0
setImmediate
setTimeout3

提出问题

在理解node.js的异步的时候有一些不懂的地方,使用node.js的开发者一定都知道它是单线程的,异步不阻塞且高并发的一门语言,但是node.js在实现异步的时候,两个异步任务开启了,是就是谁快就谁先完成这么简单,还是说异步任务最后也会有一个先后执行顺序?对于一个单线程的的异步语言它是怎么实现高并发的呢?

好接下来我们就带着这两个问题来真正的理解node.js中的异步(微任务与事件循环)。

Node 的异步语法比浏览器更复杂,因为它可以跟内核对话,不得不搞了一个专门的库 libuv 做这件事。这个库负责各种回调函数的执行时间,异步任务最后基于事件循环机制还是要回到主线程,一个个排队执行。

详细讲解

1.本轮循环与次轮循环

异步任务可以分成两种。

  1. 追加在本轮循环的异步任务
  2. 追加在次轮循环的异步任务

所谓”循环”,指的是事件循环(event loop)。这是 JavaScript 引擎处理异步任务的方式,后文会详细解释。这里只要理解,本轮循环一定早于次轮循环执行即可。

Node 规定,process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环。

2.process.nextTick()

1)process.nextTick不要因为有next就被好多小伙伴当作次轮循环

2)Node 执行完所有同步任务,接下来就会执行process.nextTick的任务队列。

3)开发过程中如果想让异步任务尽可能快地执行,可以使用process.nextTick来完成。

3.微任务(microtack)

根据语言规格,Promise对象的回调函数,会进入异步任务里面的”微任务”(microtask)队列。

微任务队列追加在process.nextTick队列的后面,也属于本轮循环。

根据语言规格,Promise对象的回调函数,会进入异步任务里面的”微任务”(microtask)队列。

微任务队列追加在process.nextTick队列的后面,也属于本轮循环。所以,下面的代码总是先输出3,再输出4。

process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));

//输出结果3,4

process.nextTick(() => console.log(1));
Promise.resolve().then(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));

//输出结果 1,3,3,4

注意,只有前一个队列全部清空以后,才会执行下一个队列。两个队列的概念 nextTickQueue 和微队列microTaskQueue,也就是说开启异步任务也分为几种,像promise对象这种,开启之后直接进入微队列中,微队列内的就是那个任务快就那个先执行完,但是针对于队列与队列之间不同的任务,还是会有先后顺序,这个先后顺序是由队列决定的。

4.事件循环的阶段(idle, prepare忽略了这个阶段)

事件循环最阶段最详细的讲解(官网:https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#setimmediate-vs-settimeout)

  1. timers阶段

    次阶段包括setTimeout()和setInterval()

  2. IO callbacks

    大部分的回调事件,普通的caollback

  3. poll阶段

    网络连接,数据获取,读取文件等操作

  4. check阶段

    setImmediate()在这里调用回调

  5. close阶段
    一些关闭回调,例如socket.on('close', ...)

  • 事件循环注意点

1)Node 开始执行脚本时,会先进行事件循环的初始化,但是这时事件循环还没有开始,会先 完成下面的事情。

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

最后,上面这些事情都干完了,事件循环就正式开始了。

2)事件循环同样运行在单线程环境下,高并发也是依靠事件循环,每产生一个事件,就会加入到该阶段对应的队列中,此时事件循环将该队列中的事件取出,准备执行之后的callback。

3)假设事件循环现在进入了某个阶段,即使这期间有其他队列中的事件就绪,也会先将当前队列的全部回调方法执行完毕后,再进入到下一个阶段。

5.事件循环中的setTimeOut与setImmediate

由于setTimeout在 timers 阶段执行,而setImmediate在 check 阶段执行。所以,setTimeout会早于setImmediate完成。

setTimeout(() => console.log(1));
setImmediate(() => console.log(2));

上面代码应该先输出1,再输出2,但是实际执行的时候,结果却是不确定,有时还会先输出2,再输出1。

这是因为setTimeout的第二个参数默认为0。但是实际上,Node 做不到0毫秒,最少也需要1毫秒,根据官方文档,第二个参数的取值范围在1毫秒到2147483647毫秒之间。也就是说,setTimeout(f, 0)等同于setTimeout(f, 1)。

实际执行的时候,进入事件循环以后,有可能到了1毫秒,也可能还没到1毫秒,取决于系统当时的状况。如果没到1毫秒,那么 timers 阶段就会跳过,进入 check 阶段,先执行setImmediate的回调函数。

但是,下面的代码一定是先输出2,再输出1。

const fs = require('fs');
fs.readFile('test.js', () => {
 setTimeout(() => console.log(1));
 setImmediate(() => console.log(2));
});

上面代码会先进入 I/O callbacks 阶段,然后是 check 阶段,最后才是 timers 阶段。因此,setImmediate才会早于setTimeout执行。

6.同步任务中async以及promise的一些误解

  • 问题1:

在那道面试题中,在同步任务的过程中,不知道大家有没有疑问,为什么不是执行完async2输出后执行async1 end输出,而是接着执行promise1?

解答:引用阮一峰老师书中一句话:“ async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。”
简单的说,先去执行后面的同步任务代码,执行完成后,也就是表达式中的 Promise 解析完成后继续执行 async 函数并返回解决结果。(其实还是本轮循环promise的问题,最后的resolve属于异步,位于本轮循环的末尾。)

  • 问题2:

console.log('promise2')为什么也是在resolve之前执行?

解答:注:此内容来源与阮一峰老师的ES6书籍,调用resolve或者reject并不会终结promise的参数函数的执行。因为立即resolved的Promise是本轮循环的末尾执行,同时总是晚于本轮循环的同步任务。正规的写法调用resolve或者reject以后,Promise的使命就完成了,后继操作应该放在then方法后面。所以最好在它的前面加上return语句,这样就不会出现意外

new Promise((resolve,reject) => {
    return resolve(1);
    //后面的语句不会执行
    console.log(2);
}
  • 问题3:

promise3和script end的执行顺序是否有疑问?

解答:因为立即resolved的Promise是本轮循环的末尾执行,同时总是晚于本轮循环的同步任务。 Promise 是一个立即执行函数,但是他的成功(或失败:reject)的回调函数 resolve 却是一个异步执行的回调。当执行到 resolve() 时,这个任务会被放入到回调队列中,等待调用栈有空闲时事件循环再来取走它。本轮循环中最后执行的。

整体结论

顺序的整体总结就是:
同步任务-> 本轮循环->次轮循环

附件:参考资料

node.js官网:

觉得本文对你有帮助?请分享给更多人

欢迎大家关注我的公众号——程序员成长指北。请自行微信搜索——“程序员成长指北”

async和await的讲解

async和await的讲解

声明async函数的几个方法

//普通的函数声明

async function A(){}

//声明一个函数表达式

let A=async function(){}

//async形式的箭头函数

let A=async ()=>{}

初识async和await

async与await实例应用,基础代码
控制器调用与server中查询数据

exports.getBlogList =async (ctx,next)=>{
return ctx.body = await ArticleServer.getBlogListServer();
}


exports.getBlogListServer= async ()=>{
let where={
id:'1'
}
let blogList= await articleBlogDa.findAll({where:where});
console.log("哈哈哈"+blogList);
return  blogList;
}

  1. 方法执行后的返回值:await命令后面可以是Promise对象或值,如果是值,就会转到一个立即resolve的Promise对象。async函数返回的是一个Promise对象,如果结果是值,会经过Promise包装返回。

  2. await与并行:如果在一个async的方法中,有多个await操作的时候,程序会变成完全的串行操作,一个完事等另一个但是为了发挥node的异步优势,当异步操作之间不存在结果的依赖关系时,可以使用promise.all来实现并行,all中的所有方法是一同执行的。

  3. 执行后的结果:async函数中,如果有多个await关键字时,如果有一个await的状态变成了rejected,那么后面的操作都不会继续执行,promise也是同理await的返回结果就是后面promise执行的结果,可能是resolves或者rejected的值使用场景循环遍历方便了代码需要同步的操作(文件读取,数据库操作等)

async与await一些注意关键点小结

  • await关键字必须位于async函数内部
  • await关键字后面需要一个promise对象(不是的话就调用resolve转换它)
  • await关键字的返回结果就是其后面Promise执行的结果,可能是resolved或者rejected的值
  • 不能在普通箭头函数中使用await关键字,需要在箭头函数前面添加async
  • await用来串行的执行异步操作,现实现并行可以考虑promise.all

async与await缺点

async函数中,如果有多个await关键字时,如果有一个await的状态变成了rejected,那么后面的操作都不会继续执行,promise也是同理有这样一个函数async

function getData()
{
let value=await get();
value++;
await set();//set完成后返回数据
return value;
    
}

直接调用

var value=getData();

是对于这个函数直接调用的时候并不是你想要的返回值,因为async方法返回的永远是一个promise,即使开发者返回的是一个常量,也会被自动调用的promise.resolve方法转换为一个promise。因此对于这种情况,上层调用方法也需要是async函数,采用如下方法

async function xxxx(){
var value=await getData();
return value;
}

对于这种调用,如果还存在更高层次的方法调用,那么从底层的异步操作开始,一直到最顶层一个不需要返回值的函数为止,全部的方法都要变成async。

await与async大多数人的误区

这个误区在一道面试题那篇文章中详细讲解过,但是还是想提一下。
看一段代码:

async function async1(){
    console.log('async1 start')
    await async2()
    console.log('async1 end')
  }
async function async2(){
    console.log('async2')
}
async1();
console.log('i am koala')

我想会有一些开发者认为await是把同步变为异步,执行顺序是这样

async1 start
async2
async1 end
i am koala

然而并不是,正确的执行顺序是

async1 start
async2
i am koala
async1 end

解释一下原因:

“ async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。” ——阮一峰ES6

简单的说,先去执行后面的同步任务代码,执行完成后,也就是表达式中的 Promise 解析完成后继续执行 async 函数并返回解决结果。(其实还是本轮循环promise的问题,最后的resolve属于异步,位于本轮循环的末尾。)

经常被面试官问道的JavaScript数据类型知识你真的懂吗?

前言


之前面试了几个开发者,他们确实做过不少项目,能力也是不错的,但是发现js基础不扎实, 于是决定写一下这篇javascrip数据类型相关的基础文章,其实也不仅仅是因为面试了他们,之前自己在面试的时候,也曾经被虐过,面试官说过的最深刻的一句话我到现在都记得。

基础很重要,只有基础好才会很少出bug,大多数的bug都是基础不扎实造成的。

这里给出两道我们公司数据类型基础相关的面试题和答案,如果都能做对并且知道为什么(可以选择忽略本文章):

//类型转换相关问题
var bar=true;
console.log(bar+0);
console.log(bar+"xyz");
console.log(bar+true);
console.log(bar+false);
console.log('1'>bar);
console.log(1+'2'+false);
console.log('2' + ['koala',1]);

var obj1 = {
   a:1,
   b:2
}
console.log('2'+obj1)

var obj2 = {
    toString:function(){
        return 'a'
    }
}
console.log('2'+obj2)

//输出结果  1 truexyz 2 1 false 12false 2koala,1 2[object Object] 2a
//作用域和NaN 这里不具体讲作用域,意在说明NaN
var b=1;
function outer(){
    var b=2;
    function inner(){
        b++;
        console.log(b);
        var b=3;
    }
    inner();
}
outer();
//输出结果 NaN

本篇文章会以一个面试官问问题的角度来进行分析讲解

js中的数据类型

面试官:说一说javascript中有哪些数据类型?

JavaScript **有七种内置数据类型,包括基本类型对象类型

基本类型

基本类型分为以下六种:

  • string(字符串)
  • boolean(布尔值)
  • number(数字)
  • symbol(符号)
  • null(空值)
  • undefined(未定义)

注意

  1. stringnumberbooleannull undefined 这五种类型统称为原始类型(Primitive),表示不能再细分下去的基本类型;

  2. symbol是ES6中新增的数据类型,symbol 表示独一无二的值,通过 Symbol 函数调用生成,由于生成的 symbol 值为原始类型,所以 Symbol 函数不能使用 new 调用;

  3. nullundefined 通常被认为是特殊值,这两种类型的值唯一,就是其本身。

对象类型

对象类型也叫引用类型,arrayfunction是对象的子类型。对象在逻辑上是属性的无序集合,是存放各种值的容器。对象值存储的是引用地址,所以和基本类型值不可变的特性不同,对象值是可变的。

js弱类型语言

面试官:说说你对javascript是弱类型语言的理解?

JavaScript 是弱类型语言,而且JavaScript 声明变量的时候并没有预先确定的类型,变量的类型就是其值的类型,也就是说变量当前的类型由其值所决定,夸张点说上一秒种的String,下一秒可能就是个Number类型了,这个过程可能就进行了某些操作发生了强制类型转换。虽然弱类型的这种不需要预先确定类型的特性给我们带来了便利,同时也会给我们带来困扰,为了能充分利用该特性就必须掌握类型转换的原理。

js中的强制转换规则

面试官:javascript中强制类型转换是一个非常易出现bug的点,知道强制转换时候的规则吗?

注:规则最好配合下面什么时候发生转换使用这些规则看效果更佳。

ToPrimitive(转换为原始值)

ToPrimitive对原始类型不发生转换处理,只针对引用类型(object)的,其目的是将引用类型(object)转换为非对象类型,也就是原始类型。

ToPrimitive 运算符接受一个值,和一个可选的期望类型作参数ToPrimitive 运算符将值转换为非对象类型,如果对象有能力被转换为不止一种原语类型,可以使用可选的 期望类型 来暗示那个类型。

转换后的结果原始类型是由期望类型决定的,期望类型其实就是我们传递的type。直接看下面比较清楚。
ToPrimitive方法大概长这么个样子具体如下。

/**
* @obj 需要转换的对象
* @type 期望转换为的原始数据类型,可选
*/
ToPrimitive(obj,type)
type不同值的说明
  • type为string:
  1. 先调用objtoString方法,如果为原始值,则return,否则进行第2步
  2. 调用objvalueOf方法,如果为原始值,则return,否则进行第3步
  3. 抛出TypeError 异常
  • type为number:
  1. 先调用objvalueOf方法,如果为原始值,则return,否则进行第2步
  2. 调用objtoString方法,如果为原始值,则return,否则第3步
  3. 抛出TypeError 异常
  • type参数为空
  1. 该对象为Date,则type被设置为String
  2. 否则,type被设置为Number
Date数据类型特殊说明:

对于Date数据类型,我们更多期望获得的是其转为时间后的字符串,而非毫秒值(时间戳),如果为number,则会取到对应的毫秒值,显然字符串使用更多。
其他类型对象按照取值的类型操作即可。

ToPrimitive总结

ToPrimitive转成何种原始类型,取决于type,type参数可选,若指定,则按照指定类型转换,若不指定,默认根据实用情况分两种情况,Datestring,其余对象为number。那么什么时候会指定type类型呢,那就要看下面两种转换方式了。

toString

Object.prototype.toString()

toString() 方法返回一个表示该对象的字符串。

每个对象都有一个 toString() 方法,当对象被表示为文本值时或者当以期望字符串的方式引用对象时,该方法被自动调用。

这里先记住,valueOf()toString() 在特定的场合下会自行调用。

valueOf

Object.prototype.valueOf()方法返回指定对象的原始值。

JavaScript 调用 valueOf() 方法用来把对象转换成原始类型的值(数值、字符串和布尔值)。但是我们很少需要自己调用此函数,valueOf 方法一般都会被 JavaScript 自动调用。

不同内置对象的valueOf实现:

  • String => 返回字符串值
  • Number => 返回数字值
  • Date => 返回一个数字,即时间值,字符串中内容是依赖于具体实现的
  • Boolean => 返回Boolean的this值
  • Object => 返回this

对照代码会更清晰一些:

var str = new String('123');
console.log(str.valueOf());//123

var num = new Number(123);
console.log(num.valueOf());//123

var date = new Date();
console.log(date.valueOf()); //1526990889729

var bool = new Boolean('123');
console.log(bool.valueOf());//true

var obj = new Object({valueOf:()=>{
    return 1
}})
console.log(obj.valueOf());//1

Number

Number运算符转换规则:

  • null 转换为 0
  • undefined 转换为 NaN
  • true 转换为 1,false 转换为 0
  • 字符串转换时遵循数字常量规则,转换失败返回 NaN

注意:对象这里要先转换为原始值,调用ToPrimitive转换,type指定为number了,继续回到ToPrimitive进行转换。

String

String 运算符转换规则

  • null 转换为 'null'
  • undefined 转换为 undefined
  • true 转换为 'true'false 转换为 'false'
  • 数字转换遵循通用规则,极大极小的数字使用指数形式

注意:对象这里要先转换为原始值,调用ToPrimitive转换,type就指定为string了,继续回到ToPrimitive进行转换(上面有将到ToPrimitive的转换规则)。

String(null)                 // 'null'
String(undefined)            // 'undefined'
String(true)                 // 'true'
String(1)                    // '1'
String(-1)                   // '-1'
String(0)                    // '0'
String(-0)                   // '0'
String(Math.pow(1000,10))    // '1e+30'
String(Infinity)             // 'Infinity'
String(-Infinity)            // '-Infinity'
String({})                   // '[object Object]'
String([1,[2,3]])            // '1,2,3'
String(['koala',1])          //koala,1

Boolean

ToBoolean 运算符转换规则

除了下述 6 个值转换结果为 false,其他全部为 true

  1. undefined
  2. null
  3. -0
  4. 0或+0
  5. NaN
  6. ''(空字符串)

假值以外的值都是真值。其中包括所有对象(包括空对象)的转换结果都是true,甚至连false对应的布尔对象new Boolean(false)也是true

Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false

Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true

js转换规则不同场景应用

面试官问:知道了具体转换成什么的规则,但是都在什么情况下发生什么样的转换呢?

什么时候自动转换为string类型

  • 在没有对象的前提下

    字符串的自动转换,主要发生在字符串的加法运算时。当一个值为字符串,另一个值为非字符串,则后者转为字符串。

'2' + 1 // '21'
'2' + true // "2true"
'2' + false // "2false"
'2' + undefined // "2undefined"
'2' + null // "2null"
  • 当有对象且与对象+时候
//toString的对象
var obj2 = {
    toString:function(){
        return 'a'
    }
}
console.log('2'+obj2)
//输出结果2a

//常规对象
var obj1 = {
   a:1,
   b:2
}
console.log('2'+obj1)
//输出结果 2[object Object]

//几种特殊对象
'2' + {} // "2[object Object]"
'2' + [] // "2"
'2' + function (){} // "2function (){}"
'2' + ['koala',1] // 2koala,1

对下面'2'+obj2详细举例说明如下:

  1. 左边为stringToPrimitive原始值转换后不发生变化
  2. 右边转化时同样按照ToPrimitive进行原始值转换,由于指定的type是number,进行ToPrimitive转化调用obj2.valueof(),得到的不是原始值,进行第三步
  3. 调用toString() return 'a'
  4. 符号两边存在string,而且是+号运算符则都采用String规则转换为string类型进行拼接
  5. 输出结果2a

对下面'2'+obj1详细举例说明如下:

  1. 左边为stringToPrimitive转换为原始值后不发生变化
  2. 右边转化时同样按照ToPrimitive进行原始值转换,由于指定的type是number,进行ToPrimitive转化调用obj2.valueof(),得到{ a: 1, b: 2 }
  3. 调用toString() return [object Object]
  4. 符号两边存在string,而且是+号运算符则都采用String规则转换为string类型进行拼接
  5. 输出结果2[object Object]

代码中几种特殊对象的转换规则基本相同,就不一一说明,大家可以想一下流程。

注意:不管是对象还不是对象,都有一个转换为原始值的过程,也就是ToPrimitive转换,只不过原始类型转换后不发生变化,对象类型才会发生具体转换。

string类型转换开发过程中可能出错的点:

var obj = {
  width: '100'
};

obj.width + 20 // "10020"

预期输出结果120 实际输出结果10020

什么时候自动转换为Number类型

  • 有加法运算符,但是无String类型的时候,都会优先转换为Number类型

    例子:

    true + 0 // 1
    true + true // 2
    true + false //1
  • 除了加法运算符,其他运算符都会把运算自动转成数值。
    例子:

    '5' - '2' // 3
    '5' * '2' // 10
    true - 1  // 0
    false - 1 // -1
    '1' - 1   // 0
    '5' * []    // 0
    false / '5' // 0
    'abc' - 1   // NaN
    null + 1 // 1
    undefined + 1 // NaN
    
    //一元运算符(注意点)
    +'abc' // NaN
    -'abc' // NaN
    +true // 1
    -false // 0

注意:null转为数值时为0,而undefined转为数值时为NaN

判断等号也放在Number里面特殊说明

== 抽象相等比较与+运算符不同,不再是String优先,而是Number优先。
下面列举x == y的例子

  1. 如果x,y均为number,直接比较
    没什么可解释的了
1 == 2 //false
  1. 如果存在对象,ToPrimitive()type为number进行转换,再进行后面比较
var obj1 = {
    valueOf:function(){
        return '1'
    }
}
1 == obj1  //true
//obj1转为原始值,调用obj1.valueOf()
//返回原始值'1'
//'1'toNumber得到 1 然后比较 1 == 1
[] == ![] //true
//[]作为对象ToPrimitive得到 ''  
//![]作为boolean转换得到0 
//'' == 0 
//转换为 0==0 //true
  1. 存在boolean,按照ToNumberboolean转换为1或者0,再进行后面比较
//boolean 先转成number,按照上面的规则得到1  
//3 == 1 false
//0 == 0 true
3 == true // false
'0' == false //true

4.如果xstringynumberx转成number进行比较

//'0' toNumber()得到 0  
//0 == 0 true
'0' == 0 //true

什么时候进行布尔转换

  • 布尔比较时
  • if(obj) , while(obj) 等判断时或者 三元运算符只能够包含布尔值

条件部分的每个值都相当于false,使用否定运算符后,就变成了true

if ( !undefined
  && !null
  && !0
  && !NaN
  && !''
) {
  console.log('true');
} // true

//下面两种情况也会转成布尔类型
expression ? true : false
!! expression

js中的数据类型判断

面试官问:如何判断数据类型?怎么判断一个值到底是数组类型还是对象?

三种方式,分别为 typeofinstanceof Object.prototype.toString()

typeof

通过 typeof操作符来判断一个值属于哪种基本类型。

typeof 'seymoe'    // 'string'
typeof true        // 'boolean'
typeof 10          // 'number'
typeof Symbol()    // 'symbol'
typeof null        // 'object' 无法判定是否为 null
typeof undefined   // 'undefined'

typeof {}           // 'object'
typeof []           // 'object'
typeof(() => {})    // 'function'

上面代码的输出结果可以看出,

  1. null 的判定有误差,得到的结果
    如果使用 typeofnull得到的结果是object

  2. 操作符对对象类型及其子类型,例如函数(可调用对象)、数组(有序索引对象)等进行判定,则除了函数都会得到 object 的结果。

综上可以看出typeOf对于判断类型还有一些不足,在对象的子类型和null情况下。

instanceof

通过 instanceof 操作符也可以对对象类型进行判定,其原理就是测试构造函数的 prototype 是否出现在被检测对象的原型链上。

[] instanceof Array            // true
({}) instanceof Object         // true
(()=>{}) instanceof Function   // true

复制代码注意:instanceof 也不是万能的。
举个例子:

let arr = []
let obj = {}
arr instanceof Array    // true
arr instanceof Object   // true
obj instanceof Object   // true

在这个例子中,arr 数组相当于 new Array() 出的一个实例,所以 arr.__proto__ === Array.prototype,又因为 Array 属于 Object 子类型,即 Array.prototype.__proto__ === Object.prototype,因此 Object 构造函数在 arr 的原型链上。所以 instanceof 仍然无法优雅的判断一个值到底属于数组还是普通对象。

还有一点需要说明下,有些开发者会说 Object.prototype.__proto__ === null,岂不是说 arr instanceof null 也应该为 true,这个语句其实会报错提示右侧参数应该为对象,这也印证 typeof null 的结果为 object 真的只是javascript中的一个 bug

Object.prototype.toString() 可以说是判定 JavaScript 中数据类型的终极解决方法了,具体用法请看以下代码:

Object.prototype.toString.call({})              // '[object Object]'
Object.prototype.toString.call([])              // '[object Array]'
Object.prototype.toString.call(() => {})        // '[object Function]'
Object.prototype.toString.call('seymoe')        // '[object String]'
Object.prototype.toString.call(1)               // '[object Number]'
Object.prototype.toString.call(true)            // '[object Boolean]'
Object.prototype.toString.call(Symbol())        // '[object Symbol]'
Object.prototype.toString.call(null)            // '[object Null]'
Object.prototype.toString.call(undefined)       // '[object Undefined]'

Object.prototype.toString.call(new Date())      // '[object Date]'
Object.prototype.toString.call(Math)            // '[object Math]'
Object.prototype.toString.call(new Set())       // '[object Set]'
Object.prototype.toString.call(new WeakSet())   // '[object WeakSet]'
Object.prototype.toString.call(new Map())       // '[object Map]'
Object.prototype.toString.call(new WeakMap())   // '[object WeakMap]'

我们可以发现该方法在传入任何类型的值都能返回对应准确的对象类型。用法虽简单明了,但其中有几个点需要理解清楚:

  • 该方法本质就是依托Object.prototype.toString() 方法得到对象内部属性 [[Class]]
  • 传入原始类型却能够判定出结果是因为对值进行了包装
  • nullundefined 能够输出结果是内部实现有做处理

NaN相关总结

NaN的概念

NaN 是一个全局对象的属性,NaN 是一个全局对象的属性,NaN是一种特殊的Number类型。

什么时候返回NaN (开篇第二道题也得到解决)

  • 无穷大除以无穷大
  • 给任意负数做开方运算
  • 算数运算符与不是数字或无法转换为数字的操作数一起使用
  • 字符串解析成数字

一些例子:

Infinity / Infinity;   // 无穷大除以无穷大
Math.sqrt(-1);         // 给任意负数做开方运算
'a' - 1;               // 算数运算符与不是数字或无法转换为数字的操作数一起使用
'a' * 1;
'a' / 1;
parseInt('a');         // 字符串解析成数字
parseFloat('a');

Number('a');   //NaN
'abc' - 1   // NaN
undefined + 1 // NaN
//一元运算符(注意点)
+'abc' // NaN
-'abc' // NaN

误区

toStringString的区别

  • toString
  1. toString()可以将数据都转为字符串,但是nullundefined不可以转换。

    console.log(null.toString())
    //报错 TypeError: Cannot read property 'toString' of null
    
    console.log(undefined.toString())
    //报错 TypeError: Cannot read property 'toString' of undefined
  2. toString()括号中可以写数字,代表进制

    二进制:.toString(2);

    八进制:.toString(8);

    十进制:.toString(10);

    十六进制:.toString(16);

  • String
  1. String()可以将nullundefined转换为字符串,但是没法转进制字符串

    console.log(String(null));
    // null
    console.log(String(undefined));
    // undefined

深浅拷贝这一章

结论:在栈内存中的数据发生数据变化的时候,系统会自动为新的变量分配一个新的之值在栈内存中,两个变量相互独立,互不影响的。
大概新的之值新的子值吧。

ES6中类与继承的理解(java对比记忆)

前言

先上两段代码:
java中定义类:

public class Person{
    private String name;
    private int age;
   
    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }
    public void getInfo(){ 
        System.out.println(name+age);
    }
}

Es6中定义一个类:

class Person{
    constructor(name,age){
        this.name=name;
        this.age=age;
    }
    getInfo(){
        return this.name+','+this.age;
    }
}
//调用
let person=new Person("koala","123");

通过上面两段代码引出我们今天要说的相关内容

类中的变量

  • 二者异
    在java中可以直接声明各种类型的私有变量,在ES6中的类不可以直接在类中声明私有变量,声明后会报错。
    注意:但是随着v8的更新,在node12版本中,ES增加了一些新规范,其中就有支持类的私有变量这一条。
    代码如下:
class Greet {
  #name = 'World';
  get name() {
    return this.#name;
  }
  set name(name) {
    this.#name = name;
  }
  sayHello() {
    console.log(`Hello, ${this.#name}`);
  }
}

在类的外部或去#name变量会抛出异常

const greet = new Greet()
greet.#name = 'NewName';
// -> SyntaxError
console.log(greet.#name)
// -> SyntaxError

类中的构造函数

  • 二者同:

如果声明一个一个类的时候没有声明构造函数,那么会默认添加一个空的构造函数,构造函数在new实例化一个对象的时候会被调用

  • 二者异:

在ES6中,可以在构造函数中直接定义类方法(类方法也可以是箭头函数),代码如下

constructor(name,age){
          this.name=name;
          this.age=age;
          this.getInfo()=()=>{
              console.log("name"+this.name+"sex"+this.sex);
             }         
 } 

类中的方法

  • 二者同:

有参,无参函数,函数调用方式相同。静态方法,ES6中用static声明一个静态方法,方法只能用类名直接调用,不能通过类的实例调用

  • 二者异:

ES6在类中声明函数,无需使用function关键字,java的类中必须使用关键字声明函数。

ES6方法内部访问类属性的时候需要this来访问,java不需要。

ES6的构造函数中可以定义函数,java不可。

类中的继承

  • 二者同:

继承关键字都是extends,super方法的使用

  • 二者异:

继承的调用:

ES6需要注意的是super只能调用父类方法,而不能调用父类的属性,方法定义再原型链中,属性定义在类的内部

java中,super关键字,可以通过super关键字来实现对父类成员的访问,用来引用当前对象的父类。

继承过程中的构造函数:

ES6中,子类中,super方法是必须调用的,因为子类本身没有自身的this对象,需要通过super方法拿到父类的this对象。在子类中,没有构造函数,那么在默认的构造方法内部自动调用super方法,继承父类的全部属性,子类的构造方法中,必须先调用super方法,然后才能调用this关键字声明其它属性。(子类的this就是在这里调用super之后,拿到父类的this,然后修改这个this来的)

class Student extends Person{
    constructor(name,sex){
        console.log(this);//Error
        super(name,sex);
        this.sex=sex;
    }
}

java中,子类是不继承父类的构造器(构造方法或者构造函数)的,它只是调用(隐式或显式)。如果父类的构造器带有参数,则必须在子类的构造器中显式地通过 super 关键字调用父类的构造器并配以适当的参数列表。

如果父类构造器没有参数,则在子类的构造器中不需要使用 super **关键字调用父类构造器,系统会自动调用父类的无参构造器。 **

看一段面试问的比较多的代码实例:

class SuperClass {
  private int n;
  SuperClass(){
    System.out.println("SuperClass()");
  }
  SuperClass(int n) {
    System.out.println("SuperClass(int n)");
    this.n = n;
  }
}
class SubClass extends SuperClass{
  private int n;
  
  SubClass(){
    super(300);
    System.out.println("SubClass");
  }  
  
  public SubClass(int n){
    System.out.println("SubClass(int n):"+n);
    this.n = n;
  }
}
public class TestSuperSub{
  public static void main (String args[]){
    SubClass sc = new SubClass();
    SubClass sc2 = new SubClass(200); 
  }
}

输出结果:

SuperClass(int n)
SubClass
SuperClass()
SubClass(int n):200

ES6中的类与原型链的关系

看一下文初定义的一个的javascript类。它和原型链对等的代码如下:

//类
class Person{
    constructor(name,age){
        this.name=name;
        this.age=age;
    }
    getInfo(){
        return this.name+','+this.age;
    }
}

//原型链
function Person(name, age) {
    this.name = name;
    this.age = age;
}
  
Person.prototype.getInfo = function () {
    return this.name+','+this.age;
};
console.log(Person)
let person = new Person("koala", 123);

针对代码进行一下说明讲解:

  • 声明的新对象都不具备原型链(类)的函数,但是却可以调用原型链的函数
Object.getOwnPropertyNames(p)//[ 'name', 'age' ] 从输出结果可以看出只有这两个属性,不具有getInfo函数
console.log(p.getInfo());//输出结果 kaola,123

不管是在原型链还是类中,获取的结果都是['name','age']

  • 直接打印class,发现类实际是个函数,就是对应的构造函数,this关键字代表实例对象,简单的说是class就是构造函数,prototype对象的constructor属性,直接指向“类”的本身。
//直接打印类
console.log(Person);
console.log(Point===Point.prototype.constructor);//true
  • 与ES5一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。
 person.hasOwnProperty('name') // true
 person.hasOwnProperty('age') // true
 person.hasOwnProperty('getInfo') // false
 person.__proto__.hasOwnProperty('getInfo') // true getInfo是原型对象的属性
  • 类的所有实例共享一个原型对象
let person1 = new Person("koala1", 124);
console.log(person.__proto__===person1.__proto__);//true
  • 类的实例 构造方法默认返回实例对象(this) 但是返回this可以修改

ES6中类的出现有什么好处

  • js中的类仍然是基于原型的。即使在创建类后修改类的构造函数上的原型对象仍然不会有任何问题。
    例子代码如下:
class Foo {
    constructor(name) {
        this.name = name;
    }

    test1() {
        console.log("test1: name = " + this.name);
    }
}
Foo.prototype.test2 = function() {
    console.log("test2: name = " + this.name);
};
  • 类的语法简单,不易出错,尤其是在继承层次结构上简单很多。

  • 类保护你免受无法使用构造函数使用新的常见错误(通过让构造函数抛出异常,如果这不是构造函数的有效对象)。

代码例子如下:
ES6中类出现后实现继承:

// ES6
class Person {
    constructor(first, last) {
        this.first = first;
        this.last = last;
    }

    personMethod() {
        // ...
    }
}

class Employee extends Person {
    constructor(first, last, position) {
        super(first, last);
        this.position = position;
    }

    employeeMethod() {
        // ...
    }
}

class Manager extends Employee {
    constructor(first, last, position, department) {
        super(first, last, position);
        this.department = department;
    }

    managerMethod() {
        // ...
    }
}

ES6中类未出现前实现继承:

// ES5
var Person = function(first, last) {
    if (!(this instanceof Person)) {
        throw new Error("Person is a constructor function, use new with it");
    }
    this.first = first;
    this.last = last;
};

Person.prototype.personMethod = function() {
    // ...
};

var Employee = function(first, last, position) {
    if (!(this instanceof Employee)) {
        throw new Error("Employee is a constructor function, use new with it");
    }
    Person.call(this, first, last);
    this.position = position;
};
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
Employee.prototype.employeeMethod = function() {
    // ...
};

var Manager = function(first, last, position, department) {
    if (!(this instanceof Manager)) {
        throw new Error("Manager is a constructor function, use new with it");
    }
    Employee.call(this, first, last, position);
    this.department = department;
};
Manager.prototype = Object.create(Employee.prototype);
Manager.prototype.constructor = Manager;
Manager.prototype.managerMethod = function() {
    // ...
};

附录:

java中的继承 http://www.runoob.com/java/java-inheritance.html

(sequelize)bulkCreate函数中的updateOnDuplicate参数怎么使用

(sequelize)bulkCreate函数中的updateOnDuplicate参数怎么使用?

前言:

Sequelize中提供的增删改查相关的函数都能转成原生的sql语句。本篇文章介绍一个不是很常见但是有时候批量插入很方便的函数——bulkCreate。

bulkCreate讲解

官方文档理解

bulkCreate() - 创建多条记录

bulkCreate(records, [options]) -> Promise.<Array.<Instance>>

批量创建并保存多个实例。

处理成功后,会在回调函数中返回一个包含多个实例的数组。

参数

名称 类型 说明
records Array 要创建实例的对象(键/值 对)列表
[options] Object
[options.fields] Array 要插入的字段。默认全部
[options.validate=true] Boolean 插入每条记录前进行验证
[options.hooks=true] Boolean 在执行前/后创建钩子
[options.individualHooks=false] Boolean 在执行前/后为每个实例创建钩子
[options.ignoreDuplicates=false] Boolean 忽略重复主键(Postgres不支持)
[options.updateOnDuplicate] Array 如果行键已存在是否更新(mysql & mariadb支持). 默认为更新
[options.transaction] Transaction 在事务中执行查询

注意:options.updateOnDuplicate参数中的行键已存在我的理解是:数据库表中现有的记录的唯一索引或者主键如果已经存在,执行更新操作

对应原生sql语句讲解

在MySQL数据库中,如果在insert语句后面带上ON DUPLICATE KEY UPDATE 子句。

  • 要插入的行与表中现有记录的惟一索引或主键中产生重复值,那么就会发生旧行的更新;
  • 如果插入的行数据与现有表中记录的唯一索引或者主键不重复,则执行新纪录插入操作。

项目中例子

看下面的代码:

//对应的node.js代码
db.usernotice.bulkCreate(userNoticeRecord,{validate: true, updateOnDuplicate: ["user_id", "notice_id", "update_time"]});

//生成对应的原生sql语句代码
INSERT INTO `usernotice` (`id`,`user_id`,`notice_id`,`is_read`,`is_zan`,`create_time`) VALUES (NULL,'m_******','345345',false,false,'2019-05-27 17:25:07') ON DUPLICATE KEY UPDATE `user_id`=VALUES(`user_id`),`notice_id`=VALUES(`notice_id`),`update_time`=VALUES(`update_time`);

SQL 和 NoSQL 的区别与选择(理论篇)

概念

关系型数据库

SQL (Structured Query Language) 数据库,指关系型数据库 - 主要代表:SQL Server,Oracle,MySQL(开源),PostgreSQL(开源)。

非关系型数据库

NoSQL(Not Only SQL)泛指非关系型数据库 - 主要代表:MongoDB,Redis,CouchDB。

二者对比

存储方式:

SQL数据存在特定结构的表中;而 NoSQL 则更加灵活和可扩展,存储方式可以省是JSON文档、哈希表或者其他方式。

SQL通常以数据库表形式存储数据。举个栗子,存个学生借书数据:


数据表
而NoSQL存储方式比较灵活,比如使用类JSON文件存储上表中熊大的借阅数据:

表/数据集合的数据的关系

在 SQL 中,必须定义好表和字段结构后才能添加数据,例如定义表的主键(primary key)索引(index),触发器(trigger),存储过程(stored procedure)等。表结构可以在被定义之后更新,但是如果有比较大的结构变更的话就会变得比较复杂。

NoSQL中,数据可以在任何时候任何地方添加,不需要先定义表。例如下面这段代码会自动创建一个新的"借阅表"数据集合


自动创建借阅表
NoSQL 也可以在数据集中建立索引。以 MongoDB 为例,会自动在数据集合创建后创建唯一值_id字段,这样的话就可以在数据集创建后增加索引。

从这点来看,NoSQL 可能更加适合初始化数据还不明确或者未定的项目中。

外部数据存储

SQL 中如何需要增加外部关联数据的话,规范化做法是在原表中增加一个外键,关联外部数据表。例如需要在借阅表中增加审核人信息,先建立一个审核人表


审核人表
再在原来的借阅人表中增加审核人外键


借阅人表
这样如果我们需要更新审核人个人信息的时候只需要更新审核人表而不需要对借阅人表做更新。

而在NoSQL中除了这种规范化的外部数据表做法以外,我们还能用如下的非规范化方式把外部数据直接放到原数据集中,以提高查询效率。缺点也比较明显,更新审核人数据的时候将会比较麻烦。

SQL中的JOIN查询

SQL 中可以使用 JOIN 表链接方式将多个关系数据表中的数据用一条简单的查询语句查询出来。

而 NoSQL 暂未提供类似 JOIN 的查询方式对多个数据集中的数据做查询。所以大部分 NoSQL 使用非规范化的数据存储方式存储数据。

数据耦合性

SQL 中不允许删除已经被使用的外部数据,例如审核人表中的"熊三"已经被分配给了借阅人熊大,那么在审核人表中将不允许删除熊三这条数据,以保证数据完整性。

而 NoSQL 中则没有这种强耦合的概念,可以随时删除任何数据。

事务

事务是 SQL 的一个明显优点,SQL 中如果多张表数据需要同批次被更新,即如果其中一张表更新失败的话其他表也不能更新成功。这种场景可以通过事务来控制,可以在所有命令完成后再统一提交事务。

在 NoSQL 数据库中,对于一个文档的更新操作是原子性的。换句话说,如果你要更新一个文档中的三个值,要么三个值都更新成功要么它们保持不变。然而,对于操作多个文档时没有事务相对应的操作。在 MongoDB 中有一个操作是 transaction-like options ,但是,需要我们手动的加入到代码中。

增删改查语法

  • SQL 是一种声明性语言。SQL 语言的功能强大,并且已经成为了一种国际的通用标准,尽管大多数系统在语法上有一些细微的差别。

  • NoSQL 数据库使用类似 JOSN 为参数的 JavaScript 来进行查询!基本操作是相同的,但是嵌套的 JOSN 将会产生复杂的查询。

数据完整性

关系型的数据库允许通过定义外键来进行数据库的完整性约束。两个表通过外健约束后,开发者或者用户不能添加、修改和移除一条表的记录,如果这些操作导致数据产生无效的数据或者单条无用记录(也就是常说的脏数据)。

在 NoSQL 数据库中没有数据完整性的约束选项。你可以存储任何你想要存储的数据。理想情况下,单个文档将是项目的所有信息的唯一来源。

查询性能

通常情况下,NoSQL 比 SQL 语言更快。这并没有什么好震惊的,NoSQL 中更加简单的非规范化存储允许我们在一次查询中得到特定项的所有信息。不需要使用 SQL 中复杂的 JOIN 操作。

也就是说,你的项目的设计和数据的需求会有很大的影响。一个好的SQL数据库的设计的表现一定会比一个设计不好的 NoSQL 数据库性能好很多,反之亦然。

如何选择

适合使用SQL开发的项目:

  • 可以预先定义逻辑相关的离散数据的需求
  • 数据一致性与完整性是必要的
  • 具有良好的开发者经验和技术支持的标准的成熟技术

适合使用NoSQL开发的项目:

  • 不相关,不确定和逐步发展的数据需求
  • 更简单或者更宽松的能够快速开始编程的项目
  • 速度和可扩展性至关重要的,需要对大数据库有性能要求
  • 需要使用 CouchDB但因为数据改变太频繁而占满内存

SQL是精确的。它最适合于具有精确标准的定义明确的项目。典型的使用场景是在线商店和银行系统。

NoSQL是多变的。它最适合于具有不确定需求的数据。典型的使用场景是社交网络,客户管理和网络分析系统。

读完本文后的思考

两个项目,大家看下分别适合哪种类型数据库?为什么这么选择?在评论区讨论

  1. 一款儿童服装电商类平台
  2. 一款社交网络平台,一些私信,点赞留言功能未确定,可能会随时删减少

node事件循环 > 3.微任务(microtack)

http://www.inode.club/#/node/eventLoop
输出结果 展示错了吧

/输出结果3,4

process.nextTick(() => console.log(1));
Promise.resolve().then(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
//输出结果 1,3,3,4

注意,只有前一个队列全部清空以后,才会执行下一个队列。两个队列的概念 nextTickQueue 和微队列microTaskQueue,也就是说开启异步任务也分为几种,像promise对象这种,开启之后直接进入微队列中,微队列内的就是那个任务快就那个先执行完,但是针对于队列与队列之间不同的任务,还是会有先后顺序,这个先后顺序是由队列决定的。

5道常见手写javascript已有api的面试题

前言

有些内容虽然不用,但是不代表面试不考。这里为大家送上5道常见的手写面试题,希望你们在面试中能遇到某一道也好,这样这篇文章就没有白写。

模仿实现new创建对象

/**
 * 模仿 new
 * @return {} 
 */
function createNew() {
  let obj = {};
  let context = [].shift.call(arguments); // 获取构造函数
  obj.__proto__ = context.prototype;
  context.apply(obj, [...arguments]);
  return obj;
}

// @test
function Person(name) {
  this.name = name;
}
let p = createNew(Person, 'kaola');

模仿实现instanceof

/**
 * 模仿实现 instanceof
 * @param   left  [左侧参数为一个实例对象]
 * @param   right [右侧为要判断的构造器函数]
 * @return  [true / false]
 */
function instanceOf (left, right) {
  let prototype = right.prototype; // 获取目标原型对象

  left = left.__proto__;

  while (true) {
    if(left == null) {
      return false;
    } else if (left == prototype) {
      return true;
    }
    left = left.__proto__
  }
}

模仿实现call

/**
 * 模仿call
 * @param  context [要绑定的this对象]
 * @return         [返回函数执行结果]
 */
Function.prototype.call_new = function(context) {
  let context = context || window;
  context.fn = this;

  let args = [...arguments].clice(1);
  let result = context.fn(...args);
  delete context.fn;
  return result;
}


// @test

 foo.call_new(obj, 1,2,3)

模仿实现apply

/**
 * 模仿apply
 * @param   context [要绑定的this对象]
 * @return  [执行结果]
 */
Function.prototype.apply_new = function(context) {
  let context = context || window;

  context.fn = this;

  let args = [...arguments][1];
  let result;
  if(args) {
    result = context.fn(...args);
  } else {
    result = context.fn()
  }

  delete context.fn;
  return result;

}

模仿实现bind

/**
 * 模仿 bind
 * @param   context [要绑定的this对象]
 * @return  [执行结果]
 */
Function.ptototype.bind_new(context) {
  let self = this;
  let args = [...arguments].slice(1);
  return function() {
    let args1 = [...arguments].slice(1);
    return self.apply(context, args.concat(args1));
  }
}

vue中8种组件通信方式, 值得收藏!

之前写了一篇关于vue面试总结的文章, 有不少网友提出组件之间通信方式还有很多, 这篇文章便是专门总结组件之间通信的

vue是数据驱动视图更新的框架, 所以对于vue来说组件间的数据通信非常重要,那么组件之间如何进行数据通信的呢?
首先我们需要知道在vue中组件之间存在什么样的关系, 才更容易理解他们的通信方式, 就好像过年回家,坐着一屋子的陌生人,相互之间怎么称呼,这时就需要先知道自己和他们之间是什么样的关系。
vue组件中关系说明:
image

如上图所示, A与B、A与C、B与D、C与E组件之间是父子关系; B与C之间是兄弟关系;A与D、A与E之间是隔代关系; D与E是堂兄关系(非直系亲属)
针对以上关系我们归类为:

  • 父子组件之间通信
  • 非父子组件之间通信(兄弟组件、隔代关系组件等)

本文会介绍组件间通信的8种方式如下图目录所示:并介绍在不同的场景下如何选择有效方式实现的组件间通信方式,希望可以帮助小伙伴们更好理解组件间的通信。

一、props / $emit

父组件通过props的方式向子组件传递数据,而通过$emit 子组件可以向父组件通信。

1. 父组件向子组件传值

下面通过一个例子说明父组件如何向子组件传递数据:在子组件article.vue中如何获取父组件section.vue中的数据articles:['红楼梦', '西游记','三国演义']

// section父组件
<template>
  <div class="section">
    <com-article :articles="articleList"></com-article>
  </div>
</template>

<script>
import comArticle from './test/article.vue'
export default {
  name: 'HelloWorld',
  components: { comArticle },
  data() {
    return {
      articleList: ['红楼梦', '西游记', '三国演义']
    }
  }
}
</script>
// 子组件 article.vue
<template>
  <div>
    <span v-for="(item, index) in articles" :key="index">{{item}}</span>
  </div>
</template>

<script>
export default {
  props: ['articles']
}
</script>

总结: prop 只可以从上一级组件传递到下一级组件(父子组件),即所谓的单向数据流。而且 prop 只读,不可被修改,所有修改都会失效并警告。

2. 子组件向父组件传值

对于$emit 我自己的理解是这样的: $emit绑定一个自定义事件, 当这个语句被执行时, 就会将参数arg传递给父组件,父组件通过v-on监听并接收参数。 通过一个例子,说明子组件如何向父组件传递数据。
在上个例子的基础上, 点击页面渲染出来的ariticleitem, 父组件中显示在数组中的下标

// 父组件中
<template>
  <div class="section">
    <com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article>
    <p>{{currentIndex}}</p>
  </div>
</template>

<script>
import comArticle from './test/article.vue'
export default {
  name: 'HelloWorld',
  components: { comArticle },
  data() {
    return {
      currentIndex: -1,
      articleList: ['红楼梦', '西游记', '三国演义']
    }
  },
  methods: {
    onEmitIndex(idx) {
      this.currentIndex = idx
    }
  }
}
</script>
<template>
  <div>
    <div v-for="(item, index) in articles" :key="index" @click="emitIndex(index)">{{item}}</div>
  </div>
</template>

<script>
export default {
  props: ['articles'],
  methods: {
    emitIndex(index) {
      this.$emit('onEmitIndex', index)
    }
  }
}
</script>

二、 $children / $parent

image
上面这张图片是vue官方的解释,通过$parent$children就可以访问组件的实例,拿到实例代表什么?代表可以访问此组件的所有方法和data。接下来就是怎么实现拿到指定组件的实例。

使用方法

// 父组件中
<template>
  <div class="hello_world">
    <div>{{msg}}</div>
    <com-a></com-a>
    <button @click="changeA">点击改变子组件值</button>
  </div>
</template>

<script>
import ComA from './test/comA.vue'
export default {
  name: 'HelloWorld',
  components: { ComA },
  data() {
    return {
      msg: 'Welcome'
    }
  },

  methods: {
    changeA() {
      // 获取到子组件A
      this.$children[0].messageA = 'this is new value'
    }
  }
}
</script>
// 子组件中
<template>
  <div class="com_a">
    <span>{{messageA}}</span>
    <p>获取父组件的值为:  {{parentVal}}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      messageA: 'this is old'
    }
  },
  computed:{
    parentVal(){
      return this.$parent.msg;
    }
  }
}
</script>

要注意边界情况,如在#app上拿$parent得到的是new Vue()的实例,在这实例上再拿$parent得到的是undefined,而在最底层的子组件拿$children是个空数组。也要注意得到$parent$children的值不一样,$children 的值是数组,而$parent是个对象

总结

上面两种方式用于父子组件之间的通信, 而使用props进行父子组件通信更加普遍; 二者皆不能用于非父子组件之间的通信。

三、provide/ inject

概念:

provide/ injectvue2.2.0新增的api, 简单来说就是父组件中通过provide来提供变量, 然后再子组件中通过inject来注入变量。

注意: 这里不论子组件嵌套有多深, 只要调用了inject 那么就可以注入provide中的数据,而不局限于只能从当前父组件的props属性中回去数据

举例验证

接下来就用一个例子来验证上面的描述:
假设有三个组件: A.vue、B.vue、C.vue 其中 C是B的子组件,B是A的子组件

// A.vue

<template>
  <div>
	<comB></comB>
  </div>
</template>

<script>
  import comB from '../components/test/comB.vue'
  export default {
    name: "A",
    provide: {
      for: "demo"
    },
    components:{
      comB
    }
  }
</script>
// B.vue

<template>
  <div>
    {{demo}}
    <comC></comC>
  </div>
</template>

<script>
  import comC from '../components/test/comC.vue'
  export default {
    name: "B",
    inject: ['for'],
    data() {
      return {
        demo: this.for
      }
    },
    components: {
      comC
    }
  }
</script>
// C.vue
<template>
  <div>
    {{demo}}
  </div>
</template>

<script>
  export default {
    name: "C",
    inject: ['for'],
    data() {
      return {
        demo: this.for
      }
    }
  }
</script>

四、ref / refs

ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例,可以通过实例直接调用组件的方法或访问数据, 我们看一个ref 来访问组件的例子:

// 子组件 A.vue

export default {
  data () {
    return {
      name: 'Vue.js'
    }
  },
  methods: {
    sayHello () {
      console.log('hello')
    }
  }
}
// 父组件 app.vue

<template>
  <component-a ref="comA"></component-a>
</template>
<script>
  export default {
    mounted () {
      const comA = this.$refs.comA;
      console.log(comA.name);  // Vue.js
      comA.sayHello();  // hello
    }
  }
</script>

五、eventBus

eventBus 又称为事件总线,在vue中可以使用它来作为沟通桥梁的概念, 就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件, 所以组件都可以通知其他组件。

eventBus也有不方便之处, 当项目较大,就容易造成难以维护的灾难

在Vue的项目中怎么使用eventBus来实现组件之间的数据通信呢?具体通过下面几个步骤

1. 初始化

首先需要创建一个事件总线并将其导出, 以便其他模块可以使用或者监听它.

// event-bus.js

import Vue from 'vue'
export const EventBus = new Vue()

2. 发送事件

假设你有两个组件: additionNumshowNum, 这两个组件可以是兄弟组件也可以是父子组件;这里我们以兄弟组件为例:

<template>
  <div>
    <show-num-com></show-num-com>
    <addition-num-com></addition-num-com>
  </div>
</template>

<script>
import showNumCom from './showNum.vue'
import additionNumCom from './additionNum.vue'
export default {
  components: { showNumCom, additionNumCom }
}
</script>
// addtionNum.vue 中发送事件

<template>
  <div>
    <button @click="additionHandle">+加法器</button>    
  </div>
</template>

<script>
import {EventBus} from './event-bus.js'
console.log(EventBus)
export default {
  data(){
    return{
      num:1
    }
  },

  methods:{
    additionHandle(){
      EventBus.$emit('addition', {
        num:this.num++
      })
    }
  }
}
</script>

3. 接收事件

// showNum.vue 中接收事件

<template>
  <div>计算和: {{count}}</div>
</template>

<script>
import { EventBus } from './event-bus.js'
export default {
  data() {
    return {
      count: 0
    }
  },

  mounted() {
    EventBus.$on('addition', param => {
      this.count = this.count + param.num;
    })
  }
}
</script>

这样就实现了在组件addtionNum.vue中点击相加按钮, 在showNum.vue中利用传递来的 num 展示求和的结果.

4. 移除事件监听者

如果想移除事件的监听, 可以像下面这样操作:

import { eventBus } from 'event-bus.js'
EventBus.$off('addition', {})

六、Vuex

1. Vuex介绍

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化.
Vuex 解决了多个视图依赖于同一状态来自不同视图的行为需要变更同一状态的问题,将开发者的精力聚焦于数据的更新而不是数据在组件之间的传递上

2. Vuex各个模块

  1. state:用于数据的存储,是store中的唯一数据源
  2. getters:如vue中的计算属性一样,基于state数据的二次包装,常用于数据的筛选和多个数据的相关性计算
  3. mutations:类似函数,改变state数据的唯一途径,且不能用于处理异步事件
  4. actions:类似于mutation,用于提交mutation来改变状态,而不直接变更状态,可以包含任意异步操作
  5. modules:类似于命名空间,用于项目中将各个模块的状态分开定义和操作,便于维护

3. Vuex实例应用

// 父组件

<template>
  <div id="app">
    <ChildA/>
    <ChildB/>
  </div>
</template>

<script>
  import ChildA from './components/ChildA' // 导入A组件
  import ChildB from './components/ChildB' // 导入B组件

  export default {
    name: 'App',
    components: {ChildA, ChildB} // 注册A、B组件
  }
</script>
// 子组件childA

<template>
  <div id="childA">
    <h1>我是A组件</h1>
    <button @click="transform">点我让B组件接收到数据</button>
    <p>因为你点了B,所以我的信息发生了变化:{{BMessage}}</p>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        AMessage: 'Hello,B组件,我是A组件'
      }
    },
    computed: {
      BMessage() {
        // 这里存储从store里获取的B组件的数据
        return this.$store.state.BMsg
      }
    },
    methods: {
      transform() {
        // 触发receiveAMsg,将A组件的数据存放到store里去
        this.$store.commit('receiveAMsg', {
          AMsg: this.AMessage
        })
      }
    }
  }
</script>
// 子组件 childB

<template>
  <div id="childB">
    <h1>我是B组件</h1>
    <button @click="transform">点我让A组件接收到数据</button>
    <p>因为你点了A,所以我的信息发生了变化:{{AMessage}}</p>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        BMessage: 'Hello,A组件,我是B组件'
      }
    },
    computed: {
      AMessage() {
        // 这里存储从store里获取的A组件的数据
        return this.$store.state.AMsg
      }
    },
    methods: {
      transform() {
        // 触发receiveBMsg,将B组件的数据存放到store里去
        this.$store.commit('receiveBMsg', {
          BMsg: this.BMessage
        })
      }
    }
  }
</script>

vuex的store,js

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
  // 初始化A和B组件的数据,等待获取
  AMsg: '',
  BMsg: ''
}

const mutations = {
  receiveAMsg(state, payload) {
    // 将A组件的数据存放于state
    state.AMsg = payload.AMsg
  },
  receiveBMsg(state, payload) {
    // 将B组件的数据存放于state
    state.BMsg = payload.BMsg
  }
}

export default new Vuex.Store({
  state,
  mutations
})

七、 localStorage / sessionStorage

这种通信比较简单,缺点是数据和状态比较混乱,不太容易维护。
通过window.localStorage.getItem(key) 获取数据
通过window.localStorage.setItem(key,value) 存储数据

注意用JSON.parse() / JSON.stringify() 做数据格式转换
localStorage / sessionStorage可以结合vuex, 实现数据的持久保存,同时使用vuex解决数据和状态混乱问题.

$attrs$listeners

现在我们来讨论一种情况, 我们一开始给出的组件关系图中A组件与D组件是隔代关系, 那它们之前进行通信有哪些方式呢?

  1. 使用props绑定来进行一级一级的信息传递, 如果D组件中状态改变需要传递数据给A, 使用事件系统一级级往上传递
  2. 使用eventBus,这种情况下还是比较适合使用, 但是碰到多人合作开发时, 代码维护性较低, 可读性也低
  3. 使用Vuex来进行数据管理, 但是如果仅仅是传递数据, 而不做中间处理,使用Vuex处理感觉有点大材小用了.

vue2.4中,为了解决该需求,引入了$attrs$listeners , 新增了inheritAttrs 选项。 在版本2.4以前,默认情况下父作用域的不被认作props的属性百年孤独,将会“回退”且作为普通的HTML特性应用在子组件的根元素上。接下来看一个跨级通信的例子:

// app.vue
// index.vue

<template>
  <div>
    <child-com1
      :name="name"
      :age="18"
      :gender=""
      :height="158"
      title="程序员成长指北"
    ></child-com1>
  </div>
</template>
<script>
const childCom1 = () => import("./childCom1.vue");
export default {
  components: { childCom1 },
  data() {
    return {
      name: "zhang",
      age: "18",
      gender: "女",
      height: "158"
    };
  }
};
</script>
// childCom1.vue

<template class="border">
  <div>
    <p>name: {{ name}}</p>
    <p>childCom1的$attrs: {{ $attrs }}</p>
    <child-com2 v-bind="$attrs"></child-com2>
  </div>
</template>
<script>
const childCom2 = () => import("./childCom2.vue");
export default {
  components: {
    childCom2
  },
  inheritAttrs: false, // 可以关闭自动挂载到组件根元素上的没有在props声明的属性
  props: {
    name: String // name作为props属性绑定
  },
  created() {
    console.log(this.$attrs);
     // { "age": "18", "gender": "女", "height": "158", "title": "程序员成长指北" }
  }
};
</script>
// childCom2.vue

<template>
  <div class="border">
    <p>age: {{ age}}</p>
    <p>childCom2: {{ $attrs }}</p>
  </div>
</template>
<script>

export default {
  inheritAttrs: false,
  props: {
    age: String
  },
  created() {
    console.log(this.$attrs); 
    // { "name": "zhang", "gender": "女", "height": "158", "title": "程序员成长指北" }
  }
};
</script>

总结

常见使用场景可以分为三类:

  • 父子组件通信: props; $parent / $children; provide / inject ; ref ; $attrs / $listeners
  • 兄弟组件通信: eventBus ; vuex
  • 跨级通信: eventBus;Vuex;provide / inject$attrs / $listeners

今天就分享这么多,如果对分享的内容感兴趣,可以关注公众号「程序员成长指北」,或者加入技术交流群,大家一起讨论。


加入我们一起学习吧!

closure.md 文章

案例1-基本介绍

function A(){
    var localVal=10;
    return localVal;
}

A();//输出30


function A(){
    var localVal=10;
    return function(){
         console.log(localVal);
         return localVal;
    }
}
var func=A();
func();//输出10

上面的注释应该是输出 10。
还有闭包这篇首页无法导航,没具体看什么原因。

mysql按日期分组统计的查询

最近写的一个用户数据统计相关接口,需要用到按照每天进行分组统计。

直接看语句

SELECT DATE_FORMAT( create_time, "%Y-%m-%d" ) ,sum(user_id) FROM orders WHERE order_state=2 GROUP BY DATE_FORMAT( create_time, "%Y-%m-%d" ) ;

DATE_FORMAT函数说明

  • 语法
DATE_FORMAT(date,format)

//上面代码中使用的是
DATE_FORMAT( create_time, "%Y-%m-%d" )

date 参数是合法的日期。format 规定日期/时间的输出格式。

  • format可以使用的格式
格式 描述
%a 缩写星期名
%b 缩写月名
%c 月,数值
%D 带有英文前缀的月中的天
%d 月的天,数值(00-31)
%e 月的天,数值(0-31)
%f 微秒
%H 小时 (00-23)
%h 小时 (01-12)
%I 小时 (01-12)
%i 分钟,数值(00-59)
%j 年的天 (001-366)
%k 小时 (0-23)
%l 小时 (1-12)
%M 月名
%m 月,数值(00-12)
%p AM 或 PM
%r 时间,12-小时(hh:mm:ss AM 或 PM)
%S 秒(00-59)
%s 秒(00-59)
%T 时间, 24-小时 (hh:mm:ss)
%U 周 (00-53) 星期日是一周的第一天
%u 周 (00-53) 星期一是一周的第一天
%V 周 (01-53) 星期日是一周的第一天,与 %X 使用
%v 周 (01-53) 星期一是一周的第一天,与 %x 使用
%W 星期名
%w 周的天 (0=星期日, 6=星期六)
%X 年,其中的星期日是周的第一天,4 位,与 %V 使用
%x 年,其中的星期一是周的第一天,4 位,与 %v 使用
%Y 年,4 位
%y 年,2 位

说说对BFC的理解

BFC(Block Formatting Context),块级格式化上下文,是一个独立的渲染区域,让处于 BFC 内部的元素与外部的元素相互隔离,使内外元素的定位不会相互影响。

  • 触发条件 (以下任意一条)
    • float的值不为none
    • overflow的值不为visible
    • display的值为table-cell、tabble-caption和inline-block之一
    • position的值不为static或则releative中的任何一个

在IE下, Layout,可通过zoom:1 触发

  • .BFC布局与普通文档流布局区别
    普通文档流布局:

    1. 浮动的元素是不会被父级计算高度
    2. 非浮动元素会覆盖浮动元素的位置
    3. margin会传递给父级元素
    4. 两个相邻元素上下的margin会重叠

    BFC布局规则:

    1. 浮动的元素会被父级计算高度(父级元素触发了BFC)
    2. 非浮动元素不会覆盖浮动元素的位置(非浮动元素触发了BFC)
    3. margin不会传递给父级(父级触发BFC)
    4. 属于同一个BFC的两个相邻元素上下margin会重叠
  • 开发中的应用

    • 阻止margin重叠
    • 可以包含浮动元素 —— 清除内部浮动(清除浮动的原理是两个 div都位于同一个 BFC 区域之中)
    • 自适应两栏布局
    • 可以阻止元素被浮动元素覆盖

原型链这么看好像并不难

对象着手

在谈原型链之前,先了解对象。

  • 所有引用类型(函数,数组,对象)都拥有__proto__属性(隐式原型)

  • 所有函数拥有prototype属性(显式原型)(仅限函数)

  • 原型对象:拥有prototype属性的对象,在定义函数时就被创建

prototype与__proto__两个概念

  • prototype:此属性只有构造函数才有,它指向的是当前构造函数的原型对象。
  • proto:此属性是任何对象在创建时都会有的一个属性,它指向了产生当前对象的构造函数的原型对象,由于并非标准规定属性,不要随便去更改这个属性的值,以免破坏原型链,但是可以借助这个属性来学习,所谓的原型链就是由__proto__连接而成的链。

原型链详解

在js代码中 通过对象创建(下面一段简单的代码)详细分析原型链 一段简单代码:

function foo(){}
foo.prototype.z=3;

var obj=new foo();
obj.y=2;
obj.x=1;

//调用
obj.x;//1
obj.y;//2

obj.z;//3

typeof obj.toString;//'function'
'z' in obj;//true
obj.hasOwnProperty('z');//false

obj.z=5;
obj.z;//5
'z' in obj;//true
obj.hasOwnProperty('z');//true
foo.prototype.z;//3

代码简单分析

上面一段代码,声明第一个函数foo的时候,它就会带一个foo.prototype的属性,这个属性是一个对象属性,用new foo();构造器的方式构造一个新的对象obj。这时候这个obj的原型会指向foo的prototype属性。 对于这个foo函数的原型也会指向Object.prototype,这个Object.prototype也是有原型的,它的原型指向null。

代码对象原型链图:

原型链分析图原型链分析图

对象访问属性顺序

对象访问属性的顺序,是采用向上查找,如果当前对象没有,它会一直向上原型链中查找,一直找到null,如果还没有会返回undefind。

对象中值修改说明

代码中修改obj.z的值后,再次输出obj.z的时候是5,foo.prototype.z是3,说明我们在修改或添加对象的属性的时候,只是修改了对象本身obj.prototype.z中的值,而原型链中foo.prototype.z的值并不会修改。

in,hasOwnProperty等方法的出现

首先查看整个原型链,会想这两个方法是怎么来的,在foo的的proto指向上一级Object.prototype的时候,就可以访问Object中的一些函数和属性了,其中就包括这两个方法。

第一次调用

'z' in obj;//true  
obj.hasOwnProperty('z');//false

表示的是z并不是obj这个对象上的,而是对象的原型链上的。

 'z' in obj;//true
  obj.hasOwnProperty('z');//true
  foo.prototype.z;//3

第二次修改了obj.z的值,z就是obj这个对象上的了,但是也并没有修改原型链中的z的值。

特殊说明

_proto_是每一个对象都有的属性,它的指向会有一个特殊说明,大多数情况下 _proto_指向了产生当前对象的构造函数的原型对象,也就是那个 prototype。但是会有特殊的情况

  • 特殊情况
 var a={};
 var b=Object.create(a);

object.create是创建了一个空对象,空对象的原型指向a,a也是空对象,这其中不存在prototype;Object.create在继承中也常被使用,创建一个空对象指向()内的对象,这这样实现了b继承a,也不会篡改a中的内容,在这里就不具体说明了。

原理图分析

原理图分析原理图分析

总结

到底什么是原型链?

proto是任何对象都有的属性,在js中会形成一条proto连起来的链条,递归访问proto必须最终到头,并且值是null。

误区

写这篇总结的过程中,发现很多文章都写了“JS中万物皆对象”。难道真的是这样吗? JS世界很大,并不只有对象

说Node.js做后端开发,stream有必要了解下

什么是stream

定义

流的英文stream,流(Stream)是一个抽象的数据接口,Node.js中很多对象都实现了流,流是EventEmitter对象的一个实例,总之它是会冒数据(以 Buffer 为单位),或者能够吸收数据的东西,它的本质就是让数据流动起来。
可能看一张图会更直观:

水桶管道流转图

注意:stream不是node.js独有的概念,而是一个操作系统最基本的操作方式,只不过node.js有API支持这种操作方式。linux命令的|就是stream

为什么要学习stream

视频播放例子

小伙伴们肯定都在线看过电影,对比定义中的图-水桶管道流转图source就是服务器端的视频,dest就是你自己的播放器(或者浏览器中的flash和h5 video)。大家想一下,看电影的方式就如同上面的图管道换水一样,一点点从服务端将视频流动到本地播放器,一边流动一边播放,最后流动完了也就播放完了。

说明:视频播放的这个例子,如果我们不使用管道和流动的方式,直接先从服务端加载完视频文件,然后再播放。会造成很多问题

  1. 因内存占有太多而导致系统卡顿或者崩溃
  2. 因为我们的网速 内存 cpu运算速度都是有限的,而且还要有多个程序共享使用,一个视频文件加载完可能有几个g那么大。

读取大文件data的例子

有一个这样的需求,想要读取大文件data的例子

使用文件读取

const http = require('http');
const fs = require('fs');
const path = require('path');

const server = http.createServer(function (req, res) {
    const fileName = path.resolve(__dirname, 'data.txt');
    fs.readFile(fileName, function (err, data) {
        res.end(data);
    });
});
server.listen(8000);

使用文件读取这段代码语法上并没有什么问题,但是如果data.txt文件非常大的话,到了几百M,在响应大量用户并发请求的时候,程序可能会消耗大量的内存,这样可能造成用户连接缓慢的问题。而且并发请求过大的话,服务器内存开销也会很大。这时候我们来看一下用stream实现。

const http = require('http');
const fs = require('fs');
const path = require('path');

const server = http.createServer(function (req, res) {
    const fileName = path.resolve(__dirname, 'data.txt');
    let stream = fs.createReadStream(fileName);  // 这一行有改动
    stream.pipe(res); // 这一行有改动
});
server.listen(8000);

使用stream就可以不需要把文件全部读取了再返回,而是一边读取一边返回,数据通过管道流动给客户端,真的减轻了服务器的压力。

看了两个例子我想小伙伴们应该知道为什么要使用stream了吧!因为一次性读取,操作大文件,内存和网络是吃不消的,因此要让数据流动起来,一点点的进行操作。

stream流转过程

再次看这张水桶管道流转图

水桶管道流转图
图中可以看出,stream整个流转过程包括source,dest,还有连接二者的管道pipe(stream的核心),分别介绍三者来带领大家搞懂stream流转过程。

stream从哪里来-soucre

stream的常见来源方式有三种:

  1. 从控制台输入
  2. http请求中的request
  3. 读取文件

这里先说一下从控制台输入这种方式,2和3两种方式stream应用场景章节会有详细的讲解。

看一段process.stdin的代码

process.stdin.on('data', function (chunk) {
    console.log('stream by stdin', chunk)
    console.log('stream by stdin', chunk.toString())
})
//控制台输入koalakoala后输出结果
stream by stdin <Buffer 6b 6f 61 6c 61 6b 6f 61 6c 61 0a>
stream by stdin koalakoala

运行上面代码:然后从控制台输入任何内容都会被data 事件监听到,process.stdin就是一个stream对象,data
stream对象用来监听数据传入的一个自定义函数,通过输出结果可看出process.stdin是一个stream对象。

说明: stream对象可以监听"data","end","opne","close","error"等事件。node.js中监听自定义事件使用.on方法,例如process.stdin.on(‘data’,…), req.on(‘data’,…),通过这种方式,能很直观的监听到stream数据的传入和结束

连接水桶的管道-pipe

从水桶管道流转图中可以看到,在sourcedest之间有一个连接的管道pipe,它的基本语法是source.pipe(dest)sourcedest就是通过pipe连接,让数据从source流向了dest

stream到哪里去-dest

stream的常见输出方式有三种:

  1. 输出控制台
  2. http请求中的response
  3. 写入文件

stream应用场景

stream的应用场景主要就是处理IO操作,而http请求文件操作都属于IO操作。这里再提一下stream的本质——由于一次性IO操作过大,硬件开销太多,影响软件运行效率,因此将IO分批分段进行操作,让数据像水管一样流动起来,直到流动完成,也就是操作完成。下面对几个常用的应用场景分别进行介绍

介绍一个压力测试的小工具

一个对网络请求做压力测试的工具abab 全称 Apache bench ,是 Apache 自带的一个工具,因此使用 ab 必须要安装 Apache 。mac os 系统自带 Apachewindows 用户视自己的情况进行安装。运行 ab 之前先启动 Apachemac os 启动方式是 sudo apachectl start

Apache bench对应参数的详细学习地址,有兴趣的可以看一下
Apache bench对应参数的详细学习地址

介绍这个小工具的目的是对下面几个场景可以进行直观的测试,看出使用stream带来了哪些性能的提升。

get请求中应用stream

这样一个需求:

使用node.js实现一个http请求,读取data.txt文件,创建一个服务,监听8000端口,读取文件后返回给客户端,讲get请求的时候用一个常规文件读取与其做对比,请看下面的例子。

  • 常规使用文件读取返回给客户端response例子 ,文件命名为getTest1.js
// getTest.js
const http = require('http');
const fs = require('fs');
const path = require('path');

const server = http.createServer(function (req, res) {
    const method = req.method; // 获取请求方法
    if (method === 'GET') { // get 请求方法判断
        const fileName = path.resolve(__dirname, 'data.txt');
        fs.readFile(fileName, function (err, data) {
            res.end(data);
        });
    }
});
server.listen(8000);
  • 使用stream返回给客户端response
    将上面代码做部分修改,文件命名为getTest2.js
// getTest2.js
// 主要展示改动的部分
const server = http.createServer(function (req, res) {
    const method = req.method; // 获取请求方法
    if (method === 'GET') { // get 请求
        const fileName = path.resolve(__dirname, 'data.txt');
        let stream = fs.createReadStream(fileName);
        stream.pipe(res); // 将 res 作为 stream 的 dest
    }
});
server.listen(8000);

对于下面get请求中使用stream的例子,会不会有些小伙伴提出质疑,难道response也是一个stream对象,是的没错,对于那张水桶管道流转图,response就是一个dest。

虽然get请求中可以使用stream,但是相比直接file文件读取·res.end(data)有什么好处呢?这时候我们刚才推荐的压力测试小工具就用到了。getTest1getTest2两段代码,将data.txt内容增加大一些,使用ab工具进行测试,运行命令ab -n 100 -c 100 http://localhost:8000/,其中-n 100表示先后发送100次请求,-c 100表示一次性发送的请求数目为100个。对比结果分析使用stream后,有非常大的性能提升,小伙伴们可以自己实际操作看一下。

post中使用stream

一个通过post请求微信小程序的地址生成二维码的需求。

/*
* 微信生成二维码接口
* params src 微信url / 其他图片请求链接
* params localFilePath: 本地路径
* params data: 微信请求参数
* */
const downloadFile=async (src, localFilePath, data)=> {
    try{
        const ws = fs.createWriteStream(localFilePath);
        return new Promise((resolve, reject) => {
            ws.on('finish', () => {
                resolve(localFilePath);
            });
            if (data) {
                request({
                    method: 'POST',
                    uri: src,
                    json: true,
                    body: data
                }).pipe(ws);
            } else {
                request(src).pipe(ws);
            }
        });
    }catch (e){
        logger.error('wxdownloadFile error: ',e);
        throw e;
    }
}

看这段使用了stream的代码,为本地文件对应的路径创建一个stream对象,然后直接.pipe(ws),将post请求的数据流转到这个本地文件中,这种stream的应用在node后端开发过程中还是比较常用的。

post与get使用stream总结

request和reponse一样,都是stream对象,可以使用stream的特性,二者的区别在于,我们再看一下水桶管道流转图


request是source类型,是图中的源头,而response是dest类型,是图中的目的地。

在文件操作中使用stream

一个文件拷贝的例子

const fs = require('fs')
const path = require('path')

// 两个文件名
const fileName1 = path.resolve(__dirname, 'data.txt')
const fileName2 = path.resolve(__dirname, 'data-bak.txt')
// 读取文件的 stream 对象
const readStream = fs.createReadStream(fileName1)
// 写入文件的 stream 对象
const writeStream = fs.createWriteStream(fileName2)
// 通过 pipe执行拷贝,数据流转
readStream.pipe(writeStream)
// 数据读取完成监听,即拷贝完成
readStream.on('end', function () {
    console.log('拷贝完成')
})

看了这段代码,发现是不是拷贝好像很简单,创建一个可读数据流readStream,一个可写数据流writeStream,然后直接通过pipe管道把数据流转过去。这种使用stream的拷贝相比存文件的读写实现拷贝,性能要增加很多,所以小伙伴们在遇到文件操作的需求的时候,尽量先评估一下是否需要使用stream实现。

前端一些打包工具的底层实现

目前一些比较火的前端打包构建工具,都是通过node.js编写的,打包和构建的过程肯定是文件频繁操作的过程,离不来stream,例如现在比较火的gulp,有兴趣的小伙伴可以去看一下源码。

stream的种类

  • Readable Stream 可读数据流
  • Writeable Stream 可写数据流
  • Duplex Stream 双向数据流,可以同时读和写
  • Transform Stream 转换数据流,可读可写,同时可以转换(处理)数据(不常用)

之前的文章都是围绕前两种可读数据流和可写数据流,第四种流不太常用,需要的小伙伴网上搜索一下,接下来对第三种数据流Duplex Stream 说明一下。

Duplex Stream 双向的,既可读,又可写。
Duplex streams同时实现了 Readable Writable 接口。 Duplex streams的例子包括

  • tcp sockets
  • zlib streams
  • crypto streams
    我在项目中还未使用过双工流,一些Duplex Stream的内容可以参考这篇文章NodeJS Stream 双工流

stream有什么弊端

  • rs.pipe(ws) 的方式来写文件并不是把 rs 的内容 append 到 ws 后面,而是直接用 rs 的内容覆盖 ws 原有的内容
  • 已结束/关闭的流不能重复使用,必须重新创建数据流
  • pipe 方法返回的是目标数据流,如 a.pipe(b) 返回的是 b,因此监听事件的时候请注意你监听的对象是否正确
  • 如果你要监听多个数据流,同时你又使用了 pipe 方法来串联数据流的话,你就要写成:
    代码实例:
 data
        .on('end', function() {
            console.log('data end');
        })
        .pipe(a)
        .on('end', function() {
            console.log('a end');
        })
        .pipe(b)
        .on('end', function() {
            console.log('b end');
        });

stream的常见类库

总结

看完了这篇文章是不是对stream有了一定的了解,并且知道了node对于文件处理还是有完美的解决方案的。本文中三次展示了水桶管道流转图,总要的事情说三遍希望小伙伴们记住它,除了以上内容小伙伴们会不会有一些思考,比如

  1. stream数据流转具体内容是什么呢?二进制还是string类型还是其他类型,该类型为stream带来了什么好处?
  2. 水桶管道流转图中的水管,也就是pipe函数什么时候触发的呢?在什么情况下触流转发?底层机制是什么?
    上面的疑问(由于篇幅过长拆分为两篇)会在我stream的第二篇文章为大家详细讲解。

为什么JavaScript是单线程?

js最初设计是运行在浏览器中,单线程是为了防止DOM渲染冲突问题
比如有这样一个场景: 假设有2个线程, 当它们同时对同一个DON进行操作, 一个进行修改,而另一个进行删除, 此时就会产生冲突 所以为了避免出现这样的冲突, javascript从一诞生就是单线程.
但是呢单线程始终是一个痛点, 为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程。但是子线程完全受主线程控制,且不得操作 DOM

javascript中的闭包这一篇就够了

什么是闭包

维基百科中的概念

  • 在计算机科学中,闭包(也称词法闭包或函数闭包)是指一个函数或函数的引用,与一个引用环境绑定在一起,这个引用环境是一个存储该函数每个非局部变量(也叫自由变量)的表。
  • 闭包,不同于一般的函数,它允许一个函数在立即词法作用域外调用时,仍可访问非本地变量

学术上

  • 闭包是指在 JavaScript 中,内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回return掉(寿命终结)了之后。

个人理解

  • 闭包是在函数里面定义一个函数,该函数可以是匿名函数,该子函数能够读写父函数的局部变量。

闭包的常见案例分析

案例分析是从浅入深希望大家都看完!

  • 案例1---基本介绍:
function A(){
    var localVal=10;
    return localVal;
}

A();//输出30


function A(){
    var localVal=10;
    return function(){
         console.log(localVal);
         return localVal;
    }
}
var func=A();
func();//输出10

两段代码,在第二段代码中,函数A内的匿名函数可以访问到函数A中的局部变量这就是闭包的基本使用。

  • 案例2---前端实现点击事件
!function(){
    var localData="localData here";
    document.addEventListener('click',function(){
    console.log(localData);    
    });
}();

前端原始点击事件操作也用到了闭包来访问外部的局部变量。

  • 案例3---ajax请求
!function(){
    var localData="localData here";
    var url="http://www.baidu.com";
    $.ajax({
      url:url,
      success:function(){
          //do sth...
          console.log(localData);
      }
    })
}();

在ajax请求的方法中也用到了闭包,访问外部的局部变量。

  • 案例4---for循环案例
var arrays = [];

for (var i=0; i<3; i++) {
    arrays.push(function() {
        console.log('>>> ' + i); //all are 3
    });
}

上面的这段代码,刚看了代码一定会以为陆续打印出1,2,3,实际输出的是3,3,3,出现这种情况的原因是匿名函数保存的是引用,当for循环结束的时候,i已经变成3了,所以打印的时候变成3。出现这种情况的解决办法是利用闭包解决问题。

for (var i=0; i<3; i++) {
    (function(n) {
        tasks.push(function() {
            console.log('>>> ' + n);
        });
    })(i);
}

闭包里的匿名函数,读取变量的顺序,先读取本地变量,再读取父函数的局部变量,如果找不到到全局里面搜索,i作为局部变量存到闭包里面,所以调整后的代码可以能正常打印1,2,3。

闭包与内存泄漏

  • javascript回收后内存的方式:

javascript的主要通过计数器方式回收内存,假设有a,b,c三个对象,当a引用b的时候,那么b的引用计算器增加1(通俗的说用到那个对象哪个对象引用计算器增加1),同时b引用c的时候,c引用计数器增加1,当a被释放的时候,b的引用计数器减少1,变成0的时候这个对象被释放,c计数器变成0,被释放,但是当遇到b和c之间互相引用的时候,无法通过计数器方式释放内存。

  • 闭包可以导致上面所说b和c互相引用无法释放内存
    第一个案例的代码拿过来分析:
function A(){
    var localVal=10;
    return function(){
         console.log(localVal);
         return localVal;
    }
}
var func=A();
func();//输出10

当A函数结束的时候,想要释放,发现它的localVal变量被匿名函数引用,所有A函数无法释放,导致内存泄漏。但是也有好处,闭包正是可以做到这一点,因为它不会释放外部的引用,从而函数内部的值可以得以保留。

说明:闭包不代表一定会带来内存泄漏,良好的使用闭包是不会造成内存泄漏的。

闭包的应用

  • 封装
var person = function(){    
    //变量作用域为函数内部,外部无法访问    
    var name = "default";       
       
    return {    
       getName : function(){    
           return name;    
       },    
       setName : function(newName){    
           name = newName;    
       }    
    }    
}();    
     
print(person.name);//直接访问,结果为undefined    
print(person.getName());    
person.setName("kaola");    
print(person.getName());    
   
得到结果如下:  
   
undefined  
default  
kaola
  • 实例中的for循环另一种形式
doucument.body.innerHTML="<div id=div1>aaa</div>"+"<div id=div2>bbb</div>"+"<div id=div3>ccc</div>";
for(var i=1;i<4;i++){
    !function(i){
        document.getElementById('div'+i);
        addEventListener('click',function(){
           alert(i);//1,2,3 
        });
    }
}
  • 结果缓存
var CachedSearchBox = (function(){    
    var cache = {},    
       count = [];    
    return {    
       attachSearchBox : function(dsid){    
           if(dsid in cache){//如果结果在缓存中    
              return cache[dsid];//直接返回缓存中的对象    
           }    
           var fsb = new uikit.webctrl.SearchBox(dsid);//新建    
           cache[dsid] = fsb;//更新缓存    
           if(count.length > 100){//保正缓存的大小<=100    
              delete cache[count.shift()];    
           }    
           return fsb;          
       },    
     
       clearSearchBox : function(dsid){    
           if(dsid in cache){    
              cache[dsid].clearSelection();      
           }    
       }    
    };    
})();    
     
CachedSearchBox.attachSearchBox("input");

说明:开发中会碰到很多情况,设想我们有一个处理过程很耗时的函数对象,每次调用都会花费很长时间,那么我们就需要将计算出来的值存储起来,当调用这个函数的时候,首先在缓存中查找,如果找不到,则进行计算,然后更新缓存并返回值,如果找到了,直接返回查找到的值即可。闭包正是可以做到这一点,因为它不会释放外部的引用,从而函数内部的值可以得以保留。

面试题分析

闭包测试题: 求输出结果

function fun(n,o){
    console.log(o);
    return {
        fun:function(m){//[2]
            return fun(m,n);//[1]
        }
    }
}

var a=fun(0);
a.fun(1);
a.fun(2);
a.fun(3);
var b=fun(0).fun(1).fun(2).fun(3);
var c=fun(0).fun(1);
c.fun(2);
c.fun(3);

由于分析内容比较多,大家可直接参考这篇文章
https://cnodejs.org/topic/567ed16eaacb6923221de48f

分析内容说明,在看这篇文章的时候,注意两点可能会看的更明白:

  • JS的词法作用域,JS变量作用域存在于函数体中即函数体,并且变量的作用域是在函数定义声明的时候就是确定的,而非在函数运行时。
  • 在JS中调用函数的时候,如果用一个参数的方法调用两个参数的方法,这时候只是第二个参数未定义,代码不会报错停止运行,正常流程往下走,像面试题中仍然会返回一个对象。

总结

  1. 闭包其实是在函数内部定义一个函数。
  2. 闭包在使用的时候不会释放外部的引用,闭包函数内部的值会得到保留。
  3. 闭包里面的匿名函数,读取变量的顺序,先读取本地变量,再读取父函数的局部变量。
  4. 对于闭包外部无法引用它内部的变量,因此在函数内部创建的变量执行完后会立刻释放资源,不污染全局对象。
  5. 闭包使用的时候要考虑到内存泄漏,因为不释放外部引用,但是合理的使用闭包是内存使用不是内存泄漏。

Sequelize Unknown column 'createdAt' in 'field list'?

报错:

在使用sequelize创建model后,使用model进行findAll查询的时候报错:

Sequelize Unknown column '*.createdAt' in 'field list'?

报错原因:

在sequelize中启用了时间戳(即sequelize中创建的model开启了时间戳),但是在数据库中的实际表定义不包含时间戳列。

当你做model.findAll时,会将model中的每一进行别名查询(也包括model中开启的时间戳),从而创建下面例子查询,这面就包含了数据库中实际没有的时间戳列(createdAt,updatedAt):

SELECT `users`.*, `userDetails`.`userId` AS `userDetails.userId`,`userDetails`.`firstName` AS `userDetails.firstName`, `userDetails`.`id` AS `userDetails.id`, `userDetails`.`createdAt` AS `userDetails.createdAt`, `userDetails`.`updatedAt` AS `userDetails.updatedAt` FROM `users` LEFT OUTER JOIN `userDetails` AS `userDetails` ON `users`.`id` = `userDetails`.`userId`;

解决办法

在model创建的时候禁用时间戳,例如禁用userDetails模型的时间戳:

var userDetails = sequelize.define('userDetails', {
    userId :Sequelize.INTEGER,
    firstName : Sequelize.STRING,
    lastName : Sequelize.STRING,
    birthday : Sequelize.DATE
}, {
    timestamps: false
});

或在sequelize连接mysql的时候,直接禁用所有时间戳开启:

var sequelize = new Sequelize('sequelize_test', 'root', null, {
    host: "127.0.0.1",
    dialect: 'mysql',
    define: {
        timestamps: false
    }
});

作为一个前端工程师也要掌握的几种文件路径知识

前言

之前在做webpack配置时候多次用到路径相关内容,最近在写项目的时候,有一个文件需要上传到阿里云oss的功能,同时本地服务器也需要保留一个文件备份。多次用到了文件路径相关内容以及Node核心API的path模块,所以系统的学习了一下,整理了这篇文章。

node中的路径分类

node中的路径大致分5类,dirname,filename,process.cwd(),./,../,其中dirname,filename,process.cwd()绝对路径

通过代码对每个分类进行说明:

文件目录结构如下:

代码pra/
  - node核心API/
      - fs.js
      - path.js

path.js中的代码

const path = require('path');
console.log(__dirname);
console.log(__filename);
console.log(process.cwd());
console.log(path.resolve('./'));

在代码pra目录下运行命令 node node核心API/path.js,我们可以看到结果如下:

/koala/Desktop/程序员成长指北/代码pra/node核心API
/koala/Desktop/程序员成长指北/代码pra/node核心API/path.js
/koala/Desktop/程序员成长指北/代码pra
/koala/Desktop/程序员成长指北/代码pra

然后我们有可以在node核心API目录下运行这个文件,node path.js,运行结果如下:

/koala/Desktop/程序员成长指北/代码pra/node核心API
/koala/Desktop/程序员成长指北/代码pra/node核心API/path.js
/koala/Desktop/程序员成长指北/代码pra/node核心API
/koala/Desktop/程序员成长指北/代码pra/node核心API

对比输出结果,暂时得到的结论是

  • __dirname: 总是返回被执行的 js 所在文件夹的绝对路径
  • __filename: 总是返回被执行的 js 的绝对路径
  • process.cwd(): 总是返回运行 node 命令时所在的文件夹的绝对路径
  • ./: 跟 process.cwd() 一样,返回 node 命令时所在的文件夹的绝对路径

为什么说上面是暂时得到的结论,因为是有错误的,再看一段代码:
我们在path.js中加上这句代码

exports.A = 1;

之前直接通过readFile读取文件路径报错,

fs.readFile('./path.js',function(err,data){
   
});

现在在刚才报错的fs.js里面加这两句代码看看:

const test = require('./path.js');
console.log(test)

代码pra/目录下运行node node核心API/fs.js,最后查看结果,说明是可以访问到的:

{ A: 1 }

那么关于 ./ 正确的结论是:

require() 中使用是跟 __dirname 的效果相同,不会因为启动脚本的目录不一样而改变,在其他情况下跟 process.cwd() 效果相同,是相对于启动脚本所在目录的路径。

路径知识总结:

  • __dirname: 获得当前执行文件所在目录的完整目录名
  • __filename: 获得当前执行文件的带有完整绝对路径的文件名
  • process.cwd():获得当前执行node命令时候的文件夹目录名
  • ./: 不使用require时候,./process.cwd()一样,使用require时候,与__dirname一样

只有在 require() 时才使用相对路径(./, ../) 的写法,其他地方一律使用绝对路径,如下:

// 当前目录下
 path.dirname(__filename) + '/path.js'; 
// 相邻目录下
 path.resolve(__dirname, '../regx/regx.js');

path

前面讲解了路径的相关比较,接下来单独聊聊path这个模块,这个模块在很多地方比较常用,所以,对于我们来说,掌握他,对我们以后的发展更有利,不用每次看webpack的配置文件还要去查询一下这个api是干什么用的,很影响我们的效率

这是api官网地址:https://nodejs.org/api/path.html

个人认为官网中的api没有必要都掌握,下面会对一些常用的api进行讲解,我经常用到的,或者作为一个前端开发工程师在webpack等工程配置的时候经常用到的。

path.normalize

举例说明

const path = require('path');

console.log(path.normalize('/koala/Desktop//程序员成长指北//代码pra/..'));

规范后的结果

/koala/Desktop/程序员成长指北/代码pra

作用总结

规范化路径,把不规范的路径规范化。

path.join

举例说明

const path = require('path');
console.log(path.join('src', 'task.js'));

const path = require('path');
console.log(path.join(''));

转化后的结果

src/task.js
.

作用总结

path.join([...paths])

  1. 传入的参数是字符串的路径片段,可以是一个,也可以是多个
  2. 返回的是一个拼接好的路径,但是根据平台的不同,他会对路径进行不同的规范化,举个例子,Unix系统是/Windows系统是\,那么你在两个系统下看到的返回结果就不一样。
  3. 如果返回的路径字符串长度为零,那么他会返回一个.,代表当前的文件夹。
  4. 如果传入的参数中有不是字符串的,那就直接会报错

path.parse

举例说明

const path = require('path');
console.log(path.parse('/koala/Desktop/程序员成长指北/代码pra/node核心API'));

运行结果

{ root: '/',
  dir: '/koala/Desktop/程序员成长指北/代码pra',
  base: 'node核心API',
  ext: '',
  name: 'node核心API' 
}

作用总结

他返回的是一个对象,那么我们来把这么几个名词熟悉一下:

  1. root:代表根目录
  2. dir:代表文件所在的文件夹
  3. base:代表整一个文件
  4. name:代表文件名
  5. ext: 代表文件的后缀名

path.basename

举例说明

const path = require('path');
console.log(path.basename('/koala/Desktop/程序员成长指北/代码pra/node核心API'));
console.log(path.basename('/koala/Desktop/程序员成长指北/代码pra/node核心API/path.js', '.js'));

运行结果

看了上面代码的例子,我想应该知道了basename结果,嘿嘿。

node核心API
path

作用总结

basename接收两个参数,第一个是path,第二个是ext(可选参数),当输入第二个参数的时候,打印结果不出现后缀名

path.dirname

举例说明

const path = require('path');
console.log(path.dirname('/koala/Desktop/程序员成长指北/代码pra/node核心API'));

运行结果

/koala/Desktop/程序员成长指北/代码pra

作用总结

返回文件的目录完整地址

path.extname

举例说明

const path = require('path');
path.extname('index.html');
path.extname('index.coffee.md');
path.extname('index.');
path.extname('index');
path.extname('.index');

运行结果

.html
.md
.
''
''

作用总结

返回的是后缀名,但是最后两种情况返回'',大家注意一下。

path.resolve

举例说明

const path = require('path');
console.log(path.resolve('/foo/bar', '/bar/faa', '..', 'a/../c'));

输出结果

/bar/c

作用总结

path.resolve([...paths])

path.resolve就相当于是shell下面的cd操作,从左到右运行一遍cd path命令,最终获取的绝对路径/文件名,这个接口所返回的结果了。但是resolve操作和cd操作还是有区别的,resolve的路径可以没有,而且最后进入的可以是文件。具体cd步骤如下

cd /foo/bar/    //这是第一步, 现在的位置是/foo/bar/
cd /bar/faa     //这是第二步,这里和第一步有区别,他是从/进入的,也就时候根目录,现在的位置是/bar/faa
cd ..       //第三步,从faa退出来,现在的位置是 /bar
cd a/../c   //第四步,进入a,然后在推出,在进入c,最后位置是/bar/c

path.relative

举例说明

const path = require('path');

console.log(path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb'));

console.log(path.relative('/data/demo', '/data/demo'));

console.log(path.relative('/data/demo', ''));

运行结果

../../impl/bbb
 ""
 ../../koala/Desktop/程序员成长指北/代码pra/node核心API

作用总结

path.relative(from, to)

描述:从from路径,到to路径的相对路径。

边界:

  • 如果from、to指向同个路径,那么,返回空字符串。
  • 如果from、to中任一者为空,那么,返回当前工作路径。

总结

本篇文章关于路径的知识就说到这里,基础很重要的,既能节约开发时间,又能减少报错。

今天就分享这么多,如果对分享的内容感兴趣,可以关注公众号「程序员成长指北」,或者加入技术交流群,大家一起讨论。

进阶技术路线


加入我们一起学习吧!

高阶函数详解与实战训练

前言

一道经典面试题:

//JS实现一个无限累加的add函数
add(1)  //1 
add(1)(2)  //3
add(1)(2)(3)  //6

当大家看到这个面试题的时候,能否在第一时间想到使用高阶函数实现?想到在实际项目开发过程中,用到哪些高级函数?有没有想过自己创造一个高阶函数呢?开始本篇文章的学习

高阶函数定义

高阶函数英文叫 Higher-order function。高阶函数是对其他函数进行操作的函数,操作可以是将它们作为参数,或者返回它们。简单总结为高阶函数是一个接收函数作为参数或者将函数作为返回输出的函数。

函数作为参数情况

Array.prototype.mapArray.prototype.filterArray.prototype.reduceArray.prototype.sort是JavaScript中内置的高阶函数。它们接受一个函数作为参数,并应用这个函数到列表的每一个元素。下面是一些内置高阶函数的具体说明讲解,以及和不使用高阶函数情况下的对比

Array.prototype.map

map()(映射)方法最后生成一个新数组,不改变原始数组的值。其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。

array.map(callback,[ thisObject]);

callback(回调函数)

[].map(function(currentValue, index, array) {
    // ...
});

传递给 map 的回调函数(callback)接受三个参数,分别是currentValue——正在遍历的元素、index(可选)——元素索引、array(可选)——原数组本身,除了 callback 之外还可以接受 this 值(可选),用于执行 callback 函数时使用的this 值。

来个简单的例子方便理解,现在有一个数组[1,2,3,4],我们想要生成一个新数组,其每个元素皆是之前数组的两倍,那么我们有下面两种使用高阶和不使用高阶函数的方式来实现。

不使用高阶函数

// koala
const arr1 = [1, 2, 3, 4];
const arr2 = [];
for (let i = 0; i < arr1.length; i++) {
  arr2.push( arr1[i] * 2);
}

console.log( arr2 );
// [2, 4, 6, 8]
console.log( arr1 );
// [1, 2, 3, 4]

使用高阶函数

// kaola
const arr1 = [1, 2, 3, 4];
const arr2 = arr1.map(item => item * 2);

console.log( arr2 );
// [2, 4, 6, 8]
console.log( arr1 );
// [1, 2, 3, 4]

map高阶函数注意点

callback需要有return值,否则会出现所有项映射为undefind;

// kaola
const arr1 = [1, 2, 3, 4];
const arr2 = arr1.map(item => {});

console.log( arr2 );
// [ undefined, undefined, undefined, undefined ]
console.log( arr1 );
// [1, 2, 3, 4]

map高阶函数对应的一道经典面试题

//输出结果
["1", "2", "3"].map(parseInt);

看了这道题不知道会不会有大多数开发者认为输出结果是[1,2,3],错误

正确的输出结果为:

[1,NaN,NaN]
分析与讲解

因为mapcallback函数有三个参数,正在遍历的元素, 元素索引(index), 原数组本身(array)。parseInt有两个参数,string和radix(进制),注意第二个参数进制当为0或者没有参数的时候,parseInt()会根据string来判断数字的基数。当忽略参数 radix , JavaScript 默认数字的基数如下:

  • 如果 string 以 "0x" 开头,parseInt() 会把 string 的其余部分解析为十六进制的整数。
  • 如果 string 以 0 开头,那么 ECMAScript v3 允许 parseInt() 的一个实现把其后的字符解析为八进制或十六进制的数字。
  • 如果 string 以 1 ~ 9 的数字开头,parseInt() 将把它解析为十进制的整数。

只传入parseInt的话,map callback会自动忽略第三个参数array。而index参数不会被忽略。而不被忽略的index(0,1,2)就会被parseInt当做第二个参数。

将其拆开看:

parseInt("1",0);//上面说过第二个参数为进制,所以"1",radix为0上面提到过,会忽略,根据string 以 1 ~ 9 的数字开头,parseInt() 将把它解析为十进制的整数1。
parseInt("2",1);//此时将2转为1进制数,由于超过进制数1,所以返回NaN。
parseInt("3",2);//此时将3转为2进制数,由于超过进制数1,所以返回NaN。

所以最终的结果为[1,NaN,NaN]

那么如果想要得到[1,2,3]该怎么写。

["1","2","3"].map((x)=>{
    return parseInt(x);
});

也可以简写为:
["1","2","3"].map(x=>parseInt(x));

这样写为什么就能返回想要的值呢?因为,传一个完整函数进去,有形参,有返回值。这样就不会造成因为参数传入错误而造成结果错误了,最后返回一个经纯函数处理过的新数组。

Array.prototype.reduce

reduce() 方法对数组中的每个元素执行一个提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。传递给 reduce 的回调函数(callback)接受四个参数,分别是累加器 accumulatorcurrentValue——正在操作的元素、currentIndex(可选)——元素索引,但是它的开始会有特殊说明、array(可选)——原始数组本身,除了 callback 之外还可以接受初始值 initialValue 值(可选)。

  • 如果没有提供 initialValue,那么第一次调用 callback 函数时,accumulator 使用原数组中的第一个元素,currentValue 即是数组中的第二个元素。 在没有初始值的空数组上调用 reduce 将报错。

  • 如果提供了 initialValue,那么将作为第一次调用 callback 函数时的第一个参数的值,即 accumulator,currentValue 使用原数组中的第一个元素。

例子,现在有一个数组 [0, 1, 2, 3, 4],需要计算数组元素的和,需求比较简单,来看下代码实现。

不使用高阶函数

//koala
const arr = [0, 1, 2, 3, 4];
let sum = 0;
for (let i = 0; i < arr.length; i++) {
  sum += arr[i];
}

console.log( sum );
// 10
console.log( arr );
// [0, 1, 2, 3, 4]

使用高阶函数

无 initialValue 值

const arr = [0, 1, 2, 3, 4];
let sum = arr.reduce((accumulator, currentValue, currentIndex, array) => {
  return accumulator + currentValue;
});

console.log( sum );
// 10
console.log( arr );
// [0, 1, 2, 3, 4]

上面是没有 initialValue 的情况,代码的执行过程如下,callback 总共调用四次。

callback accumulator currentValue currentIndex array return value
first call 0 1 1 [0, 1, 2, 3, 4] 1
second call 1 2 2 [0, 1, 2, 3, 4] 3
third call 3 3 3 [0, 1, 2, 3, 4] 6
fourth call 6 4 4 [0, 1, 2, 3, 4] 10

有 initialValue 值

我们再来看下有 initialValue 的情况,假设 initialValue 值为 10,我们看下代码。

//koala
const arr = [0, 1, 2, 3, 4];
let sum = arr.reduce((accumulator, currentValue, currentIndex, array) => {
  return accumulator + currentValue;
}, 10);

console.log( sum );
// 20
console.log( arr );
// [0, 1, 2, 3, 4]

代码的执行过程如下所示,callback 总共调用五次。

callback accumulator currentValue currentIndex array return value
first call 10 0 0 [0, 1, 2, 3, 4] 10
second call 10 1 1 [0, 1, 2, 3, 4] 11
third call 11 2 2 [0, 1, 2, 3, 4] 13
fourth call 13 3 3 [0, 1, 2, 3, 4] 16
fifth call 16 4 4 [0, 1, 2, 3, 4] 20

Array.prototype.filter

filter(过滤,筛选) 方法创建一个新数组,原始数组不发生改变。

array.filter(callback,[ thisObject]);

其包含通过提供函数实现的测试的所有元素。接收的参数和 map 是一样的,filter的callback函数需要返回布尔值true或false. 如果为true则表示通过啦!如果为false则失败,其返回值是一个新数组,由通过测试为true的所有元素组成,如果没有任何数组元素通过测试,则返回空数组。

来个例子介绍下,现在有一个数组 [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4],我们想要生成一个新数组,这个数组要求没有重复的内容,即为去重。

不使用高阶函数

const arr1 = [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4];
const arr2 = [];
for (let i = 0; i < arr1.length; i++) {
  if (arr1.indexOf( arr1[i] ) === i) {
    arr2.push( arr1[i] );
  }
}
console.log( arr2 );
// [1, 2, 3, 5, 4]
console.log( arr1 );
// [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4]

使用高阶函数

const arr1 = [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4];
const arr2 = arr1.filter( (element, index, self) => {
    return self.indexOf( element ) === index;
});

console.log( arr2 );
// [1, 2, 3, 5, 4]
console.log( arr1 );
// [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4]

filter注意点说明

callback在过滤测试的时候,一定要是Boolean值吗?
例子:

var arr = [0, 1, 2, 3];
var arrayFilter = arr.filter(function(item) {
    return item;
});
console.log(arrayFilter); // [1, 2, 3]

通过例子可以看出:过滤测试的返回值只要是弱等于== true/false就可以了,而非非得返回 === true/false.

Array.prototype.sort

sort() 方法用原地算法对数组的元素进行排序,并返回数组,该排序方法会在原数组上直接进行排序,并不会生成一个排好序的新数组。排序算法现在是稳定的。默认排序顺序是根据字符串Unicode码点。

// 语法
arr.sort([compareFunction])

compareFunction参数是可选的,用来指定按某种顺序进行排列的函数。注意该函数有两个参数:

参数1:firstEl

第一个用于比较的元素。

参数2:secondEl

第二个用于比较的元素。看下面的例子与说明:

// 未指明compareFunction函数

['Google', 'Apple', 'Microsoft'].sort(); // ['Apple', 'Google', 'Microsoft'];

// apple排在了最后:
['Google', 'apple', 'Microsoft'].sort(); // ['Google', 'Microsoft", 'apple']

// 无法理解的结果:
[10, 20, 1, 2].sort(); // [1, 10, 2, 20]
//正确的结果
[6, 8, 1, 2].sort(); // [1, 2,6, 8]

// 指明compareFunction函数
'use strict';
var arr = [10, 20, 1, 2];
    arr.sort(function (x, y) {
        if (x < y) {
            return -1;
        }
        if (x > y) {
            return 1;
        }
        return 0;
    });
console.log(arr); // [1, 2, 10, 20]

如果没有指明 compareFunction ,那么元素会按照转换为的字符串的诸个字符的Unicode位点进行排序。例如 "Banana" 会被排列到 "cherry" 之前。当数字按由小到大排序时,10 出现在 2 之前,但因为(没有指明 compareFunction),比较的数字会先被转换为字符串,所以在Unicode顺序上 "10" 要比 "2" 要靠前。

如果指明了 compareFunction ,那么数组会按照调用该函数的返回值排序。即 a 和 b 是两个将要被比较的元素:

  • 如果 compareFunction(a, b) 小于 0 ,那么 a 会被排列到 b 之前;

  • 如果 compareFunction(a, b) 等于 0 , a 和 b 的相对位置不变。备注: ECMAScript 标准并不保证这一行为,而且也不是所有浏览器都会遵守(例如 Mozilla 在 2003 年之前的版本);

  • 如果 compareFunction(a, b) 大于 0 , b 会被排列到 a 之前。
    compareFunction(a, b) 必须总是对相同的输入返回相同的比较结果,否则排序的结果将是不确定的。

sort排序算法的底层实现

看了上面sort的排序介绍,我想小伙伴们肯定会对sort排序算法的内部实现感兴趣,我在sf上面搜了一下,发现有些争议。于是去查看了V8引擎的源码,发现在源码中的710行

源码地址:https://github.com/v8/v8/blob/ad82a40509c5b5b4680d4299c8f08d6c6d31af3c/src/js/array.js

// In-place QuickSort algorithm.
// For short (length <= 22) arrays, insertion sort is used for efficiency.

V8 引擎 sort 函数只给出了两种排序 InsertionSortQuickSort数量小于等于22的数组使用 InsertionSort,比22大的数组则使用 QuickSort,有兴趣的可以看看具体算法实现。

注意:不同的浏览器引擎可能算法实现并不同,我这里只是查看了V8引擎的算法实现,有兴趣的小伙伴可以查看下其他开源浏览器具体sort的算法实现。

如何改进排序算法实现数字正确排序呢?

对于要比较数字而非字符串,比较函数可以简单的以 a 减 b,如下的函数将会将数组升序排列,降序排序则使用b-a。

let compareNumbers= function (a, b) {
    return a - b;
}
let koala=[10, 20, 1, 2].sort(compareNumbers)

console.log(koala);
// [1 , 2 , 10 , 20]

函数作为返回值输出

返回一个函数,下面直接看两个例子来加深理解。

isType 函数

我们知道在判断类型的时候可以通过Object.prototype.toString.call 来获取对应对象返回的字符串,比如:

let isString = obj => Object.prototype.toString.call( obj ) === '[object String]';

let isArray = obj => Object.prototype.toString.call( obj ) === '[object Array]';

let isNumber = obj => Object.prototype.toString.call( obj ) === '[object Number]';

可以发现上面三行代码有很多重复代码,只需要把具体的类型抽离出来就可以封装成一个判断类型的方法了,代码如下。

let isType = type => obj => {
  return Object.prototype.toString.call( obj ) === '[object ' + type + ']';
}

isType('String')('123');        // true
isType('Array')([1, 2, 3]);    // true
isType('Number')(123);            // true

这里就是一个高阶函数,因为 isType 函数将 obj => { ... } 这一函数作为返回值输出。

add求和函数

前言中的面试题,用 JS 实现一个无限累加的函数 add,示例如下:

add(1); // 1
add(1)(2);  // 3
add(1)(2)(3) // 6

分析面试题的结构,都是将函数作为返回值输出,然后接收新的参数并进行计算。

我们知道打印函数时会自动调用 toString()方法(如果不知道的可以去看我的这篇文章),函数 add(a) 返回一个sum(b)函数,函数 sum() 中累加计算 a = a + b,只需要重写sum.toString()方法返回变量 a 就可以了。

function add(a) {
    function sum(b) { // 使用闭包
        a = a + b; // 累加
        return sum;
     }
     sum.toString = function() { // 重写toString()方法
        return a;
    }
     return sum; // 返回一个函数
}

add(1); // 1
add(1)(2);  // 3
add(1)(2)(3) // 6

如何自己创建高阶函数

前面讲了语言中内置的各种高阶函数。知道了到底啊什么是高阶函数,有哪些类型的高阶函数。那么让我们自己创建一个高阶函数吧!

假设 JavaScript 没有原生的map方法。 我们自己构建个类似map的高阶函数,从而创建我们自己的高阶函数。
假设我们有一个字符串数组,我们希望把它转换为整数数组,其中每个元素代表原始数组中字符串的长度。

const strArray=['JavaScript','PHP','JAVA','C','Python'];
function mapForEach(arr,fn){
    const newArray = [];
    for(let i = 0; i<arr.length;i++){
        newArray.push({
            fn(arr[i])
        );
    }
    return newArray;
}
const lenArray = mapForEach(strArray,function(item){
    return item.length;
});

console.log(lenArray);//[10,3,4,1,6]

代码分析讲解:

我们创建了一个高阶函数 mapForEach ,它接受一个数组和一个回调函数 fn。 它循环遍历传入的数组,并在每次迭代时在 newArray.push 方法调用回调函数 fn 。

回调函数 fn 接收数组的当前元素并返回该元素的长度,该元素存储在 newArray 中。 for 循环完成后,newArray 被返回并赋值给 lenArray。

总结

我们已经了解了高阶函数和一些内置的高阶函数,还学习了如何创建自己的高阶函数。简而言之,高阶函数是一个可以接收函数作为参数,甚至返回一个函数的函数。 它就像常规函数一样,只是多了接收和返回其他函数的附加能力,即参数和输出。

公众号技术栈路线

大家好,我是koala,公众号「程序员成长指北」作者,这篇文章是【JS必知必会系列】的高阶函数讲解。目前在做一个node后端工程师进阶路线,加入我们一起学习吧!

加入我们

mysql查询结果单位换算后保留两位小数

数据库表中sale列的值为23456.789

  1. format函数

说明:format(x,d)是mysql自带的格式化小数函数,format()函数会对小数部分进行四舍五入操作,整数部分从右向左每三位一个逗号进行格式化输出

format函数结果:

23,456.79
  1. truncate函数

说明:truncate(x,d)是mysql自带的函数,truncate()函数会将小数部分d位以后的值直接舍去

truncate函数结果:

23456.78
  1. round函数(常用)

说明:round(x,d)是mysql自带的函数,format()函数会对小数部分进行四舍五入操作,但是不会出现format函数中的带逗号格式

round函数数结果:

23456.79
  1. convert函数(常用)

说明:MySQL 的CAST()和CONVERT()函数可用来获取一个类型的值,并产生另一个类型的值。简单的说就是类型转换。
CONVERT(xxx,类型),但是类型是有限制的,下面几种类型

  • 二进制,同带binary前缀的效果 : BINARY
  • 字符型,可带参数 : CHAR()
  • 日期 : DATE
  • 时间: TIME
  • 日期时间型 : DATETIME
  • 浮点数 : DECIMAL
  • 整数 : SIGNED
  • 无符号整数 : UNSIGNED

根据文初给出的数据,应该是这些写

convert(23456.789,decimal(10,2));

convert函数数结果,这里也是四舍五入:

23456.79

注:解释下dem的意思

DECIMAL(10,2) 总共能存10位数字,末尾2位是小数,字段最大值99999999.99(小数点不算在长度内)

回调地狱解决方案之Promise

为什么出现Promise

在javascript开发过程中,代码是单线程执行的,同步操作,彼此之间不会等待,这可以说是它的优势,但是也有它的弊端,如一些网络操作,浏览器事件,文件等操作等,都必须异步执行,针对这些情况,起初的操作都是使用回调函数实现。

实现方式如下(伪代码):

function One(callback) {
    if (success) {
        callback(err, result);
    } else {
        callback(err, null);
    }
}

One(function (err, result) {
    //执行完One函数内的内容,成功的结果回调回来向下执行
})

上述代码只是一层级回调,如果代码复杂后,会出现多层级的回调,代码可读性也会很差,那有没有一种方式,不用考虑里面的内容,直接根据结果成功还是失败执行下面的代码呢?有的,Promise(承诺),在ES6中对Promise进行了同意的规范。

Promise的含义

  • 书上这么说:

    Promise 是异步编程的一种解决方案,比传统的解决方案–回调函数和事件--更合理和更强大。它由社区最早提出和实现,ES6将其写进了语言标准,统一了语法,原生提供了Promise

    所谓Promise ,简单说就是一个容器,里面保存着某个未来才回结束的事件(通常是一个异步操作)的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。
    Promise 对象的状态不受外界影响

  • Promise/a+ 官方网站给出的定义
    A promise represents the eventual result of an asyncchronous operation

    翻译:表示一个异步操作的最终结果。

  • 我的理解:

    Promise是回调函数可以规范链式调用

Promise原理与讲解

原理
  1. Promise的三种状态
  • pending:进行中
  • fulfilled :执行成功
  • rejected :执行失败

注意Promise在某一时刻只能处于一种状态

  1. Promise的状态改变
  • pending------》fulfilled(resolved)
  • pending------》rejected

Promise的状态改变,状态只能由pending转换为rejected或者rejected,一旦状态改变完成后将无法改变(不可逆性)

用代码讲原理
  1. 创建一个Promise

创建Promise需要用到Promise的构造函数来实现,代码如下:

var promise=new Promise(function(resolve,reject){
   // ...some async code 
   
   if(/* 一些异步操作成功*/)
   {
       resolve(value);
   }else
   {
       reject(error);
   }

})

代码分析:

  • 在异步操作完成之后,会针对不同的返回结果调用resolve和reject。
  • resolve和reject是两个函数,resolve是异步操作成功时候被调用,将异步操作的返回值作为参数传递到外部;reject是异步操作出异常时候被调用,将错误信息作为参数传递出去。
  • ==Promise其实没有做任何实质的代码操作,它只是对异步操作回调函数的不同结果定义了不同状态。==
  • resolve函数和reject函数只是把异步结果传递出去
  1. 异步结果传递出去后,then来接
    Promise对象将结果传递出来后,使用then方法来获取异步操作的值:
    代码如下:
promise.then(function(value){
   //success
   
},function(error){

});

代码分析:

  • then方法将两个匿名函数作为参数,接收resolve和reject这两个函数的值。
  • value是执行成功的值,error是执行出错时的错误信息。
  • 对于error错误异常结果出现的时候,可以不单独写匿名错误的函数,可以直接用catch抛出
promise.then(function (data){
    //success
})
.catch(function(error){
  //error  
})
  • 注意then方法==只是==用来获取异步操作的值。
  1. then的返回值又是怎样呢?
    先看一段调用两次then的代码:
//之前创建promise操作后
promise.then(function(value){
    conlose.log(value);  //有值
}.then(function(value)
{
   conlose.log(value);   //未定义
});

代码分析:

  • 上面的第二个then方法中的值虽然是未定义,但是每一个then一定会==返回一个新的peomise对==象,但是默认是一个空对象。
  • 对于这个空对象我们如果想继续做一些什么,需要进行处理,可以用非空Promise对这个空的进行赋值覆盖,然后继续then的链式调用。
  • then 中的==retuen==关键字很重要,联系着下一个then的调用。
几个常用api
  • Promise.resolve
    resolve方法用来将一个非Promise对象转化为Promise对象

转换的对象是一个常量或者不具备状态的语句,转换后的对象自动处于resolve状态。
转换的后的结果和原来一样

var promise =Promise.resolve("hello world");
promise.then(function(result){
  console.log(result);   //输出结果 hello world
})

转换的对象如果直接是一个异步方法,不可以这么使用。

  • Promise.all(常用api)
    多个promise需要执行的时候,可以使用promise.all方法统一声明,该方法可以将多个Promise对象包装成一个Promise。

代码如下

promise.all(
//一系列promise操作
).then(function(results){
    
    
}).catch(function(error){
    
});

代码分析:

  • promise.all对多有执行结果做一个包装传给了then
  • promise.all中的执行顺序是怎么样的,Promise的执行顺序是从被创建开始的,也就是在调用all的时候,==所有的promise都已经开始执行==了,all方法只是等到==所有的对象都执行完成==,才会吧结果==传递给then==。
  • all中的promise,如果有一个状态变成了reject那么转换后的Promise字节变成reject,错误信息传递哥catch,不会传递给then。(但是并不是说all这里面刚开始执行成功的操作就不算数了)

Promise在开发中的应用

项目开发中promise的应用代码:

Promise.all([
            self.count({phoneNumber: mobile, createdOn: {$gt: hour}}),
            self.count({ip: ip, createdOn: {$gt: hour}})
        ]).then(function (results) {
            if (results[0] >= 5) {
                return callback({code: -1, message: '短信发送频率过快,每手机号1小时内只能发送5次'});
            }
            if (results[1] >= 5) {
                return callback({code: -1, message: '短信发送频率过快,每IP1小时内只能发送5次'});
            }
            let code = {
                phoneNumber: mobile,
                code: tool.makeRandomStr(4, 1).toLowerCase(),
                createdOn: new Date(),
                expiredOn: new Date(new Date().getTime() + (20 * 60 * 1000)),			//20分钟失效
                ip: ip,
                isUsed: false
            };
            self.create(code, function (err, newCode) {
                if (newCode) {
                    sms.sendSMS(mobile, newCode.code, 'ali', function (err, body) {
                        console.log(body);
                        if (err)
                            console.log("短信验证码发送失败:", err);
                    });
                    callback({code: 0, message: "验证码已经发送"});
                } else {
                    callback({code: -1, message: "验证码发送失败,请重试"});
                }
            })
        })

项目开发过程中使用promise.all的代码,当时是为了实现短信验证码发送前的校验功能。
all中的两个promise,第一个是统计时间内该手机号发送验证码数量;第二个是统计时间内该ip发送验证码的数量。

Promise使用过程中注意事项与误区

注意事项在上面原理讲解过程中,基本都提到过,只是重要的事情多说两遍。

  • 状态不可逆性
  • resolve函数和reject函数只是传递异步结果
  • then进行层级调用的时候,每次的返回值都一个空promise对象,如果想继续使用,赋值替换掉空promise对象,但是返回的时候return关键字很重要,不要忘了。
  • promise.all中的执行顺序是并行的,但是会等全部完成的结果传递给then
  • ==执行顺序==,promise是then方法调用之后才会执行吗?还是从创建那一刻就开始执行?
    promise从创建那一刻就开始执行,只是把结果传递给了then,then与promise的执行无关。

Promise的反思

Promise的讲解就到这里,但是大家在开发过程中,会发现有些时候多次操作异步会出现很多层级的调用,也就是

promise.then(...)

.then(...)

.then(...)

这种情况,代码虽然看起来会比callback的回调简介和规范了很多,但是还是感觉一些复杂,有没有更好的解决办法呢?请看下一篇博客

回调的终极使用--async和await的讲解

描述一下css盒模型

css盒模型

基本概念

就是来装页面上的元素的矩形区域。CSS中的盒子模型包括IE盒子模型和标准的W3C盒子模型。

  • 标准盒模型
    image

从上图可以看到标准 W3C 盒子模型的范围包括 margin、border、padding、content,并且 content 部分不包含其他部分。

  • IE盒子模型
    图片

从上图可以看到IE盒子模型的范围也包括 margin、border、padding、content, 与标准W3C盒子模型不同的是: IE盒子模型的content部分包含了 border 和 padding

在CSS3中引入了box-sizing属性,box-sizing:content-box;表示标准的盒子模型,box-sizing:border-box表示的是IE盒子模型

最后,前面我们还提到了,box-sizing:padding-box,这个属性值的宽度包含了左右padding+width

也很好理解性记忆,包含什么,width就从什么开始算起。

如何写优雅的SQL原生语句?

前言:

上一篇讲Mysql基本架构时,以“sql查询语句在MySql架构中具体是怎么执行的?”进行了全面的讲解。知道了sql查询语句在MySql架构中的具体执行流程,但是为了能够更好更快的写出sql语句,我觉得非常有必要知道sql语句中各子句的执行顺序。看过上一篇文章的小伙伴应该都知道,sql语句最后各子句的执行应该是在执行器中完成的,存储引擎对执行器提供的数据读写接口。现在开始我们的学习

语句中各子句完整执行顺序概括(按照顺序号执行)

  1. from (注:这里也包括from中的子语句)
  2. join
  3. on
  4. where
  5. group by(开始使用select中的别名,后面的语句中都可以使用)
  6. avg,sum.... 等聚合函数
  7. having
  8. select
  9. distinct
  10. order by
  11. limit

每个子句执行顺序分析

所有的 查询语句都是从from开始执行的,在执行过程中,每个步骤都会为下一个步骤生成一个虚拟表,这个虚拟表将作为下一个执行步骤的输入。

1. from

form是一次查询语句的开端。

  • 如果是一张表,会直接操作这张表;
  • 如果这个from后面是一个子查询,会先执行子查询中的内容,子查询的结果也就是第一个虚拟表T1。(注意:子查询中的执行流程也是按照本篇文章讲的顺序哦)。
  • 如果需要关联表,使用join,请看2,3

2. join

如果from后面是多张表,join关联,会首先对前两个表执行一个笛卡尔乘积,这时候就会生成第一个虚拟表T1(注意:这里会选择相对小的表作为基础表);

3. on

对虚表T1进行ON筛选,只有那些符合的行才会被记录在虚表T2中。(注意,这里的这里如果还有第三个表与之关联,会用T2与第三个表进行笛卡尔乘积生产T3表,继续重复3. on步骤生成T4表,不过下面的顺序讲解暂时不针对这里的T3和T4,只是从一个表关联查询T2继续说)

4. where

对虚拟表T2进行WHERE条件过滤。只有符合的记录才会被插入到虚拟表T3中。

5.group by

group by 子句将中的唯一的值组合成为一组,得到虚拟表T4。如果应用了group by,那么后面的所有步骤都只能操作T4的列或者是执行6.聚合函数(count、sum、avg等)。(注意:原因在于分组后最终的结果集中只包含每个组中的一行。谨记,不然这里会出现很多问题,下面的代码误区会特别说。)

6. avg,sum.... 等聚合函数

聚合函数只是对分组的结果进行一些处理,拿到某些想要的聚合值,例如求和,统计数量等,并不生成虚拟表。

7. having

应用having筛选器,生成T5。HAVING子句主要和GROUP BY子句配合使用,having筛选器是第一个也是为唯一一个应用到已分组数据的筛选器。

8. select

执行select操作,选择指定的列,插入到虚拟表T6中。

9. distinct

对T6中的记录进行去重。移除相同的行,产生虚拟表T7.(注意:事实上如果应用了group by子句那么distinct是多余的,原因同样在于,分组的时候是将列中唯一的值分成一组,同时只为每一组返回一行记录,那么所以的记录都将是不相同的。 )

10. order by

应用order by子句。按照order_by_condition排序T7,此时返回的一个游标,而不是虚拟表。sql是基于集合的理论的,集合不会预先对他的行排序,它只是成员的逻辑集合,成员的顺序是无关紧要的。对表进行排序的查询可以返回一个对象,这个对象包含特定的物理顺序的逻辑组织。这个对象就叫游标。
oder by的几点说明

  • 因为order by返回值是游标,那么使用order by 子句查询不能应用于表表达式。
  • order by排序是很需要成本的,除非你必须要排序,否则最好不要指定order by,
  • order by的两个参数 asc(升序排列) desc(降序排列)

11. limit

取出指定行的记录,产生虚拟表T9, 并将结果返回。

limit后面的参数可以是 一个limit m ,也可以是limit m n,表示从第m条到第n条数据。

(注意:很多开发人员喜欢使用该语句来解决分页问题。对于小数据,使用LIMIT子句没有任何问题,当数据量非常大的时候,使用LIMIT n, m是非常低效的。因为LIMIT的机制是每次都是从头开始扫描,如果需要从第60万行开始,读取3条数据,就需要先扫描定位到60万行,然后再进行读取,而扫描的过程是一个非常低效的过程。所以,对于大数据处理时,是非常有必要在应用层建立一定的缓存机制)

开发某需求写的一段sql

SELECT `userspk`.`avatar` AS `user_avatar`, 
`a`.`user_id`, 
`a`.`answer_record`, 
 MAX(`score`) AS `score`
FROM (select * from pkrecord  order by score desc) as a 
INNER JOIN `userspk` AS `userspk` 
ON `a`.`user_id` = `userspk`.`user_id`
WHERE `a`.`status` = 1 
AND `a`.`user_id` != 'm_6da5d9e0-4629-11e9-b5f7-694ced396953' 
GROUP BY `user_id`
ORDER BY `a`.`score` DESC 
LIMIT 9;

查询结果:

  • 先简要说一下我要查询的内容:

想要查询pk记录表中分数最高的9个用户记录和他们的头像。

  • 通过这段sql实际想一遍sql各字句的执行顺序

pk记录表的数据结构设计,每个用户每天每个馆下可能会有多条记录,所以需要进行分组,并且查询结果只想拿到每个分组内最高的那条记录

这段sql的一些说明:

  1. 可能有些同学会认为子查询没有必要
    直接查询pk记录表就可以,但是并不能拿到预期的结果,因为分组后的每个组结果是不进行排序的,而且max拿到的最高分数肯定是对应的该分组下最高分数,但是其它记录可能就不是最高分数对应的那条记录。所以子查询非常有必要,它能够对原始的数据首先进行排序,分数最高的那条就是第一条对应的第一条记录。

看一下代码和执行结果与带有子查询的进行比较,就能理解我上面说的一段话:

//不使用子查询
SELECT `userspk`.`avatar` AS `user_avatar`, 
`pkrecord`.`user_id`, 
`pkrecord`.`answer_record`, 
`pkrecord`.`id`, 
 MAX(`score`) AS `score`
FROM pkrecord
INNER JOIN `userspk` AS `userspk` 
ON `pkrecord`.`user_id` = `userspk`.`user_id`
WHERE `pkrecord`.`status` = 1 
AND `pkrecord`.`user_id` != 'm_6da5d9e0-4629-11e9-b5f7-694ced396953' 
GROUP BY `user_id`
ORDER BY `pkrecord`.`score` DESC 
LIMIT 9;

查询结果

  1. 在子查询中对数据已经进行排序后,外层排序方式如果和子查询排序分数相同,都是分数倒序,外层的排序可以去掉,没有必要写两遍。

sql语句中的别名

别名在哪些情况使用

在 SQL 语句中,可以为表名称及字段(列)名称指定别名

  • 表名称指定别名

同时查询两张表的数据的时候:
未设置别名前:

SELECT article.title,article.content,user.username FROM article, user

WHERE article.aid=1 AND article.uid=user.uid

设置别名后:

SELECT a.title,a.content,u.username FROM article AS a, user AS u where a.aid=1 and a.uid=u.uid

好处:使用表别名查询,可以使 SQL 变得简洁而更易书写和阅读,尤其在 SQL 比较复杂的情况下

  • 查询字段指定别名

查询一张表,直接对查询字段设置别名

SELECT username AS name,email FROM user

查询两张表

好处:字段别名一个明显的效果是可以自定义查询数据返回的字段名;当两张表有相同的字段需要都被查询出,使用别名可以完美的进行区分,避免冲突

SELECT a.title AS atitle,u.username,u.title AS utitle FROM article AS a, user AS u where a.uid=u.uid
  • 关联查询时候,关联表自身的时候,一些分类表,必须使用别名。

  • 别名也可以在group by与having的时候都可使用

  • 别名可以在order by排序的时候被使用

    查看上面一段sql

  • delete , update MySQL都可以使用别名,别名在多表(级联)删除尤为有用

delete t1,t2 from t_a t1 , t_b t2 where t1.id = t2.id
  • 子查询结果需要使用别名

    查看上面一段sql

别名使用注意事项

  • 虽然定义字段别名的 AS 关键字可以省略,但是在使用别名时候,建议不要省略 AS 关键字

书写sql语句的注意事项

书写规范上的注意

  • 字符串类型的要加单引号
  • select后面的每个字段要用逗号分隔,但是最后连着from的字段不要加逗号
  • 使用子查询创建临时表的时候要使用别名,否则会报错。

为了增强性能的注意

  • 不要使用“select * from ……”返回所有列,只检索需要的列,可避免后续因表结构变化导致的不必要的程序修改,还可降低额外消耗的资源
  • 不要检索已知的列
select  user_id,name from User where user_id = ‘10000050’
  • 使用可参数化的搜索条件,如=, >, >=, <, <=, between, in, is null以及like ‘%’;尽量不要使用非参数化的负向查询,这将导致无法使用索引,如<>, !=, !>, !<, not in, not like, not exists, not between, is not null, like ‘%’
  • 当需要验证是否有符合条件的记录时,使用exists,不要使用count(*),前者在第一个匹配记录处返回,后者需要遍历所有匹配记录
  • Where子句中列的顺序与需使用的索引顺序保持一致,不是所有数据库的优化器都能对此顺序进行优化,保持良好编程习惯(索引相关)
  • 不要在where子句中对字段进行运算或函数(索引相关)
  1. 如where amount / 2 > 100,即使amount字段有索引,也无法使用,改成where amount > 100 * 2就可使用amount列上的索引
  2. 如where substring( Lastname, 1, 1) = ‘F’就无法使用Lastname列上的索引,而where Lastname like ‘F%’或者where Lastname >= ‘F’ and Lastname < ‘G’就可以
  • 在有min、max、distinct、order by、group by操作的列上建索引,避免额外的排序开销(索引相关)

  • 小心使用or操作,and操作中任何一个子句可使用索引都会提高查询性能,但是or条件中任何一个不能使用索引,都将导致查询性能下降,如where member_no = 1 or provider_no = 1,在member_no或provider_no任何一个字段上没有索引,都将导致表扫描或聚簇索引扫描(索引相关)

  • Between一般比in/or高效得多,如果能在between和in/or条件中选择,那么始终选择between条件,并用>=和<=条件组合替代between子句,因为不是所有数据库的优化器都能把between子句改写为>=和<=条件组合,如果不能改写将导致无法使用索引(索引相关)

  • 调整join操作顺序以使性能最优,join操作是自顶向下的,尽量把结果集小的两个表关联放在前面,可提高性能。(join相关)
    注意:索引和关联我会单独拿出来两篇文章进行详细讲解,在这个注意事项中只是简单提一下。

sequelize如何使用原生语句

前言:

最近写的一个接口需要用到子查询,并且子查询的结果并不是可以在另外一个查询语句中直接当作in的选项,没办法区sequelize官网上看了一下如何直接使用原生语句

sequelize文档内容翻译

看一段官网中代码例子,一一说明

//1
sequelize.query('SELECT 1', {
  logging: console.log,
  
  plain: false,
 
  raw: false,

  type: Sequelize.QueryTypes.SELECT
})

//2
sequelize
  .query('SELECT * FROM projects', { raw: true })
  .then(projects => {
    console.log(projects)
  })
//3
sequelize.query('SELECT * FROM projects WHERE status = ?',
  { replacements: ['active'], type: sequelize.QueryTypes.SELECT }
).then(projects => {
  console.log(projects)
})
//4
sequelize.query('SELECT * FROM projects WHERE status = :status ',
  { replacements: { status: 'active' }, type: sequelize.QueryTypes.SELECT }
).then(projects => {
  console.log(projects)
})
  • sequelize中提供了query函数,用于直接操作原生语句

  • 该函数将返回两个参数 - 结果数组和包含元数据的对象,对于mysql将是返一对象的两个引用。

  • query函数的第二个参数,是一个对象,对象里面几个常用参数进行说明。

    1. pain:如果plain为真,那么sequelize只返回第一个
      记录结果集。如果为false,则返回所有记录。
    2. type:正在执行的查询的类型(也可以is hi更新哦,具体哪些去看官网api)。查询类型影响返回结果之前的格式化方式。
    3. raw:查询对类型是否有模型定义,如果您的查询没有模型定义,请将此设置为true。
    4. logging: console.log记录查询的函数
      是否会为发送的每个SQL查询调用
      到服务器。
  • 对于查找条件where后面的字段

    1. 如果传递数组,?将按它们在数组中出现的顺序进行替换。
    2. 如果传递了一个对象,:key则将替换该对象中的键。如果对象包含查询中未找到的键,会抛出查询异常。
  • 对于替换where后面的变量,也可以使用in关键字从数组匹配,也可以使用通配符like%等
    代码如下:

//in关键字使用官网例子
sequelize.query('SELECT * FROM projects WHERE status IN(:status) ',
  { replacements: { status: ['active', 'inactive'] }, type: sequelize.QueryTypes.SELECT }
).then(projects => {
  console.log(projects)
})

//like通配符关键字使用官网例子
sequelize.query('SELECT * FROM users WHERE name LIKE :search_name ',
  { replacements: { search_name: 'ben%'  }, type: sequelize.QueryTypes.SELECT }
).then(projects => {
  console.log(projects)
})
  • 查询的时候还可以直接传递model,如果传递模型,则返回的数据将是该模型的实例,上面其它字段也可以在这使用
sequelize
  .query('SELECT * FROM projects', {
    model: Projects,
    mapToModel: true // 如果有任何映射字段,则在这里传递true
  })
  .then(projects => {
    // Each record will now be an instance of Project
  })

sequelize原生语句具体使用实例

看一段我在开发过程中怎么使用的原生语句查询
代码如下:

let sqlRank = `SELECT userspk.avatar AS user_avatar, 
        userspk.gender AS user_gender, 
        userspk.nickname AS user_nickname,
        a.id AS pk_record_id,
        a.user_id, 
        a.answer_record, 
        a.pk_type, 
         MAX(score) AS score, 
        a.create_time
        FROM (select * from pkrecord  order by score desc,create_time asc) as a 
        INNER JOIN userspk AS userspk 
        ON a.user_id = userspk.user_id
        WHERE a.status = 1 
        AND a.pk_type = 'noreal' 
        AND a.subject_id = :subject_id
        GROUP BY user_id
        ORDER BY a.score DESC 
        LIMIT 3;`
let pkRankResult= await ctx.main.query(sqlRank,  {
    replacements: {
        subject_id: subject_id,
    },
    type: Sequelize.QueryTypes.SELECT }
);

注意 replacements中的subject_id变量是从前端请求获取的参数值,是一个变量哦

附件:

官网中原生语句查询地址:
http://docs.sequelizejs.com/manual/raw-queries.html

公司要求会使用框架vue,面试题会被问及哪些?

如果你是一个已经在学习前端开发的初学者亦或者是一名在代码界纵横多年的程序员,那你一定知道现在最火的前端框架之一Vue.js。它相比于React与Angular上手更加容易,或许这也是很多初学者选择vue的原因之一。
在这里插入图片描述
我们看到很多招聘上都写着掌握vue开发项目,那么面试都会问什么呢?别急,下面是我给大家整理了一份比较全面的Vue面试高频考题解析小册。
在这里插入图片描述

对于MVVM的理解

MVVM 是 Model-View-ViewModel 的缩写
Model: 代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。我们可以把Model称为数据层,因为它仅仅关注数据本身,不关心任何行为
View: 用户操作界面。当ViewModel对Model进行更新的时候,会通过数据绑定更新到View
ViewModel: 业务逻辑层,View需要什么数据,ViewModel要提供这个数据;View有某些操作,ViewModel就要响应这些操作,所以可以说它是Model for View.
总结: MVVM模式简化了界面与业务的依赖,解决了数据频繁更新。MVVM 在使用当中,利用双向绑定技术,使得 Model 变化时,ViewModel 会自动更新,而 ViewModel 变化时,View 也会自动变化。

开发中常用的指令有哪些

  • v-model :一般用在表达输入,很轻松的实现表单控件和数据的双向绑定
  • v-html: 更新元素的 innerHTML
  • v-showv-if: 条件渲染, 注意二者区别

使用了v-if的时候,如果值为false,那么页面将不会有这个html标签生成。
v-show则是不管值为true还是false,html元素都会存在,只是CSS中的display显示或隐藏

  • v-on : click: 可以简写为@click,@绑定一个事件。如果事件触发了,就可以指定事件的处理函数
  • v-for:基于源数据多次渲染元素或模板块
  • v-bind: 当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM

语法:v-bind:title="msg"
简写::title="msg"

请详细说下你对vue生命周期的理解

vue生命周期总共分为8个阶段: 创建前/后,载入前/后,更新前/后, 销毁前/后。

  • beforeCreate (创建前)vue实例的挂载元素$el和数据对象 data都是undefined, 还未初始化
  • created (创建后) 完成了 data数据初始化, el还未初始化
  • beforeMount (载入前) vue实例的$eldata都初始化了, 相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上。
  • mounted (载入后) 在el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互
  • beforeUpdate (更新前) 在数据更新之前调用,发生在虚拟DOM重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。
  • updated (更新后) 在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
  • beforeDestroy (销毁前) 在实例销毁之前调用。实例仍然完全可用。
  • destroyed (销毁后) 在实例销毁之后调用。调用后,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。

Vue的双向数据绑定原理是什么

vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
具体实现步骤,感兴趣的可以看看:

  1. 当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty 都加上 setter和getter 这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
  2. compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
  3. Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
    1、在自身实例化时往属性订阅器(dep)里面添加自己
    2、自身必须有一个update()方法
    3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
  4. MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果
//vue实现数据双向绑定的原理就是用Object.defineproperty()重新定义(set方法)对象设置属性值和(get方法)获取属性值的操纵来实现的。
//Object.property()方法的解释:Object.property(参数1,参数2,参数3)  返回值为该对象obj
//其中参数1为该对象(obj),参数2为要定义或修改的对象的属性名,参数3为属性描述符,属性描述符是一个对象,主要有两种形式:数据描述符和存取描述符。这两种对象只能选择一种使用,不能混合使用。而get和set属于存取描述符对象的属性。
//这个方法会直接在一个对象上定义一个新属性或者修改对象上的现有属性,并返回该对象。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
</head>
<body>
    <div id="myapp">
        <input v-model="message" /><br>
        <span v-bind="message"></span>
    </div>
    <script type="text/javascript">
        var model = {
            message: ""
        };
        var models = myapp.querySelectorAll("[v-model=message]");
        for (var i = 0; i < models.length; i++) {
            models[i].onkeyup = function() {
                model[this.getAttribute("v-model")] = this.value;
            }
        }
        // 观察者模式 / 钩子函数
        // defineProperty 来定义一个对象的某个属性
        Object.defineProperty(model, "message", {
            set: function(newValue) {
                var binds = myapp.querySelectorAll("[v-bind=message]");
                for (var i = 0; i < binds.length; i++) {
                    binds[i].innerHTML = newValue;
                };
                var models = myapp.querySelectorAll("[v-model=message]");
                for (var i = 0; i < models.length; i++) {
                    models[i].value = newValue;
                };
                this.value = newValue;
            },
            get: function() {
                return this.value;
            }
        })
</script>
</body>
</html>

Proxy 相比于 defineProperty 的优势

Object.defineProperty() 的问题主要有三个:

  • 不能监听数组的变化
  • 必须遍历对象的每个属性
  • 必须深层遍历嵌套的对象

Proxy 在 ES2015 规范中被正式加入,它有以下几个特点:

  • 针对对象:针对整个对象,而不是对象的某个属性,所以也就不需要对 keys 进行遍历。这解决了上述 Object.defineProperty() 第二个问题
  • 支持数组:Proxy 不需要对数组的方法进行重载,省去了众多 hack,减少代码量等于减少了维护成本,而且标准的就是最好的。

除了上述两点之外,Proxy 还拥有以下优势:

  • Proxy 的第二个参数可以有 13 种拦截方法,这比起 Object.defineProperty() 要更加丰富
  • Proxy 作为新标准受到浏览器厂商的重点关注和性能优化,相比之下 Object.defineProperty() 是一个已有的老方法。

vue-router 有哪几种导航守卫?

  • 全局守卫
  • 路由独享守卫
  • 路由组件内的守卫

1.全局守卫

vue-router全局有三个守卫:

  1. router.beforeEach 全局前置守卫 进入路由之前
  2. router.beforeResolve 全局解析守卫(2.5.0+) 在beforeRouteEnter调用之后调用
  3. router.afterEach 全局后置钩子 进入路由之后

使用方法:

 // main.js 入口文件
    import router from './router'; // 引入路由
    router.beforeEach((to, from, next) => { 
      next();
    });
    router.beforeResolve((to, from, next) => {
      next();
    });
    router.afterEach((to, from) => {
      console.log('afterEach 全局后置钩子');
    });

2.路由独享守卫

如果你不想全局配置守卫的话,你可以为某些路由单独配置守卫:

const router = new VueRouter({
      routes: [
        {
          path: '/foo',
          component: Foo,
          beforeEnter: (to, from, next) => { 
            // 参数用法什么的都一样,调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖
            // ...
          }
        }
      ]
    })

3.路由组件内的守卫

  1. beforeRouteEnter 进入路由前, 在路由独享守卫后调用 不能 获取组件实例 this,组件实例还没被创建
  2. beforeRouteUpdate (2.2) 路由复用同一个组件时, 在当前路由改变,但是该组件被复用时调用 可以访问组件实例 this
  3. beforeRouteLeave 离开当前路由时, 导航离开该组件的对应路由时调用,可以访问组件实例 this

Vue的路由实现:hash模式 和 history模式

hash模式:

在浏览器中符号“#”,#以及#后面的字符称之为hash,用window.location.hash读取;
特点:hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。
hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 http://www.xiaogangzai.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。

history模式:

history采用HTML5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。
history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 http://www.xxx.com/items/id。后端如果缺少对 /items/id 的路由处理,将返回 404 错误。Vue-Router 官网里如此描述:“不过这种模式要玩好,还需要后台配置支持……所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。”

组件之间的传值通信

组件之间通讯分为三种: 父传子、子传父、兄弟组件之间的通讯

1. 父组件给子组件传值

使用props,父组件可以使用props向子组件传递数据。

父组件vue模板father.vue:

<template>
    <child :msg="message"></child>
</template>

<script>
import child from './child.vue';
export default {
    components: {
        child
    },
    data () {
        return {
            message: 'father message';
        }
    }
}
</script>

子组件vue模板child.vue:

<template>
    <div>{{msg}}</div>
</template>

<script>
export default {
    props: {
        msg: {
            type: String,
            required: true
        }
    }
}
</script>

2. 子组件向父组件通信

父组件向子组件传递事件方法,子组件通过$emit触发事件,回调给父组件。

父组件vue模板father.vue:

<template>
    <child @msgFunc="func"></child>
</template>

<script>
import child from './child.vue';
export default {
    components: {
        child
    },
    methods: {
        func (msg) {
            console.log(msg);
        }
    }
}
</script>

子组件vue模板child.vue:

<template>
    <button @click="handleClick">点我</button>
</template>

<script>
export default {
    props: {
        msg: {
            type: String,
            required: true
        }
    },
    methods () {
        handleClick () {
            //........
            this.$emit('msgFunc');
        }
    }
}
</script>

3. 非父子, 兄弟组件之间通信

vue2中废弃了$dispatch和$broadcast广播和分发事件的方法。父子组件中可以用props和$emit()。如何实现非父子组件间的通信,可以通过实例一个vue实例Bus作为媒介,要相互通信的兄弟组件之中,都引入Bus,然后通过分别调用Bus事件触发和监听来实现通信和参数传递。
Bus.js可以是这样:

import Vue from 'vue'
export default new Vue()

在需要通信的组件都引入Bus.js:

<template>
	<button @click="toBus">子组件传给兄弟组件</button>
</template>

<script>
import Bus from '../common/js/bus.js'
export default{
	methods: {
	    toBus () {
	        Bus.$emit('on', '来自兄弟组件')
	    }
	  }
}
</script>

另一个组件也import Bus.js 在钩子函数中监听on事件

import Bus from '../common/js/bus.js'
export default {
    data() {
      return {
        message: ''
      }
    },
    mounted() {
       Bus.$on('on', (msg) => {
         this.message = msg
       })
     }
   }

Vue与Angular以及React的区别?

版本在不断更新,以下的区别有可能不是很正确。而且工作中只用到vue,对angular和react不怎么熟

Vue与AngularJS的区别

  • Angular采用TypeScript开发, 而Vue可以使用javascript也可以使用TypeScript
  • AngularJS依赖对数据做脏检查,所以Watcher越多越慢;Vue.js使用基于依赖追踪的观察并且使用异步队列更新,所有的数据都是独立触发的。
  • AngularJS社区完善, Vue的学习成本较小

Vue与React的区别

  • vue组件分为全局注册和局部注册,在react中都是通过import相应组件,然后模版中引用;
  • props是可以动态变化的,子组件也实时更新,在react中官方建议props要像纯函数那样,输入输出一致对应,而且不太建议通过props来更改视图;
  • 子组件一般要显示地调用props选项来声明它期待获得的数据。而在react中不必需,另两者都有props校验机制;
  • 每个Vue实例都实现了事件接口,方便父子组件通信,小型项目中不需要引入状态管理机制,而react必需自己实现;
  • 使用插槽分发内容,使得可以混合父组件的内容与子组件自己的模板;
  • 多了指令系统,让模版可以实现更丰富的功能,而React只能使用JSX语法;
  • Vue增加的语法糖computed和watch,而在React中需要自己写一套逻辑来实现;
  • react的思路是all in js,通过js来生成html,所以设计了jsx,还有通过js来操作css,社区的styled-component、jss等;而 vue是把html,css,js组合到一起,用各自的处理方式,vue有单文件组件,可以把html、css、js写到一个文件中,html提供了模板引擎来处理。
  • react做的事情很少,很多都交给社区去做,vue很多东西都是内置的,写起来确实方便一些, 比如 redux的combineReducer就对应vuex的modules, 比如reselect就对应vuex的getter和vue组件的computed, vuex的mutation是直接改变的原始数据,而redux的reducer是返回一个全新的state,所以redux结合immutable来优化性能,vue不需要。
  • react是整体的思路的就是函数式,所以推崇纯组件,数据不可变,单向数据流,当然需要双向的地方也可以做到,比如结合redux-form,组件的横向拆分一般是通过高阶组件。而vue是数据可变的,双向绑定,声明式的写法,vue组件的横向拆分很多情况下用mixin。

vuex是什么?怎么使用?哪种功能场景使用它?

  1. vuex 就是一个仓库,仓库里放了很多对象。其中 state 就是数据源存放地,对应于一般 vue 对象里面的 data
  2. state 里面存放的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据发生改变,依赖这相数据的组件也会发生更新
  3. 它通过 mapState 把全局的 state 和 getters 映射到当前组件的 computed 计算属性

vuex的使用借助官方提供的一张图来说明:
在这里插入图片描述
Vuex有5种属性: 分别是 state、getter、mutation、action、module;

state

Vuex 使用单一状态树,即每个应用将仅仅包含一个store 实例,但单一状态树和模块化并不冲突。存放的数据状态,不可以直接修改里面的数据。

mutations

mutations定义的方法动态修改Vuex 的 store 中的状态或数据。

getters

类似vue的计算属性,主要用来过滤一些数据。

action

actions可以理解为通过将mutations里面处里数据的方法变成可异步的处理数据的方法,简单的说就是异步操作数据。view 层通过 store.dispath 来分发 action。

vuex 一般用于中大型 web 单页应用中对应用的状态进行管理,对于一些组件间关系较为简单的小型应用,使用 vuex 的必要性不是很大,因为完全可以用组件 prop 属性或者事件来完成父子组件之间的通信,vuex 更多地用于解决跨组件通信以及作为数据中心集中式存储数据。

  • 使用Vuex解决非父子组件之间通信问题
    vuex 是通过将 state 作为数据中心、各个组件共享 state 实现跨组件通信的,此时的数据完全独立于组件,因此将组件间共享的数据置于 State 中能有效解决多层级组件嵌套的跨组件通信问题。

  • vuex 作为数据存储中心
    vuex 的 State 在单页应用的开发中本身具有一个“数据库”的作用,可以将组件中用到的数据存储在 State 中,并在 Action 中封装数据读写的逻辑。这时候存在一个问题,一般什么样的数据会放在 State 中呢? 目前主要有两种数据会使用 vuex 进行管理:
    1、组件之间全局共享的数据
    2、通过后端异步请求的数据
    比如做加入购物车、登录状态等都可以使用Vuex来管理数据状态。

一般面试官问到这里vue基本知识就差不多了, 如果更深入的研究就是和你探讨关于vue的底层源码;或者是具体在项目中遇到的问题,下面列举几个项目中可能遇到的问题:

  1. 开发时,改变数组或者对象的数据,但是页面没有更新如何解决?

  2. vue弹窗后如何禁止滚动条滚动?

  3. 如何在 vue 项目里正确地引用 jquery 和 jquery-ui的插件

公众号技术栈路线

大家好,我是koala,公众号「程序员成长指北」作者,这篇文章是【JS必知必会系列】的高阶函数讲解。目前在做一个node后端工程师进阶路线,加入我们一起学习吧!

加入我们

​搞不懂JS中赋值·浅拷贝·深拷贝的请看这里

前言

为什么写拷贝这篇文章?同事有一天提到了拷贝,他说赋值就是一种浅拷贝方式,另一个同事说赋值和浅拷贝并不相同。
我也有些疑惑,于是我去MDN搜一下拷贝相关内容,发现并没有关于拷贝的实质概念,没有办法只能通过实践了,同时去看一些前辈们的文章总结了这篇关于拷贝的内容,本文也属于公众号【程序员成长指北】学习路线中【JS必知必会】内容。

数据类型与堆栈的关系

基本类型与引用类型

  • 基本类型:undefined,null,Boolean,String,Number,Symbol

  • 引用类型:Object,Array,Date,Function,RegExp等

存储方式

  • 基本类型:基本类型值在内存中占据固定大小,保存在栈内存中(不包含闭包中的变量)

  • 引用类型:引用类型的值是对象,保存在堆内存中。而栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址(引用),引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

注意:

  1. 闭包中的变量并不保存在栈内存中,而是保存在堆内存中。这一点比较好想,如果闭包中的变量保存在了栈内存中,随着外层中的函数从调用栈中销毁,变量肯定也会被销毁,但是如果保存在了堆内存中,内存函数仍能访问外层已销毁函数中的变量。看一段对应代码理解下:
function A() {
  let a = 'koala'
  function B() {
      console.log(a)
  }
  return B
}
  1. 本篇所讲的浅拷贝和深拷贝都是对于引用类型的,对于基础类型不会有这种操作。

赋值操作

基本数据类型复制

看一段代码

let a ='koala';
let b = a;
b='程序员成长指北'
console.log(a); // koala

基本数据类型复制配图:

结论:在栈内存中的数据发生数据变化的时候,系统会自动为新的变量分配一个新的之值在栈内存中,两个变量相互独立,互不影响的。

引用数据类型复制

看一段代码

let a = {x:'kaola', y:'kaola1'}
let b = a;
b.x = '程序员成长指北';
console.log(a.x); // 程序员成长指北

引用数据类型复制配图:


结论:引用类型的复制,同样为新的变量b分配一个新的值,报错在栈内存中,不同的是这个变量对应的具体值不在栈中,栈中只是一个地址指针。两个变量地址指针相同,指向堆内存中的对象,因此b.x发生改变的时候,a.x也发生了改变。

浅拷贝

浅拷贝定义:

不知道的api我一般比较喜欢看MDN,浅拷贝的概念MDN官方并没有给出明确定义,但是搜到了一个函数Array.prototype.slice,官方说它可以实现原数组的浅拷贝。
对于官方给的结论,我们通过两段代码验证一下,并总结出浅拷贝的定义。

  • 第一段代码:
var a = [ 1, 3, 5, { x: 1 } ];
var b = Array.prototype.slice.call(a);
b[0] = 2;
console.log(a); // [ 1, 3, 5, { x: 1 } ];
console.log(b); // [ 2, 3, 5, { x: 1 } ];

从输出结果可以看出,浅拷贝后,数组a[0]并不会随着b[0]改变而改变,说明a和b在栈内存中引用地址并不相同。

  • 第二段代码
var a = [ 1, 3, 5, { x: 1 } ];
var b = Array.prototype.slice.call(a);
b[3].x = 2;
console.log(a); // [ 1, 3, 5, { x: 2 } ];
console.log(b); // [ 1, 3, 5, { x: 2 } ];

从输出结果可以看出,浅拷贝后,数组中对象的属性会根据修改而改变,说明浅拷贝的时候拷贝的已存在对象的对象的属性引用。

  • 浅拷贝定义

通过这个官方的slice浅拷贝函数分析浅拷贝定义

新的对象复制已有对象中非对象属性的值和对象属性的引用。如果这种说法不理解换一种一个新的对象直接拷贝已存在的对象的对象属性的引用,即浅拷贝。

浅拷贝实例

Object.assign

  • 语法:

语法:Object.assign(target, ...sources)

ES6中拷贝对象的方法,接受的第一个参数是拷贝的目标target,剩下的参数是拷贝的源对象sources(可以是多个)

  • 举例说明:
let target = {};
let source = {a:'koala',b:{name:'程序员成长指北'}};
Object.assign(target ,source);
console.log(target); // { a: 'koala', b: { name: '程序员成长指北' } }
source.a = 'smallKoala';
source.b.name = '程序员成长指北哦'
console.log(source); // { a: 'smallKoala', b: { name: '程序员成长指北哦' } }
console.log(target); // { a: 'koala', b: { name: '程序员成长指北哦' } }

从打印结果可以看出,Object.assign是一个浅拷贝,它只是在根属性(对象的第一层级)创建了一个新的对象,但是对于属性的值是对象的话只会拷贝一份相同的内存地址。

  • Object.assign注意事项
  1. 只拷贝源对象的自身属性(不拷贝继承属性)
  2. 它不会拷贝对象不可枚举的属性
  3. undefinednull无法转成对象,它们不能作为Object.assign参数,但是可以作为源对象
Object.assign(undefined) // 报错
Object.assign(null) // 报错

let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true
  1. 属性名为 Symbol 值的属性,可以被Object.assign拷贝。

Array.prototype.slice

这个函数在浅拷贝概念定义的时候已经进行了分析,看上文。

Array.prototype.concat

  • 语法

var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])
参数:将数组和/或值连接成新数组

  • 举例说明
let array = [{a: 1}, {b: 2}];
let array1 = [{c: 3},{d: 4}];
let array2=array.concat(array1);
array1[0].c=123;
console.log(array2);// [ { a: 1 }, { b: 2 }, { c: 123 }, { d: 4 } ]
console.log(array1);// [ { c: 123 }, { d: 4 } ]

Array.prototype.concat也是一个浅拷贝,只是在根属性(对象的第一层级)创建了一个新的对象,但是对于属性的值是对象的话只会拷贝一份相同的内存地址。

...扩展运算符

  • 语法

var cloneObj = { ...obj };

  • 举例说明
let obj = {a:1,b:{c:1}}
let obj2 = {...obj};
obj.a=2;
console.log(obj); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}

obj.b.c = 2;
console.log(obj); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}

扩展运算符也是浅拷贝,对于值是对象的属性无法完全拷贝成2个不同对象,但是如果属性都是基本类型的值的话,使用扩展运算符也是优势方便的地方。

补充说明:以上4中浅拷贝方式都不会改变原数组,只会返回一个浅拷贝了原数组中的元素的一个新数组。

自己实现一个浅拷贝

实现原理:新的对象复制已有对象中非对象属性的值和对象属性的引用,也就是说对象属性并不复制到内存。

  • 实现代码:
function cloneShallow(source) {
    var target = {};
    for (var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            target[key] = source[key];
        }
    }
    return target;
}
  • for in与hasOwnProperty函数说明,怕有些人小伙伴可能不清楚具体内容

for in

for...in语句以任意顺序遍历一个对象自有的、继承的、可枚举的、非Symbol的属性。对于每个不同的属性,语句都会被执行。

hasOwnProperty

语法:obj.hasOwnProperty(prop)
prop是要检测的属性字符串名称或者Symbol

该函数返回值为布尔值,所有继承了 Object 的对象都会继承到 hasOwnProperty 方法,和 in 运算符不同,该函数会忽略掉那些从原型链上继承到的属性和自身属性。

深拷贝操作

说了赋值操作和浅拷贝操作,大家是不是已经能想到什么是深拷贝了,下面直接说深拷贝的定义。

深拷贝定义

深拷贝会另外拷贝一份一个一模一样的对象,从堆内存中开辟一个新的区域存放新对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

深拷贝实例

JSON.parse(JSON.stringify())

JSON.stringify()是前端开发过程中比较常用的深拷贝方式。原理是把一个对象序列化成为一个JSON字符串,将对象的内容转换成字符串的形式再保存在磁盘上,再用JSON.parse()反序列化将JSON字符串变成一个新的对象

  • 举例说明:
let arr = [1, 3, {
    username: ' koala'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'smallKoala'; 
console.log(arr4);// [ 1, 3, { username: 'smallKoala' } ]
console.log(arr);// [ 1, 3, { username: ' koala' } ]

实现了深拷贝,当改变数组中对象的值时候,原数组中的内容并没有发生改变。JSON.stringify()虽然可以实现深拷贝,但是还有一些弊端比如不能处理函数等。

  • JSON.stringify()实现深拷贝注意点
  1. 拷贝的对象的值中如果有函数,undefined,symbol则经过JSON.stringify()序列化后的JSON字符串中这个键值对会消失
  2. 无法拷贝不可枚举的属性,无法拷贝对象的原型链
  3. 拷贝Date引用类型会变成字符串
  4. 拷贝RegExp引用类型会变成空对象
  5. 对象中含有NaN、Infinity和-Infinity,则序列化的结果会变成null
  6. 无法拷贝对象的循环应用(即obj[key] = obj)

自己实现一个简单深拷贝

深拷贝,主要用到的**是递归,遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。
实现代码:

    //定义检测数据类型的功能函数
    function isObject(obj) {
	    return typeof obj === 'object' && obj != null;
    }
   function cloneDeep(source) {

    if (!isObject(source)) return source; // 非对象返回自身
      
    var target = Array.isArray(source) ? [] : {};
    for(var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            if (isObject(source[key])) {
                target[key] = cloneDeep(source[key]); // 注意这里
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}

该简单深拷贝未考虑内容:
遇到循环引用,会陷入一个循环的递归过程,从而导致爆栈

// RangeError: Maximum call stack size exceeded

小伙伴们有没有什么好办法呢,可以写下代码在评论区一起讨论哦!

第三方深拷贝库

该函数库也有提供_.cloneDeep用来做 Deep Copy(lodash是一个不错的第三方开源库,有好多不错的函数,也可以看具体的实现源码)

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);
// false

拷贝内容总结

用一张图总结

今天就分享这么多,如果对分享的内容感兴趣,可以关注公众号「程序员成长指北」,或者加入技术交流群,大家一起讨论。

进阶技术路线


加入我们一起学习吧!

MySQL 基础架构你不知道的那些事!

提出问题:

对于一个做后台不久的我,起初做项目只是实现了功能,所谓的增删改查,和基本查询索引的建立。直到有一个面试官问我一个问题,一条sql查询语句在mysql数据库中具体是怎么执行的?我被虐了,很开心,感谢他。于是开始了深入学习mysql。本篇文章通过

一条sql查询语句在mysql数据库中具体是怎么执行的?

来具体讲解mysql的基础架构。

讲解

mysql> select * from Student where ID=1;

上面一条简单的查询语句很简单,但我想好多开发者并不知道在MYSQL内部的执行过程。

Mysql基本架构示意图


从图中可以看出Mysql可以大体分为Server层和存储引擎层两部分。

Server层包括连接器、查询缓存、分析器、优化器、执行器等,这些涵盖了MySQL的大多数核心服务和所有的内置函数(如日期、时间、数学和加密函数等),跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。

存储引擎层负责数据的存储和提取,提供数据的读写接口。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。目前开发中最常用的存储引擎是InnoDB,它从MySQL5.5.5版本开始成为默认存储引擎,不过开发者也可以通过指定存储引擎的类型来选择别的引擎。

即使存储引擎不同,但是也会共用一个Server层,接下来对Server层中的执行流程,依次对其作用进行讲解。

连接器

运行查询语句开始查询的前提是第一步先连接数据库,这时候等待你的就是连接器。连接器负责和客户端建立连接、获取权限、维持和管理连接。

常规的开发模式,客户端与服务器需要建立连接。二者在完成经典的TCP握手后,Server层连接器就要开始认证你的身份,这个时候是服务器端代码使用的用户名和密码。

连接器一些内容说明:

  • 连接时:如果用户名或密码不对,服务器端会收到“Access denied for user”的错误,客户端报错无法使用。
  • 连接时:如果用户名密码认证通过,连接器会到权限表中查出你拥有的权限。之后,通过本次连接查询到的权限进行各种逻辑判断,并且都将依赖于此次连接读到的权限(这里要注意也就是说一个数据库用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在的连接的权限。修改权限后,只有再建立新的连接才可以使用新设置的权限)
  • 连接完成后:如果一直没有对数据库有操作,则本次连接将处于空闲。show processlist可以查看所有的连接,其中Command列表示的是连接状态中Command列为“Sleep”则表示是一个空闲连接
mysql>show processlist
  • 连接断开相关:客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。
    如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒: Lost connection to MySQL server during query。这时候如果你要继续,就需要重连,然后再执行请求了。数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接。短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。

  • 建立连接的过程通常是比较复杂的,所以我建议你在使用中要尽量减少建立连接的动作,也就是尽量使用长连接。

较好的连接方式长连接产生的问题以及解决办法:

全部使用长连接后,你可能会发现,有些时候 MySQL 占用内存涨得特别快,这是因为 MySQL 在执行过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时候才释放。所以如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(OOM),从现象看就是 MySQL 异常重启了。怎么解决这个问题呢?你可以考虑以下两种方案。

  1. 定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。

  2. 如果你用的是 MySQL 5.7 或更新版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状

查询缓存

第一步连接建立完成后,就可以执行查询语句了。第二部:查询缓存。
Mysql确定了查询语句,会先到查询缓存中,看之前是否执行过这条查询语句。之前如果执行过这条查询语句,查询结果可能会以key-value的方式直接缓存在内存中。key是查询的语句,value是查询到的值,这样的话查询缓存会直接把value值返回给客户端。查询语句如果步子查询缓存中,会正常往下执行,获取到新的查询结果后会被存入到查询缓存中。

说明:

  • 大多数情况下并不建议使用查询缓存。查询缓存往往弊大于利。

    查询缓存的失效非常频繁,只要有对某个表的更新,该表的所有查询缓存都会被清空。所以很可能你费劲把结果存起来,还没有使用,就被一个更新全部清空了,尤其是对于更新压力大的数据库来说,查询缓存的命中率很低。但是也不是不能使用,假如一张静态表(系统配置表),很长时间更新一次,这种情况就比较适合使用查询缓存。

  • 如何设置Mysql不使用查询缓存

    将Mysql参数query_cache_type设置成DEMAND,这样默认的SQL语句都不使用查询缓存

  • 如何对某一条查询语句指定使用查询缓存
    确定使用查询缓存的语句,可以用SQL_CACHE显示指定

    mysql> select SQL_CACHE * from Student where ID=1;
    
    

注意:

Mysql 8.0版本直接将查询缓存对整块功能删除掉了,8.0之后将不再出现查询缓存。

分析器

如果在查询缓存中未找到缓存数据,就会开始真正的执行查询语句。Mysql需要直到这条查询语句要做什么?因此需要对SQL语句做解析。

解析流程:

  • 词法分析

    分析器首先会做词法分析,查询语句中包括了多个字符串和空格组成,Mysql需要识别出里面的字符串分别代表什么。

    mysql> select * from Student where ID=1;
    

    分析这条查询语句,"select"关键字可以识别出是一个查询语句。 字符串"Student"识别出是表名"Student",把字符串"ID"识别成列"ID"。

  • 语法分析

    词法分析后,语句法分析会根据语法规则,判断输入的SQL语句是否满足MySql语法。如果语法不对,会收到“You have an error in your SQL syntax”的错误提醒。例如

    mysql> elect * from Student where ID=1;
    
    ERROR 1064 (42000) You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'elect * from t where ID=1' at line 1
    

    技巧:一般语法错误看错误提示的时候,要关注的是紧接“use near”的内容

优化器

分析器执行之后,到达了优化器。

优化器会做那些优化处理:

  • 当在表中有多个索引的时候,优化器会决定这条查询语句使用哪个索引

  • 一个查询语句有多表关联(join)的时候,决定各个表的连接顺序。

    例子如下:

    mysql> select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20;
    
    1. 该例子既可以先从表t1里面取出c=10的记录的ID值,再根据ID值关联到表t2,再判断t2里面d的值是否等于20.
    2. 也可以先从表 t2 里面取出 d=20 的记录的 ID 值,再根据 ID 值关联到 t1,再判断 t1 里面 c 的值是否等于 10。

    两种关联查询方案结果肯定是一样的,但是执行效率会有不同,优化器就是决定选择使用哪一个方案。

执行器

MySQL 通过分析器知道了你要做什么,通过优化器知道了该怎么做(执行方案是什么?),于是就进入了执行器阶段,开始执行语句。

开始执行的时候,要先判断一下你对这个表 Student 有没有执行查询的权限,如果没有,就会返回没有权限的错误,如下所示 (在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。查询也会在优化器之前调用 precheck 验证权限)。

mysql> select * from Student where ID=10;
 
ERROR 1142 (42000): SELECT command denied to user 'b'@'localhost' for table 'Student'

如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。

存储引擎

来到存储引擎,执行存储引擎提供的数据读写接口。
这条查询语句,执行器(注意这里读写数据的还是存储引擎)读写数据的流程要分两种情况考虑:

  • 表 Student 中,ID字段没有索引,执行流程如下:

    调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是 1,如果不是则跳过,如果是则将这行存在结果集中;

    调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。

    执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。

    至此,这个语句就执行完成了。

  • 表 Student 中,ID字段有索引,那么执行器的执行流程是这样的:

    有索引的表,执行的逻辑也差不多。第一次调用的是“取满足条件的第一行”这个接口,之后循环取“满足条件的下一行”这个接口,这些接口都是引擎中已经定义好的。

总结

到此,一条查询语句在mysql架构中执行基本流程进行了一个大概的讲解。在这个流程中,会有很多细节和可深挖学习的地方,例如关联(join)、索引、日志系统等,接下来会继续学习并记录一些MySql深入的东西。

程序员面试,必要的注意事项


又是七月面试季节,又一波大学生进入社会,面临着各种面试问题,网上有很多面经,但更多的是在讲面试的经历或者面试被问到的技术相关的。这里我给大家分享一些无关乎技术,但在面试中需要注意的一些事项。

友情提示:去面试的时候,千万别空着手去啊!

不要误会,不是让你给面试官送礼,毕竟我们都是金光闪闪的程序员,靠技术说话,不吃这一套。

带上个人简历

面试的时候:一定要带简历!带简历!带简历!重要的事情说三遍。我在面试其他人的时候,都会先看对方的简历,然后才开始面试。从你的简历中能够看出非常多的信息,甚至看到你的简历就能初步确定你是不是我们要找的人(因为约面试一般是hr处理的,技术面你时不一定看过你的简历)。

你能体会到在面试官满怀期待的眼神中,你一句“没带”的无奈吗?交给面试官的简历不需要做的很花哨,但是也要保持干净整洁,千万不要从你的混合零钱钥匙的裤兜里掏出你叠了几次的简历。

面试官是个怎样的人

很多人可能觉得,面试的时候自己都比紧张,很容易忽视细节,更没有心情去关心面试官是一个什么样的人。但其实这是很重要的一个点,因为如果你入职了,面试官就会成为你的同事甚至上级,作为开发人员,队友和上级这两个角色都挺重要的呀!

那么怎么样来观察面试官是一个什么样的人呢?下面3个点,你要注意了:

  • 在面试问答环节,当面试官问到你回答不上来的问题时,看面试官是咄咄逼人的感觉还是以交流的态度引导你来认识这个问题,如果是前者,你可能要考虑一下要不要选择加入这个团队,因为这也是选择未来共事的人

  • 看看面试官的精神面貌,可能有的人觉得程序员还有什么精神面貌可看的,其实这里是想让你看看面试官是不是非常疲劳的样子,因为有的公司长期加班,很多开发者都处于疲劳状态;而这个状态以后也可能是你的工作常态

你有什么想问的?

在面试中,基本上在面试快要结束的时候,面试官都会问,“你有没有什么想了解我们公司的”或者“你有什么想问的问题”,有很多人都容易忽略这个问题,直接回答“没有了”。这真的是一个很失败的回答,因为对于面试官来讲,你没有想要了解公司的欲望,或者说不太感兴趣;对于你自身来说,错失了你最有机会了解公司招聘你这个岗位的动机。

这个环节一般可以提问两三个问题,不宜太多。我觉得以下几个问题可以根据你的情况提问:

  1. 问:如果我被录用了,接下来我要做什么?这个问题挺重要的,涉及到了你后面在团队中要干的活,也是你了解可能参与项目的业务以及开发会使用到哪些技术栈,这样你心理大概有一个谱,和你会的技术栈是否相符等,帮助你考虑要不要加入他们

  2. 可以问问开发团队规模,这个问题可以知道你进入团队遇到难题有没有人一起讨论解决,如果你期望找到有人可以带带你的公司,这个问题就很有必要

剩下可以问问有没有技术分享会或者其他分享会议, 如果聊的好,也可以问问在面试时遇到的自己不会的问题或者技术点,但不是在网上立马就能找到答案的或者说固有答案的,也可以问问面试官,或者说一起探讨

上面说了几个要问点, 下面也提一下有些此时问题不适合问的:

直接问薪资是不可取的,在技术面试进行面试时,千万别问薪资,人家会觉得你很不专业,并且心思都放在了钱上,而且谈薪资也不是技术要和你详细谈的事。

不要问私事:有些面试者比较随意,想直接从面试官身上获得一手的信息,比如问面试官“一个月工资多少钱”、“平时你加班严重吗”、“你觉得这家公司怎么样”这就好像一个刚认识的人打探你的家事一样,会让人很反感的。

不要被期权、股权诱惑

有很多公司为了招人,会向面试者许诺期权、股权之类的,其实很多时候,这只是安抚员工的一种形式,和直接给钱比,还是给钱更划算。

公积金和社保给怎么交

据我所知,很多小公司社保和公积金都不交的,或者交一个,但基数也小,这个怎么说呢,有些小公司,考虑到自己的发展,有时候也可以理解。

因为每个人的能力不同,有些面试者可能是学历或者能力的问题,进不了大公司,但是就像上一点提到的那样,我还是建议你去一个能正常给你交社保和公积金的公司,哪怕少一点。

不尊重你和你的财产的公司

举个例子,有的公司不提供工作手机,希望使用你自己的手机用于开发测试,更有的公司使用自己的电脑设备开发,不给予任何补助,在这种情况下,你需要考虑下,因为在你的财产方面都没有收到尊重,你会期望以后工作中还能受到更多的尊重吗?

多了解下公司文化

在面试一家公司前,首先你要做的是去网上了解一下这家公司大概的情况,主要业务是哪些?有哪些部门?最近几年的效益和口碑怎么样?等等。这不仅仅有助于你面试,还有助于你面试结束决定是否去这家公司上班。

除此之外,到公司之后,可以简单参观参观公司的情况,感受一下员工的工作状态,周围的工作环境等等,自己喜不喜欢这样的环境?

尽量去大厂,尽量不要去外包公司

很多人特别是一些初入行的问要不要去外包公司,也有很多人想去外包公司,以为去外包公司可以锻炼自己,可事实是否如此呢?
不是外包不挣钱,是外包对技术提升没有多少用处,一篇程序猿的相关文章分析,想去外包公司的无非三种人

  1. 完全不知道外包怎么回事的人;

2.为了多拿一点点钱的人,有些人如果去非外包公司可能只能拿到6k,但去外包公司可能拿到7k元,这种人,我想提醒你们,去非外包公司,只要你好好干,这6k可能会变成10k,但去外包公司你那7k元,两年后撑死变成8k。
3.就是完全找不到工作的人,因为外包公司做的是量,大量外派人员到其他大公司,就是卖人。
我也很认同所分析出的这三种人,经常看到一些在外包公司工作的设计师们,我们下班了,还在加班;我们放假了,还在加班;我们睡前刷刷朋友圈,也会有那么几个在外包公司工作的在晒加班。。

今天就分享这么多,如果对分享的内容感兴趣,可以关注公众号,或者加入技术交流群,大家一起讨论。

npm config使用

环境变量

讲config之前,先说一下环境变量这个事。

Linux系统中查看所有环境变量

env

node中常用到的环境变量是NODE_ENV,查看是否存在

echo $NODE_ENV

Linux系统中添加环境变量

env

某些时候需要删除环境变量

unset NODE_ENV

环境变量追加值

export PATH=$PATH:/home/download:/usr/local/

config模块包使用

安装

npm install config

代码中使用

config安装成功后,可以在项目中创建几种环境的配置文件。

  1. test 测试环境
  2. product 生产环境
  3. development 开发环境
    以development环境举例:
{
  "port": 9001,
  "host": "http://localhost:9001",
  "redis":{
    "host": "localhost",
    "port": 6379,
    "password":"root",
    "db": 2,
    "connectTimeout": 3000,
    "prefix": "koala:"
  }
 }

代码中使用配置文件

const config=require('config');

app.use(require('./middleware/redis')(config.get('redis')));

开启app服务

  • 常规启动
export NODE_ENV=development
node app.js
  • 一条命令启动 app服务器
export NODE_ENV='development' && node app.js
```linux
- pm2指定名称与环境启动app服务

```linux
pm2 start app.js --env development --name koala

--name name指的是pm2指定的进程名称。

Javascript随机获取数组中不重复的n个元素

前言:

最近有一个需求,从一组试题中随机获取5道题用于用户复习,获取到的5道试题应该每次是随机的而且不重复的。

代码实现:

实现方式1

大多数人的写法,从数组中取数据,放入新的数组,取完一个数据从原始数组中删除数据,在放入新数组的时候判断是否已出现过。

function getTenNum(n) {
    var reslut = [];
    var testArray = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,1,2];
    for (var i = 0; i < n; ++i) {
        var random = Math.floor(Math.random() * testArray.length);
        if(result.incledes(testArray[random])){
            continue;
        }
        reslut.push(testArray[random]);
        testArray.splice(random,1);
    }
    return reslut;        
}
var resArr = getTenNum(10);
这个 应该 是大多数人的 代码

实现方式2 推荐

实现思路

把源数组分成左右两段,左边按顺序递增,保存已选择的随机数;右侧是剩余可选的数值;每次从右侧选一个,与左侧最后一个位置的数值交换就可以达到目的。

然后考虑把左侧用一个新数组表示,右侧选中的数移入新数组,再将左侧应该交换过来的值移过来……

算法图解:

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

var count = arr.length;
for (var i = 0; i < 10; i++) {
    var index = ~~(Math.random() * count) + i;
    if(result.incledes(arr[index])){
            continue;
    }
    result[i] = arr[index];
    arr[index] = arr[i];
    count--;
}

console.log(result);

实现方式3

实现思路

基本与实现方式2雷同,只是提示大家有举一反三的能力。从数组中随机抽取数字,放到新的数组中,然后把数组末尾的数字换到抽取到的数字位置,接下来从除去末尾的里面再次随机抽取。

var arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

var result = [ ];

var ranNum = 5;

for (var i = 0; i < ranNum; i++) {

var ran = Math.floor(Math.random() * (arr.length - i));
if(result.incledes(arr[ran])){
            continue;
}
result.push(arr[ran]);

arr[ran] = arr[arr.length - i - 1];

};

javascript数组常用函数与实战总结

前言

在node.js后端开发过程中,数组这种数据类型(Object类型)再常见不过,本文主要介绍数组的一些常见函数,以及在实战开发过程中能更好的操作数组的lodash包

函数介绍

向数组末尾添加值

push

说明:向数组的末尾添加一个或多个元素,并返回新的长度
代码:

let array=[11,22];
let arrayChange=array.push("333");
console.log(arrayChange)
//返回的结果就是数组改变后的长度:3
console.log(arrayChange.length)//undefined

向数组头部添加值

unshift

说明:将参数添加到原数组开头,并返回数组的长度
代码:

let array=[11,22];
let arrayChange=array.unshift("333");
console.log(array) //[333,11,22]
console.log(arrayChange)
//返回的结果就是数组改变后的长度:3
console.log(arrayChange.length)//undefined

删除数组元素

splice

说明:
当splice传递两个参数的时候,参数1:开始删除的下标位置,参数2:删除数组元素的个数,返回新的数组。当splice传递三个参数的时候,参数1:开始删除的下表位置,参数2:删除数组元素的个数,参数3:向数组添加的新元素。注意数组下标0开始。

代码:

let array=[11,22,33,44];
let arrayChange=array.splice(1,2);//movePos.splice(开始删除的下表位置,删除数组元素的个数);
console.log(arrayChange) ; //返回新的数组:22,11
console.log(arrayChange.length) ;//返回数组长度2

let array =[111,222,333,444];
let arrayChange=array.splice(2,1,"666")//movePos.splice(开始删除的下表位置,删除数组元素的个数,向数组添加的新项目。);从下标2开始删除一位,并用666替换删除下表位置的元素
console.loge(arrayChange + "<br />")
//返回新的数组 11,22,666,44

获取数组的最后一个元素

常规获取元素最后一个值

let array=['1','2','3','312哦哦'];
console.log(array[array.length-1]);

pop 也可以说是删除数组的最后一个元素,与删除数组的第一个元素shift用法基本相同

说明:注意使用pop获取数组最后一个元素的时候,同时会删除掉数组的最后一个元素;使用shift获取数组最后一个元素的时候,同时会删除掉数组的最后一个元素,二者都是返回的那个元素的值,原始数组也发生变化。

let array=['1','2','3','312哦哦'];
console.log(array.pop())
console.log(array)

Lodash中的函数_last

说明:不会改变原始数组

let array=['1','2','3','312哦哦'];
console.log(_.last(array))

颠倒数组元素(数组的反转)

  • 数组传统方法 reverse

    说明:颠倒数组元素后返回新的数组

代码:

let array=[11,22];

let arrayChange=movePos.reverse();

console.log(arrayChange) //返回新的数组:22,11

console.log(arrayChange.length) //返回数组长度2
  • lodash提供的函数 _.reverse

代码:

let array=[11,22];
console.log(_.reverse(array))

分隔数组放入字符串

join

说明:用于把数组中的所有元素放入一个字符串。元素是通过指定的分隔符进行分隔的。返回一个字符串
代码

let array=[11,22];

let arrayChange=array.join("+");
console.log(arr)  //11+22

连接两个或多个数组

contact

说明:用于连接两个或多个数组,并返回一个新数组,新数组是将参数添加到原数组中构成

let array=[11,22];
let arrayChange=array.concat(4,5);
console.log(arrayChange);//[11, 22, 4, 5]

数组元素的去重

  • 传统方法遍历去重 for of

代码:

    let arr = ['数字','花朵','数字'];
    let result=[];
    for (let i of arr) {
        if (!result.includes(i)) {
            result.push(i)
        }
    }
  • ES6 新增了Set这一数据结构 类似数组 但是Set成员具有唯一性,基于唯一性适合做数组去重

代码:

let array=['数字','花朵','数字','地球','人类','头发','眼睛','细胞'];
console.log(...(new Set(array)))
  • lodash提供的函数 _uniq

代码:

let array=['数字','花朵','数字','地球','人类','头发','眼睛','细胞'];
console.log(_.uniq(array));

数组求和

lodash中的函数 _sum

代码:

let sorce=_.sum([32,45,86,43]);

获取数组中指定键值对的值组成数组

lodash中的函数 _map

说明:例如这样一个包含对象的数组[{id:1,name:'koala'},{id:2,name:'koala1'}],想拿到数组对象中id的数据组成一个数组。map的参数1:原型数组,参数2对象中的某一个键值对

代码:

let array=[{id:1,name:'koala'},{id:2,name:'koala1'}];
let result=_map(array,'id');
//[1,2]

获取数组中某个值的角标

注意:下面两个函数都是返回遇到的第一个符合的值的下标值。
indexOf

说明:

  1. 用于在字符串和数组中找到目标的索引
  2. 在字符串中使用的话会转换类型为 "hello1".indexOf(1) //结果5
  3. 在数组中使用不会转换类型 [1,2,3,"4"].indexOf(4) //-1
  4. [,,,,,].indexOf(undefined) //-1
  5. [null,undefined,NaN].indexOf(NaN)] //-1 NaN是找不到的其他可以哟
  6. let num = 2019; (""+num).indexOf(0) //1
    7.正常情况下indexOf返回数组中第一个数的位置是0
    代码:
console.log('哈哈',("hello1".indexOf(1)))//  哈哈 5
console.log('哈哈',("hello1111".indexOf(1)))//  哈哈 5
console.log('哈哈',("hello1".indexOf('h')))//0

lodash中的函数**_.findIndex**
说明:对于一个数组,里面每个值是对象的时候,这个函数,可以不完全判断对象一定是相同的。
代码:

var users = [
    { 'user': 'barney',  'active': false ,'role':1},
    { 'user': 'fred',    'active': false ,'role':2},
    { 'user': 'fred', 'active': true ,'role':3}
  ];

console.log(  _.findIndex(users, { 'user': 'fred', 'role': 3 }));// 输出2

数组包含值判断

  • indexOf

    说明:返回对应元素下标,在上面已经详细介绍过。

  • includes

    说明:返回的直接是true/false,同时对NaN找不到的问题也得到解决。效率应该会比indexOf高一些。includes可以有两个参数,参数1:表示要判断的值,参数2:表示判断的起始位置,可以是负数,表示从右数过来第几个,但是不改变判断搜索的方向,搜索方向还是从左到右。

代码:

let array111=['koala','kaola1',NaN];
console.log(array111.includes('koala'));//true
console.log(array111.includes(NaN));//true

const arr1 = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', NaN]
console.log('%s', arr1.includes('d', 3))//true
console.log('%s', arr1.includes('d', 4))//false

console.log('%s', arr1.includes('k', -1))//false
console.log('%s', arr1.includes('k', -2))//true
console.log('%s', arr1.includes('i', -3))//false

把一个字符串分割成字符串数组

split

说明:split函数两个参数,参数1:字符串或正则表达式,从该参数指定的地方分割 (必须),参数2:可选。该参数可指定返回的数组的最大长度。如果设置了该参数,返回的子串不会多于这个参数指定的数组。如果没有设置该参数,整个字符串都会被分割,不考虑它的长度

var str="How are you doing today?"

document.write(str.split(" ") + "<br />")
document.write(str.split("") + "<br />")
document.write(str.split(" ",3))

输出:
How,are,you,doing,today?
H,o,w, ,a,r,e, ,y,o,u, ,d,o,i,n,g, ,t,o,d,a,y,?
How,are,you


"2:3:4:5".split(":")	//将返回["2", "3", "4", "5"]
"|a|b|c".split("|")	//将返回["", "a", "b", "c"]

"hello".split("")	//可返回 ["h", "e", "l", "l", "o"]

"hello".split("")	//可返回 ["h", "e", "l", "l", "o"]

"hello".split("", 3)	//可返回 ["h", "e", "l"]

附:

注意,文中提到的所有lodash中的函数,在使用的时候需要先

const _ = require('lodash');

一道面试题:

给定任意非负整数,反复累加各位数字直到结果为个位数为止。例如给定非负整数912,第一次累加9+1+2 = 12, 第二次累加1+2 = 3, 3为个位数,循 环终止返回3。请编程实现。

function add(num){
    if(isNaN(num)) return;
    if(num<10) return num
    const res=num.toString().split('').reduce((sum,value)=>{
        return sum+Number(value)
    },0)
    return add(res);
}
add(345);
3

删库跑路后真的没有办法弥补了吗?

提出问题?

  1. 服务器数据库异常重启了会造成什么样的影响?
  2. 不小心删除了数据库怎么办,或者不小心删除了数据库表中数据怎么办?
  3. 一条更新语句在数据库系统内部执行时与数据库日志系统有什么联系?
  4. 数据库备份,是每天一备比较好,还是每周一备比较好?
    接下来在讲解日志系统的同时,回答上面的几个问题。

日志系统详解:

redo日志(重做日志)

redo是引擎层的日志,而且是InnoDB特有的。InnoDB的redo log是有固定大小的,比如可以配置为 一组4个文件(logfile-1,logfile-2,logfile-3,logfile-4),每个文件的大小是1GB,那么它总共可以记录4GB的操作。一个环状循环结构,从头开始写,写到末尾又回到开始循环写。

redo中的环状结构

结构图:


write pos是当前记录的位置,一边写一边后移,环状结构,写到3号文件末尾就会回到0号文件开头。checkpoint是当前擦除的位置,也是往后推移并且循环的。注意擦除记录前要把记录更新到数据文件(这里可以联想 粉板 老板正式记账本的例子)

redo日志作用(回答提出问题1)

  1. 在MySQL中,如果每一次的更新操作都写进磁盘,然后磁盘也找到对应的那条记录,然后再更新,整个过程io成本,查找成本都很高,为了解决这个问题,提升效率,就会用到redo日志,MySQL经常说的的WAL技术,WAL的全称是write-Ahead-Logging,它的关键点就是先写日志,再写磁盘。具体说,当有一条记录需要更新的时候,InoDB引擎会先记录到redo log,并更新内存,这时候更新就算完成了。同时InnoDb引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。
  2. 正是因为有了redo log ,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录多不会消失,这个能力叫crash-safe。只要数据库的物理记录还在redo log中,就是服务器数据库出现问题重启,数据库恢复后,数据记录仍然可以恢复。

binlog日志(归档日志)

Mysql基础架构整体分为两部分:Server层和引擎层,引擎层主要负责存储相关的事宜。上面说到在引擎层有自己的日志,而且只在InnoDB引擎中才有。Server层也有自己的日志,称为binlog(归档日志)。它是采用追写入日志的方式。追加写是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

binlog日志作用(回答提出问题2)

只依靠redo日志的crash-safe特性在应对数据库误删,表数据误删等操作时候,有些时候redo日志是无力的,但是binlog日志解决这些问题,因为binlog会记录所有的逻辑操作,并且采用“追加写”的形式。

举个例子如果公司员工发现某天下午有一个误删表数据操作,要求找回数据,应该怎么做?(注:这里要考虑是在刚备份之后误删除,还是备份之前误删除,下面的例子是在备份之前删除的,找之前删除的数据)

  1. 首先,找到最近的一次全量备份,这要看你们公司的数据库是多久备份一次(有的公司是一天,有的公司是一周,而且会定期删除,很多公司只保留最近一个月的数据库备份),拿到备份数据后,把这个备份数据恢复到临时表
  2. 然后从备份的时间点开始,将备份的binlog依次取出来,重放到误删表之前的那个时刻
  3. 这时候临时库跟误删之前的线上库一样了,然后把表数据从临时数据取出来,按需恢复到线上库去。

redo日志与binlog日志对比

redo日志与binlog日志有哪些不同?
其实上面好多都提到过,再次总结一遍,加深印象。

  1. redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有的引擎都可以使用。
  2. redo log是物理日志,记录的是某个数据页上做了什么修改;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加一”
  3. redo log是环状结构,循环写,空间固定会用完,用完后需要擦除;binlog是可以追加写入的。“追加写”是只belog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

更新语句执行流程(与日志关系)

数据库语句:

mysql> update Student set c=c+1 where ID=2;

通过分析这一条更新语句,画出流程图,问题3也就得到解决。


注意流程图的最后三步,这是更新语句和日志关系密切的地方,将redo日志拆成了两个步骤:prepare和commit,它俩的中间是执行器写入binlog。(注:如果不这么做,假如一个日志提交成功的时候,另一个日志提交之前发生了数据库发生了崩溃,但是crash-safe恢复或者误删库恢复的时候可能造成二者数据不统一出现问题。)

开发过程中如何为mysql设置这两种保存日志的配置

  • redo log

    innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数我建议你设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。

  • binlog

    sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数我也建议你设置成 1,这样可以保证 MySQL 误删除操作(删除表数据,删除库数据) 通过binlog 仍可恢复。

如何查看这两种日志

关于日志系统的一些误区和疑问

  • 大家会不会想有了redo日志就可以了,为什么还要出现binlog日志呢?

    解答:

    1. redo日志是只要InnoDB引擎才提供的一个内容。
    2. redo日志是环状结构循环写入,并且到了配置的固定大小后会被擦除,误删除数据库或表数据的时候,备份可能会出现无法全部还原。
  • 这里有一个问题:如果在擦除和记账重合那一刻,数据库异常重启了,新的数据库操作会怎么记录,是擦除一部分,记录上,会丢失,还是等待重启后往上添加数据?

  • 关于数据库备份,是一天一备比较好,还是一周一备份比较好,一般备份文件保留多久?提出问题4解决

    解答:对于数据库备份周期这个问题,需要考虑以下指标:数据存量、增量、备份成本、恢复效率。

    1. 如果数据存量大到一天都没法备份完成,只能一周一次甚至更长时间
    2. 业务数据的增量,如果增量非常大,如果一周备份一次,可能会出现增量备份失败问题,而且恢复时长和成功率也比较困难,则可以考虑一日一备。
    3. 业务比较重要,且对恢复时间的忍耐程度低,之前多次发生过数据回滚的需求,数据增量还不小,可以考虑一天一备。相反业务实际不重要,出问题可以容忍一定的不可用,增量还不多,可以考虑一周一备份。

    总的来说就是和项目,需求,场景有很多关系。

  • 写入redo日志也是io操作,数据更新直接写入磁盘也是io操作,为什么说写入redo日志效率高节省io成本呢?

    解答:redo日志的写入是顺序写入的,不用去“找位置”,而直接更新数据到磁盘的话,需要到磁盘中找到位置再写入,肯定前者的效率高。

总结与宣传

以上内容是关于数据库日志系统的讲解,同时解决了我开篇提出的几个数据库日志相关的问题,希望能帮助大家更好的了解学习数据库,如果有问题可以随时关注公众号联系,互相学习哦。

深入理解 JavaScript, 从作用域与作用域链开始

1. 什么是作用域

作用域是你的代码在运行时,某些特定部分中的变量,函数和对象的可访问性。换句话说,作用域决定了变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期

2. JavaScript中的作用域

在 JavaScript 中有两种作用域

  • 全局作用域
  • 局部作用域

如果一个变量在函数外面或者大括号{}外声明,那么就定义了一个全局作用域,在ES6之前局部作用域只包含了函数作用域,ES6为我们提供的块级作用域,也属于局部作用域

2.1 全局作用域

拥有全局作用域的对象可以在代码的任何地方访问到, 在js中一般有以下几种情形拥有全局作用域:

  1. 最外层的函数以及最外层变量:
var globleVariable= 'global';  // 最外层变量
function globalFunc(){         // 最外层函数
    var childVariable = 'global_child';  //函数内变量
    function childFunc(){        // 内层函数
        console.log(childVariable);
    }
    console.log(globleVariable)
}
console.log(globleVariable);  // global
globalFunc();                 // global
console.log(childVariable)   // childVariable is not defined
console.log(childFunc)       // childFunc is not defined

从上面代码中可以看到globleVariableglobalFunc在任何地方都可以访问到, 反之不具有全局作用域特性的变量只能在其作用域内使用。

  1. 未定义直接赋值的变量(由于变量提升使之成为全局变量)
function func1(){
    special = 'special_variable';
    var normal = 'normal_variable';
}
func1();
console.log(special);    //special_variable
console.log(normal)     // normal is not defined

虽然我们可以在全局作用域中声明函数以及变量, 使之成为全局变量, 但是不建议这么做,因为这可能会和其他的变量名冲突,一方面如果我们再使用const或者let声明变量, 当命名发生冲突时会报错。

// 变量冲突
var globleVariable = "person";
let globleVariable = "animal"; // Error, thing has already been declared

另一方面如果你使用var申明变量,第二个申明的同样的变量将覆盖前面的,这样会使你的代码很难调试。

var name = 'koala'
var name = 'xiaoxiao'
console.log(name);  // xiaoxiao

2.2 局部作用域

和全局作用于相反,局部作用域一般只能在固定代码片段内可以访问到。最常见的就是函数作用域

2.2.1 函数作用域

定义在函数中的变量就在函数作用域中。并且函数在每次调用时都有一个不同的作用域。这意味着同名变量可以用在不同的函数中。因为这些变量绑定在不同的函数中,拥有不同作用域,彼此之间不能访问。

//全局作用域
function test(){
    var num = 9;
    // 内部可以访问
    console.log("test中:"+num);
}
//test外部不能访问
console.log("test外部:"+num);

注意点:

  • 如果在函数中定义变量时,如果不添加var关键字,造成变量提升,这个变量成为一个全局变量。
function doSomeThing(){
    // 在工作中一定避免这样写
    thing = 'writting';
    console.log('内部:'+thing);
}
console.log('外部:'+thing)
  • 任何一对花括号{...}中的语句集都属于一个块, 在es6之前,在块语句中定义的变量将保留在它已经存在的作用域中:
var name = '程序员成长指北';
for(var i=0; i<5; i++){
    console.log(i)
}
console.log('{}外部:'+i);
// 0 1 2 3 4  {}外部:5

我们可以看到变量name和变量i是同级作用域。

2.2.2 在ES6块级作用域未讲解之前注意点

变量提升

变量提升英文名字hoisting,MDN中对它的解释是变量申明是在任意代码执行前处理的,在代码区中任意地方申明变量和在最开始(最上面)的地方申明是一样的。也就是说,看起来一个变量可以在申明之前被使用!这种行为就是所谓的“hoisting”,也就是变量提升,看起来就像变量的申明被自动移动到了函数或全局代码的最顶上。
看一段代码:

var tmp = new Date();
function f() {
    console.log(tmp);
    if(false) {
        var tmp='hello';
    }
}

这道题应该很多小伙伴在面试中遇到过,有人会认为输出的是当前日期。但是正确的结果是undefined。这就是由于变量提升造成的,在这里申明提升了,定义的内容并不会提升,提升后对应的代码如下:

var tmp = new Date();
function f() {
    var tmp;
    console.log(tmp);
    if(false) {
        tmp='hello';
    }
}
f();

console在输出的时候,tmp变量仅仅申明了但未定义。所以输出undefined。虽然能够输出,但是并不推荐这种写法推荐的做法是在申明变量的时候,将所用的变量都写在作用域(全局作用域或函数作用域)的最顶上,这样代码看起来就会更清晰,更容易看出来哪个变量是来自函数作用域的,哪个又是来自作用域链

重复声明

看一个例子:

// var
var name = 'koloa';
console.log(name); // koala
if(true){
    var name = '程序员成长指北';
    console.log(name); // 程序员成长指北
}
console.log(name); // 程序员成长指北

虽然看起来里面name申明了两次,但上面说了,js的var变量只有全局作用域和函数作用域两种,且申明会被提升,因此实际上name只会在最顶上开始的地方申明一次,var name='程序员成长指北'的申明会被忽略,仅用于赋值。也就是说上面的代码实际上跟下面是一致的。

// var
var name = 'koloa';
    console.log(name); // koala
if(true){
    name = '程序员成长指北';
    console.log(name); // 程序员成长指北
}
console.log(name); // 程序员成长指北
变量和函数同时出现的提升

如果有函数和变量同时声明了,会出现什么情况呢?看下面但代码

console.log(foo);
var foo ='i am koala';
function foo(){}

输出结果是function foo(){},也就是函数内容

如果是另外一种形式呢?

console.log(foo);
var foo ='i am koala';
var foo=function (){}

输出结果是undefined

对两种结果进行分析说明:

第一种:函数申明。就是上面第一种,function foo(){}这种形式

另一种:函数表达式。就是上面第二种,var foo=function(){}这种形式

第二种形式其实就是var变量的声明定义,因此上面的第二种输出结果为undefined应该就能理解了。

而第一种函数申明的形式,在提升的时候,会被整个提升上去,包括函数定义的部分!因此第一种形式跟下面的这种方式是等价的!

var foo=function (){}
console.log(foo);
var foo ='i am koala';

原因是:

  1. 函数声明被提升到最顶上;
  2. 申明只进行一次,因此后面var foo='i am koala'的申明会被忽略。
  3. 函数申明的优先级优于变量申明,且函数声明会连带定义一起被提升(这里与变量不同)

接下来讲,在ES6中引入的块级作用域之后的事!

2.2.2 块级作用域

ES6新增了letconst命令,可以用来创建块级作用域变量,使用let命令声明的变量只在let命令所在代码块内有效。

let 声明的语法与 var 的语法一致。你基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中。块级作用域有以下几个特点:

  • 变量不会提升到代码块顶部且不允许从外部访问块级作用域内部变量
console.log(bar);//抛出`ReferenceErro`异常: 某变量 `is not defined`
let bar=2;
for (let i =0; i<10;i++){
    console.log(i)
}
console.log(i);//抛出`ReferenceErro`异常: 某变量 `is not defined`

其实这个特点带来了许多好处,开发者需要检查代码时候,可以避免在作用域外意外但使用某些变量,而且保证了变量不会被混乱但复用,提升代码的可维护性。就像代码中的例子,一个只在for循环内部使用的变量i不会再去污染整个作用域。

  • 不允许反复声明

ES6的letconst不允许反复声明,与var不同

// var
function test(){
    var name = 'koloa';
    var name = '程序员成长指北';
    console.log(name); // 程序员成长指北
}

// let || const
function test2(){
    var name ='koloa';
    let name= '程序员成长指北'; 
    // Uncaught SyntaxError: Identifier 'count' has already been declared
}

看到这里是不是感觉到了块级作用域的出现还是很有必要的。

3. 作用域链

在讲解作用域链之前先说一下,先了解一下 JavaScript是如何执行的?

3.1 JavaScript是如何执行的?


JavaScript代码执行分为两个阶段:

3.1.1 分析阶段

javascript编译器编译完成,生成代码后进行分析

  • 分析函数参数
  • 分析变量声明
  • 分析函数声明

分析阶段的核心,在分析完成后(也就是接下来函数执行阶段的瞬间)会创建一个AO(Active Object 活动对象)

3.1.2 执行阶段

分析阶段分析成功后,会把给AO(Active Object 活动对象)给执行阶段

  • 引擎询问作用域,作用域中是否有这个叫X的变量
  • 如果作用域有X变量,引擎会使用这个变量
  • 如果作用域中没有,引擎会继续寻找(向上层作用域),如果到了最后都没有找到这个变量,引擎会抛出错误。

执行阶段的核心就是,具体怎么,后面会讲解LHS查询RHS查询

3.1.3 JavaScript执行举例说明

看一段代码:

function a(age) {
    console.log(age);
    var age = 20
    console.log(age);
    function age() {
    }
    console.log(age);
}
a(18);
首先进入分析阶段

前面已经提到了,函数运行的瞬间,创建一个AO (Active Object 活动对象)

AO = {}

第一步:分析函数参数:

形式参数:AO.age = undefined
实参:AO.age = 18

第二步,分析变量声明:

// 第3行代码有var age
// 但此前第一步中已有AO.age = 18, 有同名属性,不做任何事
即AO.age = 18

第三步,分析函数声明:

// 第5行代码有函数age
// 则将function age(){}付给AO.age
AO.age = function age() {}

函数声明注意点:AO上如果有与函数名同名的属性,则会被此函数覆盖。但是一下面这种情况

var age = function () {
            console.log('25');
        }

声明的函数并不会覆盖AO链中同名的属性

进入执行阶段

分析阶段分析成功后,会把给AO(Active Object 活动对象)给执行阶段,引擎会询问作用域,的过程。所以上面那段代码AO链中最初应该是

AO.age = function age() {}
//之后
AO.age=20
//之后
AO.age=20

所以最后的输出结果是:

function age(){
    
}
20
20

3.2 作用域链概念

看了前面一个完整的javascript函数执行过程,让我们来说下作用域链的概念吧。JavaScript上每一个函数执行时,会先在自己创建的AO上找对应属性值。若找不到则往父函数的AO上找,再找不到则再上一层的AO,直到找到大boss:window(全局作用域)。 而这一条形成的“AO链” 就是JavaScript中的作用域链。

3.3 过程LHS和RHS查询特殊说明

LHS,RHS 这两个术语就是出现在引擎对变量进行查询的时候。在《你不知道的Javascript(上)》也有很清楚的描述。在这里,我想引用freecodecamp 上面的回答来解释:

LHS = 变量赋值或写入内存。想象为将文本文件保存到硬盘中。 RHS = 变量查找或从内存中读取。想象为从硬盘打开文本文件。 Learning Javascript, LHS RHS

3.3.1 LHS和RHS特性

  • 都会在所有作用域中查询
  • 严格模式下,找不到所需的变量时,引擎都会抛出ReferenceError异常。
  • 非严格模式下,LHR稍微比较特殊: 会自动创建一个全局变量
  • 查询成功时,如果对变量的值进行不合理的操作,比如:对一个非函数类型的值进行函数调用,引擎会抛出TypeError异常

3.3.2 LHS和RHS举例说明

例子来自于《你不知道的Javascript(上)》

function foo(a) {
    var b = a;
    return a + b;
}
var c = foo( 2 );

直接看引擎在作用域这个过程:
LSH(写入内存):

c=, a=2(隐式变量分配), b=

RHS(读取内存)

读foo(2), = a, a ,b
(return a + b 时需要查找a和b)

3.4 作用域链总结

最后对作用域链做一个总结,引用《你不知道的Javascript(上)》中的一张图解释

怎么绘制出0.5px的线条

怎么绘制出0.5px的线条

scale方法

<style>
.hr.scale-half {
    height: 1px;
    transform: scaleY(0.5);
}
</style>
  <p>1px + scaleY(0.5)</p>
  <div class="hr scale-half"></div>

viewport

利用动态的改变<viewport>让网页的像素区域和屏幕的像素区域重合,这样就很轻松的去写1px的边框,并且它会等于屏幕本身的1px。

/当 devicePixelRatio = 2 输出:

<meta name="viewport" content="initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no">

//当 devicePixelRatio = 3 输出:

<meta name="viewport" content="initial-scale=0.3333333333333333, maximum-scale=0.3333333333333333, minimum-scale=0.3333333333333333, user-scalable=no">

svg+background

svg 单独是这样:

<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='1px'>
    <line x1='0' y1='0' x2='100%' y2='0' stroke='#000'></line>
</svg>

设置 background 为svg文件:

<style>
.line.svg {
    height: 1px;
    background: url("data:image/svg+xml;utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='1px'><line x1='0' y1='0' x2='100%' y2='0' stroke='#000'></line></svg>");
}
</style>
<p>svg+background</p>
<div class="line svg"></div>

JS中的for循环——你可能不知道的点。

提出问题

问题1:

看一段for循环的代码,大家先想一下执行结果是什么?

var arr = [2,4,6,8,10];
var arrLength = arr.length;

for (var i = 0; i < arrLength; i++) {
    setTimeout(function() {
        console.log(i);
        console.log(arr[i]);
    }, 2000);
}

问题2:

for循环中出现多个异步函数(比如ajax请求,或者node后端执行一些数据库操作或文件操作),如果想要这些异步串行变为同步应该怎么做?

问题1解决与相关讲解

结果

预期结果

0 2  1 4  2 6  3 8  4 10

运行后的结果

5 undefined  5 undefined  5 undefined  5 undefined  5 undefined

产生结果的原因

setTimeout()函数回调属于异步任务,会出现在宏任务队列中,被压到了任务队列的最后,在这段代码应该是for循环这个同步任务执行完成后才会轮到它,所以for循环在遍历过程中i不断加1,直到i判断失败一次才停止,这时候i为5,也就是说空跑了5次循环。等到了setTimeOut预定的时间后就会执行在for遍历过程中声明的5个setTimeout。所以最终运行后会出现上面的结果,与预期结果不符。

注:关于宏任务队列,同步任务等相关的问题,如果有问题,可以查看我的另一篇文章详细了解。

正确执行的解决方案

1. 闭包,立即执行函数

想要得到预期的结果,第一种办法是使用闭包,在闭包函数内部形成了局部作用域,每循环一次,形成一个自己的局部作用域,不受外部变量变化的影响。
代码如下:

var arr = [2,4,6,8,10];
var arrLength = arr.length;

for (var i = 0; i < arrLength; i++) {
    (function(i) {
        setTimeout(function() {
            console.log('i是' + i);
            console.log('value是' + arr[i]);
        }, 2000);
    })(i);
}

2. let

将代码中的var改成let,let非常适合用于 for循环内部的块级作用域。JS中的for循环体比较特殊,每次执行都是一个全新的独立的块作用域,用let声明的变量传入到 for循环体的作用域后,不会发生改变,不受外界的影响。

代码如下:

var arr = [2,4,6,8,10];
var arrLength = arr.length;
// i虽然在全局作用域声明,但是在for循环体局部作用域中使用的时候,变量会被固定,不受外界干扰。
for (let i = 0; i < arrLength; i++) {
    (function(i) {
        setTimeout(function() {
            console.log('i是' + i);
            console.log('value是' + arr[i]);
        }, 2000);
    })(i);
}

问题2解决与相关讲解

for循环中使用异步,在node.js后端开发或者前端ajax请求的时候还是比较常见的。有多种解决方案

  1. 回调 callback 嵌套异步操作、再回调的方式
  2. Promise + then() 层层嵌套
  3. async和await

选择我个人认为最优秀的解决方式3async和await进行讲解。

async + await “外异内同”

例子:

如果要去将一批数据发送到服务器,只有前一批发送成功(即服务器返回成功的响应),才开始下一批数据的发送,否则终止发送。这就是一个典型的 “for 循环中存在相互依赖的异步操作” 的例子

例子对应伪代码:

async function task () {
    for (let val of [1, 2, 3, 4]) {
        // await 是要等待响应的
        let result = await send(val);
        if (!result) {
            break;
        }
    }
}
task();

伪代码中使用await之后,实现了异步变成同步的转化,只有for循环中当次对应的发送请求完成且获取结果,才会继续往下执行。

await几点说明:

  • await执行的那一行语句是同步的。
  • async函数执行后,总是返回一个promise对象,可以理解为这个函数是一个异步函数(外异)但是----------------------引用阮一峰老师书中一句话:

当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

我对阮一峰老师的话再具体说明一下,可能有些同学还不是特别理解。实际上我们调用了await,这时候await这条语句下面的语句已经不会执行了(内同),而是先给外层async函数返回了一个promise对象,await后面对应的应该也是一个promise对象只有该对象 resolve 掉,产生结果,await 那一行代码才算真正执行完,才继续往下走。(注意:await执行之后应该是一个resolve的结果而不是promise对象了)。

node.js后端开发-await在for循环中的应用

看一段后端项目中应用await的代码:

//dayResult是一个查询到的数组
for (const item of dayResult)
    {
        //TODO 查询用户vip表 用户体验vip距离到期的用户列表
        let userIds=await db.vip.findAll({
            where:{
                experience_time:{
                    '$lte':moment().subtract(15-item.day,'day').endOf('day') ,//获取四天前都0时0分秒
                    '$gte':moment().subtract(15-item.day,'day').startOf('day') ,//获取四天前都0时0分秒
                        },
                        vip_type:0
                    },
                attributes:['user_id',Sequelize.literal(`'${item.id}' as notice_id`)],
                raw:true
            });
        userNoticeRecord=userNoticeRecord.concat(userIds)
        }

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.