遍历方法

有用到object对象的转换成数组,然后又想到了遍历方法,所以,也想记录下1. 终止或者跳出循环break跳出循环体,所在循环体已结束continue跳出本次循环,进行下一次循环,所在的循环体未结束return 终止函数执行for (let i = 0; i < 5; i++) { if (i == 3) break; console.log(“The number is " + i); /* 只输出 0 , 1 , 2 , 到3就跳出循环了 /}for (let i = 0; i <= 5; i++) { if (i == 3) continue; console.log(“The number is " + i); / 不输出3,因为continue跳过了,直接进入下一次循环 /}2.遍历方法假数据const temporaryArray = [6,2,3,4,5,1,1,2,3,4,5];const objectArray = [ { id: 1, name: ’d’ }, { id: 2, name: ’d’ }, { id: 3, name: ‘c’ }, { id: 1, name: ‘a’ }];const temporaryObject = { a: 1, b: 2, c: 3, d: 4,};const length = temporaryArray.length;普通for循环遍历for(let i = 0; i < length; i++) { console.log(temporaryArray[i]);}for in 循环/ for in 循环主要用于遍历普通对象,* 当用它来遍历数组时候,也能达到同样的效果,* 但是这是有风险的,因为 i 输出为字符串形式,而不是数组需要的数字下标,* 这意味着在某些情况下,会发生字符串运算,导致数据错误* /for(let i in temporaryObject) { / hasOwnProperty只加载自身属性 / if(temporaryObject.hasOwnProperty(i)) { console.log(temporaryObject[i]); }}for of 循环,用于遍历可迭代的对象for(let i of temporaryArray) { console.log(i);}forEach 第一个值为数组当前索引的值,第二个为索引值,只能遍历数组,无返回值,也无法跳出循环let a = temporaryArray.forEach(function(item, index) { console.log(index, item);});map 返回新数组,只能遍历数组temporaryArray.map(function(item) { console.log(item);});filter 是数组的内置对象,不改变原数组,有返回值temporaryArray.filter(function(item) { console.log(item%2 == 0);});some判断是否有符合的值let newArray = temporaryArray.some(function(item) { return item > 1;});console.log(newArray);every判断数组里的值是否全部符合条件let newArray1 = temporaryArray.every(function(item) { return item > 6;});console.log(newArray1);reduce(function(accumulator, currentValue, currentIndex, array) {}, initValue)accumulator:初始值或者累加计算结束后的返回值, currentValue遍历时的当前元素值,currentIndex当前索引值,array当前数组initValue为初始值,若不添加参数initValue,则accumulator为当前数组的第一个元素值,若添加,则accumulator为initValue值,累加器accumulator从initValue开始运算let temporaryObject3 = {};let newArray2 = objectArray.reduce(function(countArray, currentValue) { / 利用temporaryObject3里存放id来判断原数组里的对象是否相同,若id相同,则继续下一步,不同则将该对象放入新数组中 * 则countArray为去重后的数组 * */ temporaryObject3[currentValue.id] ? ’’ : temporaryObject3[currentValue.id] = true && countArray.push(currentValue); return countArray;}, []);console.log(newArray2);正在努力学习中,若对你的学习有帮助,留下你的印记呗(点个赞咯^_^)往期好文推荐:判断ios和Android及PC端实现文字的省略号css实现波浪线及立方体 ...

October 25, 2018 · 1 min · jiezi

对象的判空、遍历及转换和对数组的操作

在联调时,发现后台返回的不是数组,或者返回的类型与不为空时的类型不一样,这里小结下对对象的操作1.对象的判空/* 先声明一个对象字面量 /let isObject = {};将json对象转化成json字符串,然后进行判断是否等于字符串’{}’,直接写{}无法进行判断console.log(JSON.stringify(isObject) == ‘{}’);使用for循环来判断,若不为空,则返回falselet listObject = { a: 1, b: 3, c: 4 };function isEmptyObject() { for(let key in listObject) { return false; } return true;}console.log(isEmptyObject());使用Object.keys()来判断,返回的是一个数组,根据数组长度来判断function isEmptyObject2(obj) { return Object.keys(obj).length == 0;}console.log(isEmptyObject2(isObject));对这个对象中一定存在的属性进行判断console.log(isObject.id == null);2.将对象转换成数组let listData = {a: 1, b: 2, c: 3};let temporaryArray = [];只需要将对象中的值转换成数组时for(let i in listData) { temporaryArray.push(listData[i]);}console.log(temporaryArray);将对象中的键与值一起转换成数组let temporaryArray2 = [];for(let i in listData) { let temporaryObject = {}; temporaryObject[i] = listData[i]; temporaryArray2.push(temporaryObject);}console.log(temporaryArray2);使用Object原生属性console.log(Object.values(listData));3.将非数组转换成数组Array.prototype.slice.call(arguments)/ 该方法可以将类数组对象转换为数组,所谓类数组对象,就是含 length 和索引属性的对象* 返回的数组长度取决于对象 length 属性的值,并且非索引属性的值或索引大于 length 的值都不会被返回到数组中* Array.prototype.slice.call(obj,start,end) start:方法中slice截取的开始位置,end终止位置,默认从0开始* /let testObject = { 0: ‘a’, 1: ‘b’, 2: ‘c’, name: ‘admin’, length: 3,};/ 对象中必须定义length,且属性值不能为非数字,非数字无法返回 /console.log(Array.prototype.slice.call(testObject));/ 或者简写为[].slice.call(testObject) /console.log([].slice.call(testObject));Array.from()/ Array、Set、Map 和字符串都是可迭代对象(WeakMap/WeakSet 并不是可迭代对象)* 这些对象都有默认的迭代器,即具有 Symbol.iterator 属性* 所有通过生成器创建的迭代器都是可迭代对象* 可以用 for of 循环 * /let testObject2 = { 0: ‘a’, 1: ‘b’, 2: ‘c’, name: ‘admin’, length: 3,};console.log(Array.from(testObject2));4.判断是否为数组,返回true则为数组let temporaryArray3 = [1,2,1,2,3];console.log(temporaryArray3 instanceof Array);console.log(temporaryArray3.constructor == Array);console.log(Array.isArray(temporaryArray3));console.log(Object.prototype.toString.call(temporaryArray3) === “[object Array]”);5.将数组转换成键值对对象,可以利用for循环let transformObject = Object.assign({}, temporaryArray3);console.log(transformObject);let transformObject2 = {…temporaryArray3};console.log(transformObject2);6.讲讲深拷贝与浅拷贝浅拷贝其实只是引用的拷贝,两者还是指向内存中的同一个地址深拷贝就是两者指向不同的内存地址,是真正意义上的拷贝Object.assignassign对对象分配赋值,实现浅拷贝,但是它有一个特殊的地方,可以处理第一层的深拷贝。assign(targetObject, obj, obj1) assign会改变targetObject的值,较好的做法就是将targetObject定义为空对象{}assign是将这个3者合并,若有相同的属性,后面的属性会覆盖前面对象的属性let temporaryObject2 = {a: 1, d: {e: 3}};let temporaryObject3 = {b: 1,a: 2};let mergeObject = Object.assign({}, temporaryObject2, temporaryObject3);console.log(mergeObject);/ mergeObject.d.e不会改变temporaryObject2里a的值,但会改变temporaryObject2里e的值 */mergeObject.a = 5;mergeObject.d.e = 5;console.log(mergeObject);较常用的对象深拷贝方法let copyObject = JSON.parse(JSON.stringify(temporaryObject2));console.log(copyObject);copyObject.a = 0;console.log(copyObject);数组slice浅拷贝let copyArray = [1, 2, [1, 5], 3];let copyArray1 = copyArray.slice(0);console.log(copyArray1);copyArray1[1] = 20;copyArray1[2][1] = 23;console.log(copyArray1);数组concat浅拷贝let copyArray2 = copyArray.concat();console.log(copyArray2);copyArray2[1] = 20;copyArray2[2][1] = 23;console.log(copyArray2);正在努力学习中,若对你的学习有帮助,留下你的印记呗(点个赞咯^_^)往期好文推荐:判断ios和Android及PC端纯css实现瀑布流(multi-column多列及flex布局)实现文字的省略号 ...

October 25, 2018 · 1 min · jiezi

vue-split-table【表格合并和编辑插件】

前言vue-split-table应用的效果图vue-split-table开源地址,欢迎star,现在已经开源和同步到npm上轻松搞定表格拆分或者合并,编辑,再也不怕被产品怼啦1.核心源码分析<td>里面嵌套<table>实现表格拆分;原生实现复选框的单选和全选功能;props属性像父组件暴露属性值;$emit自定义事件方法向父组件传值;作用域插槽<template slot-scope>由父向子传入html标签;嵌套<input>实现表格编辑,v-for不允许<input>里面使用v-model改变item值问题解决;webpack打包配置vue-split-table开源地址,欢迎star和pr2.暴露的Attributes参数说明类型是否必传DefaultheadData表头内容Array必传-bodyData表体内容Array必传-checkFlag是否有复选列Boolean可选TruetableEditFlag表格是否可编辑Boolean可选TrueoperFlag是否有操作列Boolean可选True3.暴露的Eventsname说明参数multipleData当选项发生变化触发multipleDataeditData表格编辑文本框失焦触发editData4.slot事件名说明operate配置操作列后就可通过设置slot来配置操作的内容5.撸起示例代码基于vue工程<template> <split-table :headData=“headData” :bodyData=“bodyData” @multipleData=“multipleData” @editData=“editData”> <template slot=“operate” slot-scope=“props”> <span @click=“splitEdit(props.rowData)">修改</span> <span @click=“splitAdd(props.rowData)">新增</span> <span @click=“splitDel(props.rowData)">删除</span> </template> </split-table></template><script> import SplitTable from ‘vue-split-table’; export default { components: { SplitTable }, data () { return { headData: [“城市”, “美食”, “好玩的地方”], bodyData: [ { city: “北京”, food: “北京烤鸭”, fun: [“故宫”, “颐和园”, “长城”] }, { city: “深圳”,food: [“肠粉”, “牛肉火锅”],fun: [“西冲”, “华侨城”, “世界之窗”] }, { city: [“重庆”, “成都”, “武汉”], food: [“重庆老火锅”,“重庆烤鱼”,“重庆小面”,“成都小吃”,“武汉热干面”], fun: [“洪崖洞”, “峨眉山”, “黄鹤楼”] } ], } }, methods: { splitEdit(rowData) { console.log(“rowData值为”, rowData); }, editData(data) { console.log(“编辑所在行的值为”, data); } splitAdd(data) { console.log(“新增所在行的值为”, data); }, splitDel(data) { console.log(“删除所在行的值为”, data); }, multipleData(data) { console.log(“复选框选择的值为”, data); } } }</script>参考文章:时钟组件 ...

