设计模式手册之状态模式

什么是“状态模式”?状态模式:对象行为是基于状态来改变的。内部的状态转化,导致了行为表现形式不同。所以,用户在外面看起来,好像是修改了行为。Webpack4系列教程(17篇) + 设计模式手册(16篇):GitHub地址博客主题推荐:Theme Art Design,“笔记记录+搭建知识体系”的利器。原文地址: 设计模式手册之状态模式2. 优缺点优点封装了转化规则,对于大量分支语句,可以考虑使用状态类进一步封装。每个状态都是确定的,所以对象行为是可控的。缺点状态模式的关键是将事物的状态都封装成单独的类,这个类的各种方法就是“此种状态对应的表现行为”。因此,状态类会增加程序开销。3. 代码实现3.1 ES6 实现在JavaScript中,可以直接用JSON对象来代替状态类。下面代码展示的就是FSM(有限状态机)里面有3种状态:download、pause、deleted。控制状态转化的代码也在其中。DownLoad类就是,常说的Context对象,它的行为会随着状态的改变而改变。const FSM = (() => { let currenState = “download”; return { download: { click: () => { console.log(“暂停下载”); currenState = “pause”; }, del: () => { console.log(“先暂停, 再删除”); } }, pause: { click: () => { console.log(“继续下载”); currenState = “download”; }, del: () => { console.log(“删除任务”); currenState = “deleted”; } }, deleted: { click: () => { console.log(“任务已删除, 请重新开始”); }, del: () => { console.log(“任务已删除”); } }, getState: () => currenState };})();class Download { constructor(fsm) { this.fsm = fsm; } handleClick() { const { fsm } = this; fsm[fsm.getState()].click(); } hanldeDel() { const { fsm } = this; fsm[fsm.getState()].del(); }}// 开始下载let download = new Download(FSM);download.handleClick(); // 暂停下载download.handleClick(); // 继续下载download.hanldeDel(); // 下载中,无法执行删除操作download.handleClick(); // 暂停下载download.hanldeDel(); // 删除任务3.2 Python3 实现python的代码采用的是“面向对象”的编程,没有过度使用函数式的闭包写法(python写起来也不难)。因此,负责状态转化的类,专门拿出来单独封装。其他3个状态类的状态,均由这个状态类来管理。# 负责状态转化class StateTransform: def init(self): self.__state = ‘download’ self.__states = [‘download’, ‘pause’, ‘deleted’] def change(self, to_state): if (not to_state) or (to_state not in self.__states) : raise Exception(‘state is unvalid’) self.__state = to_state def get_state(self): return self.__state# 以下是三个状态类class DownloadState: def init(self, transfomer): self.__state = ‘download’ self.__transfomer = transfomer def click(self): print(‘暂停下载’) self.__transfomer.change(‘pause’) def delete(self): print(‘先暂停, 再删除’) class PauseState: def init(self, transfomer): self.__state = ‘pause’ self.__transfomer = transfomer def click(self): print(‘继续下载’) self.__transfomer.change(‘download’) def delete(self): print(‘删除任务’) self.__transfomer.change(‘deleted’)class DeletedState: def init(self, transfomer): self.__state = ‘deleted’ self.__transfomer = transfomer def click(self): print(‘任务已删除, 请重新开始’) def delete(self): print(‘任务已删除’)# 业务代码class Download: def init(self): self.state_transformer = StateTransform() self.state_map = { ‘download’: DownloadState(self.state_transformer), ‘pause’: PauseState(self.state_transformer), ‘deleted’: DeletedState(self.state_transformer) } def handle_click(self): state = self.state_transformer.get_state() self.state_map[state].click() def handle_del(self): state = self.state_transformer.get_state() self.state_map[state].delete()if name == ‘main’: download = Download() download.handle_click(); # 暂停下载 download.handle_click(); # 继续下载 download.handle_del(); # 下载中,无法执行删除操作 download.handle_click(); # 暂停下载 download.handle_del(); # 删除任务4. 参考23种设计模式全解析菜鸟教程状态模式《JavaScript设计模式与开发实践》

March 11, 2019 · 2 min · jiezi

JS基础——call、bind、apply 和 this

this首先我们分析一下下面代码的执行结果function foo(num) { console.log( “foo: " + num ); this.count++; //记录函数执行次数}foo.count = 0;function test(){ for(let i = 0 ; i < 4; i++){ foo(i) } console.log(‘foo执行次数:’+foo.count)}test()执行结果如下:foo: 0foo: 1foo: 2foo: 3foo执行次数:0由此可见this指向的并不是函数自身。this 是在运行时进行绑定的,它的指向取决于它的调用位置。我们可以通过分析调用栈来找到函数的调用位置。(调用位置为当前正在执行的函数的前一个调用中)。当我们在第一个例子中增加debugger,在浏览器工具中可以清晰的看到函数的调用栈:foo函数的调用位置在test函数中,此时this的指向默认绑定为window,而不是foo对象。我们可以可以通过修改this.count为foo.count来达到目的。绑定this的绑定规则有:默认绑定、隐式绑定、显式绑定、new绑定,上面函数调用中this就是默认绑定。new绑定function foo(a) { this.a = a;} var bar = new foo(2);console.log(bar.a); //2var bar2 = new foo(3);console.log(bar2.a) //3当使用new调用foo函数时,会构造一个新对象并把它绑定到 foo显式绑定我们可以通过call、bind、apply来改变this的指向。call、apply本质上没有区别,都是立即执行,只是第二个参数的传值方式不一样。bind返回要执行的函数,需要的时候再执行。(bind后的函数当使用new执行时,绑定无效)function foo() { console.log(this.a);}var obj = { a:2};var fn = foo.bind( obj );fn() //2new fn() //undefined隐式绑定当函数存在上下文对象时会影响调用位置function foo() { console.log( this.a );}var obj2 = { a: 42, foo: foo };var obj1 = { a: 2, obj2: obj2 };obj1.obj2.foo(); //42箭头函数箭头函数会继承外层函数调用的 this 绑定 ...

March 8, 2019 · 1 min · jiezi

【30秒一个知识点】Adapter

本系列翻译自开源项目 30-seconds-of-code这是一个非常优秀的系列,文章总结了大量的使用es6语法实现的代码模块不是说真的三十秒就能理解,也需要你认真的思考,其中有一些点非常精妙,很值得一读。本文在我的github同步更新,你可以看到当前翻译的全部系列。如果您对本期有不同或者更好的见解,请在下方评论告,喜欢请点个赞,谢谢阅读。ary创建一个可以接收n个参数的函数, 忽略其他额外的参数。调用提供的函数fn,参数最多为n个, 使用 Array.prototype.slice(0,n) 和展开操作符 (…)。const ary = (fn, n) => (…args) => fn(…args.slice(0, n));示例const firstTwoMax = ary(Math.max, 2);[[2, 6, ‘a’], [8, 4, 6], [10]].map(x => firstTwoMax(…x)); // [6, 8, 10]call给定一个key和一组参数,给定一个上下文时调用它们。主要用于合并。使用闭包调用上下文中key对应的值,即带有存储参数的函数。const call = (key, …args) => context => contextkey;示例Promise.resolve([1, 2, 3]) .then(call(‘map’, x => 2 * x)) .then(console.log); // [ 2, 4, 6 ]const map = call.bind(null, ‘map’);Promise.resolve([1, 2, 3]) .then(map(x => 2 * x)) .then(console.log); // [ 2, 4, 6 ]collectInto将一个接收数组参数的函数改变为可变参数的函数。给定一个函数,返回一个闭包,该闭包将所有输入收集到一个数组接受函数中。const collectInto = fn => (…args) => fn(args);示例const Pall = collectInto(Promise.all.bind(Promise));let p1 = Promise.resolve(1);let p2 = Promise.resolve(2);let p3 = new Promise(resolve => setTimeout(resolve, 2000, 3));Pall(p1, p2, p3).then(console.log); // [1, 2, 3] (after about 2 seconds)flipFlip以一个函数作为参数,然后把第一个参数作为最后一个参数。返回一个可变参数的闭包,在应用其他参数前,先把第一个以外的其他参数作为第一个参数。const flip = fn => (first, …rest) => fn(…rest, first);示例let a = { name: ‘John Smith’ };let b = {};const mergeFrom = flip(Object.assign);let mergePerson = mergeFrom.bind(null, a);mergePerson(b); // == bb = {};Object.assign(b, a); // == bover创建一个函数,这个函数可以调用每一个被传入的并且才有参数的函数,然后返回结果。使用 Array.prototype.map() 和 Function.prototype.apply()将每个函数应用给定的参数。const over = (…fns) => (…args) => fns.map(fn => fn.apply(null, args));示例const minMax = over(Math.min, Math.max);minMax(1, 2, 3, 4, 5); // [1,5]overArgs创建一个函数,它可以调用提供的被转换参数的函数。使用Array.prototype.map()将transforms应用于args,并结合扩展运算符(…)将转换后的参数传递给fn。const overArgs = (fn, transforms) => (…args) => fn(…args.map((val, i) => transformsi));示例const square = n => n * n;const double = n => n * 2;const fn = overArgs((x, y) => [x, y], [square, double]);fn(9, 3); // [81, 6]pipeAsyncFunctions为异步函数执行从左到右的函数组合。在扩展操作符(…)中使用Array.prototype.reduce() 来使用Promise.then()执行从左到右的函数组合。这些函数可以返回简单值、Promise的组合,也可以定义为通过await返回的async值。所有函数必须是一元的。const pipeAsyncFunctions = (…fns) => arg => fns.reduce((p, f) => p.then(f), Promise.resolve(arg));示例const sum = pipeAsyncFunctions( x => x + 1, x => new Promise(resolve => setTimeout(() => resolve(x + 2), 1000)), x => x + 3, async x => (await x) + 4);(async() => { console.log(await sum(5)); // 15 (after one second)})();pipeFunctions执行从左到右的函数组合。在展开操作符(…)中使用Array.prototype.reduce()来执行从左到右的函数组合。第一个(最左边的)函数可以接受一个或多个参数; 其余的函数必须是一元的。const pipeFunctions = (…fns) => fns.reduce((f, g) => (…args) => g(f(…args)));示例const add5 = x => x + 5;const multiply = (x, y) => x * y;const multiplyAndAdd5 = pipeFunctions(multiply, add5);multiplyAndAdd5(5, 2); // 15promisify把一个异步函数转换成返回promise的。使用局部套用返回一个函数,该函数返回一个调用原始函数的Promise。使用的…操作符来传入所有参数。const promisify = func => (…args) => new Promise((resolve, reject) => func(…args, (err, result) => (err ? reject(err) : resolve(result))) );示例const delay = promisify((d, cb) => setTimeout(cb, d));delay(2000).then(() => console.log(‘Hi!’)); // Promise resolves after 2srearg创建一个调用提供的函数的函数,该函数的参数按照指定的索引排列。利用 Array.prototype.map() 根据 indexes 和展开操作符 (…) 对参数进行重新排序,将转换后的参数传递给 fn.const rearg = (fn, indexes) => (…args) => fn(…indexes.map(i => args[i]));示例var rearged = rearg( function(a, b, c) { return [a, b, c]; }, [2, 0, 1]);rearged(‘b’, ‘c’, ‘a’); // [‘a’, ‘b’, ‘c’]spreadOver接受一个可变参数函数并返回一个闭包,该闭包接受一个参数数组以映射到函数的输入。使用闭包和扩展操作符(…)将参数数组映射到函数的输入。const spreadOver = fn => argsArr => fn(…argsArr);示例const arrayMax = spreadOver(Math.max);arrayMax([1, 2, 3]); // 3unary创建一个最多接受一个参数的函数,忽略任何其他参数。只把第一个参数传递给要调用的函数fn。const unary = fn => val => fn(val);示例[‘6’, ‘8’, ‘10’].map(unary(parseInt)); // [6, 8, 10]推荐阅读【React深入】setState的执行机制【React深入】React事件机制 ...

March 6, 2019 · 2 min · jiezi

es6 Set的几种使用场景

// 数组去重 let arr = [1, 1, 2, 3]; let unique = [… new Set(arr)]; let a = new Set([1, 2, 3]); let b = new Set([4, 3, 2]); // 并集 let union = […new Set([…a, …b])]; // 交集 let intersect = […new Set([…a].filter(x => b.has(x)))]; // 差集 let difference = Array.from(new Set([…a].filter(x => !b.has(x))));

February 26, 2019 · 1 min · jiezi

10个最佳ES6特性

原文链接译者按: 人生苦短,我用ES6。原文: Top 10 ES6 Features Every Busy JavaScript Developer Must Know译者: Fundebug为了保证可读性,本文采用意译而非直译,并且对源代码进行了大量修改。另外,本文版权归原作者所有,翻译仅用于学习。小编推荐:Fundebug专注于JavaScript、微信小程序、微信小游戏,Node.js和Java线上bug实时监控。真的是一个很好用的bug监控服务,众多大佬公司都在使用。ES6,正式名称是ECMAScript2015,但是ES6这个名称更加简洁。ES6已经不再是JavaScript最新的标准,但是它已经广泛用于编程实践中。如果你还没用过ES6,现在还不算太晚…下面是10个ES6最佳特性,排名不分先后:函数参数默认值模板字符串多行字符串解构赋值对象属性简写箭头函数PromiseLet与Const类模块化1. 函数参数默认值不使用ES6为函数的参数设置默认值:function foo(height, color){ var height = height || 50; var color = color || ‘red’; //…}这样写一般没问题,但是,当参数的布尔值为false时,是会出事情的!比如,我们这样调用foo函数:foo(0, “”, “")因为0的布尔值为false,这样height的取值将是50。同理color的取值为‘red’。使用ES6function foo(height = 50, color = ‘red’){ // …}2. 模板字符串不使用ES6使用+号将变量拼接为字符串:var name = ‘Your name is ’ + first + ’ ’ + last + ‘.‘使用ES6将变量放在大括号之中:var name = Your name is ${first} ${last}.ES6的写法更加简洁、直观。3. 多行字符串不使用ES6使用“nt”将多行字符串拼接起来:var roadPoem = ‘Then took the other, as just as fair,\n\t’ + ‘And having perhaps the better claim\n\t’ + ‘Because it was grassy and wanted wear,\n\t’ + ‘Though as for that the passing there\n\t’ + ‘Had worn them really about the same,\n\t’使用ES6将多行字符串放在反引号之间就好了:var roadPoem = Then took the other, as just as fair, And having perhaps the better claim Because it was grassy and wanted wear, Though as for that the passing there Had worn them really about the same,4. 解构赋值不使用ES6当需要获取某个对象的属性值时,需要单独获取:var data = $(‘body’).data(); // data有house和mouse属性var house = data.house;var mouse = data.mouse;使用ES6一次性获取对象的子属性:var { house, mouse} = $(‘body’).data()对于数组也是一样的:var [col1, col2] = $(’.column’);5. 对象属性简写不使用ES6对象中必须包含属性和值,显得非常多余:var bar = ‘bar’;var foo = function (){ // …}var baz = { bar: bar, foo: foo};使用ES6对象中直接写变量,非常简单:var bar = ‘bar’;var foo = function (){ // …}var baz = { bar, foo };6. 箭头函数不使用ES6普通函数体内的this,指向调用时所在的对象。function foo() { console.log(this.id);}var id = 1;foo(); // 输出1foo.call({ id: 2 }); // 输出2使用ES6箭头函数体内的this,就是定义时所在的对象,而不是调用时所在的对象。var foo = () => { console.log(this.id);}var id = 1;foo(); // 输出1foo.call({ id: 2 }); // 输出17. Promise不使用ES6嵌套两个setTimeout回调函数:setTimeout(function(){ console.log(‘Hello’); // 1秒后输出"Hello” setTimeout(function() { console.log(‘Fundebug’); // 2秒后输出"Fundebug" }, 1000);}, 1000);使用ES6使用两个then是异步编程串行化,避免了回调地狱:var wait1000 = new Promise(function(resolve, reject){ setTimeout(resolve, 1000);});wait1000 .then(function() { console.log(“Hello”); // 1秒后输出"Hello" return wait1000; }) .then(function() { console.log(“Fundebug”); // 2秒后输出"Fundebug" });8. Let与Const使用Varvar定义的变量未函数级作用域:{ var a = 10;}console.log(a); // 输出10使用let与constlet定义的变量为块级作用域,因此会报错:(如果你希望实时监控JavaScript应用的错误,欢迎免费使用Fundebug){ let a = 10;}console.log(a); // 报错“ReferenceError: a is not defined”const与let一样,也是块级作用域。9. 类不使用ES6使用构造函数创建对象:function Point(x, y){ this.x = x; this.y = y; this.add = function() { return this.x + this.y; };}var p = new Point(1, 2);console.log(p.add()); // 输出3使用ES6使用Class定义类,更加规范,且你能够继承:class Point{ constructor(x, y) { this.x = x; this.y = y; } add() { return this.x + this.y; }}var p = new Point(1, 2);console.log(p.add()); // 输出310. 模块化JavaScript一直没有官方的模块化解决方案,开发者在实践中主要采用CommonJS和AMD规范。而ES6制定了模块(Module)功能。不使用ES6Node.js采用CommenJS规范实现了模块化,而前端也可以采用,只是在部署时需要使用Browserify等工具打包。这里不妨介绍一下CommenJS规范。module.js中使用module.exports导出port变量和getAccounts函数:module.exports = { port: 3000, getAccounts: function() { … }}main.js中使用require导入module.js:var service = require(‘module.js’)console.log(service.port) // 输出3000使用ES6ES6中使用export与import关键词实现模块化。module.js中使用export导出port变量和getAccounts函数:export var port = 3000export function getAccounts(url) { …}main.js中使用import导入module.js,可以指定需要导入的变量:import {port, getAccounts} from ‘module’console.log(port) // 输出3000也可以将全部变量导入:import * as service from ‘module’console.log(service.port) // 3000 ...

February 25, 2019 · 2 min · jiezi

JS module的导出和导入

最近看了些Vue框架写的程序,发现自己的前端知识还停留在几年以前,发现现在Javascript程序里有各种各样的对module的导入和到处,导入乍一看跟python的语法挺像的无非就是把from和import这两个关键词的使用颠倒了一下顺序。仔细看下来还是和python挺不一样的import模块的前提是模块有导出,并且还分默认导出和命名导出,有些麻烦。所以今天这篇文章就把所有的export形式和相应的import使用汇总一下。ES6在语言标准的层面上,实现了模块功能,成为浏览器和服务器通用的模块解决方案,完全可以取代 CommonJS 和 AMD 规范,基本特点如下:每一个模块只加载一次, 每一个JS只执行一次, 如果下次再去加载同目录下同文件,直接从内存中读取;每一个模块内声明的变量都是局部变量, 不会污染全局作用域;模块内部的变量或者函数可以通过export导出;一个模块可以导入别的模块2.模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能;3.一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量;var year = ‘2018’;var month = ‘Febuary’;export {year, month};export 导出模块export语法声明用于导出函数、对象、指定文件(或模块)的原始值。 有两种模块导出方式:命名式导出(名称导出)和默认导出(定义式导出),命名式导出每个模块可以有多个,而默认导出每个模块仅一个 。命名式导出模块可以通过export前缀关键词声明导出对象,导出对象可以是多个。这些导出对象用名称进行区分,称之为命名式导出export { func }; // 导出一个已定义的函数funcexport const foo = Math.sqrt(100); // 导出一个常量我们可以使用和from关键字来实现的模块的继承:export * from ‘base_module’;模块导出时,可以指定模块的导出成员。导出成员可以认为是类中的公有成员,而非导出成员可以认为是类中的私有成员:var name = ‘Kevin的居酒屋’;var domain = ‘http://coffee.toast.com’; export {name, domain}; // 相当于导出{name:name,domain:domain}模块导出时,我们可以使用as关键字对导出成员进行重命名,上面的导出可以这样写:export {name as siteName, domain}注意一下语法错误:export 1; var a = 100;export a;export在导出接口的时候,必须与模块内部的变量具有一一对应的关系。直接导出1没有任何意义,也不可能在import的时候有一个变量与之对应export a虽然看上去成立,但是a的值是一个数字,根本无法完成解构,因此必须写成export {a}的形式。即使a被赋值为一个函数,也是不建议使用上面的形式导出的因为大部分风格都建议,模块中最好在末尾用一个export导出所有的接口,就像上面那些例子一样。默认导出默认导出也被称做定义式导出。命名式导出可以导出多个值,但在import引用时,也要使用相同的名称来引用相应的值。默认导出只有导出一个单一值,这个输出可以是一个函数、类或其它类型的值,这样在模块import导入时也会更 容易引用。export default function() {}; // 导出一个函数export default class(){}; // 导出一个类默认导出可以理解为另一种形式的命名导出,默认导出可以认为是使用了default名称的命名导出。下面两种导出方式是等价的:const D = 123; export default D;export { D as default };使用名称导出一个模块时:// “my-module.js” 模块function cube(x) { return x * x * x;}const foo = Math.PI + Math.SQRT2;export { cube, foo };在另一个模块(js文件)中,我们可以像下面这样引用:import { cube, foo } from ‘my-module’;console.log(cube(3));console.log(foo);使用默认导出一个模块时:// “my-module.js"模块export default function (x) {return x * x * x;}在另一个模块中,我们可以像下面这样引用,相对名称导出来说使用更为简单:import cube from ‘my-module’;console.log(cube(3)); // 27import导入模块import语法声明用于从已导出的模块、脚本中导入函数、对象、指定文件(或模块)的原始值。import模块导入与export模块导出功能相对应,也存在两种模块导入方式:命名式导入(名称导入)和默认导入(定义式导入)。注意事项: import必须放在文件的最开始,且前面不允许有其他逻辑代码,这和其他所有编程语言的导入风格一致。命名导入我们可以通过指定名称将导入成员插入到当作用域中。可以导入单个成员或多个成员:注意,花括号里面的变量与export后面的变量一一对应import {myMember} from “my-module”;import {foo, bar} from “my-module”;通过符号,我们可以导入模块中的全部属性和方法。当导入模块全部导出内容时,就是将导出模块(’my-module.js’)所有的导出绑定内容,插入到当前模块(’myModule’)的作用域中:import * as myModule from “my-module”;默认导入在模块导出时,可能会存在默认导出。同样的,在导入时可以使用import指令导入这些默认值。直接导入默认值:import defaultName from “my-module”;import myDefault, {foo, bar} from “my-module”; // 指定成员导入和默认导入default关键字// my-module.jsexport default function() {} // 等效于:function func() {};export {func as default};在import的时候,可以这样用:import a from ‘./my-module’; // 等效于,或者说就是下面这种写法的简写import {default as a} from ‘./my-module’;这个语法糖的好处就是import的时候,可以省去{}。简单的说,如果import的时候,你发现某个变量没有花括号括起来(没有*号),那么你在脑海中应该把它还原成有花括号的{default as …}语法,所以import $,{each,map} from ‘jquery’;import后面第一个$是{default as $}的替代写法。 ...

February 24, 2019 · 1 min · jiezi

es6声明类实现继承

class声明一个animal类(对象):class Animal{ constructor(){//这个constructor方法内定义的方法和属性是实例化对象自己的,不共享;construstor外定义的方法和属性是所有实例对象(共享)可以调用的 this.type = ‘animal’ //this关键字代表Animal对象的实例对象 } says(say){ console.log(this.type+’ says ’ +say); }}let animal = new Animal();animal.says(‘hello’);//控制台输出‘animal says hello’这里声明一个Cat类,来继承Animal类的属性和方法class Cat extends Animal(){ constructor(){ super();//super关键字,用来指定父类的实例对象 this.type = ‘cat’; }} let cat = new Cat();cat.says(‘hello’);//输出‘cat says hello’

February 21, 2019 · 1 min · jiezi

求数组的交集,并集,还有差集

最近在看阮一峰老师的《ES6 入门》在看到Set数据结构实现数组的交集,并集还有差集,但是阮一峰老师实现差集貌似有点问题,特地来做下笔记:const a = {fn: 1};const set = new Set([1,1,2,2,3,4,5,5,5,a,‘a’]);const b = new Set([6,7,8,9,5,4,3,‘a’,‘v’]);// 并集const union = new Set([…set, …b]);// 交集const intersect = new Set([…set].filter(x => b.has(x)));// 差集const difference = new Set([union].filter(x => (!set.has(x) || !b.has(x))));最后这条代码才能求到正确的差集

February 21, 2019 · 1 min · jiezi

ES6的开发环境搭建

古语有云:“君子生非异也,善假于物;工欲善其事,必先利其器。” 由于有些低版本的浏览器还是不支持ES6语法,学习ES6,首先要学会搭建一个基本的ES6开发环境,利用工具,把ES6的语法转变成ES5的语法。1、使用Babel把ES6编译成ES51.1 建立工程目录先建立一个项目的工程目录,并在目录下边建立两个文件夹:dist 和 src1.2 初始化项目在安装Babel之前,需使用npm init先初始化我们的项目。通过cmd打开命令行工具,进入项目目录,输入下边的命令:npm init -y命令执行完成后,会在项目根目录下生产package.json文件。1.3 全局安装Babel-clinpm install -g babel-cli1.4 本地安装转码规则npm install –save-dev babel-preset-es2015 babel-cli安装完成后,我们可以看一下我们的package.json文件,已经多了devDependencies选项。1.5 新建.babelrc在根目录下新建.babelrc文件,.babelrc是Babel的配置文件。该文件是用来设置转码规则和插件的,基本格式如下:{ “presets”:[“es2015”], “plugins”:[]}1.6 babel基本用法# 转码结果输出到标准输出$ babel example.js# 转码结果写入一个文件 (–out-file 或 -o 参数指定输出文件)$ babel example.js –out-file compiled.js或者$ babel example.js -o compiled.js# 整个目录转码 (–out-dir 或 -d 参数指定输出目录)$ babel src –out-dir lib或者$ babel src -d lib# -s 参数生成source map文件$ babel src -d lib -s在src目录下,新建index.js文件,使用ES6中的 let声明和字符串模板let name = ‘Bread and Dream’;let greeting = hello ${name};在命令行输入babel src/index.js -o dist/index.js这时dist目录会生成 index.js 文件,输出结果为:‘use strict’;var name = ‘Bread and Dream’;var greeting = ‘hello ’ + name;项目文件最终结构1.7 简化转化命令:使用babel命令行,一大长串,很容易忘记,所以,我们可以进行改造,打开package.json文件,添加以下代码{ “scripts”: { “build”: “babel src/index.js -o dist/index.js” },}修改好后,以后我们就可以在命令行输入 npm run build 进行转换了。(注:build是自定义的,为了语义化命名为build,当然也可以命名成其他的,例如 compile)2、webpack + Babel 构建 ES6 开发平台2.1 搭建 webpack 基本文件目录首先全局安装webpack(这里使用 webpack@3.12.0 版本)npm install -g webpack@3新建一个文件夹,命名为 webpack_demo;命令行切换到 webpack_demo的文件目录下,执行下面的命令, 生成默认 package.json 配置文件npm init -y利用 npm 安装 webpacknpm i –save-dev webpack@3新建 webpack.config.js(用于配置 webpack 的运行方式),最简单的配置如下:module.exports= {/* webpack 入口起点 /entry:’./index.js’,/ webpack 输出 /output:{ // 输出 文件名 filename:’./test.js’},}2.2 安装babel相关安装 babel-core 包npm i –save-dev babel-core安装babel-preset-env 和 babel-preset-stage-0 包npm i –save-dev babel-preset-env babel-preset-stage-0 babel-preset-env 是一个主流的 Babel 插件数组;Stage-X 是实验阶段的 Presets2.3 将 webpack 和 Babel 结合在一起需要在两者之间建立一条纽带,而通过 webpack 的 loaders 就可以生成这条纽带,因此要修改 webpack.config.js 配置文件:/ webpack loaders 配置 / module:{ rules:[ { test:/.js$/, use:{ loader:‘babel-loader’, }, } ],},就是针对以 .js 结尾的文件使用 babel-loader。由于项目中还不存在 babel-loader,所以要先安装该模块:npm i –save-dev babel-loader通过使用 loader 语法配置 Babel 的 presets使用 webpack 提供的方法,具体在 webpack.config.js 的 module.rules.use.options 中配置 / webpack loaders 配置 */ module:{ rules:[ { test:/.js$/, use:{ loader:‘babel-loader’, options:{ presets:[ ‘babel-preset-env’, ‘babel-preset-stage-0’ ] } }, } ], },最终的项目结构为:在当前目录下执行 webpack 命令webpack输入文件 index.js 中的ES6代码已经被转换成输出文件 test.js 中的 ES5 代码了:3、Traceur转码器Google公司的Traceur转码器,也可以将ES6代码转为ES5代码。3.1 直接在页面中使用:<!– 加载Traceur编译器 –><script src=“http://google.github.io/traceur-compiler/bin/traceur.js" type=“text/javascript”></script><!– 打开实验选项,否则有些特性可能编译不成功 –><script> traceur.options.experimental = true;</script>写ES6代码,用:<script type=“module”> // ES6代码</script>注意:script标签的type属性的值是module,而不是text/javascript。这是Traceur编译器识别ES6代码的标识。3.2 Traceur的命令行转换方法:首先需要用npm安装。$ npm install -g traceur直接运行ES6代码,以index.js为例$ traceur index.js将ES6输出为ES5脚本$ traceur –script index.js –out es5.js为了防止有些特性编译不成功,最好加上–experimental选项。$ traceur –script index.js –out es5.js –experimental4、直接在线编译Babel提供一个REPL在线编译器,可以在线将ES6代码转为ES5代码。转换后的代码,可以直接作为ES5代码插入网页运行。5、总结使用babel搭建环境的顺序:创建项目创建两个文件夹src和dist使用npm init 初始化项目生成package.json(项目信息文件)使用 npm install -g babel-cli 全局安装使用 npm install –save-dev babel-preset-es2015 babel-cli本地安装创建.babelrc文件(babel转换的配置文件)在src下创建js,编写ES6语法babel src/index.js -o dist/index.js转码生成ES5语法babel 本质就是一个 JavaScript 编译器,通过:将 JavaScript 源代码解析成抽象语法树(AST);将源代码的 AST 结果一系列转换生成目标代码的 AST;将目标代码的 AST 转换成 JavaScript 代码。当然,感兴趣的小伙伴可以深入研究下babel及其插件的源码,了解其运行机制,以便更全面的掌握ES6转ES5的相关原理、机制。 ...

February 20, 2019 · 2 min · jiezi

ES6 Proxy的学习与理解

问题前一段时间在字节跳动时聊到了Proxy。起因是问道Vue中数据绑定的实现,回答通过设置setter和getter实现,问这样有什么缺点,答在对对象的属性的监控方面存在瑕疵,例如通过直接设置数组下标进行赋值,或者对对象直接进行修改,是无法观察到的,必须使用Vue.set添加,或者使用Array.prototype.push等方法。面试官介绍说在Vue3中已经通过Proxy解决了这个问题。Proxy是ES6中添加的内置对象,和Reflect配合功能十分强大。正好今天看到一个问题。理解根据MDN的文档,Proxy是对原对象的包装。可以包装的内容包括一系列get、set等,值得注意的是getPrototypeOf同样是一种可以拦截的操作。同时,对于未定义的操作保持原结果。在instanceof的页面,可以看到如下示例function C() {}function D() {}var o = new C();// true, because: Object.getPrototypeOf(o) === C.prototypeo instanceof C;那么,在上面那个问题中,既然未定义proxy的getPrototypeOf,那它就该与原对象保持一致。使用以下代码进行验证:Object.getPrototypeOf(proxy) === Array.prototype //true进一步思考那么,是不是对于一切行为,在不做任何拦截的情况下,就能保证与目标对象的行为完全一致呢?很显然,这是不可能的。例如a = {}b = new Proxy(a, {})console.log(a === b) //false以及this的指向问题(案例来自阮一峰文章)const target = { m: function () { console.log(this === proxy); }};const handler = {};const proxy = new Proxy(target, handler);target.m() // falseproxy.m() // true虽然大部分情况下这应该不会成为大的障碍,但遇到错误的时候可以从这里入手寻找问题。

February 16, 2019 · 1 min · jiezi

Understanding ES6 -- 深入理解ES6书籍

understanding es6 – Nicholas C. Zakas块级绑定function拓展对象功能解构symbolSets and MapsIterators and Generatorsclass改进的数组promise代理和反射 – Proxy&Reflectionmodules附录A – 小的改变附录B – undestanding es7

February 16, 2019 · 1 min · jiezi

工作中常用到的ES6语法

一、let和const在JavaScript中咱们以前主要用关键var来定义变量,ES6之后,新增了定义变量的两个关键字,分别是let和const。 对于变量来说,在ES5中var定义的变量会提升到作用域中所有的函数与语句前面,而ES6中let定义的变量则不会,let声明的变量会在其相应的代码块中建立一个暂时性死区,直至变量被声明。 let和const都能够声明块级作用域,用法和var是类似的,let的特点是不会变量提升,而是被锁在当前块中。一个非常简单的例子:function test(){if(true){console.log(a)//TDZ,俗称临时死区,用来描述变量不提升的现象let a =1}}test()// a is not definedfunction test(){if(true){let a =1}console.log(a)}test()// a is not defined唯一正确的使用方法:先声明,再访问。function test(){if(true){let a =1console.log(a)}}test()// 1const 声明常量,一旦声明,不可更改,而且常量必须初始化赋值。 const虽然是常量,不允许修改默认赋值,但如果定义的是对象Object,那么可以修改对象内部的属性值。const type ={a:1}type.a =2//没有直接修改type的值,而是修改type.a的属性值,这是允许的。console.log(type)// {a: 2}const和let的异同点相同点:const和let都是在当前块内有效,执行到块外会被销毁,也不存在变量提升(TDZ),不能重复声明。 不同点:const不能再赋值,let声明的变量可以重复赋值。 const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。块级作用域的使用场景除了上面提到的常用声明方式,我们还可以在循环中使用,最出名的一道面试题:循环中定时器闭包的考题 在for循环中使用var声明的循环变量,会跳出循环体污染当前的函数。for(var i =0; i <5; i++){setTimeout(()=>{console.log(i)//5, 5, 5, 5, 5},0)}console.log(i)//5 i跳出循环体污染外部函数//将var改成let之后for(let i =0; i <5; i++){setTimeout(()=>{console.log(i)// 0,1,2,3,4},0)}console.log(i)//i is not defined i无法污染外部函数在实际开发中,我们选择使用var、let还是const,取决于我们的变量是不是需要更新,通常我们希望变量保证不被恶意修改,而使用大量的const。使用const声明,声明一个对象的时候,也推荐使用const,当你需要修改声明的变量值时,使用let,var能用的场景都可以使用let替代。symbolES6 以前,我们知道5种基本数据类型分别是Undefined,Null,Boolean,Number以及String,然后加上一种引用类型Object构成了JavaScript中所有的数据类型,但是ES6出来之后,新增了一种数据类型,名叫symbol,像它的名字表露的一样,意味着独一无二,意思是每个 Symbol类型都是独一无二的,不与其它 Symbol 重复。 可以通过调用 Symbol() 方法将创建一个新的 Symbol 类型的值,这个值独一无二,不与任何值相等。var mySymbol=Symbol();console.log(typeof mySymbol)//“symbol"二、字符串ES6字符串新增的方法UTF-16码位:ES6强制使用UTF-16字符串编码。关于UTF-16的解释请自行百度了解。codePointAt():该方法支持UTF-16,接受编码单元的位置而非字符串位置作为参数,返回与字符串中给定位置对应的码位,即一个整数值。String.fromCodePoiont():作用与codePointAt相反,检索字符串中某个字符的码位,也可以根据指定的码位生成一个字符。normalize():提供Unicode的标准形式,接受一个可选的字符串参数,指明应用某种Unicode标准形式。在ES6中,新增了3个新方法。每个方法都接收2个参数,需要检测的子字符串,以及开始匹配的索引位置。模板字符串字符串是JavaScript中基本类型之一,应该算是除了对象之外是使用最为频繁的类型吧,字符串中包含了例如substr,replace,indexOf,slice等等诸多方法,ES6引入了模板字符串的特性,用反引号来表示,可以表示多行字符串以及做到文本插值(利用模板占位符)。// 以前的多行字符串我们这么写:console.log(“hello world 1n\hello cala”);// “hello world// hello cala”//有了模板字符串之后console.log(hello worldstring text line 2);// “hello world// hello cala"可以用${}来表示模板占位符,可以将你已经定义好的变量传进括弧中,例如:var name=“cala”;var age=22;console.log(hello,I’am ${name},my age is ${age})//hello,I’am cala,my age is 22includes(str, index):如果在字符串中检测到指定文本,返回true,否则false。let t =‘abcdefg’if(t.includes(‘cde’)){console.log(2)}//truestartsWith(str, index):如果在字符串起始部分检测到指定文本,返回true,否则返回false。let t =‘abcdefg’if(t.startsWith(‘ab’)){console.log(2)}//trueendsWith(str, index):如果在字符串的结束部分检测到指定文本,返回true,否则返回false。let t =‘abcdefg’if(t.endsWith(‘fg’)){console.log(2)}//true如果你只是需要匹配字符串中是否包含某子字符串,那么推荐使用新增的方法,如果需要找到匹配字符串的位置,使用indexOf()。三、函数函数的默认参数 在ES5中,我们给函数传参数,然后在函数体内设置默认值,如下面这种方式。function a(num, callback){num = num ||6callback = callback ||function(data){console.log(‘ES5: ‘, data)}callback(num * num)}a()//ES5: 36,不传参输出默认值//你还可以这样使用callbacka(10,function(data){console.log(data *10)// 1000, 传参输出新数值})在ES6中,我们使用新的默认值写法function a(num =6, callback =function(data){console.log(‘ES6: ‘, data)}){callback(num * num)}a()//ES6: 36, 不传参输出默认值a(10,function(data){console.log(data *10)// 1000,传参输出新数值})四、箭头函数(=>)(箭头函数比较重要,现在简单提一下,迟一点有空专门写一篇箭头函数的文章。)const arr =[5,10]const s = arr.reduce((sum, item)=> sum + item)console.log(s)// 15箭头函数中this的使用跟普通函数也不一样,在JavaScript的普通函数中,都会有一个自己的this值,主要分为: 普通函数: 1、函数作为全局函数被调用时,this指向全局对象 2、函数作为对象中的方法被调用时,this指向该对象 3、函数作为构造函数的时候,this指向构造函数new出来的新对象 4、还可以通过call,apply,bind改变this的指向 箭头函数:1、箭头函数没有this,函数内部的this来自于父级最近的非箭头函数,并且不能改变this的指向。 2、箭头函数没有super 3、箭头函数没有arguments 4、箭头函数没有new.target绑定。 5、不能使用new 6、没有原型 7、不支持重复的命名参数。箭头函数的简单理解1、箭头函数的左边表示输入的参数,右边表示输出的结果。const s = a => aconsole.log(s(2))// 22、在箭头函数中,this属于词法作用域,直接由上下文确定,对于普通函数中指向不定的this,箭头函数中处理this无疑更加简单,如下://ES5普通函数functionMan(){this.age=22;returnfunction(){this.age+1;}}var cala=newMan();console.log(cala())//undefined//ES6箭头函数functionMan(){this.age=22;return()=>this.age+1;}var cala=newMan();console.log(cala())//233、箭头函数中没有arguments(我们可以用rest参数替代),也没有原型,也不能使用new 关键字,例如://没有argumentsvar foo=(a,b)=>{return arguments[0]arguments[1]}console.log(foo(3,5))//arguments is not defined//没有原型varObj=()=>{};console.log(Obj.prototype);// undefined//不能使用new 关键字varObj=()=>{“hello world”};var o =newObj();// TypeError: Obj is not a constructor4、箭头函数给数组排序const arr =[10,50,30,40,20]const s = arr.sort((a, b)=> a - b)console.log(s)// [10,20,30,40,50]尾调用优化 尾调用是指在函数return的时候调用一个新的函数,由于尾调用的实现需要存储到内存中,在一个循环体中,如果存在函数的尾调用,你的内存可能爆满或溢出。ES6中,引擎会帮你做好尾调用的优化工作,你不需要自己优化,但需要满足下面3个要求: 1、函数不是闭包 2、尾调用是函数最后一条语句 3、尾调用结果作为函数返回尾调用实际用途——递归函数优化在ES5时代,我们不推荐使用递归,因为递归会影响性能。 但是有了尾调用优化之后,递归函数的性能有了提升。//新型尾优化写法"use strict”;function a(n, p =1){if(n <=1){return1 p}let s = n * preturn a(n -1, s)}//求 1 x 2 x 3的阶乘let sum = a(3)console.log(sum)// 6五、ES6对象新增方法Object.assign()Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。 Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。该方法使用源对象的[[Get]]和目标对象的[[Set]],所以它会调用相关 getter 和 setter。因此,它分配属性,而不仅仅是复制或定义新的属性。如果合并源包含getter,这可能使其不适合将新属性合并到原型中。为了将属性定义(包括其可枚举性)复制到原型,应使用Object.getOwnPropertyDescriptor()和Object.defineProperty() 。 String类型和 Symbol 类型的属性都会被拷贝。 合并对象var o1 ={ a:1};var o2 ={ b:2};var o3 ={ c:3};var obj =Object.assign(o1, o2, o3);console.log(obj);// { a: 1, b: 2, c: 3 }console.log(o1);// { a: 1, b: 2, c: 3 }, 注意目标对象自身也会改变。合并具有相同属性的对象var o1 ={ a:1, b:1, c:1};var o2 ={ b:2, c:2};var o3 ={ c:3};var obj =Object.assign({}, o1, o2, o3);console.log(obj);// { a: 1, b: 2, c: 3 }六、Map和SetMap和Set都叫做集合,但是他们也有所不同。Set常被用来检查对象中是否存在某个键名,Map集合常被用来获取已存的信息。 Set是有序列表,含有相互独立的非重复值。Array和Set对比都是一个存储多值的容器,两者可以互相转换,但是在使用场景上有区别。如下: Array的indexOf方法比Set的has方法效率低下 Set不含有重复值(可以利用这个特性实现对一个数组的去重) Set通过delete方法删除某个值,而Array只能通过splice。两者的使用方便程度前者更优 Array的很多新方法map、filter、some、every等是Set没有的(但是通过两者可以互相转换来使用) Object和Map对比Object是字符串-值,Map是值-值 Object键为string类型,Map的键是任意类型 手动计算Object尺寸,Map.size可以获取尺寸 Map的排序是插入顺序 Object有原型,所以映射中有一些缺省的键。可以理解为Map=Object.create(null)Set操作集合letset=newSet()// Set转化为数组let arr =Array.from(set)let arr =[…set]// 实例属性(继承自Set)set.constructor ===Setset.size// 操作方法set.add(1)// 添加一个值set.delete(1)//删除一个值set.has(1)//判断是否有这个值(Array中的indexOf)set.clear()//清除所有值// 获取用于遍历的成员方法(Set的遍历顺序就是插入顺序)set.keys()// 返回键名的遍历器set.values()// 返回键值得遍历器set.entries()// 返回键值对的遍历器set.forEach()// 循环遍历每个值(和Array的方法一致)for(let key of set.keys()){}for(let val of set.values()){}for(let entry of set.entries()){}// 使用数组方法来处理set值set=newSet(arr)set=newSet([…set].map((x)=> x = x 2))set=newSet([…set].filter((x)=> x >2))Map的方法集合let map =newMap()// 实例属性(继承自Map)map.constructor ===Mapmap.size// 操作方法map.set(1,2)map.get(1)map.delete(1)map.has(1)map.clear()// 遍历方法map.keys()map.values()map.entries()map.forEach()// Map和数组的转换map =newMap([[‘key’,‘val’],[2,1]])// 要求双成员数组let arr =[…map]// 值得注意的是Map的键是跟内存绑定的map.set([1],’s’)map.get([1])let arr =[1]let arr1 =[1]map.set(arr,’s’)map.get(arr)map.set(arr1,’s’)map.get(arr1)七、迭代器(Iterator)1、entries() 返回迭代器:返回键值对//数组const arr =[‘a’,‘b’,‘c’];for(let v of arr.entries()){console.log(v)}// [0, ‘a’] [1, ‘b’] [2, ‘c’]//Setconst arr =newSet([‘a’,‘b’,‘c’]);for(let v of arr.entries()){console.log(v)}// [‘a’, ‘a’] [‘b’, ‘b’] [‘c’, ‘c’]//Mapconst arr =newMap();arr.set(‘a’,‘a’);arr.set(‘b’,‘b’);for(let v of arr.entries()){console.log(v)}// [‘a’, ‘a’] [‘b’, ‘b’]2、values() 返回迭代器:返回键值对的value//数组const arr =[‘a’,‘b’,‘c’];for(let v of arr.values()){console.log(v)}//‘a’ ‘b’ ‘c’//Setconst arr =newSet([‘a’,‘b’,‘c’]);for(let v of arr.values()){console.log(v)}// ‘a’ ‘b’ ‘c’//Mapconst arr =newMap();arr.set(‘a’,‘a’);arr.set(‘b’,‘b’);for(let v of arr.values()){console.log(v)}// ‘a’ ‘b'3、keys() 返回迭代器:返回键值对的key//数组const arr =[‘a’,‘b’,‘c’];for(let v of arr.keys()){console.log(v)}// 0 1 2//Setconst arr =newSet([‘a’,‘b’,‘c’]);for(let v of arr.keys()){console.log(v)}// ‘a’ ‘b’ ‘c’//Mapconst arr =newMap();arr.set(‘a’,‘a’);arr.set(‘b’,‘b’);for(let v of arr.keys()){console.log(v)}// ‘a’ ‘b’虽然上面列举了3种内建的迭代器方法,但是不同集合的类型还有自己默认的迭代器,在for of中,数组和Set的默认迭代器是values(),Map的默认迭代器是entries()。for of循环解构对象本身不支持迭代,但是我们可以自己添加一个生成器,返回一个key,value的迭代器,然后使用for of循环解构key和value。const obj ={a:1,b:2,Symbol.iterator{for(let i in obj){yield[i, obj[i]]}}}for(let[key, value] of obj){console.log(key, value)}// ‘a’ 1, ‘b’ 2字符串迭代器const str =‘abc’;for(let v of str){console.log(v)}// ‘a’ ‘b’ ‘c’ES6给数组添加了几个新方法:find()、findIndex()、fill()、copyWithin()1、find():传入一个回调函数,找到数组中符合当前搜索规则的第一个元素,返回它,并且终止搜索。const arr =[1,“2”,3,3,“2”]console.log(arr.find(n =>typeof n ===“number”))// 12、findIndex():传入一个回调函数,找到数组中符合当前搜索规则的第一个元素,返回它的下标,终止搜索。const arr =[1,“2”,3,3,“2”]console.log(arr.findIndex(n =>typeof n ===“number”))// 03、fill():用新元素替换掉数组内的元素,可以指定替换下标范围。arr.fill(value, start,end)4、copyWithin():选择数组的某个下标,从该位置开始复制数组元素,默认从0开始复制。也可以指定要复制的元素范围。arr.copyWithin(target, start,end)const arr =[1,2,3,4,5]console.log(arr.copyWithin(3))// [1,2,3,1,2] 从下标为3的元素开始,复制数组,所以4, 5被替换成1, 2const arr1 =[1,2,3,4,5]console.log(arr1.copyWithin(3,1))// [1,2,3,2,3] 从下标为3的元素开始,复制数组,指定复制的第一个元素下标为1,所以4, 5被替换成2, 3const arr2 =[1,2,3,4,5]console.log(arr2.copyWithin(3,1,2))// [1,2,3,2,5] 从下标为3的元素开始,复制数组,指定复制的第一个元素下标为1,结束位置为2,所以4被替换成2ES6中类class、Promise与异步编程、代理(Proxy)和反射(Reflection)API,这几块内容比较复杂,以后有机会再详细写。 ...

February 16, 2019 · 3 min · jiezi

ES6系统学习----从Apollo Client看解构赋值

一:背景在前一篇关于Apollo Client 的博客中已经说明了Apollo Client 是一个强大的JavaScript GraphQL 客户端。既然是一个JavaScript的客户端,他肯定就要遵守ES的规范。下面是Apollo Client的Query的代码:<Query query={CURRENT_PERSON}> {({ loading, error, data }) => { if (loading) return <Text>Loading…</Text> if (error) return <Text>Error!: ${error}</Text> const { currentPerson } = data return ( // 向子组件中传递currentPerson ) }}</Query>分析:这段Query是用来获取当前登陆用户的信息。在Apollo Client封装好的Query组件中有一个箭头函数,箭头函数的函数体主要是用来执行React-Native渲染页面的。看一下官网中在Mutation中对于解构赋值的描述。为了便于在UI中跟踪Mutation结果,在渲染时将Mutation的结果解构成loading, error。这就说明了,在箭头函数的参数中{ loading, error, data }是一个解构赋值的表达式,将QueryResult解构赋值成data 或 loading 或 error。只要QueryResult中有一个结果,那么在表达式中就会将这个值解构赋值出来。 这样在函数体中就会根据解析的结果不同执行不同的操作。二:解构赋值在ES6中允许按照一定的模式从数组和对象等中提取值,然后对变量进行赋值,这被称为解构赋值。一般是通过模式匹配的方式进行赋值。目前所接触到的解构赋值的场景一共有五类,之后有新认识的会进行不断的补充。第一类:数组的解构赋值等号左边必须采用方括号的形式进行接收解构的值等号左边的数组的每一项要和等号右边的解构目标中的每一项相匹配,同顺序。let [a,b] = [1,2] // a为1,b为2let [b,a] = [1,2] // a为2,b为1如果解构不成功,那么等号左侧接收变量的那部分值就为undefinedlet [a,b,c] = [1,2] // a为1,b为2,c为undefined使用…的形式是ES6的延展操作符,在开发中经常使用,尤其是在React-Native中使用延展操作符从父组件向子组件传递数据是非常方便的,不需要将所有的属性都点出来并接收之后再传递。let [a, …b] = [1,2,3,4] // a为1,b为[2,3,4]不完全解构:即左侧的接收变量可以比右侧的项少。但左侧若比右侧的多就会造成多的变量为undefined,同第3项。let [a] = [1,2] // a为1嵌套解构: 若存在多维数组,只要等号左边的模式相匹配也是可以解构出来的。let[a,[b,c],d] = [1,[2,3],4] //a=1,b=2,c=3,d=4使用默认值:在解构赋值的过程中出现上边第五种情况,即左侧定义的接收变量数目比右侧要解构的单项多,那么此时就可以使用默认值【注意:生效的前提是默认值所在的位置必须严格等于undefined】let [x=1] = [null]console.log(x) //为null在这里x输出的值不是undefined,而是null,他解构的目标就是不存在的,是null,不是未定义undefined。因此他的输出值为null。第二类:对象的解构赋值等号左边必须采用花括号的形式进行接收只要等号左侧的变量名与等号右侧对象的属性名相同,不论顺序,都可以接收到相应的值。【这一条就和前面开头的背景一样使用对象的属性名进行接收,无论位置,都可以进行接收】let {second,first} = { first: “ff”, second:“ss” } // second为ss, first为ff对象的解构赋值也相当于是模式匹配。它内部的工作机制是,先找到相应的同名属性,再赋值给相应的变量。let {first:second } = { first: “ff”, second:“ss” } // second为ff, first报错:first is not defined同样的对象的解构赋值也可以适用于嵌套结构let person = { firstPeple: {name: “zs”, age: 22} } let { firstPeple:{ name, age } } = personconsole.log(name) // zsconsole.log(age) // 22使用默认值,生效的前提是对象的属性值严格等于undefined,解释说明和数组的默认值类似。第三类:字符串的解构赋值字符串在处于解构赋值的环境中时被暂时看作是一个类似数组的对象,因此他的每一个字符都可以看成数组的每一项。这里就不再解释说明了。第四类:布尔值和数值的解构赋值这类解构赋值目前为止我并未在实际开发中遇到,以后遇到会结合实际情况进行说明。解构赋值时,如果等号的邮编时数值或者是布尔值,那么他就会先转换成对象,再进行解构赋值。let {toString: a} = 123console.log(a)console.log(Number.prototype.toString)console.log(Number.prototype.toString === a) // true在这里,转换成的对象都具有toString属性,因此相应的变量都可以取到值。第五类:函数参数的解构赋值function add([x + y]){console.log(x+y) // 输出3}add([1,2])上边的普通函数是将一个数组进行的解构赋值。同样的在函数的参数中还可以放对象的解构赋值。在开头的背景中,就是在箭头函数的参数中对一个Query查询的返回值对象进行解构赋值。最后的话从实际应用中去学习,复习,虽然所遭遇的坑会非常多。但是这一路走来,收获的将会非常的多。 ...

February 15, 2019 · 1 min · jiezi

ES6 类继承 和 super的使用

ES6继承 详细类容参考:http://es6.ruanyifeng.com/#do…1、super()使用class A { construcor(a, b) { this.a = a; this.b = b; }}class B extends A { constructor(x, y, z) { super(x, y); this.z = z; console.log(x, ‘—’, y, ‘—-’, z, ‘—-’); }}let b = new B(1, 2, 8);// 1 “—” 2 “—-” 8 “—-“注意:ES6中继承的子类中,如果使用构造函数constructor()那么就必须使用 super()方法初始化,这样下面才可以调用this关键字。super()只能用在子类的构造函数之中,用在其他地方就会报错。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。 —阮一峰2、父类中的静态方法,子类中可以通过类名直接调用class A2 { static hello() { console.log(“hello world”); }}class B2 extends A2 { constructor() { super(); }}B2.hello();// hello world3、Object.getPrototypeOf()判断子类继承的父类Object.getPrototypeOf(B2);// A2

February 12, 2019 · 1 min · jiezi

「前端面试题系列5」ES6 中箭头函数的用法

前言年味儿渐散,收拾下心情,继续敲代码吧。对于即将到来金三银四的求职季,相信不少同学都在默默地做着准备。本系列旨在梳理前端庞杂的知识点,并尽可能通俗易懂地表述出来,也希望能帮到有需要的同学。这是前端面试题系列的第 5 篇,你可能错过了前面的篇章,可以在这里找到:this 的原理以及用法伪类与伪元素的区别及实战如何实现一个圣杯布局?今日头条 面试题和思路解析面试中,我经常会问及 ES6 的知识点,因为平时工作中用得很多。当问到箭头函数时,不少候选人都会赞叹地说:箭头函数很好用,而且再也不用操心 this 的指向了。我接着问:箭头函数是挺好用的,但是你有没有遇到过,不适合使用箭头函数的场景呢?这时,能回答得上来的候选人就很少了。箭头函数在大多数情况下,是很好用的,但是为什么在有些场景,使用箭头函数后会产生问题?是不是箭头函数还不够完善?又有哪些场景会发生问题?该如何解决呢?这,正是本文想要一起探讨的。箭头函数的写法为什么叫箭头函数( Arrow Function )?因为它的写法,看上去就是一个箭头:const multiply = num => num * num;它等价于:const multiply = function (num) { return num * num;};此外,还可以传多个参数,以及可变参数。// 多参数const multiply = (num1, num2) => num1 * num2;// 可变参数const sum = (num1, num2, …rest) => { let result = num1 + num2; for (let i = 0; i < rest.length; i++) { result += rest[i]; } return result;};当有多条语句时,需要配上 {…} 和 return。另外,如果返回的结果是对象,则需要配上 (),像下面这样:const func = val => ({ value: val });从上述的写法来看,相较普通函数而言,箭头函数的确简便了很多,提升了我们代码的易用性。但它并非在任何场景下都适用,接下来,将会介绍几种不适合箭头函数的场景,并会提出可行的解决方案。不适合的场景1、对象的方法看下面这个例子:const obj = { x: 1, print: () => { console.log(this === window); // => true console.log(this.x); // undefined }};obj.print();this.x 打印出来是 undefined。为什么?然后,我在上面加了一行,发现 this 指向了 window。解析:print 方法用了箭头函数,其内部的 this 指向的还是上下文 window,上下文中并没有定义 x,所以 this.x 输出为 undefined。解决办法:用 ES6 的短语法,或者传统的函数表达式都可以。所以,print 要这样写:print () { console.log(this === test); // => true console.log(this.x); // 1}2、原型方法同样的规则也适用于原型方法的定义,使用箭头函数会导致运行时的执行上下文错误。function Cat (name) { this.name = name;}Cat.prototype.sayCatName = () => { console.log(this === window); // => true return this.name;};const cat = new Cat(‘Miao’);cat.sayCatName(); // => undefined解决办法是:用回传统的函数表达式,像下面这样:Cat.prototype.sayCatName = function () { console.log(this === cat); // => true return this.name;};sayCatName 变回传统的函数表达式之后,被调用时的执行上下文就会指向新创建的 cat 实例。3、事件的回调看下面这个例子:const btn = document.getElementById(‘myButton’);btn.addEventListener(‘click’, () => { console.log(this === window); // => true this.innerHTML = ‘Clicked button’;});这里会有问题,因为 this 指向了 window。解析:当为一个 DOM 事件绑定回调函数后,触发回调函数时的 this,需要指向当前发生事件的 DOM 节点,也就是这里的 btn。当回调发生时,浏览器会用 btn 的上下文去调用处理函数。所以最后的 this.innerHTML 等价于 window.innerHTML,问题就在这里。解决办法:用函数表达式代替箭头函数。像这样:btn.addEventListener(‘click’, function() { console.log(this === btn); // => true this.innerHTML = ‘Clicked button’;});另外,在 react 中的事件回调,也经常会遇到类似的问题。// jsx render<Button onClick={this.handleClickButton.bind(this)}> …</Button>// callbackhandleClickButton () { …}注意:这里 onClick 的回调函数,并非字符串,而是一个实实在在的函数。可以将 onClick 理解为一个中间变量,所以 react 在处理函数时的 this 指向就会丢失。为了解决这个问题,我们需要为回调函数绑定 this,使得事件处理函数无论如何传递,this 都指向我们实例化的那个对象。在这里,如果用箭头函数,可以这样改写:<Button onClick={ event => this.handleClickButton(event) }> …</Button>箭头函数并没有自己的 this,所以事件处理函数的调用者并不受影响。4、构造函数箭头函数不能通过 new 关键字调用。const Message = (text) => { this.text = text;};var helloMessage = new Message(‘Hello World!’);// Uncaught TypeError: Message is not a constructor解析:从报错信息可以看出,箭头函数没有 constructor 方法,所以不能用作构造函数。 JavaScript 会通过抛出异常的方式,进行隐式地预防。解决方法:用函数表达式代替箭头函数。总结回顾 MDN 给出的解释:箭头函数表达式的语法比函数表达式更短,并且没有自己的this,arguments,super或 new.target。这些函数表达式更适用于那些本来需要匿名函数的地方,并且它们不能用作构造函数。所以说,箭头函数无疑是 ES6 带来的重大改进,在正确的场合使用箭头函数,能让代码变得更加简洁短小。但箭头函数也不是万能的,不能用的时候,千万别硬往上套。比如,在需要动态上下文的场景中,使用箭头函数需要格外地小心,这些场景包括:对象的方法、原型方法、事件的回调、构造函数。并非一定要用箭头函数,才能解决问题。PS:欢迎关注我的公众号 “超哥前端小栈”,交流更多的想法与技术。 ...

February 11, 2019 · 2 min · jiezi

利用ES6进行Promise封装总结

原生Promise解析简介promise是异步编程的一种解决方案,比传统的解决方案–回调函数和事件–更合理和强大。promise简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果,从语法上来说,Promise是一个对象,从它可以获取异步操作的消息,Promise提供统一的API,各种异步操作都可以用同样的方法进行处理特点对象的状态不受外界影响,Promise对象代表一个异步操作,有三种状态:Pendding、fulfilled、rejected。只有异步操作的结果,可以决定当前是哪一种状态,其他操作都无法改变这个状态。一旦状态改变,就不会在变,任何时候都可以得到这个结果,只有两种可能:从Pendding变为fulfilled和从Pendding变为rejected。只要这两种情况发生,状态就凝固了,会一直保持这个结果,这时就称为resolved。利用es6进行Promise封装处理同步任务原生方法调用方式 new Promise((resolve,reject)=>{ resolve(1) }).then(res=>{ console.log(res) //1 })同步封装思考 1.由调用方式可见Promise是一个类 2.它接收一个回调函数,这个回调函数接受resolve和reject方法作为参数 3.当状态改变后执行then方法,并将resolve或reject的结果作为then方法接受回调函数的参数 class Mypromise{ constructor(callback){ this.status=‘pendding’ //成功结果 this.s_res = null // 失败结果 this.f_res = null callback((arg)=>{ // 使用箭头函数this不会丢失 // 改变状态为成功 this.status = ‘fulfilled’ this.s_res = arg },(arg)=>{ // 改变状态为失败 this.status = ‘rejected’ this.f_res = arg }) } then(onresolve,onreject){ if(this.status === ‘fulfilled’){ // 当状态为成功时 onresolve(this.s_res) }else if(this.status === ‘rejected’){ // 当状态为失败时 onreject(this.f_res) } } }处理异步任务原生调用方式 new Promise((resolve,reject)=>{ setTimeOut(()=>{ resolve(1) },1000) }).then(res=>{ console.log(res) })异步封装思考 1.根据js执行机制,setTimeOut属于宏任务,then回调函数属于微任务,当主线程执行完成后,会从异步队列中 取出本次的微任务先执行。 2.也就是说,then方法执行时,状态还没有改变,所有我们需要将then方法执行的回调保存起来,等到异步代码执行 完成后,在统一执行then方法的回调函数 class Mypromise{ constructor(callback){ this.status=‘pendding’ //成功结果 this.s_res = null // 失败结果 this.f_res = null this.query = [] // ++ callback((arg)=>{ // 使用箭头函数this不会丢失 // 改变状态为成功 this.status = ‘fulfilled’ this.s_res = arg // 当状态改变后,统一执行then方法的回调 this.query.forEach(item=>{ item.resolve(arg) }) },(arg)=>{ // 改变状态为失败 this.status = ‘rejected’ this.f_res = arg // 当状态改变后,统一执行then方法的回调 this.query.forEach(item=>{ item.reject(arg) }) }) } then(onresolve,onreject){ if(this.status === ‘fulfilled’){ // 当状态为成功时 onresolve(this.s_res) }else if(this.status === ‘rejected’){ // 当状态为失败时 onreject(this.f_res) }else{ // ++ 状态没有改变 this.query.push({ // 保存回调函数到队列中 resolve:onresolve, reject:onreject }) } } } 处理链式调用原生调用方式 new Promise((resolve,reject)=>{ resolve(1) }).then(res=>{ return res }).then(res=>{ console.log(res) })链式调用思考 原生的Promise对象的then方法,返回的也是一个Promise对象,一个新的Promise才能支持链式调用 下一个then方法可以接受上一个then方法的返回值作为回调函数的参数 主要考虑上一个then方法的返回值: 1.Promise对象/具有then方法的对象 2.其他值 第一个then方法返回一个Promise对象,它的回调函数接受resFn和rejFN两个回调函数作为参数, 把成功状态的处理封装为handle函数,接受成功的结果作为参数 在handle函数,根据onresolve返回值的不同做出不同的处理 class Mypromise{ constructor(callback){ this.status=‘pendding’ //成功结果 this.s_res = null // 失败结果 this.f_res = null this.query = [] // ++ callback((arg)=>{ // 使用箭头函数this不会丢失 // 改变状态为成功 this.status = ‘fulfilled’ this.s_res = arg // 当状态改变后,统一执行then方法的回调 this.query.forEach(item=>{ item.resolve(arg) }) },(arg)=>{ // 改变状态为失败 this.status = ‘rejected’ this.f_res = arg // 当状态改变后,统一执行then方法的回调 this.query.forEach(item=>{ item.reject(arg) }) }) } then(onresolve,onreject){ return new Mypromise((resFN,rejFN)=>{ if(this.status === ‘fulfilled’){ // 当状态为成功时 handle(this.s_res) }else if(this.status === ‘rejected’){ // 当状态为失败时 errBack(this.f_res) }else{ // ++ 状态没有改变 this.query.push({ // 保存回调函数到队列中 resolve:onresolve, reject:onreject }) } function handle(value){ // 当then方法的onresolve方法有返回值时,保存其返回值,没有使用其保存的值 let returnVal = onresolve instanceof Function && onresolve(value) || value // 如果onresolve方法返回的是promise对象,则调用其then方法 if(returnVal&&returnVal[’then’] instanceof Function){ returnVal.then(res=>{ resFN(res) },err=>{ rejFN(err) }) }else{ resFN(returnVal) } } function errBack(reason){ if(onreject instanceof Function){ let returnVal = reject(reason) if(typeof returnVal !== ‘undenfined’ && returnVal[’then’] instanceof Function){ returnVal.then(res=>{ resFN(res) },err=>{ rejFN(err) }) }else{ resFN(returnVal) } }else{ rejFN(reason) } } }) } } Promise.all和Promise.race方法原生调用方式 Promise.all方法接受一个数组,数组中的每一项都是一个Promise实例,只有数组中的所有Promise实例的状态 都变为fulfilled时,此时整个状态才会变成fulfilled,此时数组中所有Promise实例的返回值组成一个新的数组, 进行传递。 Promise.race方法和Promise.all方法一样,如果不是Promise实例,就会先调用Promise.resolve方法,将参数 转为Promise实例,在进行下一步处理。 只要数组中有一个参数的状态变为fulfilled就会进行传递 // 将现有对象转换为Promise对象 Mypromise.resolve = (arg)=>{ if(typeof arg == ‘undefined’ || arg==null){ // 不带有任何参数 return new Mypromise(resolve=>{ resolve(arg) }) }else if(arg instanceof Mypromise){ // 是一个Mypromise实例 return arg }else if(arg[’then’] instanceof Function){ // 具有then方法的对象 return new Mypromise((resolve,reject)=>{ arg.then(res=>{ resolve(res) },err=>{ reject(err) }) }) }else{ // 参数不是具有then方法的对象,或根本不是对象 return new Mypromise(resolve=>{ resolve(arg) }) } } Mypromise.all = (arr)=>{ if(!Array.isArray(arr)){ throw new TypeError(‘参数必须是一个数组’) } return new Mypromise((resolve,reject)=>{ let i=0,result=[] next() functon next(){ // 如果不是Mypromise实例需要转换 Mypromise.resolve(arr[i]).then(res=>{ result.push(res) i++ if(i===arr.length){ resolve(result) }else{ next() } },reject) } }) } Mypromise.race = (arr)=>{ if(!Array.isArray(arr)){ throw new TypeError(‘参数必须是一个数组’) } return new Mypromise((resolve,reject)=>{ let done = false arr.forEach(item=>{ Mypromise.resolve(item).then(res=>{ if(!done){ resolve(res) done = true } },err=>{ if(!done){ reject(res) done = true } }) }) }) }处理Mypromise状态确定不能改变的特性 在重写callback中的resolve和reject方法执行前,先判断状态是否为’pendding’ ...

February 11, 2019 · 3 min · jiezi

【ES6】改变 JS 内置行为的代理与反射

代理(Proxy)可以拦截并改变 JS 引擎的底层操作,如数据读取、属性定义、函数构造等一系列操作。ES6 通过对这些底层内置对象的代理陷阱和反射函数,让开发者能进一步接近 JS 引擎的能力。一、代理与反射的基本概念什么是代理和反射呢?代理是用来替代另一个对象(target),JS 通过new Proxy()创建一个目标对象的代理,该代理与该目标对象表面上可以被当作同一个对象来对待。当目标对象上的进行一些特定的底层操作时,代理允许你拦截这些操作并且覆写它,而这原本只是 JS 引擎的内部能力。如果你对些代理&反射的概念比较困惑的话,可以直接看后面的应用示例,最后再重新看这些定义就会更清晰!拦截行为使用了一个能够响应特定操作的函数( 被称为陷阱),每个代理陷阱对应一个反射(Reflect)方法。ES6 的反射 API 以 Reflect 对象的形式出现,对象每个方法都与对应的陷阱函数同名,并且接收的参数也与之一致。以下是 Reflect 对象的一些方法:代理陷阱覆写的特性方法 get读取一个属性的值Reflect.get()set写入一个属性Reflect.set()hasin 运算符Reflect.has()deletePropertydelete 运算符Reflect.deleteProperty()getPrototypeOfObject.getPrototypeOf()Reflect.getPrototypeOf()isExtensibleObject.isExtensible()Reflect.isExtensible()definePropertyObject.defineProperty()Reflect.definePropertyapply调用一个函数Reflect.apply()construct使用 new 调用一个函数Reflect.construct()每个陷阱函数都可以重写 JS 对象的一个特定内置行为,允许你拦截并修改它。综合来说,想要控制或改变JS的一些底层操作,可以先创建一个代理对象,在这个代理对象上挂载一些陷阱函数,陷阱函数里面有反射方法。通过接下来的应用示例可以更清晰的明白代理的过程。二、开始一个简单的代理当你使用 Proxy 构造器来创建一个代理时,需要传递两个参数:目标对象(target)以及一个处理器( handler),先创建一个仅进行传递的代理如下:// 目标对象let target = {}; // 代理对象let proxy = new Proxy(target, {});proxy.name = “hello”;console.log(proxy.name); // “hello"console.log(target.name); // “hello"target.name = “world”;console.log(proxy.name); // “world"console.log(target.name); // “world上例中的 proxy 代理对象将所有操作直接传递给 target 目标对象,代理对象 proxy 自身并没有存储该属性,它只是简单将值传递给 target 对象,proxy.name 与 target.name 的属性值总是相等,因为它们都指向 target.name。此时代理陷阱的处理器为空对象,当然处理器可以定义了一个或多个陷阱函数。2.1 set 验证对象属性的存储假设你想要创建一个对象,并要求其属性值只能是数值,这就意味着该对象的每个新增属性都要被验证,并且在属性值不为数值类型时应当抛出错误。这时需要使用 set 陷阱函数来拦截传入的 value,该陷阱函数能接受四个参数:trapTarget :将接收属性的对象( 即代理的目标对象)key :需要写入的属性的键( 字符串类型或符号类型)value :将被写入属性的值;receiver :操作发生的对象( 通常是代理对象)set 陷阱对应的反射方法和默认特性是Reflect.set(),和陷阱函数一样接受这四个参数,并会基于操作是否成功而返回相应的结果:let targetObj = {};let proxyObj = new Proxy(targetObj, { set: set});/* 定义 set 陷阱函数 /function set (trapTarget, key, value, receiver) { if (isNaN(value)) { throw new TypeError(“Property " + key + " must be a number.”); } return Reflect.set(trapTarget, key, value, receiver);}/ 测试 /proxyObj.count = 123;console.log(proxyObj.count); // 123console.log(targetObj.count); // 123proxyObj.anotherName = “proxy” // TypeError: Property anotherName must be a number.示例中set 陷阱函数成功拦截传入的 value 值,你可以尝试一下,如果注释或不return Reflect.set()会发生什么?,答案是拦截陷阱就不会有反射响应。需要注意的是,直接给 targetObj 目标对象赋值时是不会触发 set 代理陷阱的,需要通过给代理对象赋值才会触发 set 代理陷阱与反射。2.2 get 验证对象属性的读取JS 非常有趣的特性之一,是读取不存在的属性时并不会抛出错误,而会把undefined当作该属性的值。对于大型的代码库,当属性名称存在书写错误时(不会抛错)会导致严重的问题。这时使用 get 代理陷阱验证对象结构(Object Shape),访问不存在的属性时就抛出错误,使对象结构验证变得简单。get 陷阱函数会在读取属性时被调用,即使该属性在对象中并不存在,它能接受三个参数:trapTarget :将会被读取属性的对象( 即代理的目标对象)key :需要读取的属性的键( 字符串类型或符号类型)receiver :操作发生的对象( 通常是代理对象)Reflect.get()方法接受与之相同的参数,并返回默认属性的默认值。let proxyObj = new Proxy(targetObj, { set: set, get: get});/ 定义 get 陷阱函数 /function get(trapTarget, key, receiver) { if (!(key in receiver)) { throw new TypeError(“Property " + key + " doesn’t exist.”); } return Reflect.get(trapTarget, key, receiver);}console.log(proxyObj.count); // 123console.log(proxyObj.newcount) // TypeError: Property newcount doesn’t exist.这段代码允许添加新的属性,并且此后可以正常读取该属性的值,但当读取的属性并不存在时,程序抛出了一个错误,而不是将其默认为undefined。还可以使用 has 陷阱验证in运算符,使用 deleteProperty 陷阱函数避免属性被delete删除。注:in运算符用于判断对象中是否存在某个属性,如果自有属性或原型属性匹配这个名称字符串或Symbol,那么in运算符返回 true。targetObj = { name: ’targetObject’};console.log(“name” in targetObj); // trueconsole.log(“toString” in targetObj); // true其中 name 是对象自身的属性,而 toString 则是原型属性( 从 Object 对象上继承而来),所以检测结果都为 true。has 陷阱函数会在使用in运算符时被调用,并且会传入两个参数(同名反射Reflect.has()方法也一样):trapTarget :需要读取属性的对象( 代理的目标对象)key :需要检查的属性的键( 字符串类型或 Symbol符号类型)deleteProperty 陷阱函数会在使用delete运算符去删除对象属性时下被调用,并且也会被传入两个参数(Reflect.deleteProperty() 方法也接受这两个参数):trapTarget :需要删除属性的对象( 即代理的目标对象) ;key :需要删除的属性的键( 字符串类型或符号类型) 。一些思考:分析过 Vue 源码的都了解过,给一个 Vue 实例中挂载的 data,是通过Object.defineProperty代理 vm._data 中的对象属性,实现双向绑定…… 同理可以考虑使用 ES6 的 Proxy 的 get 和 set 陷阱实现这个代理。三、对象属性陷阱3.1 数据属性与访问器属性ES5 最重要的特征之一就是引入了 Object.defineProperty() 方法定义属性的特性。属性的特性是为了实现javascript引擎用的,属于内部值,因此不能直接访问他们。属性分为数据属性和访问器属性。使用Object.defineProperty()方法修改数据属性的特性值的示例如下:let obj1 = { name: ‘myobj’,}/ 数据属性*/Object.defineProperty(obj1,’name’,{ configurable: false, // default true writable: false, // default true enumerable: true, // default true value: ‘jenny’ // default undefined})console.log(obj1.name) // ‘jenny’其中[[Configurable]] 表示能否通过 delete 删除属性从而重新定义为访问器属性;[[Enumerable]] 表示能否通过for-in循环返回属性;[[Writable]] 表示能否修改属性的值; [[Value]] 包含这个属性的数据值。对于访问器属性,该属性不包含数据值,包含一对getter和setter函数,定义访问器属性必须使用Object.defineProperty()方法:let obj2 = { age: 18}/* 访问器属性 /Object.defineProperty(obj2,’_age’,{ configurable: false, // default true enumerable: false, // default true get () { // default undefined return this.age }, set (num) { // default undefined this.age = num }})/ 修改访问器属性调用 getter /obj2._age = 20 console.log(obj2.age) // 20/ 输出访问器属性 /console.log(Object.getOwnPropertyDescriptor(obj2,’_age’)) // { get: [Function: get],// set: [Function: set],// enumerable: false,// configurable: false }[[Get]] 在读取属性时调用的函数, [[Set]] 再写入属性时调用的函数。使用访问器属性的常用方式,是设置一个属性的值导致其他属性发生变化。3.2 检查属性的修改代理允许你使用 defineProperty 同名函数陷阱函数拦截Object.defineProperty()的调用,defineProperty 陷阱函数接受下列三个参数:trapTarget :需要被定义属性的对象( 即代理的目标对象);key :属性的键( 字符串类型或符号类型);descriptor :为该属性准备的描述符对象。defineProperty 陷阱函数要求在操作后返回一个布尔值用于判断操作是否成功,如果返回了 false 则抛出错误,故可以使用该功能来限制哪些属性可以被Object.defineProperty() 方法定义。例如,如果想阻止定义Symbol符号类型的属性,你可以检查传入的属性值,若是则返回 false:/ 定义代理 /let proxy = new Proxy({}, { defineProperty(trapTarget, key, descriptor) { if (typeof key === “symbol”) { return false; } return Reflect.defineProperty(trapTarget, key, descriptor); }});Object.defineProperty(proxy, “name”, { value: “proxy”});console.log(proxy.name); // “proxy"let nameSymbol = Symbol(“name”);// 抛出错误Object.defineProperty(proxy, nameSymbol, { value: “proxy”})四、函数代理4.1 构造函数 & 立即执行函数的两个内部方法:[[Call]] 与[[Construct]]会在函数被调用时调用,通过代理函数来为这两个内部方法设置陷阱,从而控制函数的行为。[[Construct]]会在函数被使用new运算符调用时执行,代理触发construct()陷阱函数,并和Reflect.construct()一样接收到下列两个参数:trapTarget :被执行的函数( 即代理的目标对象) ;argumentsList :被传递给函数的参数数组。[[Call]]会在函数被直接调用时执行,代理触发apply()陷阱函数,它和Reflect.apply()都接收三个参数:trapTarget :被执行的函数( 代理的目标函数) ;thisArg :调用过程中函数内部的 this 值;argumentsList :被传递给函数的参数数组。每个函数都包含call()和apply()方法,用于重置函数运行的作用域即 this 指向,区别只是接收参数的方式不同:call()的参数需要逐个列举、apply()是参数数组。显然,apply 与 construct 要求代理目标对象必须是一个函数,这两个代理陷阱在函数的执行方式上开启了很多的可能性,结合使用就可以完全控制任意的代理目标函数的行为。4.2 验证函数的参数看到apply()和construct()陷阱的参数都有被传递给函数的参数数组argumentsList,所以可以用来验证函数的参数。例如需要保证所有参数都是某个特定类型的,并且不能通过 new 构造使用,示例如下:/ 定义 sum 目标函数 /function sum(…values) { return values.reduce((previous, current) => previous + current, 0);}/ 定义 apply 陷阱函数 /function applyRef (trapTarget, thisArg, argumentList) { argumentList.forEach((arg) => { if (typeof arg !== “number”) { throw new TypeError(“All arguments must be numbers.”); } }); return Reflect.apply(trapTarget, thisArg, argumentList);}/ 定义 construct 陷阱函数 /function constructRef () { throw new TypeError(“This function can’t be called with new.”);}/ 定义 sumProxy 代理函数 */let sumProxy = new Proxy(sum, { apply: applyRef, construct: constructRef});console.log(sumProxy(1, 2, 3, 4)); // 10// console.log(sumProxy(1, “2”, 3, 4)); // TypeError: All arguments must be numbers.// let result = new sumProxy() // TypeError: This function can’t be called with new.sum() 函数会将所有传递进来的参数值相加,此代码通过将 sum() 函数封装在 sumProxy() 代理中,如果传入参数的值不是数值类型,该函数仍然会尝试加法操作,但在函数运行之前拦截了函数调用,触发apply陷阱函数以保证每个参数都是数值。出于安全的考虑,这段代码使用 construct 陷阱抛出错误,以确保该函数不会被使用 new 运算符调用实例对象 instance 对象会被同时判定为 proxy 与 target 对象的实例,是因为 instanceof 运算符使用了原型链来进行推断,而原型链查找并没有受到这个代理的影响,因此 proxy 对象与 target 对象对于 JS 引擎来说就有同一个原型。4.3 调用类的构造函数ES6 中新引入了class类的概念,类使用constructor构造函数封装数据,并规定必须始终使用 new 来调用,原因是类构造器的内部方法 [[Call]] 被明确要求抛出错误。代理可以拦截对于 [[Call]] 方法的调用,你可以借助代理调用的类构造器。例如在缺少 new 的情况下创建一个新实例,就使用 apply 陷阱函数实现:class Person { constructor(name) { this.name = name; }}let PersonProxy = new Proxy(Person, { apply: function(trapTarget, thisArg, argumentList) { return new trapTarget(…argumentList); }});let me = PersonProxy(“Jenny”);console.log(me.name); // “Jenny"console.log(me instanceof Person); // trueconsole.log(me instanceof PersonProxy); // true类构造器即类的构造函数,使用代理时它的行为就像函数一样,apply陷阱函数重写了默认的构造行为。关于类的更多有趣的用法,可参考 【ES6】更易于继承的类语法总结来说,代理的用途非常广泛,因为它提供了修改 JS 内置对象的所有行为的入口。上述例子只是简单的一些应用入门,还有更多复杂的示例,推荐阅读《深入理解ES6》。继续加油鸭少年!!! ...

February 4, 2019 · 3 min · jiezi

关于 Promise 的 9 个提示

关于 Promise 的 9 个提示正如同事所说的那样,Promise 在工作中表现优异。这篇文章会给你一些如何改善与 Promise 之间关系的建议。1. 你可以在 .then 里面 return 一个 Promise让我来说明这最重要的一点是的!你可以在 .then 里面 return 一个 Promise而且,return 的这个 Promise 将在下一个 .then 中自动解析。.then(r => { return serverStatusPromise(r); // 返回 { statusCode: 200 } 的 Promise}).then(resp => { console.log(resp.statusCode); // 200; 注意自动解析的 promise})2. 每次执行 .then 的时候都会自动创建一个新的 Promise如果熟悉 javascript 的链式风格,那么你应该会感到很熟悉。但是对于一个初学者来说,可能就不会了。在 Promise 中不论你使用 .then 或者 .catch 都会创建一个新的 Promise。这个 Promise 是刚刚链式调用的 Promise 和 刚刚加上的 .then / .catch 的组合。让我们来看一个 ????:var statusProm = fetchServerStatus();var promA = statusProm.then(r => (r.statusCode === 200 ? “good” : “bad”));var promB = promA.then(r => (r === “good” ? “ALL OK” : “NOTOK”));var promC = statusProm.then(r => fetchThisAnotherThing());上面 Promise 的关系可以在流程图中清晰的描述出来:需要特别注意的是 promA、 promB 和 promC 全部都是不同的但是有关联的 Promise。我喜欢把 .then 想像成一个大型管道,当上游节点出现问题时,水就会停止流向下游。例如,如果 promB 失败,下游节点不会受到影响,但是如果 statusProm 失败,那么下游的所有节点都将受到影响,即 rejected。3. 对调用者来说,Promise 的 resolved/rejected 状态是唯一的我认为这个是让 Promise 好好运行的最重要的事情之一。简单来说,如果在你的应用中 Promise 在很多不同的模块之间共享,那么当 Promise 返回 resolved/rejected 状态时,所有的调用者都会收到通知。这也意味着没有人可以改变你的 Promise,所以可以放心的把它传递出去。function yourFunc() { const yourAwesomeProm = makeMeProm(); yourEvilUncle(yourAwesomeProm); // 无论 Promise 受到了怎样的影响,它最终都会成功执行 return yourAwesomeProm.then(r => importantProcessing(r));}function yourEvilUncle(prom) { return prom.then(r => Promise.reject(“destroy!!”)); // 可能遭受的影响}通过上面的例子可以看出,Promise 的设计使得自身很难被改变。正如我上面所说的:“保持冷静,并将 Promise 传递下去”。4. Promise 构造函数不是解决方案我看到很多开发者喜欢用构造函数的风格,他们认为这就是 Promise 的方式。但这却是一个谎言,实际的原因是构造函数 API 和之前回调函数的 API 相似,而且这样的习惯很难改变。如果你发现自己正在到处使用 Promise 构造函数,那你的做法是错的!要真正的向前迈进一步并且摆脱回调,你需要小心谨慎并且最小程度地使用 Promise 构造函数。让我们看一下使用 Promise 构造函数 的具体情况:return new Promise((res, rej) => { fs.readFile("/etc/passwd", function(err, data) { if (err) return rej(err); return res(data); });});Promise 构造函数 应该只在你想要把回调转换成 Promise 时使用。一旦你掌握了这种创建 Promise 的优雅方式,它将会变的非常有吸引力。让我们看一下冗余的 Promise 构造函数。☠️错误的return new Promise((res, rej) => { var fetchPromise = fetchSomeData(…..); fetchPromise .then(data => { res(data); // 错误!!! }) .catch(err => rej(err))})????正确的return fetchSomeData(…); // 正确的!用 Promise 构造函数 封装 Promise 是多余的,并且违背了 Promise 本身的目的。????高级技巧如果你是一个 nodejs 开发者,我建议你可以看一看 util.promisify。这个方法可以帮助你把 node 风格的回调转换为 Promise。const {promisify} = require(‘util’);const fs = require(‘fs’);const readFileAsync = promisify(fs.readFile);readFileAsync(‘myfile.txt’, ‘utf-8’) .then(r => console.log(r)) .catch(e => console.error(e));</div>5. 使用 Promise.resolveJavascript 提供了 Promise.resolve 方法,像下面的例子这样简洁:var similarProm = new Promise(res => res(5));// ^^ 等价于var prom = Promise.resolve(5);它有多种使用情况,我最喜欢的一种是可以把普通的(异步的)js 对象转化成 Promise。// 将同步函数转换为异步函数function foo() { return Promise.resolve(5);}当不确定它是一个 Promise 还是一个普通的值的时候,你也可以做一个安全的封装。function goodProm(maybePromise) { return Promise.resolve(maybePromise);}goodProm(5).then(console.log); // 5var sixPromise = fetchMeNumber(6);goodProm(sixPromise).then(console.log); // 6goodProm(Promise.resolve(Promise.resolve(5))).then(console.log); // 5, 注意,它会自动解析所有的 Promise!6.使用 Promise.rejectJavascript 也提供了 Promise.reject 方法。像下面的例子这样简洁:var rejProm = new Promise((res, reject) => reject(5));rejProm.catch(e => console.log(e)) // 5我最喜欢的用法是提前使用 Promise.reject 来拒绝。function foo(myVal) { if (!mVal) { return Promise.reject(new Error(‘myVal is required’)) } return new Promise((res, rej) => { // 从你的大回调到 Promise 的转换! })}简单来说,使用 Promise.reject 可以拒绝任何你想要拒绝的 Promise。在下面的例子中,我在 .then 里面使用:.then(val => { if (val != 5) { return Promise.reject(‘Not Good’); }}).catch(e => console.log(e)) // 这样是不好的注意:你可以像 Promise.resolve 一样在 Promise.reject 中传递任何值。你经常在失败的 Promise 中发现 Error 的原因是因为它主要就是用来抛出一个异步错误的。7. 使用 Promise.allJavascript 提供了 Promise.all 方法。像 … 这样的简洁,好吧,我想不出来例子了????。在伪算法中,Promise.all 可以被概括为:接收一个 Promise 数组 然后同时运行他们 然后等到他们全部运行完成 然后 return 一个新的 Promise 数组 他们其中有一个失败或者 reject,都可以被捕获。下面的例子展示了所有的 Promise 完成的情况:var prom1 = Promise.resolve(5);var prom2 = fetchServerStatus(); // 返回 {statusCode: 200} 的 PromiseProimise.all([prom1, prom2]).then([val1, val2] => { // 注意,这里被解析成一个数组 console.log(val1); // 5 console.log(val2.statusCode); // 200})下面的例子展示了当他们其中一个失败的情况:var prom1 = Promise.reject(5);var prom2 = fetchServerStatus(); // 返回 {statusCode: 200} 的 PromiseProimise.all([prom1, prom2]).then([val1, val2] => { console.log(val1); console.log(val2.statusCode); }).catch(e => console.log(e)) // 5, 直接跳转到 .catch注意:Promise.all 是很聪明的!如果其中一个 Promise 失败了,它不会等到所有的 Promise 完成,而是立即中止!8. 不要害怕 reject,也不要在每个 .then 后面加冗余的 .catch我们是不是会经常担心错误会在它们之间的某处被吞噬?为了克服这个恐惧,这里有一个简单的小提示:让 reject 来处理上游函数的问题。在理想的情况下,reject 方法应该是应用的根源,所有的 reject 都会向下传递。不要害怕像下面这样写return fetchSomeData(…);现在如果你想要处理函数中 reject 的情况,请决定是解决问题还是继续 reject。???? 解决 reject解决 reject 是很简单的,在 .catch 不论你返回什么内容,都将被假定为已解决的。然而,如果你在 .catch 中返回 Promise.reject,那么这个 Promise 将会是失败的。.then(() => 5.length) // <– 这里会报错.catch(e => { return 5; // <– 重新使方法正常运行}).then(r => { console.log(r); // 5}).catch(e => { console.error(e); // 这个方法永远不会被调用 :)})????拒绝一个 reject拒绝一个 reject 是简单的。不需要做任何事情。 就像我刚刚说的,让它成为其他函数的问题。通常情况下,父函数有比当前函数处理 reject 更好的方法。需要记住的重要的一点是,一旦你写了 catch 方法,就意味着你正在处理这个错误。这个和同步 try/catch的工作方式相似。如果你确实想要拦截一个 reject:(我强烈建议不要这样做!).then(() => 5.length) // <– 这里会报错.catch(e => { errorLogger(e); // 做一些错误处理 return Promise.reject(e); // 拒绝它,是的,你可以这么做!}).then(r => { console.log(r); // 这个 .then (或者任何后面的 .then) 将永远不会被调用,因为我们在上面使用了 reject :)}).catch(e => { console.error(e); //<– 它变成了这个 catch 方法的问题}).then(x,y) 和 then(x).catch(x) 之间的分界线.then 接收的第二个回调函数参数也可以用来处理错误。它和 then(x).catch(x) 看起来很像,但是他们处理错误的区别在于他们自身捕获的错误。我会用下面的例子来说明这一点:.then(function() { return Promise.reject(new Error(‘something wrong happened’));}).catch(function(e) { console.error(e); // something wrong happened});.then(function() { return Promise.reject(new Error(‘something wrong happened’));}, function(e) { // 这个回调处理来自当前 .then 方法之前的错误 console.error(e); // 没有错误被打印出来});当你想要处理的是来自上游 Promise 而不是刚刚在 .then 里面加上去的错误的时候, .then(x,y) 变的很方便。提示: 99.9% 的情况使用简单的 then(x).catch(x) 更好。9. 避免 .then 回调地狱这个提示是相对简单的,尽量避免 .then 里包含 .then 或者 .catch。相信我,这比你想象的更容易避免。☠️错误的request(opts).catch(err => { if (err.statusCode === 400) { return request(opts) .then(r => r.text()) .catch(err2 => console.error(err2)) }})????正确的request(opts).catch(err => { if (err.statusCode === 400) { return request(opts); }}).then(r => r.text()).catch(err => console.erro(err));有些时候我们在 .then 里面需要很多变量,那就别无选择了,只能再创建一个 .then 方法链。.then(myVal => { const promA = foo(myVal); const promB = anotherPromMake(myVal); return promA .then(valA => { return promB.then(valB => hungryFunc(valA, valB)); // 很丑陋! })})我推荐使用 ES6 的解构方法混合着 Promise.all 方法就可以解决这个问题。.then(myVal => { const promA = foo(myVal); const promB = anotherPromMake(myVal); return Promise.all([prom, anotherProm])}).then(([valA, valB]) => { // 很好的使用 ES6 解构 console.log(valA, valB) // 所有解析后的值 return hungryFunc(valA, valB)})注意:如果你的 node/浏览器/老板/意识允许,还可以使用 async/await 方法来解决这个问题。我真心希望这篇文章对你理解 Promise 有所帮助。 ...

February 2, 2019 · 4 min · jiezi

webpack4+react+antd+axios+router4+redux 学习以及脚手架搭建_014

webpack4 学习脚手架搭建安装和初始化首先附上官方的文档github地址https://github.com/xiaopingzh…会不定时更新,如果觉得有帮助到你,给个Star当做鼓励可好。.├── README.md├── build│ ├── webpack.dev.conf.js│ ├── webpack.dll.conf.js│ └── webpack.prod.conf.js├── dist├── dll├── manifest.json├── package-lock.json├── package.json├── public│ ├── favicon.ico│ └── index.html├── src│ ├── components│ │ ├── Bread│ │ │ └── Bread.js│ │ └── SiderBar│ │ └── SiderBar.js│ ├── index.js│ ├── layouts│ │ └── BasicLayout.js│ ├── pages│ │ ├── Counter│ │ │ └── Counter.js│ │ └── Home│ │ └── Home.js│ ├── redux│ │ ├── actions│ │ │ └── counter.js│ │ ├── reducer.js│ │ ├── reducers│ │ │ └── counter.js│ │ └── store.js│ ├── request│ │ └── request.js│ ├── router│ │ └── Router.js│ └── util│ └── loadable.js└── yarn.lock新创建一个目录并初始化npm,在本地安装webpack,再安装webpack-cli>npm initThis utility will walk you through creating a package.json file.It only covers the most common items, and tries to guess sensible defaults.See npm help json for definitive documentation on these fieldsand exactly what they do.Use npm install &lt;pkg&gt; afterwards to install a package andsave it as a dependency in the package.json file.Press ^C at any time to quit.package name: (webpack4)version: (1.0.0)description:entry point: (index.js)test command:git repository:keywords:author:license: (ISC)About to write to /Users/xiaopingzhang/UCloud/webpack4/package.json:{ “name”: “webpack4”, “version”: “1.0.0”, “description”: “”, “main”: “index.js”, “scripts”: { “test”: “echo "Error: no test specified" && exit 1” }, “author”: “”, “license”: “ISC”}Is this OK? (yes) yes初始化之后按照提示一步步往下就可以了,可以输入该项目的描述等等信息。一开始也没有关系,后面也还可以更改。下一步 本地安装webpack,再安装webpack-clinpm install webpack webpack-cli –save-dev==–save-dev 是你开发时候依赖的东西,–save 是你发布之后还依赖的东西。==>npm install webpack webpack-cli –save-dev> fsevents@1.2.7 install /Users/xiaopingzhang/UCloud/webpack4/node_modules/fsevents> node installnode-pre-gyp WARN Using needle for node-pre-gyp https download[fsevents] Success: “/Users/xiaopingzhang/UCloud/webpack4/node_modules/fsevents/lib/binding/Release/node-v57-darwin-x64/fse.node” is installed via remote> webpack-cli@3.2.1 postinstall /Users/xiaopingzhang/UCloud/webpack4/node_modules/webpack-cli> lightercollective *** Thank you for using webpack-cli! ***Please consider donating to our open collective to help us maintain this package. https://opencollective.com/webpack/donate ***npm WARN webpack4@1.0.0 No descriptionnpm WARN webpack4@1.0.0 No repository field.+ webpack-cli@3.2.1+ webpack@4.29.0added 458 packages from 239 contributors and audited 5208 packages in 18.624sfound 0 vulnerabilities安装好之后,也会显示安装的哪个版本,一般安装没有啥问题。实在安装不成功,试一下全局安装。2.新建src文件夹,入口的js文件和html文件。.├── index.html├── package.json└── src └── index.jsindex.js文件const component = () => { let element = document.createElement(“div”); element.innerHTML = “webpackworks”; return element;};document.body.appendChild(component());index.html<!DOCTYPE html><html> <head> <title>Start</title> </head> <body> <script src="./dist/main.js"></script> </body></html>3.学会使用webpack编译文件输入 npx webpack>npx webpackHash: 9ad2a368debc9967c1f4Version: webpack 4.29.0Time: 269msBuilt at: 2019-01-27 21:15:22 Asset Size Chunks Chunk Namesmain.js 1.01 KiB 0 [emitted] mainEntrypoint main = main.js[0] ./src/index.js 218 bytes {0} [built]WARNING in configurationThe ‘mode’ option has not been set, webpack will fallback to ‘production’ for this value. Set ‘mode’ option to ‘development’ or ‘production’ to enable defaults for each environment.You can also set it to ’none’ to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/再用浏览器打开index.html,查看网页是否正常的显示了。webpack 把入口文件 index.js 经过处理之后,生成 main.js配置文件经过第一部分的尝试,已经初步了解webpack的作用,这一部分通过配置文件进行相应的一些设置。babelBabel 把用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本。 这一过程叫做“源码到源码”编译, 也被称为转换编译。npm install –save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-0新建babel配置文件.babelrc{ “presets”: [ “es2015”, “react”, “stage-0” ], “plugins”: []}//babel-core 调用Babel的API进行转码//babel-loader//babel-preset-es2015 用于解析 ES6//babel-preset-react 用于解析 JSX//babel-preset-stage-0 用于解析 ES7 提案新建配置文件webpack.base.conf.jswebpack.dev.conf.jswebpack.prod.conf.js分别是公共配置,开发配置,生产配置。目前目录结构为.├── build│ ├── webpack.base.conf.js│ ├── webpack.dev.conf.js│ └── webpack.prod.conf.js├── dist│ └── main.js├── index.html├── package.json└── src └── index.js加载js/jsx文件npm install –save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-0在 src 目录下新建.babelrc{ “presets”: [ [ “env”, { “targets”: { “browsers”: [">1%", “last 3 versions”] } } ], “stage-2”, “latest”, “react” ], “plugins”: [ “syntax-dynamic-import”, “transform-class-properties”, <!–[–> <!– “import”,–> <!– {–> <!– “libraryName”: “antd”,–> <!– “libraryDirectory”: “es”,–> <!– “style”: true–> // “style”: “css” //主题设置 <!– }–> <!–]–> 不用antd 可以去掉 ]}文件新增 { test: /.(js|jsx)$/, exclude: /(node_modules|bower_components)/, //排除 include: [ path.resolve(__dirname, ‘../src’) ], //包括 use: { loader: ‘babel-loader’ } },加载CSS文件npm install –save-dev style-loader css-loader在配置文件里添加 { test: /.css$/, use: [“style-loader”, “css-loader”] }加载图片npm install –save-dev url-loader file-loader在配置文件里添加 { test: /.(png|jpg|gif)$/, use: [ { loader: “url-loader”, options: { limit: 8192 } } ] }options limit:8192意思是,小于等于8K的图片会被转成base64编码,直接插入HTML中,减少HTTP请求。加载less在这个踩了一个坑,记得安装 lessnpm install –save-dev less-loader less更改antd 默认主题设置需要,不用的话应该把相应的设置忽略即可。 { test: /.less$/, use: [ { loader: ‘style-loader’ }, { loader: ‘css-loader’ // translates CSS into CommonJS }, { loader: ’less-loader’, // compiles Less to CSS options: { modifyVars: { ‘font-size-base’: ‘12px’, ‘primary-color’: ‘#0EA679’ }, javascriptEnabled: true } } ] }加载字体那么,像字体这样的其他资源如何处理呢?file-loader 和 url-loader 可以接收并加载任何文件,然后将其输出到构建目录。这就是说,我们可以将它们用于任何类型的文件,包括字体。更新 webpack.config.js 来处理字体文件: { test: /.(woff|woff2|eot|ttf|otf)$/, use: [“file-loader”] }增加HtmlWebpackPluginHtmlWebpackPlugin作用是生成一个HTML模板。HtmlWebpackPlugin简化了HTML文件的创建,以便为你的webpack包提供服务。这对于在文件名中包含每次会随着编译而发生变化哈希的 webpack bundle 尤其有用。你可以让插件为你生成一个HTML文件,使用lodash模板提供你自己的模板,或使用你自己的loader首先需要安装插件:npm install –save-dev html-webpack-plugin在生产配置文件里添加 plugins: [ new HtmlWebpackPlugin({ template: ‘public/index.html’, title: ’title’, // 更改HTML的title的内容 favicon: ‘public/favicon.ico’, minify: { removeComments: true, collapseWhitespace: true, removeAttributeQuotes: true, }, }),清理 /dist 文件夹在每次构建前清理 /dist 文件夹.npm install clean-webpack-plugin –save-devnew CleanWebpackPlugin([’../dist’])模块热替换https://webpack.docschina.org…模块热替换(Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新各种模块,而无需进行完全刷新。有两种方式一更改package.json"dev": “webpack –config build/webpack.dev.config.js –color –progress –hot"更改index.jsimport React from ‘react’;import ReactDom from ‘react-dom’;if (module.hot) { module.hot.accept();}//增加二更改配置文件const webpack = require(‘webpack’);devServer: { hot: true}plugins:[ new webpack.HotModuleReplacementPlugin()]reduxhttps://www.redux.org.cn/官方文档先给上,一开始学的时候也以为这个比较难,开始写就不会了。网上看看例子,自己在coding一下就差不多了。这边用到了一个中间件 redux-thunknpm install –save redux-thunk附上写的代码store注释的部分为生产环境使用。为了方便debug代码,在控制台打印readux日志。// import { createStore, applyMiddleware } from ‘redux’;// import thunk from ‘redux-thunk’;// import rootReducer from ‘./reducer’;// const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);// const store = createStoreWithMiddleware(rootReducer);// export default store;// 打印操作日志,方便调试,生产环境可以去掉,用上面注释的配置。import thunk from “redux-thunk”; // redux 作者开发的异步处理方案 可以在action 里传入 dispatch getStateimport { createLogger } from “redux-logger”; // 利用redux-logger打印日志import { createStore, applyMiddleware } from “redux”; // 引入redux createStore、中间件及composeimport { composeWithDevTools } from “redux-devtools-extension”; // devToolsEnhancer,import reducer from “./reducer”; // 引入reducers集合// 调用日志打印方法 collapsed是让action折叠,看着舒服点const loggerMiddleware = createLogger({ collapsed: true });// 创建一个中间件集合const middleware = [thunk, loggerMiddleware];// 创建storeconst store = createStore( reducer, composeWithDevTools(applyMiddleware(…middleware)));export default store;actionexport const INCREMENT = ‘counter/INCREMENT’;export const DECREMENT = ‘counter/DECREMENT’;export const RESET = ‘counter/RESET’;export function increment() { return { type: INCREMENT };}export function decrement() { return { type: DECREMENT };}export function reset() { return { type: RESET };}reducer每个页面的reduce文件import { INCREMENT, DECREMENT, RESET } from ‘../actions/counter’;const initState = { count: 0,};export default function reducer(state = initState, action) { switch (action.type) { case INCREMENT: return { count: state.count + 1, }; case DECREMENT: return { count: state.count - 1, }; case RESET: return { count: 0 }; default: return state; }}redecers 整合所有文件的reducerimport { combineReducers } from “redux”;import counter from “./reducers/counter”;export default combineReducers({ counter});react-loadablehttps://github.com/jamiebuild…官方文档先附上// 加载页面import Loadable from ‘react-loadable’;import React, { Component } from ‘react’;import { Spin, Icon } from ‘antd’;const antIcon = <Icon type=“loading” style={{ fontSize: 24 }} spin />;const antLong = ( <Icon type=“loading” style={{ fontSize: 24, color: ‘red’ }} spin />);const antError = ( <Icon type=“loading” style={{ fontSize: 24, color: ‘red’ }} spin />);export const Loading = props => { if (props.error) { return ( <Spin size=“large” tip=“加载错误 。。。” indicator={antError} style={{ position: ‘absolute’, color: ‘red’, top: ‘40%’, left: ‘50%’ }} /> ); } else if (props.timedOut) { return ( <Spin size=“large” tip=“加载超时 。。。” indicator={antLong} style={{ position: ‘absolute’, color: ‘red’, top: ‘40%’, left: ‘50%’ }} /> ); } else if (props.pastDelay) { return ( <Spin size=“large” tip=“Loading 。。。” indicator={antError} style={{ position: ‘absolute’, color: ‘red’, top: ‘40%’, left: ‘50%’ }} /> ); } else { return null; }};export const importPath = ({ loader }) => { return Loadable({ loader, loading: Loading, delay: 200, timeout: 10000 });};axios 统一拦截所有的请求和返回数据在需要用到的地方引入这个文件就ok了。只是简单的写了一个例子,后续再完善吧。axios使用起来很简洁。import axios from “axios”;import { message } from “antd”;import NProgress from “nprogress”;import “nprogress/nprogress.css”;// 拦截所有有请求与回复// Add a request interceptoraxios.interceptors.request.use( config => { NProgress.start(); return config; }, error => { message.error(“请求错误,请重试”); return Promise.reject(error); });// Add a response interceptoraxios.interceptors.response.use( response => { // NProgress.done(); // if (response.data.RetCode === 101) { // message.error(response.data.Message); // return response; // } // if (response.data.RetCode === 100) { // message.error(response.data.Message); // return response; // } return response; }, error => { message.error(“请求错误,请重试”); NProgress.done(); return Promise.reject(error); });export default request;公共路径(public path)插件配置 plugins: [ // 处理html new HtmlWebpackPlugin({ template: ‘public/index.html’, path: ‘../public/index.html’, inject: ‘body’, title: ‘管理平台’, favicon: ‘public/favicon.ico’, filename: ‘index.html’, hash: true, minify: { html5: true, removeComments: true, collapseWhitespace: true, removeAttributeQuotes: true } }), new CleanWebpackPlugin([’../dist’], { allowExternal: true }), new BundleAnalyzerPlugin(), new MiniCssExtractPlugin({ chunkFilename: ‘[chunkhash].css’ }), new webpack.HashedModuleIdsPlugin(), new webpack.DllReferencePlugin({ context: __dirname, manifest: require(’../dll/manifest.json’) }), new CopyWebpackPlugin([ { from: ‘dll/Dll.js’, to: DIST_PATH } ]) ]html-webpack-pluginconst HtmlWebpackPlugin = require(‘html-webpack-plugin’);new HtmlWebpackPlugin({ template: ‘public/index.html’, path: ‘../public/index.html’, inject: ‘body’, title: ‘管理平台’, favicon: ‘public/favicon.ico’, filename: ‘index.html’, hash: true, minify: { html5: true, removeComments: true, collapseWhitespace: true, removeAttributeQuotes: true } }),copy-webpack-pluginconst CopyWebpackPlugin = require(‘copy-webpack-plugin’);new CopyWebpackPlugin([ { from: ‘dll/Dll.js’, to: DIST_PATH } ])clean-webpack-pluginconst CleanWebpackPlugin = require(‘clean-webpack-plugin’);new CleanWebpackPlugin([’../dist’], { allowExternal: true })webpack-bundle-analyzerconst BundleAnalyzerPlugin = require(‘webpack-bundle-analyzer’) .BundleAnalyzerPlugin; new BundleAnalyzerPlugin(), mini-css-extract-pluginconst MiniCssExtractPlugin = require(‘mini-css-extract-plugin’); new MiniCssExtractPlugin({ chunkFilename: ‘[chunkhash].css’ }) 附上三个配置文件webpack.dev.conf.jsconst path = require(‘path’);const webpack = require(‘webpack’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);const CopyWebpackPlugin = require(‘copy-webpack-plugin’);const DIST_PATH = path.resolve(__dirname, ‘../dist’); //生产目录const APP_PATH = path.resolve(__dirname, ‘../src’); //源文件目录module.exports = { mode: ‘development’, entry: { index: ‘./src/index.js’ }, output: { path: DIST_PATH, //出口路径 filename: ‘index.js’, chunkFilename: ‘js/[name].[chunkhash].js’, //按需加载名称 // publicPath: “./” }, // 源错误检查 devtool: ‘inline-source-map’, //模块配置 module: { rules: [ { test: /.(js|jsx)$/, exclude: /(node_modules|bower_components)/, //排除 include: [ path.resolve(__dirname, ‘../src’), path.resolve(__dirname, ‘../node_modules/antd/’) ], //包括 use: { loader: ‘babel-loader’ } }, { test: /.css$/, use: [‘style-loader’, ‘css-loader’] }, { test: /.(png|jpg|gif)$/, use: [ { loader: ‘url-loader’, options: { limit: 8192 } } ] }, { test: /.(woff|woff2|eot|ttf|otf)$/, use: [‘file-loader’] }, //更改antd主题设置 { test: /.less$/, use: [ { loader: ‘style-loader’ }, { loader: ‘css-loader’ // translates CSS into CommonJS }, { loader: ’less-loader’, // compiles Less to CSS options: { modifyVars: { ‘font-size-base’: ‘12px’, ‘primary-color’: ‘#0EA679’ }, javascriptEnabled: true } } ] } ] }, //插件 plugins: [ new HtmlWebpackPlugin({ template: ‘public/index.html’, path: ‘../public/index.html’, inject: ‘body’, favicon: ‘public/favicon.ico’, title: ‘管理平台’, overlay: true, minify: { html5: false }, hash: true }), // 热更新 new webpack.HotModuleReplacementPlugin(), new webpack.HashedModuleIdsPlugin(), new webpack.DllReferencePlugin({ context: __dirname, manifest: require(’../dll/manifest.json’) }), new CopyWebpackPlugin([ { from: ‘dll/Dll.js’, to: DIST_PATH } ]) ], // 热更新 devServer: { port: ‘3300’, contentBase: DIST_PATH, historyApiFallback: true, hot: true, // 开启 https: false, compress: false, noInfo: true, open: true, proxy: { // ‘/’: { // target: ‘’, // changeOrigin: true, // secure: false, // }, } }};webpack.dll.conf.jsconst path = require(‘path’);const webpack = require(‘webpack’);const CleanWebpackPlugin = require(‘clean-webpack-plugin’);const vendors = [ ‘antd’, ‘axios’, ’nprogress’, ‘react’, ‘react-dom’, ‘react-loadable’, ‘react-redux’, ‘react-router’, ‘react-router-dom’, ‘redux’];module.exports = { entry: { vendor: vendors }, output: { path: path.resolve(__dirname, ‘../dll’), filename: ‘Dll.js’, library: ‘[name][hash]’ }, plugins: [ new webpack.DllPlugin({ path: path.resolve(__dirname, ‘../dll’, ‘manifest.json’), name: ‘[name][hash]’, context: __dirname }), new CleanWebpackPlugin([’../dll’], { allowExternal: true }) ]};webpack.prod.conf.jsconst path = require(‘path’);const webpack = require(‘webpack’);const CopyWebpackPlugin = require(‘copy-webpack-plugin’);const CleanWebpackPlugin = require(‘clean-webpack-plugin’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);const MiniCssExtractPlugin = require(‘mini-css-extract-plugin’);const BundleAnalyzerPlugin = require(‘webpack-bundle-analyzer’) .BundleAnalyzerPlugin;const DIST_PATH = path.resolve(__dirname, ‘../dist’); //生产目录module.exports = { mode: ‘production’, entry: { index: ‘./src/index.js’ }, output: { path: DIST_PATH, //出口路径 filename: ‘index.js’, chunkFilename: ‘[name]_[hash].js’, //按需加载名称 // publicPath: ‘./’ }, // 源错误检查 devtool: ‘source-map’, //模块配置 module: { rules: [ { test: /.(js|jsx)$/, exclude: /(node_modules|bower_components)/, //排除 include: [ path.resolve(__dirname, ‘../src’), path.resolve(__dirname, ‘../node_modules/antd/’) ], //包括 use: { loader: ‘babel-loader’ } }, { test: /.css$/, use: [‘style-loader’, ‘css-loader’] }, { test: /.(png|jpg|gif)$/, use: [ { loader: ‘url-loader’, options: { limit: 8192 } } ] }, //更改antd主题设置 { test: /.less$/, use: [ { loader: ‘style-loader’ }, { loader: ‘css-loader’ // translates CSS into CommonJS }, { loader: ’less-loader’, // compiles Less to CSS options: { minimize: true, modifyVars: { ‘font-size-base’: ‘12px’, ‘primary-color’: ‘#0EA679’ }, javascriptEnabled: true } } ] }, { test: /.(woff|woff2|eot|ttf|otf)$/, use: [‘file-loader’] } ] }, //插件 plugins: [ // 处理html new HtmlWebpackPlugin({ template: ‘public/index.html’, path: ‘../public/index.html’, inject: ‘body’, title: ‘管理平台’, favicon: ‘public/favicon.ico’, filename: ‘index.html’, hash: true, minify: { html5: true, removeComments: true, collapseWhitespace: true, removeAttributeQuotes: true } }), new CleanWebpackPlugin([’../dist’], { allowExternal: true }), new BundleAnalyzerPlugin(), new MiniCssExtractPlugin({ chunkFilename: ‘[chunkhash].css’ }), new webpack.HashedModuleIdsPlugin(), new webpack.DllReferencePlugin({ context: __dirname, manifest: require(’../dll/manifest.json’) }), new CopyWebpackPlugin([ { from: ‘dll/Dll.js’, to: DIST_PATH } ]) ] // 热更新};学习过程中的踩坑生产环境打包报错ERROR in Path must be a string. Received undefinedChild html-webpack-plugin for “index.html”: 1 asset Entrypoint undefined = index.html 这个错误不影响打包结果,应该是版本问题导致。https://github.com/jantimon/h…写完才发现有些忘记记录了,会保持更新。学习的过程中也学习参考了其他优秀的博客和github,以及文档。https://github.com/brickspert…https://github.com/NewPrototy…https://github.com/axios/axioshttps://github.com/jamiebuild…https://www.webpackjs.com/con… ...

January 31, 2019 · 9 min · jiezi

ES6 export 和 export default的区别

ES6中 export 和 export default 与 import使用的区别,使用 react native 代码详解一、使用export 和 import1、export 定义导出一个子组件 Greetingimport React, { Component } from “react”;import { View, Text } from “react-native”;export class Greeting extends Component { render() { return( <View> <Text>{this.props.name}</Text> <View> ) }}2、在父组件中导入子组件import React, { Component } from “react”;import { View, Text } from “react-native”;// greeting文件存储在src目录下import { Greeting } from “./src/greeting”;import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(greeting.js)对外接口的名称Greeting相同。如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名import { bieming as Greeting } from “./src/greeting”;3、export default 场景:从前面的例子可以看出,使用import命令的时候,用户需要知道所要加载的变量名,否则无法加载。但是用户肯定不愿意去阅读子组件看看导出名称叫啥,然后回来导入,所以就有了 export default。import React, { Component } from “react”;import { View, Text } from “react-native”;export default class Greeting extends Component { render() { return( <View> <Text>{this.props.name}</Text> <View> ) }}4、import 导入模块import React, { Component } from “react”;import { View, Text } from “react-native”;// greeting文件存储在src目录下import Greeting from “./src/greeting”;// 或者import AnyName from “./src/greeting”;上面代码的import命令,可以用任意名称指向greeting.js输出的方法,这时就不需要知道原模块输出的变量名。需要注意的是,这时import命令后面,不使用大括号。总结:现在流行的前端框架,angular+ 主要使用 export 导出模块,react native 中使用 export default 导出模块,如今编辑器非常强大,安装插件会自动弹出模块名称,知道其导出怎么使用就可以了 ...

January 30, 2019 · 1 min · jiezi

JavaScript 是如何工作的:编写自己的 Web 开发框架 + React 及其虚拟 DOM 原理

这是专门探索 JavaScript 及其所构建的组件的系列文章的第 19 篇。如果你错过了前面的章节,可以在这里找到它们:JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述!JavaScript 是如何工作的:深入V8引擎&编写优化代码的5个技巧!JavaScript 是如何工作的:内存管理+如何处理4个常见的内存泄漏!JavaScript 是如何工作的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式!JavaScript 是如何工作的:深入探索 websocket 和HTTP/2与SSE +如何选择正确的路径!JavaScript 是如何工作的:与 WebAssembly比较 及其使用场景!JavaScript 是如何工作的:Web Workers的构建块+ 5个使用他们的场景!JavaScript 是如何工作的:Service Worker 的生命周期及使用场景!JavaScript 是如何工作的:Web 推送通知的机制!JavaScript 是如何工作的:使用 MutationObserver 跟踪 DOM 的变化!JavaScript 是如何工作的:渲染引擎和优化其性能的技巧!JavaScript 是如何工作的:深入网络层 + 如何优化性能和安全!JavaScript 是如何工作的:CSS 和 JS 动画底层原理及如何优化它们的性能!JavaScript 是如何工作的:解析、抽象语法树(AST)+ 提升编译速度5个技巧!JavaScript 是如何工作的:深入类和继承内部原理+Babel和 TypeScript 之间转换!JavaScript 是如何工作的:存储引擎+如何选择合适的存储API!JavaScript 是如何工作的:Shadow DOM 的内部结构+如何编写独立的组件!JavaScript 是如何工作的:WebRTC 和对等网络的机制!响应式原理Proxy 允许我们创建一个对象的虚拟代理(替代对象),并为我们提供了在访问或修改原始对象时,可以进行拦截的处理方法(handler),如 set()、get() 和 deleteProperty() 等等,这样我们就可以避免很常见的这两种限制(vue 中):添加新的响应性属性要使用 Vue.$set(),删除现有的响应性属性要使用数组的更新检测Proxylet proxy = new Proxy(target, habdler);target:用 Proxy 包装的目标对象(可以是数组对象,函数,或者另一个代理)handler:一个对象,拦截过滤代理操作的函数实例方法方法 描述 handler.apply() 拦截 Proxy 实例作为函数调用的操作 handler.construct() 拦截 Proxy 实例作为函数调用的操作 handler.defineProperty() 拦截 Object.defineProperty() 的操作 handler.deleteProperty() 拦截 Proxy 实例删除属性操作 handler.get() 拦截 读取属性的操作 handler.set() 截 属性赋值的操作 handler.getOwnPropertyDescriptor() 拦截 Object.getOwnPropertyDescriptor() 的操作 handler.getPrototypeOf() 拦截 获取原型对象的操作 handler.has() 拦截 属性检索操作 handler.isExtensible() 拦截 Object.isExtensible() 操作 handler.ownKeys() 拦截 Object.getOwnPropertyDescriptor() 的操作 handler.preventExtension() 截 Object().preventExtension() 操作 handler.setPrototypeOf() 拦截Object.setPrototypeOf()操作 Proxy.revocable() 创建一个可取消的 Proxy 实例 ReflectReflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与处理器对象的方法相同。Reflect不是一个函数对象,因此它是不可构造的。与大多数全局对象不同,Reflect没有构造函数。你不能将其与一个new运算符一起使用,或者将Reflect对象作为一个函数来调用。Reflect的所有属性和方法都是静态的(就像Math对象)。为什么要设计 Reflect ?1. 更加有用的返回值早期写法:try { Object.defineProperty(target, property, attributes); // success} catch (e) { // failure}Reflect 写法:if (Reflect.defineProperty(target, property, attributes)) { // success} else { // failure}2. 函数式操作早期写法:’name’ in Object //trueReflect 写法:Reflect.has(Object,’name’) //true3. 可变参数形式的构造函数 一般写法:var obj = new F(…args)Reflect 写法:var obj = Reflect.construct(F, args)当然还有很多,大家可以自行到 MND 上查看什么是代理设计模式代理模式(Proxy),为其他对象提供一种代理以控制对这个对象的访问。代理模式使得代理对象控制具体对象的引用。代理几乎可以是任何对象:文件,资源,内存中的对象,或者是一些难以复制的东西。现实生活中的一个类比可能是银行账户的访问权限。例如,你不能直接访问银行帐户余额并根据需要更改值,你必需向拥有此权限的人(在本例中 你存钱的银行)询问。var account = { balance: 5000}var bank = new Proxy(account, { get: function (target, prop) { return 9000000; }});console.log(account.balance); // 5,000 console.log(bank.balance); // 9,000,000 console.log(bank.currency); // 9,000,000 在上面的示例中,当使用 bank 对象访问 account 余额时,getter 函数被重写,它总是返回 9,000,000 而不是属性值,即使属性不存在。var bank = new Proxy(account, { set: function (target, prop, value) { // Always set property value to 0 return Reflect.set(target, prop, 0); }});account.balance = 5800;console.log(account.balance); // 5,800bank.balance = 5400;console.log(account.balance); // 0通过重写 set 函数,可以修改其行为。可以更改要设置的值,更改其他属性,甚至根本不执行任何操作。响应式现在已经对代理设计模式的工作方式有了基本心,让就开始编写 JavaScript 框架吧。为了简单起见,将模拟 AngularJS 语法。声明控制器并将模板元素绑定到控制器属性:<div ng-controller=“InputController”> <!– “Hello World!” –> <input ng-bind=“message”/> <input ng-bind=“message”/></div><script type=“javascript”> function InputController () { this.message = ‘Hello World!’; } angular.controller(‘InputController’, InputController);</script>首先,定义一个带有属性的控制器,然后在模板中使用这个控制器。最后,使用 ng-bind 属性启用与元素值的双向绑定。解析模板并实例化控制器要使属性绑定,需要获得一个控制器来声明这些属性, 因此,有必要定义一个控制器并将其引入框架中。在控制器声明期间,框架将查找带有 ng-controller 属性的元素。如果它符合其中一个已声明的控制器,它将创建该控制器的新实例,这个控制器实例只负责这个特定的模板。var controllers = {};var addController = function (name, constructor) { // Store controller constructor controllers[name] = { factory: constructor, instances: [] }; // Look for elements using the controller var element = document.querySelector(’[ng-controller=’ + name + ‘]’); if (!element){ return; // No element uses this controller } // Create a new instance and save it var ctrl = new controllers[name].factory; controllers[name].instances.push(ctrl); // Look for bindings…..};addController(‘InputController’, InputController);这是手动处理的控制器变量声明。 controllers 对象包含通过调用 addController 在框架内声明的所有控制器。对于每个控制器,保存一个 factory 函数,以便在需要时实例化一个新控制器,该框架还存储模板中使用的相同控制器的每个新实例。查找 bind 属性现在,已经有了控制器的一个实例和使用这个实例的一个模板,下一步是查找具有使用控制器属性的绑定的元素。 var bindings = {}; // Note: element is the dom element using the controller Array.prototype.slice.call(element.querySelectorAll(’[ng-bind]’)) .map(function (element) { var boundValue = element.getAttribute(’ng-bind’); if(!bindings[boundValue]) { bindings[boundValue] = { boundValue: boundValue, elements: [] } } bindings[boundValue].elements.push(element); });上述中,它存储对象的所有绑的值定。该变量包含要与当前值绑定的所有属性和绑定该属性的所有 DOM 元素。双向绑定在框架完成了初步工作之后,接下就是有趣的部分:双向绑定。它涉及到将 controller 属性绑定到 DOM 元素,以便在代码更新属性值时更新 DOM。另外,不要忘记将 DOM 元素绑定到 controller 属性。这样,当用户更改输入值时,它将更新 controller 属性,接着,它还将更新绑定到此属性的所有其他元素。使用代理检测代码的更新如上所述,Vue3 组件中通过封装 proxy 监听响应属性更改。 这里仅为控制器添加代理来做同样的事情。// Note: ctrl is the controller instancevar proxy = new Proxy(ctrl, { set: function (target, prop, value) { var bind = bindings[prop]; if(bind) { // Update each DOM element bound to the property bind.elements.forEach(function (element) { element.value = value; element.setAttribute(‘value’, value); }); } return Reflect.set(target, prop, value); }});每当设置绑定属性时,代理将检查绑定到该属性的所有元素,然后用新值更新它们。在本例中,我们只支持 input 元素绑定,因为只设置了 value 属性。响应事件最后要做的是响应用户交互,DOM 元素在检测到值更改时触发事件。监听这些事件并使用事件的新值更新绑定属性,由于代理,绑定到相同属性的所有其他元素将自动更新。Object.keys(bindings).forEach(function (boundValue) { var bind = bindings[boundValue]; // Listen elements event and update proxy property bind.elements.forEach(function (element) { element.addEventListener(‘input’, function (event) { proxy[bind.boundValue] = event.target.value; // Also triggers the proxy setter }); }) });React && Virtual DOM接着将学习了解决如何使用单 个HTML 文件运行 React,解释这些概念:functional component,函数组件, JSX 和 Virtual DOM。React 提供了用组件构建代码的方法,收下,创建 watch 组 件。<!– Skipping all HTML5 boilerplate –><script src=“https://unpkg.com/react@16.2.0/umd/react.development.js"></script><script src=“https://unpkg.com/react-dom@16.2.0/umd/react-dom.development.js"></script><!-- For JSX support (with babel) –><script src=“https://unpkg.com/babel-standalone@6.24.2/babel.min.js" charset=“utf-8”></script> <div id=“app”></div> <!– React mounting point–><script type=“text/babel”> class Watch extends React.Component { render() { return <div>{this.props.hours}:{this.props.minutes}</div>; } } ReactDOM.render(<Watch hours=“9” minutes=“15”/>, document.getElementById(‘app’));</script>忽略依赖项的 HTML 样板和脚本,剩下的几行就是 React 代码。首先,定义 Watch 组件及其模板,然后挂载React 到 DOM中,来渲染 Watch 组件。向组件中注入数据我们的 Wacth 组件很简单 ,它只展示我们传给它的时和分钟。你可以尝试修改这些属性的值(在 React中称为 props )。它将最终显示你传给它的内容,即使它不是数字。const Watch = (props) => <div>{props.hours}:{props.minutes}</div>;ReactDOM.render(<Watch hours=“Hello” minutes=“World”/>, document.getElementById(‘app’));props 只是通过周围组件传递给组件的数据,组件使用 props 进行业务逻辑和呈现。但是一旦 props 不属于组件,它们就是不可变的(immutable)。因此,提供 props 的组件是能够更新props 值的唯一代码。使用 props 非常简单,使用组件名称作为标记名称创建 DOM 节点。 然后给它以 props 名的属性,接着通过组件中的 this.props 可以获得传入的值。那些不带引号的 HTML 呢?注意到 render 函数返回的不带引号的 HTML, 这个使用是 JSX 语法,它是在 React 组件中定义 HTML 模板的简写语法。// Equivalent to JSX: <Watch hours=“9” minutes=“15”/>React.createElement(Watch, {‘hours’: ‘9’, ‘minutes’: ‘15’});现在你可能希望避免使用 JSX 来定义组件的模板,实际上,JSX 看起来像 语法糖。以下代码片段,分别使用 JSX 和 React 语法以构建相同结果。// Using JS with React.createElementReact.createElement(‘form’, null, React.createElement(‘div’, {‘className’: ‘form-group’}, React.createElement(’label’, {‘htmlFor’: ’email’}, ‘Email address’), React.createElement(‘input’, {’type’: ’email’, ‘id’: ’email’, ‘className’: ‘form-control’}), ), React.createElement(‘button’, {’type’: ‘submit’, ‘className’: ‘btn btn-primary’}, ‘Submit’))// Using JSX<form> <div className=“form-group”> <label htmlFor=“email”>Email address</label> <input type=“email” id=“email” className=“form-control”/> </div> <button type=“submit” className=“btn btn-primary”>Submit</button></form>进一步探索虚拟 DOM最后一部分比较复杂,但是很有趣,这将帮助你了解 React 底层的原理。更新页面上的元素 (DOM树中的节点) 涉及到使用 DOM API。它将重新绘制页面,但可能很慢(请参阅本文了解原因)。许多框架,如 React 和 Vue.js 绕过了这个问题,它们提出了一个名为虚拟 DOM 的解决方案。{ “type”:“div”, “props”:{ “className”:“form-group” }, “children”:[ { “type”:“label”, “props”:{ “htmlFor”:“email” }, “children”:[ “Email address”] }, { “type”:“input”, “props”:{ “type”:“email”, “id”:“email”, “className”:“form-control”}, “children”:[] } ]}想法很简单。读取和更新 DOM 树非常昂贵。因此,尽可能少地进行更改并更新尽可能少的节点。减少对 DOM API 的调用及将 DOM 树结构保存在内存中, 由于讨论的是 JavaScript 框架,因此选择JSON 数据结构比较合理。这种处理方式会立即展示了虚拟 DOM 中的变化。此外虚拟 DOM 会先缓存一些更新操作,以便稍后在真正 DOM 上渲染,这个样是为了频繁操作重新渲染造成一些性能问题。你还记得 React.createElement 吗? 实际上,这个函数作用是 (直接调用或通过 JSX 调用) 在 Virtual DOM 中 创建一个新节点。要应用更新,Virtual DOM核心功能将发挥作用,即 协调算法,它的工作是提供最优的解决方案来解决以前和当前虚拟DOM 状态之间的差异。原文:https://medium.freecodecamp.o…https://medium.freecodecamp.o…代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》! ...

January 29, 2019 · 4 min · jiezi

JavaScript ES6 async/await的简单学习demo

传统回调函数// demo1-callback.js/** 现在我们要做个事情,写个回调函数,每秒输出一个递增的数字,输出三次 普通回调函数的写法 /function logNumber(n, callback){ setTimeout(() => { console.log(n); n++; callback(n) }, 1000);}// 现在调用它logNumber(1, function(n){ logNumber(n, function(m){ logNumber(m, function(q){ }) })})Promise// demo2-promise.js/* 现在我们改用promise来重写demo1的函数 /// 我们在这里暴露那个promise以供demo3调用function generatorLogNumber(n){ return new Promise(res => { setTimeout(() => { console.log(n); n++; res(n) }, 1000); })}// 现在使用它generatorLogNumber(1) .then(n => { generatorLogNumber(n) .then(m => { generatorLogNumber(m) .then(q => { }) }) })// 这里把这个promise暴露出去以供demo3使用,记得把本demo的调用函数注释掉(就是15-24行注释掉)module.exports = generatorLogNumber;async/await// demo3-async-await.js/* 现在我们改用更加方便的async/await方式来调用demo2的promise */// 首先把那个promise引入进来const generatorLogNumber = require(’./demo2-promise.js’);(async () => {//双括号表示立即执行的匿名函数 const n = await generatorLogNumber(1); const m = await generatorLogNumber(n); const q = await generatorLogNumber(m);})()// 可以node demo3-async-await.js 来运行看看 ...

January 29, 2019 · 1 min · jiezi

温故而知新:JS 变量提升与时间死区

开始执行脚本时,执行脚本的第一步是编译代码,然后再开始执行代码,如图另外,在编译优化方面来说,最开始时也并不是全部编译好脚本,而是当函数执行时,才会先编译,再执行脚本,如图编译阶段:经历了词法分析,语法分析生成AST,以及代码生成。并且在此阶段,它只会扫描并且抽出环境中的声明变量,声明函数以便准备分配内存,所有的函数声明和变量声明都会被添加到名为Lexical Environment的JavaScript内部数据结构内的内存中。因此,它们可以在源代码中实际声明之前使用。但是,Javascript只会存储函数声明和变量声明在内存,并不会存储他们的值执行阶段:给变量x赋值,首先询问内存你这有变量x吗,如果有,则给变量x赋值,如果没有则创建变量x并且给它赋值。变量提升如下图,左边灰色块区域,是演示函数执行前的编译阶段,先抽出所有声明变量和声明函数,并进行内存分配。然后再开始执行代码,在执行第一行代码的时候,若是变量a存在于内存中,则直接给变量a赋值。而执行到第二行时,变量b并没有在内存中,则会创建变量b并给它赋值。Lexical enviroment是一种包含标识符变量映射的数据结构LexicalEnviroment = { Identifier: <value>, Indentifier: <function object>}简而言之,Lexical enviroment就是程序执行过程中变量和函数存在的地方。let,const变量console.log(a)let a = 3;输出ReferenceError: a is not defined所以let和const变量并不会被提升吗?这个答案会比较复杂。所有的声明(function, var, let, const and class)在JavaScript中都会被提升,然而var声明被undefined值初始化,但是let和const声明的值仍然未被初始化。它们仅仅只在Javascript引擎运行期间它们的词法绑定被执行在才会被初始化。这意味着引擎在源代码中声明它的位置计算其值之前,你无法访问该变量。这就是我们所说的时间死区,即变量创建和初始化之间的时间,我们无法访问该变量。如果JavaScript引擎仍然无法在声明它们的行中找到let或者const的值,它将为它们分配undefined值或返回错误值(在const的情况下会返回错误值)。6a9a50532bf60f5fac6b3c.png](evernotecid://F2BCA3B5-CC5A-4EB3-BD61-DD865800F342/appyinxiangcom/10369121/ENResource/p1163)let a;console.log(a); // outputs undefineda = 5;在编译阶段,JavaScript引擎遇到变量a并将它存储在lexical enviroment,但是因为它是一个let变量,所以引擎不会为它初始化任何值。所以,在编译阶段,lexical enviroment看起来像下面这样。// 编译阶段lexicalEnvironment = { a: <uninitialized>}现在如果我们尝试在声明它之前访问该变量,JavaScript引擎将会尝试从词法环境中拿到这个变量的值,因为这个变量未被初始化,它将抛出一个引用错误。在执行期间,当引擎到达了变量声明的行,它将试图执行它的绑定,因为该变量没有与之关联的值,因此它将为其赋值为unedfined// 执行阶段lexicalEnviroment = { a: undefined}之后,undefined将会被打印到控制台,然后将值5赋值给变量a,lexical enviroment中变量a的值也会从undefined更新为5functionn foo() { console.log(a)}let a = 20;foo(); function foo() { console.log(a): // ReferenceError: a is not defined}foo();let a = 20;Class Declaration就像let和const声明一样,class在JavaScript中也会被提升,并且和let,const一样,知道执行之前,它们都会保持uninitialized。因此它们同样会受到Temporal Deal Zone(时间死区)的影响。例如let peter = new Person(‘Peter’, 25); // ReferenceError: Person is not definedconsole.log(peter);class Person { constructor(name, age) { this.name = name; this.age = age; }}因此要访问class,必须先声明它class Person { constructor(name, age) { this.name = name; this.age = age; }}let peter = new Person(‘Peter’, 25); console.log(peter);// Person { name: ‘Peter’, age: 25 }所以在编译阶段,上面代码的lexical environment(词法环境)将如下所示:lexicalEnvironment = { Person: <uninitialized>}当引擎执行class声明时,它将使用值初始化类。lexicalEnvironment = { Person: <Person object>}提升Class Expressionslet peter = new Person(‘Peter’, 25);console.log(peter);let Person = class { constructor(name, age) { this.name = name; this.age = age; }}let peter = new Person(‘Peter’, 25); console.log(peter);var Person = class { constructor(name, age) { this.name = name; this.age = age; }}所以现在我们知道在提升过程中我们的代码并没有被JavaScript引擎实际移动。正确理解提升机制将有助于避免因变量提升而产生的任何未来错误和混乱。为了避免像未定义的变量或引用错误一样可能产生的副作用,请始终尝试将变量声明在各自作用域的顶部,并始终尝试在声明变量时初始化变量。Hoisting in Modern JavaScript — let, const, and var ...

January 27, 2019 · 1 min · jiezi

从0开始使用webpack搭建react工作流

很多人想搭建一套属于自己的前端工作流,最开始的时候我们的工作流萌芽是从写一个项目的时候,拷贝以前写过的一个项目文件夹改完直接使用开始的,后来使用了grunt和gulp,再到webpack,每一个前端人员想掌握如何书写一个符合自己项目的工作流,以便复用,达到高效工作的目的。另外一方面,即使不是想搭建自己的工作流,而是使用现成的脚手架,大家都会用。vue init webpack 项目名跑一下vue官方的例子,但是实际工作的时候,文件夹结构一变,或者组件的倒入和导出和官方例子不一致,就彻底不会了。很多人会vue也仅仅限于能跑起来vue官方的例子,或者在它的基础上复制,但是深度的定制以符合实际生产环境,是很多人不会的,甚至连改一个图片的路径都搞不定,明显这样的“会”就相当于你知道蜡烛是用来照明的,但是在冬天的夜里,你冻得直哆嗦,旁边有一堆木头,你却不能用蜡烛引燃木料取暖一样。更简单直接的说法,就是,鹦鹉学舌而已。显然,彻底掌握如何从零开始搭建一个能够贴近实际项目的工作流,是一个想要满足工作最基本要求的人必备的技能。今天我们就来实现它。我们通过从零开始实现一个react开发环境的脚手架,让大家能够彻底的掌握如何深度定制vue、react和angular项目的能力,同时能够让大家慢慢的形成自己的一套工作流,大幅度提高工作效率。OK,开始吧。1.我们新建一个文件夹,helloworld.2.我们进入文件夹,初始化项目。npm init3.安装webpack。npm i webpack –save-dev为什么用webpack?因为现在公司基本都用它。我们使用webpack 4.29.0,也就是最新版,因为最新版本配置起来最容易,功能也最强大。4.安装Webpack命令行工具,webpack-cli。 为什么要装它?因为webpack其实配置起来挺麻烦的,用它稍微好点儿。npm i webpack-cli –save-dev5.打开package.json,添加一句:“build”: “webpack"报错了,人家提示的特别到位,说你没有入口文件,人家缺啥你就补啥就行了。index.js里面随便写点啥:console.log(‘大彬哥666’);再跑一遍:npm run buildok,很美好。ng](/img/bVbnMEu)ok,game over.有同学可能会说,等会儿,老师,你这个咋跟我学过的不一样,不得配置入口文件和输出文件吗?并!不!需!要!那是你没遇见我,你早遇见我,你早就不配置了。6.我们确实可以打包了,但是这样好像还是不行啊,我们通常情况下分为开发环境和生产环境,现在这样怪怪的。没关系马上就满足你的需求,解决你的难言之隐,让你找回男人的尊严。我们搞一把开发模式和生产模式,一图抵万言:“dev”: “webpack –mode development”,“build”: “webpack –mode production"我们回到gitbash里面,我们走一个npm run dev很好,直接就给搬到dist文件夹了,但是我们想上线肯定得是压缩的:npm run build搞定鸟。又有人说了,老师我们公司项目不是用的默认入口和输出,咋办,我们公司比较崇拜你,所有的文件都是用dabinge666文件夹包一层的(下面可以不做直接看6)。“dev”: “webpack –mode development ./dabinge666/src/js/index.js –output ./dabinge666/main.js”,“build”: “webpack –mode production ./dabinge666/src/js/index.js –output ./dabinge666/main.js"信彬哥,无bug.7.配置完了webpack打包这块,我们想写代码都时候用ES6或者ES7,因为这两个装起B来666.也好搞,先装babel加载器npm i @babel/core babel-loader @babel/preset-env –save-dev然后配置, “dev”: “webpack –mode development –module-bind js=babel-loader”, “build”: “webpack –mode production –module-bind js=babel-loader"最后:npm run build 打开main.js,已经编译了。8.好,我们开始再把B格提升一个档次,我们玩玩react.首先装reactnpm i react react-dom –save-dev然后装babel-preset-reactnpm i @babel/preset-react –save-dev新建 .babelrc,输入,{ “presets”: ["@babel/preset-env”, “@babel/preset-react”]}新建一个webpack.config.js,输入module.exports = { module: { rules: [ { test: /.(js|jsx)$/, exclude: /node_modules/, use: { loader: “babel-loader” } } ] }};然后新建一个App.jsimport React from “react”;import ReactDOM from “react-dom”;const App = () => { return ( <div> <p>大彬哥一如既往的666</p> </div> );};export default App;ReactDOM.render(<App />, document.getElementById(“app”));最后引入到index.js里面import App from “./App”;然后重新build,又可以了,岂止是很赞,简直是很赞。到这里react安装就搞定了。9.如果你想搞点sass了你可以继续搞,因为不是每一个项目都用,我就不搞了,我只搞最原生的css,当然顺便也把html搞了。npm i mini-css-extract-plugin css-loader –save-devnpm i html-webpack-plugin html-loader –save-devwebpack.config.js配置文件如下:const HtmlWebPackPlugin = require(“html-webpack-plugin”);const MiniCssExtractPlugin = require(“mini-css-extract-plugin”);module.exports = { module: { rules: [ { test: /.js$/, exclude: /node_modules/, use: { loader: “babel-loader” } }, { test: /.html$/, use: [ { loader: “html-loader”, options: { minimize: true } } ] }, { test: /.css$/, use: [MiniCssExtractPlugin.loader, “css-loader”] } ] }, plugins: [ new HtmlWebPackPlugin({ template: “./src/index.html”, filename: “./index.html” }), new MiniCssExtractPlugin({ filename: “[name].css”, chunkFilename: “[id].css” }) ]};10.搞了这么多,我其实想实现的就是,我修改点东西,然后自动服务器刷新,最后开发完了,然后build一次完事儿。这个简单:npm i webpack-dev-server –save-dev配置一下,然后输入npm start就可以了。通过上面的过程呢,我们就实现了完整的工作流,但是有些具体的项目可以根据需要去添加对应的loaders等,不如有人写less,那就加less的loaders,还有我们需要对最终上线的文件(比如bundle.js)加时间戳去缓存,这些都是个性化的不同项目的需求了,大家可以在我的这个基础上继续搞。最后我们总结一下,工作流实现了:1.ES6编译2.css编译3.html压缩4.react支持5.服务器自动刷新大家可以在我的基础上继续添加功能,实现自己的工作流,有了工作流配合着组件库,就能真正的工业化生产,大幅度的提高效率。本文所有源码:https://github.com/leolau2012… ...

January 25, 2019 · 1 min · jiezi

5分钟读懂JavaScript预编译流程

大家都知道JavaScript是解释型语言,既然是解释型语言,就是编译一行,执行一行,那又何来预编译一说呢?脚本执行js引擎都做了什么呢?今天我们就来看看吧。1-JavaScript运行三部曲语法分析预编译解释执行语法分析很简单,就是引擎检查你的代码有没有什么低级的语法错误; 解释执行顾名思义便是执行代码了; 预编译简单理解就是在内存中开辟一些空间,存放一些变量与函数 ;2-JS预编译什么时候发生预编译到底什么时候发生? 误以为预编译仅仅发生在script内代码块执行前 这倒并没有错 预编译确确实实在script代码内执行前发生了 但是它大部分会发生在函数执行前3-实例分析先来区分理解一下这2个概念: 变量声明 var … 函数声明 function(){}<script>var a = 1;console.log(a);function test(a) { console.log(a); var a = 123; console.log(a); function a() {} console.log(a); var b = function() {} console.log(b); function d() {}}var c = function (){console.log(“I at C function”);}console.log(c);test(2);</script>分析过程如下:页面产生便创建了GO全局对象(Global Object)(也就是window对象);第一个脚本文件加载;脚本加载完毕后,分析语法是否合法;开始预编译 查找变量声明,作为GO属性,值赋予undefined; 查找函数声明,作为GO属性,值赋予函数体;预编译//抽象描述 GO/window = { a: undefined, c: undefined, test: function(a) { console.log(a); var a = 123; console.log(a); function a() {} console.log(a); var b = function() {} console.log(b); function d() {} } }解释执行代码(直到执行调用函数test(2)语句)//抽象描述 GO/window = { a: 1, c: function (){ console.log(“I at C function”); } test: function(a) { console.log(a); var a = 123; console.log(a); function a() {} console.log(a); var b = function() {} console.log(b); function d() {} } }执行函数test()之前,发生预编译创建AO活动对象(Active Object);查找形参和变量声明,值赋予undefined;实参值赋给形参;查找函数声明,值赋予函数体;预编译之前面1、2两小步如下://抽象描述 AO = { a:undefined, b:undefined, }预编译之第3步如下://抽象描述 AO = { a:2, b:undefined, }预编译之第4步如下://抽象描述 AO = { a:function a() {}, b:undefined d:function d() {} }执行test()函数时如下过程变化://抽象描述 AO = { a:function a() {}, b:undefined d:function d() {} } —> AO = { a:123, b:undefined d:function d() {} } —> AO = { a:123, b:function() {} d:function d() {} }执行结果:注意:预编译阶段发生变量声明和函数声明,没有初始化行为(赋值),匿名函数不参与预编译 ; 只有在解释执行阶段才会进行变量初始化 ;预编译(函数执行前)创建AO对象(Active Object)查找函数形参及函数内变量声明,形参名及变量名作为AO对象的属性,值为undefined实参形参相统一,实参值赋给形参查找函数声明,函数名作为AO对象的属性,值为函数引用预编译(脚本代码块script执行前)查找全局变量声明(包括隐式全局变量声明,省略var声明),变量名作全局对象的属性,值为undefined查找函数声明,函数名作为全局对象的属性,值为函数引用预编译小结预编译两个小规则函数声明整体提升-(具体点说,无论函数调用和声明的位置是前是后,系统总会把函数声明移到调用前面)变量 声明提升-(具体点说,无论变量调用和声明的位置是前是后,系统总会把声明移到调用前,注意仅仅只是声明,所以值是undefined)预编译前奏imply global 即任何变量,如果未经声明就赋值,则此变量就位全局变量所有。(全局域就是Window)一切声明的全局变量,全是window的属性; var a = 12;等同于Window.a = 12;函数预编译发生在函数执行前一刻。 ...

January 23, 2019 · 1 min · jiezi

[译] 使用 Proxy 来监测 Javascript 中的类

原文地址:Using Proxy to Track Javascript Class原文作者:Amir Harel译文出自:掘金翻译计划本文永久链接:https://github.com/xitu/gold-miner/blob/master/TODO1/using-proxy-to-track-javascript-class.md译者:SHERlocked93校对者:salomezhang, cyuamber使用 Proxy 来监测 Javascript 中的类Photo by Fabian Grohs on UnsplashProxy 对象(Proxy)是 ES6 的一个非常酷却鲜为人知的特性。虽然这个特性存在已久,但是我还是想在本文中对其稍作解释,并用一个例子说明一下它的用法。什么是 Proxy正如 MDN 上简单而枯燥的定义:Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。虽然这是一个不错的总结,但是我却并没有从中搞清楚 Proxy 能做什么,以及它能帮我们实现什么。首先,Proxy 的概念来源于元编程。简单的说,元编程是允许我们运行我们编写的应用程序(或核心)代码的代码。例如,臭名昭著的 eval 函数允许我们将字符串代码当做可执行代码来执行,它是就属于元编程领域。Proxy API 允许我们在对象和其消费实体中创建中间层,这种特性为我们提供了控制该对象的能力,比如可以决定怎样去进行它的 get 和 set,甚至可以自定义当访问这个对象上不存在的属性的时候我们可以做些什么。Proxy 的 APIvar p = new Proxy(target, handler);Proxy 构造函数获取一个 target 对象,和一个用来拦截 target 对象不同行为的 handler 对象。你可以设置下面这些拦截项:has — 拦截 in 操作。比如,你可以用它来隐藏对象上某些属性。get — 用来拦截读取操作。比如当试图读取不存在的属性时,你可以用它来返回默认值。set — 用来拦截赋值操作。比如给属性赋值的时候你可以增加验证的逻辑,如果验证不通过可以抛出错误。apply — 用来拦截函数调用操作。比如,你可以把所有的函数调用都包裹在 try/catch 语句块中。这只是一部分拦截项,你可以在 MDN 上找到完整的列表。下面是将 Proxy 用在验证上的一个简单的例子:const Car = { maker: ‘BMW’, year: 2018,};const proxyCar = new Proxy(Car, { set(obj, prop, value) { if (prop === ‘maker’ && value.length < 1) { throw new Error(‘Invalid maker’); } if (prop === ‘year’ && typeof value !== ’number’) { throw new Error(‘Invalid year’); } obj[prop] = value; return true; }});proxyCar.maker = ‘’; // throw exceptionproxyCar.year = ‘1999’; // throw exception可以看到,我们可以用 Proxy 来验证赋给被代理对象的值。使用 Proxy 来调试为了在实践中展示 Proxy 的能力,我创建了一个简单的监测库,用来监测给定的对象或类,监测项如下:函数执行时间函数的调用者或属性的访问者统计每个函数或属性的被访问次数。这是通过在访问任意对象、类、甚至是函数时,调用一个名为 proxyTrack 的函数来完成的。如果你希望监测是谁给一个对象的属性赋的值,或者一个函数执行了多久、执行了多少次、谁执行的,这个库将非常有用。我知道可能还有其他更好的工具来实现上面的功能,但是在这里我创建这个库就是为了用一用这个 API。使用 proxyTrack首先,我们看看怎么用:function MyClass() {}MyClass.prototype = { isPrime: function() { const num = this.num; for(var i = 2; i < num; i++) if(num % i === 0) return false; return num !== 1 && num !== 0; }, num: null,};MyClass.prototype.constructor = MyClass;const trackedClass = proxyTrack(MyClass);function start() { const my = new trackedClass(); my.num = 573723653; if (!my.isPrime()) { return ${my.num} is not prime; }}function main() { start();}main();如果我们运行这段代码,控制台将会输出:MyClass.num is being set by start for the 1 timeMyClass.num is being get by isPrime for the 1 timeMyClass.isPrime was called by start for the 1 time and took 0 mils.MyClass.num is being get by start for the 2 timeproxyTrack 接受 2 个参数:第一个是要监测的对象/类,第二个是一个配置项对象,如果没传递的话将被置为默认值。我们看看这个配置项默认值长啥样:const defaultOptions = { trackFunctions: true, trackProps: true, trackTime: true, trackCaller: true, trackCount: true, stdout: null, filter: null,};可以看到,你可以通过配置你关心的监测项来监测你的目标。比如你希望将结果输出出来,那么你可以将 console.log 赋给 stdout。还可以通过赋给 filter 的回调函数来自定义地控制输出哪些信息。你将会得到一个包括有监测信息的对象,并且如果你希望保留这个信息就返回 true,反之返回 false。在 React 中使用 proxyTrack因为 React 的组件实际上也是类,所以你可以通过 proxyTrack 来实时监控它。比如:class MyComponent extends Component{…}export default connect(mapStateToProps)(proxyTrack(MyComponent, { trackFunctions: true, trackProps: true, trackTime: true, trackCaller: true, trackCount: true, filter: (data) => { if( data.type === ‘get’ && data.prop === ‘componentDidUpdate’) return false; return true; }}));可以看到,你可以将你不关心的信息过滤掉,否则输出将会变得杂乱无章。实现 proxyTrack我们来看看 proxyTrack 的实现。首先是这个函数本身:export function proxyTrack(entity, options = defaultOptions) { if (typeof entity === ‘function’) return trackClass(entity, options); return trackObject(entity, options);}没什么特别的嘛,这里只是调用相关函数。再看看 trackObject:function trackObject(obj, options = {}) { const { trackFunctions, trackProps } = options; let resultObj = obj; if (trackFunctions) { proxyFunctions(resultObj, options); } if (trackProps) { resultObj = new Proxy(resultObj, { get: trackPropertyGet(options), set: trackPropertySet(options), }); } return resultObj;}function proxyFunctions(trackedEntity, options) { if (typeof trackedEntity === ‘function’) return; Object.getOwnPropertyNames(trackedEntity).forEach((name) => { if (typeof trackedEntity[name] === ‘function’) { trackedEntity[name] = new Proxy(trackedEntity[name], { apply: trackFunctionCall(options), }); } });}可以看到,假如我们希望监测对象的属性,我们创建了一个带有 get 和 set 拦截器的被监测对象。下面是 set 拦截器的实现:function trackPropertySet(options = {}) { return function set(target, prop, value, receiver) { const { trackCaller, trackCount, stdout, filter } = options; const error = trackCaller && new Error(); const caller = getCaller(error); const contextName = target.constructor.name === ‘Object’ ? ’’ : ${target.constructor.name}.; const name = ${contextName}${prop}; const hashKey = set_${name}; if (trackCount) { if (!callerMap[hashKey]) { callerMap[hashKey] = 1; } else { callerMap[hashKey]++; } } let output = ${name} is being set; if (trackCaller) { output += by ${caller.name}; } if (trackCount) { output += for the ${callerMap[hashKey]} time; } let canReport = true; if (filter) { canReport = filter({ type: ‘get’, prop, name, caller, count: callerMap[hashKey], value, }); } if (canReport) { if (stdout) { stdout(output); } else { console.log(output); } } return Reflect.set(target, prop, value, receiver); };}更有趣的是 trackClass 函数(至少对我来说是这样):function trackClass(cls, options = {}) { cls.prototype = trackObject(cls.prototype, options); cls.prototype.constructor = cls; return new Proxy(cls, { construct(target, args) { const obj = new target(…args); return new Proxy(obj, { get: trackPropertyGet(options), set: trackPropertySet(options), }); }, apply: trackFunctionCall(options), });}在这个案例中,因为我们希望拦截这个类上不属于原型上的属性,所以我们给这个类的原型创建了个代理,并且创建了个构造函数拦截器。别忘了,即使你在原型上定义了一个属性,但如果你再给这个对象赋值一个同名属性,JavaScript 将会创建一个这个属性的本地副本,所以赋值的改动并不会改变这个类其他实例的行为。这就是为何只对原型做代理并不能满足要求的原因。戳这里查看完整代码。如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。 ...

January 23, 2019 · 3 min · jiezi

Generator初识

一、简介Generator函数是ES6引入的新型函数,用于异步编程,跟Promise对象联合使用的话会极大降低异步编程的编写难度和阅读难度。与普通函数的区别:function关键字与函数名之间有一个星号;函数体内部使用yield语句,定义不同的内部状态(yield在英语里的意思就是“产出”)。二、简单示例1、yield和returnfunction* Foo() { yield ‘hello’; yield ‘world’; return ‘!’;}var foo = Foo();console.log(foo.next());console.log(foo.next());console.log(foo.next());注意:generator函数不能直接使用,是通过next()方法获取yield/return的返回结果,而return可以提前终止函数。2、yield字符串方式function Foo() { yield* ‘hello’;}var foo = Foo();console.log(foo.next());console.log(foo.next());console.log(foo.next());console.log(foo.next());console.log(foo.next());console.log(foo.next());数组方式function* Foo() { yield* [‘a’, ‘b’, ‘c’];}var foo = Foo();console.log(foo.next());console.log(foo.next());console.log(foo.next());console.log(foo.next());3、与for of配合使用yield和returnfunction* Foo() { yield 1; yield 2; return 3;}var foo = Foo();for(var v of foo) { console.log(v);}可以看出for of不执行return值yieldfunction Foo() { yield* ‘hello’;}var foo = Foo();for(var v of foo) { console.log(v);}未完待续…

January 22, 2019 · 1 min · jiezi

记一次小程序开发中如何使用async-await并封装公共异步请求

前言在平常的项目开发中肯定会遇到同步异步执行的问题,还有的就是当执行某一个操作依赖上一个执行所返回的结果,那么这个时候你会如何解决这个问题呢;1.是用settimeout让它异步执行,显然这只是让它加入异步任务队列中去执行,但并不能保证等待其返回结果再去执行另一个操作。2.还是自己封装callback函数?那样就会陷入所谓的回调地狱,代码层层嵌套,环环相扣,逻辑稍微复杂就会很难去维护。3.当然es6中的promise倒是很好的解决了这样的问题,再配合es7的async和await就更完美了,await返回的也是一个promise对象,这个关于promise和async,await的使用方法就不说了。实现方案首先小程序目前还是不支持es7的async和await的,那么如何让它支持呢1、下载[regenerator][1]并把下载好的runtime.js文件夹放到自己小程序的utils目录下,包总共才20kb多,体积很小的。![图片描述][2]2、在需要调的地方引入 import regeneratorRuntime from ‘../../utils/runtime.js'3、如何封装并使用封装:const postData = async function(url, data) { wx.showLoading({ title: ‘加载中’, }) let promiseP = await new Promise(function(resolve, reject) { wx.request({ url: baseUrl + url, data: data, method: ‘POST’, header: { ‘content-type’: ‘application/json’, ‘access-token’: wx.getStorageSync(’token’) }, success: function(res) { wx.hideLoading(); if (res.statusCode === 200) { resolve(res) } else { reject(res.data) } }, fail: function(err) { wx.hideLoading(); reject(err) if (err.code === 401) {} } }) }) return promiseP}module.exports = { postData}使用:import regeneratorRuntime from ‘../../utils/runtime.js’;const app = getApp(), postData = require(’../../service/koalaApi.js’);async demo() { await postData(app.globalData.baseUrl + ‘/test’,{ data: {} }).then((res) => { console.log(res) })}下面进行了更完善的一个封装,包括各种错误判断的处理和简化,通过传参的方式,来灵活调用// 当前hostconst url_host = require(‘API.js’).host // 当前版本const currentVersion = require(‘util.js’).currentVersion // 当前路径import { currentPagePath } from ‘util.js’ // 调用fetch方法,然后依次链式传入// url, method, header, data, loading(是否显示loading) function fetch(url, method, header, data, loading) { // 判断给服务端传递undefined的问题 let fetchP = new Promise(function (resolve, reject) { if (loading) { wx.showLoading({ icon: ’loading’ }) } if(data && data.unionId && typeof data.unionId === “undefined”){ wx.hideLoading() return reject({ ok:false, error: ‘unionId -> ’ + typeof data.unionId }); } wx.request({ url: url_host + url, method: method ? method : ‘GET’, header: { ‘content-type’: ‘application/json’, // 默认值 ‘version’: currentVersion, ‘pagePath’: currentPagePath() }, data: data, success: function (res) { if (res.statusCode < 500) { resolve(res.data) } else { showError() reject(res.data) } }, fail: function (err) { showError() reject(err) }, complete: function (comp) { if (loading) { wx.hideLoading() } } }) }) return fetchP}// 服务器开小差了function showError () { wx.hideLoading() // 获取头文件路径 wx.navigateTo({ url: ‘/pages/serverError/serverError’, })}module.exports = { fetch}思考1、为什么引入regeneratorRuntime,就能够使用async/await?不需要配合babel吗?2、regeneratorRuntime都做了什么?总结1、首先先明白babel和polyfill分别干啥的;Babel 是一个广泛使用的转码器,Babel 默认只转换新的 JavaScript 句法,而不转换新的 API。例如,Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转译。如果想使用这些新的对象和方法,必须使用 babel-polyfill,为当前环境提供一个垫片。2、Polyfill用于实现浏览器并不支持的原生API的代码。3、在明白上面的意思之后,还需要明白的是,babel-polyfill是一股脑把全部都给你添加到js文件中,而现在的runtime将会判断你哪些需要加载的,有选择性的进行加载,并且后者也不会污染全局变量。在这里regeneratorRuntime最终转化成es6的generator来用的。具体的可以自己去下babel官网,输入相关代码可以看下最终转换后的代码。 ...

January 19, 2019 · 2 min · jiezi

从0到1使用VUE-CLI3开发实战(三): ES6/ES7知识储备

今天群里有小伙伴跟我聊天,问了我几个关于ES6的问题,我才意识到,大部分初学者在学习的过程中,都是学了HTML/CSS/JS之后就开始上手学习框架了,而对于ES6的重视程度却不是那么足,或是仅仅了解部分ES6的用法。由于本项目将会用到大部分ES6的新特性,为避免将来有小伙伴看不懂还得去查,今天这篇就单独为我们之后的开发做一下ES6的知识储备。用 let / const 来代替 var因为 JavaScript 的 var 关键字是声明全局的变量,所以在 ES6 中引入了两个新的变量声明来解决这个问题,即 let 和 const 。 它们都用于声明变量。 区别在于 const 在声明后不能改变它的值,而 let 则可以。 和 var 不一样的是,let 和 const 不存在变量提升。一个 var 的例子:var snack = ‘Meow Mix’;function getFood(food) { if (food) { var snack = ‘Friskies’; return snack; } return snack;}getFood(false); // undefined使用 let 替换了 var 后的表现:let snack = ‘Meow Mix’;function getFood(food) { if (food) { let snack = ‘Friskies’; return snack; } return snack;}getFood(false); // ‘Meow Mix’当我们重构使用 var 的老代码时,一定要注意这种变化。盲目使用 let 替换 var 后可能会导致预期意外的结果。注意:let 和 const 是块级作用域语句。所以在语句块以外引用这些变量时,会造成引用错误 ReferenceError。console.log(x);let x = ‘hi’; // ReferenceError: x is not defined在本项目中,将使用 let 声明一个变量,使用 const 来声明一个不可改变的常量。用块级作用域代替 IIFES我们以往创建一个 立即执行函数 时,一般是在函数最外层包裹一层括号。ES6支持块级作用域,我们现在可以通过创建一个代码块(Block)来实现,不必通过创建一个函数来实现,(function () { var food = ‘Meow Mix’;}());console.log(food); // Reference Error使用支持块级作用域的ES6的版本:{ let food = ‘Meow Mix’;};console.log(food); // Reference Error箭头函数有些时候,我们在函数嵌套中需要访问上下文中的 this。比如下面的例子:function Person(name) { this.name = name;}Person.prototype.prefixName = function (arr) { return arr.map(function (character) { return this.name + character; // Cannot read property ’name’ of undefined });};一种通用的方式是把上下文中的 this 保存在一个变量里:function Person(name) { this.name = name;}Person.prototype.prefixName = function (arr) { var that = this; // Store the context of this return arr.map(function (character) { return that.name + character; });};我们也可以把 this 通过属性传进去:function Person(name) { this.name = name;}Person.prototype.prefixName = function (arr) { return arr.map(function (character) { return this.name + character; }, this);};还可以直接使用 bind:function Person(name) { this.name = name;}Person.prototype.prefixName = function (arr) { return arr.map(function (character) { return this.name + character; }.bind(this));};而如果使用 箭头函数,this 的值不用我们再做如上几段代码的特殊处理,直接使用即可。上面的代码可以重写为下面这样:function Person(name) { this.name = name;}Person.prototype.prefixName = function (arr) { return arr.map(character => this.name + character);};当你需要维护一个 this 上下文的时候尽量使用 箭头函数。当我们编写只返回一个表达式值的简单函数时,也可以使用箭头函数,如下:var squares = arr.map(function (x) { return x * x }); // Function Expressionconst arr = [1, 2, 3, 4, 5];const squares = arr.map(x => x * x); // Arrow Function for terser implementation字符串在ES6中,字符串对象新增了 .includes() 和 .repeat() 方法。.includes( )var string = ‘food’;var substring = ‘foo’;console.log(string.indexOf(substring) > -1);现在,我们可以使用 .inclues() 方法,替代以往判断内容 > -1 的方式。.includes() 方法会极简地返回一个布尔值结果。const string = ‘food’;const substring = ‘foo’;console.log(string.includes(substring)); // true.repeat()function repeat(string, count) { var strings = []; while(strings.length < count) { strings.push(string); } return strings.join(’’);}在ES6中,我们可以使用一个极简的方法来实现重复字符:// String.repeat(numberOfRepetitions)‘meow’.repeat(3); // ‘meowmeowmeow’字符串模版字面量使用 字符串模板字面量,我可以在字符串中直接使用特殊字符,而不用转义。var text = “This string contains "double quotes" which are escaped.";let text = This string contains "double quotes" which don't need to be escaped anymore.;字符串模板字面量 还支持直接插入变量,可以实现字符串与变量的直接连接输出。var name = ‘Tiger’;var age = 13;console.log(‘My cat is named ’ + name + ’ and is ’ + age + ’ years old.’);更简单的版本:const name = ‘Tiger’;const age = 13;console.log(My cat is named ${name} and is ${age} years old.);ES5中,我们要这样生成多行文本:var text = ( ‘cat\n’ + ‘dog\n’ + ’nickelodeon’);或者:var text = [ ‘cat’, ‘dog’, ’nickelodeon’].join(’\n’);字符串模板字面量 让我们不必特别关注多行字符串中的换行转义符号,直接换行即可:let text = ( catdognickelodeon);字符串模板字面量 内部可以使用表达式,像这样:let today = new Date();let text = The time and date is ${today.toLocaleString()};解构解构让我们可以使用非常便捷的语法,直接将数组或者对象中的值直接分别导出到多个变量中,解构数组解构数组var arr = [1, 2, 3, 4];var a = arr[0];var b = arr[1];var c = arr[2];var d = arr[3];let [a, b, c, d] = [1, 2, 3, 4];console.log(a); // 1console.log(b); // 2解构对象解构对象var luke = { occupation: ‘jedi’, father: ‘anakin’ };var occupation = luke.occupation; // ‘jedi’var father = luke.father; // ‘anakin’let luke = { occupation: ‘jedi’, father: ‘anakin’ };let {occupation, father} = luke;console.log(occupation); // ‘jedi’console.log(father); // ‘anakin’模块ES6之前,浏览器端的模块化代码,我们使用像Browserify这样的库,在 Node.js 中,我们则使用 require。在ES6中,我们现在可以直接使用AMD 和 CommonJS这些模块了。使用 CommonJS 的出口module.exports = 1;module.exports = { foo: ‘bar’ };module.exports = [‘foo’, ‘bar’];module.exports = function bar () {};使用 ES6 的出口在ES6中,提供了多种设置模块出口的方式,比如我们要导出一个变量,那么使用 变量名 :export let name = ‘David’;export let age = 25;还可以为对象 导出一个列表:function sumTwo(a, b) { return a + b;}function sumThree(a, b, c) { return a + b + c;}export { sumTwo, sumThree };我们也可以使用简单的一个 export 关键字来导出一个结果值:export function sumTwo(a, b) { return a + b;}export function sumThree(a, b, c) { return a + b + c;}最后,我们可以 导出一个默认出口:function sumTwo(a, b) { return a + b;}function sumThree(a, b, c) { return a + b + c;}let api = { sumTwo, sumThree};export default api;/* * 与以下的语句是对等的: * export { api as default }; /实践:总是在模块的 最后 使用 export default 方法。它让模块的出口更清晰明了,节省了阅读整个模块来寻找出口的时间。更多的是,在大量CommonJS模块中,通用的习惯是设置一个出口值或者出口对象。坚持这个规则,可以让我们的代码更易读,且更方便的联合使用CommonJS和ES6模块。ES6 中的导入ES6提供了好几种模块的导入方式。我们可以单独引入一个文件:import ‘underscore’;这里需要注意的是, 整个文件的引入方式会执行该文件内的最上层代码。就像Python一样,我们还可以命名引用:import { sumTwo, sumThree } from ‘math/addition’;我们甚至可以使用 as 给这些模块重命名:import { sumTwo as addTwoNumbers, sumThree as sumThreeNumbers} from ‘math/addition’;另外,我们能 引入所有的东西 (也称为命名空间引入)import * as util from ‘math/addition’;最后,我们能可以从一个模块的众多值中引入一个列表:import * as additionUtil from ‘math/addtion’;const { sumTwo, sumThree } = additionUtil;像这样引用默认对象:import api from ‘math/addition’;// Same as: import { default as api } from ‘math/addition’;我们建议一个模块导出的值应该越简洁越好,不过有时候有必要的话命名引用和默认引用可以混着用。如果一个模块是这样导出的:// foos.jsexport { foo as default, foo1, foo2 };那我们可以如此导入这个模块的值:import foo, { foo1, foo2 } from ‘foos’;我们还可以导入commonjs模块,例如React:import React from ‘react’;const { Component, PropTypes } = React;更简化版本:import React, { Component, PropTypes } from ‘react’;注意:被导出的值是被 绑定的,而不是引用。所以,改变一个模块中的值的话,会影响其他引用本模块的代码,一定要避免此种改动发生。参数在ES5中,许多种方法来处理函数的 参数默认值(default values),参数数量(indefinite arguments),参数命名(named parameters)。ES6中,我们可以使用非常简洁的语法来处理上面提到的集中情况。默认参数function addTwoNumbers(x, y) { x = x || 0; y = y || 0; return x + y;}ES6中,我们可以简单为函数参数启用默认值:function addTwoNumbers(x=0, y=0) { return x + y;}addTwoNumbers(2, 4); // 6addTwoNumbers(2); // 2addTwoNumbers(); // 0rest 参数ES5中,遇到参数数量不确定时,我们只能如此处理:function logArguments() { for (var i=0; i < arguments.length; i++) { console.log(arguments[i]); }}使用 rest 操作符,我们可以给函数传入一个不确定数量的参数列表:function logArguments(…args) { for (let arg of args) { console.log(arg); }}命名参数命名函数ES5中,当我们要处理多个 命名参数 时,通常会传入一个 选项对象 的方式,这种方式被jQuery采用。function initializeCanvas(options) { var height = options.height || 600; var width = options.width || 400; var lineStroke = options.lineStroke || ‘black’;}我们可以利用上面提到的新特性 解构 ,来完成与上面同样功能的函数:We can achieve the same functionality using destructuring as a formal parameterto a function:function initializeCanvas( { height=600, width=400, lineStroke=‘black’}) { // … } // Use variables height, width, lineStroke here如果我们需要把这个参数变为可选的,那么只要把该参数解构为一个空对象就好了:function initializeCanvas( { height=600, width=400, lineStroke=‘black’} = {}) { // … }展开操作我们可以利用展开操作符(Spread Operator)来把一组数组的值,当作参数传入:Math.max(…[-1, 100, 9001, -32]); // 9001类 Classes在ES6以前,我们实现一个类的功能的话,需要首先创建一个构造函数,然后扩展这个函数的原型方法,就像这样:function Person(name, age, gender) { this.name = name; this.age = age; this.gender = gender;}Person.prototype.incrementAge = function () { return this.age += 1;};继承父类的子类需要这样:function Personal(name, age, gender, occupation, hobby) { Person.call(this, name, age, gender); this.occupation = occupation; this.hobby = hobby;}Personal.prototype = Object.create(Person.prototype);Personal.prototype.constructor = Personal;Personal.prototype.incrementAge = function () { return Person.prototype.incrementAge.call(this) += 20;};ES6提供了一些语法糖来实现上面的功能,我们可以直接创建一个类:class Person { constructor(name, age, gender) { this.name = name; this.age = age; this.gender = gender; } incrementAge() { this.age += 1; }}继承父类的子类只要简单的使用 extends 关键字就可以了:class Personal extends Person { constructor(name, age, gender, occupation, hobby) { super(name, age, gender); this.occupation = occupation; this.hobby = hobby; } incrementAge() { super.incrementAge(); this.age += 20; console.log(this.age); }}实践:ES6新的类语法把我们从晦涩难懂的实现和原型操作中解救出来,这是个非常适合初学者的功能,而且能让我们写出更干净整洁的代码。SymbolsSymbols在ES6版本之前就已经存在了,但现在我们拥有一个公共的接口来直接使用它们。Symbols是不可更改的(immutable)并且唯一的(unique),它可用作任何hash数据类型中的键。Symbol()调用 Symbol() 或者 Symbol(描述文本) 会创建一个唯一的、在全局中不可以访问的Symbol对象。一个 Symbol() 的应用场景是:在自己的项目中使用第三方代码库,且你需要给他们的对象或者命名空间打补丁代码,又不想改动或升级第三方原有代码的时候。例如,如果你想给 React.Component 这个类添加一个 refreshComponent 方法,但又确定不了这个方法会不会在下个版本中加入,你可以这么做:const refreshComponent = Symbol();React.Component.prototype[refreshComponent] = () => { // do something}Symbol.for(key)使用 Symbol.for(key) 也是会创建一个不可改变的Symbol对象,但区别于上面的创建方法,这个对象是在全局中可以被访问到的。两次相同的 Symbol.for(key) 调用会返回相同的Symbol实例。提示:这并不同于 Symbol(description)。Symbol(‘foo’) === Symbol(‘foo’) // falseSymbol.for(‘foo’) === Symbol(‘foo’) // falseSymbol.for(‘foo’) === Symbol.for(‘foo’) // trueSymbols常用的一个使用场景,尤其是使用 Symbol.for(key) 方法,是用于实现代码间的互操作。在你的代码中,通过在包含一些已知接口的第三方库的对象参数中查找Symbol成员,你可以实现这种互操作。例如:function reader(obj) { const specialRead = Symbol.for(‘specialRead’); if (obj[specialRead]) { const reader = objspecialRead; // do something with reader } else { throw new TypeError(‘object cannot be read’); }}之后在另一个库中:const specialRead = Symbol.for(‘specialRead’);class SomeReadableType { specialRead { const reader = createSomeReaderFrom(this); return reader; }}注意:关于Symbol互操作的使用,一个值得一提的例子是Symbol.iterable 。Symbol.iterable存在ES6的所有可枚举对象中:数组(Arrays)、字符串(strings)、生成器(Generators)等等。当它作为一个方法被调用时,它将会返回一个带有枚举接口的对象。MapsMaps 是一个JavaScript中很重要(迫切需要)的数据结构。在ES6之前,我们创建一个 hash 通常是使用一个对象:var map = new Object();map[key1] = ‘value1’;map[key2] = ‘value2’;但是,这样的代码无法避免函数被特别的属性名覆盖的意外情况:> getOwnProperty({ hasOwnProperty: ‘Hah, overwritten’}, ‘Pwned’);> TypeError: Property ‘hasOwnProperty’ is not a functionMaps 让我们使用 set,get 和 search 操作数据。let map = new Map();> map.set(’name’, ‘david’);> map.get(’name’); // david> map.has(’name’); // trueMaps最强大的地方在于我们不必只能使用字符串来做key了,现在可以使用任何类型来当作key,而且key不会被强制类型转换为字符串。let map = new Map([ [’name’, ‘david’], [true, ‘false’], [1, ‘one’], [{}, ‘object’], [function () {}, ‘function’]]);for (let key of map.keys()) { console.log(typeof key); // > string, boolean, number, object, function}提示:当使用 map.get() 判断值是否相等时,非基础类型比如一个函数或者对象,将不会正常工作。有鉴于此,还是建议使用字符串,布尔和数字类型的数据类型。我们还可以使用 .entries() 方法来遍历整个map对象:for (let [key, value] of map.entries()) { console.log(key, value);}WeakMaps在ES5之前的版本,我们为了存储私有数据,有好几种方法。像使用这种下划线命名约定:class Person { constructor(age) { this._age = age; } _incrementAge() { this._age += 1; }}在一个开源项目中,命名规则很难维持得一直很好,这样经常会造成一些困扰。此时,我们可以选择使用WeakMaps来替代Maps来存储我们的数据:let _age = new WeakMap();class Person { constructor(age) { _age.set(this, age); } incrementAge() { let age = _age.get(this) + 1; _age.set(this, age); if (age > 50) { console.log(‘Midlife crisis’); } }}使用WeakMaps来保存我们私有数据的理由之一是不会暴露出属性名,就像下面的例子中的 Reflect.ownKeys():> const person = new Person(50);> person.incrementAge(); // ‘Midlife crisis’> Reflect.ownKeys(person); // []一个使用WeakMaps存储数据更实际的例子,是存储与DOM元素相关联的数据,而这不会对DOM元素本身产生污染:let map = new WeakMap();let el = document.getElementById(‘someElement’);// Store a weak reference to the element with a keymap.set(el, ‘reference’);// Access the value of the elementlet value = map.get(el); // ‘reference’// Remove the referenceel.parentNode.removeChild(el);el = null;value = map.get(el); // undefined上面的例子中,一旦对象被垃圾回收器给销毁了,WeakMaps会自动的把这个对象所对应的键值对数据同时销毁。提示:结合这个例子,再考虑下jQuery是如何实现缓存带有引用的DOM元素这个功能的。使用WeakMaps的话,当被缓存的DOM元素被移除的时,jQuery可以自动释放相应元素的内存。通常情况下,在涉及DOM元素存储和缓存的情况下,使用WeakMaps是非常有效的。PromisesPromises让我们把多缩进难看的代码(回调地狱):func1(function (value1) { func2(value1, function (value2) { func3(value2, function (value3) { func4(value3, function (value4) { func5(value4, function (value5) { // Do something with value 5 }); }); }); });});写成这样:func1(value1) .then(func2) .then(func3) .then(func4) .then(func5, value5 => { // Do something with value 5 });在ES6之前,我们使用bluebird 或者Q。现在我们有了原生版本的 Promises:new Promise((resolve, reject) => reject(new Error(‘Failed to fulfill Promise’))) .catch(reason => console.log(reason));这里有两个处理函数,resolve(当Promise执行成功完毕时调用的回调函数) 和 reject (当Promise执行不接受时调用的回调函数)Promises的好处:大量嵌套错误处理回调函数会使代码变得难以阅读理解。使用Promises,我们可以通过清晰的路径将错误事件让上传递,并且适当地处理它们。此外,Promise处理后的值,无论是解决(resolved)还是拒绝(rejected)的结果值,都是不可改变的。下面是一些使用Promises的实际例子:var request = require(‘request’);return new Promise((resolve, reject) => { request.get(url, (error, response, body) => { if (body) { resolve(JSON.parse(body)); } else { resolve({}); } });});我们还可以使用 Promise.all() 来 并行化 的处理一组异步的操作。let urls = [ ‘/api/commits’, ‘/api/issues/opened’, ‘/api/issues/assigned’, ‘/api/issues/completed’, ‘/api/issues/comments’, ‘/api/pullrequests’];let promises = urls.map((url) => { return new Promise((resolve, reject) => { $.ajax({ url: url }) .done((data) => { resolve(data); }); });});Promise.all(promises) .then((results) => { // Do something with results of all our promises });Generators 生成器就像Promises如何让我们避免回调地狱一样,Generators也可以使我们的代码扁平化,同时给予我们开发者像开发同步代码一样的感觉来写异步代码。Generators本质上是一种支持的函数,随后返回表达式的值。Generators实际上是支持暂停运行,随后根据上一步的返回值再继续运行的一种函数。下面代码是一个使用generators函数的简单例子:function sillyGenerator() { yield 1; yield 2; yield 3; yield 4;}var generator = sillyGenerator();> console.log(generator.next()); // { value: 1, done: false }> console.log(generator.next()); // { value: 2, done: false }> console.log(generator.next()); // { value: 3, done: false }> console.log(generator.next()); // { value: 4, done: false }就像上面的例子,当next运行时,它会把我们的generator向前“推动”,同时执行新的表达式。我们能利用Generators来像书写同步代码一样书写异步代码。// Hiding asynchronousity with Generatorsfunction request(url) { getJSON(url, function(response) { generator.next(response); });}这里我们写个generator函数将要返回我们的数据:function* getData() { var entry1 = yield request(‘http://some_api/item1’); var data1 = JSON.parse(entry1); var entry2 = yield request(‘http://some_api/item2’); var data2 = JSON.parse(entry2);}借助于 yield,我们可以保证 entry1 确实拿到数据并转换后再赋值给 data1。当我们使用generators来像书写同步代码一样书写我们的异步代码逻辑时,没有一种清晰简单的方式来处理期间可能会产生的错误或者异常。在这种情况下,我们可以在我们的generator中引入Promises来处理,就像下面这样:function request(url) { return new Promise((resolve, reject) => { getJSON(url, resolve); });}我们再写一个函数,其中使用 next 来步进我们的generator的同事,再利用我们上面的 request 方法来产生(yield)一个Promise。function iterateGenerator(gen) { var generator = gen(); var ret; (function iterate(val) { ret = generator.next(); if(!ret.done) { ret.value.then(iterate); } })();}在Generator中引入了Promises后,我们就可以通过Promise的 .catch 和 reject 来捕捉和处理错误了。使用了我们新版的Generator后,新版的调用就像老版本一样简单可读(译者注:有微调):iterateGenerator(function* getData() { var entry1 = yield request(‘http://some_api/item1’); var data1 = JSON.parse(entry1); var entry2 = yield request(‘http://some_api/item2’); var data2 = JSON.parse(entry2);});在使用Generator后,我们可以重用我们的老版本代码实现,以此展示了Generator的力量。当使用Generators和Promises后,我们可以像书写同步代码一样书写异步代码的同时优雅地解决了错误处理问题。此后,我们实际上可以开始利用更简单的一种方式了,它就是async-await。Async Awaitasync await 给我们提供了一种更轻松的、更简单的可以替代的实现上面 Generators 配合 Promises 组合代码的一种编码方式,让我们来看看例子:var request = require(‘request’);function getJSON(url) { return new Promise(function(resolve, reject) { request(url, function(error, response, body) { resolve(body); }); });}async function main() { var data = await getJSON(); console.log(data); // NOT undefined!}main();它们看上去和Generators很像。我强烈推荐使用 async await 来替代Generators + Promises的写法。Getter/Setter 函数ES6 实现了 getter 和 setter 函数,比如下面这个例子:class Employee { constructor(name) { this._name = name; } get name() { if(this._name) { return ‘Mr. ’ + this._name.toUpperCase(); } else { return undefined; } } set name(newName) { if (newName == this._name) { console.log(‘I already have this name.’); } else if (newName) { this._name = newName; } else { return false; } } }var emp = new Employee(“James Bond”);if (emp.name) { console.log(emp.name); }emp.name = “Bond 007”;console.log(emp.name); 浏览器也在对象中实现了 getter 和 setter 函数,我们可以使用它们来实现 计算属性,在设置和获取一个属性之前加上监听器和处理。var person = { firstName: ‘James’, lastName: ‘Bond’, get fullName() { console.log(‘Getting FullName’); return this.firstName + ’ ’ + this.lastName; }, set fullName (name) { console.log(‘Setting FullName’); var words = name.toString().split(’ ‘); this.firstName = words[0] || ‘’; this.lastName = words[1] || ‘’; } }person.fullName; person.fullName = ‘Bond 007’;person.fullName; 总结虽然我们不使用ES6依然能完成整个项目,但能熟练使用ES6新特性,将会是代码更简洁优雅。所以,尽快把ES6的知识掌握了吧。记得点好看呦! ...

January 18, 2019 · 8 min · jiezi

[WIP] 一个基于Angular最新版搭建的博客类小项目

前言:因为项目需要学习了一下Angular,按照官网的教程做了一个简单粗糙的小项目。大致的功能有博客的新建,删除,查看详情,列表展示。使用内存Web API模拟远程服务器,进行数据操作。临时安排的变动,可能没有太多的时间持续研究,所以现在这里记录一下。以下是我边学习边记的一些笔记,方便日后回来可以快速回忆起相关技术点。路由一个配置了路由的Angular应用有一个路由服务的单例。当浏览器的url改变的时候,路由就会去配置里找到相应的路由信息,根据该路由就可以决定展示哪一个组件。路由器会把类似URL的路径映射到视图而不是页面。浏览器本应该加载一个新页面,但是路由器拦截了这一行为。<router-outlet>标签官网上说它起到了placeholder的作用,当路由根据url找到要展示的组件时,就会把这个组件填充到<router-outlet>中去。获取路由中的参数信息: +this.route.snapshot.paramMap.get(‘id’);route.snapshot 是一个路由信息的静态快照,抓取自组建刚刚创建完毕之后paramMap是一个从URL中提取的路由参数值的字典。javascript(+)操作符会把字符串转为数字数据绑定用指令 [(ngModel)]=“data.content"可实现遇到报错 在app.module中导入import { FormsModule } from ‘@angular/forms’;HTTP获取资源主要是使用了common包下的HttpClientModule这一个模块,使用它则需要把它先导入进app.module中,然后在需要用到的service中导入相应的模块,例如HttpClient,HttpHeaders等。HttpClient提供了很丰富的Http协议的请求方法。实际使用时可参考源码。请求失败处理:catchError() 操作符会拦截失败的 Observable。 它把错误对象传给错误处理器,错误处理器会处理这个错误。因此异常被处理后,浏览器的控制台不会报错。状态管理 ngrx与Redux的关系:ngrx/store的灵感来源于Redux,是一款集成RxJS的Angular状态管理库,由Angular的布道者Rob Wormald开发。它和Redux的核心思想相同,但使用RxJS实现观察者模式。它遵循Redux核心原则,但专门为Angular而设计。Actions(行为)是信息的载体,它发送数据到reducer(归约器),然后reducer更新store(存储)。Actions是store能接受数据的唯一方式。组件化思想把所有特性都放在同一个组件中,将会使应用变得难以维护。因此需要把大型组件分成小一点的子组件,每个子组件都要集中经历处理某个特定的任务或者工作流。依赖注入DI,是一种重要的应用设计模式。实现方式:类从外部源中请求获取依赖,无需自己创建。DI框架会在实例化该类时向其提供这个类所声明的依赖项。@Injectable()装饰器是每个Angular服务定义中的基本要素,把当前类标志为可注入的服务。注入器:负责创建服务实例提供商:告诉注入器如何创建实例服务组件不直接获取或者保存数据,而是把这一块的功能放在service中,让组件专注于如何展示数据,如此开发使得项目的每个模块功能更加明确。service里的方法应该都是异步的。Observable(可观察对象)的版本可以实现。ES6相关一个模块从另一个模块import进来,若是一个值 :直接取值取到的是一个会被缓存的结果(例如获取WebStorage中更新的值,必须要刷新页面)函数,获取一个值的引用,会调用该函数,取到最新的值控制台输出与Debug有时候会发现控制台输出的结果与直接在js文件里进行debug所显示的结果不一致,那是因为,console.log()采用了懒加载的机制,它展示的是一个引用的值,当你点击三角箭头的时候,它会去加载当前变量的值。但debug的数据具有“实时性”。不一致的情况经常出现在一些异步代码中。如果你看到这里,觉得内容有误或有可改进之处,欢迎你的提出~

January 17, 2019 · 1 min · jiezi

JS 异步编程六种方案

前言我们知道Javascript语言的执行环境是"单线程"。也就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务。这种模式虽然实现起来比较简单,执行环境相对单纯,但是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步和异步。本文主要介绍异步编程几种办法,并通过比较,得到最佳异步编程的解决方案!想阅读更多优质文章请猛戳GitHub博客一、同步与异步我们可以通俗理解为异步就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有”堵塞“效应。比如,有一个任务是读取文件进行处理,异步的执行过程就是下面这样这种不连续的执行,就叫做异步。相应地,连续的执行,就叫做同步"异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,“异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。接下来介绍下异步编程六种方法。二、回调函数(Callback)回调函数是异步操作最基本的方法。以下代码就是一个回调函数的例子:ajax(url, () => { // 处理逻辑})但是回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,你可能就会写出如下代码:ajax(url, () => { // 处理逻辑 ajax(url1, () => { // 处理逻辑 ajax(url2, () => { // 处理逻辑 }) })})回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。此外它不能使用 try catch 捕获错误,不能直接 return。三、事件监听这种方式下,异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。下面是两个函数f1和f2,编程的意图是f2必须等到f1执行完成,才能执行。首先,为f1绑定一个事件(这里采用的jQuery的写法)f1.on(‘done’, f2);上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写:function f1() { setTimeout(function () { // … f1.trigger(‘done’); }, 1000);}上面代码中,f1.trigger(‘done’)表示,执行完成后,立即触发done事件,从而开始执行f2。这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合”,有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。四、发布订阅我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。首先,f2向信号中心jQuery订阅done信号。jQuery.subscribe(‘done’, f2);然后,f1进行如下改写:function f1() { setTimeout(function () { // … jQuery.publish(‘done’); }, 1000);}上面代码中,jQuery.publish(‘done’)的意思是,f1执行完成后,向信号中心jQuery发布done信号,从而引发f2的执行。f2完成执行后,可以取消订阅(unsubscribe)jQuery.unsubscribe(‘done’, f2);这种方法的性质与“事件监听”类似,但是明显优于后者。因为可以通过查看“消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。五、Promise/A+Promise本意是承诺,在程序中的意思就是承诺我过一段时间后会给你一个结果。 什么时候会用到过一段时间?答案是异步操作,异步是指可能比较长时间才有结果的才做,例如网络请求、读取本地文件等1.Promise的三种状态Pending—-Promise对象实例创建时候的初始状态Fulfilled—-可以理解为成功的状态Rejected—-可以理解为失败的状态这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,比如说一旦状态变为 resolved 后,就不能再次改变为Fulfilledlet p = new Promise((resolve, reject) => { reject(‘reject’) resolve(‘success’)//无效代码不会执行})p.then( value => { console.log(value) }, reason => { console.log(reason)//reject })当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的new Promise((resolve, reject) => { console.log(’new Promise’) resolve(‘success’)})console.log(’end’)// new Promise => end2.promise的链式调用每次调用返回的都是一个新的Promise实例(这就是then可用链式调用的原因)如果then中返回的是一个结果的话会把这个结果传递下一次then中的成功回调如果then中出现异常,会走下一个then的失败回调在 then中使用了return,那么 return 的值会被Promise.resolve() 包装(见例1,2)then中可以不传递参数,如果不传递会透到下一个then中(见例3)catch 会捕获到没有捕获的异常接下来我们看几个例子: // 例1 Promise.resolve(1) .then(res => { console.log(res) return 2 //包装成 Promise.resolve(2) }) .catch(err => 3) .then(res => console.log(res))// 例2Promise.resolve(1) .then(x => x + 1) .then(x => { throw new Error(‘My Error’) }) .catch(() => 1) .then(x => x + 1) .then(x => console.log(x)) //2 .catch(console.error)// 例3let fs = require(‘fs’)function read(url) { return new Promise((resolve, reject) => { fs.readFile(url, ‘utf8’, (err, data) => { if (err) reject(err) resolve(data) }) })}read(’./name.txt’) .then(function(data) { throw new Error() //then中出现异常,会走下一个then的失败回调 }) //由于下一个then没有失败回调,就会继续往下找,如果都没有,就会被catch捕获到 .then(function(data) { console.log(‘data’) }) .then() .then(null, function(err) { console.log(’then’, err)// then error }) .catch(function(err) { console.log(’error’) })Promise不仅能够捕获错误,而且也很好地解决了回调地狱的问题,可以把之前的回调地狱例子改写为如下代码:ajax(url) .then(res => { console.log(res) return ajax(url1) }).then(res => { console.log(res) return ajax(url2) }).then(res => console.log(res))它也是存在一些缺点的,比如无法取消 Promise,错误需要通过回调函数捕获。六、生成器Generators/ yieldGenerator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator 最大的特点就是可以控制函数的执行。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。Generator 函数除了状态机,还是一个遍历器对象生成函数。可暂停函数, yield可暂停,next方法可启动,每次返回的是yield后的表达式结果。yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。我们先来看个例子:function foo(x) { let y = 2 * (yield (x + 1)) let z = yield (y / 3) return (x + y + z)}let it = foo(5)console.log(it.next()) // => {value: 6, done: false}console.log(it.next(12)) // => {value: 8, done: false}console.log(it.next(13)) // => {value: 42, done: true}可能结果跟你想象不一致,接下来我们逐行代码分析:首先 Generator 函数调用和普通函数不同,它会返回一个迭代器当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6当执行第二次 next 时,传入的参数12就会被当作上一个yield表达式的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 12,所以第二个 yield 等于 2 12 / 3 = 8当执行第三次 next 时,传入的参数13就会被当作上一个yield表达式的返回值,所以 z = 13, x = 5, y = 24,相加等于 42我们再来看个例子:有三个本地文件,分别1.txt,2.txt和3.txt,内容都只有一句话,下一个请求依赖上一个请求的结果,想通过Generator函数依次调用三个文件//1.txt文件2.txt//2.txt文件3.txt//3.txt文件结束let fs = require(‘fs’)function read(file) { return new Promise(function(resolve, reject) { fs.readFile(file, ‘utf8’, function(err, data) { if (err) reject(err) resolve(data) }) })}function r() { let r1 = yield read(’./1.txt’) let r2 = yield read(r1) let r3 = yield read(r2) console.log(r1) console.log(r2) console.log(r3)}let it = r()let { value, done } = it.next()value.then(function(data) { // value是个promise console.log(data) //data=>2.txt let { value, done } = it.next(data) value.then(function(data) { console.log(data) //data=>3.txt let { value, done } = it.next(data) value.then(function(data) { console.log(data) //data=>结束 }) })})// 2.txt=>3.txt=>结束从上例中我们看出手动迭代Generator 函数很麻烦,实现逻辑有点绕,而实际开发一般会配合 co 库去使用。co是一个为Node.js和浏览器打造的基于生成器的流程控制工具,借助于Promise,你可以使用更加优雅的方式编写非阻塞代码。安装co库只需:npm install co上面例子只需两句话就可以轻松实现function* r() { let r1 = yield read(’./1.txt’) let r2 = yield read(r1) let r3 = yield read(r2) console.log(r1) console.log(r2) console.log(r3)}let co = require(‘co’)co(r()).then(function(data) { console.log(data)})// 2.txt=>3.txt=>结束=>undefined我们可以通过 Generator 函数解决回调地狱的问题,可以把之前的回调地狱例子改写为如下代码:function *fetch() { yield ajax(url, () => {}) yield ajax(url1, () => {}) yield ajax(url2, () => {})}let it = fetch()let result1 = it.next()let result2 = it.next()let result3 = it.next()七、async/await1.Async/Await简介使用async/await,你可以轻松地达成之前使用生成器和co函数所做到的工作,它有如下特点:async/await是基于Promise实现的,它不能用于普通的回调函数。async/await与Promise一样,是非阻塞的。async/await使得异步代码看起来像同步代码,这正是它的魔力所在。一个函数如果加上 async ,那么该函数就会返回一个 Promiseasync function async1() { return “1”}console.log(async1()) // -> Promise {<resolved>: “1”}Generator函数依次调用三个文件那个例子用async/await写法,只需几句话便可实现let fs = require(‘fs’)function read(file) { return new Promise(function(resolve, reject) { fs.readFile(file, ‘utf8’, function(err, data) { if (err) reject(err) resolve(data) }) })}async function readResult(params) { try { let p1 = await read(params, ‘utf8’)//await后面跟的是一个Promise实例 let p2 = await read(p1, ‘utf8’) let p3 = await read(p2, ‘utf8’) console.log(‘p1’, p1) console.log(‘p2’, p2) console.log(‘p3’, p3) return p3 } catch (error) { console.log(error) }}readResult(‘1.txt’).then( // async函数返回的也是个promise data => { console.log(data) }, err => console.log(err))// p1 2.txt// p2 3.txt// p3 结束// 结束2.Async/Await并发请求如果请求两个文件,毫无关系,可以通过并发请求let fs = require(‘fs’)function read(file) { return new Promise(function(resolve, reject) { fs.readFile(file, ‘utf8’, function(err, data) { if (err) reject(err) resolve(data) }) })}function readAll() { read1() read2()//这个函数同步执行}async function read1() { let r = await read(‘1.txt’,‘utf8’) console.log(r)}async function read2() { let r = await read(‘2.txt’,‘utf8’) console.log(r)}readAll() // 2.txt 3.txt八、总结1.JS 异步编程进化史:callback -> promise -> generator -> async + await2.async/await 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。3.async/await可以说是异步终极解决方案了。(1) async/await函数相对于Promise,优势体现在:处理 then 的调用链,能够更清晰准确的写出代码并且也能优雅地解决回调地狱问题。当然async/await函数也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低,代码没有依赖性的话,完全可以使用 Promise.all 的方式。(2) async/await函数对 Generator 函数的改进,体现在以下三点:内置执行器。Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。更好的语义。 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。参考文章Promises/A+前端面试之道Javascript异步编程的4种方法你不知道的JavaScript(中卷)async 函数的含义和用法Async/Await替代Promise的6个理由 ...

January 14, 2019 · 3 min · jiezi

前端基本功-常见概念(三)

前端基本功-常见概念(一) 点这里前端基本功-常见概念(二) 点这里前端基本功-常见概念(三) 点这里1.HTML / XML / XHTMLhtml:超文本标记语言,显示信息,不区分大小写xhtml:升级版的html,区分大小写xml:可扩展标记语言被用来传输和存储数据2.AMD/CMD/CommonJs/ES6 ModuleAMD:AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。AMD是requirejs 在推广过程中对模块定义的规范化产出,提前执行,推崇依赖前置。用define()定义模块,用require()加载模块,require.config()指定引用路径等首先我们需要引入require.js文件和一个入口文件main.js。main.js中配置require.config()并规定项目中用到的基础模块。 /** 网页中引入require.js及main.js / <script src=“js/require.js” data-main=“js/main”></script> / main.js 入口文件/主模块 / // 首先用config()指定各模块路径和引用名 require.config({ baseUrl: “js/lib”, paths: { “jquery”: “jquery.min”, //实际路径为js/lib/jquery.min.js “underscore”: “underscore.min”, } }); // 执行基本操作 require([“jquery”,“underscore”],function($,){ // some code here });引用模块的时候,我们将模块名放在[]中作为reqiure()的第一参数;如果我们定义的模块本身也依赖其他模块,那就需要将它们放在[]中作为define()的第一参数。 // 定义math.js模块 define(function () { var basicNum = 0; var add = function (x, y) { return x + y; }; return { add: add, basicNum :basicNum }; }); // 定义一个依赖underscore.js的模块 define([‘underscore’],function(){ var classify = function(list){ _.countBy(list,function(num){ return num > 30 ? ‘old’ : ‘young’; }) }; return { classify :classify }; }) // 引用模块,将模块放在[]内 require([‘jquery’, ‘math’],function($, math){ var sum = math.add(10,20); $("#sum").html(sum); });CMD:seajs 在推广过程中对模块定义的规范化产出,延迟执行,推崇依赖就近require.js在申明依赖的模块时会在第一之间加载并执行模块内的代码: define([“a”, “b”, “c”, “d”, “e”, “f”], function(a, b, c, d, e, f) { // 等于在最前面声明并初始化了要用到的所有模块 if (false) { // 即便没用到某个模块 b,但 b 还是提前执行了 b.foo() } });CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。 / CMD写法 / define(function(require, exports, module) { var a = require(’./a’); //在需要时申明 a.doSomething(); if (false) { var b = require(’./b’); b.doSomething(); } }); / sea.js / // 定义模块 math.js define(function(require, exports, module) { var $ = require(‘jquery.js’); var add = function(a,b){ return a+b; } exports.add = add; }); // 加载模块 seajs.use([‘math.js’], function(math){ var sum = math.add(1+2); });CommonJs:Node.js是commonJS规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块。 // 定义模块math.js var basicNum = 0; function add(a, b) { return a + b; } module.exports = { //在这里写上需要向外暴露的函数、变量 add: add, basicNum: basicNum } // 引用自定义的模块时,参数包含路径,可省略.js var math = require(’./math’); math.add(2, 5); // 引用核心模块时,不需要带路径 var http = require(‘http’); http.createService(…).listen(3000);commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。ES6 Module:ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。/ 定义模块 math.js /var basicNum = 0;var add = function (a, b) { return a + b;};export { basicNum, add };/ 引用模块 /import { basicNum, add } from ‘./math’;function test(ele) { ele.textContent = add(99 + basicNum);}如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。这也更趋近于ADM的引用写法。/ export default **///定义输出export default { basicNum, add };//引入import math from ‘./math’;function test(ele) { ele.textContent = math.add(99 + math.basicNum);}ES6的模块不是对象,import命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能。ES6 模块与 CommonJS 模块的差异CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。- 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。本节参考文章:前端模块化:CommonJS,AMD,CMD,ES63.ES5的继承/ES6的继承ES5的继承时通过prototype或构造函数机制来实现。ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.apply(this))。ES6的继承机制完全不同,实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this。具体的:ES6通过class关键字定义类,里面有构造方法,类之间通过extends关键字实现继承。子类必须在constructor方法中调用super方法,否则新建实例报错。因为子类没有自己的this对象,而是继承了父类的this对象,然后对其进行加工。如果不调用super方法,子类得不到this对象。ps:super关键字指代父类的实例,即父类的this对象。在子类构造函数中,调用super后,才可使用this关键字,否则报错。区别:(以SubClass,SuperClass,instance为例)ES5中继承的实质是:(那种经典寄生组合式继承法)通过prototype或构造函数机制来实现,先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.apply(this))。先由子类(SubClass)构造出实例对象this然后在子类的构造函数中,将父类(SuperClass)的属性添加到this上,SuperClass.apply(this, arguments)子类原型(SubClass.prototype)指向父类原型(SuperClass.prototype)所以instance是子类(SubClass)构造出的(所以没有父类的[[Class]]关键标志)所以,instance有SubClass和SuperClass的所有实例属性,以及可以通过原型链回溯,获取SubClass和SuperClass原型上的方法ES6中继承的实质是:先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this先由父类(SuperClass)构造出实例对象this,这也是为什么必须先调用父类的super()方法(子类没有自己的this对象,需先由父类构造)然后在子类的构造函数中,修改this(进行加工),譬如让它指向子类原型(SubClass.prototype),这一步很关键,否则无法找到子类原型(注,子类构造中加工这一步的实际做法是推测出的,从最终效果来推测)然后同样,子类原型(SubClass.prototype)指向父类原型(SuperClass.prototype)所以instance是父类(SuperClass)构造出的(所以有着父类的[[Class]]关键标志)所以,instance有SubClass和SuperClass的所有实例属性,以及可以通过原型链回溯,获取SubClass和SuperClass原型上的方法静态方法继承实质上只需要更改下SubClass.__proto__到SuperClass即可本节参考文章:链接4.HTTP request报文/HTTP response报文请求报文响应报文请求行 请求头 空行 请求体状态行 响应头 空行 响应体HTTP request报文结构是怎样的首行是Request-Line包括:请求方法,请求URI,协议版本,CRLF首行之后是若干行请求头,包括general-header,request-header或者entity-header,每个一行以CRLF结束请求头和消息实体之间有一个CRLF分隔根据实际请求需要可能包含一个消息实体 一个请求报文例子如下:GET /Protocols/rfc2616/rfc2616-sec5.html HTTP/1.1Host: www.w3.orgConnection: keep-aliveCache-Control: max-age=0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36Referer: https://www.google.com.hk/Accept-Encoding: gzip,deflate,sdchAccept-Language: zh-CN,zh;q=0.8,en;q=0.6Cookie: authorstyle=yesIf-None-Match: “2cc8-3e3073913b100"If-Modified-Since: Wed, 01 Sep 2004 13:24:52 GMTname=qiu&age=25请求报文HTTP response报文结构是怎样的首行是状态行包括:HTTP版本,状态码,状态描述,后面跟一个CRLF首行之后是若干行响应头,包括:通用头部,响应头部,实体头部响应头部和响应实体之间用一个CRLF空行分隔最后是一个可能的消息实体 响应报文例子如下:HTTP/1.1 200 OKDate: Tue, 08 Jul 2014 05:28:43 GMTServer: Apache/2Last-Modified: Wed, 01 Sep 2004 13:24:52 GMTETag: “40d7-3e3073913b100"Accept-Ranges: bytesContent-Length: 16599Cache-Control: max-age=21600Expires: Tue, 08 Jul 2014 11:28:43 GMTP3P: policyref=“http://www.w3.org/2001/05/P3P/p3p.xml"Content-Type: text/html; charset=iso-8859-1{“name”: “qiu”, “age”: 25}响应报文5.面向对象的工厂模式/构造函数工厂模式集中实例化了对象,避免实例化对象大量重复问题//工厂模式function createObject(a,b){ var obj = new Object(); //集中实例化 obj.a = a; obj.b = b; obj.c = function () { return this.a + this.b; }; return obj; //返回实例化对象}var box = createObject(‘abc’,10);var box1 = createObject(‘abcdef’,20);alert(box.c()); //返回abc10alert(box1.c()); //返回abcdef20//构造函数function Create(a,b) { this.a =a; this.b =b; this.c = function () { return this.a + this.b; };}var box = new Create(‘abc’,10);alert(box.run()); //返回abc10构造函数相比工厂模式:没有集中实例化没有返回对象实例直接将属性和方法赋值给this解决了对象实例归属问题构造函数编写规范:构造函数也是函数,但是函数名的第一个字母大写必须使用new运算符 + 函数名(首字母大写)例如:var box = new Create();构造函数和普通函数的区别:普通函数,首字母无需大写构造函数,用普通函数调用方式无效查看归属问题,要创建两个构造函数:function Create(a,b) { this.a =a; this.b =b; this.c = function () { return this.a + this.b; };}function DeskTop(a,b) { this.a =a; this.b =b; this.c = function () { return this.a + this.b; };}var box = new Create(‘abc’,10);var box1 = new DeskTop(‘def’,20);alert(box instanceof Object);//这里要注意:所有的构造函数的对象都是Object.alert(box instanceof Create); //truealert(box1 instanceof Create); //falsealert(box1 instanceof DeskTop); //true6. new Promise / Promise.resolve()Promise.resolve()可以生成一个成功的PromisePromise.resolve()语法糖例1:Promise.resolve(‘成功’)等同于new Promise(function(resolve){resolve(‘成功’)})例2:var resolved = Promise.resolve(‘foo’);resolved.then((str) => console.log(str);//foo)相当于var resolved = new Promise((resolve, reject) => { resolve(‘foo’)});resolved.then((str) => console.log(str);//foo)Promise.resolve方法有下面三种形式:Promise.resolve(value);Promise.resolve(promise);Promise.resolve(theanable);这三种形式都会产生一个新的Promise。其中:第一种形式提供了自定义Promise的值的能力,它与Promise.reject(reason)对应。两者的不同,在于得到的Promise的状态不同。第二种形式,提供了创建一个Promise的副本的能力。第三种形式,是将一个类似Promise的对象转换成一个真正的Promise对象。它的一个重要作用是将一个其他实现的Promise对象封装成一个当前实现的Promise对象。例如你正在用bluebird,但是现在有一个Q的Promise,那么你可以通过此方法把Q的Promise变成一个bluebird的Promise。实际上第二种形式可以归在第三种形式中。本节参考文章:ES6中的Promise.resolve()推荐阅读:性感的Promise…7.伪类 / 伪元素伪类伪类 用于当已有元素处于的某个状态时,为其添加对应的样式,这个状态是根据用户行为而动态变化的。当用户悬停在指定的元素时,我们可以通过 :hover 来描述这个元素的状态。虽然它和普通的 CSS 类相似,可以为已有的元素添加样式,但是它只有处于 DOM 树无法描述的状态下才能为元素添加样式,所以将其称为伪类。伪元素伪元素 用于创建一些不在文档树中的元素,并为其添加样式。我们可以通过 :before 来在一个元素前增加一些文本,并为这些文本添加样式。虽然用户可以看到这些文本,但是这些文本实际上不在文档树中。本节参考文章:前端面试题-伪类和伪元素、总结伪类与伪元素8.DOMContentLoaded / loadDOM文档加载的步骤为:解析HTML结构。DOM树构建完成。//DOMContentLoaded加载外部脚本和样式表文件。解析并执行脚本代码。加载图片等外部文件。页面加载完毕。//load触发的时机不一样,先触发DOMContentLoaded事件,后触发load事件。原生js// 不兼容老的浏览器,兼容写法见jQuery中ready与load事件,或用jQuerydocument.addEventListener(“DOMContentLoaded”, function() { // …代码…}, false);window.addEventListener(“load”, function() { // …代码…}, false);jQuery// DOMContentLoaded$(document).ready(function() { // …代码…});//load$(document).load(function() { // …代码…});head 中资源的加载head 中 js 资源加载都会停止后面 DOM 的构建,但是不影响后面资源的下载。css资源不会阻碍后面 DOM 的构建,但是会阻碍页面的首次渲染。body 中资源的加载body 中 js 资源加载都会停止后面 DOM 的构建,但是不影响后面资源的下载。css 资源不会阻碍后面 DOM 的构建,但是会阻碍页面的首次渲染。DomContentLoaded 事件的触发上面只是讲了 html 文档的加载与渲染,并没有讲 DOMContentLoaded 事件的触发时机。直截了当地结论是,DOMContentLoaded 事件在 html文档加载完毕,并且 html 所引用的内联 js、以及外链 js 的同步代码都执行完毕后触发。大家可以自己写一下测试代码,分别引用内联 js 和外链 js 进行测试。load 事件的触发当页面 DOM 结构中的 js、css、图片,以及 js 异步加载的 js、css 、图片都加载完成之后,才会触发 load 事件。注意:页面中引用的js 代码如果有异步加载的 js、css、图片,是会影响 load 事件触发的。video、audio、flash 不会影响 load 事件触发。推荐阅读:再谈 load 与 DOMContentLoaded本节参考文章:DOMContentLoaded与load的区别、事件DOMContentLoaded和load的区别9. 为什么将css放在头部,将js文件放在尾部因为浏览器生成Dom树的时候是一行一行读HTML代码的,script标签放在最后面就不会影响前面的页面的渲染。那么问题来了,既然Dom树完全生成好后页面才能渲染出来,浏览器又必须读完全部HTML才能生成完整的Dom树,script标签不放在body底部是不是也一样,因为dom树的生成需要整个文档解析完毕。我们再来看一下chrome在页面渲染过程中的,绿色标志线是First Paint的时间。纳尼,为什么会出现firstpaint,页面的paint不是在渲染树生成之后吗?其实现代浏览器为了更好的用户体验,渲染引擎将尝试尽快在屏幕上显示的内容。它不会等到所有HTML解析之前开始构建和布局渲染树。部分的内容将被解析并显示。也就是说浏览器能够渲染不完整的dom树和cssom,尽快的减少白屏的时间。假如我们将js放在header,js将阻塞解析dom,dom的内容会影响到First Paint,导致First Paint延后。所以说我们会 将js放在后面,以减少First Paint的时间,但是不会减少DOMContentLoaded被触发的时间。本节参考文章:DOMContentLoaded与load的区别10.clientheight / offsetheightclientheight:内容的可视区域,不包含border。clientheight=padding+height-横向滚动轴高度。这里写图片描述offsetheight,它包含padding、border、横向滚动轴高度。 offsetheight=padding+height+border+横向滚动轴高度scrollheight,可滚动高度,就是将滚动框拉直,不再滚动的高度,这个很好理解。 It includes the element’s padding, but not its border or margin.本节参考文章:css clientheight、offsetheight、scrollheight详解11.use strict 有什么意义和好处使调试更加容易。那些被忽略或默默失败了的代码错误,会产生错误或抛出异常,因此尽早提醒你代码中的问题,你才能更快地指引到它们的源代码。防止意外的全局变量。如果没有严格模式,将值分配给一个未声明的变量会自动创建该名称的全局变量。这是JavaScript中最常见的错误之一。在严格模式下,这样做的话会抛出错误。消除 this 强制。如果没有严格模式,引用null或未定义的值到 this 值会自动强制到全局变量。这可能会导致许多令人头痛的问题和让人恨不得拔自己头发的bug。在严格模式下,引用 null或未定义的 this 值会抛出错误。不允许重复的属性名称或参数值。当检测到对象中重复命名的属性,例如:var object = {foo: “bar”, foo: “baz”};)或检测到函数中重复命名的参数时,例如:function foo(val1, val2, val1){})严格模式会抛出错误,因此捕捉几乎可以肯定是代码中的bug可以避免浪费大量的跟踪时间。使 eval() 更安全。在严格模式和非严格模式下, eval() 的行为方式有所不同。最显而易见的是,在严格模式下,变量和声明在 eval() 语句内部的函数不会在包含范围内创建(它们会在非严格模式下的包含范围中被创建,这也是一个常见的问题源)。在 delete 使用无效时抛出错误。 delete 操作符(用于从对象中删除属性)不能用在对象不可配置的属性上。当试图删除一个不可配置的属性时,非严格代码将默默地失败,而严格模式将在这样的情况下抛出异常。本节参考文章:经典面试题(4)12.常见 JavaScript 内存泄漏意外的全局变量JavaScript 处理未定义变量的方式比较宽松:未定义的变量会在全局对象创建一个新变量。在浏览器中,全局对象是 window 。function foo(arg) { bar = “this is a hidden global variable”;}真相是:function foo(arg) { window.bar = "this is an explicit global variable";}函数 foo 内部忘记使用 var ,意外创建了一个全局变量。此例泄漏了一个简单的字符串,无伤大雅,但是有更糟的情况。另一种意外的全局变量可能由 this 创建:function foo() { this.variable = "potential accidental global";}// Foo 调用自己,this 指向了全局对象(window)// 而不是 undefinedfoo();在 JavaScript 文件头部加上 ‘use strict’,可以避免此类错误发生。启用严格模式解析 JavaScript ,避免意外的全局变量。被遗忘的计时器或回调函数在 JavaScript 中使用 setInterval 非常平常。一段常见的代码:var someResource = getData();setInterval(function() { var node = document.getElementById(‘Node’); if(node) { // 处理 node 和 someResource node.innerHTML = JSON.stringify(someResource)); }}, 1000);此例说明了什么:与节点或数据关联的计时器不再需要,node 对象可以删除,整个回调函数也不需要了。可是,计时器回调函数仍然没被回收(计时器停止才会被回收)。同时,someResource 如果存储了大量的数据,也是无法被回收的。对于观察者的例子,一旦它们不再需要(或者关联的对象变成不可达),明确地移除它们非常重要。老的 IE 6 是无法处理循环引用的。如今,即使没有明确移除它们,一旦观察者对象变成不可达,大部分浏览器是可以回收观察者处理函数的。观察者代码示例:var element = document.getElementById(‘button’);function onClick(event) { element.innerHTML = ’text’;}element.addEventListener(‘click’, onClick);对象观察者和循环引用注意事项老版本的 IE 是无法检测 DOM 节点与 JavaScript 代码之间的循环引用,会导致内存泄漏。如今,现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法,已经可以正确检测和处理循环引用了。换言之,回收节点内存时,不必非要调用 removeEventListener 了。脱离 DOM 的引用有时,保存 DOM 节点内部数据结构很有用。假如你想快速更新表格的几行内容,把每一行 DOM 存成字典(JSON 键值对)或者数组很有意义。此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。将来你决定删除这些行时,需要把两个引用都清除。var elements = { button: document.getElementById(‘button’), image: document.getElementById(‘image’), text: document.getElementById(’text’)};function doStuff() { image.src = ‘http://some.url/image'; button.click(); console.log(text.innerHTML); // 更多逻辑}function removeButton() { // 按钮是 body 的后代元素 document.body.removeChild(document.getElementById(‘button’)); // 此时,仍旧存在一个全局的 #button 的引用 // elements 字典。button 元素仍旧在内存中,不能被 GC 回收。}此外还要考虑 DOM 树内部或子节点的引用问题。假如你的 JavaScript 代码中保存了表格某一个 <td> 的引用。将来决定删除整个表格的时候,直觉认为 GC 会回收除了已保存的 <td> 以外的其它节点。实际情况并非如此:此 <td> 是表格的子节点,子元素与父元素是引用关系。由于代码保留了 <td> 的引用,导致整个表格仍待在内存中。保存 DOM 元素引用的时候,要小心谨慎。闭包闭包是 JavaScript 开发的一个关键方面:匿名函数可以访问父级作用域的变量。避免滥用本节参考文章:4类 JavaScript 内存泄漏及如何避免13.引用计数 / 标记清除js垃圾回收有两种常见的算法:引用计数和标记清除。引用计数就是跟踪对象被引用的次数,当一个对象的引用计数为0即没有其他对象引用它时,说明该对象已经无需访问了,因此就会回收其所占的内存,这样,当垃圾回收器下次运行就会释放引用数为0的对象所占用的内存。标记清除法是现代浏览器常用的一种垃圾收集方式,当变量进入环境(即在一个函数中声明一个变量)时,就将此变量标记为“进入环境”,进入环境的变量是不能被释放,因为只有执行流进入相应的环境,就可能会引用它们。而当变量离开环境时,就标记为“离开环境”。垃圾收集器在运行时会给储存在内存中的所有变量加上标记,然后会去掉环境中的变量以及被环境中的变量引用的变量的标记,当执行完毕那些没有存在引用 无法访问的变量就被加上标记,最后垃圾收集器完成清除工作,释放掉那些打上标记的变量所占的内存。 function problem() { var A = {}; var B = {}; A.a = B; B.a = A;}引用计数存在一个弊端就是循环引用问题(上边)标记清除不存在循环引用的问题,是因为当函数执行完毕之后,对象A和B就已经离开了所在的作用域,此时两个变量被标记为“离开环境”,等待被垃圾收集器回收,最后释放其内存。分析以下代码: function createPerson(name){ var localPerson = new Object(); localPerson.name = name; return localPerson; } var globalPerson = createPerson(“Junga”); globalPerson = null;//手动解除全局变量的引用在这个????中,变量globalPerson取得了createPerson()函数的返回的值。在createPerson()的内部创建了一个局部变量localPerson并添加了一个name属性。由于localPerson在函数执行完毕之后就离开执行环境,因此会自动解除引用,而对于全局变量来说则需要我们手动设置null,解除引用。不过,解除一个值的引用并不意味着自动回收该值所占用的内存,解除引用真正的作用是让值脱离执行环境,以便垃圾收集器下次运行时将其收回。本节参考文章:JavaScript的内存问题14.前后端路由差别1.后端每次路由请求都是重新访问服务器2.前端路由实际上只是JS根据URL来操作DOM元素,根据每个页面需要的去服务端请求数据,返回数据后和模板进行组合。本节参考文章:2018前端面试总结…15.window.history / location.hash通常 SPA 中前端路由有2种实现方式:window.historylocation.hash下面就来介绍下这两种方式具体怎么实现的一.history1.history基本介绍window.history 对象包含浏览器的历史,window.history 对象在编写时可不使用 window 这个前缀。history是实现SPA前端路由是一种主流方法,它有几个原始方法:history.back() - 与在浏览器点击后退按钮相同history.forward() - 与在浏览器中点击按钮向前相同history.go(n) - 接受一个整数作为参数,移动到该整数指定的页面,比如go(1)相当于forward(),go(-1)相当于back(),go(0)相当于刷新当前页面如果移动的位置超出了访问历史的边界,以上三个方法并不报错,而是静默失败在HTML5,history对象提出了 pushState() 方法和 replaceState() 方法,这两个方法可以用来向历史栈中添加数据,就好像 url 变化了一样(过去只有 url 变化历史栈才会变化),这样就可以很好的模拟浏览历史和前进后退了,现在的前端路由也是基于这个原理实现的。2.history.pushStatepushState(stateObj, title, url) 方法向历史栈中写入数据,其第一个参数是要写入的数据对象(不大于640kB),第二个参数是页面的 title, 第三个参数是 url (相对路径)。stateObj :一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。关于pushState,有几个值得注意的地方:pushState方法不会触发页面刷新,只是导致history对象发生变化,地址栏会有反应,只有当触发前进后退等事件(back()和forward()等)时浏览器才会刷新这里的 url 是受到同源策略限制的,防止恶意脚本模仿其他网站 url 用来欺骗用户,所以当违背同源策略时将会报错3.history.replaceStatereplaceState(stateObj, title, url) 和pushState的区别就在于它不是写入而是替换修改浏览历史中当前纪录,其余和 pushState一模一样4.popstate事件定义:每当同一个文档的浏览历史(即history对象)出现变化时,就会触发popstate事件。注意:仅仅调用pushState方法或replaceState方法 ,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用JavaScript调用back、forward、go方法时才会触发。另外,该事件只针对同一个文档,如果浏览历史的切换,导致加载不同的文档,该事件也不会触发。用法:使用的时候,可以为popstate事件指定回调函数。这个回调函数的参数是一个event事件对象,它的state属性指向pushState和replaceState方法为当前URL所提供的状态对象(即这两个方法的第一个参数)。5.history实现spa前端路由代码<a class=“api a”>a.html</a><a class=“api b”>b.html</a> // 注册路由 document.querySelectorAll(’.api’).forEach(item => { item.addEventListener(‘click’, e => { e.preventDefault(); let link = item.textContent; if (!!(window.history && history.pushState)) { // 支持History API window.history.pushState({name: ‘api’}, link, link); } else { // 不支持,可使用一些Polyfill库来实现 } }, false) }); // 监听路由 window.addEventListener(‘popstate’, e => { console.log({ location: location.href, state: e.state }) }, false)popstate监听函数里打印的e.state便是history.pushState()里传入的第一个参数,在这里即为{name: ‘api’}二.Hash1.Hash基本介绍url 中可以带有一个 hash http://localhost:9000/#/a.htmlwindow 对象中有一个事件是 onhashchange,以下几种情况都会触发这个事件:直接更改浏览器地址,在最后面增加或改变#hash;通过改变location.href或location.hash的值;通过触发点击带锚点的链接;浏览器前进后退可能导致hash的变化,前提是两个网页地址中的hash值不同。2.Hash实现spa前端路由代码 // 注册路由 document.querySelectorAll(’.api’).forEach(item => { item.addEventListener(‘click’, e => { e.preventDefault(); let link = item.textContent; location.hash = link; }, false) }); // 监听路由 window.addEventListener(‘hashchange’, e => { console.log({ location: location.href, hash: location.hash }) }, false)本节参考文章:vue 单页应用(spa)前端路由实现原理 ...

January 13, 2019 · 5 min · jiezi

前端常用代码片段(四)

前端常用代码片段(一) 点这里前端常用代码片段(二) 点这里前端常用代码片段(三) 点这里前端常用代码片段(四) 点这里前端常用代码片段(五) 点这里前端常用代码片段(六) 点这里1.简述一下你对HTML语义化的理解?并写出一段语义化的HTML?语义化是指根据内容的结构化(内容语义化),选择合适的标签(代码语义化),便于开发者阅读和写出更优雅的代码的同时,让浏览器的爬虫和机器很好的解析。用正确的标签做正确的事情html语义化让页面的内容结构化,结构更清晰,便于对浏览器、搜索引擎解析语义化的HTML在没有CSS的情况下也能呈现较好的内容结构与代码结构搜索引擎的爬虫也依赖于HTML标记来确定上下文和各个关键字的权重,利于SEO;使阅读源代码的人对网站更容易将网站分块,便于阅读维护理解HTML5增加了许多语义化标签如:header footer nav article ……语义化HTML示例:<!– 这是开放的 –><header> <h1>header</h1></header><section class=“main”> main</section><aside>aside</aside><footer> footer</footer>2. HTML5有哪些新特性、移除了那些元素?如何处理HTML5新标签的浏览器兼容问题?如何区分 HTML 和 HTML5?HTML5 是定义 HTML 标准的最新的版本。 该术语表示两个不同的概念:它是一个新版本的HTML语言,具有新的元素,属性和行为,它有更大的技术集,允许更多样化和强大的网站和应用程序。这个集合有时称为HTML5和朋友,通常缩写为HTML5。HTML5新特性:HTML5 现在已经不是 SGML 的子集,主要是关于图像,位置,存储,多任务等功能的增加绘画 canvas;用于媒介回放的 video 和 audio 元素;本地离线存储 localStorage 长期存储数据,浏览器关闭后数据不丢失sessionStorage 的数据在浏览器关闭后自动删除语意化更好的内容元素,比如 article、footer、header、nav、section表单控件(input type),calendar、date、time、email、url、search新的技术webworker, websocket, Geolocation移除元素:纯表现的元素basefont ,big,center,font, s,strike,tt,u对可用性产生负面影响的元素:frame,frameset,noframes处理HTML5新标签的浏览器兼容问题:IE8/IE7/IE6支持通过document.createElement方法产生的标签,可以利用这一特性让这些浏览器支持HTML5新标签,浏览器支持新标签后,还需要添加标签默认的样式。可直接使用成熟的框架、比如html5shiv<!–[if lt IE 9]> <script src=“html5shiv.js”></script><![endif]–>如何区分 HTML 和 HTML5:DOCTYPE声明新增元素3. 为什么要初始化CSS样式(reset css)?因为浏览器的兼容问题,不同浏览器对有些标签的默认值是不同的,如果没对CSS初始化往往会出现浏览器之间的页面显示差异最简单粗暴的* { margin: 0; padding: 0;}更好的选择Normalize.css 相比于传统的CSS reset,Normalize.css是一种现代的、为HTML5准备的优质替代方案Reset CSS:只选对的,不选"贵"的,因根据具体项目来做选择权衡,不应该滥用css定义的权重?页面显示样式的优先级取决于其“特殊性”’,特殊性越高,就显示最高的,当特殊性相等时,显示后者特殊性表述为4个部分:0,0,0,0一个选择器的特殊性如下确定:对于选择器是#id的属性值,特殊性值为:0,1,0,0对于属性选择器,class或伪类,特殊性值为:0,0,1,0对于标签选择器或伪元素,特殊性值为:0,0,0,1通配符‘’对特殊性值为:0,0,0,0内联样式特殊性值为:1,0,0,04. 讲讲position的值relative和absolute的区别?absolute:生成绝对定位的元素,相对于值不为 static的第一个父元素进行定位relative:生成相对定位的元素,相对于其正常位置进行定位5. 如何水平垂直居中div?(至少给出2种解决方法)1.absolute + transform:<div class=“parent”> <div class=“child”>Demo</div></div><style> .parent { position: relative; } .child { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); }</style>2.inline-block + text-align + table-cell + vertical-align<div class=“parent”> <div class=“child”>Demo</div></div><style> .parent { text-align: center; display: table-cell; vertical-align: middle; } .child { display: inline-block; }</style>3.flex + justify-content + align-items<div class=“parent”> <div class=“child”>Demo</div></div><style> .parent { display: flex; justify-content: center; / 水平居中 */ align-items: center; /垂直居中/ }</style>6. 渐进增强 VS 优雅降级,你怎么看?渐进增强(Progressive Enhancement):一开始就针对低版本浏览器进行构建页面,完成基本的功能,然后再针对高级浏览器进行效果、交互、追加功能达到更好的体验优雅降级(Graceful Degradation):一开始就构建站点的完整功能,然后针对浏览器测试和修复。比如一开始使用 CSS3 的特性构建了一个应用,然后逐步针对各大浏览器进行 hack 使其可以在低版本浏览器上正常浏览7. JavaScript 数组去重?(简述思路即可)遍历数组法: 这应该是最简单的去重方法(实现思路:新建一新数组,遍历数组,值不在新数组就加入该新数组中)// 遍历数组去重法function unique(arr){ var _arr = [] //遍历当前数组 for(var i = 0; i < arr.length; i++){ //如果当前数组的第i已经保存进了临时数组,那么跳过, //否则把当前项push到临时数组里面 if (_arr.indexOf(arr[i]) == -1) _arr.push(arr[i]) } return _arr}注意点:indexOf 为 ES5 的方法,注意浏览器兼容,需要自己实现 indexOf对象键值对(hash) 法:速度快,高效,占用更大的内存换取更快的时间,用 JavaScript 中的 Object 对象来当做哈希表,hash去重的核心是构建了一个 hash 对象来替代 indexOf// hash 去重法function unique(arr){ var _arr = [], hash = {} for (var i = 0; i < arr.length; i++) { var item = arr[i] var key = typeof(item) + item // 对象的键值只能是字符串, typeof(item) + item来去分1和'1’的情况 if(hash[key] !== 1){ _arr.push(item) hash[key] = 1 } } return _arr}炫酷的 es6 Set数据结构: ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值function unique(arr){ return Array.from(new Set(arr)) // Array.from方法用于将两类对象转为真正的数组: // 类似数组的对象(array-like object)和可遍历(iterable)的对象}8. 使用原生ajax获取 Linus Torvalds 的GitHub信息(API:api.github.com/users/torva…),并将JSON字符串解析为JSON对象,并讲讲对JSON的了解这是对 ajax与json的考察ajax的全称:Asynchronous Javascript And XML,异步传输+js+xml 现在差不多都用JSON创建XMLHttpRequest对象,也就是创建一个异步调用对象创建一个新的HTTP请求,并指定该HTTP请求的方法、URL及验证信息设置响应HTTP请求状态变化的函数发送HTTP请求获取异步调用返回的数据数据处理下面就来贴代码吧:var api = ‘https://api.github.com/users/torvalds'var xhr = new XMLHttpRequest() // 创建XMLHttpRequest对象if(window.XMLHttpRequest){ // 兼容处理 xhr = new XMLHttpRequest()}else{ xhr = new ActiveXObject(‘Microsoft.XMLHTTP’)// 兼容ie6以下下}xhr.open(‘get’,api,true) //设置请求信息 xhr.send() //提交请求//等待服务器返回内容xhr.onreadystatechange = function() { if ( xhr.readyState == 4 && xhr.status == 200 ) { console.log(JSON.parse(xhr.responseText)) // 使用JSON.parse解析JSON字符串 } }JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。它是基于JavaScript的一个子集。数据格式简单, 易于读写, 占用带宽小 如:{“age”:“12”, “name”:“back”}JSON.parse() 方法解析一个JSON字符串JSON.stringify() 方法将一个JavaScript值转换为一个JSON字符串9. 简单谈谈前端性能优化减少http请求次数:CSS Sprites, JS、CSS源码压缩、图片大小控制合适;网页Gzip,CDN托管,data缓存 ,图片服务器。减少DOM操作次数,优化javascript性能。少用全局变量、缓存DOM节点查找的结果。减少IO读取操作。尽量避免使用CSS Expression(css表达式)又称Dynamic properties(动态属性)。图片预加载,将样式表放在顶部,将脚本放在底部。10. 费波纳茨数组就是当前项等于前两项的和var arr=[];for(var i=0;i<10;i++ ){ i<=1?arr.push(1):arr.push(arr[i-1]+arr[i-2]);}console.log(arr)11.数据排列执行num(1,5),返回'123454321’执行num(2,5),返回'23456765432’方法1:var num = function(n,m){ var arr = [] var len=(m-n)*2+1 for(var i=0;i<len;i++){ n<m?(arr.push(n++)):(arr.push(m–)) } return arr.join()}num(2,5)方法2:var num = function (n,m) { let arr = [m] for(let i = m - 1; i >= n; i–){ arr.push(i); arr.unshift(i) } return arr.join()}num(2,5)12.翻转一个字符串let a=“hello word”;let b=[…str].reverse().join("");//drow olleh13.setInterval 时间是否会有误差?产生误差的原因?其原理是什么?setInterval异步函数,异步执行,js被解析的时候,碰到他,先不解析他,放他在一旁,先去解析同步的,等资源空闲下来的才去解析他,这样一来,解析其他代码肯定需要时间,这不就有延误嘛。然后解析setInterval内部函数不也一样需要耗时,函数简单些还好写,你要是写了一大堆,可能产生的延误就不是一点点的;14.布局方式弹性布局固定布局流体布局混合布局绝对定位布局15.清除浮动的方式:父级div定义height最后一个浮动元素后加空div标签 并添加样式clear:both。包含浮动元素的父标签添加样式overflow为hidden或auto。父级div定义zoom16.怎么判断两个对象相等?obj={ a:1, b:2}obj2={ a:1, b:2}obj3={ a:1, b:2}JSON.stringify(obj)==JSON.stringify(obj2);//trueJSON.stringify(obj)==JSON.stringify(obj3);//false17.ES6强制参数ES6提供了默认参数的概念,当函数的参数未传入或者传入值为 undefined 时,会应用参数的默认值。默认值可以是个表达式,所以我们可以将默认值设置为一个执行函数,如果该参数没有传值,就会执行我们的默认函数:const required = () => {throw new Error(‘Missing parameter’)};//The below function will throw an error if either “a” or “b” is missing.const add = (a = required(), b = required()) => a + b;add(1, 2) //3add(1) // Error: Missing parameter.18. 强大的 reduce之前只是用过reduce做过数组求和,现在发现一些新的用法,原来 reduce 这么强大。基础部分reduce()方法接收一个函数callbackfn作为累加器(accumulator),数组中的每个值(从左到右)开始合并,最终为一个值。语法array.reduce(callbackfn,[initialValue])reduce()方法接收callbackfn函数,而这个函数包含四个参数:function callbackfn(preValue,curValue,index,array){}preValue: 上一次调用回调返回的值,或者是提供的初始值(initialValue)curValue: 数组中当前被处理的数组项index: 当前数组项在数组中的索引值array: 调用 reduce()方法的数组而initialValue作为第一次调用 callbackfn函数的第一个参数。1.没有initialValue初始值得情况var arr = [0,1,2,3,4];arr.reduce(function(preValue,curValue,index,array){ return preValue + curValue; }); // 10示例中的回调函数被执行四次,每次参数和返回的值如下:2.有initialValue初始值得情况var arr = [0,1,2,3,4];arr.reduce(function (preValue,curValue,index,array) { return preValue + curValue;}, 5); //15reduce()方法会执行五次回调,每次参数和返回的值如下:基础部分截取自 大漠 - JavaScript学习笔记… 全部内容可点击链接查看实例部分1.使用 reduce 替代 map + filter设想你有这么个需求:要把数组中的值进行计算后再滤掉一些值,然后输出新数组。很显然我们一般使用 map 和 filter 方法组合来达到这个目的,但这也意味着你需要迭代这个数组两次。来看看我们如何使用 reduce 只迭代数组一次,来完成同样的结果。下面这个例子我们需要把数组中的值乘 2 ,并返回大于 50 的值:const numbers = [10, 20, 30, 40];const doubledOver50 = numbers.reduce((finalList, num) => { num = num * 2; //double each number (i.e. map) //filter number > 50 if (num > 50) { finalList.push(num); } return finalList;}, []);doubledOver50; // [60, 80]2.使用 reduce 检测括号是否对齐封闭下面这个例子我们用 reduce 来检测一段 string 中的括号是否前后对应封闭。思路是定义一个名为 counter 的变量,它的初始值为 0 ,然后迭代字符串,迭代过程中碰到(就加 1,碰到)就减 1,如果括号前后对应的话,最终couter的值会是 0。//Returns 0 if balanced.const isParensBalanced = (str) => { return str.split(’’).reduce((counter, char) => { if(counter < 0) { //matched “)” before “(” return counter; } else if(char === ‘(’) { return ++counter; } else if(char === ‘)’) { return –counter; } else { //matched some other char return counter; } }, 0); //<– starting value of the counter}isParensBalanced(’(())’) // 0 <– balancedisParensBalanced(’(asdfds)’) //0 <– balancedisParensBalanced(’(()’) // 1 <– not balancedisParensBalanced(’)(’) // -1 <– not balanced3.使用 reduce 计算数组中的重复项如果你想计算数组中的每个值有多少重复值,reduce 也可以快速帮到你。下面的例子我们计算数组中每个值的重复数量,并输出一个对象来展示:var carsObj = cars.reduce(function (obj, name) { obj[name] = obj[name] ? ++obj[name] : 1; return obj;}, {});carsObj; // => { BMW: 2, Benz: 2, Tesla: 1, Toyota: 1 }实例部分截取自 ES6 的几个小技巧 全部内容可点击链接查看19.用对象解构移除一个对象中的某些属性有时你可能希望移除一个对象中的某些属性,我们一般会通过迭代这个对象(如 for..in 循环)来移除那些我们不想要的属性。实际上我们可以通过对象解构的方法将不想要的属性提取出来,并将想留下来的变量保存在rest 参数中。在下面的这个例子中,我们从对象中移除_internal和tooBig这两个属性:let {_internal, tooBig, …cleanObject} = { el1: ‘1’, el2: ‘2’, el3: ‘3’, tooBig:{}, _internal:“secret”};console.log(cleanObject); // {el1: ‘1’, el2: ‘2’, el3: ‘3’}拓展:1.嵌套对象解构let {model, engine: {vin,…uuu} } = { model: ‘bmw 2018’, engine: { v6: true, turbo: true, vin: 12345 }}console.log(uuu); // {v6: true, turbo: true}console.log(vin); // 12345console.log(model); // ‘bmw 2018’console.log(engine); // Uncaught ReferenceError: engine is not defined2.合并对象合并两个对象,新对象中相同的属性会被放在后面的对象覆盖:let object1 = { a:1, b:2,c:3 }let object2 = { b:30, c:40, d:50}let merged = {…object1, …object2} //spread and re-add into mergedconsole.log(merged) // {a:1, b:30, c:40, d:50}20.判断一个数是否是整数function isInteger(x) { return (x ^ 0) === x; } function isIntefer(x){ return (typeof x === ’number’) && (x % 1 === 0); //返回布尔}参考文章:1.12个常规前端面试题及小结2.ES6 的几个小技巧 ...

January 13, 2019 · 4 min · jiezi

前端常用代码片段(五)

前端常用代码片段(一) 点这里前端常用代码片段(二) 点这里前端常用代码片段(三) 点这里前端常用代码片段(四) 点这里1.tap事件点透问题?问题点击穿透问题:点击蒙层(mask)上的关闭按钮,蒙层消失后发现触发了按钮下面元素的click事件zepto的tap事件是绑定到document的,所以一般点击tap事件都会冒泡到document才会触发。当点击隐藏蒙层的时候默认也会手指触发到蒙层下面的元素 执行事件1. github上有个fastclick插件,用来规避click事件的延时执行2. 用touchend代替tap事件并阻止掉touchend的默认行为preventDefault()//tap事件出现点透问题$("#id").on(“tap”, function (event) { //很多处理比如隐藏什么的 event.preventDefault();});//touchend事件解决点头问题$("#id").on(“touchend”, function (event) { //很多处理比如隐藏什么的 event.preventDefault();});3:给tap事件里面的隐藏蒙层方法执行的方法300毫秒延迟$("#id").on(’tap’,function(ev){ setTimeout(function(){ $("#id").hide(); },320)})2.固定定位布局 键盘挡住输入框内容?1.通过绑定窗口改变事件,监听键盘的弹出。然后去改变固定定位元素的位置。默认键盘的宽度应该是页面的2分之一。所以我们位移的距离改成键盘的二分之一就可以window.onresize = function(){ //$(".mian")就是固定定位的元素 if($(".mian").css(’top’).replace(‘px’,’’) != 0){ $(".mian").css(’top’,0); }else{ var winHeight = $(window).height(); $(".mian").css(’top’,-(winHeight/4)); }}2.通过定时器实时监听是否触发input。如果触发input框 就把固定定位,改变成静态定位。这样就会浏览器会总动把内容顶上去。function fixedWatch(el) { //activeElement 获取焦点元素 if(document.activeElement.nodeName == ‘INPUT’) { el.css(‘position’, ‘static’); } else { el.css(‘position’, ‘fixed’); }}setInterval(function() { fixedWatch($(’.mian’));}, 500);3.去掉字符左右空格export const trimLeOrRi=(str)=>{ //删除左右两端的空格 return str.replace(/(^\s*)|(\s*$)/g, “”); }4.通过JS判断一个数组4.1 instanceof方法instanceof 运算符是用来测试一个对象是否在其原型链原型构造函数的属性var arr = []; arr instanceof Array; // true4.2.constructor方法constructor属性返回对创建此对象的数组函数的引用,就是返回对象相对应的构造函数var arr = []; arr.constructor == Array; //true4.3.toString.call这种写法,是 jQuery 正在使用的Object.prototype.toString.call(obj).slice(8,-1) == Array4.4.ES5新增方法isArray()var a = new Array(123);var b = new Date();console.log(Array.isArray(a)); //trueconsole.log(Array.isArray(b)); //false字符串也是有length属性的我们知道所有的Array都是有length属性的,就算是空数组,那么length 为0,那么字符串有没有呢?接下来我们来验证一下。var str=“sdfsd5565s6dfsd65sd6+d5fd5”;console.log(str.length) // 26结果是有的,所以我们在判断类型时,不能单纯拿有没有length属性来判断是不是数组了,我们可以用下面的方法来判断是否是数组:var obj=[1,2] ;console.log(toString.call(obj) === ‘[object Array]’);5. 删除数组尾部元素一个简单的用来清空或则删除数组尾部元素的简单方法就是改变数组的length属性值。const arr = [11, 22, 33, 44, 55, 66];// truncantingarr.length = 3;console.log(arr); //=> [11, 22, 33]// clearingarr.length = 0;console.log(arr); //=> []console.log(arr[2]); //=> undefined6.使用对象解构来处理数组可以使用对象解构的语法来获取数组的元素:const csvFileLine = ‘1997,John Doe,US,john@doe.com,New York’;const { 2: country, 4: state } = csvFileLine.split(’,’);7.在switch语句中用范围值可以使用下面的技巧来写满足范围值的switch语句://不推荐使用,只开阔眼界function getWaterState(tempInCelsius) { let state; switch (true) { case (tempInCelsius <= 0): state = ‘Solid’; break; case (tempInCelsius > 0 && tempInCelsius < 100): state = ‘Liquid’; break; default: state = ‘Gas’; } return state;}//推荐function getWaterState2(tempInCelsius) { if (tempInCelsius <= 0) { return ‘Solid’; } if (tempInCelsius < 100) { return ‘Liquid’; } return ‘Gas’;}8.平铺多维数组方法1:es6使用Spread操作,可以很容易去平铺嵌套多维数组:const arr = [11, [22, 33], [44, 55], 66];const flatArr = [].concat(…arr); //=> [11, 22, 33, 44, 55, 66]可惜,上面的方法仅仅适用于二维数组。不过,通过递归,我们可以平铺任意维度的嵌套数组。unction flattenArray(arr) { const flattened = [].concat(…arr); return flattened.some(item => Array.isArray(item)) ? flattenArray(flattened) : flattened;}const arr = [11, [22, 33], [44, [55, 66, [77, [88]], 99]]];const flatArr = flattenArray(arr); //=> [11, 22, 33, 44, 55, 66, 77, 88, 99]方法2:递归function flatten(arr){ var res = []; for(var i=0;i<arr.length;i++){ if(Array.isArray(arr[i])){ res = res.concat(flatten(arr[i])); }else{ res.push(arr[i]); } } return res;}方法3:reducefunction flatten(arr){ return arr.reduce(function(prev,item){ return prev.concat(Array.isArray(item)?flatten(item):item); },[]);}本节参考文章:5分钟掌握JavaScript小技巧9.如果数组列表太大,以下递归代码将导致堆栈溢出。你如何解决这个问题,仍然保留递归模式?var list = readHugeList();var nextListItem = function() { var item = list.pop(); if (item) { // process the list item… nextListItem(); }};通过修改nextListItem函数可以避免潜在的堆栈溢出,如下所示:var list = readHugeList();var nextListItem = function() { var item = list.pop(); if (item) { // process the list item… setTimeout( nextListItem, 0); }};堆栈溢出被消除,因为事件循环处理递归,而不是调用堆栈。当nextListItem运行时,如果item不为null,则将超时函数(nextListItem)推送到事件队列,并且函数退出,从而使调用堆栈清零。当事件队列运行超时事件时,将处理下一个项目,并设置一个计时器以再次调用nextListItem。因此,该方法从头到尾不经过直接递归调用即可处理,因此调用堆栈保持清晰,无论迭代次数如何。10.10!function f(n){ return (n > 1) ? n * f(n-1) : n}11.移动端底部input被弹出的键盘遮挡Element.scrollIntoView():方法让当前的元素滚动到浏览器窗口的可视区域内。document.querySelector(’#inputId’).scrollIntoView();//只要在input的点击事件,或者获取焦点的事件中,加入这个api就好了这个api还可以设置对齐方法,选择将input放在屏幕的上方/下方,类似的api还有: Element.scrollIntoViewIfNeeded(),这两个是解决同一个问题的,选择一个用就可以了。window.addEventListener(‘resize’, function() { if( document.activeElement.tagName === ‘INPUT’ || document.activeElement.tagName === ‘TEXTAREA’ ) { window.setTimeout(function() { if(‘scrollIntoView’ in document.activeElement) { document.activeElement.scrollIntoView(); } else { document.activeElement.scrollIntoViewIfNeeded(); } }, 0); }});本节参考文章:关于input的一些问题解决方法分享及评论12.控制input显/隐密码这个就很简单了,只需更改input的type属性值就可以了。可以看一下codepen的demo//点击函数,获取dom,判断更改属性。show(){ let input=document.getElementById(“inputId”); if(input.type==“password”){ input.type=‘text’; }else{ input.type=‘password’; } }本节参考文章:关于input的一些问题解决方法分享13.input多行输入显示换行在使用textarea标签输入多行文本的时候,如果没有对多行文本显示处理,会导致没有换行的情况,如果用户需要换行…解决方法:只要在显示内容的地方将该属性设置为 white-space: pre-line 或者 white-space:pre-wrap,多行文本就可以换行了。备注:white-space 属性用于设置如何处理元素内的空白,其中包括空白符和换行符。14.输入框首尾清除空格-trim()输入框清除首尾空格是input较为常见的需求,通常在上传的时候将首尾空格去除掉。原生清除方法://原生方法获取值,清除首尾空格返回一个新的字符串var str2 = document.getElementById(“inputId”).trim();Vue清除方法:<input v-model.trim=“msg”>15.在input中监听键盘事件在用户登录或者搜索框的时候,一般都会监听键盘事件绑定回车按键,来执行登录/搜索 等操作。原生绑定:<input onkeydown=“keydownMsg(event)” type=“text” />function keydownMsg(key) { keyCode = key.keyCode; //获取按键代码 if (keyCode == 13) { //判断按下的是否为回车键 // 在input上监听到回车 do something }}Vue按键修饰符Vue为监听键盘事件,提供了按键修饰符,并且为常用的按键提供了别名,使用方法如下:当回车按键在input中被按下的时候,会触发里面的函数。<input @keyup.enter=“enterActive”>16.元素滚动到浏览器窗口的可视区域Element.scrollIntoView()方法让当前的元素滚动到浏览器窗口的可视区域内。Element.scrollIntoViewIfNeeded()方法也是用来将不在浏览器窗口的可见区域内的元素滚动到浏览器窗口的可见区域。但如果该元素已经在浏览器窗口的可见区域内,则不会发生滚动。因而再有什么回到顶部、去到置顶位置和键盘弹出挡住输入框之类的需求,都可以简单解决了。scrollIntoViewscrollIntoView只接受一个参数,但接受两种类型的参数,分别是Boolean型参数和Object型参数。先说Boolean型参数,参数可以使true和false。如果为true,元素的顶端将和其所在滚动区的可视区域的顶端对齐。若为false,元素的底端将和其所在滚动区的可视区域的底端对齐。<body> <div class=“chunk”></div> <div class=“btn-top”>up</div> <div class=“btn-bottom”>down</div> <script> const up = document.querySelector(’.btn-top’); const down = document.querySelector(’.btn-bottom’); const test = document.querySelector(’.chunk’); up.addEventListener(‘click’, function() { test.scrollIntoView(true); }); down.addEventListener(‘click’, function() { test.scrollIntoView(false); }); </script></body>Object型参数,这个对象有两个属性,属性block 值可以是start和end;属性behavior 值auto、instant和smoothup.addEventListener(‘click’, function() { test.scrollIntoView({ block: ‘start’, behavior: ‘smooth’ });});down.addEventListener(‘click’, function() { test.scrollIntoView({ block: ’end’, behavior: ‘smooth’ });});scrollIntoViewIfNeededscrollIntoViewIfNeeded可以接受一个Boolean型参数,和scrollIntoView不同,true为默认值,但不是滚动到顶部,而是让元素在可视区域中居中对齐;false时元素可能顶部或底部对齐,视乎元素靠哪边更近。两者主要区别有两个。首先是scrollIntoViewIfNeeded是比较懒散的,如果元素在可视区域,那么调用它的时候,页面是不会发生滚动的。其次是scrollIntoViewIfNeeded只有Boolean型参数,也就是说,都是瞬间滚动,没有动画的可能了。兼容性的话scrollIntoView:Boolean型参数几乎随便用了scrollIntoViewIfNeeded:IE和FireFox全红,移动端基本都OK详细见原文本节参考文章:scrollIntoView…17.ES6 中的 解构运算符 …… 每次只能展开最外层的数组,被 [].concat 后,arr 就扁平化一次。function flatten(arr){ while(arr.some(item => Array.isArray(item))){ arr = [].concat(…arr); } return arr;}const arr = [1, [2, [3, 4]]];console.log(flatten(arr));本节参考文章:数组扁平化18.reverse数组const reverse = xs => { if (xs.length === 1) return xs; const [head, …tail] = xs; return reverse(tail).concat(head);};reverse([1,2,3,4,5,6]) // [6,5,4,3,2,1]19.正则实现trim()功能function myTrim(str) { let reg = /^\s+|\s+$/g; return str.replace(reg, “”);}console.log(myTrim(’ asdf ‘));本节参考文章:2018前端面试总结…20.js 数组每一项去除空格var s = “222 , 3334, 3666 “s.replace(/\s/g,””) // “222,3334,3666” ...

January 13, 2019 · 3 min · jiezi

前端常用代码片段(六)

本文总结的代码片段(六)–持续更新前端常用代码片段(一) 点这里前端常用代码片段(二) 点这里前端常用代码片段(三) 点这里前端常用代码片段(四) 点这里前端常用代码片段(五) 点这里前端常用代码片段(六) 点这里1.多彩的console.logconsole.log( ‘Nothing here %cHi Cat %cHey Bear’, // Console Message ‘color: blue’, ‘color: red’ // CSS Style);const styles = [‘color: green’, ‘background: yellow’].join(’;’);const message = ‘Some Important Message Here’;// 3. 传入styles和message变量console.log(’%c%s’, styles, message);本节参考文章:多彩的console.log2. 版本号比较9_11_1和9_2_910.11.111和10.2.2function version( v1, v2 ) { var arr1 = v1.replace(/[-]/g,’.’).split(’.’); var arr2 = v2.replace(/[-]/g,’.’).split(’.’); console.log(arr1,arr2); var len = Math.max(arr1.length, arr2.length); for ( var i = 0; i < len; i++ ) { if(parseInt(arr1[i]) == parseInt(arr2[i])) continue; return parseInt(arr1[i]) < parseInt(arr2[i]) ? true :false; } return false;}本节参考文章:如何比较版本号大小 ...

January 13, 2019 · 1 min · jiezi

第二章队列

什么是队列队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。允许插入的端是队尾,允许删除的端是队头。 队列是一个先进先出的线性表 。队列结构概念:入队、出队、队列头、队列尾js实现栈结构依靠数组 方法名 操作enqueue 入队dequeue 出队front 查看队列头isEmpty 检查队列是否为空size 获取队列大小clear 移除全部元素代码实现class Queue { constructor() { // 私有属性 this._items = []; } // 入队 enqueue(el) { this._items.push(el); } // 出队 dequeue(el) { this._items.shift(el); } // 查看队列头 front() { return this._items[this._items.length - 1]; } // 检查队列是否为空 isEmpty() { return this._items.length === 0; } // 查看队列的大小 size() { return this._items.length; } // 清除队列 clear() { this._items = []; }}下面来个例子炒股游戏:玩家猜涨跌,猜错了就出局,游戏最终结果为玩家全部输了为什么是炒股游戏:找不到图你让我怎么办,没办法只能随便编了下面开始码了// 玩家列表let arr = [‘a’, ‘b’, ‘c’, ’d’, ’e’, ‘f’, ‘g’, ‘h’, ‘i’, ‘j’];// 游戏规则function regulation() { return Math.random() > 0.5}function stocks() { let queue = new Queue() // 入队 arr.forEach(item => { queue.enqueue(item) }) // 当队列为空,说明全部出队,游戏结束 while (!queue.isEmpty()) { for (let i = 0; i < arr.length; i++) { // 为true说明玩家猜对了 if (regulation()) { // 循环队列 queue.enqueue(queue.dequeue()) console.log(玩家${arr[i]}赢了) } else { // 出队 queue.dequeue() console.log(玩家${arr[i]}输了) } } } console.log(‘全部玩家输了’)}此章完结,下一章链表欢迎关注,以便第一时间获取最新的文章 ...

January 13, 2019 · 1 min · jiezi

JS版数据结构(第一章栈)

什么是栈栈作为一种数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。栈有什么作用栈是非常底层的东西,在编程语言的编译器和内存中保存变量、方法调用。栈结构概念:入栈 、出栈、栈顶、栈底。js实现栈结构依靠数组方法名 操作push 栈顶添加元素pop 栈顶移除元素peek 查看栈顶isEmpty 检查栈是否为空clear 移除全部元素size 获取栈的大小下面代码实现Stack类class Stack { constructor() { // 私有属性 this._items = []; } // 栈顶添加元素 push(el) { this._items.push(el); } // 栈顶移除元素 pop(el) { return this._items.pop(el); } // 检查栈顶 peek() { return this._items[this._items.length - 1]; } // 检查栈是否为空 isEmpty() { return this._items.length === 0; } // 清除栈 clear() { this._items = []; } // 查看栈的大小 size() { return this._items.length; }}下面来个实例// 十进制转二进制function conversionbinary(number) { let stack = new Stack(); let remainder; let binary = ‘’; // 进栈 while (number > 0) { remainder = number % 2; stack.push(remainder); number = Math.floor(number / 2); } // 出栈 while (!stack.isEmpty()) { binary += stack.pop(); } return binary;}大家可能想实现一个十进制转二进制还有其他方法,不必要这么复杂。不知道大家有没有想过,即使使用其他方法实现十进制转二进制,其思想也是栈的思想。此章完结,下一章队列欢迎大家关注,以便第一时间看到更新的文章 ...

January 13, 2019 · 1 min · jiezi

JavaScript五十问——浅入深出,自己实现一个 ES 6 Promise

说到 ES6,Promise 是绕不过的问题;如果说 ES6 的 Class 是基于 Javascript 原型继承的封装,那么 Promise 则是对 callback 回调机制的改进。这篇文章,不谈 Promise 的实际应用;聊一下 Promise 的实现原理,从最简单的解决方案入手,一步一步的自己实现一个 SimplePromise。正文从最简单的 Promise 初始化和使用入手:const pro = new Promise ((res, rej) => {})pro.then(data => {}, err => {})Promise 的构造函数如上,需要传递一个函数作为参数,这个函数有两个变量: resolve, reject。而 Promise 有不同的执行状态,分三种情况:Resolve, Reject, Pending。根据以上的信息,写出最基本的 SimplePromise 的类结构:class SimplePromise{ constructor(handler){ this._status = “PENDING” handler(this._resolve.bind(this), this._reject.bind(this))//参数函数的作用域指向Class } _resolve(){} _reject(){}}接下来思考一下_resolve 与_reject两个函数的作用。我们知道,Promise 根据 then 方法来执行回调,而 then 是根据状态判断要执行的回调函数。不难推导出,_resolve 与_reject正是根据handler的执行来进行状态变更的,而状态只能由Pending向Reslove或Rejected转换,所以有:class SimplePromise{ constructor(handler){ … } _resolve(val){//异步返回的数据 if(this._status === “PENDING”){//保证状态的不可逆性 this._status = “RESOLVED” this._value = val } } _reject(val){ if(this._status === “PENDING”){ this._status = “REJECTED” this._value = val } }}then的调用逻辑下面分析 then 函数的逻辑,从调用入手:pro.then(data => {}, err => {})then 接收两个参数,第一个是执行成功调用的函数,第二个是执行失败调用的函数。class SimplePromise{ constructor(handler){ … } _resolve(val){ … } _reject(val){ … } then(success, fail){ switch (this._status){ case “PENDING”: break; case “RESOLVED”: success(this._value) break; case “REJECTED”: fail(this._value) break; } }}以上实现了最简单的一个 Promise测试代码:const pro = new SimplePromise(function(res, rej) { let random = Math.random() * 10 if(random > 5){ res(“success”) } else{ rej(“fail”) }})pro.then(function(data) { console.log(data)}, function(err) { console.log(err)})当然,这不能算是一个 Promise,目前仅仅实现了根据状态调用不同的回调函数。还没有实现异步。那如何实现异步呢?关键在于 then 函数,当判断_status为PENDING时,如何延后调用 success与fail函数,等待状态改变后再调用?支持异步这里采用数组来存储 fail 与 success 函数:class SimplePromise{ constructor(handler){ this.status = “PENDING” this._onSuccess = []//存储fail 与 success 函数 this._onFail = [] handler(this._resolve.bind(this), this._reject.bind(this)) } _resolve(val){ if(this.status === “PENDING”){ … let temp while(this._onSuccess.length > 0){//依次执行onSuccess中的回调函数 temp = this._onSuccess.shift() temp(val) } } } _reject(val){ if(this.status === “PENDING”){ … let temp while(this._onFail.length > 0){ temp = this._onFail.shift() temp(val) } } } then (success, fail){ switch (this.status){ case “PENDING”: this._onSuccess.push(success) this._onFail.push(fail) break; … } }}使用 onSuccess 和 onFail 来存储回调函数,当处理状态为 PENDING 时,将回调函数 push 到相应的数组里,当状态变更后,依次执行数组里的回调函数。测试代码:const pro = new SimplePromise(function(res, rej) { setTimeout(function(){ let random = Math.random() * 10 if(random > 5){ res(“success”) } else{ rej(“fail”) } }, 2000)})pro.then(function(data) { console.log(data)}, function(err) { console.log(err)})两秒后,会执行相应的回调。到目前为止,最最最简单的一个 Promise 骨架已经基本完成了。但是还有很多功能待完成。现在可以稍微休息一下,喝个咖啡打个鸡血,回来我们会继续让这个 Promise 骨架更加丰满起来。. . . . . .完善Promise欢迎回来,下面我们继续完善我们的 Promise。上面完成了一个最基础的 Promise,然而还远远不够。首先,Promise 需要实现链式调用,其次 Promise 还需要实现 all race resolve reject 等静态函数。首先,如何实现 then 的链式调用呢?需要 then 返回的也是一个 Promise。于是有class SimplePromise{ … then(success, fail){ return new SimplePromise((nextSuccess, nextFail) => { const onFullfil = function(val){ const res = success(val) nextSuccess(res) } const onReject = function(val){ const res = fail(val) nextSuccess(res) ; } switch (this._status){ case “PENDING”: this._onSuccess.push(onFullfil) this._onFail.push(onReject) break; case “RESOLVED”: onFullfil(this._value) break; case “REJECTED”: onReject(this._value) break; } }) }}测试代码:const sp = new SimplePromise(function (res, rej){ setTimeout(function(){ let random = Math.random() * 10 random > 5 ? res(random) : rej(random) }, 1000)})sp.then(data => { console.log(“more than 5 " + data) return data}, err =>{ console.log(“less than 5 " + err) return err}).then((data) => { console.log(data)})then的参数限制完成了链式调用,then 方法还有许多其他限制:下面思考 以下问题:代码中四个使用 promise 的语句之间的不同点在哪儿?假设 doSomething 也 doSomethingElse 都返回 PromisedoSomething().then(function () { return doSomethingElse();}).then(finalHandler);doSomething().then(function () { doSomethingElse();}).then(finalHandler);;doSomething().then(doSomethingElse()).then(finalHandler);;doSomething().then(doSomethingElse).then(finalHandler);;答案 一会儿再揭晓,我们先来梳理以下then 方法对传入不同类型参数的处理机制:直接上代码:class SimplePromise{ … then(success, fail){ return new SimplePromise((nextSuccess, nextFail) => { const onFullfil = function(val){ if(typeof success !== “function”){ nextSuccess(val) } else{ const res = success(val)//success 的返回值 if(res instanceof SimplePromise){//如果success 返回一个promise 对象 res.then(nextSuccess, nextFail) } else{ nextSuccess(res) } } } if(fail){ const onReject = function(val){ if(typeof fail !== “function”){ nextSuccess(val) } else{ const res = fail(val) if(res instanceof SimplePromise){ res.then(nextSuccess, nextFail) } else{ nextSuccess(res) } } } } else{ onReject = function(){} } switch (this._status){ case “PENDING”: this._onSuccess.push(onFullfil) this._onFail.push(onReject) break; case “RESOLVED”: onFullfil(this._value) break; case “REJECTED”: onReject(this._value) break; } }) }}对于传入 then 方法的参数,首先判断其是否为 function,判断为否,直接执行 下一个 then 的 success 函数;判断为是,接着判断函数的返回值 res 类型是否为 Promise,如果为否,直接执行下一个 then 的 success 函数,如果为是,通过 then 调用接下来的函数。所以,上面的问题就不难得到答案了。<!– 1 –>doSomething().then(function () { return doSomethingElse();//返回值为Promise}).then(finalHandler);RETURN:doSomething —>doSomethingElse(undefined) —> final(doSomethingElseResult)<!– 2 –>doSomething().then(function () { doSomethingElse();//返回值为 undefined}).then(finalHandler);RETURN:doSomething —>doSomethingElse(undefined) —> final(undefined)<!– 3 –>doSomething().then(doSomethingElse())//参数 typeof != function .then(finalHandler);RETURN:doSomethingdoSomethingElse(undefined) —> final(doSomethingResult)<!– 4 –>doSomething().then(doSomethingElse)//与1的调用方式是不同的 .then(finalHandler);RETURN:doSomething —>doSomethingElse(doSomethingResult) —> final(doSomethingElseResult)好,then 方法已经完善好了。静态函数接下来是 Promise 的各种静态函数class SimplePromise(){ … static all(){} static race(){} static resolve(){} static reject(){}}all static all(promiselist){ if(Array.isArray(promiselist)){ const len = promiselist.length; const count = 0 const arr = [] return new SimplePromise((res, rej) => { for(let i = 0; i<len; i++){ this.resolve(promiselist[i]).then(data => { arr[i] = data count ++ if(count === len){//每一个Promise都执行完毕后返回 res(arr) } }, err => { rej(err) }) } }) } } race static race(promiselist){ if(Array.isArray(promiselist)){ const len = promiselist.length return new SimplePromise((res, rej) =>{ promiselist.forEach(item =>{ this.resolve(item).then(data => { res(data) }, err =>{ rej(err) }) }) }) } }resolve static resolve(obj){ if(obj instanceof SimplePromise){ return obj } else { return new SimplePromise((res) =>{ res(obj) }) } }reject static reject(obj){ if(obj instanceof SimplePromise){ return obj } else { return new SimplePromise((res, rej) =>{ rej(obj) }) } }总结现在,一个完整的 Promise 对象就完成了。现在来总结一下 callback 回调和 Promise 的异同吧。其实,不管是 callback 还是 Promise,这二者都是将需要滞后执行方法而提前声明的方式,只不过 callback 的处理方式比较粗犷,将 cb 函数放到异步执行的结尾;而 Promise 优于 cb 的是通过定义了不同的执行状态,更加细致的进行结果处理,提供了很好的 catch 机制,这是其一;其二,then 的链式调用解决了 cb 的回调地狱;但是 then 的链式调用也不是很好的解决方案,如果封装不好,then里面套用大量的代码的话也会引起代码的不美观和阅读上的困难,这一方面的终极解决方法还是 es7 的 async/await。后记这篇文章的代码是几个星期以前写的,参考的是思否上的一篇关于promise的文章;总结的是我对promise的理解和思考,如果有不准确或错误的地方还希望各位不吝赐教!参考文档谈一谈使用 Promise 的反模式<https://blog.csdn.net/kingppy…写这篇文章的时候,我是参考我两周前的代码写的,当时的代码思路来源于思否上的谋篇博客,等我找到会贴上来 ...

January 12, 2019 · 4 min · jiezi

聊一聊ES6 CLASS 实现原理

Class是ES6中新加入的继承机制,实际是Javascript关于原型继承机制的语法糖,本质上是对原型继承的封装。本文将会讨论:1、ES6 class的实现细2、相关Object API盘点3、Javascript中的继承实现方案盘点正文1、Class 实现细节class Person{ constructor(name, age){ this.name = name this.age = age } static type = ‘being’ sayName (){ return this.name } static intro(){ console.log("") }}class Men extends Person{ constructor(name, age){ super() this.gender = ‘male’ }}const men = new Men()以上代码是ES6 class的基本使用方式,通过babel解析后,主要代码结构如下:‘use strict’;var _createClass = function () {…}();// 给类添加方法function _possibleConstructorReturn(self, call) { …}//实现superfunction _inherits(subClass, superClass) {…}// 实现继承function _classCallCheck(instance, Constructor) {…} // 防止以函数的方式调用classvar Person = function () { function Person(name, age) { _classCallCheck(this, Person); this.name = name; this.age = age; } _createClass(Person, [{ key: ‘sayName’, value: function sayName() { return this.name; } }], [{ key: ‘intro’, value: function intro() { console.log(""); } }]); return Person; }();Person.type = ‘being’; //静态变量var Men = function (_Person) { _inherits(Men, _Person); function Men(name, age) { _classCallCheck(this, Men); var _this = _possibleConstructorReturn(this, (Men.proto || Object.getPrototypeOf(Men)).call(this)); _this.gender = ‘male’; return _this; } return Men; }(Person);var men = new Men();为什么说es6的class 是基于原型继承的封装呢? 开始省略的四个函数又有什么作用呢?下面,我们就从最开始的四个函数入手,详细的解释es6的class 是如何封装的。第一:classCallCheck函数, 检验构造函数的调用方式:代码function classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(“Cannot call a class as a function”); } }我们知道,在javascript中 person = new Person() ,通常完成以下几件事:1、创建一个新的对象 Object.create()2、将 新对象的 this 指向 构造函数的原型对象3、新对象的__proto 指向 构造函数4、执行构造函数而普通函数调用,this通常指向全局因此,_classCallCheck函数是用来检测类的调用方式。防止类的构造函数以普通函数的方式调用。第二: _createClass 给类添加方法var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (“value” in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); //非静态函数 -> 原型 if (staticProps) defineProperties(Constructor, staticProps); return Constructor; // 静态函数 -> 构造函数 }; }();_createClass是一个闭包+立即执行函数,以这种方式模拟一个作用域,将defineProperties私有化。这个函数的主要作用是通过Object.defineProperty给类添加方法,其中将静态方法添加到构造函数上,将非静态的方法添加到构造函数的原型对象上。第三: inherits 实现继承function inherits(subClass, superClass) { if (typeof superClass !== “function” && superClass !== null) { throw new TypeError(“Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, // 子类的原型的__proto__指向父类的原型 //给子类添加 constructor属性 subclass.prototype.constructor === subclass { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } } ); if (superClass) //子类__proto 指向父类 Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.proto = superClass; }从这个函数就能够很明显的看出来,class实现继承的机制了。第四: _possibleConstructorReturn super()function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError(“this hasn’t been initialised - super() hasn’t been called”); //保证子类构造函数中 显式调用 super() } return call && (typeof call === “object” || typeof call === “function”) ? call : self; }要想理解这个函数的作用,需要结合他的调用场景var _this = _possibleConstructorReturn(this, (Men.proto || Object.getPrototypeOf(Men)).call(this));// function Men(){}此时已经执行完_inherits函数,Men.proto === Person相当于:var _this = _possibleConstructorReturn(this, Person.call(this));很明显,就是将子类的this 指向父类。API 总结根据以上的分析,es6 class 的实现机制也可以总结出来了:毫无疑问的,class机制还是在prototype的基础之上进行封装的——contructor 执行构造函数相关赋值——使用 Object.defineProperty()方法 将方法添加的构造函数的原型上或构造函数上——使用 Object.create() 和 Object.setPrototypeOf 实现类之间的继承 子类原型__proto__指向父类原型 子类构造函数__proto__指向父类构造函数——通过变更子类的this 作用域实现super()盘点JavaScript中的继承方式1.原型链继承2.构造函数继承3.组合继承4.ES6 extends 继承详细内容可以参考 聊一聊 JavaScript的继承方式https://segmentfault.com/a/11…后记终于写完了,在没有网络辅助的情况下写博客真是太难了!绝知此事要躬行呀!原来觉得写一篇关于class的博客还不简单吗,就是原型链继承那一套呗,现在总结下来,还是有很多地方需要注意的;学习到了很多!嗯 不说了, 我还有好几个坑要填呢~泪参考文档ES6—类的实现原理 https://segmentfault.com/a/11...JavaScript 红宝书 ...

January 10, 2019 · 2 min · jiezi

小程序全局变量的实现方式

小程序的一个很少人知道的全局对象引用global对象:前端开发人员对这个global对象应该不会很陌生,Node环境的时候全局对象就是这个,浏览器的全局对象是window。这个对象有什么用呢?小程序开发的时候可能经常会引用一些接口的调用、工具类的模块使用,每次调用都需要require或者import下真的好麻烦,而且很难维护,我们肯定会想能不能在一个统一的地方维护呢,global对象就可以实现。如下小程序的app.js代码:const api = require(’./utils/api.js’);const ajax= require(’./utils/tooAjax.js’);const storage= require(’./utils/storage.js’);const util = require(’./utils/util.js’);//第一种global.navH = 64;//自定义导航栏高度global.api = api;//apiglobal.ajax = ajax;//接口global.storage = storage;//本地存储global.util =util;//工具//第二种wx.api = api;//apiwx.ajax = ajax;//接口wx.storage = storage;//本地存储wx.util = util;//工具在其他页面就可以调用了哦,比如: //接口调用1 global.ajax.wearShowList().then((res) => { }); //接口调用2 wx.ajax.wearShowList().then((res) => { });

January 10, 2019 · 1 min · jiezi

ES6后数组可以快速去重

let arr2 = Array.from(new set(arr1))其中arr1是原数组,arr2是去重后的新数组

January 9, 2019 · 1 min · jiezi

es6类和继承的实现原理

在阅读文章之前,您至少需要对JavaScript原型继承有一定了解,如果觉得有所欠缺,可以先了解下我这篇文章:https://segmentfault.com/a/11…1.es6 class 使用javascript使用的是原型式继承,我们可以通过原型的特性实现类的继承,es6为我们提供了像面向对象继承一样的语法糖。class Parent { constructor(a){ this.filed1 = a; } filed2 = 2; func1 = function(){}}class Child extends Parent { constructor(a,b) { super(a); this.filed3 = b; } filed4 = 1; func2 = function(){}}下面我们借助babel来探究es6类和继承的实现原理。1.类的实现转换前:class Parent { constructor(a){ this.filed1 = a; } filed2 = 2; func1 = function(){}}转换后:function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(“Cannot call a class as a function”); }}var Parent = function Parent(a) { classCallCheck(this, Parent); this.filed2 = 2; this.func1 = function () { }; this.filed1 = a;};可见class的底层依然是构造函数:1.调用_classCallCheck方法判断当前函数调用前是否有new关键字。构造函数执行前有new关键字,会在构造函数内部创建一个空对象,将构造函数的proptype指向这个空对象的_proto,并将this指向这个空对象。如上,_classCallCheck中:this instanceof Parent 返回true。若构造函数前面没有new则构造函数的proptype不会不出现在this的原型链上,返回false。2.将class内部的变量和函数赋给this。3.执行constuctor内部的逻辑。4.return this (构造函数默认在最后我们做了)。2.继承实现转换前:class Child extends Parent { constructor(a,b) { super(a); this.filed3 = b; } filed4 = 1; func2 = function(){}}转换后:我们先看Child内部的实现,再看内部调用的函数是怎么实现的:var Child = function (_Parent) { _inherits(Child, _Parent); function Child(a, b) { _classCallCheck(this, Child); var _this = _possibleConstructorReturn(this, (Child.proto || Object.getPrototypeOf(Child)).call(this, a)); _this.filed4 = 1; _this.func2 = function () {}; _this.filed3 = b; return _this; } return Child;}(Parent);1.调用_inherits函数继承父类的proptype。_inherits内部实现:function inherits(subClass, superClass) { if (typeof superClass !== “function” && superClass !== null) { throw new TypeError(“Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.proto = superClass;}(1) 校验父构造函数。(2) 典型的寄生继承:用父类构造函数的proptype创建一个空对象,并将这个对象指向子类构造函数的proptype。(3) 将父构造函数指向子构造函数的_proto(这步是做什么的不太明确,感觉没什么意义。)2.用一个闭包保存父类引用,在闭包内部做子类构造逻辑。3.new检查。4.用当前this调用父类构造函数。var _this = _possibleConstructorReturn(this, (Child.proto || Object.getPrototypeOf(Child)).call(this, a));这里的Child.proto || Object.getPrototypeOf(Child)实际上是父构造函数(_inherits最后的操作),然后通过call将其调用方改为当前this,并传递参数。(这里感觉可以直接用参数传过来的Parent)function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError(“this hasn’t been initialised - super() hasn’t been called”); } return call && (typeof call === “object” || typeof call === “function”) ? call : self;}校验this是否被初始化,super是否调用,并返回父类已经赋值完的this。5.将行子类class内部的变量和函数赋给this。6.执行子类constuctor内部的逻辑。可见,es6实际上是为我们提供了一个“组合寄生继承”的简单写法。3. supersuper代表父类构造函数。super.fun1() 等同于 Parent.fun1() 或 Parent.prototype.fun1()。super() 等同于Parent.prototype.construtor()当我们没有写子类构造函数时:var Child = function (_Parent) { _inherits(Child, _Parent); function Child() { _classCallCheck(this, Child); return _possibleConstructorReturn(this, (Child.proto || Object.getPrototypeOf(Child)).apply(this, arguments)); } return Child;}(Parent);可见默认的构造函数中会主动调用父类构造函数,并默认把当前constructor传递的参数传给了父类。所以当我们声明了constructor后必须主动调用super(),否则无法调用父构造函数,无法完成继承。典型的例子就是Reatc的Component中,我们声明constructor后必须调用super(props),因为父类要在构造函数中对props做一些初始化操作。 ...

January 8, 2019 · 2 min · jiezi

初识Proxy

初识Proxy

January 8, 2019 · 1 min · jiezi

es6的set和map学习

SetES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。因为 Set 中的值总是唯一的,所以需要判断两个值是否相等。在ECMAScript规范的早期版本中,这不是基于和===操作符中使用的算法相同的算法。具体来说,对于 Set s, +0 (+0 严格相等于-0)和-0是不同的值。然而,在 ECMAScript 2015规范中这点已被更改。有关详细信息,请参阅浏览器兼容性 表中的“value equality for -0 and 0”。另外,NaN和undefined都可以被存储在Set 中, NaN之间被视为相同的值(尽管 NaN !== NaN)。Set本身是一个构造函数,用来生成 Set 数据结构。Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。let b = new Set([‘a’,‘b’,‘c’,‘a’,‘b’]);// Set(3) {“a”, “b”, “c”}set传入参数也可以一试string;let a = new Set(‘aabbcc’);// Set(3) {a,b,c}Set 结构的实例有以下属性。Set.prototype.constructor:构造函数,默认就是Set函数。Set.prototype.size:返回Set实例的成员总数。Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。add(value):添加某个值,返回 Set 结构本身。delete(value):删除某个值,返回一个布尔值,表示删除是否成功。has(value):返回一个布尔值,表示该值是否为Set的成员。clear():清除所有成员,没有返回值。Array.from方法可以将 Set 结构转为数组。或者是扩展运算符…const items = new Set([1, 2, 3, 4, 5]);const array = Array.from(items);const test = […items];遍历操作Set 结构的实例有四个遍历方法,可以用于遍历成员。keys():返回键名的遍历器values():返回键值的遍历器entries():返回键值对的遍历器forEach():使用回调函数遍历每个成员由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。let set = new Set([‘red’, ‘green’, ‘blue’]);for (let item of set.keys()) { console.log(item);}// red// green// bluefor (let item of set.values()) { console.log(item);}// red// green// bluefor (let item of set.entries()) { console.log(item);}// [“red”, “red”]// [“green”, “green”]// [“blue”, “blue”]Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的values方法。Set.prototype[Symbol.iterator] === Set.prototype.values// true这意味着,可以省略values方法,直接用for…of循环遍历 Set。let set = new Set([‘red’, ‘green’, ‘blue’]);for (let x of set) { console.log(x);}// red// green// blueWeakSetWeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。首先,WeakSet 的成员只能是对象,而不能是其他类型的值。const ws = new WeakSet();ws.add(1)// TypeError: Invalid value used in weak setws.add(Symbol())// TypeError: invalid value used in weak setWeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在MapJavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。const map = new Map([ [’name’, ‘张三’], [’title’, ‘Author’]]);map.size // 2map.has(’name’) // truemap.get(’name’) // “张三"不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构(详见《Iterator》一章)都可以当作Map构造函数的参数。这就是说,Set和Map都可以用来生成新的 Map。Map.prototype.clear()移除Map对象的所有键/值对 。Map.prototype.delete(key)如果 Map 对象中存在该元素,则移除它并返回 true;否则如果该元素不存在则返回 falseMap.prototype.get(key)返回键对应的值,如果不存在,则返回undefined。Map.prototype.has(key)返回一个布尔值,表示Map实例是否包含键对应的值。Map.prototype.keys()返回一个新的 Iterator对象, 它按插入顺序包含了Map对象中每个元素的键 。Map.prototype.set(key, value)设置Map对象中键的值。返回该Map对象。遍历方法Map 结构原生提供三个遍历器生成函数和一个遍历方法。keys():返回键名的遍历器。values():返回键值的遍历器。entries():返回所有成员的遍历器。forEach():遍历 Map 的所有成员。WeakMapWeakMap与Map的区别有两点。首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。 ...

January 6, 2019 · 1 min · jiezi

【学习笔记】阮一峰的ES6入门

let、const块作用域ES6引入块作用域考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。letlet声明的变量只在它所在的代码块有效。let不允许在相同作用域内重复声明同一个变量let声明不存在变量提升,会产生暂时性死区const声明并赋值一个只读的变量const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动,因此值类型变量不可变,而引用类型变量可以更改实体数据内容。其他特性和let一致顶层对象let、const、class命令声明的全局变量,不属于顶层对象的属性解构ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构实质是模式匹配等号右边的对象需要具备 Iterator 接口// 基本模式匹配let [a, b, , c] = [1, 2, 3]; // a = 1; b = 2; c = undefined;// 复杂模式匹配[a, b, c] = [1, [2], 3]; // a = 1; b = [2]; c = 3;[a, [b], c] = [1, [2], 3]; // a = 1; b = 2; c = 3;[a, [b], c] = [1, [2, 3], 4]; // a = 1; b = 2; c = 4;[a, [b], c] = [1, 2, 3]; // Uncaught TypeError: undefined is not a function// 与rest参数结合使用let [head, …tail] = [1, 2, 3, 4]; // head = 1; tail = [2, 3, 4]// 设置默认值,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。let [foo = 1] = []; // foo = 1;let [foo = 1] = [null]; // foo = null;// 默认值是表达式的情况,表达式惰性求值function f() { console.log(‘aaa’);}let [x = f()] = [1];// 默认值为变量的情况,变量必须已声明let [x = 1, y = x] = []; let [x = y, y = 1] = []; // ReferenceError: y is not defined// 解构字符串const [a, b, c, d, e] = ‘hello’; // a = ‘h’; b = ’e’; c = ’l’; d = ’l’; e = ‘o’let { foo, bar } = { foo: “aaa”, bar: “bbb” }; // foo = ‘aaa’; bar = ‘bbb’;// 匹配的模式: 变量let { foo: f, bar: b } = { foo: “aaa”, bar: “bbb” }; // foo = undefined; f = “aaa”; b = “bbb”// 默认值let { baz = {} } = { foo: “aaa”, bar: “bbb” }; // baz = {};let {x: y = 3} = {x: 5}; // y = 5;// 混合解构let { items: [{name}] } = { id: ‘1’, items: [{name: ‘joke’}, {name: ’tony’}] }; // name = ‘joke’let [{name, habbits: [habbit]}] = [{id: ‘1’, name: ‘kelly’, habbits: [‘piano’]}, {id: ‘2’, name: ’tony’}]; // name = ‘kelly’; habbit = ‘piano’// 嵌套赋值let obj = {};let arr = [];({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });obj // {prop:123}arr // [true]// 在直接使用赋值表达式时,需要使用圆括号包住赋值表达式,大括号写在行首会被解释为代码块let x;{x} = {x: 1}; // SyntaxError: syntax error({x} = {x: 1}); // x = 1;const arr = [1,2,3,4]let {0 : first, [arr.length - 1] : last, length} = arr; // first = 1; last = 4; length = 4// 函数参数的解构赋值function move({x, y} = { x: 0, y: 0 }) { return [x, y];}move({x: 3, y: 8}); // [3, 8]move({x: 3}); // [3, undefined]move({}); // [undefined, undefined]move(); // [0, 0]字符串模板字符串${name}扩展方法startsWithlet s = ‘Hello world!’;s.startsWith(‘world’) // trues.startsWith(‘world’, 6) // true, 第二个参数针对第n个位置直到结束endsWithlet s = ‘Hello world!’;s.endsWith(‘world’) // trues.endsWith(‘Hello’, 5) // true, 第二参数针对前n个字符includeslet s = ‘Hello world!’;s.includes(‘world’) // trues.includes(‘Hello’, 6) // false, 第二参数针对第n个位置直到结束repeat’hello’.repeat(2) // hellohello,repeat重复字符串操作padStart, padEnd// 补全字符串方法,接受两个参数,第一个参数指定字符串长度,第二个参数指定补全内容’x’.padStart(5, ‘ab’) // ababx’x’.padEnd(3, ‘a’) // xaa数值在Number原型上新增了isFinite(), isNaN(), parseInt(), parseFloat(), isInteger()方法,用来代替全局方法扩展方法Math.trunc - 去除小数部分Math.sign - 判断一个数到底是正数、负数、还是零Math.cbrt - 计算立方根函数默认值参数全等于undefined时使用默认值默认值为函数时惰性执行默认值为表达式时惰性执行,并不会缓存下计算值rest参数 - 获取函数的多余参数, 代替arguments使用name属性箭头函数箭头函数本身不存在上下文this在定义时确定不能用作构造函数没有arguments,用rest代替不能使用yield命令数组解构扩展运算符扩展方法Array.from - 将类数组对象或可遍历对象转换为数组Array.of - 将一组值转换为数组copyWithinfindfindIndexfillentrieskeysvaluesincludes - 代替indexOf,更加直观,避免NaN判断的问题flat[1, 2, [3, [4, 5]]].flat() // [1, 2, 3, [4, 5]],单层拉平[1, 2, [3, [4, 5]]].flat(2) // [1, 2, 3, 4, 5],两层拉平[1, [2, [3]]].flat(Infinity) // [1, 2, 3]flatMap - flat和map的结合对象属性简写属性名表达式let userName = ‘joe’;let firstName = ‘curly’;let lastName = ‘water’; [userName]: 23, [firstName + lastName]: 25};- 方法的name属性- 属性遍历- for…in - 遍历原型链,遍历自身和继承的可枚举属性(不含Symbol属性)- Object.keys - 返回一个数组,包括自身的所有可枚举属性(不含 Symbol 属性)的键名- Object.getOwnPropertyNames - 返回一个数组,包含对象自身的所有属性(不含 Symbol 属性)的键名- Object.getOwnPropertySymbols - 返回一个数组,包含对象自身的所有Symbol属性的键名- Reflect.ownKeys - 返回一个数组,包含对象自身的所有键名- super关键字 - 指向当前对象的原型对象,只能用在简写的对象方法中- 解构- 扩展运算符- 扩展方法- Object.is() - 严格相等,解决全等运算符NaN不等于自身,以及+0等于-0等问题- Object.assign() - 用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象,浅拷贝- Object.getOwnPropertyDescriptors() - 返回对象属性的描述对象- Object.setPrototypeOf() - 设置原型对象- Object.getPrototypeOf() - 读取原型对象- Object.keys() - 返回一个数组,包括自身的所有可枚举属性(不含 Symbol 属性)的键名- Object.values() - 返回一个数组,包括自身的所有可枚举属性(不含 Symbol 属性)的键值- Object.entries() - 方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组- Object.fromEntries() - Object.entries的逆操作# SymbolES6新加入的类型值,表示独一无二的值let s = Symbol();- Symbol函数的参数只是表示对当前 Symbol 值的描述- 不能与其他类型的值进行运算- 作为属性名,不能使用点运算符let mySymbol = Symbol();// 第一种写法 let a = {}; a[mySymbol] = ‘Hello!’;// 第二种写法 let a = {[mySymbol]: ‘Hello!’};// 第三种写法 let a = {}; Object.defineProperty(a, mySymbol, { value: ‘Hello!’ });// 以上写法都得到同样结果 a[mySymbol] // “Hello!”- 可以用于定义一组常量,保证这组常量的值都是不相等的const log = {};log.levels = {DEBUG: Symbol(‘debug’),INFO: Symbol(‘info’),WARN: Symbol(‘warn’)}; console.log(log.levels.DEBUG, ‘debug message’); console.log(log.levels.INFO, ‘info message’);# Set 和 MapSetES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。Set本身是一个构造函数,用来生成 Set 数据结构。const s = new Set([1, 2, 3]);s.add(4);s.delete(4);s.has(4);s.clear();s.size;s.keys();s.values();s.entries();s.forEach((value, key) => console.log(key + “:” + value));WeakSetWeakSet 结构与 Set 类似,也是不重复的值的集合。WeakSet 的成员只能是对象。WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。MapES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。const map = new Map();map.set(‘foo’, true);map.set(‘bar’, false);map.set([1], false);map.size // 2map.get(‘foo’);map.has(‘foo’);map.delete(‘foo’);map.clear();for (let key of map.keys()) { console.log(key);}for (let value of map.values()) { console.log(value);}for (let item of map.entries()) { console.log(item[0], item[1]);}// 或者for (let [key, value] of map.entries()) { console.log(key, value);}// 等同于使用map.entries()for (let [key, value] of map) { console.log(key, value);}WeakMapWeakMap结构与Map结构类似,也是用于生成键值对的集合。WeakMap只接受对象作为键名(null除外)WeakMap的键名所指向的对象,不计入垃圾回收机制。# ProxyProxy 是一个构造函数,可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。# Reflect保存Object对象的一些属于语言内部的方法,比如说defineProperty/get/apply好处在于:让Object操作都变成函数行为,在Proxy中可以获取对象的默认行为> - Reflect.apply(target, thisArg, args)> - Reflect.construct(target, args)> - Reflect.get(target, name, receiver)> - Reflect.set(target, name, value, receiver)> - Reflect.defineProperty(target, name, desc)> - Reflect.deleteProperty(target, name)> - Reflect.has(target, name)> - Reflect.ownKeys(target)> - Reflect.isExtensible(target)> - Reflect.preventExtensions(target)> - Reflect.getOwnPropertyDescriptor(target, name)> - Reflect.getPrototypeOf(target)> - Reflect.setPrototypeOf(target, prototype)# Promise所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。Promise对象是一个构造函数,用来生成Promise实例。状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)- Promise 新建后就会立即执行。- 调用resolve或reject并不会终结 Promise 的参数函数的执行。- then方法返回的是一个新的Promise实例- catch方法会捕获状态确定前的所有错误,包括在then回调函数中的错误- Promise 会吃掉错误,不会对后续代码执行产生影响- finally方法,不管 Promise 对象最后状态如何,都会执行的操作- Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。多个Promise实例都改变状态,才会调用新Promise实例的回调- 如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法。- Promise.race方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。第一个Promise实例都改变状态,进入新Promise实例的回调- Promise.resolve(reason)方法也会返回一个新的 Promise 实例,该实例的状态为fulfilled。- Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。const promise = new Promise(function(resolve, reject) { // … some codeif (/ 异步操作成功 /){resolve(value);} else {reject(error);}});promise .then(function(value) { console.log(value) }, function (err) {console.log(err);}) .catch(function(error) { console.log(error) });# IteratorIterator(遍历器)是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作。var it = makeIterator([‘a’, ‘b’]);it.next() // { value: “a”, done: false }it.next() // { value: “b”, done: false }it.next() // { value: undefined, done: true }function makeIterator(array) { var nextIndex = 0; return {next: function() { return nextIndex < array.length ? {value: array[nextIndex++], done: false} : {value: undefined, done: true};}};}ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)原生具备Iterator接口的数据结构- Array- Map- Set- String- TypedArray- 函数的 arguments 对象- NodeList 对象for...of循环调用遍历器接口,作为遍历所有数据结构的统一的方法。for...in循环主要是为遍历对象而设计的。forEach无法跳出循环for...of可跳出循环,严格按照顺序遍历# GeneratorGenerator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。function* helloWorldGenerator() { yield ‘hello’; yield ‘world’; return ’ending’;}var hw = helloWorldGenerator();hw.next()// { value: ‘hello’, done: false }hw.next()// { value: ‘world’, done: false }hw.next()// { value: ’ending’, done: true }hw.next()// { value: undefined, done: true }- yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行- 只有调用next方法时,Genarator函数才会执行- yield只能被Genarator函数包裹,普通函数不行- yield*表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数# asyncGenerator 函数的语法糖async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。await命令后面可以是Promise对象async函数的返回值是 Promise 对象# Classclass MyClass { constructor() {this.name = name;} get prop() {return ‘getter’;} set prop(value) {console.log(‘setter: ‘+value);}}- 类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式- 类不存在变量提升new Foo(); // Error class Foo {print () { console.log(‘Hello’); }}- name 属性- 可以使用Generator实现Symbol.iterator遍历器- 类的方法内部如果含有this,它默认指向类的实例。但是如果单独提取方法出来用,容易报错class Logger {printName(name = ’there’) { this.print(Hello ${name});}print(text) { console.log(text);}}const logger = new Logger(); const { printName } = logger; printName(); // TypeError: Cannot read property ‘print’ of undefined解决方法class Logger {constructor() { this.printName = this.printName.bind(this);}// …}class Logger {prinitName () { this.print(Hello ${name}) }// …}- 如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。class Foo {static classMethod() { return ‘hello’;}}Foo.classMethod() // ‘hello’var foo = new Foo(); foo.classMethod() // TypeError: foo.classMethod is not a function如果静态方法包含this关键字,这个this指的是类,而不是实例。父类的静态方法,可以被子类继承。- 实例属性除了在constructor()方法里面定义,也可以直接写在类的最顶层。class IncreasingCounter {_count = 0;get value() { console.log(‘Getting the current value!’); return this._count;}increment() { this._count++;}}- 静态属性class Foo {static prop = 1;}- 私有属性- 使用_约定- 结合Symbol使用避免被覆盖- 使用#代表私有属性class IncreasingCounter { #count = 0; get value() { console.log(‘Getting the current value!’); return this.#count; } increment() { this.#count++; }}```new.target - 该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。类继承 - 子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。Decorator修饰器是一个对类进行处理的函数@testableclass MyTestableClass { // …}function testable(target) { target.isTestable = true;}MyTestableClass.isTestable // truefunction testable(isTestable) { return function(target) { target.isTestable = isTestable; }}@testable(true)class MyTestableClass {}MyTestableClass.isTestable // true@testable(false)class MyClass {}MyClass.isTestable // false修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。@connect(mapStateToProps, mapDispatchToProps)export default class MyReactComponent extends React.Component {}修饰对象属性class Person { @readonly name() { return ${this.first} ${this.last} }}function readonly(target, name, descriptor){ // target要修改的对象 // name要修饰的属性名 // descriptor要修饰的属性的描述对象 descriptor.writable = false; return descriptor;}修饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。ModuleCommonJS规范始于nodejs特点:同步加载模块,运行时加载接口:// moduleA.jsmodule.exports = function( value ){ return value * 2;}// moduleB.jsvar multiplyBy2 = require(’./moduleA’);var result = multiplyBy2(4);原理:通过require读取并执行一个JS文件返回该模块的exports对象的拷贝,一旦输出一个值,模块内部的变化就影响不到这个值。CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象缓存。AMD规范为浏览器设计,常用requirejs实现特点:异步加载模块,运行时加载接口:define(‘myModule’, [‘jquery’], function($) { // $ 是 jquery 模块的输出 $(‘body’).text(‘hello world’);});require([‘myModule’], function(myModule) {});// 未使用模块名,类CommonJS使用define(function(require, exports, module) {})ES6 模块浏览器与服务器通用特点:ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。原理:通过export命令显式指定输出的代码(并非输出对象),再通过import动态引用。接口:export命令用于规定模块的对外接口import命令用于输入接口export default规定模块默认接口,本质是输出一个叫default的变量,所以在模块中只能唯一存在,并且不可更改,不能跟声明语句export 其他模块,export和import的复合写法,实际上并没有导入当前模块,只是转发import命令会被 JavaScript 引擎静态分析,先于模块内的其他语句执行,import和export命令只能在模块的顶层,不能在代码块之中import()函数完成动态加载,返回一个Promise对象// Module1var m = 1;export { m n as N};// 输出一个default变量,将变量m的值赋给变量defaultexport default m;// Module2import {m, N} from “Module1”; // 导入m和N接口import { m as M } from “Module1”; // 导入m接口,重命名为Mimport module1 from “Module1”; // 导入默认接口import * as module1 from “Module1”; // 导入所有接口export * from “Module1”; // 再输出,export *命令会忽略Module1模块的default方法。加载规则浏览器对于带有type=“module"的<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。外部ES6模块脚本特性:代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。模块脚本自动采用严格模式,不管有没有声明use strict。模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。同一个模块如果加载多次,将只执行一次。解决循环加载问题:使用函数声明做提升编程风格在let和const之间,建议优先使用const常量表示,便于理解有利于未来多线程编写JavaScript 编译器会对const进行优化,所以多使用const,有利于提高程序的运行效率静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。const a = ‘apple’;let b = “banana”;b = “batman”;优先使用解构赋值如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。这样便于以后添加返回值,以及更改返回值的顺序。对象单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign方法。使用属性表达式定义动态属性名对象的属性和方法尽量使用简写数组使用扩展运算符拷贝数组使用Array.from将类数组对象转为数组函数所有配置项都应该集中在一个对象,放在最后一个参数rest运算符代替arguments变量使用默认值语法设置函数参数的默认值。Map注意区分 Object 和 Map,如果只是需要key: value的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。模块如果模块只有一个输出值,就使用export default,如果模块有多个输出值,就不使用export default,export default与普通的export不要同时使用。如果模块默认输出一个函数,函数名的首字母应该小写。function makeStyleGuide() {}export default makeStyleGuide;如果模块默认输出一个对象,对象名的首字母应该大写。const StyleGuide = { es6: { }};export default StyleGuide; ...

January 6, 2019 · 6 min · jiezi

var和let/const的区别

let和const是 ES6 新增的命令,用于声明变量,这两个命令跟 ES5 的var有许多不同,并且let和const也有一些细微的不同,再认真阅读了阮一峰老师的文档后,发现还是有一些不知道的细节…博客、前端积累文档、公众号、GitHub内容:var和let/const的区别块级作用域不存在变量提升暂时性死区不可重复声明let、const声明的全局变量不会挂在顶层对象下面const命令两个注意点:const 声明之后必须马上赋值,否则会报错const 简单类型一旦声明就不能再更改,复杂类型(数组、对象等)指针指向的地址不能更改,内部数据可以更改。为什么需要块级作用域?ES5只有全局作用域和函数作用域,没有块级作用域。这带来很多不合理的场景:内层变量可能覆盖外层变量用来计数的循环变量泄露为全局变量var tmp = new Date();function f() { console.log(tmp); // 想打印外层的时间作用域 if (false) { var tmp = ‘hello world’; // 这里声明的作用域为整个函数 }}f(); // undefinedvar s = ‘hello’;for (var i = 0; i < s.length; i++) { console.log(s[i]); // i应该为此次for循环使用的变量}console.log(i); // 5 全局范围都可以读到块级作用域作用域function f1() { let n = 5; if (true) { let n = 10; console.log(n); // 10 内层的n } console.log(n); // 5 当前层的n}块级作用域任意嵌套{{{{ {let insane = ‘Hello World’} console.log(insane); // 报错 读不到子作用域的变量}}}};块级作用域真正使代码分割成块了{let a = …;…}{let a = …;…}以上形式,可以用于测试一些想法,不用担心变量重名,也不用担心外界干扰块级作用域声明函数:在块级作用域声明函数,因为浏览器的要兼容老代码,会产生一些问题!在块级作用域声明函数,最好使用匿名函数的形式。if(true){ let a = function () {}; // 作用域为块级 令声明的函数作用域范围更清晰}ES6 的块级作用域允许声明函数的规则,只在使用大括号的情况下成立,如果没有使用大括号,就会报错。// 报错’use strict’;if (true) function f() {} // 我们需要给if加个{}不存在变量提升变量提升的现象:在同一作用域下,变量可以在声明之前使用,值为 undefinedES5 时使用var声明变量,经常会出现变量提升的现象。// var 的情况console.log(foo); // 输出undefinedvar foo = 2;// let 的情况console.log(bar); // 报错ReferenceErrorlet bar = 2;暂时性死区:只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量var tmp = 123; // 声明if (true) { tmp = ‘abc’; // 报错 因为本区域有tmp声明变量 let tmp; // 绑定if这个块级的作用域 不能出现tmp变量}暂时性死区和不能变量提升的意义在于:为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。不允许重复声明变量在测试时出现这种情况:var a= ‘声明’;const a = ‘不报错’,这种情况是因为babel在转化的时候,做了一些处理,在浏览器的控制台中测试,就成功报错let、const不允许在相同作用域内,重复声明同一个变量function func(arg) { let arg; // 报错}function func(arg) { { let arg; // 不报错 }}let、const声明的全局变量不会挂在顶层对象下面浏览器环境顶层对象是: windownode环境顶层对象是: globalvar声明的全局变量会挂在顶层对象下面,而let、const不会挂在顶层对象下面。如下面这个栗子var a = 1;// 如果在 Node环境,可以写成 global.a// 或者采用通用方法,写成 this.awindow.a // 1let b = 1;window.b // undefinedconst命令一旦声明,必须马上赋值let p; var p1; // 不报错const p3 = ‘马上赋值’const p3; // 报错 没有赋值const一旦声明值就不能改变简单类型:不能改动const p = ‘不能改变’;p = ‘报错’复杂类型:变量指针不能变考虑如下情况:const p = [‘不能改动’]const p2 = { name: ‘OBKoro1’}p[0] = ‘不报错’p2.name = ‘不报错’p = [‘报错’]p2 = { name: ‘报错’}const所说的一旦声明值就不能改变,实际上指的是:变量指向的那个内存地址所保存的数据不得改动简单类型(number、string、boolean):内存地址就是值,即常量(一变就报错).复杂类型(对象、数组等):地址保存的是一个指针,const只能保证指针是固定的(总是指向同一个地址),它内部的值是可以改变的(不要以为const就安全了!)所以只要不重新赋值整个数组/对象, 因为保存的是一个指针,所以对数组使用的push、shift、splice等方法也是允许的,你就是把值一个一个全都删光了都不会报错。> 复杂类型还有函数,正则等,这点也要注意一下。总结:再总结一下,看到这些名词,脑子里应该会有对应的理解,如果没有的话,那可以再看看对应的内容。var和let/const的区别:块级作用域不存在变量提升暂时性死区不可重复声明let、const声明的全局变量不会挂在顶层对象下面const命令两个注意点:let可以先声明稍后再赋值,而const在 声明之后必须马上赋值,否则会报错const 简单类型一旦声明就不能再更改,复杂类型(数组、对象等)指针指向的地址不能更改,内部数据可以更改。let、const使用场景:let使用场景:变量,用以替代var。const使用场景:常量、声明匿名函数、箭头函数的时候。鼓励我一下:觉得还不错的话,给我的项目点个star吧博客、前端积累文档、公众号、GitHub参考资料:let 和 const 命令 ...

January 4, 2019 · 2 min · jiezi

ES6数组的扩展--Array.from()和Array.of()

一、 Array.from() : 将伪数组对象或可遍历对象转换为真数组1.何为伪数组如果一个对象的所有键名都是正整数或零,并且有length属性,那么这个对象就很像数组,语法上称为“类似数组的对象”(array-like object),即为伪数组。var obj = {0: ‘a’,1: ‘b’,2: ‘c’,length: 3};obj[0] // ‘a’obj[1] // ‘b’obj.length // 3obj.push(’d’) // TypeError: obj.push is not a function上面代码中,对象obj就是一个类似数组的对象。但是“类似数组的对象”并不是数组,因为它们不具备数组特有的方法。对象obj没有数组的push方法,使用该方法就会报错。2.有哪些是伪数组典型的“类似数组的对象”是函数的arguments对象,以及大多数 DOM 元素集,还有字符串。3.如何转化为真数组①数组的slice方法可以将“类似数组的对象”变成真正的数组function doSomething(){ console.log(arguments) var args = Array.prototype.slice.call(arguments); args.push(“hj”) console.log(args) return args}doSomething(1,2,3)或者你也可以写成:function doSomething(){ var args = [].slice.call(arguments); return args}doSomething(1,2,3)尽管这种方法,也可以实现将类数组转变为数组的目的,但并不直观。ES6新增Array.from()方法来提供一种明确清晰的方式以解决这方面的需求,更推荐后者的办法。②Array.from()<button>测试1</button><br><button>测试2</button><br><button>测试3</button><br><script type=“text/javascript”>let btns = document.getElementsByTagName(“button”)console.log(“btns”,btns);//得到一个伪数组//btns.forEach(item=>console.log(item)) Uncaught TypeError: btns.forEach is not a functionArray.from(btns).forEach(item=>console.log(item))将伪数组转换为数组</script>在ES6中,扩展运算符(…)也可以将某些数据结构转为数组。只不过它需要在背后调用遍历器接口Symbol.iterator。值得注意的是如果一个对象没有部署遍历器接口,使用扩展运算符是无法将类似数组对象转换成数组。function doSomething (){ return […arguments] }doSomething(‘a’,‘b’,‘c’); // [“a”,“b”,“c”]4.Array.from()用法Array.from接受三个参数,但只有input是必须的:input: 你想要转换的类似数组对象和可遍历对象map: 类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组context: 绑定map中用到的this只要是部署了iterator接口的数据结构,Array.from都能将其转为数组:let arr = Array.from(‘juejin’); console.log(arr); //[“j”, “u”, “e”, “j”, “i”, “n”]Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,处理后的值放入返回的数组。Array.from([1, 2, 3], (x) => x * x)// [1, 4, 9]// 等同于Array.from([1,2,3].map(x => x * x))如果map函数里面用到了this关键字,还可以传入Array.from的第三个参数,用来绑定this。Array.from()可以将各种值转为真正的数组,并且还提供map功能。这实际上意味着,只要有一个原始的数据结构,你就可以先对它的值进行处理,然后转成规范的数组结构,进而就可以使用数量众多的数组方法。Array.from({ length: 2 }, () => ‘jack’)// [‘jack’, ‘jack’]二、Array.of(v1, v2, v3) : 将一系列值转换成数组当调用 new Array( )构造器时,根据传入参数的类型与数量的不同,实际上会导致一些不同的结果, 例如:let items = new Array(2) ;console.log(items.length) ; // 2console.log(items[0]) ; // undefinedconsole.log(items[1]) ;let items = new Array(1, 2) ;console.log(items.length) ; // 2console.log(items[0]) ; // 1console.log(items[1]) ; // 2当使用单个数值参数来调用 Array 构造器时,数组的长度属性会被设置为该参数。 如果使用多个参数(无论是否为数值类型)来调用,这些参数也会成为目标数组的项。数组的这种行为既混乱又有风险,因为有时可能不会留意所传参数的类型。ES6 引入了Array.of( )方法来解决这个问题。该方法的作用非常类似Array构造器,但在使用单个数值参数的时候并不会导致特殊结果。Array.of( )方法总会创建一个包含所有传入参数的数组,而不管参数的数量与类型:let items = Array.of(1, 2);console.log(items.length); // 2console.log(items[0]); // 1console.log(items[1]); // 2items = Array.of(2);console.log(items.length); // 1console.log(items[0]); // 2Array.of基本上可以用来替代Array()或newArray(),并且不存在由于参数不同而导致的重载,而且他们的行为非常统一。参考文章深入理解ES6Javascript教程JavaScript学习笔记:ES6数组方法 ...

January 4, 2019 · 1 min · jiezi

Babel的简单使用

Babel的简单使用1.初始化,创建package.json npm init2.安装babel-cli npm i -d babel-cli3.安装babel npm i -d babel-preset-latest4.添加 .babelrc 文件a.创建 type nul>.babelb.内容 { “presets”:[“latest”], “plugins”:[] }5.创建src、release 文件夹6.修改package.json { … “scripts”: { … “build”:“babel src -d release” }, }“babel js -d release” 可理解为"把src文件夹下的js文件转换后存到release文件夹下"7.编写示例代码( src/test.js) let arr=[1,3,5,6,8,9]; 8.运行 npm run build > es6test@1.0.0 build D:\webWork\es6test > babel src -d release src\test.js -> release\test.js9.结果(release/test.js) “use strict”; var arr = [1, 3, 5, 6, 8, 9];

January 3, 2019 · 1 min · jiezi

[ webpack4 ] 配置属于自己的打包系统教程(二)—— 资源配置篇

GitHub 完整配置文件地址: https://github.com/yhtx1997/webpack4-Instance 由于篇幅过长分三次发布,建议按顺序看[ webpack4 ] 配置属于自己的打包系统教程(一)—— 基础配置篇[ webpack4 ] 配置属于自己的打包系统教程(二)—— 资源配置篇[ webpack4 ] 配置属于自己的打包系统教程(最终篇)—— 环境配置篇资源配置篇资源配置篇ES6 -> ES5提取 css 到单独文件css 浏览器兼容前缀补全css 代码压缩使用 sass使用 HTML 模板清理旧的打包文件静态资源加载与解析通过下面的配置 可以在 js 里引入相应的文件,然后进行解析 也可以直接解析相应的文件配置 babel 将 ES6 转换为兼容性语法(低版本语法 ES5 或 ES3)安装 babel-loadernpm install -D babel-loader @babel/core @babel/preset-env babel-loader:使用 Babel 转换 JavaScript 依赖关系的 Webpack 加载器@babel/core: 将 ES6 代码转换为 ES5@babel/preset-env: 决定使用哪些 api 为旧浏览器提供现代浏览器的新特性module: { rules: [ { test: /.m?js$/, exclude: /(node_modules|bower_components)/, use: { loader: ‘babel-loader’, options: { presets: [’@babel/preset-env’] } } } ]}加载 css安装提取 css 相关的 npm 包npm install style-loader css-loader -D提取 css 相关配置const path = require(‘path’);module.exports = { entry: { 2048: ‘./src/js/2048.js’, 1024: ‘./src/js/1024.js’, 512: ‘./src/js/512.js’ }, output: { filename: “[name].js”, path: path.resolve(__dirname, ‘dist’) }, module: { rules: [ { test: /.css$/, //匹配所有以 .css 为后缀的文件 use: [//使用以下loader来加载 ‘style-loader’, ‘css-loader’ ] } ] }}安装 sass开发 css 现在多数使用 sass 和 lass ,所以配置下 sass 相应的安装 lass 只需要把 sass-loader 切换为 less-loadernpm install sass-loader node-sass -D配置{ test: /.scss$/, use: [ “style-loader”, “css-loader”, “sass-loader” ]}CSS 分离成文件方案一 安装 extract-text-webpack-plugin方案一简单写下,推荐方案二npm install extract-text-webpack-plugin -Dextract-text-webpack-plugin 提取 css 到单独文件配置const ExtractTextPlugin = require(“extract-text-webpack-plugin”);plugins: [ new CleanWebpackPlugin([‘dist’]), new HtmlWebpackPlugin({ title:‘2048’, template: ‘./src/index.html’, minify:true, hash:true }), new ExtractTextPlugin({ filename: ‘css/[name].css’ }),],module: { rules: [ { test: /.scss$/, use: ExtractTextPlugin.extract({ fallback: “style-loader”, use: [“css-loader”,“sass-loader”] }) }, ]}方案二 安装 MiniCssExtractPlugin 推荐与extract-text-webpack-plugin相比异步加载没有重复的编译(性能)更容易使用特定于CSSnpm install mini-css-extract-plugin postcss-loader autoprefixer postcss optimize-css-assets-webpack-plugin -Dmini-css-extract-plugin 提取 css 到单独文件autoprefixer 浏览器兼容前缀补全(例如 -webkit-)optimize-css-assets-webpack-plugin 代码压缩配置const MiniCssExtractPlugin = require(“mini-css-extract-plugin”);const OptimizeCSSAssetsPlugin = require(“optimize-css-assets-webpack-plugin”);optimization: { minimizer: [ new UglifyJsPlugin({ cache: true, parallel: true, sourceMap: true // set to true if you want JS source maps }), new OptimizeCSSAssetsPlugin({}) ] },plugins: [ new CleanWebpackPlugin([‘dist’]), new HtmlWebpackPlugin({ title:‘2048’, template: ‘./src/index.html’, minify:true, hash:true }), new MiniCssExtractPlugin({ // Options similar to the same options in webpackOptions.output // both options are optional filename: “css/[name].css” })],module: { rules: [ { test: /.scss$/, use: [ MiniCssExtractPlugin.loader, ‘css-loader’, ‘postcss-loader’, ‘sass-loader’, ] }, ] }这里需要注意的是需要新建一个 postcss.config.js 文件,用来配置自动加前缀module.exports={ plugins: [ require(‘autoprefixer’)({ /* …options */ }) ]}加载数据数据文件包括 JSON 文件,CSV、TSV 和 XML JSON 默认就是支持的,所以如果需要处理剩下的使用下面的方法就可以了安装提取 数据 相关的 npm 包npm install csv-loader xml-loader -D安装提取 数据 相关的 npm 包{ test: /.(csv|tsv)$/, use: [ ‘csv-loader’ ]},{ test: /.xml$/, use: [ ‘xml-loader’ ]}加载其他资源加载其他静态资源都可以使用 file-loader 来加载npm install file-loader -D加载图片{ test: /.(png|svg|jpg|gif)$/, use: [ ‘file-loader’ ]}加载字体{ test: /.(woff|woff2|eot|ttf|otf)$/, use: [ ‘file-loader’ ]}设定 HtmlWebpackPlugin当我们真正应用我们写的代码时,需要我们新建 HTML ,并且需要我们手动的在 HTML 里引入,使用 HtmlWebpackPlugin 可以让我们不用每次都新建 HTML 以及 手动去引入我们的代码 它会帮我们每次运行 webpack 时新建一个 HTML 并引入所有打包好的 js css安装npm install html-webpack-plugin -D配置 HTML 模板const HtmlWebpackPlugin = require(‘html-webpack-plugin’);//引入HtmlWebpackPlugin//官网是将其放到了入口 entry 与出口 output 之间plugins: [ new HtmlWebpackPlugin({ title: ‘Output Management’,//表示 HTML title 标签的内容 template: ‘./src/index.html’,//表示模板路径 minify: true,//压缩代码 hash: true//加上哈希值来达到去缓存的目的 })]清理 ./dist 文件夹如果我们使用了哈希值来命名我们的文件,那么每次更该内容都会生成新的文件,同时旧的文件依然存在,这样的话一个是乱,一个是浪费 我们可以使用 CleanWebpackPlugin 在每次打包时都会将之前的旧文件清除掉安装npm install clean-webpack-plugin -D配置const CleanWebpackPlugin = require(‘clean-webpack-plugin’);plugins: [ new CleanWebpackPlugin([‘dist’]),//删除dist new HtmlWebpackPlugin({ title: ‘Output Management’,//表示 HTML title 标签的内容 template: ‘./src/index.html’,//表示模板路径 minify: true,//压缩代码 hash: true//加上哈希值来达到去缓存的目的 })] ...

January 3, 2019 · 2 min · jiezi

如何正确合理使用 JavaScript async/await !

ES7 引入的 async/await 在 JavaScript 的异步编程中是一个极好的改进。它提供了使用同步样式代码异步访问 resoruces 的方式,而不会阻塞主线程。然而,它们也存在一些坑及问题。在本文中,将从不同的角度探讨 async/await,并演示如何正确有效地使用这对兄弟。前置知识async 作用是什么 从 MDN 可以看出:async 函数返回的是一个 Promise 对象。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。如果 async 函数没有返回值, 它会返回 Promise.resolve(undefined)。await 作用是什么 从 MDN 了解到:await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,await 可以等任意表达式的结果)。如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。async/await 的优点async/await 带给我们的最重要的好处是同步编程风格。让我们看一个例子:很明显,async/await 版本比 promise 版本更容易理解。如果忽略 await 关键字,代码看起来就像任何其他同步语言,比如 Python。最佳的地方不仅在于可读性。async/await 到今天为止,所有主流浏览器都完全支持异步功能。本地浏览器的支持意味着你不必转换代码。更重要的是,它便于调试。当在函数入口点设置断点并跨过 await 行时,将看到调试器在 bookModel.fetchAll() 执行其任务时暂停片刻,然后它将移动到下一个.filter 行,这比 promise 代码要简单得多,在 promise 中,必须在 .filter 行上设置另一个断点。另一个不太明显的优点是 async 关键字。 async声明 getBooksByAuthorWithAwait()函数返回值确保是一个 promise,因此调用者可以安全地使用 getBooksByAuthorWithAwait().then(…) 或await getBooksByAuthorWithAwait()。 想想下面的例子(不好的做法!):在上述代码中,getBooksByAuthorWithPromise 可能返回 promise(正常情况下)或 null 值(异常情况下),在异常情况下,调用者不能调用 .then()。有了async 声明,这种情况就不会出现了。async/await 可能会产生误导一些文章将 async/wait 与 Promise 进行了比较,并声称它是 JavaScript 下一代异步编程风格,对此作者深表异议。async/await 是一种改进,但它只不过是一种语法糖,不会完全改变我们的编程风格。从本质上说,async 函数仍然是 promise。在正确使用 async 函数之前,你必须先了解 promise,更糟糕的是,大多数时候你需要在使用 promises 的同时使用 async 函数。考虑上面示例中的 getBooksByAuthorWithAwait() 和getbooksbyauthorwithpromise() 函数。请注意,它们不仅功能相同,而且具有完全相同的接口!这意味着 getbooksbyauthorwithwait() 将返回一个 promise,所以也可以使用 .then(…)方式来调用它。嗯,这未必是件坏事。只有 await 的名字给人一种感觉,“哦,太好了,可以把异步函数转换成同步函数了”,这实际上是错误的。async/await那么在使用 async/await 时可能会犯什么错误呢?下面是一些常见的例子。太过串行化尽管 await 可以使代码看起来像是同步的,但实际它们仍然是异步的,必须小心避免太过串行化。上述代码在逻辑上看似正确的,然而,这是错误的。await bookModel.fetchAll() 会等待 fetchAll() 直到 fetchAll() 返回结果。然后 await authorModel.fetch(authorId) 被调用。注意,authorModel.fetch(authorId) 并不依赖于 bookModel.fetchAll() 的结果,实际上它们可以并行调用!然而,用了 await,两个调用变成串行的,总的执行时间将比并行版本要长得多得多。下面是正确的方式:更糟糕的是,如果你想要一个接一个地获取项目列表,你必须依赖使用 promises:简而言之,你仍然需要将流程视为异步的,然后使用 await 写出同步的代码。在复杂的流程中,直接使用 promise 可能更方便。错误处理在 promise中,异步函数有两个可能的返回值: resolved 和 rejected。我们可以用 .then() 处理正常情况,用 .catch() 处理异常情况。然而,使用 async/await方式的,错误处理可能比较棘手。try…catch最标准的(也是作者推荐的)方法是使用 try…catch 语法。在 await 调用时,在调用 await 函数时,如果出现非正常状况就会抛出异常,await 命令后面的 promise 对象,运行结果可能是 rejected,所以最好把await 命令放在 try…catch 代码块中。如下例子:在捕捉到异常之后,有几种方法来处理它:处理异常,并返回一个正常值。(不在 catch 块中使用任何 return 语句相当于使用 return undefined,undefined 也是一个正常值。)如果你想让调用者处理它,你可以直接抛出普通的错误对象,如 throw errorr,它允许你在 promise 链中使用 async getBooksByAuthorWithAwait() 函数(也就是说,可以像getBooksByAuthorWithAwait().then(…).catch(error => …) 处理错误); 或者可以用 Error 对象将错误封装起来,如 throw new Error(error),当这个错误在控制台中显示时,它将给出完整的堆栈跟踪信息。拒绝它,就像 return Promise.reject(error) ,这相当于 throw error,所以不建议这样做。使用 try…catch 的好处:简单,传统。只要有Java或c++等其他语言的经验,理解这一点就不会有任何困难。如果不需要每步执行错误处理,你仍然可以在一个 try … catch 块中包装多个 await 调用来处理一个地方的错误。这种方法也有一个缺陷。由于 try…catch 会捕获代码块中的每个异常,所以通常不会被 promise 捕获的异常也会被捕获到。比如:运行此代码,你将得到一个错误 ReferenceError: cb is not defined。这个错误是由console.log()打印出来的的,而不是 JavaScript 本身。有时这可能是致命的:如果 BookModel 被包含在一系列函数调用中,其中一个调用者吞噬了错误,那么就很难找到这样一个未定义的错误。让函数返回两个值另一种错误处理方法是受到Go语言的启发。它允许异步函数返回错误和结果。详情请看这篇博客文章:How to write async await without try-catch blocks in Javascript简而言之,你可以像这样使用异步函数:[err, user] = await to(UserModel.findById(1));作者个人不喜欢这种方法,因为它将 Go 语言的风格带入到了 JavaScript 中,感觉不自然。但在某些情况下,这可能相当有用。使用 .catch这里介绍的最后一种方法就是继续使用 .catch()。回想一下 await 的功能:它将等待 promise 完成它的工作。值得注意的一点是 promise.catch() 也会返回一个 promise ,所以我们可以这样处理错误:这种方法有两个小问题:它是 promises 和 async 函数的混合体。你仍然需要理解 是promises 如何工作的。错误处理先于正常路径,这是不直观的。结论ES7引入的 async/await 关键字无疑是对J avaScrip t异步编程的改进。它可以使代码更容易阅读和调试。然而,为了正确地使用它们,必须完全理解 promise,因为 async/await 只不过是 promise 的语法糖,本质上仍然是 promise。原文:https://hackernoon.com/javasc…你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》! ...

January 2, 2019 · 2 min · jiezi

javascript遍历方法总结

forEach 循环JavaScript诞生已经有20多年了,我们一直使用的用来循环一个数组的方法是这样的:for (var index = 0; index < myArray.length; index++) { console.log(myArray[index]);}自从 JavaScript 5 起,我们开始可以使用内置的 forEach 方法:myArray.forEach(function (value) { console.log(value);});写法简单了许多,但也有短处:你不能中断循环(使用 break 语句或使用 return 语句)。for…in 循环JavaScript里还有一种循环方法:for…in。for…in 循环实际是为循环可枚举(enumerable)对象而设计的:var obj = {a:1, b:2, c:3}; for (var prop in obj) { console.log(“obj.” + prop + " = " + obj[prop]);} // 输出:// “obj.a = 1”// “obj.b = 2”// “obj.c = 3"你也可以用它来循环一个数组:for (var index in myArray) { // 不推荐这样 console.log(myArray[index]);}不推荐用 for…in 来循环一个数组,因为,不像对象,数组的 index 跟普通的对象属性不一样,是重要的数值序列指标。总之, for…in 是用来循环带有字符串key的对象的方法。for…of 循环JavaScript6里引入了一种新的循环方法,它就是 for…of 循环,它既比传统的 for 循环简洁,同时弥补了 forEach 和 for-in 循环的短板。我们看一下它的 for…of 的语法:for (var value of myArray) { console.log(value);}for…of 的语法看起来跟 for…in 很相似,但它的功能却丰富的多,它能循环很多东西。for…of 循环使用例子:循环一个数组(Array):let iterable = [10, 20, 30]; for (let value of iterable) { console.log(value);}// 10// 20// 30我们可以使用 const 来替代 let,这样它就变成了在循环里的不可修改的静态变量。let iterable = [10, 20, 30]; for (const value of iterable) { console.log(value);}// 10// 20// 30循环一个字符串:let iterable = “boo”; for (let value of iterable) { console.log(value);}// “b”// “o”// “o"循环一个类型化的数组(TypedArray):let iterable = new Uint8Array([0x00, 0xff]); for (let value of iterable) { console.log(value);}// 0// 255循环一个Map:let iterable = new Map([[“a”, 1], [“b”, 2], [“c”, 3]]); for (let [key, value] of iterable) { console.log(value);}// 1// 2// 3 for (let entry of iterable) { console.log(entry);}// [a, 1]// [b, 2]// [c, 3]循环一个 Set:let iterable = new Set([1, 1, 2, 2, 3, 3]); for (let value of iterable) { console.log(value);}// 1// 2// 3循环一个 DOM 集合循环一个 DOM 集合,比如 NodeList,之前我们讨论过如何循环一个 NodeList,现在方便了,可以直接使用 for…of 循环:// Note: This will only work in platforms that have// implemented NodeList.prototype[Symbol.iterator]let articleParagraphs = document.querySelectorAll(“article > p”); for (let paragraph of articleParagraphs) { paragraph.classList.add(“read”);}循环一个拥有可枚举属性的对象for…of 循环并不能直接使用在普通的对象上,但如果我们按对象所拥有的属性进行循环,可使用内置的 Object.keys() 方法:for (var key of Object.keys(someObject)) { console.log(key + “: " + someObject[key]);}循环一个生成器(generators)我们可循环一个生成器(generators):function* fibonacci() { // a generator function let [prev, curr] = [0, 1]; while (true) { [prev, curr] = [curr, prev + curr]; yield curr; }} for (let n of fibonacci()) { console.log(n); // truncate the sequence at 1000 if (n >= 1000) { break; }} ...

December 31, 2018 · 2 min · jiezi

[ 造轮子 ] 手动封装 AJAX (三) —— 最终版

导言在开始之前先想一想ajax是怎样的流程首先打开一个连接发送数据返回结果我们要自定义的设置有哪些设置请求方式设置请求头设置返回数据格式返回成功后或失败后我们要做的功能有哪些数据校验统一数据的格式支持文件上传对于传入参数的容错处理经过以上思考基本结构大致成型数据校验数据格式的统一建立连接设置请求头设置返回数据格式发送数据返回成功或失败代码如下class AJAX { constructor({url = “",method = “GET”,data = {},async = true,success,error,resType = “",headers = {}}) { //集中管理传递过来的参数 this.option = {url,method,data,async,success,error,resType,headers}; this.xhr = new XMLHttpRequest(); this.start(); } start() { //数据校验 this.checkOption(); //数据格式的统一 this.initOption(); //建立连接 this.open(); //设置请求头 this.setHeaders(); //设置返回数据格式 this.setResponseType(); //发送数据 this.sendData() //返回成功或失败 this.responseData(); }; }接下来添加校验功能首先url不能是空然后请求头必须是字面量对象格式 {key:value}再有就是一些简单的警告代码如下checkOption() { let {url,async,resType,headers} = this.option; if (url === ‘’) { throw new Error(‘请求地址不能为空’); //打印错误信息,并停止当前进程 //Console.error(‘请求地址为空’); 也可以打印错误信息,但是不能停止当前进程 } if(typeof headers !== ‘object’){ throw new Error(‘设置请求头时请传入 {key:value,key:value…} 的格式’); } if(typeof resType !== ‘string’){ throw new Error(‘设置返回数据格式时请传入字符出串格式’); } if (typeof url !== ‘string’) { //输出警告信息 console.warn(‘当前请求地址不是字符串,现在将其尝试转换为字符串’); } if (async === false && resType != ‘’) { console.warn(‘如果设置了请求方式为同步,即使设置了返回数据格式也不会生效’); }};需要注意的是返回数据格式可以设置这几个值,之后会写一个详细的传参指南接下来是数据的处理首先我们需要保证请求格式,不管传入时是大写还是小写,在我们设置请求格式时要是全部大写还有就是url可能是数字的,需要转换成字符为了方便将 async不是布尔型的转成布尔型,这是什么概念,就是传参时 写数字 1 是异步 数字 0 是同步将需要发送的内容做一个处理initOption() { let {url,async,method} = this.option; //url不是字符串转换成字符串 if (typeof url !== ‘string’) { try { this.option.url = url.toString(); console.log(转换成功: "${this.option.url}"); } catch (error) { throw new Error(‘url 转换字符串失败’); } } //async不是布尔型转成布尔型 if(typeof async !==‘boolean’){ async == true ? this.option.async = true : this.option.async = false; } //将 post get 转换为大写 this.option.method = method.toUpperCase(); //post和get数据初始化 if(this.option.method != ‘FORMDATA’){// [1] let data = this.option.data; if(typeof data === ‘object’){//[2] if( this.option.method === ‘GET’){ let arr=[]; for(let name in data){ arr.push(${name}=${data[name]});//[3] } let strData=arr.join(’&’);//[4] this.option.data=?${strData};//[5] }else if( this.option.method === ‘POST’){ let formData = new FormData();//[6] for(let key in data){ formData.append(${key},${data[key]}); } this.option.data=formData; } }else if(typeof data === ‘string’ && this.option.method === ‘GET’){//[7] this.option.data=?${data}; } }};这里详细说说对需要发送数据的处理,按照序号来说判断它不是 formData ,也就是说是 GET 和 POST 时我们进行数据处理,是 formData 不进行处理,直接发送,这是为了能够实现文件上传功能判断它是不是 {key:vlue} 这种格式的,是的话解析或拼接,不是的话跳到 [7] 如果是字符串直接加到 url 后边[3] [4] [5] 这里是为了处理成 url?key=value$key=value 这种 url 传参的数据格式[6] 是新建了一个 FormData 对象,是 ajax2.0 里边的,它最主要的可以用 ajax 实现文件上传功能,在这里是为了代码简单打开连接经过之前的数据处理这里只需要判断下是 GET 还是其他方式(post formdata),然后选择对应的连接方式 open(){ let {method,url,async,data} = this.option; if(method === ‘GET’){ this.xhr.open(method,url+data,async); }else{ this.xhr.open(method,url,async); } }设置自定义请求头将传入的参数进行解析,然后设置自定义请求头代码如下setHeaders(){ let headers = this.option.headers; for(let key in headers){ this.xhr.setRequestHeader(${key.toString()},${headers[key].toString()}) } }设置返回数据格式、发送数据由于同步请求时不能设置返回数据格式,所以做下判断发送数据这里,在经过之前的数据处理后只有 GET 方式有所区别,其他两种没有区别(支持 GET POST 以及我自己定义的一种,更多请求方法可自行扩展)setResponseType() { if (this.option.async) { this.xhr.responseType = this.option.resType; }}sendData(){ if(this.option.method == ‘GET’){ this.xhr.send(); }else{ this.xhr.send(this.option.data); }}请求完成后的数据返回请求完成后会返回数据 判断 success 以及 error 是不是函数,是的话会将数据返回给 success 或者将错误信息返回给 errorresponseData(){ this.xhr.onload = ()=>{ if(this.xhr.status >= 200 && this.xhr.status < 300 || this.xhr.status === 304){ typeof this.option.success === ‘function’ && this.option.success(this.xhr.response); }else{ typeof this.option.error === ‘function’ && this.option.error(this.xhr.statusText); } }}在实现基本功能后,突然想到 jQuery 的 ajax 是会返回一个 promise 对象,可以同时使用回掉函数,或者使用 then 和 catch 来处理数据 因此修改了下传入参数,以及返回数据的处理传参时代码如下//add resolve rejectclass AJAX { constructor({url = “",method = “GET”,data = {},async = true,success,error,resType = “",headers = {},resolve,reject}) { this.option = {url,method,data,async,success,error,resType,headers,resolve,reject}; this.xhr = new XMLHttpRequest(); this.start(); }}返回数据时代码如下responseData(){ this.xhr.onload = ()=>{ if(this.xhr.status >= 200 && this.xhr.status < 300 || this.xhr.status === 304){ typeof this.option.success === ‘function’ && this.option.success(this.xhr.response); this.option.resolve(this.xhr.response);//add }else{ typeof this.option.error === ‘function’ && this.option.error(this.xhr.statusText); this.option.reject(this.xhr.statusText);//add } }}最终代码class AJAX { constructor({url = “",method = “GET”,data = {},async = true,success,error,resType = “",headers = {},resolve,reject}) { this.option = {url,method,data,async,success,error,resType,headers,resolve,reject}; this.xhr = new XMLHttpRequest(); this.start(); } start() { //数据校验 this.checkOption(); //数据格式的统一 this.initOption(); //建立连接 this.open(); //设置请求头 this.setHeaders(); //设置返回数据格式 this.setResponseType(); //发送数据 this.sendData() //返回成功或失败 this.responseData(); }; checkOption() { let {url,async,resType,headers} = this.option; if (url === ‘’) { throw new Error(‘请求地址不能为空’); //打印错误信息,并停止当前进程 //Console.error(‘请求地址为空’); 也可以打印错误信息,但是不能停止当前进程 } if(typeof headers !== ‘object’){ throw new Error(‘设置请求头时请传入 {key:value,key:value…} 的格式’); } if(typeof resType !== ‘string’){ throw new Error(‘设置返回数据格式时请传入字符出串格式’); } // "” 与设置为"text"相同, 是默认类型 (实际上是 DOMString) // “arraybuffer” 将接收到的数据类型视为一个包含二进制数据的 JavaScript ArrayBuffer // “blob” 将接收到的数据类型视为一个包含二进制数据的 Blob 对象 // “document” 将接收到的数据类型视为一个 HTML Document 或 XML XMLDocument ,这取决于接收到的数据的 MIME 类型 // “json” 将接收到的数据类型视为 JSON 解析得到的 // “text” 将接收到的数据类型视为包含在 DOMString 对象中的文本 if (typeof url !== ‘string’) { //输出警告信息 console.warn(‘当前请求地址不是字符串,现在将其尝试转换为字符串’); } if (async === false && resType != ‘’) { console.warn(‘如果设置了请求方式为同步,即使设置了返回数据格式也不会生效’); } }; initOption() { let {url,async,method} = this.option; //url不是字符串转换成字符串 if (typeof url !== ‘string’) { try { this.option.url = url.toString(); console.log(转换成功: "${this.option.url}"); } catch (error) { throw new Error(‘url 转换字符串失败’); } } //async不是布尔型转成布尔型 if(typeof async !==‘boolean’){ async == true ? this.option.async = true : this.option.async = false; } //将 post get 转换为大写 this.option.method = method.toUpperCase(); //post和get数据初始化 if(this.option.method != ‘FORMDATA’){ let data = this.option.data; if(typeof data === ‘object’){ if( this.option.method === ‘GET’){ let arr=[]; for(let name in data){ arr.push(${name}=${data[name]}); } let strData=arr.join(’&’); this.option.data=?${strData}; }else if( this.option.method === ‘POST’){ let formData = new FormData(); for(let key in data){ formData.append(${key},${data[key]}); } this.option.data=formData; } }else if(typeof data === ‘string’ && this.option.method === ‘GET’){ this.option.data=?${data}; } } }; open(){ let {method,url,async,data} = this.option; if(method === ‘GET’){ this.xhr.open(method,url+data,async); }else{ this.xhr.open(method,url,async); } } setHeaders(){ let headers = this.option.headers; for(let key in headers){ this.xhr.setRequestHeader(${key.toString()},${headers[key].toString()}) } } setResponseType() { if (this.option.async) { this.xhr.responseType = this.option.resType; } } sendData(){ if(this.option.method == ‘GET’){ this.xhr.send(); }else{ this.xhr.send(this.option.data); } } responseData(){ this.xhr.onload = ()=>{ if(this.xhr.status >= 200 && this.xhr.status < 300 || this.xhr.status === 304){ typeof this.option.success === ‘function’ && this.option.success(this.xhr.response); this.option.resolve(this.xhr.response); }else{ typeof this.option.error === ‘function’ && this.option.error(this.xhr.statusText); this.option.reject(this.xhr.statusText); } } } all(promises) { return Promise.all(promises); };}function ajax({url,method,data,async,success,error,resType,headers}){ return new Promise((resolve, reject) => { return new AJAX({url,method,data,async,success,error,resType,headers,resolve,reject}); });}使用时可以将代码复制粘贴到单独的 js 文件然后用 script 标签引入 也可以添加一行 export 代码将最后的 ajax 暴露出去 使用import 引入 具体使用方法用法ajax({ url:‘api/login’, method:‘post’,//支持 GET POST 和我自定义的 FORMDATA ,传入时不区分大小写 data = { name:“yhtx”, id:“1997” },//除了这种还支持字符串 “name=yhtx&id=1997”;以及 formData 数据,在传入formData 数据时请将 method 设置为 FORMDATA async = true,//可以使用数字 1 代替 true ,数字 0 代替 false success(res){ //可以使用回调的形式处理数据也可以使用 then },error(err){ //可以使用回调的形式处理错误也可以使用 catch }, resType = “”,//可以传入 "” “arraybuffer” “blob” “document” “json” “text” headers = { mycookie: “46afqwiocibQEIJfa498./&678” //使用对象的方式传参 }}).then((res)=>{ //可以使用 then 的形式处理数据也可以使用回调函数}).catch((err)=>{ //可以使用 catch 的形式处理数据也可以使用回调函数}) ...

December 30, 2018 · 4 min · jiezi

理解import、require、export、module.export

理解import、require、export、module.exportES6的模块设计模块设计的思想是尽量静态化,使得编译的时候就可以确定模块的一来关系,以及输入和输出的变量。CommonJS和AMD都只能在运行时确定这些东西,commonJS模块就是对象,输入时需要查找对象属性// CommonJS模块let { stat, exists, readFile } = require(‘fs’);// 等同于let _fs = require(‘fs’);let stat = _fs.stat;let exists = _fs.exists;let readfile = _fs.readfile;nodeJS 中模块化使用的就是CommonJS的规范,实质就是整体加载fs模块,生成fs_对象,在对象上读取属性和方法,这种加载方式是“运行时加载”ES6 模块ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。// ES6模块import { stat, exists, readFile } from ‘fs’;上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。export命令一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。// profile.jsexport var firstName = ‘Michael’;export var lastName = ‘Jackson’;export var year = 1958;// profile.jsvar firstName = ‘Michael’;var lastName = ‘Jackson’;var year = 1958;export {firstName, lastName, year};export的语法,对外导出接口,在接口名与模块内部变量之间,建立了一一对应的关系。// 写法一export var m = 1;// 写法二var m = 1;export {m};// 写法三var n = 1;export {n as m};import命令注意,import命令具有提升效果,会提升到整个模块的头部,首先执行。目前阶段,通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面,但是最好不要这样做。因为import在静态解析阶段执行,所以它是一个模块之中最早执行的。下面的代码可能不会得到预期结果。require(‘core-js/modules/es6.symbol’);require(‘core-js/modules/es6.promise’);import React from ‘React’;export default 命令export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default命令。本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。// modules.jsfunction add(x, y) { return x * y;}export {add as default};// 等同于// export default add;// app.jsimport { default as foo } from ‘modules’;// 等同于// import foo from ‘modules’;// 正确export var a = 1;// 正确var a = 1;export default a;// 错误export default var a = 1;CommonJS规范每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。var x = 5;var addX = function (value) { return value + x;};module.exports.x = x;module.exports.addX = addX;// 使用var example = require(’./example.js’);console.log(example.x); // 5console.log(example.addX(1)); // 6CommonJS模块的特点所有代码都运行在模块作用域,不会污染全局作用域。模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。模块加载的顺序,按照其在代码中出现的顺序。export var foo = ‘bar’;setTimeout(() => foo = ‘baz’, 500);ES6 模块化上面代码输出变量foo,值为bar,500 毫秒之后变成baz。这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新module对象Node内部提供一个Module构建函数。所有模块都是Module的实例module.id 模块的识别符,通常是带有绝对路径的模块文件名。module.filename 模块的文件名,带有绝对路径。module.loaded 返回一个布尔值,表示模块是否已经完成加载。module.parent 返回一个对象,表示调用该模块的模块。module.children 返回一个数组,表示该模块要用到的其他模块。module.exports 表示模块对外输出的值。module.exports属性module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。var exports = module.exports;造成的结果是,在对外输出模块接口时,可以向exports对象添加方法。exports.area = function (r) { return Math.PI * r * r;};exports.circumference = function (r) { return 2 * Math.PI * r;};注意,不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系。exports.hello = function() { return ‘hello’;};module.exports = ‘Hello world’;面代码中,hello函数是无法对外输出的,因为module.exports被重新赋值了。这意味着,如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能使用module.exports输出。目录的加载规则和node中模块加载规则一致通常,我们会把相关的文件会放在一个目录里面,便于组织。这时,最好为该目录设置一个入口文件,让require方法可以通过这个入口文件,加载整个目录。在目录中放置一个package.json文件,并且将入口文件写入main字段。下面是一个例子。// package.json{ “name” : “some-library”, “main” : “./lib/some-library.js” }require发现参数字符串指向一个目录以后,会自动查看该目录的package.json文件,然后加载main字段指定的入口文件。如果package.json文件没有main字段,或者根本就没有package.json文件,则会加载该目录下的index.js文件或index.node文件。参考CommonJS规范Module 的语法 ...

December 29, 2018 · 2 min · jiezi

es6箭头函数深入学习1

基本用法ES6允许使用“箭头”(=>)定义函数。var f = v => v;上面的箭头函数等同于:var f = function(v) { return v;};如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。复制代码var f = () => 5;// 等同于var f = function () { return 5 };var sum = (num1, num2) => num1 + num2;// 等同于var sum = function(num1, num2) { return num1 + num2;};复制代码如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。var sum = (num1, num2) => { return num1 + num2; }由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号。var getTempItem = id => ({ id: id, name: “Temp” });箭头函数可以与变量解构结合使用。复制代码const full = ({ first, last }) => first + ’ ’ + last;// 等同于function full(person) { return person.first + ’ ’ + person.last;}复制代码使用注意点箭头函数有几个使用注意点。(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。(4)不可以使用yield命令,因此箭头函数不能用作Generator函数。this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。复制代码function foo() { setTimeout(() => {console.log(‘args:’, arguments);}, 100);}foo(2, 4, 6, 8)// args: [2, 4, 6, 8]复制代码上面代码中,箭头函数内部的变量arguments,其实是函数foo的arguments变量。另外,由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。复制代码(function() { return [(() => this.x).bind({ x: ‘inner’ })()];}).call({ x: ‘outer’ });// [‘outer’]复制代码上面代码中,箭头函数没有自己的this,所以bind方法无效,内部的this指向外部的this。长期以来,JavaScript语言的this对象一直是一个令人头痛的问题,在对象方法中使用this,必须非常小心。箭头函数”绑定”this,很大程度上解决了这个困扰。函数绑定箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(call、apply、bind)。但是,箭头函数并不适用于所有场合,所以ES7提出了“函数绑定”(function bind)运算符,用来取代call、apply、bind调用。虽然该语法还是ES7的一个提案,但是Babel转码器已经支持。函数绑定运算符是并排的两个双冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。复制代码foo::bar;// 等同于bar.bind(foo);foo::bar(…arguments);// 等同于bar.apply(foo, arguments);const hasOwnProperty = Object.prototype.hasOwnProperty;function hasOwn(obj, key) { return obj::hasOwnProperty(key);}复制代码如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。复制代码var method = obj::obj.foo;// 等同于var method = ::obj.foo;let log = ::console.log;// 等同于var log = console.log.bind(console);复制代码由于双冒号运算符返回的还是原对象,因此可以采用链式写法。尾调用优化什么是尾调用?尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。function f(x){ return g(x);}上面代码中,函数f的最后一步是调用函数g,这就叫尾调用。“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。严格模式ES6的尾调用优化只在严格模式下开启,正常模式是无效的。这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。func.arguments:返回调用时函数的参数。func.caller:返回调用当前函数的那个函数。尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。复制代码function restricted() { “use strict”; restricted.caller; // 报错 restricted.arguments; // 报错}restricted();复制代码箭头函数与常规函数对比一个箭头函数与一个普通的函数在两个方面不一样:下列变量的构造是词法的: arguments , super , this , new.target不能被用作构造函数:没有内部方法 [[Construct]] (该方法允许普通的函数通过 new 调用),也没有 prototype 属性。因此, new (() => {}) 会抛出错误。除了那些意外,箭头函数和普通的函数没有明显的区别。例如, typeof 和 instanceof 产生同样的结果:复制代码typeof () => {}//‘function’() => {} instanceof Function//truetypeof function () {}//‘function’function () {} instanceof Function//true复制代码函数表达式和对象字面量是例外,这种情形下必须放在括号里面,因为它们看起来像是函数声明和代码块。 ...

December 27, 2018 · 1 min · jiezi

javascript filter详解及应用

filter()简单讲filter就是一个数组过滤器,参数接收一个函数,数组的每一项经过函数过滤,返回一个符合过滤条件的新数组函数接收三个参数:item (当前遍历的数组项)i (当前项索引)arr (调用filter数组本身) // 需求找到数组内偶数 let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] let newArr = arr.filter((item, i, arr) => { //函数本身返回布尔值,只有当返回值为true时,当前项存入新数组。 return item % 2 == 0 }) console.log(newArr)再来一个应用,巧妙地用filter结合indexof实现去重let arr1 = [1, 2, 3, 4, 5, 6, 7, 8, 2, 3, 4, 5, 6, 7] let newArr = arr1.filter(function(item, i, self) { let a = self.indexOf(item) console.log(item----${item},self.indexOf(item)---${a},i----${i}) return self.indexOf(item) === i; }); console.log(newArr) //[1, 2, 3, 4, 5, 6, 7, 8]**利用filter的过滤功能和indexof返回数组项所在的索引,相同项返回第一个的索引这个特性。您的点赞是我继续写下去的动力!欢迎吐槽! 谢谢! ...

December 27, 2018 · 1 min · jiezi

深入理解ES6之迭代器与生成器

迭代器迭代器 iterator,在 Javascript 中,迭代器是一个对象(也可称作为迭代器对象),它提供了一个 next() 方法,用来返回迭代序列中的下一项。next 方法的定义,next 方法是一个函数,执行后返回一个对象包含两个属性:{ done: [boolean], value: [any] }// 创建一个迭代器对象function makeIterator(array) { var nextIndex = 0 return { next() { return nextIndex < array.length ? { value: array[nextIndex++], done: false } : { done: true } } }}// iterator 是一个迭代器对象var iterator = makeIterator([10, 20, 30])iterator.next() // {value: 10, done: false}iterator.next() // {value: 20, done: false}iterator.next() // {value: 30, done: false}iterator.next() // {done: true}可迭代对象可迭代对象必须实现一个 @@iterator 方法,也就是说在这个对象或者它的原型链上必须有一个方法名是 Symbol.iterator 的方法,当调用这个方法时它返回一个迭代器对象。可迭代对象的表现形式为,可以使用 for…of 循环,解构赋值,拓展运算符(spread),yield* 这些语法来调用 Symbol.iterator 函数。也就是说这些语法糖在被调用时本质上都是在调用 Symbol.iterator 函数。内置可迭代对象String,Array,TypedArray,Map,Set,函数的arguments对象,NodeList对象都是内置的可迭代对象,他们的原型对象中都有一个 Symbol.iterator 方法。// 可迭代对象let iterable = [10, 20, 30]// 继承自原型链Symbol.iterator in iterable // trueiterable.hasOwnProperty(Symbol.iterator) // falsefor(let value of iterable){ console.log(value)}// 10// 20// 30自定义可迭代对象字面量对象 let o = {} 默认没有 Symbol.iterator 方法,但是我们在对象上自定义一个 @@iterator 方法,此时字面量对象也可以使用 for…of循环,拓展运算符等等语法糖。// 字面量对象默认是不可迭代对象// 自定义对var myIterable = {}myIterable[Symbol.iterator] = function(){ return { arr: [10, 20, 30], next: function(){ if(this.arr.length > 0){ return {value: this.arr.shift(), done: false} }else{ return {done: true} } } }}[…myIterable] // [10, 20, 30]生成器生成器 generator,在 Javascript 中生成器是一个函数(也可称作生成器函数),它可以作为创建迭代器的工厂函数。生成器函数的返回值是一个迭代器对象,同时这个对象也是一个可迭代对象。funtion* 这种声明方式可以定义一个生成器函数。生成器函数的语法规则是,调用一个生成器函数并不会马上执行它里面的语句,而是返回一个这个生成器的 迭代器 (iterator )对象。当这个迭代器的 next() 方法被首次(后续)调用时,其内的语句会执行到第一个(后续)出现yield的位置为止,yield 后紧跟迭代器要返回的值。或者如果用的是 yield*(多了个星号),则表示将执行权移交给另一个生成器函数(当前生成器暂停执行)。调用 next()方法时,如果传入了参数,那么这个参数会作为上一条执行的 yield 语句的返回值。// 生成器函数function* generator(i){ yield i + 1 var y = yield ‘foo’ yield y}var iterator = generator(10) // 此时生成器函数不执行,返回一个迭代器iterator.next() // {value: 11, done: false} iterator.next() // {value ‘foo’, done: false}iterator.next(10) // {value: 10, done: false},将10赋值给上一条 yield ‘foo’ 左侧的值,即 y = 10,返回 yiterator.next() // {done: true}既然生成器函数可以创建迭代器对象,我们来试着将前面的例子用生成器函数的形式重写试试看。// 生成器函数function* makeIterator(array) { for (let i = 0; i < array.length; i++) { yield array[i] }}// 迭代器对象,实现和上文一样的功能var iteratorByGenerator = makeIterator([10, 20, 30])iteratorByGenerator.next() // {value: 10, done: false}iteratorByGenerator.next() // {value: 20, done: false}iteratorByGenerator.next() // {value: 30, done: false}iteratorByGenerator.next() // {done: true}从上面的代码我们可以看到,利用生成器函数来创建一个迭代器对象的方式相比于之前我们普通函数创建的方式更加简洁,也更加清晰的表明调用生成器函数返回的是一个迭代器对象。除此之外还有什么区别呢。上文已经提到,生成器函数返回的是一个可迭代的迭代器对象,这是什么意思呢?看下代码就明白了。// 生成器函数创建的迭代器对象Symbol.iterator in iteratorByGenerator // true[…iteratorByGenerator] // [10, 20, 30]// 普通函数创建的迭代器对象Symbol.iterator in iterator // false[…iterator] // Uncaught TypeError: iterator is not iterable综上所述,我们可以确定的说生成器函数是创建迭代器对象的语法糖,通过生成器函数我们可以用很简洁清晰的语法创建一个可迭代的迭代器对象。 ...

December 27, 2018 · 2 min · jiezi

es6技巧写法(持续更新中~~~)

为class绑定多个值普通写法:class="{a: true, b: true}“其他:class=”[‘btn’, ‘btn2’, {a: true, b: false}]“一个值判断a或者判断b普通写法if(flg === a || flg === b)其他[‘a’,‘b’].indexOf(flg) > -1引用一个组件普通写法import a from ‘./a.vue’componets: { a}node写法components: { a: require(’./a.vue’)}V-FOR渲染一般<li v-for="(item,index) in data” :key=“index”> {{item.uuid}} //输出uuid字段</li>解构赋值<li v-for="{uuid} in data" :key=“uuid”> {{uuid}} //直接解构赋值输出</li>CSS私有化一般设置比较长的class类名区分,或者使用BEN等命名方法css module<style module> .h3 {}</style>style样式会存在$style计算属性中//调用方式<h3 :class="$style.h3"></h3>//$style是计算属性,所以也可以这样 bool为Bool表达式<h3 :class="{$style.h3: bool}"></h3>缺点: 生成一个独一无二的class类名,只能使用类名class控制样式scoped<style scoped></style>生成Hash属性标识.且根元素受父组件的scoped影响解决办法使用>>>深度选择器//寻找div下的样式,包括子组件样式div >>> .h3 { }对象操作对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign方法。// badconst a = {};a.x = 3;// if reshape unavoidableconst a = {};Object.assign(a, { x: 3 });// goodconst a = { x: null };a.x = 3;如果对象的属性名是动态的,可以在创造对象的时候,使用属性表达式定义。// badconst obj = { id: 5, name: ‘San Francisco’,};obj[getKey(’enabled’)] = true;// goodconst obj = { id: 5, name: ‘San Francisco’, [getKey(’enabled’)]: true, //属性表达式 6};数组、对象参数使用扩展运算符(spread)连接多个数组一般let arr1 = [1,2,3]let arr2 = [4,6,7]arr2 = arr2.concat(arr1)spread 运算符let arr1 = [1,2,3]let arr2 = […arr1,4,6,7]连接多个json对象一般var a = { key1: 1, key2: 2 }var b = Object.assign({}, a, { key3: 3 })// 最后结果{key1: 1, key2: 2,key3: 3 }spread 运算符var a = { key1: 1, key2: 2 }var b = {…a, key3: 3}es6剩余参数(rest paramete)使用reset paramete是纯粹的Array实例一般function a() { console.log(arguments)}a(1,2,3)es6function a(…args) { console.log(args)}a(1,2,3)判断数组是否包含指定值IE 任何系列都不支持一般需要自己写工具函数es6var arr = [1,2,3]console.log(arr.includes(1)); // trueconsole.log(arr.includes(4)); // false顺序遍历对象key值IE 任何系列都不支持es6var obj = { key1: 1, key2: 2, key3: 3 }Object.keys(obj); // [“key1”, “key2”, “key3”]顺序遍历对象value值IE 任何系列都不支持es6var obj = { key1: 1, key2: 2, key3: 3 }Object.values(obj); // [1,2,3] ...

December 26, 2018 · 1 min · jiezi

[ ES6 ] 进阶篇(一) —— Promise

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。什么是 Promise从使用上来说是一种语法糖,会了以后写东西简单一些 Promise 翻译过来就是 承诺 诺言 约定 答应 的意思 那么我是不是可以理解为它答应我会去做某件事,或者约好了会做某事 注:示例代码部分使用 jquery 示例特点对象的状态不受外界影响promise 有三个状态:待定(pending),履行(fulfilled),拒绝(rejected)。只有返回的结果可以影响状态,其他任何操作不会影响到这个状态。//待定:初始状态,既未履行也未拒绝。//履行:意味着操作成功完成。//拒绝:意味着操作失败。就像约会,说好了要约会,遵守了约定,有其他原因拒绝了约定。只有约会这件事中的彼此才能决定是遵守还是拒绝,其他人是决定不了的。状态只会改变一次Promise对象的状态改变,只有两种可能:成功(fulfilled)和失败(rejected);只要改变了就会一直是这个结果。 还是约会,到了约会的时间之后结果就是肯定的了。别人第二天问起昨天约会去了吗?也只会回答去了;或者没去。是不可能说第一次回答去了,第二次回答没去,第三次回答去了又没去。新建 promise 就会立即执行,无法中途取消。如果不设置回调函数,Promise内部抛出的错误,不会反应到外部当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。参数Promise有两个参数名需要传递 resolve 和 reject ;分别用来返回成功和失败,他们是两个函数,只需要最后调用一下就行,不用自己去做操作const promise = new Promise((resolve, reject) => {//约会定在下午2点 // 为约会做准备 if (/到时间了/){ resolve(value);//都来了 遵守了约定 } else { reject(error);//有事来不了了 拒绝了约定 }});方法Promise 原型方法//原型方法使用方法Promise.prototype.then();//实际用的时候直接在 Promise 对象后使用方法的即可let p = new Promise();P.then();then() //之后它的参数是回调函数,可以有两个 第一个必填,表示成功了会执行的操作;第二个可以不填,表示失败了会执行的操作(推荐至少有一个接收错误的方法被调用)const promise = new Promise((resolve, reject) => {//约会定在下午2点;新建 Promise 对象 // 为约会做准备;请求数据 if (/到时间了/){ resolve(value);//都来了 遵守了约定;返回成功后的值 } else { reject(error);//有事来不了了 拒绝了约定;返回失败的原因 }});promise.then( value => { //遵守了约定,两个人可以一起做点什么;数据操作}, error => { //拒绝了约定,自己一个人做点什么;显示错误信息或者重试});catch() //捕获它的参数是一个回调函数,表示失败了会执行的操作(推荐使用 catch() 接收错误)promise.catch( error => { //显示错误信息或者重试});finally() //最后它的参数是一个回调函数,表示不管是履行了约定还是拒绝了约定 最后都会执行的操作promise.finally( () => { //执行完 Promise 后执行的操作});Promise 对象方法//直接使用 Promise 关键字就可以调用方法Promise.all();all()参数是 Promise 对象数组或者数组元素返回的都是 Promise 对象 当所有的 Promise 对象全都返回成功时,才会将所有成功的返回值传递给 then() 有一个返回失败就会直接结束当前 Promise ,并将第一个失败的返回值传递给 cath()假如说有一系列数据要获取这时就可以用 Promise.all()//获取 直接用 $.ajax() 是因为 $.ajax() 实际上返回的是 Promise 对象Promise.all([ $.ajax({url:‘api/userInfo’}), $.ajax({url:‘api/banner’}), $.ajax({url:‘api/imagesUrl’})]).then(arr=>{//全部成功才会调用 let [userInfo1,userInfo2,userInfo3] = arr; console.log(userInfo1,userInfo2,userInfo3);}).catch(err=>{//有一个失败就终止并调用 console.log(err);});reace()参数是 Promise 对象数组或者数组元素返回的都是 Promise 对象 只要有一个返回了状态不论是成功或失败都会将值传递给 then()这篇文章主要是自己用来快速查阅 Promise 相关语法的,对于看文档困难的不推荐看 ...

December 26, 2018 · 1 min · jiezi

javascript map()详解

map()不会对空数组进行遍历 let arr = [] let newArr = arr.map((item, i, arr) => { //item:遍历数组的每一项,i:数组当前项的下标,arr原数组 console.log(item---${item}, i---${i}, arr---${arr}) return item + i }) console.log(newArr) //[]**函数内console没有执行,证明数组为空是并不执行遍历返回一个新数组,长度等于原数组长度 let arr = [1, 2, 3] let newArr = arr.map((item, i, arr) => { //item:遍历数组的每一项,i:数组当前项的下标,arr原数组 console.log(item---${item}, i---${i}, arr---${arr}) return item + i }) console.log(newArr) //[undefined, undefined, undefined]**即便函数返回空 结果数组的长度和原数组是一致的不会改变原数组 let arr = [1, 2, 3] let newArr = arr.map((item, i, arr) => { //item:遍历数组的每一项,i:数组当前项的下标,arr原数组 console.log(item---${item}, i---${i}, arr---${arr}) return item + i }) console.log(newArr,arr) //[1, 3, 5]map() 参数必须是函数 let arr = [1, 2, 3] let obj = { 1: 2 } let arr1 = [1, 2] let str = 123 // let newArr = arr.map(obj) let newArr1 = arr.map(arr1) let newArr2 = arr.map(str)函数接收三个参数item (必须) 当前遍历项i (非必须)当前遍历项下标arr (非必须) 原数组完整demo let arr = [1, 2, 3] let newArr = arr.map((item, i, arr) => { //item:遍历数组的每一项,i:数组当前项的下标,arr原数组 console.log(item---${item}, i---${i}, arr---${arr}) return item + i }) console.log(newArr) //[1, 3, 5]您的点赞是我的动力 谢谢! ...

December 26, 2018 · 1 min · jiezi

JS 总结之class

class 是 ES6 的新特性,可以用来定义一个类,实际上,class 只是一种语法糖,它是构造函数的另一种写法。class Person {}typeof Person // “function"Person.prototype.constructor === Person // true???? 使用用法和使用构造函数一样,通过 new 来生成对象实例class Person {}let jon = new Person()???? constructor每个类都必须要有一个 constructor,如果没有显示声明,js 引擎会自动给它添加一个空的构造函数:class Person {}// 等同于class Person { constructor () { }}???? 实例属性和方法,原型属性和方法定义于 constructor 内的属性和方法,即定义在 this 上,属于实例属性和方法,否则属于原型属性和方法。class Person { constructor (name) { this.name = name } say () { console.log(‘hello’) }}let jon = new Person()jon.hasOwnPrototype(’name’) // truejon.hasOwnPrototype(‘say’) // false???? 属性表达式let methodName = ‘say’class Person { constructor (name) { this.name = name } [methodName] () { console.log(‘hello’) }}???? 静态方法不需要通过实例对象,可以直接通过类来调用的方法,其中的 this 指向类本身class Person { static doSay () { this.say() } static say () { console.log(‘hello’) }}Person.doSay() // hello静态方法可以被子类继承// …class Sub extends Person {}Sub.doSay() // hello可以通过 super 对象访问// …class Sub extends Person { static nice () { return super.doSay() }}Sub.nice() // hello???? 严格模式不需要使用 use strict,因为只要代码写在类和模块内,就只能使用严格模式。???? 提升class 不存在变量提升。new Person() // Uncaught ReferenceError: Person is not definedclass Person {}???? name 属性name 属性返回了类的名字,即紧跟在 class 后面的名字。class Person {}Person.name // Person???? this默认指向类的实例。???? 取值函数(getter)和存值函数(setter)class Person { get name () { return ‘getter’ } set name(val) { console.log(‘setter’ + val) }}let jon = new Person()jon.name = ‘jon’ // setter jonjon.name // getter???? class 表达式如果需要,可为类定义一个类内部名字,如果不需要,可以省略:// 需要在类内部使用类名const Person = class Obj { getClassName () { return Obj.name }}// 不需要const Person = class {}立即执行的 Class:let jon = new class { constructor(name) { this.name = name } sayName() { console.log(this.name) }}(‘jon’)jon.sayName() //jon???? 参考《ECMAScript 6 入门》Class 的基本语法 by 阮一峰 ...

December 23, 2018 · 2 min · jiezi

用sort实现orderby

工作到了这个年数, 感觉那些基本函数语法已经跟人合一了, 根本不会为操作一些数据结构而思考半天了. 在做小程序的时候遇到了个orderby的场景, 结果发现没有以为的那么简单. 也许是之前不求甚解的原因, 那么现在来解决orderby的问题.问题的产生与探讨方向在小程序中有个将list的某一条置顶的需求, 在初始化数据到时候可以使用数据库的orderby, 但在更新数据以后再重新初始化就显得有些不妥, 所以我尝试直接使用computed列表来解决这个问题.所以现在的问题是: 输入list, 输出orderby置顶字段.之前以为的sort很简单, 我就尝试了: arr.sort(i => i.stick). 字面看起来是根据stick字段来排序. 输出结果一团糟. 仔细思考了下又尝试了别的方法, 还是失败, 才决定仔细想一下应该如何处理.对sort的理解与快速shuffle先说一下之前对sort的理解.sort接受的参数返回大于0或者小于0. 根据结果来排序.所以有一个快速shuffle数组的方法:arr.sort(() => Math.random() - 0.5)因为函数的返回结果一半是大于0一半是小于0的(不严格, 但之后也认为概率是一半一半). 所以任何输出进行了如此处理, 都会变成一个随机顺序的数组.另外一个例子, 对一个数组: [1, 2, 3, 4, 5, 10, 11, 12]进行排序, 如果不传参数排序结果是错的, 因为默认是localCompare. 所以要写成:arr.sort((a, b) => a - b)这样才能得到正确从小到大的排列.以上就是我多年以来对sort的所有理解. 所以才会写出上面的: arr.sort(i => i.stick)这样搞笑的东西. 因为理解是有问题的.sort是如何排序的因为不知道sort函数得到了结果后是如何排序的. 所以对sort的理解有问题. 而我们知道reduce就是从头到尾遍历并传递每次计算的结果. sort却不知道. 所以打出每次的返回值来看一下每次得到返回值后sort做了些什么.我们要对不同数组进行同样的操作, 排序方法是一样的, 先写一下:const log = (a, b) => { console.log(a, b, a - b > 0) return a - b}开始对不同数组进行排序: 先来1到5[1, 2, 3, 4, 5].sort(log)结果: [1, 2, 3, 4, 5]2 1 true3 2 true4 3 true5 4 true尝试: 从5到1[5, 4, 3, 2, 1].sort(log)结果: [1, 2, 3, 4, 5]4 5 false3 4 false2 3 false1 2 false目前看来, sort应该是插入排序.[3, 5, 7, 9, 2, 1, 6].sort(log)看log的时候我把当前排序结果也打一下:5 3 true [3, 5]7 5 true [3, 5, 7]9 7 true [3, 5, 7, 9]2 9 false // 2还是与当前最大的9比.结果第一次false2 7 false // 于是一路比下来2 5 false2 3 false // 比到最小的, 于是确定了位置 [2, 3, 5, 7, 9]1 5 false // 1选择了与5比, 此时5是中间位置的数, 而不是最大的数1 3 false // 然后一个一个比较下来1 2 false [1, 2, 3, 5, 7, 9]6 5 true // 6还是于5比, 此时5也是中间位置的数6 9 false // 没有选择与7, 而是与9比了6 7 false从这些log能得出一些粗浅的结论:sort是插入排序每次比较的数字会根据两个因素来决定: 分别是之前比较的结果和当前排序的位置如何实现orderby首先明确思路:sort认为每个元素之间的关系是比大小, 所以我们需要做的是写出任意两个元素的相对顺序的普遍公式.先构建一组数据:let gnrt = () => ({ age: Math.round(Math.random() * 50), height: Math.round(Math.random() * 200) })let arr = Array.from({length: 10}).map(() => gnrt())我们先建立纯数字, 无顺序的orderby来理这个思路.let orderby = function (arr, …orders) { return arr.sort((a, b) => { let res = 0 for (let order of orders) { if (a[order] - b[order] !== 0) { res = a[order] - b[order] break } } return res })}调用orderby(arr, ‘height’, ‘age’)就得到了理想的orderby结果了: 根据权重排序, 如果都一样就保持顺序.#后续#这个思路清晰以后, 做兼容就容易了:如果要指定顺序, 在排序参数里带特征, 例如’height’, ‘-height’, 来决定在执行的时候是a - b 还是b - a.如果要指定排序函数(在非数字情况下). 把排序参数改写成兼容function的, 判断是string就执行默认, 是function就调用function即可.当然, 功能越完善的函数就越复杂, 函数本身只是函数复杂度和业务复杂度交换的作用. 具体实现就不写了.所以置顶排序如何实现我们已经想清楚了orderby的实现, 那么置顶排序是stick这个布尔值字段, 就必须根据我上面说的传函数进去, 并且改写orderby函数.这样又要多些2个函数, 所以我选择:[…arr.filter({stick} => stick), …arr.filter({stick} => !stick)]搞定. ...

December 22, 2018 · 2 min · jiezi

ES6-Promise对象

1 是什么先直接上图,打印一下Promise对象,观察下Promise是什么console.dir(Promise)可以知道,Promise是一个构造函数,有着reject、resolve函数。prototype有then、catch等方法,说明了只要是Promise对象都会有这两个方法。Promise构造函数是传入一个函数2 怎么用var promise = new Promise((resolve, reject) => { setTimeout(function () { console.log(‘执行完成’); resolve(‘随便什么数据’); }, 2000);});result:执行完成传入的函数有两个参数:resolve、reject,分别表示异步操作执行成功后的回调函数和异步操作失败后的回调函数。其实这里用“成功”和“失败”来描述并不准确,按照标准来讲,resolve是将Promise的状态置为fullfiled,reject是将Promise的状态置为rejected。不过在我们开始阶段可以先这么理解,后面再细究概念。运行以上代码,发现promise并没有调用,只是new了一个Promise,对应的传入函数的代码就已经执行了。因此,我们通常都是把Promise包在一个函数中,在需要的时候才去运行这个函数,如:function f() { var promise = new Promise((resolve, reject) => { setTimeout(function () { console.log(‘执行完成’); resolve(‘随便什么数据’); }, 2000); }); return promise}f()result:执行完成这时引出两个问题。为什么需要大费周章的包装这样的一个函数?resolve有什么用?2.1 为什么需要引入f().then(function(data){ console.log(data); //后面可以用传过来的数据做些其他操作 //……});在f()的返回上可以直接调用then方法,then接收一个参数,是函数,并且会拿到我们在f中调用resolve时传入的参数。运行这段代码,会在2秒后输出“执行完成”,紧接着输出“随便什么数据”。这时,我们恍然大悟了,原来then调用的function相当于我们之前写的回调函数,相当于是这种形式:function runAsync(callback){ setTimeout(function(){ console.log(‘执行完成’); callback(‘随便什么数据’); }, 2000);}runAsync(function(data){ console.log(data);});那使用Promise有什么好处呢?我们不妨考虑一个问题,如果callback函数也是一个异步操作,而且执行完后也需要有相应的回调函数,可能会变成 runAsync(function(data,function(data){}){…..})。看起来十分丑陋,假如使用Promise代码就不会这么丑陋了~Promise的优势在于,可以在then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作。2.2 链式操作的用法所以,从表面上看,Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。所以使用Promise的正确场景是这样的:runAsync1().then(function(data){ console.log(data); return runAsync2();}).then(function(data){ console.log(data); return runAsync3();}).then(function(data){ console.log(data);});function runAsync1(){ var p = new Promise(function(resolve, reject){ //做一些异步操作 setTimeout(function(){ console.log(‘异步任务1执行完成’); resolve(‘随便什么数据1’); }, 1000); }); return p; }function runAsync2(){ var p = new Promise(function(resolve, reject){ //做一些异步操作 setTimeout(function(){ console.log(‘异步任务2执行完成’); resolve(‘随便什么数据2’); }, 2000); }); return p; }function runAsync3(){ var p = new Promise(function(resolve, reject){ //做一些异步操作 setTimeout(function(){ console.log(‘异步任务3执行完成’); resolve(‘随便什么数据3’); }, 2000); }); return p; }result:异步任务1执行完成随便什么数据1异步任务2执行完成随便什么数据2异步任务3执行完成随便什么数据3我们可以通过链式编程避免了包含多函数回调的情况,当然并不是都需要return Promise对象,你也可以返回其他数据类型,在后面的then中就能直接直接接收到数据了,比如:runAsync1().then(function(data){ console.log(data); return runAsync2();}).then(function(data){ console.log(data); return ‘直接返回数据’; //这里直接返回数据}).then(function(data){ console.log(data);});result:异步任务1执行完成随便什么数据1异步任务2执行完成随便什么数据2直接返回数据2.3 reject用法前面我们一直都是使用resolve函数,并没有使用reject函数。resolve函数是表示了“执行成功”的回调,reject函数表示“执行失败”的函数。function runAsync1() { var p = new Promise(function (resolve, reject) { //做一些异步操作 setTimeout(function () { console.log(‘异步任务1执行完成’); // resolve(‘随便什么数据1’); reject(“error”) }, 1000); }); return p;}runAsync1().then((data) => { console.log(‘resolve’); console.log(data)}, (error, data) => { console.log(‘rejected’); console.log(error)})result:异步任务1执行完成rejectederror需要注意的是,只要执行reject或者resolve,后面的就不会执行了,如:function runAsync1() { var p = new Promise(function (resolve, reject) { //做一些异步操作 setTimeout(function () { console.log(‘异步任务1执行完成’); resolve(‘随便什么数据1’); reject(“error”) }, 1000); }); return p;}因为resolve在reject的前面,所以只会执行resolve,不会执行reject。3 总结Promise是一个对象引入是为了消除多重回调函数Promise还有很多方法没有介绍(catch、finally、success、fail)参考文献ECMAScript6入门大白话讲解Promise(一)ECMAScript6入门署名 广州芦苇科技Java开发团队芦苇科技-广州专业互联网软件服务公司抓住每一处细节 ,创造每一个美好关注我们的公众号,了解更多想和我们一起奋斗吗?lagou搜索“ 芦苇科技 ”或者投放简历到 server@talkmoney.cn 加入我们吧关注我们,你的评论和点赞对我们最大的支持 ...

December 22, 2018 · 1 min · jiezi

ES6新增命令:let

let的介绍let是ES6新增的命令。作用:声明变量。类似于:var。与var的区别:使用let声明的变量,只在其所在的代码块内有效。定义回顾声明变量:可以用var,也可以不用var。是否允许变量提升:允许。是否允许重复声明同一个变量:允许。变量的作用域:全局作用域、函数作用域。在全局作用域中,无论是否使用var,定义的变量都是全局变量。在函数作用域中,使用var定义函数局部变量,不使用var定义全局变量。全局变量全局可用,函数局部变量在函数内可用。代码块:用{}栝起来的代码片段。基本语法let声明的变量,只在其所在其所在的代码块内有效。let不存在变量提升。let不允许重复声明。let不允许在函数的一级作用域内重新声明参数。let存在的块级作用域,它声明的这个变量或“绑定”这个区域,形成“暂时性死区”,使其不再受外部影响。就是说,一个变量,无论其在外部是否声明,只要在某个块级作用域内使用let重新声明了,那么在这个块级作用域内该变量在声明前是不可以使用的。使用场景for循环的计数器let声明的循环变量i只在本轮循环有效,每一次循环的i都是一个新变量。特别的JavaScript引擎内部会记住上一轮循环的值,初始化本轮的变量i时,是在上一轮循环的基础上进行计算。另外,for循环设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的自作用域。代码片段part 1:var声明变量回顾<script type=“text/javascript”> var a = 1; console.log(a); // 1 b = 2; console.log(b); // 2 console.log(c); //undefined var c = 3; var a = 4; function showA(){ console.log(a); d = 5; console.log(d); } showA(); // 4,5 console.log(d); //5 function showB(){ console.log(b); var e = 6; console.log(e); } showB(); //2,6 console.log(e); //报错</script>part 2:let声明的变量时局部变量<script type=“text/javascript”> { var a = 1; let b = 2; } console.log(a); //1 console.log(b); //报错</script>part 3:for循环中使用var定义循环参数,该参数会变味全局参数<script type=“text/javascript”> var a = []; for(var i = 0; i < 10; i++){ a[i] = function(){ console.log(i); } } a6; //10</script>part 4:for循环中使用let定义循环参数,改参数是块级作用域内的参数<script type=“text/javascript”> for(let i = 0; i < 10; i++){ } console.log(i); //报错</script>part 5:对比part3,用var定义循环变量<script type=“text/javascript”> var a = []; for(let i = 0; i < 10; i++){ a[i] = function(){ console.log(i); } } a6; // 6</script>part 6:for循环中,外层是一个父作用域,里面的每层循环是一个自作用域,互不影响<script type=“text/javascript”> for(let i = 0; i < 3; i++){ let i = ‘abc’; console.log(i); //abc,abc,abc }</script>part 7:let声明的变量不允许变量提升<script type=“text/javascript”> console.log(a); //报错 let a = 1;</script>part 8:块级作用域中用let声明的变量会形成【暂时性死区】,即使在全局声明了该变量,也不能在let声明前调用<script type=“text/javascript”> let a = 1; console.log(a); //1 { a = 2; let a; console.log(a); // 报错 }</script>part 9:let声明的变量不允许重复声明<script type=“text/javascript”> let a = 1; console.log(a); // 1 let a = 2; console.log(a); //报错</script>part 10:不允许在函数的一级作用域内使用let重新声明参数<script type=“text/javascript”> function showName(name){ let name = ‘sunny’; console.log(name); } showName(’lily’); // 报错</script>part 11:可以在函数的二级或三级等作用域内使用let重新声明参数,且与一级作用域中的参数互不影响<script type=“text/javascript”> function showName(name){ console.log(name); { let name = ‘sunny’; console.log(name); } } showName(’lily’); //lily,sunny</script> ...

December 21, 2018 · 1 min · jiezi

不定参数(rest 参数 ...)

不定参数如何实现不定参数使用过 underscore.js 的人,肯定都使用过以下几个方法:.without(array, *values) //返回一个删除所有values值后的array副本.union(*arrays) //返回传入的arrays(数组)并集_.difference(array, *others)//返回来自array参数数组,并且不存在于other 数组…这些方法都有一个共同点,就是可以传入不定数量的参数,例如,我想删除掉 array 中的 value1,value2 ,可以这样使用 , .without(array,value1,value2);那么,这个需要怎样才能做到呢?我们知道,js 中 function 里面,有一个 arguments 参数,它是一个类数组,里面包含着调用这个方法的所有参数,所以可以这样处理:.without = function() { if (arguments.length > 0) { var array = arguments[0]; var values = []; for (var i = 1; i < arguments.length; i++) { values.push(arguments[i]); } //这样得到了array,和values数组,便可以进一步处理了 }};上面只是打个比方,想要支持不定参数,我们要做的就是把固定参数和动态参数从 arguments 中分离出来。但是,我们这样写的话,需要在每个支持不定参数的函数里,都 copy 这样一段代码,这样实在不是很优雅。所以需要封装成一个通用的函数。我们直接看看 underscore 是封装的好了。restArgs 源码var restArgs = function(func, startIndex) { //startIndex ,表示几个参数之后便是动态参数 startIndex = startIndex == null ? func.length - 1 : +startIndex; return function() { var length = Math.max(arguments.length - startIndex, 0); //处理arguments,将动态参数保存进rest数组 var rest = Array(length); for (var index = 0; index < length; index++) { rest[index] = arguments[index + startIndex]; } //处理0,1,2三种情况,这里要单独处理,是想优先使用call,因为,call的性能比apply要好一点 switch (startIndex) { case 0: return func.call(this, rest); case 1: return func.call(this, arguments[0], rest); case 2: return func.call(this, arguments[0], arguments[1], rest); } //如果startIndex不是0,1,2三种情况,则使用apply调用方法,将args作为参数,args将为数组[固定参数 ,rest]; var args = Array(startIndex + 1); for (index = 0; index < startIndex; index++) { args[index] = arguments[index]; } args[startIndex] = rest; return func.apply(this, args); };};//这里without主要的逻辑处理方法,作为参数,传给restArgs,在restArgs中处理完参数后,使用call或apply调用逻辑处理方法// 这时候接受到参数otherArrays,已经是一个数组了,包含了之前的动态参数。_.without = restArgs(function(array, otherArrays) { //处理without具体事件});underscore.js 中利用 js 高级函数的特性,巧妙的实现了动态参数如果要使某函数支持不定参数,只需要将该函数作为参数,传入 restArgs 中即可,例如:function addByArray(values) { var sum = 0; for (var i = 0; i < values.length; i++) { sum += values[i]; } return sum;}var add = restArgs(addByArray);//调用:addByArray([2, 5, 3, 6]); //16add(2, 5, 3, 6); //16ES6 不定参数 (…)ES6 引入了 rest 参数,(形式为"…变量名"),用于获取多余参数,这样就不需要使用 arguments 对象来实现了function add(…values) { let sum = 0; for (var val of values) { sum += val; } return sum;}add(2, 5, 3); // 10总结在 underscore 中,restArgs 只是为了支持不定参数。实际使用中,也许我们都是直接使用 ES6,或用 babel 将 ES6 转成 ES5 来支持不定参数不过,如果是在非 es6 的环境下,知道有这么一种实现方式,也是挺好的。:) ...

December 21, 2018 · 2 min · jiezi

Promise 简单实现

Promise 简单实现前言你可能知道,javascript 的任务执行的模式有两种:同步和异步。异步模式非常重要,在浏览器端,耗时很长的操作(例如 ajax 请求)都应该异步执行,避免浏览器失去响应。在异步模式编程中,我们经常使用回调函数。一不小心就可能写出以下这样的代码://事件1doSomeThing1(function() { //事件2 doSomeThing2(function() { //事件3 doSomeThing3(); });});当你的需要异步执行的函数越来越多,你的层级也会越来越深。这样的写法存在的缺点是:不利于阅读各个任务之间的高度耦合,难以维护对异常的处理比较难用 Promise 可以避免这种回调地狱,可以写成这样//事件1doSomeThing1() .then(function() { //事件2 return doSomeThing2(); }) .then(function() { //事件3 return doSomeThing3(); }) .catch(function() { //这里可以很方便的做异常处理 });在市面上有许多库都实现了 Promise,例如:Q.js 、when.js ,es6 也将 Promise 纳入了标准中es6 的 Promise 使用方法可以参考阮一峰的 http://es6.ruanyifeng.com/#do… ,我就不在做具体介绍接下来,我们模仿 ES6 的 promise,一步一步来实现一个简单版的 Promise。构造函数我们使用 Promise 的时候,const promise = new Promise((resolve, reject)=>{ // … some code if (/* 异步操作成功 */){ resolve(value); } else { reject(error); }});Promise 是一个构造函数,接收一个函数,函数里有两个参数,resolve、reject。我们可以这样子实现:class PromiseA { constructor(executor) { const resolve = value => { this.resolve(value); }; const reject = err => { this.reject(err); }; try { executor(resolve, reject); } catch (err) { reject(err); } } resolve(value) { this.resultValue = value; } reject(error) { this.resultValue = error; }}then 方法promise 中,用的最频繁的就是 then 方法,then 方法有两个参数,一个是 promise 成功时候的回调,一个是失败的回调。实现方式为:class PromiseA { constructor(executor) { const resolve = value => { this.resolve(value); }; const reject = err => { this.reject(err); }; try { executor(resolve, reject); } catch (err) { reject(err); } } resolve(value) { this.resultValue = value; if (this.fullfillCallback) { //++++++++ this.fullfillCallback(value); } } reject(error) { this.resultValue = error; if (this.rejectCallback) { //++++++++ this.rejectCallback(value); } } then(resolve, reject) { this.fullfillCallback = resolve; this.rejectCallback = resolve; }}then 方法有以下几个特性:支持链式操作每次 then 方法都是返回新的 Promise当前 promise 的状态通过返回值传递给下一个 promise错误冒泡,即如果当前 promise 没有提供 onReject 方法,会把错误冒泡到下一个 promise,方便处理then(onResolve,onReject){ //返回新的Promise并支持链式操作 return new PromiseA((resolve,reject)=>{ this.fullfillCallback = (value)=>{ try { if (onResolve) { let newValue = onResolve(value); resolve(newValue); //将当前promise执行结果,传递给下一个promise } else { resolve(value); } } catch (err) { reject(err); } } //类似fullfillCallback this.rejectCallback = (value)=>{ try { if (onReject) { let newValue = onReject(value); resolve(newValue); } else { //错误冒泡 reject(value); } } catch (err) { reject(err); } } });}这样我们就实现了一个简单版的 then 方法了加强版 then上面的实现,模拟了 then 方法的逻辑,但是还有一些缺点:then 方法只能添加一个,例如let p = new PromiseA(resolve => { setTimeout(() => { resolve(1); }, 0);});p.then(value => { console.log(’then–>’ + value);}); //无输出,因为没触发到,被后一个覆盖了p.then(value => { console.log(’then2–>’ + value);}); ////then—> 1promise 没有状态,当 promsie 在添加 then 的时候已经完成了,没法得到结果没有实现:如果上一个 promise 的返回值也是一个 Promise 对象时,则会等到这个 Promise resolve 的时候才执行下一个为了解决第一点,引入了事件监听,简单的实现如下:export default class EventEmitter { constructor() { this._events = {}; } on(type, fn, context = this) { if (!this._events[type]) { this._events[type] = []; } this._events[type].push([fn, context]); } trigger(type) { let events = this._events[type]; if (!events) { return; } let len = events.length; let eventsCopy = […events]; for (let i = 0; i < len; i++) { let event = eventsCopy[i]; let [fn, context] = event; if (fn) { fn.apply(context, [].slice.call(arguments, 1)); } } }}所以进一步对 PromiseA 进行改造:const STATUS = { PENDING: ‘pending’, FULFILLED: ‘fulfilled’, REJECTED: ‘rejected’};const EventType = { fulfill: ‘fulfill’, reject: ‘reject’};class PromiseA { constructor(executor) { //初始化事件监听及状态 this.eventEmitter = new EventEmitter(); this.status = STATUS.PENDING; const resolve = value => { if (value instanceof PromiseA) { value.then( value => { this.resolve(value); }, error => { this.reject(error); } ); } else { this.resolve(value); } }; const reject = err => { this.reject(err); }; try { executor(resolve, reject); } catch (err) { reject(err); } } resolve(value) { //增加状态 if (this.status === STATUS.PENDING) { this.status = STATUS.FULFILLED; this.resultValue = value; this.eventEmitter.trigger(EventType.fulfill, value); } } reject(error) { //增加状态 if (this.status === STATUS.PENDING) { this.status = STATUS.REJECTED; this.resultValue = error; this.eventEmitter.trigger(EventType.reject, error); } } then(onResolve, onReject) { //根据状态不同处理 if (this.status === STATUS.PENDING) { return new PromiseA((resolve, reject) => { //增加事件监听 this.eventEmitter.on(‘fulfill’, value => { try { if (onResolve) { let newValue = onResolve(value); resolve(newValue); } else { resolve(value); } } catch (err) { reject(err); } }); //增加事件监听 this.eventEmitter.on(‘reject’, value => { try { if (onReject) { let newValue = onReject(value); resolve(newValue); } else { reject(value); } } catch (err) { reject(err); } }); }); } if ( this.status === STATUS.FULFILLED || this.status === STATUS.REJECTED ) { return new PromiseA((resolve, reject) => { let callback = returnValue; if (this.status === STATUS.FULFILLED) { callback = onResolve; } if (this.status === STATUS.REJECTED) { callback = onReject; } try { let newValue = callback(this.resultValue); resolveValue(newValue, resolve, reject); } catch (err) { reject(err); } }); } }}到这里,我们的 then 方法基本就完成了。最后还有一个小知识点,就是执行时机的问题:setTimeout(function() { console.log(4);}, 0);new Promise(function(resolve) { console.log(1); resolve();}).then(function() { console.log(3);});console.log(2);//输出结果会是: 1、2、3、4promise.then,是异步的,属于 microtask,执行时机是本次事件循环结束之前,而 setTimeout 是 macrotask,执行时机是在下一次事件循环的开始之时实现这个功能,我利用了第三方库 microtask 来模拟。所以 PromiseA 修改为: resolve(value) { microtask(() => { if (this.status === STATUS.PENDING) { this.status = STATUS.FULFILLED; this.resultValue = value; this.eventEmitter.trigger(EventType.fulfill, value); } }) } reject(error) { microtask(() => { if (this.status === STATUS.PENDING) { this.status = STATUS.REJECTED; this.resultValue = error; this.eventEmitter.trigger(EventType.reject, error); } }); }到此为止,我们的 then 方法已经大功告成了。最困难的一步已经解决了catchPromise 跟普通回调的一大优势就是异常处理,我们推荐使用 Promise 的时候,总是使用 catch 来代替 then 的第二个参数。即是://badlet p = new Promise((resolve, reject) => { //…异步操作}).then( value => { //成功 }, () => { //失败 });//goodlet p = new Promise((resolve, reject) => { //…异步操作}) .then(value => { //成功 }) .catch(() => { //失败 });接下来让我们实现 catch 方法:catch(reject) { return this.then(null, reject);}哈哈~ , 你没看错。你已经实现了 catch 方法all 方法Promise.all 是一个很好用的方法。接受一个 promise 数组,然后等到所有的异步操作都完成了,就返回一个数组,包含对应的值具体实现如下:static all(promiseList = []) { //返回promise以便链式操作 return new PromiseA((resolve, reject) => { let results = []; let len = promiseList.length; let resolveCount = 0; //用于计数 let resolver = function (index, value) { resolveCount++; results[index] = value; if (resolveCount === len) { resolve(results); } }; //遍历执行所有的promise promiseList.forEach((p, i) => { if (p instanceof PromiseA) { p.then((value) => { resolver(i, value); }, (err) => { reject(err); }) } else { resolver(i, p); } }) });}race 方法race 方法为竞速,第一执行完的为准。所以只需循环一遍执行就可以了。当有第一个将 Promise 的状态改变成 fullfilled 或 reject 之后,其他的就都无效了static race(promiseList = []) { return new PromiseA((resolve, reject) => { promiseList.forEach((p, i) => { if (p instanceof PromiseA) { p.then((value) => { resolve(value); }, (err) => { reject(err); }) } else { resolve(p); } }) })}小结我们实现了一个简单版的 promise, 还有一些其他的方法在这里没有讲到。感兴趣的朋友可以自行去研究哈~附上代码完整的实现 : https://github.com/chen434202…个人博客链接:https://chen4342024.github.io… ...

December 20, 2018 · 4 min · jiezi

JS 总结之关于 this 应该知道的几个点

this 对每个 Jser 都不陌生,经常看到对象这里 this 那里 this,那什么是 this?答案就是上下文对象,即被调用函数所处的环境,也就是说,this 在函数内部指向了调用它(它指函数函数)的对象,通俗的讲,就是谁调用了函数。???? 情况 1this 指向 windowvar name = ‘xiaoming’ // 思考,为什么不能用 let 或者 const ?function foo () { console.log(this.name)}foo() // xiaoming谁调用了这个函数,答案就是 window。这好理解,因为这里的变量和函数都是直接挂在 window 上的,等同于 window.foo()。需注意,严格模式下,this 为 undefined???? 情况 2this 指向一个对象var name = ‘xiaoming’var foo = { name: ‘Jon’, getName () { console.log(this.name) }}foo.getName() // Jon谁调用了这个函数,答案是 foo 对象,所以打印了 Jon 而不是 xiaomingvar bar = foo.getNamebar() // xiaoming如果赋值到另一个变量,就变成 window 调用,所以打印了 xiaoming???? 情况 3this 指向了一个用 new 新生成的对象function Person (name) { this.name = name this.getName = function () { console.log(this.name) }}var jser = new Person(‘Jon’)jser.getName() // Jon这种方式成为 new 绑定,也叫做构造调用。JavaScript 中,new 的机制实际上和面向类的语言完全不同,构造函数只是一些使用 new 操作符时被调用的函数,它们并不会属于某个类,也不会实例化一个类。实际上,除 ES6 的 Symbol()外,所有函数都可以用 new 来调用,所以并不存在所谓的构造函数,只有对于函数进行构造调用。使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:创建(或者说构造)一个全新的对象这个新对象会被执行原型链接这个新对象会绑定到函数调用的 this如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象这个例子中,使用 new 来调用 Person(..) 时,我们会构造一个新对象(jser)并把它绑定到 Person(..) 调用中的 this 上。或者可以这么想,谁调用了 getName ?就是 jser 调用了,所以 this 指向了 jser???? 情况 4用 call,apply 和 bind 来修改 this 指向call / applycall 和 apply 是以不同对象作为上下文对象来调用某个函数,举个例子:var bar = { name: ‘bar’, getName () { console.log(this.name) }}var foo = { name: ‘foo’}bar.getName.call(foo) // foo看起来像是借用函数,对象 foo 借用了 bar 的函数 getName,所以我们判断一个对象类型,经常这么搞:let foo = [1,2,3,4,5]Object.prototype.toString.call(foo) // “[object Array]“apply 和 call 的用法一样,不同点在于 call 用参数表给调用函数传参,而 apply 使用了数组bindbind 可以永久性的修改函数中的 this 的指向,无论谁调用,this 指向都一样,并返回了完成绑定的函数,看例子:var bar = { name: ‘bar’, getName () { console.log(this.name) }}var foo = { name: ‘foo’}foo.func = bar.getName.bind(bar)foo.func() // bar这里的 func 不受 foo 影响,this 还是指向了 barvar bar = { name: ‘bar’, getName () { console.log(this.name) }}func = bar.getName.bind(bar)func() // bar这里的 func 也不受 window 影响,this 还是指向了 bar综合上述,bind 强制修改了 this,谁调用了函数 this 都不能被修改???? 忽略 this如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值再调用时会被忽略,实际应用的是默认绑定。???? 箭头函数ES6 中介绍了一种无法使用这些规则的特殊函数类型:箭头函数,根据外层(函数或者全局)作用域来决定 this,箭头函数常用于回调函数???? 情况 1 中,为什么不能用 let 声明?ES6 中,let 命令、const 命令、class 命令声明的全局变量,不属于顶层对象的属性,window 无法访问到。var 命令和 function 命令声明的全局变量,属于顶层对象的属性,window 能访问到。所以 情况 1 中改为:let name = ‘xiaoming’function foo () { console.log(this.name)}foo() // undefined❄️ 总结自:《你不知道的 JavaScript 上卷》第二部分 第 2 章《Node.js 开发指南》附录 A《ECMAScript 6 入门》let 和 const 命令 - 顶层对象的属性【进阶 3-5 期】深度解析 new 原理及模拟实现前端总结小本本 更新时间为:周二,周四,周六,内容不定。如想查看最新未完成总结,可请查看:https://github.com/KaronAmI/blog如有启发,欢迎 star,如有错误,请务必指出,十分感谢。???? ...

December 20, 2018 · 2 min · jiezi

ES6语法

ES6一些常用语法JavaScript 定义对象的属性方法一var obj.a = 1;方法二var obj[a] = 1;方法三 (这是es6的新语法允许字面量定义对象,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。)let propKey = ‘a’;let obj = { [propKey]: ‘ooo’, [‘a’+‘1’]: 111};除此之外还可以用这样的方式定义方法名(这个可能在一些特定的场景有巧用,值得期待!!!)let obj = { ’t’ + ‘Handle’ { console.log(‘Amazing!!!’); }};obj.tHandle() // Amazing!!!

December 19, 2018 · 1 min · jiezi

JavaScript是如何工作的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式!

此篇是 JavaScript是如何工作的第四篇,其它三篇可以看这里:JavaScript是如何工作的:引擎,运行时和调用堆栈的概述!JavaScript是如何工作的:深入V8引擎&编写优化代码的5个技巧!JavaScript如何工作:内存管理+如何处理4个常见的内存泄漏!通过第一篇文章回顾在单线程环境中编程的缺陷以及如何解决这些缺陷来构建健壮的JavaScript UI。按照惯例,在本文的最后,分享5个如何使用async/ wait编写更简洁代码的技巧。为什么单线程是一个限制?在发布的第一篇文章中,思考了这样一个问题:当调用堆栈中有函数调用需要花费大量时间来处理时会发生什么?例如,假设在浏览器中运行一个复杂的图像转换算法。当调用堆栈有函数要执行时,浏览器不能做任何其他事情——它被阻塞了。这意味着浏览器不能渲染,不能运行任何其他代码,只是卡住了。那么你的应用 UI 界面就卡住了,用户体验也就不那么好了。在某些情况下,这可能不是主要的问题。还有一个更大的问题是一旦你的浏览器开始处理调用堆栈中的太多任务,它可能会在很长一段时间内停止响应。这时,很多浏览器会抛出一个错误,提示是否终止页面:JavaScript程序的构建块你可能在单个.js文件中编写 JavaScript 应用程序,但可以肯定的是,你的程序由几个块组成,其中只有一个正在执行,其余的将在稍后执行。最常见的块单元是函数。大多数刚接触JavaScript的开发人员似乎都有这样的问题,就是认为所有函数都是同步完成,没有考虑的异步的情况。如下例子:你可能知道标准 Ajax 请求不是同步完成的,这说明在代码执行时 Ajax(..) 函数还没有返回任何值来分配给变量 response。一种等待异步函数返回的结果简单的方式就是 回调函数:注意:实际上可以设置同步Ajax请求,但永远不要那样做。如果设置同步Ajax请求,应用程序的界面将被阻塞——用户将无法单击、输入数据、导航或滚动。这将阻止任何用户交互,这是一种可怕的做法。以下是同步 Ajax 地,但是请千万不要这样做:这里使用Ajax请求作为示例,你可以让任何代码块异步执行。这可以通过 setTimeout(callback,milliseconds) 函数来完成。setTimeout 函数的作用是设置一个回调函数milliseconds后执行,如下:function first() { console.log(‘first’);}function second() { console.log(‘second’);}function third() { console.log(’third’);}first();setTimeout(second, 1000); // Invoke second after 1000msthird();输出:firstthirdsecond解析事件循环这里从一个有点奇怪的声明开始——尽管允许异步 JavaScript 代码(就像上例讨论的setTimeout),但在ES6之前,JavaScript本身实际上从来没有任何内置异步的概念,JavaScript引擎在任何给定时刻只执行一个块。那么,是谁告诉JS引擎执行程序的代码块呢?实际上,JS引擎并不是单独运行的——它是在一个宿主环境中运行的,对于大多数开发人员来说,宿主环境就是典型的web浏览器或Node.js。实际上,现在JavaScript被嵌入到各种各样的设备中,从机器人到灯泡,每个设备代表 JS 引擎的不同类型的托管环境。所有环境中的共同点是一个称为事件循环的内置机制,它处理程序的多个块在一段时间内通过调用调用JS引擎的执行。这意味着JS引擎只是任意JS代码的按需执行环境,是宿主环境处理事件运行及结果。例如,当 JavaScript 程序发出 Ajax 请求从服务器获取一些数据时,在函数(“回调”)中设置“response”代码,JS引擎告诉宿主环境:“我现在要推迟执行,但当完成那个网络请求时,会返回一些数据,请回调这个函数并给数据传给它”。然后浏览器将侦听来自网络的响应,当监听到网络请求返回内容时,浏览器通过将回调函数插入事件循环来调度要执行的回调函数。以下是示意图:这些Web api是什么?从本质上说,它们是无法访问的线程,只能调用它们。它们是浏览器的并发部分。如果你是一个Nojs.jsjs开发者,这些就是 c++ 的 Api。这样的迭代在事件循环中称为(tick)标记,每个事件只是一个函数回调。让我们“执行”这段代码,看看会发生什么:1.初始化状态都为空,浏览器控制台是空的的,调用堆栈也是空的2. console.log(‘Hi’)添加到调用堆栈中3. 执行console.log(‘Hi’)4. console.log(‘Hi’)从调用堆栈中移除。5. setTimeout(function cb1() { … }) 添加到调用堆栈。6. setTimeout(function cb1() { … }) 执行,浏览器创建一个计时器计时,这个作为Web api的一部分。7. setTimeout(function cb1() { … })本身执行完成,并从调用堆栈中删除。8. console.log(‘Bye’) 添加到调用堆栈9. 执行 console.log(‘Bye’)10. console.log(‘Bye’) 从调用调用堆栈移除11. 至少在5秒之后,计时器完成并将cb1回调推到回调队列。12. 事件循环从回调队列中获取cb1并将其推入调用堆栈。13. 执行cb1并将console.log(‘cb1’)添加到调用堆栈。14. 执行 console.log(‘cb1’)15. console.log(‘cb1’) 从调用堆栈中移除16. cb1 从调用堆栈中移除快速回顾:值得注意的是,ES6指定了事件循环应该如何工作,这意味着在技术上它属于JS引擎的职责范围,不再仅仅扮演宿主环境的角色。这种变化的一个主要原因是ES6中引入了 Promises,因为ES6需要对事件循环队列上的调度操作进行直接、细度的控制。setTimeout(…) 是怎么工作的需要注意的是,setTimeout(…)不会自动将回调放到事件循环队列中。它设置了一个计时器。当计时器过期时,环境将回调放到事件循环中,以便将来某个标记(tick)将接收并执行它。请看下面的代码:setTimeout(myCallback, 1000);这并不意味着myCallback将在1000毫秒后就立马执行,而是在1000毫秒后,myCallback被添加到队列中。但是,如果队列有其他事件在前面添加回调刚必须等待前后的执行完后在执行myCallback。有不少的文章和教程上开始使用异步JavaScript代码,建议用setTimeout(回调,0),现在你知道事件循环和setTimeout是如何工作的:调用setTimeout 0毫秒作为第二个参数只是推迟回调将它放到回调队列中,直到调用堆栈是空的。请看下面的代码:console.log(‘Hi’);setTimeout(function() { console.log(‘callback’);}, 0);console.log(‘Bye’);虽然等待时间被设置为0 ms,但在浏览器控制台的结果如下:HiByecallbackES6的任务队列是什么?ES6中引入了一个名为“任务队列”的概念。它是事件循环队列上的一个层。最为常见在Promises 处理的异步方式。现在只讨论这个概念,以便在讨论带有Promises的异步行为时,能够了解 Promises 是如何调度和处理。想像一下:任务队列是一个附加到事件循环队列中每个标记末尾的队列。某些异步操作可能发生在事件循环的一个标记期间,不会导致一个全新的事件被添加到事件循环队列中,而是将一个项目(即任务)添加到当前标记的任务队列的末尾。这意味着可以放心添加另一个功能以便稍后执行,它将在其他任何事情之前立即执行。任务还可能创建更多任务添加到同一队列的末尾。理论上,任务“循环”(不断添加其他任务的任等等)可以无限运行,从而使程序无法获得转移到下一个事件循环标记的必要资源。从概念上讲,这类似于在代码中表示长时间运行或无限循环(如while (true) ..)。任务有点像 setTimeout(callback, 0) “hack”,但其实现方式是引入一个定义更明确、更有保证的顺序:稍后,但越快越好。回调正如你已经知道的,回调是到目前为止JavaScript程序中表达和管理异步最常见的方法。实际上,回调是JavaScript语言中最基本的异步模式。无数的JS程序,甚至是非常复杂的程序,除了一些基本都是在回调异步基础上编写的。然而回调方式还是有一些缺点,许多开发人员都在试图找到更好的异步模式。但是,如果不了解底层的内容,就不可能有效地使用任何抽象出来的异步模式。在下一章中,我们将深入探讨这些抽象,以说明为什么更复杂的异步模式(将在后续文章中讨论)是必要的,甚至是值得推荐的。嵌套回调请看以下代码:我们有一个由三个函数组成的链嵌套在一起,每个函数表示异步系列中的一个步骤。这种代码通常被称为“回调地狱”。但是“回调地狱”实际上与嵌套/缩进几乎没有任何关系,这是一个更深层次的问题。首先,我们等待“单击”事件,然后等待计时器触发,然后等待Ajax响应返回,此时可能会再次重复所有操作。乍一看,这段代码似乎可以将其异步性自然地对应到以下顺序步骤:listen(‘click’, function (e) { // ..});然后:setTimeout(function(){ // ..}, 500);接着:ajax(‘https://api.example.com/endpoint', function (text){ // ..});最后:if (text == “hello”) { doSomething();}else if (text == “world”) { doSomethingElse();}因此,这种连续的方式来表示异步代码似乎更自然,不是吗?一定有这样的方法,对吧?Promises请看下面的代码:var x = 1;var y = 2;console.log(x + y);这非常简单:它对x和y的值进行求和,并将其打印到控制台。但是,如果x或y的值丢失了,仍然需要求值,要怎么办?例如,需要从服务器取回x和y的值,然后才能在表达式中使用它们。假设我们有一个函数loadX和loadY,它们分别从服务器加载x和y的值。然后,一旦x和y都被加载,假设我们有一个函数sum,它对x和y的值进行求和。它可能看起来像这样(很丑,不是吗?)这里有一些非常重要的事情——在这个代码片段中,我们将x和y作为异步获取的的值,并且执行了一个函数sum(…)(从外部),它不关心x或y,也不关心它们是否立即可用。当然,这种基于回调的粗略方法还有很多不足之处。 这只是一个我们不必判断对于异步请求的值的处理方式一个小步骤而已。Promise Value用Promise来重写上例:在这个代码片段中有两层Promise。fetchX 和 fetchY 先直接调用,返回一个promise,传给 sum。 sum 创建并返回一个Promise,通过调用 then 等待 Promise,完成后,sum 已经准备好了(resolve),将会打印出来。第二层是 sum(…) 创建的 Promise ( 通过 Promise.all([ ... ]) )然后返回 Promise,通过调用then(…)来等待。当 sum(…) 操作完成时,sum 传入的两个 Promise 都执行完后,可以打印出来了。这里隐藏了在sum(…)中等待x和y未来值的逻辑。注意:在sum(...)内,Promise.all([...])调用创建一个 promise(等待 promiseX 和 promiseY 解析)。 然后链式调用 .then(...)方法里再的创建了另一个 Promise,然后把 返回的 x 和 和(values[0] + values[1]) 进行求和 并返回 。因此,我们在sum(...)末尾调用then(...)方法 — 实际上是在返回的第二个 Pwwromise 上运行,而不是由Promise.all([ ... ])创建 Promise。 此外,虽然没有在第二个 Promise 结束时再调用 then方法 ,其时这里也创建一个 Promise。 Promise.then(…) 实际上可以使用两个函数,第一个函数用于执行成功的操作,第二个函数用于处理失败的操作:如果在获取x或y时出现错误,或者在添加过程中出现某种失败,sum(…) 返回的 Promise将被拒绝,传递给 then(…) 的第二个回调错误处理程序将从 Promise 接收失败的信息。从外部看,由于 Promise 封装了依赖于时间的状态(等待底层值的完成或拒绝,Promise 本身是与时间无关的),它可以按照可预测的方式组成,不需要开发者关心时序或底层的结果。一旦 Promise 决议,此刻它就成为了外部不可变的值。可链接调用 Promise 真的很有用:创建一个延迟2000ms内完成的 Promise ,然后我们从第一个then(...)回调中返回,这会导致第二个then(...)等待 2000ms。注意:因为Promise 一旦被解析,它在外部是不可变的,所以现在可以安全地将该值传递给任何一方,因为它不能被意外地或恶意地修改,这一点在多方遵守承诺的决议时尤其正确。一方不可能影响另一方遵守承诺决议的能力,不变性听起来像是一个学术话题,但它实际上是承诺设计最基本和最重要的方面之一,不应该被随意忽略。使用 Promise 还是不用?关于 Promise 的一个重要细节是要确定某个值是否是一个实际的Promise 。换句话说,它是否具有像Promise 一样行为?我们知道 Promise 是由new Promise(…)语法构造的,你可能认为 p instanceof Promise是一个足够可以判断的类型,嗯,不完全是。这主要是因为可以从另一个浏览器窗口(例如iframe)接收 Promise 值,而该窗口或框架具有自己的 Promise 值,与当前窗口或框架中的 Promise 值不同,所以该检查将无法识别 Promise 实例。此外,库或框架可以选择性的封装自己的 Promise,而不使用原生 ES6 的Promise 来实现。事实上,很可能在老浏览器的库中没有 Promise。吞掉错误或异常如果在 Promise 创建中,出现了一个javascript一场错误(TypeError 或者 ReferenceError),这个异常会被捕捉,并且使这个 promise 被拒绝。但是,如果在调用 then(…) 方法中出现了 JS 异常错误,那么会发生什么情况呢?即使它不会丢失,你可能会发现它们的处理方式有点令人吃惊,直到你挖得更深一点:看起来foo.bar()中的异常确实被吞噬了,不过,它不是。然而,还有一些更深层次的问题,我们没有注意到。 p.then(…) 调用本身返回另一个 Promise,该 Promise 将被 TypeError 异常拒绝。处理未捕获异常许多人会说,还有其他更好的方法。一个常见的建议是,Promise 应该添加一个 done(…),这实际上是将 Promise 链标记为 “done”。done(…) 不会创建并返回 Promise ,因此传递给 done(..) 的回调显然不会将问题报告给不存在的链接 Promise 。Promise 对象的回调链,不管以 then 方法或 catch 方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为 Promise 内部的错误不会冒泡到全局)。因此,我们可以提供一个 done 方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。ES8中改进了什么 ?Async/await (异步/等待)JavaScript ES8引入了 async/await,这使得使用 Promise 的工作更容易。这里将简要介绍async/await 提供的可能性以及如何利用它们编写异步代码。使用 async 声明异步函数。这个函数返回一个 AsyncFunction 对象。AsyncFunction 对象表示该函数中包含的代码的异步函数。调用使用 async 声明函数时,它返回一个 Promise。当这个函数返回一个值时,这个值只是一个普通值而已,这个函数内部将自动创建一个承诺,并使用函数返回的值进行解析。当这个函数抛出异常时,Promise 将被抛出的值拒绝。使用 async 声明函数时可以包含一个 await 符号,await 暂停这个函数的执行并等待传递的 Promise 的解析完成,然后恢复这个函数的执行并返回解析后的值。async/wait 的目的是简化使用承诺的行为让看看下面的例子:function getNumber1() { return Promise.resolve(‘374’);}// 这个函数与getNumber1相同async function getNumber2() { return 374;}类似地,抛出异常的函数等价于返回被拒绝的 Promise 的函数: function f1() { return Promise.reject(‘Some error’);}async function f2() { throw ‘Some error’;}await 关键字只能在异步函数中使用,并允许同步等待 Promise。如果在 async 函数之外使用 Promise,仍然需要使用 then 回调:还可以使用“异步函数表达式”定义异步函数。异步函数表达式与异步函数语句非常相似,语法也几乎相同。异步函数表达式和异步函数语句之间的主要区别是函数名,可以在异步函数表达式中省略函数名来创建匿名函数。异步函数表达式可以用作生命(立即调用的函数表达式),一旦定义它就会运行。var loadData = async function() { // rp is a request-promise function. var promise1 = rp(‘https://api.example.com/endpoint1'); var promise2 = rp(‘https://api.example.com/endpoint2'); // Currently, both requests are fired, concurrently and // now we’ll have to wait for them to finish var response1 = await promise1; var response2 = await promise2; return response1 + ’ ’ + response2;}更重要的是,在所有主流的浏览器都支持 async/await:最后,重要的是不要盲目选择编写异步代码的“最新”方法。理解异步 JavaScript 的内部结构非常重要,了解为什么异步JavaScript如此关键,并深入理解所选择的方法的内部结构。与编程中的其他方法一样,每种方法都有优点和缺点。编写高度可维护性、非易碎异步代码的5个技巧1、简介代码: 使用 async/await 可以编写更少的代码。 每次使用 async/await时,都会跳过一些不必要的步骤:使用.then,创建一个匿名函数来处理响应,例如:// rp是一个请求 Promise 函数。rp(‘https://api.example.com/endpoint1’).then(function(data) { // …});和:// rp is a request-promise function.var response = await rp(‘https://api.example.com/endpoint1’);2、错误处理: Async/wait 可以使用相同的代码结构(众所周知的try/catch语句)处理同步和异步错误。看看它是如何与 Promise 结合的:function loadData() { try { // Catches synchronous errors. getJSON().then(function(response) { var parsed = JSON.parse(response); console.log(parsed); }).catch(function(e) { // Catches asynchronous errors console.log(e); }); } catch(e) { console.log(e); }}与async function loadData() { try { var data = JSON.parse(await getJSON()); console.log(data); } catch(e) { console.log(e); }}3、条件:用async/ wait编写条件代码要简单得多:function loadData() { return getJSON() .then(function(response) { if (response.needsAnotherRequest) { return makeAnotherRequest(response) .then(function(anotherResponse) { console.log(anotherResponse) return anotherResponse }) } else { console.log(response) return response } })}与async function loadData() { var response = await getJSON(); if (response.needsAnotherRequest) { var anotherResponse = await makeAnotherRequest(response); console.log(anotherResponse) return anotherResponse } else { console.log(response); return response; }}4、堆栈帧:与 async/await不同,从 Promise 链返回的错误堆栈不提供错误发生在哪里。看看下面这些:function loadData() { return callAPromise() .then(callback1) .then(callback2) .then(callback3) .then(() => { throw new Error(“boom”); })}loadData() .catch(function(e) { console.log(err);// Error: boom at callAPromise.then.then.then.then (index.js:8:13)});与:async function loadData() { await callAPromise1() await callAPromise2() await callAPromise3() await callAPromise4() await callAPromise5() throw new Error(“boom”);}loadData() .catch(function(e) { console.log(err); // output // Error: boom at loadData (index.js:7:9)});5.调试:如果你使用过 Promise ,那么你知道调试它们是一场噩梦。例如,如果在一个程序中设置了一个断点,然后阻塞并使用调试快捷方式(如“停止”),调试器将不会移动到下面,因为它只“逐步”执行同步代码。使用async/wait,您可以逐步完成wait调用,就像它们是正常的同步函数一样。编辑中可能存在的bug没法实时知道,事后为了解决这些bug,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具Fundebug。原文:https://blog.sessionstack.com…你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》! ...

December 18, 2018 · 3 min · jiezi

ES6精解:变量的解构赋值

1.数组的解构赋值我们知道以前我们给一个变量赋值要这样如下:let a = 1;let b = 2;let c = 3;但是ES6出来之后,我们可以这样:let [a, b, c] = [1, 2, 3]以上就是从数组中提起值,一一对应赋值,a、b、c分别为1、2、3 let [aa, [[bb], cc]] = [1, [[2], 3]]aa,bb,cc分别对应1,2,3 let [,,d] = [10, 20, 30]; console.log(d);d的值为30 let [e, …f] = [1, 2, 3, 4, 5];e为1, f为[2,3,4,5]let [x, y, …z] = [1];x为1,y为undefined,z为[],如果没有对应值会等于undefined解构赋值也允许默认值, 如下:let [ x = ‘默认值’] = [];x的值为默认值 let [ a = 123 ] = [ undefined ]; let [ b = 321 ] = [ null ];a的值为123,b的值为321,因为ES6中是严格遵循(===),所以只有当数组对应的值为undefined的时候,才能等于默认值默认值可以引用解构赋值的其他变量,但是这个变量必须得声明过的,如下: let [ a = 0, b = a] = [];这里a、b分别为0,0但是如果这个变量没有声明,就会报错,如下: let [ a = b, b = 1] = [];这里就会报错:Uncaught ReferenceError: b is not defined2.对象的解构赋值 let { a, b } = { a: 1, b: 2 };a,b的值为1,2对象解构赋值可以无需按照顺序一一对应,但是括号两边的变量名称和属性名称要相同,才能取到正确值let { a, b } = { a: 1, c : 2};a为1,b为undefined,这里b在右边括号中没有对应的属性值与其对应,所以此时b为undefined如果变量名和属性名不一样,则要按照如下写: let { aa: fa, bb: fb } = { aa: 1, bb: 2 };fa为1,fb为2这里可以说明,对象解构赋值实际上是如下的简写:let { aa: aa, bb: bb } = { aa: 1, bb: 2}也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。 let { aa: fa } = { aa: 1 } //fa为 1 //aa为 Uncaught ReferenceError: aa is not defined这里就说明了,fa是真正被赋值的,此时的aa是模式而不是变量,如果你要让aa为变量,则如下: { let { aa: fa, aa } = { aa: 1 }; // aa 为 1 // fa 为 1 }此时aa为1,bb为1,此时aa就是变量了当然对象也可以多层嵌套如下: { let obj = { p: [1, { y: 2 }] } let { p: [x, { y }]} = obj; console.log(x); // 1 console.log(y); // 2 } { let obj = {}; let arr = []; ({a: obj.name, b: arr[0] } = { a: 321, b: 123}); console.log(obj); // { name: 321 } console.log(arr); // [123] }对象解构默认值: { let { x = 1 } = {}; console.log(x); //1 } { let { x : y = 4 } = {}; console.log(y); //4 } { let { x : y = 4 } = { x: 5}; console.log(y); //5 }默认属性生效条件必须是对象的属性严格等于undefined,如果是null就不会生效,如下: { let { x = 1 } = { x : null}; console.log(x); //null }已声明的变量解构赋值: { let x; ({ x } = { x : 1 }); console.log(1); // 1 }得要加个小括号才可以,否则会报错;数组对应对象属性的解构: { let arr = [1, 2, 3]; let { 0 : one, [arr.length-1] : three } = arr; console.log(one); // 1 console.log(three); // 3 }3.字符串的解构赋值字符串也可以解构赋值,因为字符串可以转换成一个类似数组的对象 { let [a, b, c, d] = ‘hero’; console.log(a); //h console.log(b); //e console.log(c); //r console.log(d); //o }类似数组的对象都有一个length属性,可以对这个属性解构赋值 { let { length: len } = ‘hero’; console.log(len); // 4 }4.数值和布尔值的解构赋值解构赋值时,如果等号右边是数值或者布尔值时,则会先转换成对象 { let { toString: s } = 123; console.log(s === Number.prototype.toString); //true } { let { toString: s } = true; console.log(s === Boolean.prototype.toString); //true }解构赋值的规则:如果等号右边的值不是对象或者数组则会先转为对象,由于undefined和null无法转为对象,所以无法对它们进行解构赋值,会报错,如下:{ let { props: x} = undefined; //报错 let { props: y} = null; //报错}5.函数参数的解构赋值{ const arr = [[1,2],[3,4]].map(([a, b])=> a+b); console.log(arr); //[3,7]}函数参数默认值:function func({x = 0, y = 0} = {}) { return [x, y];}console.log(func({x:5,y:6})); //[5,6]console.log(func({x:5})); //[5,0]console.log(func({})); //[0, 0]console.log(func()); //[0, 0]如果按照以下定义就不同了,如下:function fn({x, y} = { x: 1, y: 2}) { return [x, y];}console.log(fn({ x: 11, y: 22})); // [11, 22]console.log(fn({ x: 11 })); // [11, undefined]console.log(fn({})); // [undefined, undefined]console.log(fn()); // [1, 2]undefined会触发函数参数的默认值,如下:{ const a = [1, undefined, 3].map((x = ’noundefine’) => x); console.log(a); //[1, “noundefine”, 3]}6.圆括号问题圆括号只能一种情况才能使用:赋值语句的非模式部分,可以使用圆括号[(b)] = [3]; // 正确({ p: (d) } = {}); // 正确[(parseInt.prop)] = [3]; // 正确上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是p,而不是d;第三行语句与第一行语句的性质一致。本文参考链接:http://es6.ruanyifeng.com/#do…主要是看了阮一峰老师的文章,自己整理了一下,供后期自己看笔记用的 ...

December 18, 2018 · 3 min · jiezi

深入解析ES6之数据结构的用法

本文介绍了深入理解ES6之数据解构的用法,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下。如有不足之处,欢迎批评指正。一 对象解构对象解构语法在赋值语句的左侧使用了对象字面量let node = { type: true, name: false} //既声明又赋值let { type, name} = node; //或者先声明再赋值let type, name({type,name} = node);console.log(type);//trueconsole.log(name);//falsetype与name标识符既声明了本地变量,也读取了对象的相应属性值。解构赋值表达式的值为表达式右侧的值。当解构表达式的右侧的计算结果为null或者undefined时,会抛出错误。默认值当你使用解构赋值语句时,如果所指定的本地变量在对象中没有找到同名属性,那么该变量会被赋值为undefinedlet node = { type: true, name: false}, type, name, value;({type,value,name} = node); console.log(type);//trueconsole.log(name);//falseconsole.log(value);//undefined//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860你可以选择性地定义一个默认值,以便在指定属性不存在时使用该值。let node = { type: true, name: false }, type, name, value;({ type, value = true, name} = node); console.log(type);//trueconsole.log(name);//falseconsole.log(value);//true//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860赋值给不同的本地变量名let node = { type: true, name: false, value: “dd”}let { type: localType, name: localName, value: localValue = “cc”} = node;console.log(localType);console.log(localName);console.log(localValue);type:localType这种语法表示要读取名为type的属性,并把它的值存储在变量localType上。该语法与传统对象字面量的语法相反嵌套的对象结构let node = {type: “Identifier”,name: “foo”,loc: { start: { line: 1, column: 1 }, end: { line: 1, column: 4 }//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860}} let {loc: localL,loc: { start: localS, end: localE}} = node; console.log(localL);// start: {line: 1,column: 1},end: {line: 1,column: 4}console.log(localS);//{line: 1,column: 1}console.log(localE);//{line: 1,column: 4}//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860当冒号右侧存在花括号时,表示目标被嵌套在对象的更深一层中(loc: {start: localS,end: localE})二 数据解构数组解构的语法看起来跟对象解构非常相似,只是将对象字面量换成了数组字面量。let colors = [“red”, “blue”, “green”];let [firstC, secondC, thirdC, thursC = “yellow”] = colors;console.log(firstC//redconsole.log(secondC);//blueconsole.log(thirdC);//greenconsole.log(thursC);//yellow//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860你也可以在解构模式中忽略一些项,并只给感兴趣的项提供变量名。let colors = [“red”,“green”,“blue”]; let [,,thirdC] = colors;console.log(thirdC);//blue//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860thirdC之前的逗号是为数组前面的项提供的占位符。使用这种方法,你就可以轻易从数组任意位置取出值,而无需给其他项提供名称。解构赋值let colors = [“red”,“green”,“blue”], firstColor = “black”, secondColor = “purple”;[firstColor,secondColor] = colors;console.log(firstColor);//redconsole.log(secondColor);//green//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860数组解构有一个非常独特的用例,能轻易的互换两个变量的值let a =1,b =2;[a,b] = [b,a];console.log(a);//2console.log(b);//1//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860嵌套的解构let colors = [“red”, [“green”, “blue”], “yellow”];let [firstC, [, ssc]] = colors;console.log(ssc);//blue剩余项let colors = [“red”, “green”, “blue”];let [firstC, …restC] = colors;console.log(firstC);console.log(…restC);console.log(restC[0]);//greenconsole.log(restC[1]);//blue使用剩余项可以进行数组克隆let colors = [“red”, “green”, “blue”];let […restC] = colors;console.log(restC);//[“red”, “green”,“blue”]三 混合解构let node = {type: “Identifier”,name: ‘foo’,loc: { start: { line: 1, column: 1 }, end: { line: 1, column: 4 }//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860},range: [0, 3]} let {type,name: localName,loc: { start: { line: ll }, end: { column: col }},range: [, second]} = node; console.log(type);//Identifierconsole.log(localName);//fooconsole.log(ll);//1console.log(col);//4console.log(second);//3结语感谢您的观看,如有不足之处,欢迎批评指正。 ...

December 17, 2018 · 2 min · jiezi

[ ES6 ] 快速掌握常用 ES6 (二)

[ ES6 ] 快速掌握常用 ES6 (二)本篇文章是对之前文章的一个补充,可以使 JavaScript 代码更简洁函数参数默认值在 JavaScript 传统语法中如果想设置函数默认值一般我们采用判断的形式function example (a,b,c) { a = a|‘string’; b = b|‘number’; c = c|‘json’; console.log(a); console.log(b); console.log(c); // ‘string’ ’number’ ‘json’}在新的语法中我们可以在参数声明的同时赋予默认值function example (a = ‘string’,b = ’number’,c = ‘json’) { console.log(a); console.log(b); console.log(c); // ‘string’ ’number’ ‘json’}参数展开在 JavaScript 传统语法中如果不确定参数的数量,并且想获取所有的参数,一般使用 arguments (函数自带的变量,数组类型,存放所有的参数)function example (){ console.log(arguments);}在新的语法中我们可以在参数声明的同时赋予默认值function example (…oVar){ console.log(oVar);}还可以结合结构赋值,实现不用按顺序传递参数function (…opaction){ let {url,type,succ,err} = opaction; if(!url){ return false; }else{ console.log(url); console.log(type); console.log(succ); console.log(err); }}数组map() 方法创建一个新数组,然后每次从开始给回调函数传递一个原来数组的成员,直到结束let oArray = [5, 7, 1, 56];const oMap = array1.map(x => x * 3);console.log(oArray);// Array [15, 21, 3, 168]//映射: 一个对一个reduce() 方法接收一个函数作为累加器(升序执行),最终计算为一个值var numbers = [1, 2, 3, 4]; function getSum(total, num) { return total + num;}console.log(numbers.reduce(getSum)); // 10//汇总:一堆 返回 一个 filter() 方法创建一个新的数组,新数组中的元素是回调函数中符合条件的所有元素。var ages = [95, 59, 18, 21];function checkAdult(age) { return age >= 60;}console.log(ages.filter(checkAdult));// 95 //过滤:一堆 返回 部分 forEach() 方法调用数组的每个元素,并将元素传递给回调函数//遍历: 以上的都可以通过 forEach() 来手动实现,并且可以实现更加个性的自定义操作var array1 = [‘a’, ‘b’, ‘c’];array1.forEach(element => { console.log(element);});// a b cArray.from() 方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象//常见的类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的 arguments 对象// NodeList对象let ps = document.getElementsByClassName(‘p’);//所有取到的 dom 元素都是 NodeList 格式,不是真正意义上的数组Array.from(ps).filter(p => { return p.textContent.length > 9;//先用 from() 将所有取到的 p 标签转换为真正的数组 然后过滤掉前十个});// arguments对象function foo() { var args = Array.from(arguments); // arguments 也不是真正意义上的数组}json关于什么是 json 这里不过多介绍,主要看看 ES6 里 json 是怎样的key and value当键名和键值是一样的情况下可以只写一个,在引入组件与库中特定方法时,可以看到(关于如何引入其他文件,将在之后的文章写)//传统{ name: name,}//ES6{ name}function如果在之前了解过微信小程序,vue ,或者将要学习那么应该会经常看到这两种函数的写法{ onLoad() { butClick() { return false; } } methods: { butClick() { return false; } }}但是如果不用框架,写这样的代码,会报错 这是因为框架其实可以看做一个函数,上面这种代码是传递给函数的参数(这个参数的接收方法在本篇文章的开头) 这个参数是以 json 对象的形式传递的,而 ES6 中当 value 旳值是一个函数时可以省略冒号和 function 关键字//传统 json{ butClick: function (){ return false; }}//ES6 json{ butClick() { return false; }}本系列文章仅适合快速入门,想深入学习的小伙伴可以看看阮一峰老师的《ECMAScript 6 入门》 [ ES6 ] 快速掌握常用 ES6 (一) [ ES6 ] 快速掌握常用 ES6 (二) ...

December 17, 2018 · 2 min · jiezi

JavaScript 数组方法集合及示例!

数组基础知识你应该知道数组是什么,但以下是一个简单的概述:数组就像放东西的盒子,你可以放进东西(新增),拿出东西(删除)或者摆放它们的位置及拿出我们想要的东西。数组的创建以下是创建数组的几种方式:数组的新增和删除常用的方法对常见场景的概述以及用于每个场景的方法。你会注意到,大多数采用回调函数的方法都有相同的参数:( element, index, array )下面是代码块,注释中描述的场景/任务,注释下面对应的是代码: 修改数组中的每个元素无变异复制查找数组元素的索引值判断元素是否在数组中数组去重判断数组中的每一项是否都满足这些要求判断数组中的某些项是否都满足这些要求填充空数组 Wtf 排序 字符串反转数组扁平将数组转化为对象循环多维数组操作获取对角线(多维数组)原文:https://codeburst.io/javascri…你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》!

December 14, 2018 · 1 min · jiezi

通过节食来解释 JavaScript 的Reduce方法!

JavaScript中的reduce方法为您提供了一种简单的方法来获取值数组并将它们组合成一个值,或者根据多个类别对数组求和。哇,一句话说得太多了,让我们一步一步来吧!当然,你可以使用 for 循环遍历数组并对每个值执行特定操作。但是,如果你不使用 filter()、map() 和reduce() 等方法,那么你的代码将变得更加难以阅读。其他开发人员需要彻底阅读每个循环才能理解其目的,而且容易出现更多的 bug,因为你需要创建更多的变量来跟踪单个值。Filter 方法接受初始数组中的所有元素,并且只允许某些元素进入最终数组Map 方法在初始数组中的每个元素上运行一个函数,然后将其存储在最终数组中。而 reduce 方法将初始数组中的元素组合成最终值或值数组。我意识到这有点像节食。从非常简单的方法,如计算卡路里,到更复杂的饮食,如 Atkins 减肥法或称体重计(WeightWatchers),目标是将你一天中可能吃的所有食物提炼成一个(或多个)值,看看你是否在减肥的轨道上。用 For 循环模拟Reduce下面是使用for循环快速显示reduce()功能的方法。假设你有一个包含你一天中吃过的5种不同食物的卡路里计数的数组,你想知道你总共消耗了多少卡路里,代码如下:这很简单,你创建一个变量来保存最终数量,然后在运行数组时添加它。但是,你仍然需要引入一个新变量,并且循环没有提供关于循环目的的线索。一个简单的Reduce示例让我们学习如何使用reduce()方法实现相同的目标。reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。对空数组是不会执行回调函数的。所以 reduce 有一些内存的概念。在遍历数组中的每一项时,sum参数将跟踪值。在前面的例子中,我们必须在循环范围之外声明一个新变量来“记住”这些值。这与for()之间的可读性似乎没有太大区别。但是,当需要遍历数百行代码时,reduce 会让你快速了解代码块的用途。例2 - 使用对象到目前为止,我们只举例一维数组。但是,如果您可以遍历一个都是数字的数组,那么你也可以遍历一个都是对象的数组。让我们给每个元素加一个名字,这样我们就能知道我们一天到底吃了什么。你早餐吃了一份牛排,一些水果,然后午餐吃了一些沙拉和薯条,最后晚餐吃了冰淇淋,真是糟糕的一天!希望你们能看到整个数组的流动。当我们研究每一种元素时,总热量的总和( sum)就代表了一天所消耗的总热量,主要是把这些数值放进一个大桶里——一天的卡路里量。例3 - 使用多个类别所以如果这都是关于卡路里的,为什么会有这么多不同的饮食呢? 我不打算深究营养科学,但这里有一个概括性的总结——对于 “最佳” 减肥方法有很多分歧。有些人鼓励你只计算卡路里,而另一些人则关注蛋白质、碳水化合物、脂肪和其他任何因素。让我们设想一下,你希望更改代码,以便能够基于任何常见的节食系统评估您的饮食。你需要追踪每种食物的碳水化合物和脂肪,然后你需要在最后统计一下,这样你就能算出你在每个类别中消耗了多少克,以下我们的食品(有虚假的营养价值):现在,我们需要运行reduce()方法。但是,它不能在一个值中被跟踪,我们想保留我们的类别。因此,我们的累加器需要是一个与数组具有相同类型的对象。下面是这个过程的一个GIF图片:在遍历数组每个项时,你将更改对象中特定属性的值,如果该对象尚未具有正确名称的属性,则将创建该对象,如下:我们使用 bucket 作为对象,根据属性名对值进行分类。我们使用 += 操作符为来自 foods 数组的对象中的每个值添加到适当的bucket。请注意保存值的 key 的名字,这里是随意的,这是因为它是无关紧要的——我们只是想要数字,这样我们就可以分析你一天饮食的成功。如你所见,在我们的输出中有一个问题,结果包含一个 name 字段为“steak”,我们并不想存储该字段。因此,我们需要指定另一个参数——初始值。这个参数在回调之后出现,我们希望将 calories、carbs 和 fat 字段初始化为0,以便我们的 reduce 方法知道这是我们将用于 bucket 参数的唯一三个键/值对,代码如下:原文:https://codeburst.io/javascri…你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》!

December 14, 2018 · 1 min · jiezi

前端杂谈: 如何实现一个 Promise?

前端杂谈: 如何实现一个 Promise?首先, 什么是 Promise?A promise is an object that may produce a single value some time in the future: either a resolved value, or a reason that it’s not resolved (e.g., a network error occurred). A promise may be in one of 3 possible states: fulfilled, rejected, or pending. Promise users can attach callbacks to handle the fulfilled value or the reason for rejection.关键语句: Promise 是一个在将来某个时刻产生一个单一结果的对象.通俗一点来说, Promise 代表了一个值, 但是这个值我们并不确定什么时候会被返回.A promise is an object that may produce a single value some time in the future.简单看看 Promise 的历史Promise 在 1980 年代被创建出来 =>在 1988 年正式得名: Promise =>已经有很多人了解到了 Promise, 但是人们还是坚持使用 node.js 中提倡的以回调函数首个参数传 error 对象的方式处理异步代码. =>Dojo 首次大规模的使用了 Promise , 相应的 Promise/A 被提出用以规范 Promise 的实现 =>JQuery 开始使用 Promise 并真正使 Promise 广为人知 =>JQuery 没有实现部分 Promise 的功能, 这也导致了 Promie/A+ 标准的产生 =>ES6 正式引入了 Promise,并且和已有的实现了 Promise/A 规范的 library 相兼容 =>实现 Promise 之前, 让我们看看 Promise 有哪些规范Promise 是一个 thenable 对象, 也就是说 Promise 有一个 .then() 方法一个 pending 状态的 Promise 可以进入 fulfilled 和 rejected 状态promise 一旦进入 fulfilled 或 rejected 状态, 不可再改变其状态一旦 promise 改变了其状态, 它笔芯有一个值(这个值也可能是 undefined)开始实现一个 Promise首先, 让我们看看一段最普通的异步代码:// 异步方法定义var basicAsyncFunc = function(callback) { setTimeout(function() { var randomNumber = Math.random() if (randomNumber > 0.5) { callback(null, randomNumber) } else { callback(new Error(‘bad luck…’)) } }, 1000)}// 异步方法调用basicAsyncFunc((err, result) => { if (err) { console.log(the reason fail is: ${err}) return } console.log(success get result: ${result})})按照 Promise 的规范定义, 理想中 Promise 的调用方式为:// Promise 形式的异步方法定义var promiseAsyncFunc = function() {}// Promise 形式的异步方法调用promiseAsyncFunc.then( data => { console.log(success get result: ${data}) }, err => { console.log(the reason fail is: ${err}) })按照这个理想当中的调用方式, 让我们写出第一版代码.第一版 Promise:能保存回调方法// Promise 形式的异步方法定义var promiseAsyncFunc = function() { var fulfillCallback var rejectCallback setTimeout(() => { var randomNumber = Math.random() if (randomNumber > 0.5) fulfillCallback(randomNumber) else rejectCallback(randomNumber) }, 1000) return { then: function(_fulfillCallback, _rejectCallback) { fulfillCallback = _fulfillCallback rejectCallback = _rejectCallback } }}// Promise 形式的异步方法调用promiseAsyncFunc().then(fulfillCallback, rejectCallback)我们的思路是在 .then() 方法中, 将 fullfill 和 reject 结果的回调函数保存下来, 然后在异步方法中调用. 因为是异步调用, 根据 event-loop 的原理, promiseAsyncFunc().then(fulfillCallback, rejectCallback) 传入的 callback 在异步调用结束时一定是已经赋值过了.第二版 Promise:实构造函数当前我们的实现 Promise 中,异步逻辑代码和 Promise 的代码是杂糅在一起的,让我们将其区分开:var promiseAsyncFunc = function() { var fulfillCallback var rejectCallback return { fulfill: function(value) { if (fulfillCallback && typeof fulfillCallback === ‘function’) { fulfillCallback(value) } }, reject: function(err) { if (rejectCallback && typeof rejectCallback === ‘function’) { rejectCallback(err) } }, then: function(_fulfillCallback, _rejectCallback) { fulfillCallback = _fulfillCallback rejectCallback = _rejectCallback } }}let ownPromise = function(asyncCall) { let promise = promiseAsyncFunc() asyncCall(promise.fulfill, promise.reject) return promise}// Promise 形式的异步方法调用ownPromise(function(fulfill, reject) { setTimeout(() => { var randomNumber = Math.random() if (randomNumber > 0.5) fulfill(randomNumber) else reject(randomNumber) }, 1000)})我们新定义了一个方法 ownPromise() 用于创建 Promise,并在promiseAsyncFunc() 中暴露出 fulfill 和 reject 接口方便异步代码去调用。这里有一个问题,我们在调用 ownPromise()后得到了 promise 实例,此时我们可以直接调用 fulfill(),reject()这两个方法,而理论上我们应该只应暴露 promise 的then()方法。所以我们利用闭包将这两个方法隐藏:var promiseAsyncFunc = function() { var fulfillCallback var rejectCallback return { fulfill: function(value) { if (fulfillCallback && typeof fulfillCallback === ‘function’) { fulfillCallback(value) } }, reject: function(err) { if (rejectCallback && typeof rejectCallback === ‘function’) { rejectCallback(err) } }, promise: { then: function(_fulfillCallback, _rejectCallback) { fulfillCallback = _fulfillCallback rejectCallback = _rejectCallback } } }}let ownPromise = function(asyncCall) { let defer = promiseAsyncFunc() asyncCall(defer.fulfill, defer.reject) return defer.promise}// Promise 形式的异步方法调用ownPromise(function(fulfill, reject) { setTimeout(() => { var randomNumber = Math.random() if (randomNumber > 0.5) fulfill(randomNumber) else reject(randomNumber) }, 1000)})第三版 Promise: 支持状态管理为了实现规范中对于 Promise 状态变化的要求, 我们需要为 Promise 加入状态管理, 这一步较为简单, 让我们看代码:const PENDING = Symbol(‘pending’)const FULFILLED = Symbol(‘fulfilled’)const REJECTED = Symbol(‘rejected’)// Promise 形式的异步方法定义var promiseAsyncFunc = function() { var status = PENDING var fulfillCallback var rejectCallback return { fulfill: function(value) { if (status !== PENDING) return if (typeof fulfillCallback === ‘function’) { fulfillCallback(value) status = FULFILLED } }, reject(error) { if (status !== PENDING) return if (typeof rejectCallback === ‘function’) { rejectCallback(error) status = REJECTED } }, promise: { then: function(_fulfillCallback, _rejectCallback) { fulfillCallback = _fulfillCallback rejectCallback = _rejectCallback } } }}let ownPromise = function(asyncCall) { let defer = promiseAsyncFunc() asyncCall(defer.fulfill, defer.reject) return defer.promise}// Promise 形式的异步方法调用ownPromise(function(fulfill, reject) { setTimeout(() => { var randomNumber = Math.random() if (randomNumber > 0.5) fulfill(randomNumber) else reject(randomNumber) }, 1000)}).then(data => console.log(data), err => console.log(err))这段代码中我们用到了 Symbol 来表示状态常量, 对 Symbol 不了解的同学可以看这里为了判断 Promise 的状态, 我们加入了 fulfill 和 reject 两个方法。并在其中判断 promise 当前状态。如果不是 pending 状态则直接 return(因为 Promise 状态只可能改变一次)。现在我们的 promise 实现了对状态控制的规范:只允许改变一次状态只能从 pending => fulfilled 或 pending => rejected但是我们的 Promise 有一个问题: promise 的值没有被保存下来。如果 promise 在异步调用完成之后才被调用 .then() 方法,则我们无法把异步调用的结果传递给回调函数。为此我们需要为 Promise 加一个 value 字段:第四版 Promise: 保存异步调用的结果我们为 promise 加入 value 字段,用于保存 Promise 的执行结果。// Promise 形式的异步方法定义var promiseAsyncFunc = function() { var status = PENDING var fulfillCallback var rejectCallback var value return { fulfill: function(_value) { if (status !== PENDING) return value = _value status = FULFILLED if (typeof fulfillCallback === ‘function’) { fulfillCallback(value) } }, reject(error) { if (status !== PENDING) return value = error status = REJECTED if (typeof rejectCallback === ‘function’) { rejectCallback(error) } }, promise: { then: function(_fulfillCallback, _rejectCallback) { fulfillCallback = _fulfillCallback rejectCallback = _rejectCallback } } }}这里我们又发现一个问题,如果一个 Promise 已经是fulfill或reject状态。我们再调用 then() 方法时,传入的回调方法永远不会被调用(因为 status 已经不是 pending)。所以我们需要在 then()方法中对其状态进行判断:// Promise 形式的异步方法定义var promiseAsyncFunc = function() { var status = PENDING var fulfillCallback var rejectCallback var value return { fulfill: function(_value) { if (status !== PENDING) return value = _value status = FULFILLED if (typeof fulfillCallback === ‘function’) { fulfillCallback(value) } }, reject(error) { if (status !== PENDING) return value = error status = REJECTED if (typeof rejectCallback === ‘function’) { rejectCallback(error) } }, promise: { then: function(_fulfillCallback, _rejectCallback) { if (status === REJECTED) { _rejectCallback(value) return } if (status === FULFILLED) { _fulfillCallback(value) return } fulfillCallback = _fulfillCallback rejectCallback = _rejectCallback } } }}第五版 Promise: 支持链式调用为了支持链式调用,.then() 方法的返回值必须是用 thenable (根据 Promise/A+ 规范, .then() 方法的返回值需要是一个新的 Promise)为此我们加入一个工具方法 makeThenable()。如果传入的 value 本身就有 then()方法,则直接返回 value。否则返回一个有 then()方法的对象。在该对象的 then()方法中,我们根据 promise 的状态,调用不同的回调方法生成新的 value。function makeThenable(value, status){ if(value && typeof value.then === ‘function’){ return value } if(status === FULFILLED){ return { then: function(fulfillCallback, rejectCallback){ return makeThenable(fulfillCallback(value), FULFILLED) } } } if(status === REJECTED) { return { then: function(fulfillCallback, rejectCallback){ return makeThenable(rejectCallback(value), FULFILLED) } } }}有了以上的 makeThenable()方法,我们可以在 promise 的fulfill(),reject()回将 value 设置为 thenable:var promiseAsyncFunc = function() { var status = PENDING var fulfillCallback var rejectCallback var value return { fulfill: function(_value) { if (status !== PENDING) return value = makeThenable(_value, FULFILLED) // 保证当前promise的value为 thenable status = FULFILLED if (typeof fulfillCallback === ‘function’) { value.then(fulfillCallback) } }, reject(error) { if (status !== PENDING) return value = makeThenable(error, REJECTED) 、、 // 保证当前value为 thenable status = REJECTED if (typeof rejectCallback === ‘function’) { value.then(null, rejectCallback) } }, promise: { then: function(){} } }}接下来让我们看 then()方法。为了返回一个新的 promise,我们首先得创建一个新的 promise。其次当前 promise 在fulfill() 或 reject()时,应该调用新的 promise 的fullfill() 或 reject()方法。所以我们在将 fulfullCallback和rejectCallback赋值给当前 promise 时,将其包装一下。代码如下: promise: { then: function(_fulfillCallback, _rejectCallback) { let newPromiseAsyncFunc = promiseAsyncFunc() let fulfillFunc = function(value) { newPromiseAsyncFunc.fulfill(_fulfillCallback(value)) } let rejectFunc = function(err) { newPromiseAsyncFunc.fulfill(_rejectCallback(err)) } if (status === PENDING) { fulfillCallback = fulfillFunc rejectCallback = rejectFunc } else { value.then(fulfillFunc, rejectFunc) } return newPromiseAsyncFunc.promise } }如此,我们变得到了一个可以链式调用的 promise。让我们来测试一下:const PENDING = Symbol(‘pending’)const FULFILLED = Symbol(‘fulfilled’)const REJECTED = Symbol(‘rejected’)function makeThenable(value, status) { if (value && typeof value.then === ‘function’) { return value } if (status === FULFILLED) { return { then: function(fulfillCallback, rejectCallback) { return makeThenable(fulfillCallback(value), FULFILLED) } } } if (status === REJECTED) { return { then: function(fulfillCallback, rejectCallback) { return makeThenable(rejectCallback(value), FULFILLED) } } }}// Promise 形式的异步方法定义var promiseAsyncFunc = function() { var status = PENDING var fulfillCallback var rejectCallback var value return { fulfill: function(_value) { if (status !== PENDING) return value = makeThenable(_value, FULFILLED) status = FULFILLED if (typeof fulfillCallback === ‘function’) { value.then(fulfillCallback) } }, reject(error) { if (status !== PENDING) return value = makeThenable(error, REJECTED) status = REJECTED if (typeof rejectCallback === ‘function’) { value.then(null, rejectCallback) } }, promise: { then: function(_fulfillCallback, _rejectCallback) { let newPromiseAsyncFunc = promiseAsyncFunc() let fulfillFunc = function(value) { newPromiseAsyncFunc.fulfill(_fulfillCallback(value)) } let rejectFunc = function(err) { newPromiseAsyncFunc.fulfill(_rejectCallback(err)) } if (status === PENDING) { fulfillCallback = fulfillFunc rejectCallback = rejectFunc } else { value.then(fulfillFunc, rejectFunc) } return newPromiseAsyncFunc.promise } } }}let ownPromise = function(asyncCall) { let defer = promiseAsyncFunc() asyncCall(defer.fulfill, defer.reject) return defer.promise}let testChainedPromise = ownPromise(function(fulfill, reject) { setTimeout(() => { var randomNumber = Math.random() if (randomNumber > 0.5) fulfill(randomNumber) else reject(randomNumber) }, 1000)}) .then( data => { console.log(data) return ‘return value in then1 fulfill’ }, err => { console.log(err) return ‘return value in then1 reject’ } ) .then( data => { console.log(data) return ‘return value in then2 fulfill’ }, err => { console.log(err) return ‘return value in then2 reject’ } ) .then( data => { console.log(data) }, err => { console.log(err) } )/*console output:0.9931984611850693return value in then1 fulfillreturn value in then2 fulfill/第六版 Promise: Error handling这里我们只对异步调用和fulfill 回调中抛出的 error 进行处理。首先是异步调用部分,我们将其 try catch 起来,在发生异常时调用 reject 方法,并将异常作为参数传入。let ownPromise = function(asyncCall) { let defer = promiseAsyncFunc() try { asyncCall(defer.fulfill, defer.reject) } catch (e) { defer.reject(e) } return defer.promise}然后是 fulfill 中可能出现的异常。我们对fulfillCallback(value)可能出现的异常进行捕获,并将异常传递给rejectCallback。function makeThenable(value, status) { if (value && typeof value.then === ‘function’) { return value } if (status === FULFILLED) { return { then: function(fulfillCallback, rejectCallback) { try { let newValue = fulfillCallback(value) return makeThenable(newValue, FULFILLED) } catch (e) { return makeThenable(rejectCallback(e), FULFILLED) } } } } if (status === REJECTED) { return { then: function(fulfillCallback, rejectCallback) { return makeThenable(rejectCallback(value), FULFILLED) } } }}最后让我们对完整的代码进行测试:const PENDING = Symbol(‘pending’)const FULFILLED = Symbol(‘fulfilled’)const REJECTED = Symbol(‘rejected’)function makeThenable(value, status) { if (value && typeof value.then === ‘function’) { return value } if (status === FULFILLED) { return { then: function(fulfillCallback, rejectCallback) { try { let newValue = fulfillCallback(value) return makeThenable(newValue, FULFILLED) } catch (e) { return makeThenable(rejectCallback(e), FULFILLED) } } } } if (status === REJECTED) { return { then: function(fulfillCallback, rejectCallback) { return makeThenable(rejectCallback(value), FULFILLED) } } }}// Promise 形式的异步方法定义var promiseAsyncFunc = function() { var status = PENDING var fulfillCallback var rejectCallback var value return { fulfill: function(_value) { if (status !== PENDING) return value = makeThenable(_value, FULFILLED) status = FULFILLED if (typeof fulfillCallback === ‘function’) { value.then(fulfillCallback) } }, reject(error) { if (status !== PENDING) return value = makeThenable(error, REJECTED) if (typeof rejectCallback === ‘function’) { value.then(null, rejectCallback) } status = REJECTED }, promise: { then: function(_fulfillCallback, _rejectCallback) { let newPromiseAsyncFunc = promiseAsyncFunc() let fulfillFunc = function(value) { newPromiseAsyncFunc.fulfill(_fulfillCallback(value)) } let rejectFunc = function(err) { newPromiseAsyncFunc.fulfill(_rejectCallback(err)) } if (status === PENDING) { fulfillCallback = fulfillFunc rejectCallback = rejectFunc } else { value.then(fulfillFunc, rejectFunc) } return newPromiseAsyncFunc.promise } } }}let ownPromise = function(asyncCall) { let defer = promiseAsyncFunc() try { asyncCall(defer.fulfill, defer.reject) } catch (e) { defer.reject(e) } return defer.promise}let testChainedPromise = ownPromise(function(fulfill, reject) { throw Error(‘here is an error in asyncCall’) setTimeout(() => { var randomNumber = Math.random() if (randomNumber > 0.5) fulfill(randomNumber) else reject(randomNumber) }, 1000)}) .then( data => { console.log(data) return ‘return value in then1 fulfill’ }, err => { console.log(err.message) return ‘return value in then1 reject’ } ) .then( data => { console.log(data) throw Error(‘here is an error in fulfill1’) return ‘return value in then2 fulfill’ }, err => { console.log(err.message) return ‘return value in then2 reject’ } ) .then( data => { console.log(data) }, err => { console.log(err.message) } )// console out:Error: here is an error in asyncCallreturn value in then1 rejectError: here is an error in fulfill1return value in then2 reject总结以上就是我们对于 Promise 的一个简单的实现,实现思路主要参考了 Q-A promise library for javascript。该实现的 Promise 功能较为简陋,仅实现了部分 api/规范。有任何意见和建议欢迎在评论区交流 ;)进一步阅读 && 引用对于 Promise 使用以及error handle 的讲解:https://medium.com/javascript…Promise 实现库之一: Q 对于 Promise 实现的讲解:https://github.com/kriskowal/…想了解更多 前端 和 数据可视化 ?这里是我的 前端、D3.js 、 数据可视化 的 github 地址, 欢迎 star & fork ssthouse-blog如果觉得本文不错的话, 不妨点击下面的链接关注一下 : )github 主页知乎专栏掘金想直接联系我 ?邮箱: ssthouse@163.com ...

December 13, 2018 · 9 min · jiezi

简单易用的leetcode开发测试工具(npm)

描述最近在用es6解leetcode,当问题比较复杂时,有可能修正了新的错误,却影响了前面的流程。要用能用的测试工具,却又有杀鸡用牛刀的感觉,所以就写了个简单易用的leetcode开发测试工具,分享与大家。工具安装npm i leetcode_test使用示例codes:let test = require(’leetcode_test’).testvar twoSum = function(nums, target) { if (nums.length < 2) return null for (let i = 0; i < nums.length; i++) { let ele1 = nums[i] let ele2index = nums.indexOf(target - ele1) if(ele2index > -1 && ele2index !== i) return [i, ele2index] } return null};let cases = [ [[[2, 7, 11, 15], 91], [0, 1]], [[[2, 7, 11, 15], 9], [0, 1]],]test(twoSum, cases)测试用例编写说明leetcode要测试的都是函数,参数个数不定,但返回值是一个。因此,我设计用例的输入形式为一个用例就是一个两个元素的数组,第一个元素是一个数组:对应输入参数;第二个元素是一个值。上面例子的输入参数是([2, 7, 11, 15], 91),第一个参数是数组,第二个参数是数值;返回值是一个数组([0, 1])。 如果要测试的函数的输入参数就是一个数组,要注意输入形式,比如,求[1,2,3,4]平均值,要这样输入测试用例: [[[1,2,3,4]],2.5]out:test [1] fail, Input: [2,7,11,15], 91; Expected: [0,1]; Output: nullindex.js:20test [2] success, Input: [2,7,11,15], 9; Expected: [0,1]; Output: [0,1]index.js:18Result: test 2 cases, success: 1, fail: 1index.js:28running 4 ms项目地址工具地址:https://github.com/zhoutk/lee…解答地址:https://github.com/zhoutk/lee…最近一直在用,已经把输出的样子调得还能看过眼了,答案对比算法,也改进了。遇到问题,我会持续改进,大家遇到问题也可提bug给我,我会尽快处理。 ...

December 12, 2018 · 1 min · jiezi

es6之解构赋值

es6的语法已经出了很长的时间了,在使用上也可以通过babel这类的编译工具转译为浏览器可以识别的es5的语法,但是依旧有很多开发在写代码的时候,依旧没有用es6的语法,而是习惯使用老的语法,这篇文章主要会介绍解构赋值基本用法以及在实际使用场景中相比es5语法的优势,让大家从根本上了解es6语法的优势基本用法数组解构让我们一起先来看数组解构的基本用法:let [a, b, c] = [1, 2, 3] // a=1, b=2, c=3let [d, [e], f] = [1, [2], 3] // 嵌套数组解构 d=1, e=2, f=3let [g, …h] = [1, 2, 3] // 数组拆分 g=1, h=[2, 3]let [i,,j] = [1, 2, 3] // 不连续解构 i=1, j=3let [k,l] = [1, 2, 3] // 不完全解构 k=1, l=2上面的例子包含了数组解构时常用的基本用法对象解构接下来再让我们一起看看对象解构的基本用法:let {a, b} = {a: ‘aaaa’, b: ‘bbbb’} // a=‘aaaa’ b=‘bbbb’let obj = {d: ‘aaaa’, e: {f: ‘bbbb’}}let {d, e:{f}} = obj // 嵌套解构 d=‘aaaa’ f=‘bbbb’let g;(g = {g: ‘aaaa’}) // 以声明变量解构 g=‘aaaa’let [h, i, j, k] = ’nice’ // 字符串解构 h=‘n’ i=‘i’ j=‘c’ k=‘e’使用场景变量赋值我们先来看最基本的使用场景:变量赋值,先来看我们在平时开发中是怎么使用es5对变量赋值的:var data = {userName: ‘aaaa’, password: 123456}var userName = data.userNamevar password = data.passwordconsole.log(userName)console.log(password)var data1 = [‘aaaa’, 123456]var userName1 = data1[0]var password1 = data1[1]console.log(userName1)console.log(password1)上面两个例子是最简单的例子,用传统es5变量赋值,然后调用,这么写的问题就是显得代码啰嗦,明明一行可以搞定的事情非要用三行代码,来看看解构赋值是怎么干的:const {userName, password} = {userName: ‘aaaa’, password: 123456}console.log(userName)console.log(password)const [userName1, password1] = [‘aaaa’, 123456]console.log(userName1)console.log(password1)相对于es5的语法是不是更加简单明了,在数据量越大用解构赋值的优势越明显函数参数的定义一般我们在定义函数的时候,如果函数有多个参数时,在es5语法中函数调用时参数必须一一对应,否则就会出现赋值错误的情况,来看一个例子:function personInfo(name, age, address, gender) { console.log(name, age, address, gender)}personInfo(‘william’, 18, ‘changsha’, ‘man’)上面这个例子在对用户信息的时候需要传递四个参数,且需要一一对应,这样就会极易出现参数顺序传错的情况,从而导致bug,接下来来看es6解构赋值是怎么解决这个问题的:function personInfo({name, age, address, gender}) { console.log(name, age, address, gender)}personInfo({gender: ‘man’, address: ‘changsha’, name: ‘william’, age: 18})这么写我们只需要知道要传什么参数就行来,不需要知道参数的顺序也没问题交换变量的值在es5中我们需要交换两个变量的值需要借助临时变量的帮助,来看一个例子:var a=1, b=2, cc = aa = bb = cconsole.log(a, b)来看es6怎么实现:let a=1, b=2;[b, a] = [a, b]console.log(a, b)是不是比es5的写法更加方便呢函数的默认参数在日常开发中,经常会有这种情况:函数的参数需要默认值,如果没有默认值在使用的时候就会报错,来看es5中是怎么做的:function saveInfo(name, age, address, gender) { name = name || ‘william’ age = age || 18 address = address || ‘changsha’ gender = gender || ‘man’ console.log(name, age, address, gender)}saveInfo()在函数离 main先对参数做一个默认值赋值,然后再使用避免使用的过程中报错,再来看es6中的使用的方法:function saveInfo({name= ‘william’, age= 18, address= ‘changsha’, gender= ‘man’} = {}) { console.log(name, age, address, gender)}saveInfo()在函数定义的时候就定义了默认参数,这样就免了后面给参数赋值默认值的过程,是不是看起来简单多了总结这篇文章简单介绍了es6的解构赋值,如果有错误或不严谨的地方,欢迎批评指正,如果喜欢,欢迎点赞收藏 ...

December 9, 2018 · 2 min · jiezi

使用Html5多媒体实现微信语音功能

随着微信等社交App的兴起,语音聊天成为很多App必备功能,大到将语音聊天作为主要功能的社交App,小到电商App的语音客服、店小二功能,语音聊天成为了必不可少的方式。但是很多人感觉网页端语音离我们很遥远,这些更多是本地应用的工作,其实不然,随着Html5的发展,语音功能也渐渐成为前端必会的功能之一。为什么要学会HTML5 的语音呢?1.Html5 规范推进,手机的更新加速了操作系统更新,语音功能将会变成前端主要的工作之一,就像现在的canvas一样。前端实现语音功能开发速度更快,更节省人力(这意味着给老板省钱,给老板省钱就是在给自己涨工资)2.即使是现在本地应用做语音功能,熟悉前端语音交互的各种坑能够让你们的同事关系更和谐,协作更顺畅,而不是互相掐架。3.了解新的技术可以预防面试,二来可以预判技术潮流,不至于学了一堆屠龙之技或者墨守成规,更有利于让自己的知识和职业核心竞争力一直处在食物链的顶端。4.前端大部分人对语音功能有误解,以为语音功能就是HTML5 audio标签而已,事实上真的不是那么简单的"而已"不墨迹那么多,咱们直接开发一个小项目啥都明明白儿白儿了,先看效果图业务逻辑非常简单,跟我们微信用法一模一样,手按下去字变成松开结束,同时说话被录下来,松手的时候,变成按下结束,同时发送语音给对方我们一步一步一步来,首先我们先整一个html页面<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>微信语音</title> <link rel=“stylesheet” href=“css/record.css”></head><body> <div id=“wrap”> <header id=“header”> <div id=“left”> <i class=“material-icons”> chevron_left </i> 微信(184) </div> <div id=“mid”>艾达·王</div> <div id=“right”> <i class=“material-icons”> more_horiz </i> </div> </header> <div id=“contentWrap”> <ul id=“chatList”> <li class=“item_me”> <div class=“chatContent”>我是不是你最疼爱的人? <span class=“bot”></span> <span class=“top”></span> </div> <div class=“avatar”> <img src=“images/ava1.png” alt=""> </div> </li> <li class=“item_you”> <div class=“avatar”> <img src=“images/ava2.jpg” alt=""> </div> <div class=“chatContent”>奔跑吧,兄弟!(滚犊子) <span class=“bot”></span> <span class=“top”></span> </div> </li> <li class=“item_me”> <div class=“chatContent”>这里我就不多说了,上来就是一梭子代码…… <span class=“bot”></span> <span class=“top”></span> </div> <div class=“avatar”> <img src=“images/ava1.png” alt=""> </div> </li> <li class=“item_you”> <div class=“avatar”> <img src=“images/ava2.jpg” alt=""> </div> <div class=“chatContent”>大彬哥,你说你咋这么优秀呢?看见你我有一种大海的感觉 <span class=“bot”></span> <span class=“top”></span> </div> </li> <li class=“item_me”> <div class=“chatContent”>老妹儿,你是不是喜欢上我了呢…… <span class=“bot”></span> <span class=“top”></span> </div> <div class=“avatar”> <img src=“images/ava1.png” alt=""> </div> </li> <li class=“item_you”> <div class=“avatar”> <img src=“images/ava2.jpg” alt=""> </div> <div class=“chatContent”>不是,我晕船,看见你想吐…… <span class=“bot”></span> <span class=“top”></span> </div> </li> </ul> </div> <footer id=“footer”> <div id=“keyboard”> <i class=“material-icons”> keyboard </i> </div> <div id=“sayBtn”> <span id=“sendBtn” class=“sendBtn”>按下 说话</span> </div> <div id=“icon”><i class=“material-icons”> sentiment_satisfied </i></div> <div id=“add”><i class=“material-icons”> add_circle_outline </i></div> </footer> </div></body></html>css部分,{ margin: 0; padding: 0;}ul li{ list-style: none;}html,body{ height: 100%; width: 100%; overflow: hidden;}body{ background: #ebebeb;}@font-face { font-family: ‘Material Icons’; font-style: normal; font-weight: 400; src: url(../css/iconfont/MaterialIcons-Regular.eot); / For IE6-8 / src: local(‘Material Icons’), local(‘MaterialIcons-Regular’), url(../css/iconfont/MaterialIcons-Regular.woff) format(‘woff2’), url(../css/iconfont/MaterialIcons-Regular.woff2) format(‘woff’), url(../css/iconfont/MaterialIcons-Regular.ttf) format(’truetype’); } .material-icons { font-family: ‘Material Icons’; font-weight: normal; font-style: normal; font-size: 32px; / Preferred icon size / display: inline-block; / line-height: 0.01rem; / text-transform: none; letter-spacing: normal; word-wrap: normal; white-space: nowrap; direction: ltr; / Support for all WebKit browsers. / -webkit-font-smoothing: antialiased; / Support for Safari and Chrome. / text-rendering: optimizeLegibility; / Support for Firefox. / -moz-osx-font-smoothing: grayscale; / Support for IE. */ font-feature-settings: ’liga’; }#wrap{ display: flex; flex-direction: column; justify-content: space-between; height: 100%;}#header{ height: 46px; line-height: 46px; background: #363539; display: flex; align-items: center; color: #fff; justify-content: space-between;}#header #left{ display: flex; align-items: center; font-size: 14px; width: 100px;}#header #right{ display: flex; align-items: center; width: 100px; justify-content: flex-end;}#header #right i{ padding-right: 6px;}#header #mid{ text-align: center; flex: 1;}#contentWrap{ flex: 1; overflow-y:auto;}.item_me,.item_audio{ display: flex; align-items: flex-start; justify-content:flex-end; padding: 8px;}.item_you{ display: flex; align-items: flex-start; justify-content:flex-start; padding: 8px;}.avatar{ width: 40px; height: 40px;}.avatar img{width: 100%;}.item_me .chatContent{ padding: 10px; background: #a0e75a; border: 1px solid #6fb44d; margin-right: 15px; border-radius: 5px; position: relative;}.chatContent span{width:0; height:0; font-size:0; overflow:hidden; position:absolute;}.item_me .chatContent span.bot{ border-width:8px; border-style:solid dashed dashed; border-color: transparent transparent transparent #6fb44d; right:-17px; top:10px;}.item_me .chatContent span.top{ border-width:8px; border-style:solid dashed dashed; border-color:transparent transparent transparent #a0e75a ; right:-15px; top:10px;} .item_you .chatContent{ padding: 10px; background: #a0e75a; border: 1px solid #6fb44d; margin-left: 15px; border-radius: 5px; position: relative;} .item_you .chatContent span.bot{ border-width:8px; border-style:solid dashed dashed; border-color: transparent #6fb44d transparent transparent ; left:-17px; top:10px;}.item_you .chatContent span.top{ border-width:8px; border-style:solid dashed dashed; border-color:transparent #a0e75a transparent transparent ; left:-15px; top:10px;} #footer{ height: 46px; padding: 0 4px; background: #f4f5f6; border-top: 1px solid #d7d7d8; display: flex; align-items: center; color: #7f8389; justify-content: space-around;}#sayBtn{ flex: 1; display: flex; margin: 0 5px; color:#565656; font-weight: bold;}.sendBtn{ display: block; flex: 1; padding: 8px; background: #f4f5f6; border:1px solid #bec2c1; border-radius: 5px; text-align: center;}.activeBtn{ display: block; flex: 1; padding: 8px; background: #c6c7ca; border:1px solid #bec2c1; border-radius: 5px; text-align: center;}.item_audio .chatContent{ padding: 6px; background: #fff; border: 1px solid #999; border-radius: 5px; margin-right: 15px; position: relative; width:120px; min-height: 20px;}.item_audio .chatContent span.bot{ border-width:8px; border-style:solid dashed dashed; border-color: transparent transparent transparent #999; right:-17px; top:10px;}.item_audio .chatContent span.top{ border-width:8px; border-style:solid dashed dashed; border-color:transparent transparent transparent #fff ; right:-15px; top:10px;} .material-icons_wifi{ transform: rotate(90deg); color: #a5a5a5; font-size: 22px;}.redDot{ background: #f45454; border-radius: 50%; width: 8px; height: 8px; margin-right: 10px;}这里我说两个注意点,1.html部分:图省事我并没有像素级切图,图省事我也直接用了svg图标,具体库我使用的是https://material.io/tools/icons/?style=outline2.css部分:使用flex布局。我只是为了讲解Html5功能,所以flex并没有写兼容性写法,另外App头部部分写法大家注意一下,那里是非常常用的。下面说重点js部分。<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>微信语音</title> <link rel=“stylesheet” href=“css/record.css”> <script> document.addEventListener(‘DOMContentLoaded’, function () { var oSendBtn = document.getElementById(‘sendBtn’); var soundClips = document.querySelector(’.sound-clips’); var mediaRecorder; var oChatList = document.getElementById(‘chatList’); navigator.getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia); if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia( // constraints - only audio needed for this app { audio: true }) // Success callback .then(function (stream) { rec(stream); }) // Error callback .catch(function (err) { } ); } else { } function rec(stream) { mediaRecorder = new MediaRecorder(stream); oSendBtn.addEventListener(’touchstart’, function (ev) { ev.preventDefault(); this.innerHTML = ‘松开 结束’; this.classList.add(‘activeBtn’); mediaRecorder.start(); }, false); oSendBtn.addEventListener(’touchend’, function (ev) { ev.preventDefault(); this.innerHTML = ‘按下 说话’; this.classList.remove(‘activeBtn’); mediaRecorder.stop(); }, false); mediaRecorder.ondataavailable = function (e) { var clipContainer = document.createElement(’li’); var audio = document.createElement(‘audio’); clipContainer.classList.add(‘item_audio’); clipContainer.innerHTML = &lt;div class = "redDot"&gt;&lt;/div&gt; &lt;div class="chatContent"&gt; &lt;i class="material-icons material-icons_wifi"&gt;wifi&lt;/i&gt; &lt;span class="bot"&gt;&lt;/span&gt; &lt;span class="top"&gt;&lt;/span&gt; &lt;/div&gt; &lt;div class="avatar"&gt; &lt;img src="images/ava1.png" alt=""&gt; &lt;/div&gt;; audio.setAttribute(‘controls’, ‘’); oChatList.appendChild(clipContainer); var audioURL = window.URL.createObjectURL(e.data); audio.src = audioURL; oChatList.addEventListener(’touchstart’, function (ev) { if (ev.srcElement.parentNode.className!== ‘item_audio’) return; audio.play(); ev.srcElement.parentNode.removeChild(ev.srcElement.parentNode.children[0]) }, false); }; } }, false); </script></head><body> <div id=“wrap”> <header id=“header”> <div id=“left”> <i class=“material-icons”> chevron_left </i> 微信(184) </div> <div id=“mid”>艾达·王</div> <div id=“right”> <i class=“material-icons”> more_horiz </i> </div> </header> <div id=“contentWrap”> <ul id=“chatList”> <li class=“item_me”> <div class=“chatContent”>我是不是你最疼爱的人? <span class=“bot”></span> <span class=“top”></span> </div> <div class=“avatar”> <img src=“images/ava1.png” alt=""> </div> </li> <li class=“item_you”> <div class=“avatar”> <img src=“images/ava2.jpg” alt=""> </div> <div class=“chatContent”>奔跑吧,兄弟!(滚犊子) <span class=“bot”></span> <span class=“top”></span> </div> </li> <li class=“item_me”> <div class=“chatContent”>这里我就不多说了,上来就是一梭子代码…… <span class=“bot”></span> <span class=“top”></span> </div> <div class=“avatar”> <img src=“images/ava1.png” alt=""> </div> </li> <li class=“item_you”> <div class=“avatar”> <img src=“images/ava2.jpg” alt=""> </div> <div class=“chatContent”>大彬哥,你说你咋这么优秀呢?看见你我有一种大海的感觉 <span class=“bot”></span> <span class=“top”></span> </div> </li> <li class=“item_me”> <div class=“chatContent”>老妹儿,你是不是喜欢上我了呢…… <span class=“bot”></span> <span class=“top”></span> </div> <div class=“avatar”> <img src=“images/ava1.png” alt=""> </div> </li> <li class=“item_you”> <div class=“avatar”> <img src=“images/ava2.jpg” alt=""> </div> <div class=“chatContent”>不是,我晕船,看见你想吐…… <span class=“bot”></span> <span class=“top”></span> </div> </li> </ul> </div> <footer id=“footer”> <div id=“keyboard”> <i class=“material-icons”> keyboard </i> </div> <div id=“sayBtn”> <span id=“sendBtn” class=“sendBtn”>按下 说话</span> </div> <div id=“icon”><i class=“material-icons”> sentiment_satisfied </i></div> <div id=“add”><i class=“material-icons”> add_circle_outline </i></div> </footer> </div></body></html>这里实现的录影功能要注意的点很多,我们一个个说,第一个东西,navigator.getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia); if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia( { audio: true }) // Success callback .then(function (stream) { rec(stream); }) // Error callback .catch(function (err) { } ); } else { }当大家看一些html5关于录音的接口的时候,你看到这个Navigator.getUserMedia()就要小心了,这个是老规范的东西了,被废了,新的是navigator.mediaDevices.getUserMediahtml5 多媒体里面的语音这块换了好几茬规范,很乱,有些标签甚至一个浏览器都没实现过,未曾绽放就枯萎了,你也不用关心也没必要浪费那个时间知道,你只要知道我说这些就够了,因为你知道那些被废掉的过往没啥用,有那个时间还不如来一局LOL或者王者荣耀(虽然我并不懂二者的区别,不过这两个游戏应该都挺好玩吧,没玩过不懂)。里面的东西大家也不需要看懂,什么promise了,什么媒体流了,你不用知道,你就知道这样一件事就行了,上面的代码就相当于打开了水龙头(或者说按下的录音机的录音键),那么我们得有东西接着水啊,我们可以用电饭锅(录音机的话就是磁带)放水龙头下面看着它往里面ci(我们老家话,射的意思),如下代码mediaRecorder = new MediaRecorder(stream);接下来就是,一按按钮就生米煮成熟饭了,对应录音机就是录完了按按钮就播放了,但是在我们程序里面要想播放你不仅要有磁带,还得有录音机,录音机就是audio标签,没有好办,我们new一个。这个世界上没有什么对象是程序员不敢new的,new一个不行,就new两个。剩下的代码除了吓人之外,没啥缺点,简单的令人发指。mediaRecorder.ondataavailable = function (e) { var clipContainer = document.createElement(’li’); var audio = document.createElement(‘audio’); clipContainer.classList.add(‘item_audio’); clipContainer.innerHTML = &lt;div class = "redDot"&gt;&lt;/div&gt; &lt;div class="chatContent"&gt; &lt;i class="material-icons material-icons_wifi"&gt;wifi&lt;/i&gt; &lt;span class="bot"&gt;&lt;/span&gt; &lt;span class="top"&gt;&lt;/span&gt; &lt;/div&gt; &lt;div class="avatar"&gt; &lt;img src="images/ava1.png" alt=""&gt; &lt;/div&gt;; audio.setAttribute(‘controls’, ‘’); oChatList.appendChild(clipContainer); var audioURL = window.URL.createObjectURL(e.data); audio.src = audioURL; oChatList.addEventListener(’touchstart’, function (ev) { if (ev.srcElement.parentNode.className!== ‘item_audio’) return; audio.play(); ev.srcElement.parentNode.removeChild(ev.srcElement.parentNode.children[0]) }, false); };其实就是录好了就播。OK,是不是很简单 ,整个项目我说几个点吧:1.切图结构合理是你后面做功能的前提,结构做的好,后面就省事,想想诸葛不亮吧,未出茅庐人家就把html5结构搭好了,有三个section.2.原生js和ES6的基础打牢可以为你提供不同的思路,比如我这里就使用了事件委托,还有ES6模板引擎。尤其是事件委托,不用的话查找节点很麻烦,另外代码套来套去也容易乱。3.新的 知识和技术其实并不复杂,其实很简单,你想如果新技术不是为了让功能更好实现,更能解决我们的问题,那开发新技术干嘛?因为那帮大胡子的大牛们没事干怕被领导说工作量不饱和?技术是为了解决问题和让我们生活更美好服务的。4.这个项目IOS 11以下跑不通,因为IOS 11.2之前不支持这个方法,需要IOS本地应用开发人员给你提供支援,但是在android下面是很OK的。而且可以预见,再过几年IOS 原生也不用给你支援都支持了,那你开发效率得多高。不要以为这些技术很遥远,html5真正商用也不过15年左右(vue 、react、angular大规模使用才几年?),机会留给有准备的人。整个项目细节和要注意的点还是很多的,希望大家真正自己敲一遍,因为你看懂了我的文章跟你会用这个技术两码事,祝大家在前端的路上越走越远(记得常回来看看^_^)。 ...

December 8, 2018 · 5 min · jiezi

vue中的computed的this指向问题

今天在写vue项目时,用到了computed计算属性,遇到了使用箭头函数出现this指向问题,这里记录下1.箭头函数中的this箭头函数内部的this是词法作用域,由上下文确定函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象2.vue中的computed使用箭头函数list: () => { console.log(this)}不使用箭头函数allFigure: function() { console.log(this)},使用get()allFigure: { get() { console.log(this); }}3.自己的理解在computed中使用箭头函数的话,会导致this指向的不是整个的vueComponent此时使用匿名函数的形式就可以解决,this指向了vueComponent或者使用对象的形式,用set()、get()方法也不会出现问题正在努力学习中,若对你的学习有帮助,留下你的印记呗(点个赞咯^_^)往期好文推荐:判断iOS和Android及PC端纯css实现瀑布流(multi-column多列及flex布局)时间戳转换成时间日期格式及去重微信小程序之购物车和父子组件传值及calc的注意事项

December 5, 2018 · 1 min · jiezi

ECMAScript 2015(ES6)有用的提示与技巧

EcmaScript 2015(ES6)已经出来好几年了,可以巧妙地使用各种新功能。列出并讨论其中一些,你会发现它们很有用。如果你知道其他好方法,请在评论中回复,共同学习。1.必传参数ES6提供了默认参数值,没有该参数的情况下调用函数时使用的默认值。在以下示例中,将required()函数设置为 a 和 b 参数的默认值。 这意味着如果未传递a或b,则调用required()函数,将收到错误。const required = () => {throw new Error(‘Missing parameter’)};//The below function will trow an error if either “a” or “b” is missing.const add = (a = required(), b = required()) => a + b;add(1, 2) //3add(1) // Error: Missing parameter.2.强大的“reduce”Array的reduce方法非常通用。它通常用于将项目数组转换为单个值,但是你可以用它做更多的事情。大多数这些技巧都依赖于初始值是数组或对象而不是像字符串或变量这样的简单值。2.1 使用reduce同时进行 map 和 filter 操作假设有一个项目列表的情况,并且想要更新每个项目(即 Array.map 操作),然后只过滤几个项目(即 Array.filter ),这意味着需要两次循环遍历列表!在下面的示例中,我们希望将数组中的项的值加倍,然后选择大于50的项。我们使用强大的 reduce 方法高效的同时做到:const numbers = [10, 20, 30, 40];const doubledOver50 = numbers.reduce((finalList, num) => { num = num * 2; //double each number (i.e. map) //filter number > 50 if (num > 50) { finalList.push(num); } return finalList;}, []);doubledOver50; // [60, 80]2.2 使用 “reduce” 代替是 “map” 或 “filter”如果仔细查看上面的示例(2.1),就会知道 reduce 可以代替 map 或 filter !2.3 使用 reduce 来判断括号是否对称// 返回 0 表示对称const isParensBalanced = (str) => { return str.split(’’).reduce((counter, char) => { if (counter < 0) { // 匹配到 ‘)’ 在 ‘(’ 前面 return counter; } else if ( char === ‘(’) { return ++counter; } else if ( char === ‘)’) { return –counter; } else { return counter; //其它字符 } }, 0) // 初始化值为0}isParensBalanced(’(())’) // 0 <—对称isParensBalanced(’(asdfds’) // 0 <—对称isParensBalanced(’(()’) // 1 <—不对称isParensBalanced(’)(’) // 0 <—不对称2.4 计算重复的数组项(转换数组→对象)有时你希望复制数组项或将数组转换为对象。 你可以使用 reduce。在下面的例子中,计算cars 中每个值重复数量,并将这组对应的数据放到一个对象中:var cars = [‘BMW’,‘Benz’, ‘Benz’, ‘Tesla’, ‘BMW’, ‘Toyota’];var carsObj = cars.reduce(function (obj, name) { obj[name] = obj[name] ? ++obj[name] : 1; return obj;}, {});carsObj; // => { BMW: 2, Benz: 2, Tesla: 1, Toyota: 1 }reduce 还可以做更多的事情,建阅读 MDN上列出的示例。3. 对象解构3.1 删除不需要的属性有时候你想删除不需要的属性——可能是因为它们包含敏感信息或者太大了。我们不需要遍历整个对象来删除它们,只需将这些不需要的数据提取到对应变量中,并将有用的保存在rest参数中。在下面的示例中,我们希望删除_internal和tooBig属性。我们可以将它们分配给_internal和tooBig变量,并将剩余的保存到在 rest parameter cleanObject 参数中:let {_internal, tooBig, …cleanObject} = {el1: ‘1’, _internal:“secret”, tooBig:{}, el2: ‘2’, el3: ‘3’};console.log(cleanObject); // {el1: ‘1’, el2: ‘2’, el3: ‘3’}3.2 在函数参数中分解嵌套对象在下面的示例中,engine 属性是 car 对象的一个内嵌对象。如果我们想获取 engine 中的 vin 值,可以使用以下解构方式:var car = { model: ‘bmw 2018’, engine: { v6: true, turbo: true, vin: 12345 }}const modelAndVIN = ({model, engine: {vin}}) => { console.log(model: ${model} vin: ${vin});}modelAndVIN(car); // => model: bmw 2018 vin: 123453.3 合并对象ES6附带了一个扩展操作符(由三个点表示)。它通常用于解构数组值,但也可以在对象上使用它。在下面的示例中,我们使用扩展操作符(…)在新对象中进行扩展。第二个对象中的属性键将覆盖第一个对象中的属性键:let object1 = { a:1, b:2,c:3 }let object2 = { b:30, c:40, d:50}let merged = {…object1, …object2} //spread and re-add into mergedconsole.log(merged) // {a:1, b:30, c:40, d:50}4.Sets4.1 使用 set 时行数组去重在ES6中,可以使用 set 轻松时行数组去重,因为 set 只允许存储惟一的值:let arr = [1, 1, 2, 2, 3, 3];let deduped = […new Set(arr)] // [1, 2, 3]4.2 使用数组的方法使用扩展运算符(…) 将 set 转换为数组很简单且在集合上使用所有数组方法!let mySet = new Set([1,2, 3, 4, 5]);var filtered = […mySet].filter((x) => x > 3) // [4, 5]5. 数组解构5.1 交换值let param1 = 1;let param2 = 2;[param1, param2] = [param2, param1];console.log(param1) // 2console.log(param2) // 15.2 从一个函数接收和分配多个值在下面的例子中,我们在/post获取一个post,并在/comments 获取相关 comments 。由于使用 async/wait,该函数以数组的形式返回结果。使用数组析构,我们可以直接将结果赋值到相应的变量中。async function getFullPost(){ return await Promise.all([ fetch(’/post’), fetch(’/comments’) ]);}const [post, comments] = getFullPost();原文地址:https://medium.freecodecamp.o…你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》 ...

December 4, 2018 · 2 min · jiezi

前端er,你真的会用 async 吗?

async 异步函数 不完全使用攻略前言现在已经到 8012 年的尾声了,前端各方面的技术发展也层出不穷,VueConf TO 2018 大会 也发布了 Vue 3.0的计划。而在我们(我)的日常中也经常用 Vue 来编写一些项目。那么,就少不了 ES6 的登场了。那么话说回来,你真的会用 ES6 的 async 异步函数吗?1、async 介绍先上 MDN 介绍:https://developer.mozilla.org…async function 用于声明 一个 返回 AsyncFunction 对象的异步函数。异步函数是值通过事件循环异步执行的函数,它会通过一个隐式的 Promise 返回其结果。如果你的代码使用了异步函数,它的语法和结构更像是标准的同步函数人工翻译:async 关键字是用于表示一个函数里面有异步操作的含义。它通过返回一个 Promise 对象来返回结果它的最大的特点是:通过 async / await 将异步的操作,但是写法和结构却是和我们平时写的(同步代码)是一样2、示范// 一般我们会把所有请求方法都定义在一个文件里,这里定义一个方法来模拟我们的日常请求function fetch() { axios.get(’/user?ID=12345’) .then(function (response) { console.log(response); }) .catch(function (error) { console.log(error); });};// 然后在需要它的地方调用它async function getUserInfo() { const info = await fetch(); return info;}getUserInfo().then(info => console.log(info));我们可以看到,整个过程非常直观和清晰,语句语义非常明确,整个异步操作看起来就像是同步一样。如果看完上面的流程没有问题的话,那我们接下来继续深入的了解一下。3、async Promise setTimeout(定时器) 的结合使用情况接下来给大家演示一道题目,这道题是我当时面某条的面试题,估计和多人也见过,这道题非常经典而且使用场景页非常多,研究意义非常大,那么我在这里就给大家分享一下。求下面的输出结果: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(‘setTimeout’)},0) async1();new Promise(function(resolve){ console.log(‘promise1’) resolve();}).then(function(){ console.log(‘promise2’)})console.log(‘script end’)这里一共有 8 条 log 语句,先别复制到控制台上,大家给20秒钟的时间默念一下输出的顺序。1..2.. .. .. 20我先给上正确的答案:script startasync1 startasync2promise1script endpromise2async1 endsetTimeout如果你的答案和上面的正确答案有所偏差,那么说明你对 async / await 的理解还是不够深刻,希望你阅读完我的这篇文章之后可以直面各种同步异步问题了(嘻嘻,这还不点个赞嘛)我们再来回顾一下 MDN 对 async / await 的描述:当调用一个 async 函数时,会返回一个 Promise 对象。当这个 async 函数返回一个值时,Promise 的 resolve 方法会负责传递这个值;当 async 函数抛出异常时,Promise 的 reject 方法也会传递这个异常值。async 函数中可能会有 await 表达式,这会使 async 函数暂停执行,等待 Promise 的结果出来,然后恢复async函数的执行并返回解析值(resolved)。async/await的用途是简化使用 promises 异步调用的操作,并对一组 Promises执行某些操作。正如Promises类似于结构化回调,async/await类似于组合生成器和 promises。awaitawait 操作符用于等待一个Promise 对象。它只能在异步函数 async function 中使用。[return_value] = await expression;await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。若 Promise 正常处理(fulfilled),其回调的resolve函数参数作为 await 表达式的值,继续执行 async function。若 Promise 处理异常(rejected),await 表达式会把 Promise 的异常原因抛出。另外,如果 await 操作符后的表达式的值不是一个 Promise,则返回该值本身。其中非常重要的一句是:遇到 await 表达式时,会让 async 函数 暂停执行,等到 await 后面的语句(Promise)状态发生改变(resolved或者rejected)之后,再恢复 async 函数的执行(再之后 await 下面的语句),并返回解析值(Promise的值)这么多 Promise 相关的内容是因为async / await 是建立在 Promise 的基础上的呀~~然后再来回头看我们的题目,会发现,有点不对劲啊async1 endpromise2那是因为还有一个Promise.resolve 的点没有考虑,这也是我中招的点4、分析过程定义一个异步函数 async1定义一个异步函数 async2打印 ‘script start’ // *1定义一个定时器(宏任务,优先级低于微任务),在0ms 之后输出执行异步函数 async1打印 ‘async1 start’ // *2遇到await 表达式,执行 await 后面的 async2打印 ‘async2’ // *3返回一个 Promise,跳出 async1 函数体执行 new Promise 里的语句打印 ‘promise1‘ // *4resolve() , 返回一个 Promise 对象,把这个 Promise 压进队列里打印 ’script end’ // *5同步栈执行完毕回到 async1 的函数体,async2 函数没有返回 Promise,所以把要等async2 的值 resolve,把 Promise 压进队列执行 new Promise 后面的 .then,打印 ’promise2‘ // *6回到 async1 的函数体,await 返回 Promise.resolve() ,然后打印后面的 ’async1 end‘ // *7最后执行定时器(宏任务) setTimeout,打印 ’setTimeout‘ // *8我对这段代码的过程分析大致如上(如果有什么理解不对的地方请指出),这里有很关键而且是大家容易理解错误的点是:很多人以为 await 会一直等待后面的表达式执行完之后才会执行后续代码,实际上 await 是会先执行后面的表达式,然后返回一个Promise,接着就跳出整个 async 函数来执行后面的代码,也就是说执行到 await 的时候,会有一个 让出线程 的操作。等后面的同步站执行完了之后,又会回到 async 函数中等待 await 表达式的返回值,如果不是一个 Promise 对象,则会有一个期待它 resolve 成为一个 Promise对象的过程,然后继续执行 async 函数后面的代码,直到是一个 Promise 对象,则把这个 Promise 对象放入 Promise 队列里。所以说 ,’async1 end’ 和‘promise2‘ 这个不注意就会出错的难点就是这样那么现在,我们是不是大致上对async / await 理解了呢,我们来改一下这道题再来看看,把 async2 改造一下async function async1(){ console.log(‘async1 start’) await async2() console.log(‘async1 end’)}function async2(){ // 去掉了 async 关键字 console.log(‘async2’);}console.log(‘script start’)setTimeout(function(){ console.log(‘setTimeout’)},0) async1();new Promise(function(resolve){ console.log(‘promise1’) resolve();}).then(function(){ console.log(‘promise2’)})console.log(‘script end’)这次大家能做对了吗~5、日常常用示例上面写了那么多,只是为了方便大家对于异步函数的理解,下面给一些我们日常开发中使用异步函数的例子。一般来说,我们有一个业务需要分不完成,每个步骤都是异步的,并且严重依赖于上一步的执行结果,稍有不慎就会进入回调地狱(callback hell)了,这种情况下,我们可以用 async / await 来完成// 比如在这里场景,我们提交数据的时候先判定用户是否有这个权限,然后再进行下一步动作async function submitData(data) { const res = await getAuth(); // 获取授权状态 if (res….) { const data = await submit(data); } toast(data.message);}这样就可以保证两个操作的先后顺序或者是在 Vue 中,一些初始化的操作async created() { const res = await this.init(); // 获取列表等操作 const list = await this.getPage(); // 分页请求等}但是在使用过程中,我们会发现刚从回调地狱中解救,然后就陷入 async / await 地狱的诞生举一个例子:async created() { const userInfo = await this.getUserInfo(); // 获取用户数据 const list = await this.getNewsList(); // 获取文章数据}表面上看,这段语法是正确的,但并不是一个优秀实现,因为它把两个没有先后顺序的一部操作强行变成同步操作了,因为这里的代码是一行接着一行执行的,想一下,我们没有必要在获取用户数据之后才去获取文章数据,它们的工作是可以同时进行的这里给出一些常用的并发执行的实例async created() { const userInfo = this.getUserInfo(); // 它们都会返回 Promise 对象 const list = this.getNewsList(); await userInfo; await list; // …do something}// 如果有很多请求的情况下可以使用 Promise.allasync created() { Promise.all([this.getUserInfo(), this.getNewsList()]).then(()=> { // …do something });}5、图例6、小结1、异步的终极解决方案2、看起来像同步的异步操作3、便捷的捕获错误和调试4、支持并发执行5、要知道避免 async / await 地狱7、写在最后好了,关于async 异步函数的不完全指南就说到这里了,上面所提及的内容,可能也就比较浅显的内容。而且有时候,建议大家熟练使用它,在日常开发中多使用多总结才会有沉淀的效果,都是要靠自己多练,才能熟悉使用,熟能生巧!最后,如果大家觉得我有哪里写错了,写得不好,有其它什么建议(夸奖),非常欢迎大家补充。希望能让大家交流意见,相互学习,一起进步!我是一名 19 的应届新人,以上就是今天的分享,新手上路中,后续不定期周更(或者是月更哈哈),我会努力让自己变得更优秀、写出更好的文章,文章中有不对之处,烦请各位大神斧正。如果你觉得这篇文章对你有所帮助,请记得点赞或者品论留言哦~。 ...

December 3, 2018 · 2 min · jiezi

ES6迭代器的简单指南和示例

我们将在本文中分析迭代器。迭代器是在JavaScript中循环任何集合的一种新方法。它们是在ES6中引入的,由于它们的广泛用途和在不同地方的使用而变得非常流行。我们将从概念上理解迭代器是什么,以及在何处使用它们和示例。我们还将看到它在JavaScript中的一些实现。如果我问你,你会怎么做?你会说——很简单。我将使用 for、while、for-of 或 其它 方法对它们进行循环。简介假设你有这个数组const myFavouriteAuthors = [ ‘Neal Stephenson’, ‘Arthur Clarke’, ‘Isaac Asimov’, ‘Robert Heinlein’];在某些情况下,希望返回数组中的所有单独值,以便在屏幕上打印它们、操作它们或对它们执行某些操作。如下:现在,假设你拥有一个自定义数据结构来保存所有作者,而不是上面的数组,如:mypreferteauthors 是一个对象,它包含另一个对象 allAuthors。allAuthors 包含三个数组,其中包含 fiction、scienceFiction 和 fantasy。现在,如果要求你循环遍历 myFavouriteAuthors 以获得所有的作者,你的方法是什么? 你可能会尝试一些循环组合来获得所有数据。但是,如果你这样做了 ——for (let author of myFavouriteAuthors) { console.log(author)}// TypeError: {} is not iterable你将得到一个类型错误,说明该对象不可迭代。让我们看看什么是可迭代的,以及如何使对象可迭代。在本文的最后,你将了解如何在定制对象上使用for-of循环,在本例中是在 mypreferteauthors 上使用 for-of 循环。可迭代对象与迭代器 (Iterables and Iterators)在上一节中看到了问题,从我们的自定义对象中获取所有的作者是不容易的。我们需要某种方法,通过它我们可以有序地获取内部数据。我们在 mypreferteauthors 中添加一个返回所有作者的方法 getAllAuthors。如:这是一个简单的方法。它帮我们完成了获取所有作者的功能。但是,这种实现可能会出现一些问题:getAllAuthors 的名称非常具体。如果其他人正在创建自己的 mypreferteauthors,他们可能会将其命名为retrieveAllAuthors。作为开发人员,我们总是需要知道返回所有数据的特定方法,在本例中,它被命名为getAllAuthors。getAllAuthors 返回的是字符串数组,如果另一个开发人员以这种格式返回一个对象数组,该怎么办:[ {name: ‘Agatha Christie’}, {name: ‘J. K. Rowling’}, … ]开发人员必须知道返回所有数据的方法的确切名称和返回类型。如果我们规定方法的名称和它的返回类型是固定不变的呢? 让我们将这个方法命名为 — iteratorMethodECMA 也采取了类似的步骤来标准化在定制对象上循环的过程。但是,ECMA没有使用名称 iteratorMethod,而是使用名称 Symbol.iterator。Symbols 提供的名称是唯一的,不能与其他属性名称冲突。同时,Symbol.iterator 返回一个名为迭代器的对象,这个迭代器将拥有一个名为next的方法,该方法将返回一个具有键值为 value 和 done 的对象。值键 value 包含当前值,它可以是任何类型的,done 是布尔值,它表示是否获取了所有的值。下图可以帮助建立可迭代对象、迭代器和next之间的关系,这种关系称为迭代协议。根据Axel Rauschmayer博士的《探索JS》一书:可迭代是一种数据结构,它希望使其元素对外部可访问,通过实现一个关键字是Symbol.iterator的方法来实现,该方法是迭代器的工厂,也就是说,它将创建迭代器。迭代器是一个指针,用于遍历数据结构的元素,我们将使用computed property语法来设置这个键,如下:建立可迭代对象因此,正如我们在上一节学到的,我们需要实现一个名为Symbol.iterator的方法在第4行,我们创建迭代器。它是一个定义了next方法的对象。next方法根据step变量返回值。在第25行,我们检索iterator,27行,我们调用next方法,直到 done的值为 true。这正是for-of循环中发生的事情,for-of接受一个迭代器,并创建它的迭代器,它会一直调用next(),直到 done为 true。JavaScript中可迭代对象(iterable)JavaScript中的很多对象都是可迭代的。它们可能不是很好的察觉,但是如果仔细检查,就会发现迭代的特征:Arrays and TypedArraysStrings —— 遍历每个字符或Unicode代码点Maps —— 遍历其键-值对Sets —— 遍历元素arguments —— 函数中类似数组的特殊变量DOM elements (Work in Progress)JS中使用迭代的其他一些结构是:for-of – for-of 循环需要一个可迭代的对象,否则,它将抛出一个类型错误。for (const value of iterable) { … }数组解构 – 由于可迭代性,会发生析构。让我们来看看:const array = [‘a’, ‘b’, ‘c’, ’d’, ’e’];const [first, ,third, ,last] = array;等价于:const array = [‘a’, ‘b’, ‘c’, ’d’, ’e’];const iterator = arraySymbol.iterator;const first = iterator.next().valueiterator.next().value // Since it was skipped, so it’s not assignedconst third = iterator.next().valueiterator.next().value // Since it was skipped, so it’s not assignedconst last = iterator.next().value扩展操作符(…)const array = [‘a’, ‘b’, ‘c’, ’d’, ’e’];const newArray = [1, …array, 2, 3];等价于:const array = [‘a’, ‘b’, ‘c’, ’d’, ’e’];const iterator = arraySymbol.iterator;const newArray = [1];for (let nextValue = iterator.next(); nextValue.done !== true; nextValue = iterator.next()) { newArray.push(nextValue.value);}newArray.push(2)newArray.push(3)Promise.all 和 Promise.race 接受可迭代对象Maps 和 Sets让 myFavouriteAuthors 可迭代下面是一个实现,它使mypreferteauthors具有可迭代性:const myFavouriteAuthors = { allAuthors: { fiction: [ ‘Agatha Christie’, ‘J. K. Rowling’, ‘Dr. Seuss’ ], scienceFiction: [ ‘Neal Stephenson’, ‘Arthur Clarke’, ‘Isaac Asimov’, ‘Robert Heinlein’ ], fantasy: [ ‘J. R. R. Tolkien’, ‘J. K. Rowling’, ‘Terry Pratchett’ ], }, Symbol.iterator { // 获取数组中的所有作者 const genres = Object.values(this.allAuthors); // 存储当前类型和索引 let currentAuthorIndex = 0; let currentGenreIndex = 0; return { // Implementation of next() next() { // 根据当前的索引获取对应的作者信息 const authors = genres[currentGenreIndex]; // 当遍历完数组 authors是,oNotHaveMoreAuthors 为 true const doNothaveMoreAuthors = !(currentAuthorIndex < authors.length); if (doNothaveMoreAuthors) { // 加一继续访问下一个 currentGenreIndex++; // 重置 currentAuthorIndex = 0; } // 如果所有 genres 都遍历完了结,那么我们需要告诉迭代器不能提供更多的值。 const doNotHaveMoreGenres = !(currentGenreIndex < genres.length); if (doNotHaveMoreGenres) { return { value: undefined, done: true }; } // 如果一切正常,从当genre 返回 作者和当前作者索引,以便下次,下一个作者可以返回。 return { value: genres[currentGenreIndex][currentAuthorIndex++], done: false } } }; }};for (const author of myFavouriteAuthors) { console.log(author);}console.log(…myFavouriteAuthors)通过本文获得的知识,你可以很容易地理解迭代器是如何工作的,这种逻辑可能有点难以理解。因此,理解这个概念的最佳方法是多多敲死代码,多多验证!你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》 ...

November 30, 2018 · 2 min · jiezi

较常用的Math方法及ES6中的扩展

记录下与Math有关的常用方法,如:求最大值、最小值等,或者是保留几位数啥的1.数据let floatA = 2.325232;let floatB = 2.3456;let temporaryArray = [1, 2, 5, 6, 3];let minusNum = -12;let minusFloat = -12.321;let intA = 10;let min, max, num;2.求最大值、最小值{ /* 求最小值 / min = Math.min(floatA, floatB); console.log(min); / 使用apply来重新绑定this / min = Math.min.apply(null, temporaryArray); console.log(min); / 使用展开运算符 / min = Math.min(…temporaryArray); console.log(min); / 求最大值 / max = Math.max(floatA, floatB); console.log(max); / 使用apply来重新绑定this / max = Math.max.apply(null, temporaryArray); console.log(max); / 使用展开运算符 / max = Math.max(…temporaryArray); console.log(max);}3.取整{ / 四舍五入取整:取与参数最接近的整数 / num = Math.round(floatA); console.log(num); num = Math.round(minusFloat); console.log(num); / 向上取整:取大于或等于函数参数,并且与之最接近的整数 / num = Math.ceil(floatB); console.log(num); / 向下取整:取小于或等于函数参数,并且与之最接近的整数 / num = Math.floor(floatB); console.log(num); / ceil、floor结合起来,实现一个总是返回数值的整数部分的函数 / function getInteger(value) { value = Number(value); return value < 0 ? Math.ceil(value) : Math.floor(value); } console.log(getInteger(-2.3322));}4.求绝对值{ / 负整数 / num = Math.abs(minusNum); console.log(num); / 负浮点数 / num = Math.abs(minusFloat); console.log(num);}5.次幂{ / 结果是虚数或负数,则该方法将返回 NaN * 如果由于指数过大而引起浮点溢出,则该方法将返回 Infinity / / 2的3次方 / num = Math.pow(2, 3); console.log(num);}6.去平方根{ / 求参数的平方根,如果参数小于 0,则返回 NaN / num = Math.sqrt(9); console.log(num);}7.生成随机数{ / 生成0-1的随机数,大于0小于1 / num = Math.random(); console.log(num); / 生成0-10的随机数 / num = Math.random() * 10; console.log(num); / 生成任意范围随机数 / function getRandom(min, max) { return Math.random() * (max - min) + min; } console.log(getRandom(3.5, 6.5)); / 整数min与整数max生成任意范围整数随机数 / function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } console.log(getRandomInt(5, 15));}8.es6中对Math方法的扩展(部分){ / 取整(非四舍五入) * 对于非数值,内部调用 Number 强转为数值 * 对于空值或其他数据,返回NaN * / num = Math.trunc(floatA); console.log(num); num = Math.trunc(intA); console.log(num); num = Math.trunc(‘aaa’); console.log(num);}{ / 判断一个数是正数、负数或零 * 正数返回+1,负数返回-1,零返回0或-0 * 其他值 NaN * / num = Math.sign(2); console.log(num); num = Math.sign(0); console.log(num); num = Math.sign(-0); console.log(num); num = Math.sign(-14); console.log(num); num = Math.sign(‘ss’); console.log(num);}{ let a = 2; a *= 3; / 相当于 aa*a / console.log(a);}9.保留位数操作{ / 四舍五入保留两位小数 * toFixed(num) 方法可把 Number型 四舍五入为指定小数位数的数字 * num规定小数的位数,是 0 ~ 20 之间的值,包括 0 和 20 * 有些实现可以支持更大的数值范围,如果省略了该参数,将用 0 代替 * / num = floatA.toFixed(2); console.log(num); let word = 2.5; num = word.toFixed(); console.log(num); / 不四舍五入 / num = Math.floor(23.365125 * 100) / 100; console.log(num);}10.字符串转数字 { / parseInt(value, radix) * 用于解析字符串,返回一个整数 * radix表示要解析的数字的基数,该值介于 2 ~ 36 之间 * 如果省略该参数或其值为 0,则数字将以 10 为基础来解析 * 如果它以 “0x” 或 “0X” 开头,将以 16 为基数 * 如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。 * / let temporaryString = ‘123’; num = parseInt(temporaryString); console.log(num); { console.log(parseInt(“10”)); //返回 10 console.log(parseInt(“19”,10)); //返回 19 : 10 + 9 console.log(parseInt(“11”,2)); //返回 3 : 2 + 1 console.log(parseInt(“17”,8)); //返回 15 : 8 + 7 console.log(parseInt(“1f”,16)); //返回 31 : 16 + 15 console.log(parseInt(“010”)); //未定:返回 10 或 8 } / 常见的parseInt一道题 / { console.log([‘1’, ‘2’, ‘3’].map(parseInt)); / 返回[1, NaN, NaN] * map(function(value, index, array) {}) * map方法中的回调函数中的3个参数值,每个值value,索引值index,数组对象array * 上面的相当于parseInt(‘1’, 0)、parseInt(‘2’, 1)、parseInt(‘3’, 2) * / } / Number强转 */ num = Number(‘12345’); console.log(num);}正在努力学习中,若对你的学习有帮助,留下你的印记呗(点个赞咯^_^)往期好文推荐:判断iOS和Android及PC端纯css实现瀑布流(multi-column多列及flex布局)微信小程序之购物车和父子组件传值及calc的注意事项css实现波浪线及立方体 ...

November 28, 2018 · 3 min · jiezi

ES6核心特性

前言ES6 虽提供了许多新特性,但我们实际工作中用到频率较高并不多,根据二八法则,我们应该用百分之八十的精力和时间,好好专研这百分之二十核心特性,将会收到事半功倍的奇效!写文章不容易,请大家多多支持与关注!本文首发地址GitHub博客一、开发环境配置这部分着重介绍:babel 编译ES6语法,如何用webpack实现模块化。1.babel为啥需要babel?ES6 提供了许多新特性,但并不是所有的浏览器都能够完美支持。下图是各个浏览器对ES6兼容性一览表(以export为例)由上图可知,有些浏览器对于ES6并不是很友好,针对 ES6 的兼容性问题,很多团队为此开发出了多种语法解析转换工具(比如babel,jsx,traceur 等),可以把我们写的 ES6 语法转换成 ES5,相当于在 ES6 和浏览器之间做了一个翻译官。其中Babel是一个广泛使用的转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。如何配置babel?·首先要先安装node.js,运行npm init,然后会生成package.json文件·npm install –save-dev babel-core babel-preset-es2015 babel-preset-latest·创建并配置.babelrc文件//存放在项目的根目录下,与node_modules同级·npm install -g babel-cli·babel-versionBabel的配置文件是.babelrc,存放在项目的根目录下。该文件用来设置转码规则和插件,具体内容如下://.babelrc文件{ “presets”: [“es2015”, “latest”], “plugins”: []}验证配置是否成功·创建./src/index.js·内容:[1,2,3].map(item=>item+1);·运行babel./src/index.js运行后得到以下部分,说明已经成功配置了babel"use strict";[1, 2, 3].map(function (item) { return item + 1;});2.webpack为啥要使用WebPack?现今的很多网页其实可以看做是功能丰富的应用,它们拥有着复杂的JavaScript代码和一大堆依赖包,模快化工具就应运而生了,其中webpack 功能强大深受人们喜爱。Webpack的工作方式是:把你的项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包为一个(或多个)浏览器可识别的JavaScript文件。如何配置webpack?·npm install webpack babel-loader –save-dev·创建并配置 webpack.config.js//webpack.config.js文件与package.json同级·配置 package.json中的scripts·运行 npm start//配置 webpack.config.js 针对.js结尾的文件除了node_modules都用babel解析module.exports = { entry: ‘./src/index.js’, output: { path: __dirname, filename: ‘./build/bundle.js’ }, module: { rules: [{ test: /.js?$/, exclude: /(node_modules)/, loader: ‘babel-loader’ }] }}//配置 package.json中的scripts"scripts": { “start”: “webpack”, “test”: “echo "Error: no test specified" && exit 1” }二、块级作用域ES5 只有全局作用域和函数作用域(例如,我们必须将代码包在函数内来限制作用域),这导致很多问题:情况1:内层变量覆盖外层变量var tmp = new Date();function f() { console.log(tmp); //undefined if (false) { var tmp = “hello world”; }}情况2:变量泄露,成为全局变量var s = ‘hello’;for (var i = 0; i < s.length; i++) { console.log(s[i]);}console.log(i); // 5ES6 提供 let 和 const 来代替 var 声明变量,新的声明方式支持用大括号表示的块级作用域,这会带来一些好处:1.不再需要立即执行的函数表达式(IIFE)在 ES5 中,我们需要构造一个立即执行的函数表达式去保证我们不污染全局作用域。在 ES6中, 我们可以使用更简单的大括号({}),然后使用 const 或者 let 代替 var 来达到同样的效果。2.循环体中的闭包不再有问题在 ES5 中,如果循环体内有产生一个闭包,访问闭包外的变量,会产生问题。在 ES6,你可以使用 “let” 来避免问题。3.防止重复声明变量ES6 不允许在同一个作用域内用 let 或 const 重复声明同名变量。这对于防止在不同的 js 库中存在重复声明的函数表达式十分有帮助。三、数组的扩展1. Array.from() : 将伪数组对象或可遍历对象转换为真数组如果一个对象的所有键名都是正整数或零,并且有length属性,那么这个对象就很像数组,称为伪数组。典型的伪数组有函数的arguments对象,以及大多数 DOM 元素集,还有字符串。…<button>测试1</button><br><button>测试2</button><br><button>测试3</button><br><script type=“text/javascript”>let btns = document.getElementsByTagName(“button”)console.log(“btns”,btns);//得到一个伪数组btns.forEach(item=>console.log(item)) Uncaught TypeError: btns.forEach is not a function</script>针对伪数组,没有数组一般方法,直接遍历便会出错,ES6新增Array.from()方法来提供一种明确清晰的方式以解决这方面的需求。Array.from(btns).forEach(item=>console.log(item))将伪数组转换为数组2.Array.of(v1, v2, v3) : 将一系列值转换成数组当调用 new Array( )构造器时,根据传入参数的类型与数量的不同,实际上会导致一些不同的结果, 例如:let items = new Array(2) ;console.log(items.length) ; // 2console.log(items[0]) ; // undefinedconsole.log(items[1]) ;let items = new Array(1, 2) ;console.log(items.length) ; // 2console.log(items[0]) ; // 1console.log(items[1]) ; // 2当使用单个数值参数来调用 Array 构造器时,数组的长度属性会被设置为该参数。 如果使用多个参数(无论是否为数值类型)来调用,这些参数也会成为目标数组的项。数组的这种行为既混乱又有风险,因为有时可能不会留意所传参数的类型。ES6 引入了Array.of( )方法来解决这个问题。该方法的作用非常类似Array构造器,但在使用单个数值参数的时候并不会导致特殊结果。Array.of( )方法总会创建一个包含所有传入参数的数组,而不管参数的数量与类型:let items = Array.of(1, 2);console.log(items.length); // 2console.log(items[0]); // 1console.log(items[1]); // 2items = Array.of(2);console.log(items.length); // 1console.log(items[0]); // 2Array.of基本上可以用来替代Array()或newArray(),并且不存在由于参数不同而导致的重载,而且他们的行为非常统一。3.数组实例的 find() 和 findIndex()数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。[1, 4, -5, 10].find((n) => n < 0) // -5数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。[1, 5, 10, 15].findIndex(function(value, index, arr) { return value > 9;}) // 24.数组实例的includes()Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值。该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。[1, 2, 3].includes(2) // true[1, 2, 3].includes(3, -1); // true[1, 2, 3, 5, 1].includes(1, 2); // true没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值。indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。[NaN].indexOf(NaN) // -1[NaN].includes(NaN) // true5.数组实例的 entries(),keys() 和 values()ES6 提供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"四、箭头函数ES6 允许使用“箭头”(=>)定义函数。它主要有两个作用:缩减代码和改变this指向,接下来我们详细介绍:1. 缩减代码const double1 = function(number){ return number * 2; //ES5写法}const double2 = (number) => { return number * 2; //ES6写法}const double4 = number => number * 2; //可以进一步简化多个参数记得加括号 const double6 = (number,number2) => number + number2;如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。 const double = (number,number2) => { sum = number + number2 return sum; }由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。// 报错let getTempItem = id => { id: id, name: “Temp” };// 不报let getTempItem = id => ({ id: id, name: “Temp” });此外还有个好处就是简化回调函数// 正常函数写法[1,2,3].map(function (x) { return x * x;});// 箭头函数写法[1,2,3].map(x => x * x);//[1, 4, 9]2. 改变this指向长期以来,JavaScript 语言的this对象一直是一个令人头痛的问题,在对象方法中使用this,必须非常小心。箭头函数”绑定”this,很大程度上解决了这个困扰。我们不妨先看一个例子:const team = { members:[“Henry”,“Elyse”], teamName:“es6”, teamSummary:function(){ return this.members.map(function(member){ return ${member}隶属于${this.teamName}小组; // this不知道该指向谁了 }) }}console.log(team.teamSummary());//[“Henry隶属于undefined小组”, “Elyse隶属于undefined小组”]teamSummary函数里面又嵌了个函数,这导致内部的this的指向发生了错乱。那如何修改:方法一、let self = thisconst team = { members:[“Henry”,“Elyse”], teamName:“es6”, teamSummary:function(){ let self = this; return this.members.map(function(member){ return ${member}隶属于${self.teamName}小组; }) }}console.log(team.teamSummary());//[“Henry隶属于es6小组”, “Elyse隶属于es6小组”]方法二、bind函数const team = { members:[“Henry”,“Elyse”], teamName:“es6”, teamSummary:function(){ return this.members.map(function(member){ // this不知道该指向谁了 return ${member}隶属于${this.teamName}小组; }.bind(this)) }}console.log(team.teamSummary());//[“Henry隶属于es6小组”, “Elyse隶属于es6小组”]方法三、 箭头函数const team = { members:[“Henry”,“Elyse”], teamName:“es6”, teamSummary:function(){ return this.members.map((member) => { // this指向的就是team对象 return ${member}隶属于${this.teamName}小组; }) }}console.log(team.teamSummary());//[“Henry隶属于es6小组”, “Elyse隶属于es6小组”]3.使用注意点(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。五、rest 参数ES6 引入 rest 参数(形式为…变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。我们举个例子:如何实现一个求和函数?传统写法:function addNumbers(a,b,c,d,e){ var numbers = [a,b,c,d,e]; return numbers.reduce((sum,number) => { return sum + number; },0) } console.log(addNumbers(1,2,3,4,5));//15ES6写法: function addNumbers(…numbers){ return numbers.reduce((sum,number) => { return sum + number; },0) } console.log(addNumbers(1,2,3,4,5));//15也可以与解构赋值组合使用var array = [1,2,3,4,5,6];var [a,b,…c] = array;console.log(a);//1console.log(b);//2console.log(c);//[3, 4, 5, 6]rest 参数还可以与箭头函数结合const numbers = (…nums) => nums;numbers(1, 2, 3, 4, 5)// [1,2,3,4,5] 注意:①每个函数最多只能声明一个rest参数,而且 rest参数必须是最后一个参数,否则报错。②rest参数不能用于对象字面量setter之中let object = { set name(…value){ //报错 //执行一些逻辑 }}六、展开运算符与剩余参数关联最密切的就是扩展运算符。剩余参数允许你把多个独立的参数合并到一个数组中;而扩展运算符则允许将一个数组分割,并将各个项作为分离的参数传给函数。当用在字符串或数组前面时称为扩展运算符,个人觉得可以理解为rest参数的逆运算,用于将数组或字符串进行拆解。有些时候,函数不允许传入数组,此时使用展开运算符就很方便,不信的话,咱们看个例子:Math.max()方法,它接受任意数量的参数,并会返回其中的最大值。let value1 = 25, let value2 = 50;console.log(Math.max(value1, value2)); // 50但若想处理数组中的值,此时该如何找到最大值?Math.max()方法并不允许你传入一个数组。其实你可以像使用rest参数那样在该数组前添加…,并直接将其传递给 Math.max()let values = [25,50,75, 100]//等价于console.log(Math.max(25,50,75,100));console.log(Math.max(…values)); //100扩展运算符还可以与其他参数混用let values = [-25,-50,-75,-100]console.log(Math.max(…values,0)); //0扩展运算符拆解字符串与数组var array = [1,2,3,4,5];console.log(…array);//1 2 3 4 5var str = “String”;console.log(…str);//S t r i n g还可以实现拼接var defaultColors = [“red”,“greed”];var favoriteColors = [“orange”,“yellow”];var fallColors = [“fire red”,“fall orange”];console.log([“blue”,“green”,…fallColors,…defaultColors,…favoriteColors]//[“blue”, “green”, “fire red”, “fall orange”, “red”, “greed”, “orange”, “yellow”]七、解构赋值—-更方便的数据访问ES6 新增了解构,这是将一个数据结构分解为更小的部分的过程。1.解构为何有用?在ES5及更早版本中,从对象或数组中获取信息、并将特定数据存入本地变量,需要书写许多并且相似的代码。例如: var expense = { type: “es6”, amount:“45” }; var type = expense.type; var amount = expense.amount; console.log(type,amount);此代码提取了expense对象的type与amount值,并将其存在同名的本地变量上。虽然 这段代码看起来简单,但想象一下若有大量变量需要处理,你就必须逐个为其赋值;并且若有一个嵌套的数据结构需要遍历以寻找信息,你可能会为了一点数据而挖掘整个结构。这就是ES6为何要给对象与数组添加解构。当把数据结构分解为更小的部分时,从中提取你要的数据会变得容易许多。2.对象上个例子中如果采用对象解构的方法,就很容易获取expense对象的type与amount值。const { type,amount } = expense;console.log(type,amount);我们再来看个例子:let node = {type:“Identifier”, name:“foo”}, type = “Literal”,name = 5;({type,name}= node);// 使用解构来分配不同的值 console.log(type); // “Identifier” console.log(name); // “foo"注意:你必须用圆括号包裹解构赋值语句,这是因为暴露的花括号会被解析为代码块语句,而块语句不允许在赋值操作符(即等号)左侧出现。圆括号标示了里面的花括号并不是块语句、而应该被解释为表达式,从而允许完成赋值操作。默认值:可以选择性地定义一个默认值,以便在指定属性不存在时使用该值。若要这么做,需要在 属性名后面添加一个等号并指定默认值,就像这样:let node = { type: “Identifier”, name: “foo”};let { type, name, value = true} = node;console.log(type); // “Identifier” console.log(name); // “foo” console.log(value); // true嵌套对象解构:使用类似于对象字面量的语法,可以深入到嵌套的对象结构中去提取你想要的数据。let node = { type: “Identifier”, name: “foo”, loc: { start: { line: 1, column: 1 }, end: { line: 1, column: 4 } }};let { loc: { start }} = node;console.log(start.line); // 1 console.log(start.column); // 1本例中的解构模式使用了花括号,表示应当下行到node对象的loc属性内部去寻找start属性。必须传值的解构参数function setCookie(name, value, { secure, path, domain, expires}) { // 设置cookie的代码 } setCookie(“type”, “js”);//报错在此函数内,name与value参数是必需的,而secure、path、domain与expires则不是。默认情况下调用函数时未给参数解构传值会抛出错误。像上例中如果setCookie不传第三个参数,就会报错。若解构参数是可选的,可以给解构的参数提供默认值来处理这种错误。function setCookie(name, value, { secure, path, domain, expires} = {}) {}setCookie(“type”, “js”);//不会报错3.数组const names = [“Henry”,“Bucky”,“Emily”];const [name1,name2,name3] = names;console.log(name1,name2,name3);//Henry Bucky Emilyconst [name,…rest] = names;//结合展开运算符console.log(rest);//[“Bucky”, “Emily”]用{}解构返回数组个数const {length} = names;console.log(length);//3数组解构也可以用于赋值上下文,但不需要用小括号包裹表达式。这点跟对象解构的约定不同。let colors = [“red”, “green”, “blue”], firstColor = “black”, secondColor = “purple”;[firstColor, secondColor] = colors;console.log(firstColor); // “red” console.log(secondColor); // “green"默认值:数组解构赋值同样允许在数组任意位置指定默认值。当指定位置的项不存在、或其值为undefined,那么该默认值就会被使用。let colors = [“red”];let [firstColor, secondColor = “green”] = colors;console.log(firstColor); // “red” console.log(secondColor);// “green"与rest参数搭配在ES5中常常使用concat()方法来克隆数组,例如://在ES5中克隆数组 var colors = [“red”, “green”, “blue”];var clonedColors = colors.concat();console.log(clonedColors); //"[red,green,blue]“在ES6中,你可以使用剩余项的语法来达到同样效果//在ES6中克隆数组 let colors = [“red”, “green”, “blue”];let […clonedColors] = colors;console.log(clonedColors); //[red,green,blue]接下我们看个例子:如何将数组转化为对象const points = [ [4,5], [10,1], [0,40]];//期望得到的数据格式如下,如何实现?// [// {x:4,y:5},// {x:10,y:1},// {x:0,y:40}// ]let newPoints = points.map(pair => { const [x,y] = pair; return {x,y}})//还可以通过以下办法,更为简便let newPoints = points.map(([x,y]) => { return {x,y}})console.log(newPoints);混合解构const people = [ {name:“Henry”,age:20}, {name:“Bucky”,age:25}, {name:“Emily”,age:30}];//es5 写法 var age = people[0].age;console.log(age);//es6 解构const [age] = people;console.log(age);//第一次解构数组 {name:“Henry”,age:20}const [{age}] = people;//再一次解构对象console.log(age);//204.注意点当使用解构来配合var、let、const来声明变量时,必须提供初始化程序(即等号右边的值)。下面的代码都会因为缺失初始化程序而抛出语法错误:var { type, name }; // 语法错误! let { type, name }; // 语法错误!const { type, name }; // 语法错误!八、模板字符串(template string)模板字符串是增强版的字符串,用反引号()标识。**它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。模板字符串中嵌入变量和函数,需要将变量名写在${}之中。**let name = "Henry";function makeUppercase(word){ return word.toUpperCase();}let template = <h1>${makeUppercase(‘Hello’)}, ${name}!</h1>//可以存放函数和变量 <p>感谢大家收看我们的视频, ES6为我们提供了很多遍历好用的方法和语法!</p> <ul> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> </ul> ;document.getElementById('template').innerHTML = template;再举个例子,工作中常用到ElementUI库,在自定义一个弹出框时,使用模板字符串就很方便: await this.$alert( <p><strong>确认是否升级${ this.lectureName }</strong><br>(若已存在讲义套件,升级后请重新生成)</p>`, { dangerouslyUseHTMLString: true } )九、Class 和传统构造函数有何区别从概念上讲,在 ES6 之前的 JS 中并没有和其他面向对象语言那样的“类”的概念。长时间里,人们把使用 new 关键字通过函数(也叫构造器)构造对象当做“类”来使用。由于 JS 不支持原生的类,而只是通过原型来模拟,各种模拟类的方式相对于传统的面向对象方式来说非常混乱,尤其是处理当子类继承父类、子类要调用父类的方法等等需求时。ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。但是类只是基于原型的面向对象模式的语法糖。对比在传统构造函数和 ES6 中分别如何实现类://传统构造函数function MathHandle(x,y){ this.x=x; this.y=y;}MathHandle.prototype.add =function(){ return this.x+this.y;};var m=new MathHandle(1,2);console.log(m.add())//class语法class MathHandle { constructor(x,y){ this.x=x; this.y=y;} add(){ return this.x+this.y; }}const m=new MathHandle(1,2);console.log(m.add())这两者有什么联系?其实这两者本质是一样的,只不过是语法糖写法上有区别。所谓语法糖是指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。比如这里class语法糖让程序更加简洁,有更高的可读性。typeof MathHandle //“function"MathHandle===MathHandle.prototype.constructor //true对比在传统构造函数和 ES6 中分别如何实现继承://传统构造函数继承function Animal() { this.eat = function () { alert(‘Animal eat’) }}function Dog() { this.bark = function () { alert(‘Dog bark’) }}Dog.prototype = new Animal()// 绑定原型,实现继承var hashiqi = new Dog()hashiqi.bark()//Dog barkhashiqi.eat()//Animal eat//ES6继承class Animal { constructor(name) { this.name = name } eat() { alert(this.name + ’ eat’) }}class Dog extends Animal { constructor(name) { super(name) // 有extend就必须要有super,它代表父类的构造函数,即Animal中的constructor this.name = name } say() { alert(this.name + ’ say’) }}const dog = new Dog(‘哈士奇’)dog.say()//哈士奇 saydog.eat()//哈士奇 eatClass之间可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。Class 和传统构造函数有何区别Class 在语法上更加贴合面向对象的写法Class 实现继承更加易读、易理解,对初学者更加友好本质还是语法糖,使用prototype十、Promise的基本使用和原理在JavaScript的世界中,所有代码都是单线程执行的。由于这个“缺陷”,导致JavaScript的所有网络操作,浏览器事件,都必须是异步执行。Promise 是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大。ES6中的promise的出现给我们很好的解决了回调地狱的问题,所谓的回调地狱是指当太多的异步步骤需要一步一步执行,或者一个函数里有太多的异步操作,这时候就会产生大量嵌套的回调,使代码嵌套太深而难以阅读和维护。ES6认识到了这点问题,现在promise的使用,完美解决了这个问题。Promise原理一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。promise 对象初始化状态为 pending ;当调用resolve(成功),会由pending => fulfilled ;当调用reject(失败),会由pending => rejected。具体流程见下图:Promise的使用流程new Promise一个实例,而且要 returnnew Promise 时要传入函数,函数有resolve reject 两个参数成功时执行 resolve,失败时执行rejectthen 监听结果function loadImg(src){ const promise=new Promise(function(resolve,reject){ var img=document.createElement(‘img’) img.onload=function(){ resolve(img) } img.onerror=function(){ reject() } img.src=src }) return promise//返回一个promise实例}var src=“http://www.imooc.com/static/img/index/logo_new.png"var result=loadImg(src)result.then(function(img){ console.log(img.width)//resolved(成功)时候的回调函数},function(){ console.log(“failed”)//rejected(失败)时候的回调函数})result.then(function(img){ console.log(img.height)})promise会让代码变得更容易维护,像写同步代码一样写异步代码,同时业务逻辑也更易懂。十一、Iterator 和 for…of 循环JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就需要一种统一的接口机制,来处理所有不同的数据结构。遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。1.Iterator的作用:为各种数据结构,提供一个统一的、简便的访问接口;使得数据结构的成员能够按某种次序排列ES6创造了一种新的遍历命令for…of循环,Iterator接口主要供for…of消费。2.原生具备iterator接口的数据(可用for of遍历)Arrayset容器map容器String函数的 arguments 对象NodeList 对象let arr3 = [1, 2, ‘kobe’, true];for(let i of arr3){ console.log(i); // 1 2 kobe true}let str = ‘abcd’;for(let item of str){ console.log(item); // a b c d} var engines = new Set([“Gecko”, “Trident”, “Webkit”, “Webkit”]);for (var e of engines) { console.log(e);}// Gecko// Trident// Webkit 3.几种遍历方式比较for of 循环不仅支持数组、大多数伪数组对象,也支持字符串遍历,此外还支持 Map 和 Set 对象遍历。for in循环可以遍历字符串、对象、数组,不能遍历Set/MapforEach 循环不能遍历字符串、对象,可以遍历Set/Map十二、ES6模块化ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。/** 定义模块 math.js /var basicNum = 0;var add = function (a, b) { return a + b;};export { basicNum, add };/ 引用模块 **/import { basicNum, add } from ‘./math’;function test(ele) { ele.textContent = add(99 + basicNum);}如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。// export-default.jsexport default function () { console.log(‘foo’);}上面代码是一个模块文件export-default.js,它的默认输出是一个函数。其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。// import-default.jsimport customName from ‘./export-default’;customName(); // ‘foo’上面代码的import命令,可以用任意名称指向export-default.js输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时import命令后面,不使用大括号。 如果觉得文章对你有些许帮助,欢迎在我的GitHub博客点赞和关注,感激不尽!参考文章ES6笔记(一):ES6所改良的javascript“缺陷”在 ES6 中 改良的 5 个 JavaScript “缺陷”ECMAScript 6 入门深入理解ES6ES6的rest参数和扩展运算符 ...

November 26, 2018 · 6 min · jiezi

[源码阅读]通过react-infinite-scroller理解滚动加载要点

react-infinite-scroller就是一个组件,主要逻辑就是addEventListener绑定scroll事件。看它的源码主要意义不在知道如何使用它,而是知道以后处理滚动加载要注意的东西。此处跳到总结。初识参数:// 渲染出来的DOM元素nameelement: ‘div’,// 是否能继续滚动渲染hasMore: false,// 是否在订阅事件的时候执行事件initialLoad: true,// 表示当前翻页的值(每渲染一次递增)pageStart: 0,// 传递ref,返回此组件渲染的 DOMref: null,// 触发渲染的距离threshold: 250,// 是否在window上绑定和处理距离useWindow: true,// 是否反向滚动,即到顶端后渲染isReverse: false,// 是否使用捕获模式useCapture: false,// 渲染前的loading组件loader: null,// 自定义滚动组件的父元素getScrollParent: null,深入componentDidMountcomponentDidMount() { this.pageLoaded = this.props.pageStart; this.attachScrollListener();}执行attachScrollListenerattachScrollListenerattachScrollListener() { const parentElement = this.getParentElement(this.scrollComponent); if (!this.props.hasMore || !parentElement) { return; } let scrollEl = window; if (this.props.useWindow === false) { scrollEl = parentElement; } scrollEl.addEventListener( ‘mousewheel’, this.mousewheelListener, this.props.useCapture, ); scrollEl.addEventListener( ‘scroll’, this.scrollListener, this.props.useCapture, ); scrollEl.addEventListener( ‘resize’, this.scrollListener, this.props.useCapture, ); if (this.props.initialLoad) { this.scrollListener(); }}此处通过getParentElement获取父组件(用户自定义父组件或者当前dom的parentNode)然后绑定了3个事件,分别是scroll,resize,mousewheel前2种都绑定scrollListener,mousewheel是一个非标准事件,是不建议在生产模式中使用的。那么这里为什么要使用呢?mousewheel解决chrome的等待bug此处的mousewheel事件是为了处理chrome浏览器的一个特性(不知道是否是一种bug)。stackoverflow:Chrome的滚动等待问题上面这个问题主要描述,当在使用滚轮加载,而且加载会触发ajax请求的时候,当滚轮到达底部,会出现一个漫长而且无任何动作的等待(长达2-3s)。window.addEventListener(“mousewheel”, (e) => { if (e.deltaY === 1) { e.preventDefault() }})以上绑定可以消除这个"bug"。个人并没有遇到过这种情况,不知道是否有遇到过可以说说解决方案。getParentElementgetParentElement(el) { const scrollParent = this.props.getScrollParent && this.props.getScrollParent(); if (scrollParent != null) { return scrollParent; } return el && el.parentNode;}上面用到了getParentElement,很好理解,使用用户自定义的父组件,或者当前组件DOM.parentNode。scrollListenerscrollListener() { const el = this.scrollComponent; const scrollEl = window; const parentNode = this.getParentElement(el); let offset; // 使用window的情况 if (this.props.useWindow) { const doc = document.documentElement || document.body.parentNode || document.body; const scrollTop = scrollEl.pageYOffset !== undefined ? scrollEl.pageYOffset : doc.scrollTop; // isReverse指 滚动到顶端,load新组件 if (this.props.isReverse) { // 相反模式获取到顶端距离 offset = scrollTop; } else { // 正常模式则获取到底端距离 offset = this.calculateOffset(el, scrollTop); } // 不使用window的情况 } else if (this.props.isReverse) { // 相反模式组件到顶端的距离 offset = parentNode.scrollTop; } else { // 正常模式组件到底端的距离 offset = el.scrollHeight - parentNode.scrollTop - parentNode.clientHeight; } // 此处应该要判断确保滚动组件正常显示 if ( offset < Number(this.props.threshold) && (el && el.offsetParent !== null) ) { // 卸载事件 this.detachScrollListener(); // 卸载事件后再执行 loadMore if (typeof this.props.loadMore === ‘function’) { this.props.loadMore((this.pageLoaded += 1)); } }}组件核心。几个学习/复习点offsetParentoffsetParent返回一个指向最近的包含该元素的定位元素.offsetParent很有用,因为计算offsetTop和offsetLeft都是相对于offsetParent边界的。ele.offsetParent为 null 的3种情况:ele 为bodyele 的position为fixedele 的display为none此组件中offsetParent处理了2种情况在useWindow的情况下(即事件绑定在window,滚动作用在body)通过递归获取offsetParent到达顶端的高度(offsetTop)。calculateTopPosition(el) { if (!el) { return 0; } return el.offsetTop + this.calculateTopPosition(el.offsetParent); }通过判断offsetParent不为null的情况,确保滚动组件正常显示 if ( offset < Number(this.props.threshold) && (el && el.offsetParent !== null) ) {/* … / }scrollHeight和clientHeight在无滚动的情况下,scrollHeight和clientHeight相等,都为height+padding2在有滚动的情况下,scrollHeight表示实际内容高度,clientHeight表示视口高度。每次执行loadMore前卸载事件。确保不会重复(过多)执行loadMore,因为先卸载事件再执行loadMore,可以确保在执行过程中,scroll事件是无效的,然后再每次componentDidUpdate的时候重新绑定事件。renderrender() { // 获取porps const renderProps = this.filterProps(this.props); const { children, element, hasMore, initialLoad, isReverse, loader, loadMore, pageStart, ref, threshold, useCapture, useWindow, getScrollParent, …props } = renderProps; // 定义一个ref // 能将当前组件的DOM传出去 props.ref = node => { this.scrollComponent = node; // 执行父组件传来的ref(如果有) if (ref) { ref(node); } }; const childrenArray = [children]; // 执行loader if (hasMore) { if (loader) { isReverse ? childrenArray.unshift(loader) : childrenArray.push(loader); } else if (this.defaultLoader) { isReverse ? childrenArray.unshift(this.defaultLoader) : childrenArray.push(this.defaultLoader); } } // ref 传递给 ‘div’元素 return React.createElement(element, props, childrenArray);}这里一个小亮点就是,在react中,this.props是不允许修改的。这里使用了解构getScrollParent,…props} = renderProps;这里解构相当于Object.assign,定义了一个新的object,便可以添加属性了,并且this.props不会受到影响。总结react-infinite-scroller逻辑比较简单。一些注意/学习/复习点:Chrome的一个滚动加载请求的bug。本文位置offsetParent的一些实际用法。本文位置通过不断订阅和取消事件绑定让滚动执行函数不会频繁触发。本文位置scrollHeight和clientHeight区别。本文位置此库建议使用在自定义的一些组件上并且不那么复杂的逻辑上。用在第三方库可以会无法获取正确的父组件,而通过document.getElementBy..传入。面对稍微复杂的逻辑,例如,一个搜索组件,订阅onChange事件并且呈现内容,搜索"a",对呈现内容滚动加载了3次,再添加搜索词"b",这时候"ab"的内容呈现是在3次之后。 ...

November 25, 2018 · 2 min · jiezi

图学ES6-2.let与const命令

November 25, 2018 · 0 min · jiezi

1.ECMAScript 6简介

November 23, 2018 · 0 min · jiezi

[译]await VS return VS return await

原文地址:await vs return vs return await作者:Jake Archibald当编写异步函数的时候,await,return,return await三者之间有一些区别,从中选取正确的方式是很重要的。我们从下面这个异步函数开始:async function waitAndMaybeReject(){ // 等待1秒钟 await new Promise(resolve => setTimeout(resolve, 1000)); // 抛一枚硬币 const isHeads = Boolean(Math.round(Math.random())); if(isHeads) return ‘yay’; throw Error(‘Boo!’);}上面的函数会等待1秒钟后返回一个promise,然后有50%的机会成功返回yay或者抛出一个error。让我们用几种稍微不同的方式使用它。直接调用async function foo() { try{ waitAndMaybeReject(); }catch(e){ return ‘caught’; }}在此处,如果调用了foo,返回的promise的状态始终都是resolved,值也永远是undefined,而且没有等待。由于我们没有await,或者return waitAndMaybeReject()的结果,所以我们无法对它做出任何反应。像这样的代码通常是错误的。Awaitingasync function foo(){ try{ await waitAndMaybeReject(); }catch(e){ return ‘caught’; }}在此处,如果调用了foo,返回的promise将始终等待1秒钟,然后结果要么状态为resolved,值为undefined,要么状态为resolved,值为"caught"。因为我们等待了waitAndMaybeReject()的返回值,所以它的rejection会被返回并且被抛出(throw),catch的代码块就会执行。但无论如何,如果waitAndMaybeReject()没有报错而是顺利执行,我们依旧无法对它的返回值做任何事情。Returningasync function foo() { try { return waitAndMaybeReject(); } catch (e) { return ‘caught’; }}在此处,如果调用了foo,返回的promise将始终等待1秒钟,然后结果要么是状态为resolved,值为"yaa",要么是状态是reject,抛出错误Error(‘Boo!’)。通过return waitAndMaybeReject()这行代码,我们直接传递了它的返回结果,所以我们的catch代码块永远不会执行。Return-awaiting如果你想在try代码块中得到带有正确返回值的resolved状态,在catch中捕获异常,那么正确的选择就是return await。async function foo() { try { return await waitAndMaybeReject(); } catch (e) { return ‘caught’; }}在此处,如果调用foo,返回的promise将始终等待1秒钟,然后结果要么是状态为resolved,值为"yay",要么是状态为resolved,值为"caught"因为我们等待了waitAndMaybeReject()的结果,所以它的异常rejecttion会被返回并且被抛出(throw),catch的代码块就会执行。如果waitAndMaybeReject()顺利执行没有报错,就返它的结果。如果对上面的内容还是觉着困惑,那么将代码拆分成两个步骤来看可能会比较好理解:async function foo() { try { // 等待 waitAndMaybeReject() 的结果来解决, // 并且将 fullfill 的值赋给 fullfilledValue: const fulfilledValue = await waitAndMaybeReject(); // 如果 waitAndMaybeReject() reject了, // 我们的代码就会抛出异常,并且进入 catch 代码块的逻辑。 // 否则,这里的代码就会继续运行下面的语句: return fulfilledValue; } catch (e) { return ‘caught’; }}Note: 在try/catch之外的代码块中执行return await是多余的(如前所述,直接return即可),甚至Eslint还专门有规则来检测这种场景,但是在try/catch代码块之内,Eslint就允许这种操作。 ...

November 23, 2018 · 1 min · jiezi

搞懂 JavsScript 异步 — 事件轮询

JavsScript 是一门单线程的编程语言,这就意味着一个时间里只能处理一件事,也就是说 JavaScript 引擎一次只能在一个线程里处理一条语句。虽然单线程简化了编程代码,因为你不必太担心并发引出的问题,这也意味着你将在阻塞主线程的情况下执行长时间的操作,如网络请求。想象一下从API请求一些数据,根据具体的情况,服务器需要一些时间来处理请求,同时阻塞主线程,使网页长时间处于无响应的状态。这就是引入异步 JavaScript 的原因。使用异步 JavaScript(如 回调函数、promise、async/await),可以不用阻塞主线程的情况下长时间执行网络请求 :)可能你知道不知道 异步 JavsScript 是如何工作,并不要紧,但知道它是如何工作,对 JavaScript 异步更深入的了解是有帮助的。所以不在啰嗦了,我们开始吧 :)同步JavaScript是如何工作的?在深入研究异步JavaScript之前,让我们首先了解同步 JavaScript 代码如何在 JavaScript 引擎中执行。例如: const second = () => { console.log(‘Hello there!’); } const first = () => { console.log(‘Hi there!’); second(); console.log(‘The End’); } first();要理解上述代码如何在 JavaScript 引擎中执行,我们必须理解执行上下文和调用堆栈(也称为执行堆栈)的概念。函数代码在函数执行上下文中执行,全局代码在全局执行上下文中执行。每个函数都有自己的执行上下文。调用栈调用堆栈顾名思义是一个具有LIFO(后进先出)结构的堆栈,用于存储在代码执行期间创建的所有执行上下文。JavaScript 只有一个调用栈,因为它是一种单线程编程语言。调用堆栈具有 LIFO 结构,这意味着项目只能从堆栈顶部添加或删除。让我们回到上面的代码片段,并尝试理解代码如何在JavaScript引擎中执行。const second = () => { console.log(‘Hello there!’);}const first = () => { console.log(‘Hi there!’); second(); console.log(‘The End’);}first();这里发生了什么?当执行此代码时,将创建一个全局执行上下文(由main()表示)并将其推到调用堆栈的顶部。当遇到对first()的调用时,它会被推送到堆栈的顶部。接下来,console.log(‘Hi there!’)被推送到堆栈的顶部,当它完成时,它会从堆栈中弹出。之后,我们调用second(),因此second()函数被推到堆栈的顶部。console.log(‘Hello there!’)被推送到堆栈顶部,并在完成时弹出堆栈。second() 函数结束,因此它从堆栈中弹出。console.log(“the End”)被推到堆栈的顶部,并在完成时删除。之后,first()函数完成,因此从堆栈中删除它。程序在这一点上完成了它的执行,所以全局执行上下文(main())从堆栈中弹出。异步JavaScript是如何工作的?现在我们已经对调用堆栈和同步JavaScript的工作原理有了基本的了解,让我们回到异步JavaScript。阻塞是什么?让我们假设我们正在以同步的方式进行图像处理或网络请求。例如:const processImage = (image) => { /** * doing some operations on image / console.log(‘Image processed’);}const networkRequest = (url) => { / * requesting network resource **/ return someData;}const greeting = () => { console.log(‘Hello World’);}processImage(logo.jpg);networkRequest(‘www.somerandomurl.com’);greeting();做图像处理和网络请求需要时间,当processImage()函数被调用时,它会根据图像的大小花费一些时间。processImage() 函数完成后,将从堆栈中删除它。然后调用 networkRequest() 函数并将其推入堆栈。同样,它也需要一些时间来完成执行。最后,当networkRequest()函数完成时,调用greeting()函数,因为它只包含一个控制台。日志语句和控制台。日志语句通常很快,因此greeting()函数立即执行并返回。因此,我们必须等待函数(如processImage()或networkRequest())完成。这意味着这些函数阻塞了调用堆栈或主线程。因此,在执行上述代码时,我们不能执行任何其他操作,这是不理想的。那么解决办法是什么呢?最简单的解决方案是异步回调。我们使用异步回调使代码非阻塞。例如:const networkRequest = () => { setTimeout(() => { console.log(‘Async Code’); }, 2000);};console.log(‘Hello World’);networkRequest();这里我使用了setTimeout方法来模拟网络请求。请记住setTimeout不是JavaScript引擎的一部分,它是web api(在浏览器中)和C/ c++ api(在node.js中)的一部分。为了理解这段代码是如何执行的,我们必须理解更多的概念,比如事件轮询和回调队列(或消息队列)。事件轮询、web api和消息队列不是JavaScript引擎的一部分,而是浏览器的JavaScript运行时环境或Nodejs JavaScript运行时环境的一部分(对于Nodejs)。在Nodejs中,web api被c/c++ api所替代。现在让我们回到上面的代码,看看它是如何异步执行的。const networkRequest = () => { setTimeout(() => { console.log(‘Async Code’); }, 2000);};console.log(‘Hello World’);networkRequest();console.log(‘The End’);当上述代码在浏览器中加载时,console.log(’ Hello World ‘) 被推送到堆栈中,并在完成后弹出堆栈。接下来,将遇到对 networkRequest() 的调用,因此将它推到堆栈的顶部。下一个 setTimeout() 函数被调用,因此它被推到堆栈的顶部。setTimeout()有两个参数:1) 回调和2) 以毫秒(ms)为单位的时间。setTimeout() 方法在web api环境中启动一个2s的计时器。此时,setTimeout()已经完成,并从堆栈中弹出。cosole.log(“the end”) 被推送到堆栈中,在完成后执行并从堆栈中删除。同时,计时器已经过期,现在回调被推送到消息队列。但是回调不会立即执行,这就是事件轮询开始的地方。事件轮询事件轮询的工作是监听调用堆栈,并确定调用堆栈是否为空。如果调用堆栈是空的,它将检查消息队列,看看是否有任何挂起的回调等待执行。在这种情况下,消息队列包含一个回调,此时调用堆栈为空。因此,事件轮询将回调推到堆栈的顶部。然后是 console.log(“Async Code”) 被推送到堆栈顶部,执行并从堆栈中弹出。此时,回调已经完成,因此从堆栈中删除它,程序最终完成。消息队列还包含来自DOM事件(如单击事件和键盘事件)的回调。例如:document.querySelector(’.btn’).addEventListener(‘click’,(event) => { console.log(‘Button Clicked’);});对于DOM事件,事件侦听器位于web api环境中,等待某个事件(在本例中单击event)发生,当该事件发生时,回调函数被放置在等待执行的消息队列中。同样,事件轮询检查调用堆栈是否为空,并在调用堆栈为空并执行回调时将事件回调推送到堆栈。延迟函数执行我们还可以使用setTimeout来延迟函数的执行,直到堆栈清空为止。例如const bar = () => { console.log(‘bar’);}const baz = () => { console.log(‘baz’);}const foo = () => { console.log(‘foo’); setTimeout(bar, 0); baz();}foo();打印结果:foobazbar当这段代码运行时,第一个函数foo()被调用,在foo内部我们调用console.log(‘foo’),然后setTimeout()被调用,bar()作为回调函数和时0秒计时器。现在,如果我们没有使用 setTimeout, bar() 函数将立即执行,但是使用 setTimeout 和0秒计时器,将bar的执行延迟到堆栈为空的时候。0秒后,bar()回调被放入等待执行的消息队列中。但是它只会在堆栈完全空的时候执行,也就是在baz和foo函数完成之后。ES6 任务队列我们已经了解了异步回调和DOM事件是如何执行的,它们使用消息队列存储等待执行所有回调。ES6引入了任务队列的概念,任务队列是 JavaScript 中的 promise 所使用的。消息队列和任务队列的区别在于,任务队列的优先级高于消息队列,这意味着任务队列中的promise 作业将在消息队列中的回调之前执行,例如:const bar = () => { console.log(‘bar’);};const baz = () => { console.log(‘baz’);};const foo = () => { console.log(‘foo’); setTimeout(bar, 0); new Promise((resolve, reject) => { resolve(‘Promise resolved’); }).then(res => console.log(res)) .catch(err => console.log(err)); baz();};foo();打印结果:foobazPromised resolvedbar我们可以看到 promise 在 setTimeout 之前执行,因为 promise 响应存储在任务队列中,任务队列的优先级高于消息队列。小结因此,我们了解了异步 JavaScript 是如何工作的,以及调用堆栈、事件循环、消息队列和任务队列等概念,这些概念共同构成了 JavaScript 运行时环境。虽然成为一名出色的JavaScript开发人员并不需要学习所有这些概念,但是了解这些概念是有帮助的:)参考:Understanding Asynchronous JavaScript — the Event Loop一个笨笨的码农,我的世界只能终身学习! ...

November 23, 2018 · 2 min · jiezi

ES6 系列之私有变量的实现

前言在阅读 《ECMAScript 6 入门》的时候,零散的看到有私有变量的实现,所以在此总结一篇。1. 约定实现class Example { constructor() { this._private = ‘private’; } getName() { return this._private }}var ex = new Example();console.log(ex.getName()); // privateconsole.log(ex._private); // private优点写法简单调试方便兼容性好缺点外部可以访问和修改语言没有配合的机制,如 for in 语句会将所有属性枚举出来命名冲突2. 闭包实现一/** * 实现一 /class Example { constructor() { var _private = ‘’; _private = ‘private’; this.getName = function() {return _private} }}var ex = new Example();console.log(ex.getName()); // privateconsole.log(ex._private); // undefined优点无命名冲突外部无法访问和修改缺点constructor 的逻辑变得复杂。构造函数应该只做对象初始化的事情,现在为了实现私有变量,必须包含部分方法的实现,代码组织上略不清晰。方法存在于实例,而非原型上,子类也无法使用 super 调用构建增加一点点开销实现二/* * 实现二 /const Example = (function() { var _private = ‘’; class Example { constructor() { _private = ‘private’; } getName() { return _private; } } return Example;})();var ex = new Example();console.log(ex.getName()); // privateconsole.log(ex._private); // undefined优点无命名冲突外部无法访问和修改缺点写法有一点复杂构建增加一点点开销3. Symbol实现const Example = (function() { var _private = Symbol(‘private’); class Example { constructor() { this[_private] = ‘private’; } getName() { return this[_private]; } } return Example;})();var ex = new Example();console.log(ex.getName()); // privateconsole.log(ex.name); // undefined优点无命名冲突外部无法访问和修改无性能损失缺点写法稍微复杂兼容性也还好4. WeakMap实现/* * 实现一 /const _private = new WeakMap();class Example { constructor() { _private.set(this, ‘private’); } getName() { return _private.get(this); }}var ex = new Example();console.log(ex.getName()); // privateconsole.log(ex.name); // undefined如果这样写,你可能觉得封装性不够,你也可以这样写:/* * 实现二 */const Example = (function() { var _private = new WeakMap(); // 私有成员存储容器 class Example { constructor() { _private.set(this, ‘private’); } getName() { return _private.get(this); } } return Example;})();var ex = new Example();console.log(ex.getName()); // privateconsole.log(ex.name); // undefined优点无命名冲突外部无法访问和修改缺点写法比较麻烦兼容性有点问题有一定性能代价5. 最新提案class Point { #x; #y; constructor(x, y) { this.#x = x; this.#y = y; } equals(point) { return this.#x === point.#x && this.#y === point.#y; }}那么为什么不直接使用 private 字段呢?比如说这样:class Foo { private value; equals(foo) { return this.value === foo.value; }}简单点来说,就是嫌麻烦,当然也有性能上的考虑……举个例子,如果我们不使用 #,而是使用 private 关键字:class Foo { private value = ‘1’; equals(foo) { return this.value === foo.value; }}var foo1 = new Foo();var foo2 = new Foo();console.log(foo1.equals(foo2));在这里我们新建了两个实例,然后将 foo2 作为参数传入了 foo1 的实例方法中。那么我们可以获取 foo2.value 的值吗?如果我们直接 foo2.value 肯定是获取不到值的,毕竟是私有变量,可是 equals 是 Foo 的一个类方法,那么可以获取到的吗?答案是可以的。其实这点在其他语言,比如说 Java 和 C++ 中也是一样的,类的成员函数中可以访问同类型实例的私有变量,这是因为私有是为了实现“对外”的信息隐藏,在类自己内部,没有必要禁止私有变量的访问,你也可以理解为私有变量的限制是以类为单位,而不是以对象为单位,此外这样做也可以为使用者带来便利。既然获取值是可以的,那么打印的结果应该为 true,但是如果我们传入的值不是 Foo 的实例,而是一个其他对象呢?var foo1 = new Foo();console.log(foo1.equals({ value: 2}));当然这里代码也是可以正常运行的,但是对于编译器来说,就有一点麻烦了,因为编译器不知道 value 到底是 foo 的正常属性还是私有属性,所以编译器需要做判断,先判断 foo 是不是 Foo 的实例,然后再接着获取值。这也意味着每次属性访问都需要做这样一个判断,而引擎已经围绕属性访问做了高度优化,懒得改,而且还降低速度。不过除了这个工作之外,还会有一些其他的内容需要考虑,比如说:你必须将私有的 key 编码进每个词法环境for in 可以遍历这些属性吗?私有属性和正常属性同名的时候,谁会屏蔽谁?怎么防止私有属性的名称不被探测出来。关于使用 # 而不使用 private 更多的讨论可以参考这个 Issue。当然这些问题都可以被解决啦,就是麻烦了点。而如果你选择 #,实现的方式将跟 JavaScript 对象属性完全没有关系,将会使用 private slots 的方式以及使用一个新的 slot 查找语法,总之就是会比 private 的实现方式简单很多。参考《编程语言如何演化——以JS的private为例》贺师俊Exploring ES6译 JS 新语法:私有属性ES6 系列ES6 系列目录地址:https://github.com/mqyqingfeng/BlogES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ...

November 21, 2018 · 2 min · jiezi

程序员练级攻略(2018):前端基础和底层原理

这个是我订阅 陈皓老师在极客上的专栏《左耳听风》,我整理出来是为了自己方便学习,同时也分享给你们一起学习,当然如果有兴趣,可以去订阅,为了避免广告嫌疑,我这就不多说了!以下第一人称是指陈皓老师。对于前端的学习和提高,我的基本思路是这样的。首先,前端的三个最基本的东西 HTML5、CSS3 和 JavaScript(ES6)是必须要学好的。这其中有很多很多的技术,比如,CSS3 引申出来的 Canvas(位图)、SVG(矢量图) 和 WebGL(3D 图),以及 CSS 的各种图形变换可以让你做出非常丰富的渲染效果和动画效果。ES6 简直就是把 JavaScript 带到了一个新的台阶,JavaScript 语言的强大,大大释放了前端开发人员的生产力,让前端得以开发更为复杂的代码和程序,于是像 React 和 Vue 这样的框架开始成为前端编程的不二之选。我一直认为学习任何知识都要从基础出发,所以我会有很大的篇幅在讲各种技术的基础知识和基本原理,尤其是如下的这些知识,都是前端程序员需要一块一块啃掉的硬骨头。JavaScript 的核心原理。这里我会给出好些网上很不错的讲 JavaScript 的原理的文章或图书,你一定要学好语言的特性和其中的各种坑。浏览器的工作原理。这也是一块硬骨头,我觉得这是前端程序员需要了解和明白的东西,不然,你将无法深入下去。网络协议 HTTP。也是要着重了解的,尤其是 HTTP/2,还有 HTTP 的几种请求方式:短连接、长连接、Stream 连接、WebSocket 连接。前端性能调优。有了以上的这些基础后,你就可以进入前端性能调优的主题了,我相信你可以很容易上手各种性能调优技术的。框架学习。我只给了 React 和 Vue 两个框架。就这两个框架来说,Virtual DOM 技术是其底层技术,组件化是其思想,管理组件的状态是其重点。而对于 React 来说,函数式编程又是其编程思想,所以,这些基础技术都是你需要好好研究和学习的。UI 设计。设计也是前端需要做的一个事,比如像 Google 的 Material UI,或是比较流行的 Atomic Design 等应该是前端工程师需要学习的。而对于工具类的东西,这里我基本没怎么涉及,因为本文主要还是从原理和基础入手。那些工具我觉得都很简单,就像学习 Java 我没有让你去学习 Maven 一样,因为只要你去动手了,这种知识你自然就会获得,我们还是把精力重点放在更重要的地方。下面我们从前端基础和底层原理开始讲起。先来讲讲 HTML5 相关的内容。HTML5HTML5 权威指南 ,本书面向初学者和中等水平 Web 开发人员,是牢固掌握 HTML5、CSS3 和 JavaScript 的必读之作。书看起来比较厚,是因为里面的代码很多。HTML5 Canvas 核心技术 ,如果你要做 HTML5 游戏的话,这本书必读。对于 SVG、Canvas 和 WebGL 这三个对应于矢量图、位图和 3D 图的渲染来说,给前端开发带来了重武器,很多 HTML5 小游戏也因此蓬勃发展。所以,你可以学习一下。学习这三个技术,我个人觉得最好的地方是 MDN。SVG: Scalable Vector GraphicsCanvas APIThe WebGL API: 2D and 3D graphics for the web最后是几个资源列表。Awesome HTML5 。GitHub 上的 Awesome HTML5,其中有大量的资源和技术文章。Awesome SVGAwesome CanvasAwesome WebGLCSS在《程序员练级攻略(2018)》系列文章最开始,我们就推荐过 CSS 的在线学习文档,这里再推荐一下 MDN Web Doc - CSS 。我个人觉得只要你仔细读一下文档,CSS 并不难学。绝大多数觉得难的,一方面是文档没读透,另一方面是浏览器支持的标准不一致。所以,学好 CSS 最关键的还是要仔细地读文档。之后,在写 CSS 的时候,你会发现,你的 CSS 中有很多看起来相似的东西。你的 DRY - Don’t Repeat Yourself 洁癖告诉你,这是不对的。所以,你需要学会使用 LESS 和 SaSS 这两个 CSS 预处理工具,其可以帮你提高很多效率。然后,你需要学习一下 CSS 的书写规范,前面的《程序员修养》一文中提到过一些,这里再补充几个。Principles of writing consistent, idiomatic CSSOpinionated CSS styleguide for scalable applicationsGoogle HTML/CSS Style Guide如果你需要更有效率,那么你还需要使用一些 CSS Framework,其中最著名的就是 Twitter 公司的 Bootstrap,其有很多不错的 UI 组件,页面布局方案,可以让你非常方便也非常快速地开发页面。除此之外,还有,主打清新 UI 的 Semantic UI 、主打响应式界面的 Foundation 和基于 Flexbox 的 Bulma。当然,在使用 CSS 之前,你需要把你浏览器中的一些 HTML 标签给标准化掉。所以,推荐几个 Reset 或标准化的 CSS 库:Normalize 、MiniRest.css、 sanitize.css 和 unstyle.css。关于更多的 CSS 框架,你可以参看 Awesome CSS Frameworks接下来,是几个公司的 CSS 相关实践,供你参考。CodePen’s CSSGithub 的 CSSMedium’s CSS is actually pretty fing goodCSS at BBC SportRefining The Way We Structure Our CSS At Trello最后是一个可以写出可扩展的 CSS 的阅读列表 A Scalable CSS Reading ListJavaScript下面是学习 JavaScript 的一些图书和文章。JavaScript: The Good Parts ,中文翻译版为《JavaScript 语言精粹》。这是一本介绍 JavaScript 语言本质的权威图书,值得任何正在或准备从事 JavaScript 开发的人阅读,并且需要反复阅读。学习、理解、实践大师的思想,我们才可能站在巨人的肩上,才有机会超越大师,这本书就是开始。Secrets of the JavaScript Ninja ,中文翻译版为《JavaScript 忍者秘籍》,本书是 jQuery 库创始人编写的一本深入剖析 JavaScript 语言的书。适合具备一定 JavaScript 基础知识的读者阅读,也适合从事程序设计工作并想要深入探索 JavaScript 语言的读者阅读。这本书有很多晦涩难懂的地方,需要仔细阅读,反复琢磨。Effective JavaScript ,Ecma 的 JavaScript 标准化委员会著名专家撰写,作者凭借多年标准化委员会工作和实践经验,深刻辨析 JavaScript 的内部运作机制、特性、陷阱和编程最佳实践,将它们高度浓缩为极具实践指导意义的 68 条精华建议。接下来是 ES6 的学习,这里给三个学习手册源。ES6 in Depth,InfoQ 上有相关的中文版 - ES6 深入浅出。还可以看看 A simple interactive ES6 Feature list ,或是看一下 阮一峰翻译的 ES6 的教程。ECMAScript 6 Tools ,这是一堆 ES6 工具的列表,可以帮助你提高开发效率。Modern JS Cheatsheet ,这个 Cheatsheet 在 GitHub 上有 1 万 6 千颗星,你就可见其影响力了。然后,还有一组很不错的《You Don’t Know JS 系列》 的书。You Don’t Know JS: “Up & Going”You Don’t Know JS: “Scope & Closures”You Don’t Know JS: “this & Object Prototypes”You Don’t Know JS: “ES6 & Beyond”接下来是一些和编程范式相关的文章。Glossary of Modern JavaScript Concepts: Part 1 ,首先推荐这篇文章,其中收集了一些编程范式方面的内容,比如纯函数、状态、可变性和不可变性、指令型语言和声明式语言、函数式编程、响应式编程、函数式响应编程。Glossary of Modern JavaScript Concepts: Part 2 ,在第二部分中主要讨论了作用域和闭包,数据流,变更检测,组件化……下面三篇文章是德米特里·索什尼科夫(Dmitry Soshnikov)个人网站上三篇讲 JavaScript 内在的文章。JavaScript. The Core: 2nd EditionJavaScript. The Core (older ES3 version)JS scope: static, dynamic, and runtime-augmentedHow JavaScript Works” 是一组非常不错的文章(可能还没有写完),强烈推荐。这一系列的文章是 SessionStake 的 CEO 写的,现在有 13 篇,我感觉可能还没有写完。这个叫 亚历山大·兹拉特科夫(Alexander Zlatkov) 的 CEO 太猛了。An overview of the engine, the runtime, and the call stackInside the V8 engine + 5 tips on how to write optimized code ,了解 V8 引擎。这里,也推荐 Understanding V8’s Bytecode 这篇文章可以让你了解 V8 引擎的底层字节码。Memory management + how to handle 4 common memory leaks ,内存管理和 4 种常见的内存泄露问题。Event loop and the rise of Async programming + 5 ways to better coding with async/await ,Event Loop 和异步编程。Deep dive into WebSockets and HTTP/2 with SSE + how to pick the right path ,WebSocket 和 HTTP/2。A comparison with WebAssembly + why in certain cases it’s better to use it over JavaScript ,JavaScript 内在原理。The building blocks of Web Workers + 5 cases when you should use them ,Web Workers 技术。Service Workers, their lifecycle and use cases ,Service Worker 技术。The mechanics of Web Push Notifications ,Web 端 Push 通知技术。Tracking changes in the DOM using MutationObserver ,Mutation Observer 技术。The rendering engine and tips to optimize its performance ,渲染引擎和性能优化。Inside the Networking Layer + How to Optimize Its Performance and Security ,网络性能和安全相关。Under the hood of CSS and JS animations + how to optimize their performance ,CSS 和 JavaScript 动画性能优化。接下来是 Google Chrome 工程经理 阿迪·奥斯马尼(Addy Osmani) 的几篇 JavaScript 性能相关的文章,也是非常好的。The Cost Of JavaScriptJavaScript Start-up Performance其它与 JavaScript 相关的资源。JavScript has Unicode Problem ,这是一篇很有价值的 JavaScript 处理 Unicode 的文章。JavaScript Algorithms ,用 JavaScript 实现的各种基础算法库。JavaScript 30 秒代码 ,一堆你可以在 30 秒内看懂各种有用的 JavaScript 的代码,在 GitHub 上有 2 万颗星了。What the fck JavaScript ,一堆 JavaScript 搞笑和比较 tricky 的样例。Airbnb JavaScript Style Guide ,Airbnb 的 JavaScript 的代码规范,GitHub 上有 7 万多颗星。JavaScript Patterns for 2017 ,YouTube 上的一个 JavaScript 模式分享,值得一看。浏览器原理你需要了解一下浏览器是怎么工作的,所以,你必需要看《How browsers work》。这篇文章受众之大,后来被人重新整理并发布为《How Browsers Work: Behind the scenes of modern web browsers》,其中还包括中文版。这篇文章非常非常长,所以,你要有耐心看完。如果你想看个精简版的,可以看我在 Coolshell 上发的《浏览器的渲染原理简介》或是看一下这个幻灯片。然后,是对 Virtual DOM 的学习。Virtual DOM 是 React 的一个非常核心的技术细节,它也是前端渲染和性能的关键技术。所以,你有必要要好好学习一下这个技术的实现原理和算法。当然,前提条件是你需要学习过前面我所推荐过的浏览器的工作原理。下面是一些不错的文章可以帮你学习这一技术。How to write your own Virtual DOMWrite your Virtual DOM 2: Props & EventsHow Virtual-DOM and diffing works in ReactThe Inner Workings Of Virtual DOM深度剖析:如何实现一个 Virtual DOM 算法以及两个 Vitual-DOM 实现供你参考:Matt-Esch/Virtual-DOMMaquette网络协议High Performance Browser Networking ,本书是谷歌公司高性能团队核心成员的权威之作,堪称实战经验与规范解读完美结合的产物。本书目标是涵盖 Web 开发者技术体系中应该掌握的所有网络及性能优化知识。全书以性能优化为主线,从 TCP、UDP 和 TLS 协议讲起,解释了如何针对这几种协议和基础设施来优化应用。然后深入探讨了无线和移动网络的工作机制。最后,揭示了 HTTP 协议的底层细节,同时详细介绍了 HTTP 2.0、 XHR、SSE、WebSocket、WebRTC 和 DataChannel 等现代浏览器新增的能力。另外,HTTP/2也是 HTTP 的一个新的协议,于 2015 年被批准通过,现在基本上所有的主流浏览器都默认启用这个协议。所以,你有必要学习一下这个协议。下面相关的学习资源。Gitbook - HTTP/2 详解http2 explained(中译版)HTTP/2 for a Faster WebHTTP/2 的两个 RFC:RFC 7540 - Hypertext Transfer Protocol Version 2 (HTTP/2) ,HTTP/2 的协议本身。RFC 7541 - HPACK: Header Compression for HTTP/2RFC 7541 - HPACK: Header Compression for HTTP/2新的 HTML5 支持 WebSocket,所以,这也是你要学的一个重要协议。HTML5 WebSocket: A Quantum Leap in Scalability for the Web ,这篇文章比较了 HTTP 的几种链接方式,Polling、Long Polling 和 Streaming,并引入了终级解决方案 WebSocket。你知道的,了解一个技术的缘由是非常重要的。StackOverflow: My Understanding of HTTP Polling, Long Polling, HTTP Streaming and WebSockets ,这是 StackOverflow 上的一个 HTTP 各种链接方式的比较,也可以让你有所认识。An introduction to Websockets ,一个 WebSocket 的简单教程。Awesome Websockets ,GitHub 的 Awesome 资源列表。一些和 WebSocket 相关的想法,可以开阔你的思路:Introducing WebSockets: Bringing Sockets to the WebWebsockets 101Real-Time Web by Paul BanksAre WebSockets the future?小结总结一下今天的内容。我一直认为学习任何知识都要从基础出发,所以今天我主要讲述了 HTML5、CSS3 和 JavaScript(ES6)这三大基础核心,给出了大量的图书、文章以及其他一些相关的学习资源。之后,我建议你学习浏览器的工作原理和网络协议相关的内容。我认为,掌握这些原理也是学好前端知识的前提和基础。值得花时间,好好学习消化。你们的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习! ...

November 20, 2018 · 4 min · jiezi

koa,express,node 通用方法连接MySQL

这个教程不管node,express,koa都可以用下面方法连接,这里用koa做个参考新建文件目录,我是这样子的很多教程都没有涉及到版本,所以让很多初学者,拷贝他的代码,出现错误问题我的版本: “dependencies”: { “koa”: “^2.6.2”, “mysql”: “^2.16.0” }1.设置配置文件// default.js// 设置配置文件const config = { // 启动端口 port: 3000, // 数据库配置 database: { DATABASE: ‘ceshi’, USERNAME: ‘root’, PASSWORD: ‘1234’, PORT: ‘3306’, HOST: ’localhost’ } } module.exports = config2.连接数据库// mysql/index.jsvar mysql = require(‘mysql’);var config = require(’../config/default.js’)var pool = mysql.createPool({ host : config.database.HOST, user : config.database.USERNAME, password : config.database.PASSWORD, database : config.database.DATABASE});class Mysql { constructor () { } query () { return new Promise((resolve, reject) => { pool.query(‘SELECT * from ceshidata’, function (error, results, fields) { if (error) { throw error }; resolve(results) // console.log(‘The solution is: ‘, results[0].solution); }); }) }}module.exports = new Mysql()3.设置服务器// index.jsconst Koa = require(‘koa’)const config = require(’./config/default’)const mysql = require(’./mysql’)const app = new Koa()app.use(async (ctx) => { let data = await mysql.query() ctx.body = { “code”: 1, “data”: data, “mesg”: ‘ok’ } })app.listen(config.port)console.log(listening on port ${config.port})4.启动服务器,去浏览器访问先去数据库添加点数据node index.js打开浏览器localhost:3000, 然后你就会看到以下数据,自己添加的数据查询出来了然后其他相关操作,可以看mysql相关API,我下次也会分享出来首发于微信公众号:node前端不妨关注一下,我们一起学习回复:100有福利哦 ...

November 15, 2018 · 1 min · jiezi

ES6 系列之我们来聊聊装饰器

Decorator装饰器主要用于:装饰类装饰方法或属性装饰类@annotationclass MyClass { }function annotation(target) { target.annotated = true;}装饰方法或属性class MyClass { @readonly method() { }}function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}Babel安装编译我们可以在 Babel 官网的 Try it out,查看 Babel 编译后的代码。不过我们也可以选择本地编译:npm initnpm install –save-dev @babel/core @babel/clinpm install –save-dev @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties新建 .babelrc 文件{ “plugins”: [ ["@babel/plugin-proposal-decorators", { “legacy”: true }], ["@babel/plugin-proposal-class-properties", {“loose”: true}] ]}再编译指定的文件babel decorator.js –out-file decorator-compiled.js装饰类的编译编译前:@annotationclass MyClass { }function annotation(target) { target.annotated = true;}编译后:var _class;let MyClass = annotation(_class = class MyClass {}) || _class;function annotation(target) { target.annotated = true;}我们可以看到对于类的装饰,其原理就是:@decoratorclass A {}// 等同于class A {}A = decorator(A) || A;装饰方法的编译编译前:class MyClass { @unenumerable @readonly method() { }}function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}function unenumerable(target, name, descriptor) { descriptor.enumerable = false; return descriptor;}编译后:var _class;function _applyDecoratedDescriptor(target, property, decorators, descriptor, context ) { /** * 第一部分 * 拷贝属性 / var desc = {}; Object“ke” + “ys”.forEach(function(key) { desc[key] = descriptor[key]; }); desc.enumerable = !!desc.enumerable; desc.configurable = !!desc.configurable; if (“value” in desc || desc.initializer) { desc.writable = true; } /* * 第二部分 * 应用多个 decorators / desc = decorators .slice() .reverse() .reduce(function(desc, decorator) { return decorator(target, property, desc) || desc; }, desc); /* * 第三部分 * 设置要 decorators 的属性 / if (context && desc.initializer !== void 0) { desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined; } if (desc.initializer === void 0) { Object[“define” + “Property”](target, property, desc); desc = null; } return desc;}let MyClass = ((_class = class MyClass { method() {}}),_applyDecoratedDescriptor( _class.prototype, “method”, [readonly], Object.getOwnPropertyDescriptor(_class.prototype, “method”), _class.prototype),_class);function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}装饰方法的编译源码解析我们可以看到 Babel 构建了一个 _applyDecoratedDescriptor 函数,用于给方法装饰。Object.getOwnPropertyDescriptor()在传入参数的时候,我们使用了一个 Object.getOwnPropertyDescriptor() 方法,我们来看下这个方法:Object.getOwnPropertyDescriptor() 方法返回指定对象上的一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)顺便注意这是一个 ES5 的方法。举个例子:const foo = { value: 1 };const bar = Object.getOwnPropertyDescriptor(foo, “value”);// bar {// value: 1,// writable: true// enumerable: true,// configurable: true,// }const foo = { get value() { return 1; } };const bar = Object.getOwnPropertyDescriptor(foo, “value”);// bar {// get: /the getter function/,// set: undefined// enumerable: true,// configurable: true,// }第一部分源码解析在 _applyDecoratedDescriptor 函数内部,我们首先将 Object.getOwnPropertyDescriptor() 返回的属性描述符对象做了一份拷贝:// 拷贝一份 descriptorvar desc = {};Object“ke” + “ys”.forEach(function(key) { desc[key] = descriptor[key];});desc.enumerable = !!desc.enumerable;desc.configurable = !!desc.configurable;// 如果没有 value 属性或者没有 initializer 属性,表明是 getter 和 setterif (“value” in desc || desc.initializer) { desc.writable = true;}那么 initializer 属性是什么呢?Object.getOwnPropertyDescriptor() 返回的对象并不具有这个属性呀,确实,这是 Babel 的 Class 为了与 decorator 配合而产生的一个属性,比如说对于下面这种代码:class MyClass { @readonly born = Date.now();}function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}var foo = new MyClass();console.log(foo.born);Babel 就会编译为:// …(_descriptor = _applyDecoratedDescriptor(_class.prototype, “born”, [readonly], { configurable: true, enumerable: true, writable: true, initializer: function() { return Date.now(); }}))// …此时传入 _applyDecoratedDescriptor 函数的 descriptor 就具有 initializer 属性。第二部分源码解析接下是应用多个 decorators:/* * 第二部分 * @type {[type]} /desc = decorators .slice() .reverse() .reduce(function(desc, decorator) { return decorator(target, property, desc) || desc; }, desc);对于一个方法应用了多个 decorator,比如:class MyClass { @unenumerable @readonly method() { }}Babel 会编译为:_applyDecoratedDescriptor( _class.prototype, “method”, [unenumerable, readonly], Object.getOwnPropertyDescriptor(_class.prototype, “method”), _class.prototype)在第二部分的源码中,执行了 reverse() 和 reduce() 操作,由此我们也可以发现,如果同一个方法有多个装饰器,会由内向外执行。第三部分源码解析/* * 第三部分 * 设置要 decorators 的属性 /if (context && desc.initializer !== void 0) { desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined;}if (desc.initializer === void 0) { Object[“define” + “Property”](target, property, desc); desc = null;}return desc;如果 desc 有 initializer 属性,意味着当装饰的是类的属性时,会将 value 的值设置为:desc.initializer.call(context)而 context 的值为 _class.prototype,之所以要 call(context),这也很好理解,因为有可能class MyClass { @readonly value = this.getNum() + 1; getNum() { return 1; }}最后无论是装饰方法还是属性,都会执行:Object[“define” + “Property”](target, property, desc);由此可见,装饰方法本质上还是使用 Object.defineProperty() 来实现的。应用1.log为一个方法添加 log 函数,检查输入的参数:class Math { @log add(a, b) { return a + b; }}function log(target, name, descriptor) { var oldValue = descriptor.value; descriptor.value = function(…args) { console.log(Calling ${name} with, args); return oldValue.apply(this, args); }; return descriptor;}const math = new Math();// Calling add with [2, 4]math.add(2, 4);再完善点:let log = (type) => { return (target, name, descriptor) => { const method = descriptor.value; descriptor.value = (…args) => { console.info((${type}) 正在执行: ${name}(${args}) = ?); let ret; try { ret = method.apply(target, args); console.info((${type}) 成功 : ${name}(${args}) =&gt; ${ret}); } catch (error) { console.error((${type}) 失败: ${name}(${args}) =&gt; ${error}); } return ret; } }};2.autobindclass Person { @autobind getPerson() { return this; }}let person = new Person();let { getPerson } = person;getPerson() === person;// true我们很容易想到的一个场景是 React 绑定事件的时候:class Toggle extends React.Component { @autobind handleClick() { console.log(this) } render() { return ( <button onClick={this.handleClick}> button </button> ); }}我们来写这样一个 autobind 函数:const { defineProperty, getPrototypeOf} = Object;function bind(fn, context) { if (fn.bind) { return fn.bind(context); } else { return function autobind() { return fn.apply(context, arguments); }; }}function createDefaultSetter(key) { return function set(newValue) { Object.defineProperty(this, key, { configurable: true, writable: true, enumerable: true, value: newValue }); return newValue; };}function autobind(target, key, { value: fn, configurable, enumerable }) { if (typeof fn !== ‘function’) { throw new SyntaxError(@autobind can only be used on functions, not: ${fn}); } const { constructor } = target; return { configurable, enumerable, get() { /* * 使用这种方式相当于替换了这个函数,所以当比如 * Class.prototype.hasOwnProperty(key) 的时候,为了正确返回 * 所以这里做了 this 的判断 */ if (this === target) { return fn; } const boundFn = bind(fn, this); defineProperty(this, key, { configurable: true, writable: true, enumerable: false, value: boundFn }); return boundFn; }, set: createDefaultSetter(key) };}3.debounce有的时候,我们需要对执行的方法进行防抖处理:class Toggle extends React.Component { @debounce(500, true) handleClick() { console.log(’toggle’) } render() { return ( <button onClick={this.handleClick}> button </button> ); }}我们来实现一下:function _debounce(func, wait, immediate) { var timeout; return function () { var context = this; var args = arguments; if (timeout) clearTimeout(timeout); if (immediate) { var callNow = !timeout; timeout = setTimeout(function(){ timeout = null; }, wait) if (callNow) func.apply(context, args) } else { timeout = setTimeout(function(){ func.apply(context, args) }, wait); } }}function debounce(wait, immediate) { return function handleDescriptor(target, key, descriptor) { const callback = descriptor.value; if (typeof callback !== ‘function’) { throw new SyntaxError(‘Only functions can be debounced’); } var fn = _debounce(callback, wait, immediate) return { …descriptor, value() { fn() } }; }}4.time用于统计方法执行的时间:function time(prefix) { let count = 0; return function handleDescriptor(target, key, descriptor) { const fn = descriptor.value; if (prefix == null) { prefix = ${target.constructor.name}.${key}; } if (typeof fn !== ‘function’) { throw new SyntaxError(@time can only be used on functions, not: ${fn}); } return { …descriptor, value() { const label = ${prefix}-${count}; count++; console.time(label); try { return fn.apply(this, arguments); } finally { console.timeEnd(label); } } } }}5.mixin用于将对象的方法混入 Class 中:const SingerMixin = { sing(sound) { alert(sound); }};const FlyMixin = { // All types of property descriptors are supported get speed() {}, fly() {}, land() {}};@mixin(SingerMixin, FlyMixin)class Bird { singMatingCall() { this.sing(’tweet tweet’); }}var bird = new Bird();bird.singMatingCall();// alerts “tweet tweet"mixin 的一个简单实现如下:function mixin(…mixins) { return target => { if (!mixins.length) { throw new SyntaxError(@mixin() class ${target.name} requires at least one mixin as an argument); } for (let i = 0, l = mixins.length; i < l; i++) { const descs = Object.getOwnPropertyDescriptors(mixins[i]); const keys = Object.getOwnPropertyNames(descs); for (let j = 0, k = keys.length; j < k; j++) { const key = keys[j]; if (!target.prototype.hasOwnProperty(key)) { Object.defineProperty(target.prototype, key, descs[key]); } } } };}6.redux实际开发中,React 与 Redux 库结合使用时,常常需要写成下面这样。class MyReactComponent extends React.Component {}export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);有了装饰器,就可以改写上面的代码。@connect(mapStateToProps, mapDispatchToProps)export default class MyReactComponent extends React.Component {};相对来说,后一种写法看上去更容易理解。7.注意以上我们都是用于修饰类方法,我们获取值的方式为:const method = descriptor.value;但是如果我们修饰的是类的实例属性,因为 Babel 的缘故,通过 value 属性并不能获取值,我们可以写成:const value = descriptor.initializer && descriptor.initializer();参考ECMAScript 6 入门core-decoratorsES7 Decorator 装饰者模式JS 装饰器(Decorator)场景实战ES6 系列ES6 系列目录地址:https://github.com/mqyqingfeng/BlogES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ...

November 15, 2018 · 6 min · jiezi

ES6 系列之模块加载方案

前言本篇我们重点介绍以下四种模块加载规范:AMDCMDCommonJSES6 模块最后再延伸讲下 Babel 的编译和 webpack 的打包原理。require.js在了解 AMD 规范之前,我们先来看看 require.js 的使用方式。项目目录为:* project/ * index.html * vender/ * main.js * require.js * add.js * square.js * multiply.jsindex.html 的内容如下:<!DOCTYPE html><html> <head> <title>require.js</title> </head> <body> <h1>Content</h1> <script data-main=“vender/main” src=“vender/require.js”></script> </body></html>data-main=“vender/main” 表示主模块是 vender 下的 main.js。main.js 的配置如下:// main.jsrequire([’./add’, ‘./square’], function(addModule, squareModule) { console.log(addModule.add(1, 1)) console.log(squareModule.square(3))});require 的第一个参数表示依赖的模块的路径,第二个参数表示此模块的内容。由此可以看出,主模块依赖 add 模块和 square 模块。我们看下 add 模块即 add.js 的内容:// add.jsdefine(function() { console.log(‘加载了 add 模块’); var add = function(x, y) { return x + y; }; return { add: add };});requirejs 为全局添加了 define 函数,你只要按照这种约定的方式书写这个模块即可。那如果依赖的模块又依赖了其他模块呢?我们来看看主模块依赖的 square 模块, square 模块的作用是求出一个数字的平方,比如输入 3 就返回 9,该模块依赖一个乘法模块,该乘法模块即 multiply.js 的代码如下:// multiply.jsdefine(function() { console.log(‘加载了 multiply 模块’) var multiply = function(x, y) { return x * y; }; return { multiply: multiply };});而 square 模块就要用到 multiply 模块,其实写法跟 main.js 添加依赖模块一样:// square.jsdefine([’./multiply’], function(multiplyModule) { console.log(‘加载了 square 模块’) return { square: function(num) { return multiplyModule.multiply(num, num) } };});require.js 会自动分析依赖关系,将需要加载的模块正确加载。requirejs 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/requirejs而如果我们在浏览器中打开 index.html,打印的顺序为:加载了 add 模块加载了 multiply 模块加载了 square 模块29AMD在上节,我们说了这样一句话:requirejs 为全局添加了 define 函数,你只要按照这种约定的方式书写这个模块即可。那这个约定的书写方式是指什么呢?指的便是 The Asynchronous Module Definition (AMD) 规范。所以其实 AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。你去看 AMD 规范) 的内容,其主要内容就是定义了 define 函数该如何书写,只要你按照这个规范书写模块和依赖,require.js 就能正确的进行解析。sea.js在国内,经常与 AMD 被一起提起的还有 CMD,CMD 又是什么呢?我们从 sea.js 的使用开始说起。文件目录与 requirejs 项目目录相同:* project/ * index.html * vender/ * main.js * require.js * add.js * square.js * multiply.jsindex.html 的内容如下:<!DOCTYPE html><html><head> <title>sea.js</title></head><body> <h1>Content</h1> <script src=“vender/sea.js”></script> <script> // 在页面中加载主模块 seajs.use("./vender/main"); </script></body></html>main.js 的内容如下:// main.jsdefine(function(require, exports, module) { var addModule = require(’./add’); console.log(addModule.add(1, 1)) var squareModule = require(’./square’); console.log(squareModule.square(3))});add.js 的内容如下:// add.jsdefine(function(require, exports, module) { console.log(‘加载了 add 模块’) var add = function(x, y) { return x + y; }; module.exports = { add: add };});square.js 的内容如下:define(function(require, exports, module) { console.log(‘加载了 square 模块’) var multiplyModule = require(’./multiply’); module.exports = { square: function(num) { return multiplyModule.multiply(num, num) } };});multiply.js 的内容如下:define(function(require, exports, module) { console.log(‘加载了 multiply 模块’) var multiply = function(x, y) { return x * y; }; module.exports = { multiply: multiply };});跟第一个例子是同样的依赖结构,即 main 依赖 add 和 square,square 又依赖 multiply。seajs 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/seajs而如果我们在浏览器中打开 index.html,打印的顺序为:加载了 add 模块2加载了 square 模块加载了 multiply 模块9CMD与 AMD 一样,CMD 其实就是 SeaJS 在推广过程中对模块定义的规范化产出。你去看 CMD 规范的内容,主要内容就是描述该如何定义模块,如何引入模块,如何导出模块,只要你按照这个规范书写代码,sea.js 就能正确的进行解析。AMD 与 CMD 的区别从 sea.js 和 require.js 的例子可以看出:1.CMD 推崇依赖就近,AMD 推崇依赖前置。看两个项目中的 main.js:// require.js 例子中的 main.js// 依赖必须一开始就写好require([’./add’, ‘./square’], function(addModule, squareModule) { console.log(addModule.add(1, 1)) console.log(squareModule.square(3))});// sea.js 例子中的 main.jsdefine(function(require, exports, module) { var addModule = require(’./add’); console.log(addModule.add(1, 1)) // 依赖可以就近书写 var squareModule = require(’./square’); console.log(squareModule.square(3))});2.对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。看两个项目中的打印顺序:// require.js加载了 add 模块加载了 multiply 模块加载了 square 模块29// sea.js加载了 add 模块2加载了 square 模块加载了 multiply 模块9AMD 是将需要使用的模块先加载完再执行代码,而 CMD 是在 require 的时候才去加载模块文件,加载完再接着执行。感谢感谢 require.js 和 sea.js 在推动 JavaScript 模块化发展方面做出的贡献。CommonJSAMD 和 CMD 都是用于浏览器端的模块规范,而在服务器端比如 node,采用的则是 CommonJS 规范。导出模块的方式:var add = function(x, y) { return x + y;};module.exports.add = add;引入模块的方式:var add = require(’./add.js’);console.log(add.add(1, 1));我们将之前的例子改成 CommonJS 规范:// main.jsvar add = require(’./add.js’);console.log(add.add(1, 1))var square = require(’./square.js’);console.log(square.square(3));// add.jsconsole.log(‘加载了 add 模块’)var add = function(x, y) { return x + y;};module.exports.add = add;// multiply.jsconsole.log(‘加载了 multiply 模块’)var multiply = function(x, y) { return x * y;};module.exports.multiply = multiply;// square.jsconsole.log(‘加载了 square 模块’)var multiply = require(’./multiply.js’);var square = function(num) { return multiply.multiply(num, num);};module.exports.square = square;CommonJS 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/commonJS如果我们执行 node main.js,打印的顺序为:加载了 add 模块2加载了 square 模块加载了 multiply 模块9跟 sea.js 的执行结果一致,也是在 require 的时候才去加载模块文件,加载完再接着执行。CommonJS 与 AMD引用阮一峰老师的《JavaScript 标准参考教程(alpha)》:CommonJS 规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。由于 Node.js 主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以 CommonJS 规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用 AMD 规范。ES6ECMAScript2015 规定了新的模块加载方案。导出模块的方式:var firstName = ‘Michael’;var lastName = ‘Jackson’;var year = 1958;export {firstName, lastName, year};引入模块的方式:import {firstName, lastName, year} from ‘./profile’;我们再将上面的例子改成 ES6 规范:目录结构与 requirejs 和 seajs 目录结构一致。<!DOCTYPE html><html> <head> <title>ES6</title> </head> <body> <h1>Content</h1> <script src=“vender/main.js” type=“module”></script> </body></html>注意!浏览器加载 ES6 模块,也使用 <script> 标签,但是要加入 type=“module” 属性。// main.jsimport {add} from ‘./add.js’;console.log(add(1, 1))import {square} from ‘./square.js’;console.log(square(3));// add.jsconsole.log(‘加载了 add 模块’)var add = function(x, y) { return x + y;};export {add}// multiply.jsconsole.log(‘加载了 multiply 模块’)var multiply = function(x, y) { return x * y;};export {multiply}// square.jsconsole.log(‘加载了 square 模块’)import {multiply} from ‘./multiply.js’;var square = function(num) { return multiply(num, num);};export {square}ES6-Module 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/ES6值得注意的,在 Chrome 中,如果直接打开,会报跨域错误,必须开启服务器,保证文件同源才可以有效果。为了验证这个效果你可以:cnpm install http-server -g然后进入该目录,执行http-server在浏览器打开 http://localhost:8080/ 即可查看效果。打印的顺序为:加载了 add 模块加载了 multiply 模块加载了 square 模块29跟 require.js 的执行结果是一致的,也就是将需要使用的模块先加载完再执行代码。ES6 与 CommonJS引用阮一峰老师的 《ECMAScript 6 入门》:它们有两个重大差异。CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。第二个差异可以从两个项目的打印结果看出,导致这种差别的原因是:因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。重点解释第一个差异。CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。举个例子:// 输出模块 counter.jsvar counter = 3;function incCounter() { counter++;}module.exports = { counter: counter, incCounter: incCounter,};// 引入模块 main.jsvar mod = require(’./counter’);console.log(mod.counter); // 3mod.incCounter();console.log(mod.counter); // 3counter.js 模块加载以后,它的内部变化就影响不到输出的 mod.counter 了。这是因为 mod.counter 是一个原始类型的值,会被缓存。但是如果修改 counter 为一个引用类型的话:// 输出模块 counter.jsvar counter = { value: 3};function incCounter() { counter.value++;}module.exports = { counter: counter, incCounter: incCounter,};// 引入模块 main.jsvar mod = require(’./counter.js’);console.log(mod.counter.value); // 3mod.incCounter();console.log(mod.counter.value); // 4value 是会发生改变的。不过也可以说这是 “值的拷贝”,只是对于引用类型而言,值指的其实是引用。而如果我们将这个例子改成 ES6:// counter.jsexport let counter = 3;export function incCounter() { counter++;}// main.jsimport { counter, incCounter } from ‘./counter’;console.log(counter); // 3incCounter();console.log(counter); // 4这是因为ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的 import 有点像 Unix 系统的“符号连接”,原始值变了,import 加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。Babel鉴于浏览器支持度的问题,如果要使用 ES6 的语法,一般都会借助 Babel,可对于 import 和 export 而言,只借助 Babel 就可以吗?让我们看看 Babel 是怎么编译 import 和 export 语法的。// ES6var firstName = ‘Michael’;var lastName = ‘Jackson’;var year = 1958;export {firstName, lastName, year};// Babel 编译后’use strict’;Object.defineProperty(exports, “__esModule”, { value: true});var firstName = ‘Michael’;var lastName = ‘Jackson’;var year = 1958;exports.firstName = firstName;exports.lastName = lastName;exports.year = year;是不是感觉有那么一点奇怪?编译后的语法更像是 CommonJS 规范,再看 import 的编译结果:// ES6import {firstName, lastName, year} from ‘./profile’;// Babel 编译后’use strict’;var _profile = require(’./profile’);你会发现 Babel 只是把 ES6 模块语法转为 CommonJS 模块语法,然而浏览器是不支持这种模块语法的,所以直接跑在浏览器会报错的,如果想要在浏览器中运行,还是需要使用打包工具将代码打包。webpackBabel 将 ES6 模块转为 CommonJS 后, webpack 又是怎么做的打包的呢?它该如何将这些文件打包在一起,从而能保证正确的处理依赖,以及能在浏览器中运行呢?首先为什么浏览器中不支持 CommonJS 语法呢?这是因为浏览器环境中并没有 module、 exports、 require 等环境变量。换句话说,webpack 打包后的文件之所以在浏览器中能运行,就是靠模拟了这些变量的行为。那怎么模拟呢?我们以 CommonJS 项目中的 square.js 为例,它依赖了 multiply 模块:console.log(‘加载了 square 模块’)var multiply = require(’./multiply.js’);var square = function(num) { return multiply.multiply(num, num);};module.exports.square = square;webpack 会将其包裹一层,注入这些变量:function(module, exports, require) { console.log(‘加载了 square 模块’); var multiply = require("./multiply"); module.exports = { square: function(num) { return multiply.multiply(num, num); } };}那 webpack 又会将 CommonJS 项目的代码打包成什么样呢?我写了一个精简的例子,你可以直接复制到浏览器中查看效果:// 自执行函数(function(modules) { // 用于储存已经加载过的模块 var installedModules = {}; function require(moduleName) { if (installedModules[moduleName]) { return installedModules[moduleName].exports; } var module = installedModules[moduleName] = { exports: {} }; modules[moduleName](module, module.exports, require); return module.exports; } // 加载主模块 return require(“main”);})({ “main”: function(module, exports, require) { var addModule = require("./add"); console.log(addModule.add(1, 1)) var squareModule = require("./square"); console.log(squareModule.square(3)); }, “./add”: function(module, exports, require) { console.log(‘加载了 add 模块’); module.exports = { add: function(x, y) { return x + y; } }; }, “./square”: function(module, exports, require) { console.log(‘加载了 square 模块’); var multiply = require("./multiply"); module.exports = { square: function(num) { return multiply.multiply(num, num); } }; }, “./multiply”: function(module, exports, require) { console.log(‘加载了 multiply 模块’); module.exports = { multiply: function(x, y) { return x * y; } }; }})最终的执行结果为:加载了 add 模块2加载了 square 模块加载了 multiply 模块9参考《JavaScript 标准参考教程(alpha)》《ECMAScript6 入门》手写一个CommonJS打包工具(一)ES6 系列ES6 系列目录地址:https://github.com/mqyqingfeng/BlogES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ...

November 13, 2018 · 5 min · jiezi

ES6 系列之 defineProperty 与 proxy

前言我们或多或少都听过“数据绑定”这个词,“数据绑定”的关键在于监听数据的变化,可是对于这样一个对象:var obj = {value: 1},我们该怎么知道 obj 发生了改变呢?definePropetyES5 提供了 Object.defineProperty 方法,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。语法Object.defineProperty(obj, prop, descriptor)参数obj: 要在其上定义属性的对象。prop: 要定义或修改的属性的名称。descriptor: 将被定义或修改的属性的描述符。举个例子:var obj = {};Object.defineProperty(obj, “num”, { value : 1, writable : true, enumerable : true, configurable : true});// 对象 obj 拥有属性 num,值为 1虽然我们可以直接添加属性和值,但是使用这种方式,我们能进行更多的配置。函数的第三个参数 descriptor 所表示的属性描述符有两种形式:数据描述符和存取描述符。两者均具有以下两种键值:configurable当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,也能够被删除。默认为 false。enumerable当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。数据描述符同时具有以下可选键值:value该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。writable当且仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变。默认为 false。存取描述符同时具有以下可选键值:get一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。set一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。值得注意的是:属性描述符必须是数据描述符或者存取描述符两种形式之一,不能同时是两者。这就意味着你可以:Object.defineProperty({}, “num”, { value: 1, writable: true, enumerable: true, configurable: true});也可以:var value = 1;Object.defineProperty({}, “num”, { get : function(){ return value; }, set : function(newValue){ value = newValue; }, enumerable : true, configurable : true});但是不可以:// 报错Object.defineProperty({}, “num”, { value: 1, get: function() { return 1; }});此外,所有的属性描述符都是非必须的,但是 descriptor 这个字段是必须的,如果不进行任何配置,你可以这样:var obj = Object.defineProperty({}, “num”, {});console.log(obj.num); // undefinedSetters 和 Getters之所以讲到 defineProperty,是因为我们要使用存取描述符中的 get 和 set,这两个方法又被称为 getter 和 setter。由 getter 和 setter 定义的属性称做”存取器属性“。当程序查询存取器属性的值时,JavaScript 调用 getter方法。这个方法的返回值就是属性存取表达式的值。当程序设置一个存取器属性的值时,JavaScript 调用 setter 方法,将赋值表达式右侧的值当做参数传入 setter。从某种意义上讲,这个方法负责“设置”属性值。可以忽略 setter 方法的返回值。举个例子:var obj = {}, value = null;Object.defineProperty(obj, “num”, { get: function(){ console.log(‘执行了 get 操作’) return value; }, set: function(newValue) { console.log(‘执行了 set 操作’) value = newValue; }})obj.value = 1 // 执行了 set 操作console.log(obj.value); // 执行了 get 操作 // 1这不就是我们要的监控数据改变的方法吗?我们再来封装一下:function Archiver() { var value = null; // archive n. 档案 var archive = []; Object.defineProperty(this, ’num’, { get: function() { console.log(‘执行了 get 操作’) return value; }, set: function(value) { console.log(‘执行了 set 操作’) value = value; archive.push({ val: value }); } }); this.getArchive = function() { return archive; };}var arc = new Archiver();arc.num; // 执行了 get 操作arc.num = 11; // 执行了 set 操作arc.num = 13; // 执行了 set 操作console.log(arc.getArchive()); // [{ val: 11 }, { val: 13 }]watch API既然可以监控数据的改变,那我可以这样设想,即当数据改变的时候,自动进行渲染工作。举个例子:HTML 中有个 span 标签和 button 标签<span id=“container”>1</span><button id=“button”>点击加 1</button>当点击按钮的时候,span 标签里的值加 1。传统的做法是:document.getElementById(‘button’).addEventListener(“click”, function(){ var container = document.getElementById(“container”); container.innerHTML = Number(container.innerHTML) + 1;});如果使用了 defineProperty:var obj = { value: 1}// 储存 obj.value 的值var value = 1;Object.defineProperty(obj, “value”, { get: function() { return value; }, set: function(newValue) { value = newValue; document.getElementById(‘container’).innerHTML = newValue; }});document.getElementById(‘button’).addEventListener(“click”, function() { obj.value += 1;});代码看似增多了,但是当我们需要改变 span 标签里的值的时候,直接修改 obj.value 的值就可以了。然而,现在的写法,我们还需要单独声明一个变量存储 obj.value 的值,因为如果你在 set 中直接 obj.value = newValue 就会陷入无限的循环中。此外,我们可能需要监控很多属性值的改变,要是一个一个写,也很累呐,所以我们简单写个 watch 函数。使用效果如下:var obj = { value: 1}watch(obj, “num”, function(newvalue){ document.getElementById(‘container’).innerHTML = newvalue;})document.getElementById(‘button’).addEventListener(“click”, function(){ obj.value += 1});我们来写下这个 watch 函数:(function(){ var root = this; function watch(obj, name, func){ var value = obj[name]; Object.defineProperty(obj, name, { get: function() { return value; }, set: function(newValue) { value = newValue; func(value) } }); if (value) obj[name] = value } this.watch = watch;})()现在我们已经可以监控对象属性值的改变,并且可以根据属性值的改变,添加回调函数,棒棒哒~proxy使用 defineProperty 只能重定义属性的读取(get)和设置(set)行为,到了 ES6,提供了 Proxy,可以重定义更多的行为,比如 in、delete、函数调用等更多行为。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。我们来看看它的语法:var proxy = new Proxy(target, handler);proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。var proxy = new Proxy({}, { get: function(obj, prop) { console.log(‘设置 get 操作’) return obj[prop]; }, set: function(obj, prop, value) { console.log(‘设置 set 操作’) obj[prop] = value; }});proxy.time = 35; // 设置 set 操作console.log(proxy.time); // 设置 get 操作 // 35除了 get 和 set 之外,proxy 可以拦截多达 13 种操作,比如 has(target, propKey),可以拦截 propKey in proxy 的操作,返回一个布尔值。// 使用 has 方法隐藏某些属性,不被 in 运算符发现var handler = { has (target, key) { if (key[0] === ‘_’) { return false; } return key in target; }};var target = { _prop: ‘foo’, prop: ‘foo’ };var proxy = new Proxy(target, handler);console.log(’_prop’ in proxy); // false又比如说 apply 方法拦截函数的调用、call 和 apply 操作。apply 方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组,不过这里我们简单演示一下:var target = function () { return ‘I am the target’; };var handler = { apply: function () { return ‘I am the proxy’; }};var p = new Proxy(target, handler);p();// “I am the proxy"又比如说 ownKeys 方法可以拦截对象自身属性的读取操作。具体来说,拦截以下操作:Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.keys()下面的例子是拦截第一个字符为下划线的属性名,不让它被 for of 遍历到。let target = { _bar: ‘foo’, prop: ‘bar’, prop: ‘baz’};let handler = { ownKeys (target) { return Reflect.ownKeys(target).filter(key => key[0] !== ‘’); }};let proxy = new Proxy(target, handler);for (let key of Object.keys(proxy)) { console.log(target[key]);}// “baz"更多的拦截行为可以查看阮一峰老师的 《ECMAScript 6 入门》值得注意的是,proxy 的最大问题在于浏览器支持度不够,而且很多效果无法使用 poilyfill 来弥补。watch API 优化我们使用 proxy 再来写一下 watch 函数。使用效果如下:(function() { var root = this; function watch(target, func) { var proxy = new Proxy(target, { get: function(target, prop) { return target[prop]; }, set: function(target, prop, value) { target[prop] = value; func(prop, value); } }); if(target[name]) proxy[name] = value; return proxy; } this.watch = watch;})()var obj = { value: 1}var newObj = watch(obj, function(key, newvalue) { if (key == ‘value’) document.getElementById(‘container’).innerHTML = newvalue;})document.getElementById(‘button’).addEventListener(“click”, function() { newObj.value += 1});我们也可以发现,使用 defineProperty 和 proxy 的区别,当使用 defineProperty,我们修改原来的 obj 对象就可以触发拦截,而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截。ES6 系列ES6 系列目录地址:https://github.com/mqyqingfeng/BlogES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ...

November 9, 2018 · 4 min · jiezi

ES6 系列之 Babel 是如何编译 Class 的(下)

前言在上一篇 《 ES6 系列 Babel 是如何编译 Class 的(上)》,我们知道了 Babel 是如何编译 Class 的,这篇我们学习 Babel 是如何用 ES5 实现 Class 的继承。ES5 寄生组合式继承function Parent (name) { this.name = name;}Parent.prototype.getName = function () { console.log(this.name)}function Child (name, age) { Parent.call(this, name); this.age = age;}Child.prototype = Object.create(Parent.prototype);var child1 = new Child(‘kevin’, ‘18’);console.log(child1);原型链示意图为:关于寄生组合式继承我们在 《JavaScript深入之继承的多种方式和优缺点》 中介绍过。引用《JavaScript高级程序设计》中对寄生组合式继承的夸赞就是:这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。ES6 extendClass 通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。以上 ES5 的代码对应到 ES6 就是:class Parent { constructor(name) { this.name = name; }}class Child extends Parent { constructor(name, age) { super(name); // 调用父类的 constructor(name) this.age = age; }}var child1 = new Child(‘kevin’, ‘18’);console.log(child1);值得注意的是:super 关键字表示父类的构造函数,相当于 ES5 的 Parent.call(this)。子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类就得不到 this 对象。也正是因为这个原因,在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。子类的 __proto__在 ES6 中,父类的静态方法,可以被子类继承。举个例子:class Foo { static classMethod() { return ‘hello’; }}class Bar extends Foo {}Bar.classMethod(); // ‘hello’这是因为 Class 作为构造函数的语法糖,同时有 prototype 属性和 proto 属性,因此同时存在两条继承链。(1)子类的 proto 属性,表示构造函数的继承,总是指向父类。(2)子类 prototype 属性的 proto 属性,表示方法的继承,总是指向父类的 prototype 属性。class Parent {}class Child extends Parent {}console.log(Child.proto === Parent); // trueconsole.log(Child.prototype.proto === Parent.prototype); // trueES6 的原型链示意图为:我们会发现,相比寄生组合式继承,ES6 的 class 多了一个 Object.setPrototypeOf(Child, Parent) 的步骤。继承目标extends 关键字后面可以跟多种类型的值。class B extends A {}上面代码的 A,只要是一个有 prototype 属性的函数,就能被 B 继承。由于函数都有 prototype 属性(除了 Function.prototype 函数),因此 A 可以是任意函数。除了函数之外,A 的值还可以是 null,当 extend null 的时候:class A extends null {}console.log(A.proto === Function.prototype); // trueconsole.log(A.prototype.proto === undefined); // trueBabel 编译那 ES6 的这段代码:class Parent { constructor(name) { this.name = name; }}class Child extends Parent { constructor(name, age) { super(name); // 调用父类的 constructor(name) this.age = age; }}var child1 = new Child(‘kevin’, ‘18’);console.log(child1);Babel 又是如何编译的呢?我们可以在 Babel 官网的 Try it out 中尝试:‘use strict’;function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError(“this hasn’t been initialised - super() hasn’t been called”); } return call && (typeof call === “object” || typeof call === “function”) ? call : self;}function _inherits(subClass, superClass) { if (typeof superClass !== “function” && superClass !== null) { throw new TypeError(“Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.proto = superClass;}function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(“Cannot call a class as a function”); }}var Parent = function Parent(name) { _classCallCheck(this, Parent); this.name = name;};var Child = function(_Parent) { _inherits(Child, _Parent); function Child(name, age) { _classCallCheck(this, Child); // 调用父类的 constructor(name) var _this = _possibleConstructorReturn(this, (Child.proto || Object.getPrototypeOf(Child)).call(this, name)); _this.age = age; return _this; } return Child;}(Parent);var child1 = new Child(‘kevin’, ‘18’);console.log(child1);我们可以看到 Babel 创建了 _inherits 函数帮助实现继承,又创建了 _possibleConstructorReturn 函数帮助确定调用父类构造函数的返回值,我们来细致的看一看代码。_inheritsfunction _inherits(subClass, superClass) { // extend 的继承目标必须是函数或者是 null if (typeof superClass !== “function” && superClass !== null) { throw new TypeError(“Super expression must either be null or a function, not " + typeof superClass); } // 类似于 ES5 的寄生组合式继承,使用 Object.create,设置子类 prototype 属性的 proto 属性指向父类的 prototype 属性 subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); // 设置子类的 proto 属性指向父类 if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.proto = superClass;}关于 Object.create(),一般我们用的时候会传入一个参数,其实是支持传入两个参数的,第二个参数表示要添加到新创建对象的属性,注意这里是给新创建的对象即返回值添加属性,而不是在新创建对象的原型对象上添加。举个例子:// 创建一个以另一个空对象为原型,且拥有一个属性 p 的对象const o = Object.create({}, { p: { value: 42 } });console.log(o); // {p: 42}console.log(o.p); // 42再完整一点:const o = Object.create({}, { p: { value: 42, enumerable: false, // 该属性不可写 writable: false, configurable: true }});o.p = 24;console.log(o.p); // 42那么对于这段代码:subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } });作用就是给 subClass.prototype 添加一个可配置可写不可枚举的 constructor 属性,该属性值为 subClass。_possibleConstructorReturn函数里是这样调用的:var _this = _possibleConstructorReturn(this, (Child.proto || Object.getPrototypeOf(Child)).call(this, name));我们简化为:var _this = _possibleConstructorReturn(this, Parent.call(this, name));_possibleConstructorReturn 的源码为:function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError(“this hasn’t been initialised - super() hasn’t been called”); } return call && (typeof call === “object” || typeof call === “function”) ? call : self;}在这里我们判断 Parent.call(this, name) 的返回值的类型,咦?这个值还能有很多类型?对于这样一个 class:class Parent { constructor() { this.xxx = xxx; }}Parent.call(this, name) 的值肯定是 undefined。可是如果我们在 constructor 函数中 return 了呢?比如:class Parent { constructor() { return { name: ‘kevin’ } }}我们可以返回各种类型的值,甚至是 null:class Parent { constructor() { return null }}我们接着看这个判断:call && (typeof call === “object” || typeof call === “function”) ? call : self;注意,这句话的意思并不是判断 call 是否存在,如果存在,就执行 (typeof call === “object” || typeof call === “function”) ? call : self因为 && 的运算符优先级高于 ? :,所以这句话的意思应该是:(call && (typeof call === “object” || typeof call === “function”)) ? call : self;对于 Parent.call(this) 的值,如果是 object 类型或者是 function 类型,就返回 Parent.call(this),如果是 null 或者基本类型的值或者是 undefined,都会返回 self 也就是子类的 this。这也是为什么这个函数被命名为 _possibleConstructorReturn。总结var Child = function(_Parent) { _inherits(Child, _Parent); function Child(name, age) { _classCallCheck(this, Child); // 调用父类的 constructor(name) var _this = _possibleConstructorReturn(this, (Child.proto || Object.getPrototypeOf(Child)).call(this, name)); _this.age = age; return _this; } return Child;}(Parent);最后我们总体看下如何实现继承:首先执行 _inherits(Child, Parent),建立 Child 和 Parent 的原型链关系,即 Object.setPrototypeOf(Child.prototype, Parent.prototype) 和 Object.setPrototypeOf(Child, Parent)。然后调用 Parent.call(this, name),根据 Parent 构造函数的返回值类型确定子类构造函数 this 的初始值 _this。最终,根据子类构造函数,修改 _this 的值,然后返回该值。ES6 系列ES6 系列目录地址:https://github.com/mqyqingfeng/BlogES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ...

November 8, 2018 · 4 min · jiezi

ES6 系列之 Babel 是如何编译 Class 的(上)

前言在了解 Babel 是如何编译 class 前,我们先看看 ES6 的 class 和 ES5 的构造函数是如何对应的。毕竟,ES6 的 class 可以看作一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。constructorES6 中:class Person { constructor(name) { this.name = name; } sayHello() { return ‘hello, I am ’ + this.name; }}var kevin = new Person(‘Kevin’);kevin.sayHello(); // hello, I am Kevin对应到 ES5 中就是:function Person(name) { this.name = name;}Person.prototype.sayHello = function () { return ‘hello, I am ’ + this.name;};var kevin = new Person(‘Kevin’);kevin.sayHello(); // hello, I am Kevin我们可以看到 ES5 的构造函数 Person,对应 ES6 的 Person 类的 constructor 方法。值得注意的是:类的内部所有定义的方法,都是不可枚举的(non-enumerable)以上面的例子为例,在 ES6 中:Object.keys(Person.prototype); // []Object.getOwnPropertyNames(Person.prototype); // [“constructor”, “sayHello”]然而在 ES5 中:Object.keys(Person.prototype); // [‘sayHello’]Object.getOwnPropertyNames(Person.prototype); // [“constructor”, “sayHello”]实例属性以前,我们定义实例属性,只能写在类的 constructor 方法里面。比如:class Person { constructor() { this.state = { count: 0 }; }}然而现在有一个提案,对实例属性和静态属性都规定了新的写法,而且 Babel 已经支持。现在我们可以写成:class Person { state = { count: 0 };}对应到 ES5 都是:function Person() { this.state = { count: 0 };}静态方法所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。ES6 中:class Person { static sayHello() { return ‘hello’; }}Person.sayHello() // ‘hello’var kevin = new Person();kevin.sayHello(); // TypeError: kevin.sayHello is not a function对应 ES5:function Person() {}Person.sayHello = function() { return ‘hello’;};Person.sayHello(); // ‘hello’var kevin = new Person();kevin.sayHello(); // TypeError: kevin.sayHello is not a function静态属性静态属性指的是 Class 本身的属性,即 Class.propName,而不是定义在实例对象(this)上的属性。以前,我们添加静态属性只可以这样:class Person {}Person.name = ‘kevin’;因为上面提到的提案,现在可以写成:class Person { static name = ‘kevin’;}对应到 ES5 都是:function Person() {};Person.name = ‘kevin’;new 调用值得注意的是:类必须使用 new 调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用 new 也可以执行。class Person {}Person(); // TypeError: Class constructor Foo cannot be invoked without ’new’getter 和 setter与 ES5 一样,在“类”的内部可以使用 get 和 set 关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。class Person { get name() { return ‘kevin’; } set name(newName) { console.log(’new name 为:’ + newName) }}let person = new Person();person.name = ‘daisy’;// new name 为:daisyconsole.log(person.name);// kevin对应到 ES5 中:function Person(name) {}Person.prototype = { get name() { return ‘kevin’; }, set name(newName) { console.log(’new name 为:’ + newName) }}let person = new Person();person.name = ‘daisy’;// new name 为:daisyconsole.log(person.name);// kevinBabel 编译至此,我们已经知道了有关“类”的方法中,ES6 与 ES5 是如何对应的,实际上 Babel 在编译时并不会直接就转成这种形式,Babel 会自己生成一些辅助函数,帮助实现 ES6 的特性。我们可以在 Babel 官网的 Try it out 页面查看 ES6 的代码编译成什么样子。编译(一)ES6 代码为:class Person { constructor(name) { this.name = name; }}Babel 编译为:“use strict”;function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(“Cannot call a class as a function”); }}var Person = function Person(name) { _classCallCheck(this, Person); this.name = name;};_classCallCheck 的作用是检查 Person 是否是通过 new 的方式调用,在上面,我们也说过,类必须使用 new 调用,否则会报错。当我们使用 var person = Person() 的形式调用的时候,this 指向 window,所以 instance instanceof Constructor 就会为 false,与 ES6 的要求一致。编译(二)ES6 代码为:class Person { // 实例属性 foo = ‘foo’; // 静态属性 static bar = ‘bar’; constructor(name) { this.name = name; }}Babel 编译为:‘use strict’;function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(“Cannot call a class as a function”); }}var Person = function Person(name) { _classCallCheck(this, Person); this.foo = ‘foo’; this.name = name;};Person.bar = ‘bar’;编译(三)ES6 代码为:class Person { constructor(name) { this.name = name; } sayHello() { return ‘hello, I am ’ + this.name; } static onlySayHello() { return ‘hello’ } get name() { return ‘kevin’; } set name(newName) { console.log(’new name 为:’ + newName) }}对应到 ES5 的代码应该是:function Person(name) { this.name = name;}Person.prototype = { sayHello: function () { return ‘hello, I am ’ + this.name; }, get name() { return ‘kevin’; }, set name(newName) { console.log(’new name 为:’ + newName) }}Person.onlySayHello = function () { return ‘hello’};Babel 编译后为:‘use strict’;var _createClass = function() { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (“value” in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function(Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; };}();function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(“Cannot call a class as a function”); }}var Person = function() { function Person(name) { _classCallCheck(this, Person); this.name = name; } _createClass(Person, [{ key: ‘sayHello’, value: function sayHello() { return ‘hello, I am ’ + this.name; } }, { key: ’name’, get: function get() { return ‘kevin’; }, set: function set(newName) { console.log(’new name 为:’ + newName); } }], [{ key: ‘onlySayHello’, value: function onlySayHello() { return ‘hello’; } }]); return Person;}();我们可以看到 Babel 生成了一个 _createClass 辅助函数,该函数传入三个参数,第一个是构造函数,在这个例子中也就是 Person,第二个是要添加到原型上的函数数组,第三个是要添加到构造函数本身的函数数组,也就是所有添加 static 关键字的函数。该函数的作用就是将函数数组中的方法添加到构造函数或者构造函数的原型中,最后返回这个构造函数。在其中,又生成了一个 defineProperties 辅助函数,使用 Object.defineProperty 方法添加属性。默认 enumerable 为 false,configurable 为 true,这个在上面也有强调过,是为了防止 Object.keys() 之类的方法遍历到。然后通过判断 value 是否存在,来判断是否是 getter 和 setter。如果存在 value,就为 descriptor 添加 value 和 writable 属性,如果不存在,就直接使用 get 和 set 属性。写在后面至此,我们已经了解了 Babel 是如何编译一个 Class 的,然而,Class 还有一个重要的特性就是继承,Class 如何继承,Babel 又该如何编译,欢迎期待下一篇《 ES6 系列之 Babel 是如何编译 Class 的(下)》ES6 系列ES6 系列目录地址:https://github.com/mqyqingfeng/BlogES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ...

November 5, 2018 · 4 min · jiezi

【ES6】更易于继承的类语法

和其它面向对象编程语言一样,ES6 正式定义了 class 类以及 extend 继承语法糖,并且支持静态、派生、抽象、迭代、单例等,而且根据 ES6 的新特性衍生出很多有趣的用法。一、类的基本定义基本所有面向对象的语言都支持类的封装与继承,那什么是类?类是面向对象程序设计的基础,包含数据封装、数据操作以及传递消息的函数。类的实例称为对象。ES5 之前通过函数来模拟类的实现如下:// 构造函数function Person(name) { this.name = name;}// 原型上的方法Person.prototype.sayName = function(){ console.log(this.name);};// new 一个实例var friend = new Person(“Jenny”);friend.sayName(); // Jennyconsole.log(friend instanceof Person); // trueconsole.log(friend instanceof Object); // true总结来说,定义一个类的思路如下:1.需要构造函数封装数据2.在原型上添加方法操作数据,3.通过New创建实例ES6 使用class关键字定义一个类,这个类有特殊的方法名[[Construct]]定义构造函数,在 new 创建实例时调用的就是[[Construct]],示例如下:/ES6/// 等价于 let Person = class {class Person { // 构造函数 constructor(name) { this.name = name; } // 等价于Person.prototype.sayName sayName() { console.log(this.name); }}console.log(typeof Person); // functionconsole.log(typeof Person.prototype.sayName); // functionlet friend = new Person(“Jenny”);friend.sayName(); // Jennyconsole.log(friend instanceof Person); // trueconsole.log(friend instanceof Object); // true上面的例子中class定义的类与自定义的函数模拟类功能上貌似没什么不同,但本质上还有很大差异的:函数声明可以被提升,但是class类声明与let类似,不能被提升;类声明自动运行在严格模式下,“use strict”;类中所有方法都是不可枚举的,enumerable 为 false。二、更灵活的类类和函数一样,是JavaScript的一等公民(可以传入函数、从函数返回、赋值),并且注意到类与对象字面量还有更多相似之处,这些特点可以扩展出类更灵活的定义与使用。2.1 拥有访问器属性对象的属性有数据属性和访问属性,类中也可以通过get、set关键字定义访问器属性:class Person { constructor(name) { this.name = name; } get value () { return this.name + this.age } set value (num) { this.age = num }}let friend = new Person(“Jenny”);// 调用的是 setterfriend.value = 18// 调用的是 getterconsole.log(friend.value) // Jenny182.2 可计算的成员名称类似 ES6 对象字面量扩展的可计算属性名称,类也可以用[表达式]定义可计算成员名称,包括类中的方法和访问器属性:let methodName = ‘sayName’class Person { constructor(name) { this.name = name; } methodName + ‘Default’ { console.log(this.name); } get methodName { return this.name } set methodName { this.name = str }}let friend = new Person(“Jenny”);// 方法friend.sayNameDefault(); // Jenny// 访问器属性friend.sayName = ’lee’console.log(friend.sayName) // lee想进一步熟悉对象新特性可参考:【ES6】对象的新功能与解构赋值2.3 定义默认迭代器ES6 中常用的集合对象(数组、Set/Map集合)和字符串都是可迭代对象,如果类是用来表示值这些可迭代对象的,那么定义一个默认迭代器会更有用。ES6 通过给Symbol.iterator属性添加生成器的方式,定义默认迭代器:class Person { constructor(name) { this.name = name; } *Symbol.iterator { for (let item of this.name){ yield item } }}var abbrName = new Person(new Set([‘j’, ‘j’, ’e’, ’e’, ’n’, ‘y’, ‘y’, ‘y’,]))for (let x of abbrName) { console.log(x); // j e n y}console.log(…abbrName) // j e n y定义默认迭代器后类的实例就可以使用for-of循环和展开运算符(…)等迭代功能。对以上迭代器内容感到困惑的可参考:【ES6】迭代器与可迭代对象2.4 作为参数的类类作为"一等公民”可以当参数使用传入函数中,当然也可以从函数中返回:function createClass(className, val) { return new className(val)}let person = createClass(Person,‘Jenny’)console.log(person) // Person { name: ‘Jenny’ }console.log(typeof person) // object2.5 创建单例使用类语法创建单例的方式通过new立即调用类表达式:let singleton = new class { constructor(name) { this.name = name; }}(‘Jenny’) console.log(singleton.name) // Jenny这里先创建匿名类表达式,然后 new 调用这个类表达式,并通过小括号立即执行,这种类语法创建的单例不会在作用域中暴露类的引用。三、类的继承回顾 ES6 之前如何实现继承?常用方式是通过原型链、构造函数以及组合继承等方式。ES6 的类使用熟悉的extends关键字指定类继承的函数,并且可以通过surpe()方法访问父类的构造函数。例如继承一个 Person 的类:class Friend extends Person { constructor(name, phone){ super(name) this.phone = phone }}let myfriend = new Friend(’lee’,2233)console.log(myfriend) // Friend { name: ’lee’, phone: 2233 }Friend 继承了 Person,术语上称 Person 为基类,Friend 为派生类。需要注意的是,surpe()只能在派生类中使用,它负责初始化 this,所以派生类使用 this 之前一定要用surpe()。3.1 继承内建对象ES6 的类继承可以继承内建对象(Array、Set、Map 等),继承后可以拥有基类的所有内建功能。例如:class MyArray extends Array {}let arr = new MyArray(1, 2, 3, 4), subarr = arr.slice(1, 3)console.log(arr.length) // 4console.log(arr instanceof MyArray) // trueconsole.log(arr instanceof Array) // trueconsole.log(subarr instanceof MyArray) // true注意到上例中,不仅 arr 是派生类 MyArray 的实例,subarr 也是派生类 MyArray 的实例,内建对象继承的实用之处是改变返回对象的类型。浏览器引擎背后是通过[Symbol.species]属性实现这一行为,它被用于返回函数的静态访问器属性,内建对象定义了[Symbol.species]属性的有 Array、ArrayBuffer、Set、Map、Promise、RegExp、Typed arrays。3.2 继承表达式的类目前extends可以继承类和内建对象,但更强大的功能从表达式导出类!这个表达式要求可以被解析为函数并具有[[Construct]]属性和原型,示例如下:function Sup(val) { this.value = val}Sup.prototype.getVal = function () { return ‘hello’ + this.value}class Derived extends Sup { constructor(val) { super(val) }}let der = new Derived(‘world’)console.log(der) // Derived { value: ‘world’ }console.log(der.getVal()) // helloworld3.3 只能继承的抽象类ES6 引入new.target元属性判断函数是否通过new关键字调用。类的构造函数也可以通过new.target确定类是如何被调用的。可以通过new.target创建抽象类(不能实例化的类),例如:class Abstract { constructor(){ if(new.target === Abstract) { throw new Error(‘抽象类(不能直接实例化)’) } }}class Instantiable extends Abstract { constructor() { super() }}// let abs = new Abstract() // Error: 抽象类(不能直接实例化) let abs = new Instantiable()console.log(abs instanceof Abstract) // true虽然不能直接使用 Abstract 抽象类创建实例,但是可以作为基类派生其它类。四、类的静态成员ES6 使用static关键字声明静态成员或方法。在类的方法或访问器属性前都可以使用static,唯一的限制是不能用于构造函数。静态成员的作用是某些类成员的私有化,及不可在实例中访问,必须要直接在类上访问。class Person { constructor(name) { this.name = name; } static create(name) { return new Person(name); }}let beauty = Person.create(“Jenny”);// beauty.create(’lee’) // TypeError如果基类有静态成员,那这些静态成员在派生类也可以使用。例如将上例的 Person 作为基类,派生出 Friend 类并使用基类的静态方法create( ):class Friend extends Person { constructor(name){ super(name) }}var friend = Friend.create(’lee’)console.log(friend instanceof Person) // trueconsole.log(friend instanceof Friend) // false可以看出派生类依然可以使用基类的静态方法。加油哦少年! ...

November 4, 2018 · 3 min · jiezi

ES6 系列之 Babel 将 Async 编译成了什么样子

前言本文就是简单介绍下 Async 语法编译后的代码。Asyncconst fetchData = (data) => new Promise((resolve) => setTimeout(resolve, 1000, data + 1))const fetchValue = async function () { var value1 = await fetchData(1); var value2 = await fetchData(value1); var value3 = await fetchData(value2); console.log(value3)};fetchValue();// 大约 3s 后输出 4Babel我们直接在 Babel 官网的 Try it out 粘贴上述代码,然后查看代码编译成什么样子:“use strict”;function _asyncToGenerator(fn) { return function() { var gen = fn.apply(this, arguments); return new Promise(function(resolve, reject) { function step(key, arg) { try { var info = genkey; var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then( function(value) { step(“next”, value); }, function(err) { step(“throw”, err); } ); } } return step(“next”); }); };}var fetchData = function fetchData(data) { return new Promise(function(resolve) { return setTimeout(resolve, 1000, data + 1); });};var fetchValue = (function() { var _ref = _asyncToGenerator( /#PURE/ regeneratorRuntime.mark(function _callee() { var value1, value2, value3; return regeneratorRuntime.wrap( function _callee$(_context) { while (1) { switch ((_context.prev = _context.next)) { case 0: _context.next = 2; return fetchData(1); case 2: value1 = _context.sent; _context.next = 5; return fetchData(value1); case 5: value2 = _context.sent; _context.next = 8; return fetchData(value2); case 8: value3 = _context.sent; console.log(value3); case 10: case “end”: return _context.stop(); } } }, _callee, this ); }) ); return function fetchValue() { return _ref.apply(this, arguments); };})();fetchValue();_asyncToGeneratorregeneratorRuntime 相关的代码我们在 《ES6 系列之 Babel 将 Generator 编译成了什么样子》 中已经介绍过了,这次我们重点来看看 _asyncToGenerator 函数:function _asyncToGenerator(fn) { return function() { var gen = fn.apply(this, arguments); return new Promise(function(resolve, reject) { function step(key, arg) { try { var info = genkey; var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then( function(value) { step(“next”, value); }, function(err) { step(“throw”, err); } ); } } return step(“next”); }); };}以上这段代码主要是用来实现 generator 的自动执行以及返回 Promise。当我们执行 fetchValue() 的时候,执行的其实就是 _asyncToGenerator 返回的这个匿名函数,在匿名函数中,我们执行了var gen = fn.apply(this, arguments);这一步就相当于执行 Generator 函数,举个例子:function* helloWorldGenerator() { yield ‘hello’; yield ‘world’; return ’ending’;}var hw = helloWorldGenerator();var gen = fn.apply(this, arguments) 就相当于 var hw = helloWorldGenerator();,返回的 gen 是一个具有 next()、throw()、return() 方法的对象。然后我们返回了一个 Promise 对象,在 Promise 中,我们执行了 step(“next”),step 函数中会执行:try { var info = genkey; var value = info.value;} catch (error) { reject(error); return;}step(“next”) 就相当于 var info = gen.next(),返回的 info 对象是一个具有 value 和 done 属性的对象:{value: Promise, done: false}接下来又会执行:if (info.done) { resolve(value);} else { return Promise.resolve(value).then( function(value) { step(“next”, value); }, function(err) { step(“throw”, err); } );}value 此时是一个 Promise,Promise.resolve(value) 依然会返回这个 Promise,我们给这个 Promise 添加了一个 then 函数,用于在 Promise 有结果时执行,有结果时又会执行 step(“next”, value),从而使得 Generator 继续执行,直到 info.done 为 true,才会 resolve(value)。不完整但可用的代码(function() { var ContinueSentinel = {}; var mark = function(genFun) { var generator = Object.create({ next: function(arg) { return this._invoke(“next”, arg); } }); genFun.prototype = generator; return genFun; }; function wrap(innerFn, outerFn, self) { var generator = Object.create(outerFn.prototype); var context = { done: false, method: “next”, next: 0, prev: 0, sent: undefined, abrupt: function(type, arg) { var record = {}; record.type = type; record.arg = arg; return this.complete(record); }, complete: function(record, afterLoc) { if (record.type === “return”) { this.rval = this.arg = record.arg; this.method = “return”; this.next = “end”; } return ContinueSentinel; }, stop: function() { this.done = true; return this.rval; } }; generator._invoke = makeInvokeMethod(innerFn, context); return generator; } function makeInvokeMethod(innerFn, context) { var state = “start”; return function invoke(method, arg) { if (state === “completed”) { return { value: undefined, done: true }; } context.method = method; context.arg = arg; while (true) { state = “executing”; if (context.method === “next”) { context.sent = context._sent = context.arg; } var record = { type: “normal”, arg: innerFn.call(self, context) }; if (record.type === “normal”) { state = context.done ? “completed” : “yield”; if (record.arg === ContinueSentinel) { continue; } return { value: record.arg, done: context.done }; } } }; } window.regeneratorRuntime = {}; regeneratorRuntime.wrap = wrap; regeneratorRuntime.mark = mark;})();“use strict”;function _asyncToGenerator(fn) { return function() { var gen = fn.apply(this, arguments); return new Promise(function(resolve, reject) { function step(key, arg) { try { var info = genkey; var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then( function(value) { step(“next”, value); }, function(err) { step(“throw”, err); } ); } } return step(“next”); }); };}var fetchData = function fetchData(data) { return new Promise(function(resolve) { return setTimeout(resolve, 1000, data + 1); });};var fetchValue = (function() { var _ref = _asyncToGenerator( /#PURE/ regeneratorRuntime.mark(function _callee() { var value1, value2, value3; return regeneratorRuntime.wrap( function _callee$(_context) { while (1) { switch ((_context.prev = _context.next)) { case 0: _context.next = 2; return fetchData(1); case 2: value1 = _context.sent; _context.next = 5; return fetchData(value1); case 5: value2 = _context.sent; _context.next = 8; return fetchData(value2); case 8: value3 = _context.sent; console.log(value3); case 10: case “end”: return _context.stop(); } } }, _callee, this ); }) ); return function fetchValue() { return _ref.apply(this, arguments); };})();fetchValue();请原谅我水了一篇文章……ES6 系列ES6 系列目录地址:https://github.com/mqyqingfeng/BlogES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ...

October 31, 2018 · 4 min · jiezi

ES6 系列之 Babel 将 Generator 编译成了什么样子

前言本文就是简单介绍下 Generator 语法编译后的代码。Generatorfunction* helloWorldGenerator() { yield ‘hello’; yield ‘world’; return ’ending’;}我们打印下执行的结果:var hw = helloWorldGenerator();console.log(hw.next()); // {value: “hello”, done: false}console.log(hw.next()); // {value: “world”, done: false}console.log(hw.next()); // {value: “ending”, done: true}console.log(hw.next()); // {value: undefined, done: true}Babel具体的执行过程就不说了,我们直接在 Babel 官网的 Try it out 粘贴上述代码,然后查看代码被编译成了什么样子:/** * 我们就称呼这个版本为简单编译版本吧 /var _marked = /#PURE/ regeneratorRuntime.mark(helloWorldGenerator);function helloWorldGenerator() { return regeneratorRuntime.wrap( function helloWorldGenerator$(_context) { while (1) { switch ((_context.prev = _context.next)) { case 0: _context.next = 2; return “hello”; case 2: _context.next = 4; return “world”; case 4: return _context.abrupt(“return”, “ending”); case 5: case “end”: return _context.stop(); } } }, _marked, this );}猛一看,好像编译后的代码还蛮少的,但是细细一看,编译后的代码肯定是不能用的呀,regeneratorRuntime 是个什么鬼?哪里有声明呀?mark 和 wrap 方法又都做了什么?难道就不能编译一个完整可用的代码吗?regenerator如果你想看到完整可用的代码,你可以使用 regenerator,这是 facebook 下的一个工具,用于编译 ES6 的 generator 函数。我们先安装一下 regenerator:npm install -g regenerator然后新建一个 generator.js 文件,里面的代码就是文章最一开始的代码,我们执行命令:regenerator –include-runtime generator.js > generator-es5.js我们就可以在 generator-es5.js 文件看到编译后的完整可用的代码。而这一编译就编译了 700 多行…… 编译后的代码可以查看 generator-es5.js总之编译后的代码还蛮复杂,我们可以从中抽离出大致的逻辑,至少让简单编译的那段代码能够跑起来。mark 函数简单编译后的代码第一段是这样的:var _marked = /#PURE/ regeneratorRuntime.mark(helloWorldGenerator);我们查看完整编译版本中 mark 函数的源码:runtime.mark = function(genFun) { genFun.proto = GeneratorFunctionPrototype; genFun.prototype = Object.create(Gp); return genFun;};这其中又涉及了 GeneratorFunctionPrototype 和 Gp 变量,我们也查看下对应的代码:function Generator() {}function GeneratorFunction() {}function GeneratorFunctionPrototype() {}…var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(IteratorPrototype);GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype;GeneratorFunctionPrototype.constructor = GeneratorFunction;GeneratorFunctionPrototype[toStringTagSymbol] = GeneratorFunction.displayName = “GeneratorFunction”;这段代码构建了一堆看起来很复杂的关系链,其实这是参照着 ES6 规范构建的关系链:图中 +@@toStringTag:s = ‘Generator’ 的就是 Gp,+@@toStringTag:s = ‘GeneratorFunction’ 的就是 GeneratorFunctionPrototype。构建关系链的目的在于判断关系的时候能够跟原生的保持一致,就比如:function f() {}var g = f();console.log(g.proto === f.prototype); // trueconsole.log(g.proto.proto === f.proto.prototype); // true为了简化起见,我们可以把 Gp 先设置为一个空对象,不过正如你在上图中看到的,next()、 throw()、return() 函数都是挂载在 Gp 对象上,实际上,在完整的编译代码中,确实有为 Gp 添加这三个函数的方法:// 117 行function defineIteratorMethods(prototype) { [“next”, “throw”, “return”].forEach(function(method) { prototype[method] = function(arg) { return this._invoke(method, arg); }; });}// 406 行defineIteratorMethods(Gp);为了简单起见,我们将整个 mark 函数简化为:runtime.mark = function(genFun) { var generator = Object.create({ next: function(arg) { return this._invoke(’next’, arg) } }); genFun.prototype = generator; return genFun;};wrap 函数除了设置关系链之外,mark 函数的返回值 genFun 还作为了 wrap 函数的第二个参数传入:function helloWorldGenerator() { return regeneratorRuntime.wrap( function helloWorldGenerator$(_context) { … }, _marked, this );}我们再看下 wrap 函数:function wrap(innerFn, outerFn, self) { var generator = Object.create(outerFn.prototype); var context = new Context([]); generator._invoke = makeInvokeMethod(innerFn, self, context); return generator;}所以当执行 var hw = helloWorldGenerator(); 的时候,其实执行的是 wrap 函数,wrap 函数返回了 generator,generator 是一个对象,原型是 outerFn.prototype, outerFn.prototype 其实就是 genFun.prototype, genFun.prototype 是一个空对象,原型上有 next() 方法。所以当你执行 hw.next() 的时候,执行的其实是 hw 原型的原型上的 next 函数,next 函数执行的又是 hw 的 _invoke 函数:generator._invoke = makeInvokeMethod(innerFn, self, context);innerFn 就是 wrap 包裹的那个函数,其实就是 helloWordGenerato$ 函数,呐,就是这个函数:function helloWorldGenerator$(_context) { while (1) { switch ((_context.prev = _context.next)) { case 0: _context.next = 2; return “hello”; case 2: _context.next = 4; return “world”; case 4: return _context.abrupt(“return”, “ending”); case 5: case “end”: return _context.stop(); } }}而 context 你可以直接理解为这样一个全局对象:var ContinueSentinel = {};var context = { done: false, method: “next”, next: 0, prev: 0, abrupt: function(type, arg) { var record = {}; record.type = type; record.arg = arg; return this.complete(record); }, complete: function(record, afterLoc) { if (record.type === “return”) { this.rval = this.arg = record.arg; this.method = “return”; this.next = “end”; } return ContinueSentinel; }, stop: function() { this.done = true; return this.rval; }};每次 hw.next 的时候,就会修改 next 和 prev 属性的值,当在 generator 函数中 return 的时候会执行 abrupt,abrupt 中又会执行 complete,执行完 complete,因为 this.next = end 的缘故,再执行就会执行 stop 函数。我们来看下 makeInvokeMethod 函数:var ContinueSentinel = {};function makeInvokeMethod(innerFn, self, context) { var state = ‘start’; return function invoke(method, arg) { if (state === ‘completed’) { return { value: undefined, done: true }; } context.method = method; context.arg = arg; while (true) { state = ’executing’; var record = { type: ’normal’, arg: innerFn.call(self, context) }; if (record.type === “normal”) { state = context.done ? ‘completed’ : ‘yield’; if (record.arg === ContinueSentinel) { continue; } return { value: record.arg, done: context.done }; } } };}基本的执行过程就不分析了,我们重点看第三次执行 hw.next() 的时候:第三次执行 hw.next() 的时候,其实执行了this._invoke(“next”, undefined);我们在 invoke 函数中构建了一个 record 对象:var record = { type: “normal”, arg: innerFn.call(self, context)};而在 innerFn.call(self, context) 中,因为 _context.next 为 4 的缘故,其实执行了:_context.abrupt(“return”, ’ending’);而在 abrupt 中,我们又构建了一个 record 对象:var record = {};record.type = ‘return’;record.arg = ’ending’;然后执行了 this.complete(record),在 complete 中,因为 record.type === “return"this.rval = ’ending’;this.method = “return”;this.next = “end”;然后返回了全局对象 ContinueSentinel,其实就是一个全局空对象。然后在 invoke 函数中,因为 record.arg === ContinueSentinel 的缘故,没有执行后面的 return 语句,就直接进入下一个循环。于是又执行了一遍 innerFn.call(self, context),此时 _context.next 为 end, 执行了 _context.stop(), 在 stop 函数中:this.done = true;return this.rval; // this.rval 其实就是 ending所以最终返回的值为:{ value: ’ending’, done: true};之后,我们再执行 hw.next() 的时候,因为 state 已经是 ‘completed’ 的缘故,直接就返回 { value: undefined, done: true}不完整但可用的源码当然这个过程,看文字理解起来可能有些难度,不完整但可用的代码如下,你可以断点调试查看具体的过程:(function() { var ContinueSentinel = {}; var mark = function(genFun) { var generator = Object.create({ next: function(arg) { return this._invoke(“next”, arg); } }); genFun.prototype = generator; return genFun; }; function wrap(innerFn, outerFn, self) { var generator = Object.create(outerFn.prototype); var context = { done: false, method: “next”, next: 0, prev: 0, abrupt: function(type, arg) { var record = {}; record.type = type; record.arg = arg; return this.complete(record); }, complete: function(record, afterLoc) { if (record.type === “return”) { this.rval = this.arg = record.arg; this.method = “return”; this.next = “end”; } return ContinueSentinel; }, stop: function() { this.done = true; return this.rval; } }; generator._invoke = makeInvokeMethod(innerFn, context); return generator; } function makeInvokeMethod(innerFn, context) { var state = “start”; return function invoke(method, arg) { if (state === “completed”) { return { value: undefined, done: true }; } context.method = method; context.arg = arg; while (true) { state = “executing”; var record = { type: “normal”, arg: innerFn.call(self, context) }; if (record.type === “normal”) { state = context.done ? “completed” : “yield”; if (record.arg === ContinueSentinel) { continue; } return { value: record.arg, done: context.done }; } } }; } window.regeneratorRuntime = {}; regeneratorRuntime.wrap = wrap; regeneratorRuntime.mark = mark;})();var _marked = regeneratorRuntime.mark(helloWorldGenerator);function helloWorldGenerator() { return regeneratorRuntime.wrap( function helloWorldGenerator$(_context) { while (1) { switch ((_context.prev = _context.next)) { case 0: _context.next = 2; return “hello”; case 2: _context.next = 4; return “world”; case 4: return _context.abrupt(“return”, “ending”); case 5: case “end”: return _context.stop(); } } }, _marked, this );}var hw = helloWorldGenerator();console.log(hw.next());console.log(hw.next());console.log(hw.next());console.log(hw.next());ES6 系列ES6 系列目录地址:https://github.com/mqyqingfeng/BlogES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ...

October 30, 2018 · 5 min · jiezi

ES6 系列之异步处理实战

前言我们以查找指定目录下的最大文件为例,感受从回调函数 -> Promise -> Generator -> Async异步处理方式的改变。API 介绍为了实现这个功能,我们需要用到几个 Nodejs 的 API,所以我们来简单介绍一下。fs.readdirreaddir 方法用于读取目录,返回一个包含文件和目录的数组。fs.statstat 方法的参数是一个文件或目录,它产生一个对象,该对象包含了该文件或目录的具体信息。此外,该对象还有一个 isFile() 方法可以判断正在处理的到底是一个文件,还是一个目录。思路分析我们基本的实现思路就是:用 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)});Promisevar 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)] })}使用方式为:findLargest(’./’).then(function(filename) { console.log(’largest file was:’, filename);}).catch(function() { console.log(error);});Generatorvar fs = require(‘fs’);var path = require(‘path’);var co = require(‘co’)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) { var files = yield readDir(dir); var stats = yield files.map(function(file) { return stat(path.join(dir, file)) }) let largest = stats .filter(function(stat) { return stat.isFile() }) .reduce((prev, next) => { if (prev.size > next.size) return prev return next }) return files[stats.indexOf(largest)]}使用方式为:co(findLargest, ‘./’).then(function(filename) { console.log(’largest file was:’, filename);}).catch(function() { console.log(error);});Asyncvar 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) }) })}async function findLargest(dir) { var files = await readDir(dir); let promises = files.map(file => stat(path.join(dir, file))) var stats = await Promise.all(promises) let largest = stats .filter(function(stat) { return stat.isFile() }) .reduce((prev, next) => { if (prev.size > next.size) return prev return next }) return files[stats.indexOf(largest)]}使用方式为:findLargest(’./’).then(function(filename) { console.log(’largest file was:’, filename);}).catch(function() { console.log(error);});ES6 系列ES6 系列目录地址:https://github.com/mqyqingfeng/BlogES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ...

October 25, 2018 · 3 min · jiezi