koala-coding / goodblog Goto Github PK
View Code? Open in Web Editor NEW我是koala, 公众号【程序员成长指北】的作者, 专注Node.js技术栈分享,从前端到Node.js再到后端数据库,帮您成为优秀的Node.js全栈工程师。和我一起进阶全栈吧!
Home Page: http://www.inode.club
我是koala, 公众号【程序员成长指北】的作者, 专注Node.js技术栈分享,从前端到Node.js再到后端数据库,帮您成为优秀的Node.js全栈工程师。和我一起进阶全栈吧!
Home Page: http://www.inode.club
redo
是引擎层的日志,而且是InnoDB特有的。InnoDB的redo log
是有固定大小的,比如可以配置为 一组4个文件(logfile-1,logfile-2,logfile-3,logfile-4),每个文件的大小是1GB,那么它总共可以记录4GB的操作。一个环状循环结构,从头开始写,写到末尾又回到开始循环写。
结构图:
write pos
是当前记录的位置,一边写一边后移,环状结构,写到3号文件末尾就会回到0号文件开头。checkpoint是当前擦除的位置,也是往后推移并且循环的。注意擦除记录前要把记录更新到数据文件(这里可以联想 粉板 老板正式记账本的例子)
WAL
的全称是write-Ahead-Logging
,它的关键点就是先写日志,再写磁盘
。具体说,当有一条记录需要更新的时候,InoDB引擎会先记录到redo log,并更新内存,这时候更新就算完成了。同时InnoDb引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。crash-safe
。只要数据库的物理记录还在redo log中,就是服务器数据库出现问题重启,数据库恢复后,数据记录仍然可以恢复。Mysql基础架构整体分为两部分:Server层和引擎层,引擎层主要负责存储相关的事宜。上面说到在引擎层有自己的日志,而且只在InnoDB引擎中才有。Server层也有自己的日志,称为binlog(归档日志)。它是采用追写入日志的方式。追加写是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
只依靠redo日志的crash-safe特性在应对数据库误删,表数据误删等操作时候,有些时候redo日志是无力的,但是binlog日志解决这些问题,因为binlog会记录所有的逻辑操作,并且采用“追加写”的形式。
举个例子如果公司员工发现某天下午有一个误删表数据操作,要求找回数据,应该怎么做?(注:这里要考虑是在刚备份之后误删除,还是备份之前误删除,下面的例子是在备份之前删除的,找之前删除的数据)
redo日志与binlog日志有哪些不同?
其实上面好多都提到过,再次总结一遍,加深印象。
环状结构
,循环写,空间固定会用完,用完后需要擦除;binlog是可以追加写入的。“追加写”是只belog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。数据库语句:
mysql> update Student set c=c+1 where ID=2;
通过分析这一条更新语句,画出流程图,问题3也就得到解决。
注意流程图的最后三步,这是更新语句和日志关系密切的地方,将redo日志拆成了两个步骤:prepare和commit,它俩的中间是执行器写入binlog。(注:如果不这么做,假如一个日志提交成功的时候,另一个日志提交之前发生了数据库发生了崩溃,但是crash-safe恢复或者误删库恢复的时候可能造成二者数据不统一出现问题。)
redo log
innodb_flush_log_at_trx_commit
这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数我建议你设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。
binlog
sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数我也建议你设置成 1,这样可以保证 MySQL 误删除操作(删除表数据,删除库数据) 通过binlog 仍可恢复。
大家会不会想有了redo日志就可以了,为什么还要出现binlog日志呢?
解答:
这里有一个问题:如果在擦除和记账重合那一刻,数据库异常重启了,新的数据库操作会怎么记录,是擦除一部分,记录上,会丢失,还是等待重启后往上添加数据?
关于数据库备份,是一天一备比较好,还是一周一备份比较好,一般备份文件保留多久?提出问题4解决
解答:对于数据库备份周期这个问题,需要考虑以下指标:数据存量、增量、备份成本、恢复效率。
总的来说就是和项目,需求,场景有很多关系。
写入redo日志也是io操作,数据更新直接写入磁盘也是io操作,为什么说写入redo日志效率高节省io成本呢?
解答:redo日志的写入是顺序写入的,不用去“找位置”,而直接更新数据到磁盘的话,需要到磁盘中找到位置再写入,肯定前者的效率高。
以上内容是关于数据库日志系统的讲解,同时解决了我开篇提出的几个数据库日志相关的问题,希望能帮助大家更好的了解学习数据库,如果有问题可以随时关注公众号联系,互相学习哦。
博客的图片丢失了
最近写的一个用户数据统计相关接口,需要用到按照每天进行分组统计。
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( create_time, "%Y-%m-%d" )
date 参数是合法的日期。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 位 |
看一段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);
}
for循环中出现多个异步函数(比如ajax请求,或者node后端执行一些数据库操作或文件操作),如果想要这些异步串行变为同步应该怎么做?
预期结果
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。所以最终运行后会出现上面的结果,与预期结果不符。
注:关于宏任务队列,同步任务等相关的问题,如果有问题,可以查看我的另一篇文章详细了解。
想要得到预期的结果,第一种办法是使用闭包,在闭包函数内部形成了局部作用域,每循环一次,形成一个自己的局部作用域,不受外部变量变化的影响。
代码如下:
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);
}
将代码中的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);
}
for循环中使用异步,在node.js后端开发或者前端ajax请求的时候还是比较常见的。有多种解决方案
选择我个人认为最优秀的解决方式3async和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 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。
我对阮一峰老师的话再具体说明一下,可能有些同学还不是特别理解。实际上我们调用了await,这时候await这条语句下面的语句已经不会执行了(内同),而是先给外层async函数返回了一个promise对象,await后面对应的应该也是一个promise对象只有该对象 resolve 掉,产生结果,await 那一行代码才算真正执行完,才继续往下走。(注意:await执行之后应该是一个resolve的结果而不是promise对象了)。
看一段后端项目中应用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)
}
在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
代码:
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]
代码:
let arr = ['数字','花朵','数字'];
let result=[];
for (let i of arr) {
if (!result.includes(i)) {
result.push(i)
}
}
代码:
let array=['数字','花朵','数字','地球','人类','头发','眼睛','细胞'];
console.log(...(new Set(array)))
代码:
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
说明:
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
先上两段代码:
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");
通过上面两段代码引出我们今天要说的相关内容
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
看一下文初定义的一个的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']
//直接打印类
console.log(Person);
console.log(Point===Point.prototype.constructor);//true
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
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() {
// ...
};
附录:
BFC(Block Formatting Context),块级格式化上下文,是一个独立的渲染区域,让处于 BFC 内部的元素与外部的元素相互隔离,使内外元素的定位不会相互影响。
在IE下, Layout,可通过
zoom:1
触发
.BFC布局与普通文档流布局区别
普通文档流布局:
BFC布局规则:
开发中的应用
对于一个做后台不久的我,起初做项目只是实现了功能,所谓的增删改查,和基本查询索引的建立。直到有一个面试官问我一个问题,一条sql查询语句在mysql数据库中具体是怎么执行的?我被虐了,很开心,感谢他。于是开始了深入学习mysql。本篇文章通过
一条sql查询语句在mysql数据库中具体是怎么执行的?
来具体讲解mysql的基础架构。
mysql> select * from Student where ID=1;
上面一条简单的查询语句很简单,但我想好多开发者并不知道在MYSQL内部的执行过程。
从图中可以看出Mysql可以大体分为Server层和存储引擎层两部分。
Server层包括连接器、查询缓存、分析器、优化器、执行器等,这些涵盖了MySQL的大多数核心服务和所有的内置函数(如日期、时间、数学和加密函数等),跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
存储引擎层负责数据的存储和提取,提供数据的读写接口。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。目前开发中最常用的存储引擎是InnoDB,它从MySQL5.5.5版本开始成为默认存储引擎,不过开发者也可以通过指定存储引擎的类型来选择别的引擎。
即使存储引擎不同,但是也会共用一个Server层,接下来对Server层中的执行流程,依次对其作用进行讲解。
运行查询语句开始查询的前提是第一步先连接数据库,这时候等待你的就是连接器。连接器负责和客户端建立连接、获取权限、维持和管理连接。
常规的开发模式,客户端与服务器需要建立连接。二者在完成经典的TCP握手后,Server层连接器就要开始认证你的身份,这个时候是服务器端代码使用的用户名和密码。
连接器一些内容说明:
mysql>show processlist
连接断开相关:客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。
如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒: Lost connection to MySQL server during query。这时候如果你要继续,就需要重连,然后再执行请求了。数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接。短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。
建立连接的过程通常是比较复杂的,所以我建议你在使用中要尽量减少建立连接的动作,也就是尽量使用长连接。
较好的连接方式长连接产生的问题以及解决办法:
全部使用长连接后,你可能会发现,有些时候 MySQL 占用内存涨得特别快,这是因为 MySQL 在执行过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时候才释放。所以如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(OOM),从现象看就是 MySQL 异常重启了。怎么解决这个问题呢?你可以考虑以下两种方案。
定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。
如果你用的是 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;
两种关联查询方案结果肯定是一样的,但是执行效率会有不同,优化器就是决定选择使用哪一个方案。
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深入的东西。
(sequelize)bulkCreate函数中的updateOnDuplicate参数怎么使用?
Sequelize中提供的增删改查相关的函数都能转成原生的sql语句。本篇文章介绍一个不是很常见但是有时候批量插入很方便的函数——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参数中的行键已存在我的理解是:数据库表中现有的记录的唯一索引或者主键如果已经存在,执行更新操作
在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`);
之前写了一篇关于vue面试总结的文章, 有不少网友提出组件之间通信方式还有很多, 这篇文章便是专门总结组件之间通信的
vue是数据驱动视图更新的框架, 所以对于vue来说组件间的数据通信非常重要,那么组件之间如何进行数据通信的呢?
首先我们需要知道在vue中组件之间存在什么样的关系, 才更容易理解他们的通信方式, 就好像过年回家,坐着一屋子的陌生人,相互之间怎么称呼,这时就需要先知道自己和他们之间是什么样的关系。
vue组件中关系说明:
如上图所示, A与B、A与C、B与D、C与E组件之间是父子关系; B与C之间是兄弟关系;A与D、A与E之间是隔代关系; D与E是堂兄关系(非直系亲属)
针对以上关系我们归类为:
本文会介绍组件间通信的8种方式如下图目录所示:并介绍在不同的场景下如何选择有效方式实现的组件间通信方式,希望可以帮助小伙伴们更好理解组件间的通信。
props
/ $emit
父组件通过props
的方式向子组件传递数据,而通过$emit
子组件可以向父组件通信。
下面通过一个例子说明父组件如何向子组件传递数据:在子组件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 只读,不可被修改,所有修改都会失效并警告。
对于$emit
我自己的理解是这样的: $emit
绑定一个自定义事件, 当这个语句被执行时, 就会将参数arg传递给父组件,父组件通过v-on监听并接收参数。 通过一个例子,说明子组件如何向父组件传递数据。
在上个例子的基础上, 点击页面渲染出来的ariticle
的item
, 父组件中显示在数组中的下标
// 父组件中
<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
上面这张图片是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
/ inject
是vue2.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
又称为事件总线,在vue中可以使用它来作为沟通桥梁的概念, 就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件, 所以组件都可以通知其他组件。
eventBus也有不方便之处, 当项目较大,就容易造成难以维护的灾难
在Vue的项目中怎么使用eventBus
来实现组件之间的数据通信呢?具体通过下面几个步骤
首先需要创建一个事件总线并将其导出, 以便其他模块可以使用或者监听它.
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
假设你有两个组件: additionNum
和 showNum
, 这两个组件可以是兄弟组件也可以是父子组件;这里我们以兄弟组件为例:
<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>
// 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
展示求和的结果.
如果想移除事件的监听, 可以像下面这样操作:
import { eventBus } from 'event-bus.js'
EventBus.$off('addition', {})
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化.
Vuex 解决了多个视图依赖于同一状态
和来自不同视图的行为需要变更同一状态
的问题,将开发者的精力聚焦于数据的更新而不是数据在组件之间的传递上
state
:用于数据的存储,是store中的唯一数据源getters
:如vue中的计算属性一样,基于state数据的二次包装,常用于数据的筛选和多个数据的相关性计算mutations
:类似函数,改变state数据的唯一途径,且不能用于处理异步事件actions
:类似于mutation
,用于提交mutation
来改变状态,而不直接变更状态,可以包含任意异步操作modules
:类似于命名空间,用于项目中将各个模块的状态分开定义和操作,便于维护// 父组件
<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组件是隔代关系, 那它们之前进行通信有哪些方式呢?
props
绑定来进行一级一级的信息传递, 如果D组件中状态改变需要传递数据给A, 使用事件系统一级级往上传递eventBus
,这种情况下还是比较适合使用, 但是碰到多人合作开发时, 代码维护性较低, 可读性也低在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
; vuexeventBus
;Vuex;provide
/ inject
、$attrs
/ $listeners
今天就分享这么多,如果对分享的内容感兴趣,可以关注公众号「程序员成长指北」,或者加入技术交流群,大家一起讨论。
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 表链接方式将多个关系数据表中的数据用一条简单的查询语句查询出来。
而 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是多变的。它最适合于具有不确定需求的数据。典型的使用场景是社交网络,客户管理和网络分析系统。
两个项目,大家看下分别适合哪种类型数据库?为什么这么选择?在评论区讨论
结论:在栈内存中的数据发生数据变化的时候,系统会自动为新的变量分配一个新的之值在栈内存中,两个变量相互独立,互不影响的。
大概新的之值是新的子值吧。
js最初设计是运行在浏览器中,单线程是为了防止DOM渲染冲突问题
比如有这样一个场景: 假设有2个线程, 当它们同时对同一个DON进行操作, 一个进行修改,而另一个进行删除, 此时就会产生冲突 所以为了避免出现这样的冲突, javascript从一诞生就是单线程.
但是呢单线程始终是一个痛点, 为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程。但是子线程完全受主线程控制,且不得操作 DOM
又是七月面试季节,又一波大学生进入社会,面临着各种面试问题,网上有很多面经,但更多的是在讲面试的经历或者面试被问到的技术相关的。这里我给大家分享一些无关乎技术,但在面试中需要注意的一些事项。
友情提示:去面试的时候,千万别空着手去啊!
不要误会,不是让你给面试官送礼,毕竟我们都是金光闪闪的程序员,靠技术说话,不吃这一套。
面试的时候:一定要带简历!带简历!带简历!重要的事情说三遍。我在面试其他人的时候,都会先看对方的简历,然后才开始面试。从你的简历中能够看出非常多的信息,甚至看到你的简历就能初步确定你是不是我们要找的人(因为约面试一般是hr处理的,技术面你时不一定看过你的简历)。
你能体会到在面试官满怀期待的眼神中,你一句“没带”的无奈吗?交给面试官的简历不需要做的很花哨,但是也要保持干净整洁,千万不要从你的混合零钱钥匙的裤兜里掏出你叠了几次的简历。
很多人可能觉得,面试的时候自己都比紧张,很容易忽视细节,更没有心情去关心面试官是一个什么样的人。但其实这是很重要的一个点,因为如果你入职了,面试官就会成为你的同事甚至上级,作为开发人员,队友和上级这两个角色都挺重要的呀!
那么怎么样来观察面试官是一个什么样的人呢?下面3个点,你要注意了:
在面试问答环节,当面试官问到你回答不上来的问题时,看面试官是咄咄逼人的感觉还是以交流的态度引导你来认识这个问题,如果是前者,你可能要考虑一下要不要选择加入这个团队,因为这也是选择未来共事的人
看看面试官的精神面貌,可能有的人觉得程序员还有什么精神面貌可看的,其实这里是想让你看看面试官是不是非常疲劳的样子,因为有的公司长期加班,很多开发者都处于疲劳状态;而这个状态以后也可能是你的工作常态
在面试中,基本上在面试快要结束的时候,面试官都会问,“你有没有什么想了解我们公司的”或者“你有什么想问的问题”,有很多人都容易忽略这个问题,直接回答“没有了”。这真的是一个很失败的回答,因为对于面试官来讲,你没有想要了解公司的欲望,或者说不太感兴趣;对于你自身来说,错失了你最有机会了解公司招聘你这个岗位的动机。
这个环节一般可以提问两三个问题,不宜太多。我觉得以下几个问题可以根据你的情况提问:
问:如果我被录用了,接下来我要做什么?这个问题挺重要的,涉及到了你后面在团队中要干的活,也是你了解可能参与项目的业务以及开发会使用到哪些技术栈,这样你心理大概有一个谱,和你会的技术栈是否相符等,帮助你考虑要不要加入他们
可以问问开发团队规模,这个问题可以知道你进入团队遇到难题有没有人一起讨论解决,如果你期望找到有人可以带带你的公司,这个问题就很有必要
剩下可以问问有没有技术分享会或者其他分享会议, 如果聊的好,也可以问问在面试时遇到的自己不会的问题或者技术点,但不是在网上立马就能找到答案的或者说固有答案的,也可以问问面试官,或者说一起探讨
上面说了几个要问点, 下面也提一下有些此时问题不适合问的:
直接问薪资是不可取的,在技术面试进行面试时,千万别问薪资,人家会觉得你很不专业,并且心思都放在了钱上,而且谈薪资也不是技术要和你详细谈的事。
不要问私事:有些面试者比较随意,想直接从面试官身上获得一手的信息,比如问面试官“一个月工资多少钱”、“平时你加班严重吗”、“你觉得这家公司怎么样”这就好像一个刚认识的人打探你的家事一样,会让人很反感的。
有很多公司为了招人,会向面试者许诺期权、股权之类的,其实很多时候,这只是安抚员工的一种形式,和直接给钱比,还是给钱更划算。
据我所知,很多小公司社保和公积金都不交的,或者交一个,但基数也小,这个怎么说呢,有些小公司,考虑到自己的发展,有时候也可以理解。
因为每个人的能力不同,有些面试者可能是学历或者能力的问题,进不了大公司,但是就像上一点提到的那样,我还是建议你去一个能正常给你交社保和公积金的公司,哪怕少一点。
举个例子,有的公司不提供工作手机,希望使用你自己的手机用于开发测试,更有的公司使用自己的电脑设备开发,不给予任何补助,在这种情况下,你需要考虑下,因为在你的财产方面都没有收到尊重,你会期望以后工作中还能受到更多的尊重吗?
在面试一家公司前,首先你要做的是去网上了解一下这家公司大概的情况,主要业务是哪些?有哪些部门?最近几年的效益和口碑怎么样?等等。这不仅仅有助于你面试,还有助于你面试结束决定是否去这家公司上班。
除此之外,到公司之后,可以简单参观参观公司的情况,感受一下员工的工作状态,周围的工作环境等等,自己喜不喜欢这样的环境?
很多人特别是一些初入行的问要不要去外包公司,也有很多人想去外包公司,以为去外包公司可以锻炼自己,可事实是否如此呢?
不是外包不挣钱,是外包对技术提升没有多少用处,一篇程序猿的相关文章分析,想去外包公司的无非三种人
2.为了多拿一点点钱的人,有些人如果去非外包公司可能只能拿到6k,但去外包公司可能拿到7k元,这种人,我想提醒你们,去非外包公司,只要你好好干,这6k可能会变成10k,但去外包公司你那7k元,两年后撑死变成8k。
3.就是完全找不到工作的人,因为外包公司做的是量,大量外派人员到其他大公司,就是卖人。
我也很认同所分析出的这三种人,经常看到一些在外包公司工作的设计师们,我们下班了,还在加班;我们放假了,还在加班;我们睡前刷刷朋友圈,也会有那么几个在外包公司工作的在晒加班。。
今天就分享这么多,如果对分享的内容感兴趣,可以关注公众号,或者加入技术交流群,大家一起讨论。
有些内容虽然不用,但是不代表面试不考。这里为大家送上5道常见的手写面试题,希望你们在面试中能遇到某一道也好,这样这篇文章就没有白写。
/**
* 模仿 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
* @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
* @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
* @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
* @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.js。它相比于React与Angular上手更加容易,或许这也是很多初学者选择vue的原因之一。
我们看到很多招聘上都写着掌握vue开发项目,那么面试都会问什么呢?别急,下面是我给大家整理了一份比较全面的Vue面试高频考题解析小册。
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
: 更新元素的 innerHTMLv-show
与 v-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生命周期总共分为8个阶段: 创建前/后,载入前/后,更新前/后, 销毁前/后。
$el
和数据对象 data
都是undefined
, 还未初始化data
数据初始化, el
还未初始化$el
和data
都初始化了, 相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上。el
被新创建的 vm.$el
替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html
内容替换el
属性指向的DOM
对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
具体实现步骤,感兴趣的可以看看:
//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>
Object.defineProperty() 的问题主要有三个:
Proxy 在 ES2015 规范中被正式加入,它有以下几个特点:
除了上述两点之外,Proxy 还拥有以下优势:
vue-router全局有三个守卫:
使用方法:
// 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 全局后置钩子');
});
如果你不想全局配置守卫的话,你可以为某些路由单独配置守卫:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// 参数用法什么的都一样,调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖
// ...
}
}
]
})
this
,组件实例还没被创建this
this
在浏览器中符号“#”,#以及#后面的字符称之为hash,用window.location.hash读取;
特点:hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。
hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 http://www.xiaogangzai.com
,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。
history采用HTML5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。
history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 http://www.xxx.com/items/id
。后端如果缺少对 /items/id 的路由处理,将返回 404 错误。Vue-Router 官网里如此描述:“不过这种模式要玩好,还需要后台配置支持……所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。”
组件之间通讯分为三种: 父传子、子传父、兄弟组件之间的通讯
使用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>
父组件向子组件传递事件方法,子组件通过$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>
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不怎么熟
vuex的使用借助官方提供的一张图来说明:
Vuex有5种属性: 分别是 state、getter、mutation、action、module;
Vuex 使用单一状态树,即每个应用将仅仅包含一个store 实例,但单一状态树和模块化并不冲突。存放的数据状态,不可以直接修改里面的数据。
mutations定义的方法动态修改Vuex 的 store 中的状态或数据。
类似vue的计算属性,主要用来过滤一些数据。
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的底层源码;或者是具体在项目中遇到的问题,下面列举几个项目中可能遇到的问题:
开发时,改变数组或者对象的数据,但是页面没有更新如何解决?
vue弹窗后如何禁止滚动条滚动?
如何在 vue 项目里正确地引用 jquery 和 jquery-ui的插件
大家好,我是koala,公众号「程序员成长指北」作者,这篇文章是【JS必知必会系列】的高阶函数讲解。目前在做一个node后端工程师进阶路线,加入我们一起学习吧!
就是来装页面上的元素的矩形区域。CSS中的盒子模型包括IE盒子模型和标准的W3C盒子模型。
从上图可以看到标准 W3C 盒子模型的范围包括 margin、border、padding、content,并且 content 部分不包含其他部分。
从上图可以看到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就从什么开始算起。
上一篇讲Mysql基本架构时,以“sql查询语句在MySql架构中具体是怎么执行的?”进行了全面的讲解。知道了sql查询语句在MySql架构中的具体执行流程,但是为了能够更好更快的写出sql语句,我觉得非常有必要知道sql语句中各子句的执行顺序。看过上一篇文章的小伙伴应该都知道,sql语句最后各子句的执行应该是在执行器中完成的,存储引擎对执行器提供的数据读写接口。现在开始我们的学习
所有的 查询语句都是从from开始执行的,在执行过程中,每个步骤都会为下一个步骤生成一个虚拟表,这个虚拟表将作为下一个执行步骤的输入。
form是一次查询语句的开端。
如果from后面是多张表,join关联,会首先对前两个表执行一个笛卡尔乘积,这时候就会生成第一个虚拟表T1(注意:这里会选择相对小的表作为基础表);
对虚表T1进行ON筛选,只有那些符合的行才会被记录在虚表T2中。(注意,这里的这里如果还有第三个表与之关联,会用T2与第三个表进行笛卡尔乘积生产T3表,继续重复3. on步骤生成T4表,不过下面的顺序讲解暂时不针对这里的T3和T4,只是从一个表关联查询T2继续说)
对虚拟表T2进行WHERE条件过滤。只有符合的记录才会被插入到虚拟表T3中。
group by 子句将中的唯一的值组合成为一组,得到虚拟表T4。如果应用了group by,那么后面的所有步骤都只能操作T4的列或者是执行6.聚合函数(count、sum、avg等)。(注意:原因在于分组后最终的结果集中只包含每个组中的一行。谨记,不然这里会出现很多问题,下面的代码误区会特别说。)
聚合函数只是对分组的结果进行一些处理,拿到某些想要的聚合值,例如求和,统计数量等,并不生成虚拟表。
应用having筛选器,生成T5。HAVING子句主要和GROUP BY子句配合使用,having筛选器是第一个也是为唯一一个应用到已分组数据的筛选器。
执行select操作,选择指定的列,插入到虚拟表T6中。
对T6中的记录进行去重。移除相同的行,产生虚拟表T7.(注意:事实上如果应用了group by子句那么distinct是多余的,原因同样在于,分组的时候是将列中唯一的值分成一组,同时只为每一组返回一行记录,那么所以的记录都将是不相同的。 )
应用order by子句。按照order_by_condition排序T7,此时返回的一个游标,而不是虚拟表。sql是基于集合的理论的,集合不会预先对他的行排序,它只是成员的逻辑集合,成员的顺序是无关紧要的。对表进行排序的查询可以返回一个对象,这个对象包含特定的物理顺序的逻辑组织。这个对象就叫游标。
oder by的几点说明
取出指定行的记录,产生虚拟表T9, 并将结果返回。
limit后面的参数可以是 一个limit m ,也可以是limit m n,表示从第m条到第n条数据。
(注意:很多开发人员喜欢使用该语句来解决分页问题。对于小数据,使用LIMIT子句没有任何问题,当数据量非常大的时候,使用LIMIT n, m是非常低效的。因为LIMIT的机制是每次都是从头开始扫描,如果需要从第60万行开始,读取3条数据,就需要先扫描定位到60万行,然后再进行读取,而扫描的过程是一个非常低效的过程。所以,对于大数据处理时,是非常有必要在应用层建立一定的缓存机制)
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个用户记录和他们的头像。
pk记录表的数据结构设计,每个用户每天每个馆下可能会有多条记录,所以需要进行分组,并且查询结果只想拿到每个分组内最高的那条记录。
这段sql的一些说明:
看一下代码和执行结果与带有子查询的进行比较,就能理解我上面说的一段话:
//不使用子查询
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;
在 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
select user_id,name from User where user_id = ‘10000050’
在有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相关)
注意:索引和关联我会单独拿出来两篇文章进行详细讲解,在这个注意事项中只是简单提一下。
之前面试了几个开发者,他们确实做过不少项目,能力也是不错的,但是发现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
本篇文章会以一个面试官问问题的角度来进行分析讲解
面试官:说一说
javascript
中有哪些数据类型?
JavaScript
**有七种内置数据类型,包括基本类型和对象类型。
基本类型分为以下六种:
注意:
string
、number
、boolean
和 null
undefined
这五种类型统称为原始类型(Primitive),表示不能再细分下去的基本类型;
symbol
是ES6中新增的数据类型,symbol
表示独一无二的值,通过 Symbol
函数调用生成,由于生成的 symbol 值为原始类型,所以 Symbol
函数不能使用 new
调用;
null
和 undefined
通常被认为是特殊值,这两种类型的值唯一,就是其本身。
对象类型也叫引用类型,array
和function
是对象的子类型。对象在逻辑上是属性的无序集合,是存放各种值的容器。对象值存储的是引用地址,所以和基本类型值不可变的特性不同,对象值是可变的。
面试官:说说你对
javascript
是弱类型语言的理解?
JavaScript
是弱类型语言,而且JavaScript
声明变量的时候并没有预先确定的类型,变量的类型就是其值的类型,也就是说变量当前的类型由其值所决定,夸张点说上一秒种的String
,下一秒可能就是个Number
类型了,这个过程可能就进行了某些操作发生了强制类型转换。虽然弱类型的这种不需要预先确定类型的特性给我们带来了便利,同时也会给我们带来困扰,为了能充分利用该特性就必须掌握类型转换的原理。
面试官:
javascript
中强制类型转换是一个非常易出现bug
的点,知道强制转换时候的规则吗?
注:规则最好配合下面什么时候发生转换使用这些规则看效果更佳。
ToPrimitive
(转换为原始值)ToPrimitive
对原始类型不发生转换处理,只针对引用类型(object)的,其目的是将引用类型(object)转换为非对象类型,也就是原始类型。
ToPrimitive
运算符接受一个值,和一个可选的期望类型作参数。ToPrimitive
运算符将值转换为非对象类型,如果对象有能力被转换为不止一种原语类型,可以使用可选的 期望类型 来暗示那个类型。
转换后的结果原始类型是由期望类型决定的,期望类型其实就是我们传递的type
。直接看下面比较清楚。
ToPrimitive
方法大概长这么个样子具体如下。
/**
* @obj 需要转换的对象
* @type 期望转换为的原始数据类型,可选
*/
ToPrimitive(obj,type)
string
:obj
的toString
方法,如果为原始值,则return
,否则进行第2步obj
的valueOf
方法,如果为原始值,则return
,否则进行第3步TypeError
异常number
:obj
的valueOf
方法,如果为原始值,则return
,否则进行第2步obj
的toString
方法,如果为原始值,则return
,否则第3步TypeError
异常Date
,则type被设置为String
Number
对于Date
数据类型,我们更多期望获得的是其转为时间后的字符串,而非毫秒值(时间戳),如果为number
,则会取到对应的毫秒值,显然字符串使用更多。
其他类型对象按照取值的类型操作即可。
ToPrimitive
总结ToPrimitive
转成何种原始类型,取决于type,type参数可选,若指定,则按照指定类型转换,若不指定,默认根据实用情况分两种情况,Date
为string
,其余对象为number
。那么什么时候会指定type类型呢,那就要看下面两种转换方式了。
Object.prototype.toString()
toString()
方法返回一个表示该对象的字符串。
每个对象都有一个 toString()
方法,当对象被表示为文本值时或者当以期望字符串的方式引用对象时,该方法被自动调用。
这里先记住,valueOf()
和 toString()
在特定的场合下会自行调用。
Object.prototype.valueOf()
方法返回指定对象的原始值。
JavaScript
调用 valueOf()
方法用来把对象转换成原始类型的值(数值、字符串和布尔值)。但是我们很少需要自己调用此函数,valueOf
方法一般都会被 JavaScript
自动调用。
不同内置对象的valueOf
实现:
对照代码会更清晰一些:
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
运算符转换规则:
null
转换为 0undefined
转换为 NaN
true
转换为 1,false
转换为 0 NaN
注意:对象这里要先转换为原始值,调用
ToPrimitive
转换,type指定为number
了,继续回到ToPrimitive
进行转换。
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
ToBoolean
运算符转换规则
除了下述 6 个值转换结果为 false
,其他全部为 true
:
假值以外的值都是真值。其中包括所有对象(包括空对象)的转换结果都是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
面试官问:知道了具体转换成什么的规则,但是都在什么情况下发生什么样的转换呢?
在没有对象的前提下
字符串的自动转换,主要发生在字符串的加法运算时。当一个值为字符串,另一个值为非字符串,则后者转为字符串。
'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
详细举例说明如下:
string
,ToPrimitive
原始值转换后不发生变化ToPrimitive
进行原始值转换,由于指定的type是number
,进行ToPrimitive
转化调用obj2.valueof()
,得到的不是原始值,进行第三步toString()
return 'a'
string
,而且是+
号运算符则都采用String
规则转换为string
类型进行拼接2a
对下面'2'+obj1
详细举例说明如下:
string
,ToPrimitive
转换为原始值后不发生变化ToPrimitive
进行原始值转换,由于指定的type是number
,进行ToPrimitive
转化调用obj2.valueof()
,得到{ a: 1, b: 2 }
toString()
return [object Object]
string
,而且是+号运算符则都采用String
规则转换为string
类型进行拼接2[object Object]
代码中几种特殊对象的转换规则基本相同,就不一一说明,大家可以想一下流程。
注意:不管是对象还不是对象,都有一个转换为原始值的过程,也就是ToPrimitive
转换,只不过原始类型转换后不发生变化,对象类型才会发生具体转换。
var obj = {
width: '100'
};
obj.width + 20 // "10020"
预期输出结果120 实际输出结果10020
有加法运算符,但是无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
的例子
x
,y
均为number
,直接比较1 == 2 //false
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
boolean
,按照ToNumber
将boolean
转换为1或者0,再进行后面比较//boolean 先转成number,按照上面的规则得到1
//3 == 1 false
//0 == 0 true
3 == true // false
'0' == false //true
4.如果x
为string
,y
为number
,x
转成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
面试官问:如何判断数据类型?怎么判断一个值到底是数组类型还是对象?
三种方式,分别为 typeof
、instanceof
和 Object.prototype.toString()
通过 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'
上面代码的输出结果可以看出,
null
的判定有误差,得到的结果
如果使用 typeof
,null
得到的结果是object
操作符对对象类型及其子类型,例如函数(可调用对象)、数组(有序索引对象)等进行判定,则除了函数都会得到 object
的结果。
综上可以看出typeOf
对于判断类型还有一些不足,在对象的子类型和null
情况下。
通过 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]]
null
和 undefined
能够输出结果是内部实现有做处理NaN
的概念NaN
是一个全局对象的属性,NaN
是一个全局对象的属性,NaN
是一种特殊的Number
类型。
一些例子:
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
toString
和String
的区别toString
toString()
可以将数据都转为字符串,但是null
和undefined
不可以转换。
console.log(null.toString())
//报错 TypeError: Cannot read property 'toString' of null
console.log(undefined.toString())
//报错 TypeError: Cannot read property 'toString' of undefined
toString()
括号中可以写数字,代表进制
二进制:.toString(2);
八进制:.toString(8);
十进制:.toString(10);
十六进制:.toString(16);
String
String()
可以将null
和undefined
转换为字符串,但是没法转进制字符串
console.log(String(null));
// null
console.log(String(undefined));
// undefined
为什么写拷贝这篇文章?同事有一天提到了拷贝,他说赋值就是一种浅拷贝方式,另一个同事说赋值和浅拷贝并不相同。
我也有些疑惑,于是我去MDN搜一下拷贝相关内容,发现并没有关于拷贝的实质概念,没有办法只能通过实践了,同时去看一些前辈们的文章总结了这篇关于拷贝的内容,本文也属于公众号【程序员成长指北】学习路线中【JS必知必会】内容。
基本类型:undefined,null,Boolean,String,Number,Symbol
引用类型:Object,Array,Date,Function,RegExp等
栈内存
中(不包含闭包
中的变量)堆内存
中。而栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址(引用),引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。注意:
闭包
中的变量并不保存在栈内存中,而是保存在堆内存中。这一点比较好想,如果闭包
中的变量保存在了栈内存
中,随着外层中的函数从调用栈中销毁,变量肯定也会被销毁,但是如果保存在了堆内存中,内存函数仍能访问外层已销毁函数中的变量。看一段对应代码理解下:function A() {
let a = 'koala'
function B() {
console.log(a)
}
return B
}
看一段代码
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(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
是一个浅拷贝,它只是在根属性(对象的第一层级)创建了一个新的对象,但是对于属性的值是对象的话只会拷贝一份相同的内存地址。
undefined
和null
无法转成对象,它们不能作为Object.assign
参数,但是可以作为源对象Object.assign(undefined) // 报错
Object.assign(null) // 报错
let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true
Symbol
值的属性,可以被Object.assign拷贝。这个函数在浅拷贝概念定义的时候已经进行了分析,看上文。
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
for...in语句以任意顺序遍历一个对象自有的、继承的、可枚举的
、非Symbol的属性。对于每个不同的属性,语句都会被执行。
hasOwnProperty
语法:obj.hasOwnProperty(prop)
prop是要检测的属性字符串
名称或者Symbol
该函数返回值为布尔值,所有继承了 Object 的对象都会继承到 hasOwnProperty 方法,和 in 运算符不同,该函数会忽略掉那些从原型链上继承到的属性和自身属性。
说了赋值操作和浅拷贝操作,大家是不是已经能想到什么是深拷贝了,下面直接说深拷贝的定义。
深拷贝会另外拷贝一份一个一模一样的对象,从堆内存中开辟一个新的区域存放新对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
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()虽然可以实现深拷贝,但是还有一些弊端比如不能处理函数等。
深拷贝,主要用到的**是递归,遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。
实现代码:
//定义检测数据类型的功能函数
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
用一张图总结
今天就分享这么多,如果对分享的内容感兴趣,可以关注公众号「程序员成长指北」,或者加入技术交流群,大家一起讨论。
进阶技术路线
讲config之前,先说一下环境变量这个事。
Linux系统中查看所有环境变量
env
node中常用到的环境变量是NODE_ENV,查看是否存在
echo $NODE_ENV
Linux系统中添加环境变量
env
某些时候需要删除环境变量
unset NODE_ENV
环境变量追加值
export PATH=$PATH:/home/download:/usr/local/
安装
npm install config
代码中使用
config安装成功后,可以在项目中创建几种环境的配置文件。
{
"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
export NODE_ENV='development' && node app.js
```linux
- pm2指定名称与环境启动app服务
```linux
pm2 start app.js --env development --name koala
--name name指的是pm2指定的进程名称。
<style>
.hr.scale-half {
height: 1px;
transform: scaleY(0.5);
}
</style>
<p>1px + scaleY(0.5)</p>
<div class="hr scale-half"></div>
利用动态的改变<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 单独是这样:
<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>
面试题如下,大家可以先试着写一下输出结果,然后再看我下面的详细讲解,看看会不会有什么出入,如果把整个顺序弄清楚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 做这件事。这个库负责各种回调函数的执行时间,异步任务最后基于事件循环机制还是要回到主线程,一个个排队执行。
异步任务可以分成两种。
所谓”循环”,指的是事件循环(event loop)。这是 JavaScript 引擎处理异步任务的方式,后文会详细解释。这里只要理解,本轮循环一定早于次轮循环执行即可。
Node 规定,process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环。
1)process.nextTick不要因为有next就被好多小伙伴当作次轮循环。
2)Node 执行完所有同步任务,接下来就会执行process.nextTick的任务队列。
3)开发过程中如果想让异步任务尽可能快地执行,可以使用process.nextTick来完成。
根据语言规格,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对象这种,开启之后直接进入微队列中,微队列内的就是那个任务快就那个先执行完,但是针对于队列与队列之间不同的任务,还是会有先后顺序,这个先后顺序是由队列决定的。
事件循环最阶段最详细的讲解(官网:https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#setimmediate-vs-settimeout)
timers阶段
次阶段包括setTimeout()和setInterval()
IO callbacks
大部分的回调事件,普通的caollback
poll阶段
网络连接,数据获取,读取文件等操作
check阶段
setImmediate()在这里调用回调
close阶段
一些关闭回调,例如socket.on('close', ...)
1)Node 开始执行脚本时,会先进行事件循环的初始化,但是这时事件循环还没有开始,会先 完成下面的事情。
同步任务
发出异步请求
规划定时器生效的时间
执行process.nextTick()等等
最后,上面这些事情都干完了,事件循环就正式开始了。
2)事件循环同样运行在单线程环境下,高并发也是依靠事件循环,每产生一个事件,就会加入到该阶段对应的队列中,此时事件循环将该队列中的事件取出,准备执行之后的callback。
3)假设事件循环现在进入了某个阶段,即使这期间有其他队列中的事件就绪,也会先将当前队列的全部回调方法执行完毕后,再进入到下一个阶段。
由于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执行。
在那道面试题中,在同步任务的过程中,不知道大家有没有疑问,为什么不是执行完async2输出后执行async1 end输出,而是接着执行promise1?
解答:引用阮一峰老师书中一句话:“ async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。”
简单的说,先去执行后面的同步任务代码,执行完成后,也就是表达式中的 Promise 解析完成后继续执行 async 函数并返回解决结果。(其实还是本轮循环promise的问题,最后的resolve属于异步,位于本轮循环的末尾。)
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);
}
promise3和script end的执行顺序是否有疑问?
解答:因为立即resolved的Promise是本轮循环的末尾执行,同时总是晚于本轮循环的同步任务。 Promise 是一个立即执行函数,但是他的成功(或失败:reject)的回调函数 resolve 却是一个异步执行的回调。当执行到 resolve() 时,这个任务会被放入到回调队列中,等待调用栈有空闲时事件循环再来取走它。本轮循环中最后执行的。
顺序的整体总结就是:
同步任务-> 本轮循环->次轮循环
node.js官网:
欢迎大家关注我的公众号——程序员成长指北。请自行微信搜索——“程序员成长指北”
在javascript开发过程中,代码是单线程执行的,同步操作,彼此之间不会等待,这可以说是它的优势,但是也有它的弊端,如一些网络操作,浏览器事件,文件等操作等,都必须异步执行,针对这些情况,起初的操作都是使用回调函数实现。
实现方式如下(伪代码):
function One(callback) {
if (success) {
callback(err, result);
} else {
callback(err, null);
}
}
One(function (err, result) {
//执行完One函数内的内容,成功的结果回调回来向下执行
})
上述代码只是一层级回调,如果代码复杂后,会出现多层级的回调,代码可读性也会很差,那有没有一种方式,不用考虑里面的内容,直接根据结果成功还是失败执行下面的代码呢?有的,Promise(承诺),在ES6中对Promise进行了同意的规范。
书上这么说:
Promise 是异步编程的一种解决方案,比传统的解决方案–回调函数和事件--更合理和更强大。它由社区最早提出和实现,ES6将其写进了语言标准,统一了语法,原生提供了Promise
所谓Promise ,简单说就是一个容器,里面保存着某个未来才回结束的事件(通常是一个异步操作)的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。
Promise 对象的状态不受外界影响
Promise/a+ 官方网站给出的定义
A promise represents the eventual result of an asyncchronous operation
翻译:表示一个异步操作的最终结果。
我的理解:
Promise是回调函数可以规范的链式调用
注意Promise在某一时刻只能处于一种状态
Promise的状态改变,状态只能由pending转换为rejected或者rejected,一旦状态改变完成后将无法改变(不可逆性)
创建Promise需要用到Promise的构造函数来实现,代码如下:
var promise=new Promise(function(resolve,reject){
// ...some async code
if(/* 一些异步操作成功*/)
{
resolve(value);
}else
{
reject(error);
}
})
代码分析:
promise.then(function(value){
//success
},function(error){
});
代码分析:
promise.then(function (data){
//success
})
.catch(function(error){
//error
})
//之前创建promise操作后
promise.then(function(value){
conlose.log(value); //有值
}.then(function(value)
{
conlose.log(value); //未定义
});
代码分析:
转换的对象是一个常量或者不具备状态的语句,转换后的对象自动处于resolve状态。
转换的后的结果和原来一样
var promise =Promise.resolve("hello world");
promise.then(function(result){
console.log(result); //输出结果 hello world
})
转换的对象如果直接是一个异步方法,不可以这么使用。
代码如下
promise.all(
//一系列promise操作
).then(function(results){
}).catch(function(error){
});
代码分析:
项目开发中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的讲解就到这里,但是大家在开发过程中,会发现有些时候多次操作异步会出现很多层级的调用,也就是
promise.then(...)
.then(...)
.then(...)
这种情况,代码虽然看起来会比callback的回调简介和规范了很多,但是还是感觉一些复杂,有没有更好的解决办法呢?请看下一篇博客
回调的终极使用--async和await的讲解
最近写的一个接口需要用到子查询,并且子查询的结果并不是可以在另外一个查询语句中直接当作in的选项,没办法区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函数的第二个参数,是一个对象,对象里面几个常用参数进行说明。
对于查找条件where后面的字段
对于替换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)
})
sequelize
.query('SELECT * FROM projects', {
model: Projects,
mapToModel: true // 如果有任何映射字段,则在这里传递true
})
.then(projects => {
// Each record will now be an instance of Project
})
看一段我在开发过程中怎么使用的原生语句查询
代码如下:
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
一道经典面试题:
//JS实现一个无限累加的add函数
add(1) //1
add(1)(2) //3
add(1)(2)(3) //6
当大家看到这个面试题的时候,能否在第一时间想到使用高阶函数实现?想到在实际项目开发过程中,用到哪些高级函数?有没有想过自己创造一个高阶函数呢?开始本篇文章的学习
高阶函数
英文叫 Higher-order function
。高阶函数是对其他函数进行操作的函数,操作可以是将它们作为参数,或者返回它们。简单总结为高阶函数是一个接收函数作为参数
或者将函数作为返回输出
的函数。
Array.prototype.map
,Array.prototype.filter
,Array.prototype.reduce
和Array.prototype.sort
是JavaScript中内置的高阶函数。它们接受一个函数作为参数,并应用这个函数到列表的每一个元素。下面是一些内置高阶函数的具体说明讲解,以及和不使用高阶函数情况下的对比
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]
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]
//输出结果
["1", "2", "3"].map(parseInt);
看了这道题不知道会不会有大多数开发者认为输出结果是[1,2,3],错误
正确的输出结果为:
[1,NaN,NaN]
因为map
的callback
函数有三个参数,正在遍历的元素, 元素索引(index), 原数组本身(array)。parseInt
有两个参数,string和radix(进制),注意第二个参数进制当为0或者没有参数的时候,parseInt()会根据string来判断数字的基数。当忽略参数 radix , JavaScript 默认数字的基数如下:
只传入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));
这样写为什么就能返回想要的值呢?因为,传一个完整函数进去,有形参,有返回值。这样就不会造成因为参数传入错误而造成结果错误了,最后返回一个经纯函数处理过的新数组。
reduce()
方法对数组中的每个元素执行一个提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。传递给 reduce 的回调函数(callback
)接受四个参数,分别是累加器 accumulator
、currentValue
——正在操作的元素、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]
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 值为 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 |
filter
(过滤,筛选) 方法创建一个新数组,原始数组不发生改变。
array.filter(callback,[ thisObject]);
其包含通过提供函数实现的测试的所有元素。接收的参数和 map 是一样的,filter的callbac
k函数需要返回布尔值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]
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.
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排序算法的内部实现感兴趣,我在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 函数只给出了两种排序 InsertionSort
和 QuickSort
,数量小于等于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]
返回一个函数,下面直接看两个例子来加深理解。
我们知道在判断类型的时候可以通过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 => { ... }
这一函数作为返回值输出。
前言中的面试题,用 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后端工程师进阶路线,加入我们一起学习吧!
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对象这种,开启之后直接进入微队列中,微队列内的就是那个任务快就那个先执行完,但是针对于队列与队列之间不同的任务,还是会有先后顺序,这个先后顺序是由队列决定的。
所有引用类型(函数,数组,对象)都拥有__proto__属性(隐式原型)
所有函数拥有prototype属性(显式原型)(仅限函数)
原型对象:拥有prototype属性的对象,在定义函数时就被创建
在js代码中 通过对象创建(下面一段简单的代码)详细分析原型链 一段简单代码:
function foo(){} foo.prototype.z=3;
var obj=new foo();
obj.y=2;
obj.x=1;//调用
obj.x;//1
obj.y;//2obj.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的值并不会修改。
首先查看整个原型链,会想这两个方法是怎么来的,在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世界很大,并不只有对象
作用域是你的代码在运行时,某些特定部分中的变量,函数和对象的可访问性。换句话说,作用域决定了变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期
。
在 JavaScript 中有两种作用域
如果一个变量在函数外面或者大括号{}
外声明,那么就定义了一个全局作用域
,在ES6之前局部作用域只包含了函数作用域,ES6为我们提供的块级作用域
,也属于局部作用域
拥有全局作用域的对象可以在代码的任何地方访问到, 在js中一般有以下几种情形拥有全局作用域:
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
从上面代码中可以看到globleVariable
和globalFunc
在任何地方都可以访问到, 反之不具有全局作用域特性的变量只能在其作用域内使用。
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
和全局作用于相反,局部作用域一般只能在固定代码片段内可以访问到。最常见的就是函数作用域。
定义在函数中的变量就在函数作用域中。并且函数在每次调用时都有一个不同的作用域。这意味着同名变量可以用在不同的函数中。因为这些变量绑定在不同的函数中,拥有不同作用域,彼此之间不能访问。
//全局作用域
function test(){
var num = 9;
// 内部可以访问
console.log("test中:"+num);
}
//test外部不能访问
console.log("test外部:"+num);
注意点:
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
是同级作用域。
变量提升英文名字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';
原因是:
var foo='i am koala'
的申明会被忽略。接下来讲,在ES6中引入的块级作用域之后的事!
ES6新增了
let
和const
命令,可以用来创建块级作用域变量,使用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的let
和const
不允许反复声明,与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
}
看到这里是不是感觉到了块级作用域的出现还是很有必要的。
在讲解作用域链之前先说一下,先了解一下 JavaScript是如何执行的?
javascript编译器编译完成,生成代码后进行分析
分析阶段的核心,在分析完成后(也就是接下来函数执行阶段的瞬间)会创建一个AO(Active Object 活动对象)
分析阶段分析成功后,会把给AO(Active Object 活动对象)
给执行阶段
执行阶段的核心就是找
,具体怎么找
,后面会讲解LHS查询
与RHS查询
。
看一段代码:
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
看了前面一个完整的javascript函数执行过程,让我们来说下作用域链的概念吧。JavaScript上每一个函数执行时,会先在自己创建的AO上找对应属性值。若找不到则往父函数的AO上找,再找不到则再上一层的AO,直到找到大boss:window(全局作用域)。 而这一条形成的“AO链” 就是JavaScript中的作用域链。
找
过程LHS和RHS查询特殊说明LHS,RHS 这两个术语就是出现在引擎对变量进行查询的时候。在《你不知道的Javascript(上)》也有很清楚的描述。在这里,我想引用freecodecamp
上面的回答来解释:
LHS = 变量赋值或写入内存。想象为将文本文件保存到硬盘中。 RHS = 变量查找或从内存中读取。想象为从硬盘打开文本文件。 Learning Javascript, LHS RHS
ReferenceError
异常。LHR
稍微比较特殊: 会自动创建一个全局变量TypeError
异常例子来自于《你不知道的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)
最后对作用域链做一个总结,引用《你不知道的Javascript(上)》中的一张图解释
在使用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
}
});
案例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。
还有闭包这篇首页无法导航,没具体看什么原因。
之前在做webpack
配置时候多次用到路径相关内容,最近在写项目的时候,有一个文件需要上传到阿里云oss的功能,同时本地服务器也需要保留一个文件备份。多次用到了文件路径相关内容以及Node核心API的path
模块,所以系统的学习了一下,整理了这篇文章。
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
对比输出结果,暂时得到的结论是
为什么说上面是暂时得到的结论,因为是有错误的,再看一段代码:
我们在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这个模块,这个模块在很多地方比较常用,所以,对于我们来说,掌握他,对我们以后的发展更有利,不用每次看webpack的配置文件还要去查询一下这个api是干什么用的,很影响我们的效率
这是api官网地址:https://nodejs.org/api/path.html
个人认为官网中的api没有必要都掌握,下面会对一些常用的api进行讲解,我经常用到的,或者作为一个前端开发工程师在webpack等工程配置的时候经常用到的。
举例说明
const path = require('path');
console.log(path.normalize('/koala/Desktop//程序员成长指北//代码pra/..'));
规范后的结果
/koala/Desktop/程序员成长指北/代码pra
作用总结
规范化路径,把不规范的路径规范化。
举例说明
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])
Unix
系统是/
,Windows
系统是\
,那么你在两个系统下看到的返回结果就不一样。.
,代表当前的文件夹。举例说明
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'
}
作用总结
他返回的是一个对象,那么我们来把这么几个名词熟悉一下:
举例说明
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
(可选参数),当输入第二个参数的时候,打印结果不出现后缀名
举例说明
const path = require('path');
console.log(path.dirname('/koala/Desktop/程序员成长指北/代码pra/node核心API'));
运行结果
/koala/Desktop/程序员成长指北/代码pra
作用总结
返回文件的目录完整地址
举例说明
const path = require('path');
path.extname('index.html');
path.extname('index.coffee.md');
path.extname('index.');
path.extname('index');
path.extname('.index');
运行结果
.html
.md
.
''
''
作用总结
返回的是后缀名,但是最后两种情况返回'',大家注意一下。
举例说明
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
举例说明
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路径的相对路径。
边界:
本篇文章关于路径的知识就说到这里,基础很重要的,既能节约开发时间,又能减少报错。
今天就分享这么多,如果对分享的内容感兴趣,可以关注公众号「程序员成长指北」,或者加入技术交流群,大家一起讨论。
进阶技术路线
//普通的函数声明
async function A(){}
//声明一个函数表达式
let A=async function(){}
//async形式的箭头函数
let A=async ()=>{}
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;
}
方法执行后的返回值:await命令后面可以是Promise对象或值,如果是值,就会转到一个立即resolve的Promise对象。async函数返回的是一个Promise对象,如果结果是值,会经过Promise包装返回。
await与并行:如果在一个async的方法中,有多个await操作的时候,程序会变成完全的串行操作,一个完事等另一个但是为了发挥node的异步优势,当异步操作之间不存在结果的依赖关系时,可以使用promise.all来实现并行,all中的所有方法是一同执行的。
执行后的结果:async函数中,如果有多个await关键字时,如果有一个await的状态变成了rejected,那么后面的操作都不会继续执行,promise也是同理await的返回结果就是后面promise执行的结果,可能是resolves或者rejected的值使用场景循环遍历方便了代码需要同步的操作(文件读取,数据库操作等)
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。
这个误区在一道面试题那篇文章中详细讲解过,但是还是想提一下。
看一段代码:
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属于异步,位于本轮循环的末尾。)
流的英文stream
,流(Stream)是一个抽象的数据接口,Node.js
中很多对象都实现了流,流是EventEmitter
对象的一个实例,总之它是会冒数据(以 Buffer
为单位),或者能够吸收数据的东西,它的本质就是让数据流动起来。
可能看一张图会更直观:
注意:stream
不是node.js独有的概念,而是一个操作系统最基本的操作方式,只不过node.js有API支持这种操作方式。linux命令的|就是stream
。
小伙伴们肯定都在线看过电影,对比定义中的图-水桶管道流转图
,source
就是服务器端的视频,dest
就是你自己的播放器(或者浏览器中的flash和h5 video)。大家想一下,看电影的方式就如同上面的图管道换水一样,一点点从服务端将视频流动到本地播放器,一边流动一边播放,最后流动完了也就播放完了。
说明:视频播放的这个例子,如果我们不使用管道和流动的方式,直接先从服务端加载完视频文件,然后再播放。会造成很多问题
有一个这样的需求,想要读取大文件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
整个流转过程包括source,dest,还有连接二者的管道pipe(stream的核心),分别介绍三者来带领大家搞懂stream流转过程。
stream
的常见来源方式有三种:
http
请求中的request
这里先说一下从控制台输入
这种方式,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
数据的传入和结束
从水桶管道流转图中可以看到,在source
和dest
之间有一个连接的管道pipe
,它的基本语法是source.pipe(dest)
,source
和dest
就是通过pipe连接,让数据从source
流向了dest
。
stream的常见输出方式有三种:
http
请求中的response
stream
的应用场景主要就是处理IO
操作,而http请求
和文件操作
都属于IO
操作。这里再提一下stream
的本质——由于一次性IO
操作过大,硬件开销太多,影响软件运行效率,因此将IO
分批分段进行操作,让数据像水管一样流动起来,直到流动完成,也就是操作完成。下面对几个常用的应用场景分别进行介绍
一个对网络请求做压力测试的工具ab
,ab
全称 Apache bench
,是 Apache
自带的一个工具,因此使用 ab
必须要安装 Apache
。mac os 系统自带 Apache
,windows
用户视自己的情况进行安装。运行 ab
之前先启动 Apache
,mac os
启动方式是 sudo apachectl start
。
Apache bench对应参数的详细学习地址,有兴趣的可以看一下
Apache bench对应参数的详细学习地址
介绍这个小工具的目的是对下面几个场景可以进行直观的测试,看出使用stream带来了哪些性能的提升。
这样一个需求:
使用node.js实现一个http请求,读取data.txt文件,创建一个服务,监听8000端口,读取文件后返回给客户端,讲get请求的时候用一个常规文件读取与其做对比,请看下面的例子。
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);
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)
有什么好处呢?这时候我们刚才推荐的压力测试小工具就用到了。getTest1
和getTest2
两段代码,将data.txt
内容增加大一些,使用ab
工具进行测试,运行命令ab -n 100 -c 100 http://localhost:8000/
,其中-n 100
表示先后发送100次请求,-c 100
表示一次性发送的请求数目为100个。对比结果分析使用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后端开发过程中还是比较常用的。
request和reponse一样,都是stream对象,可以使用stream的特性,二者的区别在于,我们再看一下水桶管道流转图
,
request是source类型,是图中的源头,而response是dest类型,是图中的目的地。
一个文件拷贝的例子
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
,有兴趣的小伙伴可以去看一下源码。
Readable Stream
可读数据流Writeable Stream
可写数据流Duplex Stream
双向数据流,可以同时读和写Transform Stream
转换数据流,可读可写,同时可以转换(处理)数据(不常用)之前的文章都是围绕前两种可读数据流和可写数据流,第四种流不太常用,需要的小伙伴网上搜索一下,接下来对第三种数据流Duplex Stream 说明一下。
Duplex Stream
双向的,既可读,又可写。
Duplex streams
同时实现了 Readable
和Writable
接口。 Duplex streams
的例子包括
tcp sockets
zlib streams
crypto streams
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');
});
event-stream 用起来有函数式编程的感觉
awesome-nodejs#streams也是一个不错的第三方stream库,有兴趣的小伙伴可以github看一下
看完了这篇文章是不是对stream有了一定的了解,并且知道了node对于文件处理还是有完美的解决方案的。本文中三次展示了水桶管道流转图
,总要的事情说三遍希望小伙伴们记住它,除了以上内容小伙伴们会不会有一些思考,比如
string
类型还是其他类型,该类型为stream带来了什么好处?pipe
函数什么时候触发的呢?在什么情况下触流转发?底层机制是什么?stream
的第二篇文章为大家详细讲解。最近有一个需求,从一组试题中随机获取5道题用于用户复习,获取到的5道试题应该每次是随机的而且不重复的。
大多数人的写法,从数组中取数据,放入新的数组,取完一个数据从原始数组中删除数据,在放入新数组的时候判断是否已出现过。
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);
这个 应该 是大多数人的 代码
实现思路
把源数组分成左右两段,左边按顺序递增,保存已选择的随机数;右侧是剩余可选的数值;每次从右侧选一个,与左侧最后一个位置的数值交换就可以达到目的。
然后考虑把左侧用一个新数组表示,右侧选中的数移入新数组,再将左侧应该交换过来的值移过来……
算法图解:
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);
实现思路
基本与实现方式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];
};
说明:format(x,d)是mysql自带的格式化小数函数,format()函数会对小数部分进行四舍五入操作,整数部分从右向左每三位一个逗号进行格式化输出
format函数结果:
23,456.79
说明:truncate(x,d)是mysql自带的函数,truncate()函数会将小数部分d位以后的值直接舍去
truncate函数结果:
23456.78
说明:round(x,d)是mysql自带的函数,format()函数会对小数部分进行四舍五入操作,但是不会出现format函数中的带逗号格式
round函数数结果:
23456.79
说明:MySQL 的CAST()和CONVERT()函数可用来获取一个类型的值,并产生另一个类型的值。简单的说就是类型转换。
CONVERT(xxx,类型),但是类型是有限制的,下面几种类型
根据文初给出的数据,应该是这些写
convert(23456.789,decimal(10,2));
convert函数数结果,这里也是四舍五入:
23456.79
注:解释下dem的意思
DECIMAL(10,2) 总共能存10位数字,末尾2位是小数,字段最大值99999999.99(小数点不算在长度内)
维基百科中的概念
学术上
个人理解
案例分析是从浅入深希望大家都看完!
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中的局部变量这就是闭包的基本使用。
!function(){
var localData="localData here";
document.addEventListener('click',function(){
console.log(localData);
});
}();
前端原始点击事件操作也用到了闭包来访问外部的局部变量。
!function(){
var localData="localData here";
var url="http://www.baidu.com";
$.ajax({
url:url,
success:function(){
//do sth...
console.log(localData);
}
})
}();
在ajax请求的方法中也用到了闭包,访问外部的局部变量。
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的主要通过计数器方式回收内存,假设有a,b,c三个对象,当a引用b的时候,那么b的引用计算器增加1(通俗的说用到那个对象哪个对象引用计算器增加1),同时b引用c的时候,c引用计数器增加1,当a被释放的时候,b的引用计数器减少1,变成0的时候这个对象被释放,c计数器变成0,被释放,但是当遇到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
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
分析内容说明,在看这篇文章的时候,注意两点可能会看的更明白:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.