October 23, 2018 · 1 min · jiezi

ES6 系列之 Generator 的自动执行

单个异步任务var fetch = require(’node-fetch’);function* gen(){ var url = ‘https://api.github.com/users/github'; var result = yield fetch(url); console.log(result.bio);}为了获得最终的执行结果,你需要这样做:var g = gen();var result = g.next();result.value.then(function(data){ return data.json();}).then(function(data){ g.next(data);});首先执行 Generator 函数,获取遍历器对象。然后使用 next 方法,执行异步任务的第一阶段,即 fetch(url)。注意,由于 fetch(url) 会返回一个 Promise 对象,所以 result 的值为:{ value: Promise { <pending> }, done: false }最后我们为这个 Promise 对象添加一个 then 方法,先将其返回的数据格式化(data.json()),再调用 g.next,将获得的数据传进去,由此可以执行异步任务的第二阶段,代码执行完毕。多个异步任务上节我们只调用了一个接口,那如果我们调用了多个接口,使用了多个 yield,我们岂不是要在 then 函数中不断的嵌套下去……所以我们来看看执行多个异步任务的情况:var fetch = require(’node-fetch’);function* gen() { var r1 = yield fetch(‘https://api.github.com/users/github'); var r2 = yield fetch(‘https://api.github.com/users/github/followers'); var r3 = yield fetch(‘https://api.github.com/users/github/repos'); console.log([r1.bio, r2[0].login, r3[0].full_name].join(’\n’));}为了获得最终的执行结果,你可能要写成:var g = gen();var result1 = g.next();result1.value.then(function(data){ return data.json();}).then(function(data){ return g.next(data).value;}).then(function(data){ return data.json();}).then(function(data){ return g.next(data).value}).then(function(data){ return data.json();}).then(function(data){ g.next(data)});但我知道你肯定不想写成这样……其实,利用递归,我们可以这样写:function run(gen) { var g = gen(); function next(data) { var result = g.next(data); if (result.done) return; result.value.then(function(data) { return data.json(); }).then(function(data) { next(data); }); } next();}run(gen);其中的关键就是 yield 的时候返回一个 Promise 对象,给这个 Promise 对象添加 then 方法,当异步操作成功时执行 then 中的 onFullfilled 函数,onFullfilled 函数中又去执行 g.next,从而让 Generator 继续执行,然后再返回一个 Promise,再在成功时执行 g.next,然后再返回……启动器函数在 run 这个启动器函数中,我们在 then 函数中将数据格式化 data.json(),但在更广泛的情况下,比如 yield 直接跟一个 Promise,而非一个 fetch 函数返回的 Promise,因为没有 json 方法,代码就会报错。所以为了更具备通用性,连同这个例子和启动器,我们修改为:var fetch = require(’node-fetch’);function* gen() { var r1 = yield fetch(‘https://api.github.com/users/github'); var json1 = yield r1.json(); var r2 = yield fetch(‘https://api.github.com/users/github/followers'); var json2 = yield r2.json(); var r3 = yield fetch(‘https://api.github.com/users/github/repos'); var json3 = yield r3.json(); console.log([json1.bio, json2[0].login, json3[0].full_name].join(’\n’));}function run(gen) { var g = gen(); function next(data) { var result = g.next(data); if (result.done) return; result.value.then(function(data) { next(data); }); } next();}run(gen);只要 yield 后跟着一个 Promise 对象,我们就可以利用这个 run 函数将 Generator 函数自动执行。回调函数yield 后一定要跟着一个 Promise 对象才能保证 Generator 的自动执行吗?如果只是一个回调函数呢?我们来看个例子:首先我们来模拟一个普通的异步请求:function fetchData(url, cb) { setTimeout(function(){ cb({status: 200, data: url}) }, 1000)}我们将这种函数改造成:function fetchData(url) { return function(cb){ setTimeout(function(){ cb({status: 200, data: url}) }, 1000) }}对于这样的 Generator 函数:function* gen() { var r1 = yield fetchData(‘https://api.github.com/users/github'); var r2 = yield fetchData(‘https://api.github.com/users/github/followers'); console.log([r1.data, r2.data].join(’\n’));}如果要获得最终的结果:var g = gen();var r1 = g.next();r1.value(function(data) { var r2 = g.next(data); r2.value(function(data) { g.next(data); });});如果写成这样的话,我们会面临跟第一节同样的问题,那就是当使用多个 yield 时,代码会循环嵌套起来……同样利用递归,所以我们可以将其改造为:function run(gen) { var g = gen(); function next(data) { var result = g.next(data); if (result.done) return; result.value(next); } next();}run(gen);run由此可以看到 Generator 函数的自动执行需要一种机制,即当异步操作有了结果,能够自动交回执行权。而两种方法可以做到这一点。(1)回调函数。将异步操作进行包装,暴露出回调函数,在回调函数里面交回执行权。(2)Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权。在两种方法中,我们各写了一个 run 启动器函数,那我们能不能将这两种方式结合在一些,写一个通用的 run 函数呢?我们尝试一下:// 第一版function run(gen) { var gen = gen(); function next(data) { var result = gen.next(data); if (result.done) return; if (isPromise(result.value)) { result.value.then(function(data) { next(data); }); } else { result.value(next) } } next()}function isPromise(obj) { return ‘function’ == typeof obj.then;}module.exports = run;其实实现的很简单,判断 result.value 是否是 Promise,是就添加 then 函数,不是就直接执行。return Promise我们已经写了一个不错的启动器函数,支持 yield 后跟回调函数或者 Promise 对象。现在有一个问题需要思考,就是我们如何获得 Generator 函数的返回值呢?又如果 Generator 函数中出现了错误,就比如 fetch 了一个不存在的接口,这个错误该如何捕获呢?这很容易让人想到 Promise,如果这个启动器函数返回一个 Promise,我们就可以给这个 Promise 对象添加 then 函数,当所有的异步操作执行成功后,我们执行 onFullfilled 函数,如果有任何失败,就执行 onRejected 函数。我们写一版:// 第二版function run(gen) { var gen = gen(); return new Promise(function(resolve, reject) { function next(data) { try { var result = gen.next(data); } catch (e) { return reject(e); } if (result.done) { return resolve(result.value) }; var value = toPromise(result.value); value.then(function(data) { next(data); }, function(e) { reject(e) }); } next() })}function isPromise(obj) { return ‘function’ == typeof obj.then;}function toPromise(obj) { if (isPromise(obj)) return obj; if (‘function’ == typeof obj) return thunkToPromise(obj); return obj;}function thunkToPromise(fn) { return new Promise(function(resolve, reject) { fn(function(err, res) { if (err) return reject(err); resolve(res); }); });}module.exports = run;与第一版有很大的不同:首先,我们返回了一个 Promise,当 result.done 为 true 的时候,我们将该值 resolve(result.value),如果执行的过程中出现错误,被 catch 住,我们会将原因 reject(e)。其次,我们会使用 thunkToPromise 将回调函数包装成一个 Promise,然后统一的添加 then 函数。在这里值得注意的是,在 thunkToPromise 函数中,我们遵循了 error first 的原则,这意味着当我们处理回调函数的情况时:// 模拟数据请求function fetchData(url) { return function(cb) { setTimeout(function() { cb(null, { status: 200, data: url }) }, 1000) }}在成功时,第一个参数应该返回 null,表示没有错误原因。优化我们在第二版的基础上将代码写的更加简洁优雅一点,最终的代码如下:// 第三版function run(gen) { return new Promise(function(resolve, reject) { if (typeof gen == ‘function’) gen = gen(); // 如果 gen 不是一个迭代器 if (!gen || typeof gen.next !== ‘function’) return resolve(gen) onFulfilled(); function onFulfilled(res) { var ret; try { ret = gen.next(res); } catch (e) { return reject(e); } next(ret); } function onRejected(err) { var ret; try { ret = gen.throw(err); } catch (e) { return reject(e); } next(ret); } function next(ret) { if (ret.done) return resolve(ret.value); var value = toPromise(ret.value); if (value && isPromise(value)) return value.then(onFulfilled, onRejected); return onRejected(new TypeError(‘You may only yield a function, promise ’ + ‘but the following object was passed: “’ + String(ret.value) + ‘”’)); } })}function isPromise(obj) { return ‘function’ == typeof obj.then;}function toPromise(obj) { if (isPromise(obj)) return obj; if (‘function’ == typeof obj) return thunkToPromise(obj); return obj;}function thunkToPromise(fn) { return new Promise(function(resolve, reject) { fn(function(err, res) { if (err) return reject(err); resolve(res); }); });}module.exports = run;co如果我们再将这个启动器函数写的完善一些,我们就相当于写了一个 co,实际上,上面的代码确实是来自于 co……而 co 是什么? co 是大神 TJ Holowaychuk 于 2013 年 6 月发布的一个小模块,用于 Generator 函数的自动执行。如果直接使用 co 模块,这两种不同的例子可以简写为:// yield 后是一个 Promisevar fetch = require(’node-fetch’);var co = require(‘co’);function* gen() { var r1 = yield fetch(‘https://api.github.com/users/github'); var json1 = yield r1.json(); var r2 = yield fetch(‘https://api.github.com/users/github/followers'); var json2 = yield r2.json(); var r3 = yield fetch(‘https://api.github.com/users/github/repos'); var json3 = yield r3.json(); console.log([json1.bio, json2[0].login, json3[0].full_name].join(’\n’));}co(gen);// yield 后是一个回调函数var co = require(‘co’);function fetchData(url) { return function(cb) { setTimeout(function() { cb(null, { status: 200, data: url }) }, 1000) }}function* gen() { var r1 = yield fetchData(‘https://api.github.com/users/github'); var r2 = yield fetchData(‘https://api.github.com/users/github/followers'); console.log([r1.data, r2.data].join(’\n’));}co(gen);是不是特别的好用?ES6 系列ES6 系列目录地址:https://github.com/mqyqingfeng/BlogES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ...

October 19, 2018 · 4 min · jiezi

ES6 系列之我们来聊聊 Promise

前言Promise 的基本使用可以看阮一峰老师的 《ECMAScript 6 入门》。我们来聊点其他的。回调说起 Promise,我们一般都会从回调或者回调地狱说起,那么使用回调到底会导致哪些不好的地方呢?1. 回调嵌套使用回调,我们很有可能会将业务代码写成如下这种形式:doA( function(){ doB(); doC( function(){ doD(); } ) doE();} );doF();当然这是一种简化的形式,经过一番简单的思考,我们可以判断出执行的顺序为:doA()doF()doB()doC()doE()doD()然而在实际的项目中,代码会更加杂乱,为了排查问题,我们需要绕过很多碍眼的内容,不断的在函数间进行跳转,使得排查问题的难度也在成倍增加。当然之所以导致这个问题,其实是因为这种嵌套的书写方式跟人线性的思考方式相违和,以至于我们要多花一些精力去思考真正的执行顺序,嵌套和缩进只是这个思考过程中转移注意力的细枝末节而已。当然了,与人线性的思考方式相违和,还不是最糟糕的,实际上,我们还会在代码中加入各种各样的逻辑判断,就比如在上面这个例子中,doD() 必须在 doC() 完成后才能完成,万一 doC() 执行失败了呢?我们是要重试 doC() 吗?还是直接转到其他错误处理函数中?当我们将这些判断都加入到这个流程中,很快代码就会变得非常复杂,以至于无法维护和更新。2. 控制反转正常书写代码的时候,我们理所当然可以控制自己的代码,然而当我们使用回调的时候,这个回调函数是否能接着执行,其实取决于使用回调的那个 API,就比如:// 回调函数是否被执行取决于 buy 模块import {buy} from ‘./buy.js’;buy(itemData, function(res) { console.log(res)});对于我们经常会使用的 fetch 这种 API,一般是没有什么问题的,但是如果我们使用的是第三方的 API 呢?当你调用了第三方的 API,对方是否会因为某个错误导致你传入的回调函数执行了多次呢?为了避免出现这样的问题,你可以在自己的回调函数中加入判断,可是万一又因为某个错误这个回调函数没有执行呢?万一这个回调函数有时同步执行有时异步执行呢?我们总结一下这些情况:回调函数执行多次回调函数没有执行回调函数有时同步执行有时异步执行对于这些情况,你可能都要在回调函数中做些处理,并且每次执行回调函数的时候都要做些处理,这就带来了很多重复的代码。回调地狱我们先看一个简单的回调地狱的示例。现在要找出一个目录中最大的文件,处理步骤应该是:用 fs.readdir 获取目录中的文件列表;循环遍历文件,使用 fs.stat 获取文件信息比较找出最大文件;以最大文件的文件名为参数调用回调。代码为:var fs = require(‘fs’);var path = require(‘path’);function findLargest(dir, cb) { // 读取目录下的所有文件 fs.readdir(dir, function(er, files) { if (er) return cb(er); var counter = files.length; var errored = false; var stats = []; files.forEach(function(file, index) { // 读取文件信息 fs.stat(path.join(dir, file), function(er, stat) { if (errored) return; if (er) { errored = true; return cb(er); } stats[index] = stat; // 事先算好有多少个文件,读完 1 个文件信息,计数减 1,当为 0 时,说明读取完毕,此时执行最终的比较操作 if (–counter == 0) { var largest = stats .filter(function(stat) { return stat.isFile() }) .reduce(function(prev, next) { if (prev.size > next.size) return prev return next }) cb(null, files[stats.indexOf(largest)]) } }) }) })}使用方式为:// 查找当前目录最大的文件findLargest(’./’, function(er, filename) { if (er) return console.error(er) console.log(’largest file was:’, filename)});你可以将以上代码复制到一个比如 index.js 文件,然后执行 node index.js 就可以打印出最大的文件的名称。看完这个例子,我们再来聊聊回调地狱的其他问题:1.难以复用回调的顺序确定下来之后,想对其中的某些环节进行复用也很困难,牵一发而动全身。举个例子,如果你想对 fs.stat 读取文件信息这段代码复用,因为回调中引用了外层的变量,提取出来后还需要对外层的代码进行修改。2.堆栈信息被断开我们知道,JavaScript 引擎维护了一个执行上下文栈,当函数执行的时候,会创建该函数的执行上下文压入栈中,当函数执行完毕后,会将该执行上下文出栈。如果 A 函数中调用了 B 函数,JavaScript 会先将 A 函数的执行上下文压入栈中,再将 B 函数的执行上下文压入栈中,当 B 函数执行完毕,将 B 函数执行上下文出栈,当 A 函数执行完毕后,将 A 函数执行上下文出栈。这样的好处在于,我们如果中断代码执行,可以检索完整的堆栈信息,从中获取任何我们想获取的信息。可是异步回调函数并非如此,比如执行 fs.readdir 的时候,其实是将回调函数加入任务队列中,代码继续执行,直至主线程完成后,才会从任务队列中选择已经完成的任务,并将其加入栈中,此时栈中只有这一个执行上下文,如果回调报错,也无法获取调用该异步操作时的栈中的信息,不容易判定哪里出现了错误。此外,因为是异步的缘故,使用 try catch 语句也无法直接捕获错误。(不过 Promise 并没有解决这个问题)3.借助外层变量当多个异步计算同时进行,比如这里遍历读取文件信息,由于无法预期完成顺序,必须借助外层作用域的变量,比如这里的 count、errored、stats 等,不仅写起来麻烦,而且如果你忽略了文件读取错误时的情况,不记录错误状态,就会接着读取其他文件,造成无谓的浪费。此外外层的变量,也可能被其它同一作用域的函数访问并且修改,容易造成误操作。之所以单独讲讲回调地狱,其实是想说嵌套和缩进只是回调地狱的一个梗而已,它导致的问题远非嵌套导致的可读性降低而已。PromisePromise 使得以上绝大部分的问题都得到了解决。1. 嵌套问题举个例子:request(url, function(err, res, body) { if (err) handleError(err); fs.writeFile(‘1.txt’, body, function(err) { request(url2, function(err, res, body) { if (err) handleError(err) }) })});使用 Promise 后:request(url).then(function(result) { return writeFileAsynv(‘1.txt’, result)}).then(function(result) { return request(url2)}).catch(function(e){ handleError(e)});而对于读取最大文件的那个例子,我们使用 promise 可以简化为:var fs = require(‘fs’);var path = require(‘path’);var readDir = function(dir) { return new Promise(function(resolve, reject) { fs.readdir(dir, function(err, files) { if (err) reject(err); resolve(files) }) })}var stat = function(path) { return new Promise(function(resolve, reject) { fs.stat(path, function(err, stat) { if (err) reject(err) resolve(stat) }) })}function findLargest(dir) { return readDir(dir) .then(function(files) { let promises = files.map(file => stat(path.join(dir, file))) return Promise.all(promises).then(function(stats) { return { stats, files } }) }) .then(data => { let largest = data.stats .filter(function(stat) { return stat.isFile() }) .reduce((prev, next) => { if (prev.size > next.size) return prev return next }) return data.files[data.stats.indexOf(largest)] })}2. 控制反转再反转前面我们讲到使用第三方回调 API 的时候,可能会遇到如下问题:回调函数执行多次回调函数没有执行回调函数有时同步执行有时异步执行对于第一个问题,Promise 只能 resolve 一次,剩下的调用都会被忽略。对于第二个问题,我们可以使用 Promise.race 函数来解决:function timeoutPromise(delay) { return new Promise( function(resolve,reject){ setTimeout( function(){ reject( “Timeout!” ); }, delay ); } );}Promise.race( [ foo(), timeoutPromise( 3000 )] ).then(function(){}, function(err){});对于第三个问题,为什么有的时候会同步执行有的时候回异步执行呢?我们来看个例子:var cache = {…};function downloadFile(url) { if(cache.has(url)) { // 如果存在cache,这里为同步调用 return Promise.resolve(cache.get(url)); } return fetch(url).then(file => cache.set(url, file)); // 这里为异步调用}console.log(‘1’);getValue.then(() => console.log(‘2’));console.log(‘3’);在这个例子中,有 cahce 的情况下,打印结果为 1 2 3,在没有 cache 的时候,打印结果为 1 3 2。然而如果将这种同步和异步混用的代码作为内部实现,只暴露接口给外部调用,调用方由于无法判断是到底是异步还是同步状态,影响程序的可维护性和可测试性。简单来说就是同步和异步共存的情况无法保证程序逻辑的一致性。然而 Promise 解决了这个问题,我们来看个例子:var promise = new Promise(function (resolve){ resolve(); console.log(1);});promise.then(function(){ console.log(2);});console.log(3);// 1 3 2即使 promise 对象立刻进入 resolved 状态,即同步调用 resolve 函数,then 函数中指定的方法依然是异步进行的。PromiseA+ 规范也有明确的规定:实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。Promise 反模式1.Promise 嵌套// badloadSomething().then(function(something) { loadAnotherthing().then(function(another) { DoSomethingOnThem(something, another); });});// goodPromise.all([loadSomething(), loadAnotherthing()]).then(function ([something, another]) { DoSomethingOnThem(…[something, another]);});2.断开的 Promise 链// badfunction anAsyncCall() { var promise = doSomethingAsync(); promise.then(function() { somethingComplicated(); }); return promise;}// goodfunction anAsyncCall() { var promise = doSomethingAsync(); return promise.then(function() { somethingComplicated() });}3.混乱的集合// badfunction workMyCollection(arr) { var resultArr = []; function _recursive(idx) { if (idx >= resultArr.length) return resultArr; return doSomethingAsync(arr[idx]).then(function(res) { resultArr.push(res); return _recursive(idx + 1); }); } return _recursive(0);}你可以写成:function workMyCollection(arr) { return Promise.all(arr.map(function(item) { return doSomethingAsync(item); }));}如果你非要以队列的形式执行,你可以写成:function workMyCollection(arr) { return arr.reduce(function(promise, item) { return promise.then(function(result) { return doSomethingAsyncWithResult(item, result); }); }, Promise.resolve());}4.catch// badsomethingAync.then(function() { return somethingElseAsync();}, function(err) { handleMyError(err);});如果 somethingElseAsync 抛出错误,是无法被捕获的。你可以写成:// goodsomethingAsync.then(function() { return somethingElseAsync()}).then(null, function(err) { handleMyError(err);});// goodsomethingAsync().then(function() { return somethingElseAsync();}).catch(function(err) { handleMyError(err);});红绿灯问题题目:红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用 Promse 实现)三个亮灯函数已经存在:function red(){ console.log(‘red’);}function green(){ console.log(‘green’);}function yellow(){ console.log(‘yellow’);}利用 then 和递归实现:function red(){ console.log(‘red’);}function green(){ console.log(‘green’);}function yellow(){ console.log(‘yellow’);}var light = function(timmer, cb){ return new Promise(function(resolve, reject) { setTimeout(function() { cb(); resolve(); }, timmer); });};var step = function() { Promise.resolve().then(function(){ return light(3000, red); }).then(function(){ return light(2000, green); }).then(function(){ return light(1000, yellow); }).then(function(){ step(); });}step();promisify有的时候,我们需要将 callback 语法的 API 改造成 Promise 语法,为此我们需要一个 promisify 的方法。因为 callback 语法传参比较明确,最后一个参数传入回调函数,回调函数的第一个参数是一个错误信息,如果没有错误,就是 null,所以我们可以直接写出一个简单的 promisify 方法:function promisify(original) { return function (…args) { return new Promise((resolve, reject) => { args.push(function callback(err, …values) { if (err) { return reject(err); } return resolve(…values) }); original.call(this, …args); }); };}完整的可以参考 es6-promisifPromise 的局限性1. 错误被吃掉首先我们要理解,什么是错误被吃掉,是指错误信息不被打印吗?并不是,举个例子:throw new Error(’error’);console.log(233333);在这种情况下,因为 throw error 的缘故,代码被阻断执行,并不会打印 233333,再举个例子:const promise = new Promise(null);console.log(233333);以上代码依然会被阻断执行,这是因为如果通过无效的方式使用 Promise,并且出现了一个错误阻碍了正常 Promise 的构造,结果会得到一个立刻跑出的异常,而不是一个被拒绝的 Promise。然而再举个例子:let promise = new Promise(() => { throw new Error(’error’)});console.log(2333333);这次会正常的打印 233333,说明 Promise 内部的错误不会影响到 Promise 外部的代码,而这种情况我们就通常称为 “吃掉错误”。其实这并不是 Promise 独有的局限性,try..catch 也是这样,同样会捕获一个异常并简单的吃掉错误。而正是因为错误被吃掉,Promise 链中的错误很容易被忽略掉,这也是为什么会一般推荐在 Promise 链的最后添加一个 catch 函数,因为对于一个没有错误处理函数的 Promise 链,任何错误都会在链中被传播下去,直到你注册了错误处理函数。2. 单一值Promise 只能有一个完成值或一个拒绝原因,然而在真实使用的时候,往往需要传递多个值,一般做法都是构造一个对象或数组,然后再传递,then 中获得这个值后,又会进行取值赋值的操作,每次封装和解封都无疑让代码变得笨重。说真的,并没有什么好的方法,建议是使用 ES6 的解构赋值:Promise.all([Promise.resolve(1), Promise.resolve(2)]).then(([x, y]) => { console.log(x, y);});3. 无法取消Promise 一旦新建它就会立即执行,无法中途取消。4. 无法得知 pending 状态当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。参考《你不知道的 JavaScript 中卷》Promise 的 N 种用法JavaScript Promise 迷你书Promises/A+规范 Promise 如何使用Promise Anti-patterns一道关于Promise应用的面试题ES6 系列ES6 系列目录地址:https://github.com/mqyqingfeng/BlogES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ...

October 17, 2018 · 4 min · jiezi

麻雀虽小五脏俱全的Vue拉勾项目,看看应该有帮助

全栈系列Vue版拉勾,客官们来瞧瞧模拟拉勾app系列—vue前端界面github地址,来猛戳吧前言本项目是本人在闲暇时间编写的一个初级引导项目,麻雀虽小五脏俱全,所使用的东西绝大多数在开发中都能用得到,但难免会存在很多地方需要完善。由于近期要备战法考,且工作繁忙,没有时间维护,还存在很多BUG或需要优化的地方,希望多多提出(有空了就改),当然能给个star什么的就更好了。为了方便访问,也加入了mock数据,但不是很全,若需要完整体验,请按照下方步骤实现。前端项目由Vue框架编写,其余部分涉及到node、python等可移至下方项目或自行查阅。注意:本项目个人开发练习,不作为任何商业用途todolist登录/注册 √页面首次加载面 √城市选择 √文章阅读 √搜索职位 √条件筛选 √搜索公司 √下拉刷新上拉加载 √数据排序 √图片懒加载 √提问评论√提问信息编辑 √言职社区记录 √话题关注 √收藏√求职意向√投递记录√注销√基础设置√头像修改 √简历填写√简历生成并下载pdf√ps:还有很多很多东西,不一一列举,想到啥就做啥技术栈前端:vue全家桶es6scssmint-uimockjsjquery转发服务器:nodeexpress实际api服务器:python3mongodb爬虫:python3效果演示首次载入登录注册首页文章阅读选择城市职位查看筛选排序排序2简历修改我的设置ps:还有更多的设置就不截图了,有点大,有兴趣的clone下去看看吧线上地址说明前端地址:https://github.com/qianbin01/…代理api地址:https://github.com/qianbin01/…api地址:https://github.com/qianbin01/…爬虫地址:https://github.com/qianbin01/…项目配置ubuntu 16.04运行步骤必备步骤:运行爬虫项目运行python-api项目运行node-api转发项目运行本项目本项目步骤:git clone https://github.com/qianbin01/...cd lagou_vuenpm install/yarn installnpm run dev/npm start浏览器访问http://localhost:8085点点你们的小手吧知乎专栏:https://zhuanlan.zhihu.com/c_… 掘金:https://juejin.im/user/5b8291… 思否:https://segmentfault.com/u/qi… 希望对大家有帮助大佬们,点赞一波吧

October 10, 2018 · 1 min · jiezi

JavaScript(E5,6) 正则学习总结学习,可看可不看!

1.概述正则表达式(regular expression)是一种表达文本模式(即字符串结构)的方法。创建方式有两种方式:一种是使用字面量,以斜杠表示开始和结束。var regex = /xyz/另一种是使用RegExp构造函数。var regex = new RegExp(‘xyz’); 它们的主要区别是,第一种方法在引擎编译代码时,就会新建正则表达式,第二种方法在运行时新建正则表达式,所以前者的效率较高。而且,前者比较便利和直观,所以实际应用中,基本上都采用字面量定义正则表达式。2.实例属性i:忽略大小写m:多行模式g:全局搜索3.实例方法3.1 RegExp.prototype.test()正则实例对象的test方法返回一个布尔值,表示当前模式是否能匹配参数字符串。/小智/.test(‘小智 终身学习执行者’) // truereg.exec(str) 返回匹配结果数组,不匹配则返回null,每执行一次exec就向后匹配一次3.2 RegExp.prototype.exec()3.2.1 reg.exec(str) 返回匹配结果数组,不匹配则返回null,每执行一次exec就向后匹配一次var s = ‘_x_x’;var r1 = /x/;var r2 = /y/;r1.exec(s) // [“x”]r2.exec(s) // null3.2.1.2如果表达式里有括号(),称为组匹配,返回结果中,第一个是整体匹配结果,后面依次是每个括号匹配的结果var s = ‘x_x’;var r = /(x)/;r.exec(s) // ["x", “x”]exec方法的返回数组还包含以下两个属性:input:整个原字符串。index:整个模式匹配成功的开始位置(从0开始计数)。var r = /a(b+)a/;var arr = r.exec(’abbba_aba’);arr // [“abbba”, “bbb”]arr.index // 1arr.input // “abbba_aba“3.2.3 如果表达式中有g选项进行全局搜索,则可以多次使用 exec,下次的匹配从上次的结果后开始 var reg = /a/g;var str = ‘abc_abc_abc’var r1 = reg.exec(str);r1 // [“a”]r1.index // 0reg.lastIndex // 1var r2 = reg.exec(str);r2 // [“a”]r2.index // 4reg.lastIndex // 5var r3 = reg.exec(str);r3 // [“a”]r3.index // 8reg.lastIndex // 9var r4 = reg.exec(str);r4 // nullreg.lastIndex // 04.字符串的实例方法4.1 str.match(reg),与 reg.exec相似,但是,如果使用g选项,则str.match一次性返回所有结果。var s = ‘abba’;var r = /a/g;s.match(r) // [“a”, “a”]r.exec(s) // [“a”]4.2 str.search(reg) ,返回匹配成功的第一个位置,如果没有任何匹配,则返回-1。’x_x’.search(/x/)// 14.3 str.replace(reg,newstr) ;用第一个参数reg去匹配,用第二个参数newstr 去替换,正则表达式如果不加g修饰符,就替换第一个匹配成功的值,否则替换所有匹配成功的值。‘aaa’.replace(‘a’, ‘b’) // “baa”‘aaa’.replace(/a/, ‘b’) // “baa”‘aaa’.replace(/a/g, ‘b’) // “bbb” 4.4 str.split(reg[,maxLength]) 用匹配的模式切割,第二个参数是限制返回结果的最大数量5. 匹配规则5.1 字面量字符和元字符大部分字符在正则表达式中,就是字面的含义,比如/a/匹配a,/b/匹配b。如果在正则表达式之中,某个字符只表示它字面的含义(就像前面的a和b),那么它们就叫做“字面量字符”(literal characters)。除了字面量字符以外,还有一部分字符有特殊含义,不代表字面的意思。它们叫做“元字符”(metacharacters),主要有以下几个。(1) 点字符(.)点字符(.)匹配除回车(r)、换行(n) 、行分隔符(u2028)和段分隔符(u2029)以外的所有字符。/c.t/上面代码中,c.t匹配c和t之间包含任意一个字符的情况,只要这三个字符在同一行,比如cat、c2t、c-t等等,但是不匹配coot。(2)位置字符^ 表示字符串的开始位置$ 表示字符串的结束位置// test必须出现在开始位置/^test/.test(’test123’) // true// test必须出现在结束位置/test$/.test(’new test’) // true// 从开始位置到结束位置只有test/^test$/.test(’test’) // true/^test$/.test(’test test’) // false(3)选择符(|)竖线符号(|)在正则表达式中表示“或关系”(OR),即cat|dog表示匹配cat或dog。/11|22/.test(‘911’) // true上面代码中,正则表达式指定必须匹配11或22。5.2 转义符正则表达式中那些有特殊含义的元字符,如果要匹配它们本身,就需要在它们前面要加上反斜杠。比如要匹配+,就要写成+。/1+1/.test(‘1+1’)// false/1+1/.test(‘1+1’)// true正则表达式中,需要反斜杠转义的,一共有12个字符:^、.、[、$、(、)、|、*、+、?、{和。需要特别注意的是,如果使用RegExp方法生成正则对象,转义需要使用两个斜杠,因为字符串内部会先转义一次。(new RegExp(‘1+1’)).test(‘1+1’)// false(new RegExp(‘1\+1’)).test(‘1+1’)// true5.3 字符类字符类(class)表示有一系列字符可供选择,只要匹配其中一个就可以了。所有可供选择的字符都放在方括号内,比如[xyz] 表示x、y、z之中任选一个匹配。/[abc]/.test(‘hello world’) // false/[abc]/.test(‘apple’) // true有两个字符在字符类中有特殊含义。(1)脱字符(^)如果方括号内的第一个字符是[^xyz]表示除了x、y、z之外都可以匹配:/[^abc]/.test(‘hello world’) // true/[^abc]/.test(‘bbc’) // false如果方括号内没有其他字符,即只有[^],就表示匹配一切字符,其中包括换行符。相比之下,点号作为元字符(.)是不包括换行符的。var s = ‘Please yes\nmake my day!’;s.match(/yes.*day/) // nulls.match(/yes[^]*day/) // [ ‘yes\nmake my day’]上面代码中,字符串s含有一个换行符,点号不包括换行符,所以第一个正则表达式匹配失败;第二个正则表达式[^]包含一切字符,所以匹配成功。 (2)连字符(-) 某些情况下,对于连续序列的字符,连字符(-)用来提供简写形式,表示字符的连续范围。比如,[abc]可以写成[a-c],[0123456789]可以写成[0-9],同理[A-Z]表示26个大写字母。/a-z/.test(‘b’) // false/[a-z]/.test(‘b’) // true 以下都是合法的字符类简写形式。[0-9.,][0-9a-fA-F][a-zA-Z0-9-][1-31]上面代码中最后一个字符类[1-31],不代表1到31,只代表1到3。另外,不要过分使用连字符,设定一个很大的范围,否则很可能选中意料之外的字符。最典型的例子就是[A-z],表面上它是选中从大写的A到小写的z之间52个字母,但是由于在 ASCII 编码之中,大写字母与小写字母之间还有其他字符,结果就会出现意料之外的结果。/[A-z]/.test(’\’) // true上面代码中,由于反斜杠(’’)的ASCII码在大写字母与小写字母之间,结果会被选中。5.4 预定义模式预定义模式指的是某些常见模式的简写方式。d 匹配0-9之间的任一数字,相当于[0-9]。D 匹配所有0-9以外的字符,相当于[^0-9]。w 匹配任意的字母、数字和下划线,相当于[A-Za-z0-9]。W 除所有字母、数字和下划线以外的字符,相当于[^A-Za-z0-9]。s 匹配空格(包括换行符、制表符、空格符等),相等于[ \t\r\n\v\f]。S 匹配非空格的字符,相当于[^ \t\r\n\v\f]。b 匹配词的边界。B 匹配非词边界,即在词的内部。// \s 的例子/\s\w*/.exec(‘hello world’) // [” world”]// \b 的例子/\bworld/.test(‘hello world’) // true/\bworld/.test(‘hello-world’) // true/\bworld/.test(‘helloworld’) // false// \B 的例子/\Bworld/.test(‘hello-world’) // false/\Bworld/.test(‘helloworld’) // true通常,正则表达式遇到换行符(n)就会停止匹配。var html = “<b>Hello</b>\n<i>world!</i>”;/./.exec(html)[0]// “<b>Hello</b>“上面代码中,字符串html包含一个换行符,结果点字符(.)不匹配换行符,导致匹配结果可能不符合原意。这时使用s字符类,就能包括换行符。var html = “<b>Hello</b>\n<i>world!</i>”;/[\S\s]/.exec(html)[0]// “<b>Hello</b>\n<i>world!</i>“上面代码中,[Ss]指代一切字符。5.5 重复类模式的精确匹配次数,使用大括号({})表示。{n}表示恰好重复n次,{n,}表示至少重复n次,{n,m}表示重复不少于n次,不多于m次。/lo{2}k/.test(’look’) // true/lo{2,5}k/.test(’looook’) // true上面代码中,第一个模式指定o连续出现2次,第二个模式指定o连续出现2次到5次之间。5.6 量词符*. ? 问号表示某个模式出现0次或1次,等同于{0, 1}。. * 星号表示某个模式出现0次或多次,等同于{0,}。. + 加号表示某个模式出现1次或多次,等同于{1,}。5.7 贪婪模式上一小节的三个量词符,默认情况下都是最大可能匹配,即匹配直到下一个字符不满足匹配规则为止。这被称为贪婪模式。var s = ‘aaa’;s.match(/a+/) // [“aaa”]上面代码中,模式是/a+/,表示匹配1个a或多个a,那么到底会匹配几个a呢?因为默认是贪婪模式,会一直匹配到字符a不出现为止,所以匹配结果是3个a。如果想将贪婪模式改为非贪婪模式,可以在量词符后面加一个问号。var s = ‘aaa’;s.match(/a+?/) // [“a”]除了非贪婪模式的加号,还有非贪婪模式的星号()和非贪婪模式的问号(?)+?:表示某个模式出现1次或多次,匹配时采用非贪婪模式。?:表示某个模式出现0次或多次,匹配时采用非贪婪模式。??:表格某个模式出现0次或1次,匹配时采用非贪婪模式。5.8 组匹配(1)概述正则表达式的括号表示分组匹配,括号中的模式可以用来匹配分组的内容。/fred+/.test(‘fredd’) // true/(fred)+/.test(‘fredfred’) // true上面代码中,第一个模式没有括号,结果+只表示重复字母d,第二个模式有括号,结果+就表示匹配fred这个词。 下面是另外一个分组捕获的例子。var m = ‘abcabc’.match(/(.)b(.)/);m// [‘abc’, ‘a’, ‘c’] 上面代码中,正则表达式/(.)b(.)/一共使用两个括号,第一个括号捕获a,第二个括号捕获c。注意,使用组匹配时,不宜同时使用g修饰符,否则match方法不会捕获分组的内容。var m = ‘abcabc’.match(/(.)b(.)/g);m // [‘abc’, ‘abc’]正则表达式内部,还可以用n引用括号匹配的内容,n是从1开始的自然数,表示对应顺序的括号。/(.)b(.)\1b\2/.test(“abcabc”)// true上面的代码中,1表示第一个括号匹配的内容(即a),2表示第二个括号匹配的内容(即c)。(2)非捕获组(?:x)称为非捕获组(Non-capturing group),表示不返回该组匹配的内容,即匹配的结果中不计入这个括号。非捕获组的作用请考虑这样一个场景,假定需要匹配foo或者foofoo,正则表达式就应该写成/(foo){1, 2}/,但是这样会占用一个组匹配。这时,就可以使用非捕获组,将正则表达式改为/(?:foo){1, 2}/,它的作用与前一个正则是一样的,但是不会单独输出括号内部的内容。var m = ‘abc’.match(/(?:.)b(.)/);m // [“abc”, “c”]上面代码中的模式,一共使用了两个括号。其中第一个括号是非捕获组,所以最后返回的结果中没有第一个括号,只有第二个括号匹配的内容。(3)先行断言x(?=y)称为先行断言(Positive look-ahead),x只有在y前面才匹配,y不会被计入返回结果。比如,要匹配后面跟着百分号的数字,可以写成/d+(?=%)/。“先行断言”中,括号里的部分是不会返回的。var m = ‘abc’.match(/b(?=c)/);m // [“b”]上面的代码使用了先行断言,b在c前面所以被匹配,但是括号对应的c不会被返回。(4)先行否定断言x(?!y)称为先行否定断言(Negative look-ahead),x只有不在y前面才匹配,y不会被计入返回结果。比如,要匹配后面跟的不是百分号的数字,就要写成/d+(?!%)/。/\d+(?!.)/.exec(‘3.14’)// [“14”]上面代码中,正则表达式指定,只有不在小数点前面的数字才会被匹配,因此返回的结果就是14。6. 实战6.1 消除字符串首尾两端的空格 var str = ’ #id div.class ‘; str.replace(/^\s+|\s+$/g, ‘’) // “#id div.class"6.2 验证手机号码var reg = /1[24578]\d{9}/;reg.test(‘154554568997’); //truereg.test(‘234554568997’); //false6.3 把手机号码替换成 var reg = /1[24578]\d{9}/;var str = ‘姓名:张三 手机:18210999999 性别:男’;str.replace(reg, ‘’) //“姓名:张三 手机:* 性别:男"6.4 匹配网页标签var strHtlm = ‘小智小智<div>222222@.qq.com</div>小智小智’;var reg = /<(.+)>.+</\1>/;strHtlm.match(reg); // ["<div>222222@.qq.com</div>"]6.5 替换敏感字let str = ‘中国共产党中国人民解放军中华人民共和国’;let r = str.replace(/中国|军/g, input => { let t = ‘’; for (let i = 0; i<input.length; i++) { t += ‘’; } return t;}) console.log(r); //共产党人民解放中华人民共和国 6.6 千位分隔符let str = ‘100002003232322’;let r = str.replace(/(\d)(?=(?:\d{3})+$)/g, ‘$1,’);console.log(r); //100,002,003,232,322参考链接https://developer.mozilla.org…https://wangdoc.com/javascrip… 一个笨笨的码农,我的世界只能终身学习! ...

October 5, 2018 · 2 min · jiezi

那些必会用到的 ES6 精粹

前言最新的 ECMAScript 都已经到发布到 2018 版了。我们应该有的态度是: Stay hungry ! Stay young !从接触 vue 到工作中用到 vue 将近 2 年了,在开发 vue 项目中用到了很多 es6 的 api ,es6 给我的开发带来了很大便利。本文只总结小汪在工作和面试中经常遇到的 ES6 及之后的新 api 。有空就得多总结,一边总结,一边重温学习!!!正文1 let 和 constlet 的作用域与 const 命令相同:只在声明所在的块级作用域内有效。且不存在变量提升 。1.1 letlet 所声明的变量,可以改变。let a = 123a = 456 // 正确,可以改变let b = [123]b = [456] // 正确,可以改变1.2 constconst 声明一个只读的常量。一旦声明,常量的值就不能改变。简单类型的数据(数值、字符串、布尔值),不可以变动const a = 123a = 456 // 报错,不可改变const b = [123]b = [456] // 报错,不可以重新赋值,不可改变复合类型的数据(主要是对象和数组),可以这样子变动const a = [123]a.push(456) // 成功const b = {}b.name = ‘demo’ // 成功1.3 不存在变量提升{ let a = 10; var b = 1;}a // ReferenceError: a is not defined.b // 1所以 for循环的计数器,就很合适使用 let 命令。let a = [];for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); };}a6; // 61.4 推荐对于 数值、字符串、布尔值 经常会变的,用 let 声明。对象、数组和函数用 const 来声明。// 如经常用到的导出 函数export const funA = function(){ // ….}2 解构(Destructuring)2.1 数组一次性声明多个变量:let [a, b, c] = [1, 2, 3];console.log(a) // 1console.log(b) // 2console.log(c) // 3结合扩展运算符:let [head, …tail] = [1, 2, 3, 4];console.log(head) // 1console.log(tail) // [2, 3, 4]解构赋值允许指定默认值:let [foo = true] = [];foo // truelet [x, y = ‘b’] = [‘a’];// x=‘a’, y=‘b'2.2 对象解构不仅可以用于数组,还可以用于对象。let { a, b } = { a: “aaa”, b: “bbb” };a // “aaa"b // “bbb"数组中,变量的取值由它 排列的位置 决定;而对象中,变量必须与 属性 同名,才能取到正确的值。对象的解构也可以指定默认值。let {x = 3} = {};x // 3let {x, y = 5} = {x: 1};x // 1y // 52.3 字符串字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。const [a, b, c, d, e] = ‘hello’;a // “h"b // “e"c // “l"d // “l"e // “o"2.4 用途交换变量的值let x = 1;let y = 2;[x, y] = [y, x];从函数返回多个值// 返回一个数组function example() { let [a, b, c] = [1, 2, 3] return [a, b, c] }let [a, b, c] = example();// 返回一个对象function example() { return { foo: 1, bar: 2 };}let { foo, bar } = example();函数参数的默认值function funA (a = 1, b = 2){ return a + b;}funA(3) // 5 因为 a 是 3, b 是 2funA(3,3) // 6 因为 a 是 3, b 是 3输入模块的指定方法加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。const { SourceMapConsumer, SourceNode } = require(“source-map”);在 utils.js 中:export const function A (){ console.log(‘A’)}export const function B (){ console.log(‘B’)}export const function C (){ console.log(‘C’)}在 组件中引用时:import { A, B, C } from “./utils.js” //调用A() // 输出 A 3. 模板字符串(template string)模板字符串(template string)用反引号()标识。3.1 纯字符串所有模板字符串的空格和换行,都是被保留的.console.log(输出值为 N, 换行)// "输出值为 N换行"3.2 字符串中加变量模板字符串中嵌入变量,需要将变量名写在 ${ } 之中let x = 1;let y = 2;console.log(输出值为:${x}) // "输出值为:1"console.log(输出值为:${x + y}) // "输出值为:3"3.3 模板字符串之中还能调用函数。function fn() { return "Hello World";}console.log(输出值为:${fn()}`) // “输出值为:Hello World"4. 字符串函数扩展includes():返回布尔值,表示是否找到了参数字符串。startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。let s = ‘Hello world!’;s.startsWith(‘Hello’) // trues.endsWith(’!’) // trues.includes(‘o’) // true这三个方法都支持第二个参数,表示开始搜索的位置。let s = ‘Hello world!’;s.startsWith(‘world’, 6) // trues.endsWith(‘Hello’, 5) // trues.includes(‘Hello’, 6) // false5. 数值扩展5.1 指数运算符ES2016 新增了一个指数运算符()。2 ** 2 // 42 ** 3 // 8这个运算符的一个特点是右结合,而不是常见的左结合。多个指数运算符连用时,是从最右边开始计算的。// 相当于 2 ** (3 ** 2)2 ** 3 ** 2// 512上面代码中,首先计算的是第二个指数运算符,而不是第一个。指数运算符可以与等号结合,形成一个新的赋值运算符(=)。let a = 1.5;a **= 2;// 等同于 a = a * a;let b = 4;b *= 3;// 等同于 b = b * b * b;6. 函数的扩展除了在解构中说到的函数参数的默认值,还有不少经常会用到的方法。6. 1 rest 参数ES6 引入 rest 参数(形式为…变量名),用于获取函数的多余参数,这样就不需要使用 arguments 对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。function add(…values) { let sum = 0; for (let val of values) { sum += val; } return sum;}add(2, 5, 3) // 10上面代码的 add 函数是一个求和函数,利用 rest 参数,可以向该函数传入任意数目的参数。注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。// 报错function f(a, …b, c) { // …}6.2 箭头函数ES6 允许使用“箭头”(=>)定义函数。const f = v => v;console.log(‘输出值:’, f(3)) // 输出值: 3// 等同于const f = function (v) { return v;};如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。// 等同于const f = function () { return 5 };const sum = (num1, num2) => num1 + num2;// 等同于const sum = function(num1, num2) { return num1 + num2;};如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用 return 语句返回。const sum = (num1, num2) => { return num1 + num2; }箭头函数的一个用处是简化回调函数。const square = n => n * n;// 正常函数写法[1,2,3].map(function (x) { return x * x;});// 箭头函数写法[1,2,3].map(x => x * x);注意: 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。this 对象的指向是可变的,但是在箭头函数中,它是固定的。function foo() { setTimeout(() => { console.log(‘id:’, this.id); }, 100);}let id = 21;foo.call({ id: 42 });// id: 42上面代码中,setTimeout 的参数是一个箭头函数,这个箭头函数的定义生效是在 foo 函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时 this 应该指向全局对象window,这时应该输出 21。但是,箭头函数导致 this 总是指向函数定义生效时所在的对象(本例是{ id: 42}),所以输出的是 42。7. 数组的扩展扩展运算符(spread)是三个点(…)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。7.1 数组合并的新写法。const arr1 = [‘a’, ‘b’];const arr2 = [‘c’];const arr3 = [’d’, ’e’];// ES5 的合并数组arr1.concat(arr2, arr3);// [ ‘a’, ‘b’, ‘c’, ’d’, ’e’ ]// ES6 的合并数组[…arr1, …arr2, …arr3]// [ ‘a’, ‘b’, ‘c’, ’d’, ’e’ ]7.2 函数调用。function add(x, y) { return x + y;}const numbers = [4, 4];add(…numbers) // 87.3 复制数组的简便写法。const a1 = [1, 2];// 写法一const a2 = […a1];a2[0] = 2;a1 // [1, 2]// 写法二const […a2] = a1;a2[0] = 2;a1 // [1, 2]上面的两种写法,a2 都是 a1 的克隆,且不会修改原来的数组。7.4 将字符串转为真正的数组。[…‘hello’]// [ “h”, “e”, “l”, “l”, “o” ]7.5 数组实例的 entries(),keys() 和 values()用 for…of 循环进行遍历,唯一的区别是 keys() 是对键名的遍历、values() 是对键值的遍历,entries() 是对键值对的遍历。for (let index of [‘a’, ‘b’].keys()) { console.log(index);}// 0// 1for (let elem of [‘a’, ‘b’].values()) { console.log(elem);}// ‘a’// ‘b’for (let [index, elem] of [‘a’, ‘b’].entries()) { console.log(index, elem);}// 0 “a”// 1 “b"7.6 includes()Array.prototype.includes 方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的 includes 方法类似。ES2016 引入了该方法。[1, 2, 3].includes(2) // true[1, 2, 3].includes(4) // false[1, 2, NaN].includes(NaN) // true该方法的第二个参数表示搜索的起始位置,默认为 0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为 -4,但数组长度为 3 ),则会重置为从 0 开始。[1, 2, 3].includes(3, 3); // false[1, 2, 3].includes(3, -1); // true8. 对象的扩展8.1 属性和方法 的简洁表示法let birth = ‘2000/01/01’;const Person = { name: ‘张三’, //等同于birth: birth birth, // 等同于hello: function ()… hello() { console.log(‘我的名字是’, this.name); }};8.2 Object.assign()Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。const target = { a: 1 };const source1 = { b: 2 };const source2 = { c: 3 };Object.assign(target, source1, source2);target // {a:1, b:2, c:3}Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。const target = { a: 1, b: 1 };const source1 = { b: 2, c: 2 };const source2 = { c: 3 };Object.assign(target, source1, source2);target // {a:1, b:2, c:3}Object.assign 方法实行的是浅拷贝,而不是深拷贝。const obj1 = {a: {b: 1}};const obj2 = Object.assign({}, obj1);obj1.a.b = 2;obj2.a.b // 2上面代码中,源对象 obj1 的 a 属性的值是一个对象,Object.assign 拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。9. SetES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。Set 本身是一个构造函数,用来生成 Set 数据结构。// 基本用法const s = new Set();[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));for (let i of s) { console.log(i);}// 2 3 5 4// 去除数组的重复成员const array = [1, 1, 2, 3, 4, 4][…new Set(array)]// [1, 2, 3, 4]10. Promise 对象Promise 是异步编程的一种解决方案。Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。Promise 对象的状态改变,只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)const someAsyncThing = function(flag) { return new Promise(function(resolve, reject) { if(flag){ resolve(‘ok’); }else{ reject(’error’) } });};someAsyncThing(true).then((data)=> { console.log(‘data:’,data); // 输出 ‘ok’}).catch((error)=>{ console.log(’error:’, error); // 不执行})someAsyncThing(false).then((data)=> { console.log(‘data:’,data); // 不执行}).catch((error)=>{ console.log(’error:’, error); // 输出 ’error’})上面代码中,someAsyncThing 函数成功返回 ‘OK’, 失败返回 ‘error’, 只有失败时才会被 catch 捕捉到。最简单实现:// 发起异步请求 fetch(’/api/todos’) .then(res => res.json()) .then(data => ({ data })) .catch(err => ({ err }));来看一道有意思的面试题:setTimeout(function() { console.log(1)}, 0);new Promise(function executor(resolve) { console.log(2); for( var i=0 ; i<10000 ; i++ ) { i == 9999 && resolve(); } console.log(3);}).then(function() { console.log(4);});console.log(5);这道题应该考察 JavaScript 的运行机制的。首先先碰到一个 setTimeout,于是会先设置一个定时,在定时结束后将传递这个函数放到任务队列里面,因此开始肯定不会输出 1 。然后是一个 Promise,里面的函数是直接执行的,因此应该直接输出 2 3 。然后,Promise 的 then 应当会放到当前 tick 的最后,但是还是在当前 tick 中。因此,应当先输出 5,然后再输出 4 。最后在到下一个 tick,就是 1 。答案: “2 3 5 4 1” 11. async 函数ES2017 标准引入了 async 函数,使得异步操作变得更加方便。async 函数的使用方式,直接在普通函数前面加上 async,表示这是一个异步函数,在要异步执行的语句前面加上 await,表示后面的表达式需要等待。async 是 Generator 的语法糖async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。async function f() { return ‘hello world’;}f().then(v => console.log(v))// “hello world"上面代码中,函数 f 内部 return 命令返回的值,会被 then 方法回调函数接收到。async 函数内部抛出错误,会导致返回的 Promise 对象变为 reject 状态。抛出的错误对象会被 catch 方法回调函数接收到。async function f() { throw new Error(‘出错了’);}f().then( result => console.log(result), error => console.log(error))// Error: 出错了async 函数返回的 Promise 对象,必须等到内部所有 await 命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到 return 语句或者抛出错误。也就是说,只有 async 函数内部的异步操作执行完,才会执行 then 方法指定的回调函数。下面是一个例子:async function getTitle(url) { let response = await fetch(url); let html = await response.text(); return html.match(/<title>([\s\S]+)</title>/i)[1];}getTitle(‘https://tc39.github.io/ecma262/').then(console.log('完成’))// “ECMAScript 2017 Language Specification"上面代码中,函数 getTitle 内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行 then 方法里面的 console.log。在 vue 中,我们可能要先获取 token ,之后再用 token 来请求用户数据什么的,可以这样子用:methods:{ getToken() { return new Promise((resolve, reject) => { this.$http.post(’/token’) .then(res => { if (res.data.code === 200) { resolve(res.data.data) } else { reject() } }) .catch(error => { console.error(error); }); }) }, getUserInfo(token) { return new Promise((resolve, reject) => { this.$http.post(’/userInfo’,{ token: token }) .then(res => { if (res.data.code === 200) { resolve(res.data.data) } else { reject() } }) .catch(error => { console.error(error); }); }) }, async initData() { let token = await this.getToken() this.userInfo = this.getUserInfo(token) },}12. import 和 exportimport 导入模块、export 导出模块// example2.js // 导出默认, 有且只有一个默认export default const example2 = { name : ‘my name’, age : ‘my age’, getName = function(){ return ‘my name’ }}//全部导入 // 名字可以修改import people from ‘./example2.js’——————-我是一条华丽的分界线—————————// example1.js // 部分导出export let name = ‘my name’export let age = ‘my age’export let getName = function(){ return ‘my name’}// 导入部分 // 名字必须和 定义的名字一样。import {name, age} from ‘./example1.js’//有一种特殊情况,即允许你将整个模块当作单一对象进行导入//该模块的所有导出都会作为对象的属性存在import * as example from “./example1.js"console.log(example.name)console.log(example.age)console.log(example.getName())——————-我是一条华丽的分界线—————————// example3.js // 有导出默认, 有且只有一个默认,// 又有部分导出export default const example3 = { birthday : ‘2018 09 20’}export let name = ‘my name’export let age = ‘my age’export let getName = function(){ return ‘my name’}// 导入默认与部分import example3, {name, age} from ‘./example1.js’总结:1.当用 export default people 导出时,就用 import people 导入(不带大括号)2.一个文件里,有且只能有一个 export default。但可以有多个 export。3.当用 export name 时,就用 import { name }导入(记得带上大括号)4.当一个文件里,既有一个 export default people, 又有多个 export name 或者 export age 时,导入就用 import people, { name, age } 5.当一个文件里出现 n 多个 export 导出很多模块,导入时除了一个一个导入,也可以用 import * as example13. Class对于 Class ,小汪用在 react 中较多。13.1基本用法://定义类class FunSum { constructor(x, y) { this.x = x; this.y = y; } sum() { console.log( this.x +this.y’) }}// 使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。let f = new FunSum(10, 20);f.sum() // 3013.2 继承class ColorPoint extends Point { constructor(x, y, color) { super(x, y); // 调用父类的constructor(x, y) this.color = color; } toString() { return this.color + ’ ’ + super.toString(); // 调用父类的toString() }}上面代码中,constructor 方法和 toString 方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的 this 对象。子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super 方法,子类就得不到 this 对象。class Point { / … */ }class ColorPoint extends Point { constructor() { }}let cp = new ColorPoint(); // ReferenceError上面代码中,ColorPoint 继承了父类 Point,但是它的构造函数没有调用 super 方法,导致新建实例时报错。最后总结和写博客的过程就是学习的过程,是一个享受的过程 !!!好了,面试和工作中用到 ES6 精粹几乎都在这了。如果你觉得该文章对你有帮助,欢迎到我的 github star 一下,谢谢。github 地址文章很多内容参考了:ECMAScript 6 标准入门如果你是 JavaScript 语言的初学者,建议先看 《JavaScript 语言入门教程》你以为本文就这么结束了 ? 精彩在后面 !!!对 全栈开发 有兴趣的朋友可以扫下方二维码关注我的公众号我会不定期更新有价值的内容。微信公众号:爱写bugger的阿拉斯加分享 前端开发、后端开发 等相关的技术文章,热点资源,全栈程序员的成长之路。关注公众号并回复 福利 便免费送你视频资源,绝对干货。福利详情请点击: 免费资源分享——Python、Java、Linux、Go、node、vue、react、javaScript ...

September 20, 2018 · 7 min · jiezi

ES6走走看看—由块级作用域引出的一场变革

持续更新的github笔记,链接地址:Front-End-Basics 此篇文章的笔记地址:字符到底发生了什么变化 ES6走走看看系列,特别鸣谢奇舞读书会~块级作用域又称词法作用域,存在于:函数内部(函数作用域)块中(字符 { 和 } 之间的区域)注意:ES6允许块级作用域任意嵌套{{{{{{let text = ‘Hello World!’}}}}}}因为有了块级作用域,然后我们才有继续往下聊的可能。1、 块级声明块级声明是用于声明在指定块的作用域之外无法访问的变量。2、 let声明:用来声明一个块级作用域变量1、 声明的变量具有块级作用域的特性// 例子function getValue (condition) { if (condition) { let value = ‘blue’; return value; } console.log(value) // 报错 value is not defined}getValue()2、 在同一个作用域内不能使用let声明同名的变量// 不管是var,const或者let,新的let声明之前同名的变量,都会报错var count = 30;let count = 40;// 报错 Identifier ‘count’ has already been declared// 函数形参和函数内部的let声明变量重名,报错function test(value) { let value = 3;}test()// 报错 Identifier ‘value’ has already been declared// 在不同的作用域声明的变量重名是没问题的let count = 30;if(true) { let count = 40; // 不同的作用域,不会报错}3、 声明没有预解析,不存在变量提升,有“临时死区”(TDZ)从块的开始到变量声明这段的区域被称为临时死区,ES6明确规定,如果区块中存在let和const命令,则这个区块对这些命令声明的变量从一开始就形成封闭作用域,只要在声明之前就使用这些变量(赋值,引用等等),就会报错。if(true) { console.log(typeof value); // 报错 value is not defined let value = ‘blue’;}注意:TDZ是区域是“块开始”到“变量声明”,下面的例子不报错// typeof 说是相对安全,确实是,永远拿不到想要的结果console.log(typeof value); // 打印 undefined,没有报错if(true) { let value = ‘red’;}3、 const声明:声明常量(如PI),值一旦被设定后不可更改1、 常量声明的值是不可变的注意:const声明的对象不允许修改绑定,但可以修改该对象的属性值。const number = 6;number = 5;// 报错 Assignment to constant variableconst obj = {number: 1};obj.number = 2; // 不报错obj = {number: 3};// 报错 Assignment to constant variable2、 因为常量声明后值就不可更改了,所以声明时必须赋值// 有效的常量const count = 30;// 报错 Missing initializer in const declarationconst name;3、 声明的常量具有块级作用域的特性if(true) { const number = 5;}console.log(number)// 报错 number is not defined4、 在同一个作用域内不能使用const声明同名的变量var message = ‘Hello’;let age = 25;// 这两条语句都会报错const message = ‘Good’;const age = 30;5、 声明没有预解析,不存在变量提升,有“临时死区”(TDZ)总结:一张表格声明方式变量提升作用域是否需要初始值重复定义var是函数级不需要允许let否块级不需要不允许const否块级需要不允许扩展:再提一下变量命名,不管是var、let、const声明的变量名,可以由数字,字母,下划线及美元符号组成,但是不能以数字开头。美元符号可以放到任何一个位置,甚至单独一个美元符号。4、 循环中的块作用域绑定循环中的let声明// 第一个对比// beforefor(var i = 0; i < 5; i++) { // … 省略一些代码}console.log(i) // 5//afterfor(let i = 0; i < 5; i++) { // … 省略一些代码}console.log(i) // 报错 i is not defined// 第二个对比// beforevar funcs = [];for(var i = 0; i < 10; i++) { funcs.push(() => {console.log(i)})}funcs.forEach((ele) => { ele()})// 打印 10次 10// aftervar funcs = [];for(let i = 0; i < 10; i++) { funcs.push(() => {console.log(i)})}funcs.forEach((ele) => { ele()})// 打印 0 1 2 3 4 5 6 7 8 9注意:有一点很重要,let 声明在循环内部的行为是标准中专门定义的,它不一定与 let 不提升特性有关。循环中的const声明// for 循环会报错for (const i = 0; i < 1; i++) { console.log(i)}// 打印 0 ,然后报错 Assignment to constant variable.// for-in 和 for-of 不会报错var object = { a: true, b: true, c: true};for (const key in object) { // 不要在循环体内更改key的值,会报错 console.log(key)}// 打印 a b c注意:const可以应用在 for-in 和 for-of 循环中,是因为每次迭代不会修改已有绑定,而是会创建一个新绑定。5、 块级绑定最佳实践的进化ES6 早期普遍认为默认使用let来替代var,对于写保护的变量使用constES6 使用中普遍默认使用const,只有确实需要改变变量的值时使用let。因为大部分变量的值在初始化后不应再改变,而预料之外的变量值的改变是许多bug的源头。这样就可以在某种程度上实现代码的不可变,从而防止某些错误的发生。6、 全局变量将逐步与顶层对象的属性脱钩顶层对象,在浏览器环境指的是window对象,在Node指的是global对象。为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;var a = 1;window.a // 1另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。上图可见let 声明的变量,并没有在Window对象里,而是一个新的Script对象。扩展:如果需要在浏览器中跨frame或window访问代码,仍然可以用var在全局对象下定义变量。7、 块级函数从ECMAScript 6开始,在严格模式下,块里的函数作用域为这个块。ECMAScript 6之前不建议块级函数在严格模式下使用。‘use strict’;function f() { return 1;}{ function f() { return 2; }}f() === 1; // true// f() === 2 在非严格模式下相等注意:在非严格模式下不要用块级函数,因为在非严格模式下,块中函数的声明表现奇怪,有兼容性风险if (shouldDefineZero) { function zero() { // DANGER: 兼容性风险 console.log(“This is zero.”); }}ECMAScript 6中,如果shouldDefineZero是false,则永远不会定义zero,因为这个块不执行。这是新标准定义的。然而,这里存在历史遗留问题,无论这个块是否执行,一些浏览器会定义zero。在严格模式下,所有支持ECMAScript 6的浏览器以相同的方式处理:只有在shouldDefineZero为true的情况下定义zero,并且作用域只是这个块内。 ...

September 7, 2018 · 2 min · jiezi

Async:简洁优雅的异步之道

前言在异步处理方案中,目前最为简洁优雅的便是async函数(以下简称A函数)。经过必要的分块包装后,A函数能使多个相关的异步操作如同同步操作一样聚合起来,使其相互间的关系更为清晰、过程更为简洁、调试更为方便。它本质是Generator函数的语法糖,通俗的说法是使用G函数进行异步处理的增强版。尝试学习A函数必须有Promise基础,最好还了解Generator函数,有需要的可查看延伸小节。为了直观的感受A函数的魅力,下面使用Promise和A函数进行了相同的异步操作。该异步的目的是获取用户的留言列表,需要分页,分页由后台控制。具体的操作是:先获取到留言的总条数,再更正当前需要显示的页数(每次切换到不同页时,总数目可能会发生变化),最后传递参数并获取到相应的数据。let totalNum = 0; // Total comments number.let curPage = 1; // Current page index.let pageSize = 10; // The number of comment displayed in one page.// 使用A函数的主代码。async function dealWithAsync() { totalNum = await getListCount(); console.log(‘Get count’, totalNum); if (pageSize * (curPage - 1) > totalNum) { curPage = 1; } return getListData();}// 使用Promise的主代码。function dealWithPromise() { return new Promise((resolve, reject) => { getListCount().then(res => { totalNum = res; console.log(‘Get count’, res); if (pageSize * (curPage - 1) > totalNum) { curPage = 1; } return getListData() }).then(resolve).catch(reject); });}// 开始执行dealWithAsync函数。// dealWithAsync().then(res => {// console.log(‘Get Data’, res)// }).catch(err => {// console.log(err);// });// 开始执行dealWithPromise函数。// dealWithPromise().then(res => {// console.log(‘Get Data’, res)// }).catch(err => {// console.log(err);// });function getListCount() { return createPromise(100).catch(() => { throw ‘Get list count error’; });}function getListData() { return createPromise([], { curPage: curPage, pageSize: pageSize, }).catch(() => { throw ‘Get list data error’; });}function createPromise( data, // Reback data params = null, // Request params isSucceed = true, timeout = 1000,) { return new Promise((resolve, reject) => { setTimeout(() => { isSucceed ? resolve(data) : reject(data); }, timeout); });}对比dealWithAsync和dealWithPromise两个简单的函数,能直观的发现:使用A函数,除了有await关键字外,与同步代码无异。而使用Promise则需要根据规则增加很多包裹性的链式操作,产生了太多回调函数,不够简约。另外,这里分开了每个异步操作,并规定好各自成功或失败时传递出来的数据,近乎实际开发。1 登堂1.1 形式A函数也是函数,所以具有普通函数该有的性质。不过形式上有两点不同:一是定义A函数时,function关键字前需要有async关键字(意为异步),表示这是个A函数。二是在A函数内部可以使用await关键字(意为等待),表示会将其后面跟随的结果当成异步操作并等待其完成。以下是它的几种定义方式。// 声明式async function A() {}// 表达式let A = async function () {};// 作为对象属性let o = { A: async function () {}};// 作为对象属性的简写式let o = { async A() {}};// 箭头函数let o = { A: async () => {}};1.2 返回值执行A函数,会固定的返回一个Promise对象。得到该对象后便可监设置成功或失败时的回调函数进行监听。如果函数执行顺利并结束,返回的P对象的状态会从等待转变成成功,并输出return命令的返回结果(没有则为undefined)。如果函数执行途中失败,JS会认为A函数已经完成执行,返回的P对象的状态会从等待转变成失败,并输出错误信息。// 成功执行案例A1().then(res => { console.log(‘执行成功’, res); // 10});async function A1() { let n = 1 * 10; return n;}// 失败执行案例A2().catch(err => { console.log(‘执行失败’, err); // i is not defined.});async function A2() { let n = 1 * i; return n;}1.3 await只有在A函数内部才可以使用await命令,存在于A函数内部的普通函数也不行。引擎会统一将await后面的跟随值视为一个Promise,对于不是Promise对象的值会调用Promise.resolve()进行转化。即便此值为一个Error实例,经过转化后,引擎依然视其为一个成功的Promise,其数据为Error的实例。当函数执行到await命令时,会暂停执行并等待其后的Promise结束。如果该P对象最终成功,则会返回成功的返回值,相当将await xxx替换成返回值。如果该P对象最终失败,且错误没有被捕获,引擎会直接停止执行A函数并将其返回对象的状态更改为失败,输出错误信息。最后,A函数中的return x表达式,相当于return await x的简写。// 成功执行案例A1().then(res => { console.log(‘执行成功’, res); // 约两秒后输出100。});async function A1() { let n1 = await 10; let n2 = await new Promise(resolve => { setTimeout(() => { resolve(10); }, 2000); }); return n1 * n2;}// 失败执行案例A2().catch(err => { console.log(‘执行失败’, err); // 约两秒后输出10。});async function A2() { let n1 = await 10; let n2 = await new Promise((resolve, reject) => { setTimeout(() => { reject(10); }, 2000); }); return n1 * n2;}2 入室2.1 继发与并发对于存在于JS语句(for, while等)的await命令,引擎遇到时也会暂停执行。这意味着可以直接使用循环语句处理多个异步。以下是处理继发的两个例子。A函数处理相继发生的异步尤为简洁,整体上与同步代码无异。// 两个方法A1和A2的行为结果相同,都是每隔一秒输出10,输出三次。async function A1() { let n1 = await createPromise(); console.log(‘N1’, n1); let n2 = await createPromise(); console.log(‘N2’, n2); let n3 = await createPromise(); console.log(‘N3’, n3);}async function A2() { for (let i = 0; i< 3; i++) { let n = await createPromise(); console.log(‘N’ + (i + 1), n); }}function createPromise() { return new Promise(resolve => { setTimeout(() => { resolve(10); }, 1000); });}接下来是处理并发的三个例子。A1函数使用了Promise.all生成一个聚合异步,虽然简单但灵活性降低了,只有都成功和失败两种情况。A3函数相对A2仅仅为了说明应该怎样配合数组的遍历方法使用async函数。重点在A2函数的理解上。A2函数使用了循环语句,实际是继发的获取到各个异步值,但在总体的时间上相当并发(这里需要好好理解一番)。因为一开始创建reqs数组时,就已经开始执行了各个异步,之后虽然是逐一继发获取,但总花费时间与遍历顺序无关,恒等于耗时最多的异步所花费的时间(不考虑遍历、执行等其它的时间消耗)。// 三个方法A1, A2和A3的行为结果相同,都是在约一秒后输出[10, 10, 10]。async function A1() { let res = await Promise.all([createPromise(), createPromise(), createPromise()]); console.log(‘Data’, res);}async function A2() { let res = []; let reqs = [createPromise(), createPromise(), createPromise()]; for (let i = 0; i< reqs.length; i++) { res[i] = await reqs[i]; } console.log(‘Data’, res);}async function A3() { let res = []; let reqs = [9, 9, 9].map(async (item) => { let n = await createPromise(item); return n + 1; }); for (let i = 0; i< reqs.length; i++) { res[i] = await reqs[i]; } console.log(‘Data’, res);}function createPromise(n = 10) { return new Promise(resolve => { setTimeout(() => { resolve(n); }, 1000); });}2.2 错误处理一旦await后面的Promise转变成rejected,整个async函数便会终止。然而很多时候我们不希望因为某个异步操作的失败,就终止整个函数,因此需要进行合理错误处理。注意,这里所说的错误不包括引擎解析或执行的错误,仅仅是状态变为rejected的Promise对象。处理的方式有两种:一是先行包装Promise对象,使其始终返回一个成功的Promise。二是使用try.catch捕获错误。// A1和A2都执行成,且返回值为10。A1().then(console.log);A2().then(console.log);async function A1() { let n; n = await createPromise(true); return n;}async function A2() { let n; try { n = await createPromise(false); } catch (e) { n = e; } return n;}function createPromise(needCatch) { let p = new Promise((resolve, reject) => { reject(10); }); return needCatch ? p.catch(err => err) : p;}2.3 实现原理前言中已经提及,A函数是使用G函数进行异步处理的增强版。既然如此,我们就从其改进的方面入手,来看看其基于G函数的实现原理。A函数相对G函数的改进体现在这几个方面:更好的语义,内置执行器和返回值是Promise。更好的语义。G函数通过在function后使用来标识此为G函数,而A函数则是在function前加上async关键字。在G函数中可以使用yield命令暂停执行和交出执行权,而A函数是使用await来等待异步返回结果。很明显,async和await更为语义化。// G函数function request() { let n = yield createPromise();}// A函数async function request() { let n = await createPromise();}function createPromise() { return new Promise(resolve => { setTimeout(() => { resolve(10); }, 1000); });}内置执行器。调用A函数便会一步步自动执行和等待异步操作,直到结束。如果需要使用G函数来自动执行异步操作,需要为其创建一个自执行器。通过自执行器来自动化G函数的执行,其行为与A函数基本相同。可以说,A函数相对G函数最大改进便是内置了自执行器。// 两者都是每隔一秒钟打印出10,重复两次。// A函数A();async function A() { let n1 = await createPromise(); console.log(n1); let n2 = await createPromise(); console.log(n2);}// G函数,使用自执行器执行。spawn(G);function* G() { let n1 = yield createPromise(); console.log(n1); let n2 = yield createPromise(); console.log(n2);}function spawn(genF) { return new Promise(function(resolve, reject) { const gen = genF(); function step(nextF) { let next; try { next = nextF(); } catch(e) { return reject(e); } if(next.done) { return resolve(next.value); } Promise.resolve(next.value).then(function(v) { step(function() { return gen.next(v); }); }, function(e) { step(function() { return gen.throw(e); }); }); } step(function() { return gen.next(undefined); }); });}function createPromise() { return new Promise(resolve => { setTimeout(() => { resolve(10); }, 1000); });}延伸ES6精华:Promise Generator:JS执行权的真实操作者 ...

September 1, 2018 · 4 min · jiezi