前端面试每日-31-第182天

今天的知识点 (2019.10.15) —— 第182天[html] 举例说明HTML5的标签meter的用法[css] 让你手写一个reset的文件,你应该怎么写?要考虑哪些方面呢?[js] 为什么说js是弱类型语言,它的优缺点分别是什么?[软技能] 你知道什么是ECMAScript吗?《论语》,曾子曰:“吾日三省吾身”(我每天多次反省自己)。 前端面试每日3+1题,以面试题来驱动学习,每天进步一点! 让努力成为一种习惯,让奋斗成为一种享受!相信 坚持 的力量!!!欢迎在 Issues 和朋友们一同讨论学习! 项目地址:前端面试每日3+1 【推荐】欢迎跟 jsliang 一起折腾前端,系统整理前端知识,目前正在折腾 LeetCode,打算打通算法与数据结构的任督二脉。GitHub 地址 微信公众号欢迎大家前来讨论,如果觉得对你的学习有一定的帮助,欢迎点个Star, 同时欢迎微信扫码关注 前端剑解 公众号,并加入 “前端学习每日3+1” 微信群相互交流(点击公众号的菜单:进群交流)。 学习不打烊,充电加油只为遇到更好的自己,365天无节假日,每天早上5点纯手工发布面试题(死磕自己,愉悦大家)。希望大家在这浮夸的前端圈里,保持冷静,坚持每天花20分钟来学习与思考。在这千变万化,类库层出不穷的前端,建议大家不要等到找工作时,才狂刷题,提倡每日学习!(不忘初心,html、css、javascript才是基石!)欢迎大家到Issues交流,鼓励PR,感谢Star,大家有啥好的建议可以加我微信一起交流讨论!希望大家每日去学习与思考,这才达到来这里的目的!!!(不要为了谁而来,要为自己而来!)交流讨论欢迎大家前来讨论,如果觉得对你的学习有一定的帮助,欢迎点个[Star] https://github.com/haizlin/fe...

October 15, 2019 · 1 min · jiezi

javascript之异步函数

这篇文章详细讲解了JavaScript中的异步函数。 JavaScript中的异步代码在很短的时间内从回调发展为Promise,再到ES2017的异步函数,现在我们可以像编写同步代码那样编写基于 Promise 的代码,而且还不会阻塞主线程。 为什么需要async/await?当promise在ES2015中引入时,目的是解决异步代码的问题,但是promise不是最终的解决方案。 虽然Promise解决了著名的回调地狱,但是它自己引入了语法复杂性。 所以ES2017增加了异步函数,提高了代码可读性,对不太熟悉 Promise 的人而言,帮助就更大了。 async/await使代码看起来像是同步的,但它在后台是异步和非阻塞的。 工作原理异步函数返回一个promise,如下例所示: const doSomethingAsync = () => { return new Promise((resolve) => { setTimeout(() => resolve('I did something'), 3000) })}调用一个异步函数时,您先要设置一个await,当您 await 某个 Promise 时,函数暂停执行,直至该 Promise 产生结果,并且暂停并不会阻塞主线程。 如果 Promise 执行,则会返回值。 如果 Promise 拒绝,则会抛出拒绝的值。因为异步函数去掉了所有回调。提高了代码的可读性,这是一个例子: const doSomething = async () => { console.log(await doSomethingAsync())}一个简单的例子这是异步函数async / await的简单示例: const doSomethingAsync = () => { return new Promise((resolve) => { setTimeout(() => resolve('I did something'), 3000) })}const doSomething = async () => { console.log(await doSomethingAsync())}console.log('Before')doSomething()console.log('After')上面代码执行结果如下: ...

July 4, 2019 · 2 min · jiezi

浅析promise与自定义promise

promise的用法简单介绍一下,我觉得如果有人愿意看这篇文章,对promise的用法多多少少也了解; new Promise((res, rej) => { try{ 执行函数; res(data) } catch(e){ rej(e); }}).then(resCb, rejCb).then(cb);心血来潮,今天去自己试着写了一下promise。先说下我个人对promise的理解 1.promise 从表现形式上看,是将执行函数的返回值通过resolve或者reject 抛出data,再在then 函数里面处理data2.promise 可以链式调用then,上一个then的返回值作为下一个then中函数的实参 但实际上,promise 并非一定是在then 里面执行的,尤其是异步的时候,理应在实例函数中执行,才符合我们对单线程的理解。 promise简单点说分为两部分。一个是实例化 时候,传入的执行函数, 另一部分为then中传入的回调函数 这个关系就好比 监考老师 和 学生如果监考老师 先到了教室,那么自然而然,学生是直接依次进入教室;(实例函数执行较快,超过了then的声明)如果监考老师 在学生们后面到,那么学生们只能按顺序排队在门口等监考老师;(实例函数执行较慢)也就是说,执行函数和then函数的声明,先后顺序并非固定(一般情况下then声明先完成)这个时候就需要一个状态码去判断到底是哪种情况('default', 'resolve' ,'reject')为了方便理解,可以把resolve 和 reject看成同一类型我先直接发代码写注释时间有限,本次先不考虑then 中函数 存在异步的问题 // 简述下逻辑: //1.定义一个state 是用来判断then 和 实例化时候传入的函数 哪一个先完成 //也就是刚刚说的监考老师和学生的问题。默认值为default, 默认老师没到(实例函数未执行完成) //2.定义resolve_ 用来存放then 中定义的方法,相当于学生排队的过道,便于按顺序执行,reject_同理,以下不再重复 //3.定义resolveData用来记录抛出值,也就是res(data) 中的data,作为then中方法的参数 //4.在res抛出值的时候,将state改成resolve,相当于表明监考老师到教室了 //如果resolve_队列中已经有定义函数就依次执行它们,相当于如果有学生就进教室。 //5.then 方法声明时候,检测状态state 是不是 default ,如果是,说明还没有抛出值, //相当于监考老师还没到,学生都去排队,加入到resolve_队列; //如果状态已经不是default ,那么说明监考老师已经到了,学生不用排队,直接进教室;也就是方法直接执行 new promise_( (res, rej) => res(3)).then(data=> {console.log(data);return 6}).then(data => console.log(data))// 3,6针对实际例子说一下上面的例子明显就是 res(3) 先执行完成,然后执行then 的函数相当于监考老师先到了教室,那么,then 中的定义的函数就应该直接执行 ...

June 28, 2019 · 2 min · jiezi

20190611对async和await的一点理解

首先看下这段代码: async function submit(){ console.log('请求开始!') let data = await fetch('127.0.0.1:8888') .then(res => res.json()) .then(res => { console.log('请求成功!') return res }) console.log('请求结束');}console.log('请求成功了么?');执行这这段代码,你会发现控制板输出的数据顺序 '请求成功了么?''请求开始!''请求成功!''请求结束!'async定义MDN: async function 声明用于定义一个返回 AsyncFunction 对象的异步函数。异步函数是指通过事件循环异步执行的函数,它会通过一个隐式的 Promise 返回其结果。但是如果你的代码使用了异步函数,它的语法和结构会更像是标准的同步函数。通俗的讲,就是「异步」。让这个fn支持异步继续,所以首先打印出来的是 '请求成功了么?'async利用了影藏的promise对象,来控制函数异步进行,所以你在执行async中,console会出来一个promise对象。 同时需要搭配await来阻塞内部异步,来讲操作 await定义MDN: await表达式会暂停当前「async function」的执行,等待Pormise处理完成,若Promise正常处理,则回调的resolve函数作为await表达式的值,继续进行async function。表达式一个 Promise 对象或者任何要等待的值。 返回值返回 Promise 对象的处理结果。如果等待的不是 Promise 对象,则返回该值本身。 所以上面代码,内部的执行顺序是 async function submit(){ // 开始执行 console.log('请求开始!') // 遇到await,等待处理结果 let data = await fetch('127.0.0.1:8888') .then(res => res.json()) .then(res => { // 处理完成,返回处理结果 console.log('请求成功!') return res }) // 等待await处理完成后,执行。 console.log('请求结束');}注意:如果返回值不是Promise,则把该值转换为已经常处理的Promise,等待处理结构(就是抛出该值)async function f2() { var y = await 20; console.log(y); // 20}f2(); 应用场景await可以阻塞主函数,直到后面的Promise对象处理完成,这就很容易的解决了按顺控制异步操作。 ...

June 11, 2019 · 1 min · jiezi

ES6学习4Class

JS语言传统方法通过构造函数定义并生成新对象,ES6引入了Class这个概念作为对象的模板,通过class关键字可以定义类。 基本语法function Point(x, y) { this.x = x; this.y = y;}Point.prototype.toString = function () { return '(' + this.x + ',' + this.y + ')';}Point.protortype.doStuff = function () { console.log('stuff');}// 等同于class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return '(' + this.x + ',' + this.y + ')'; } doStuff() { console.log('stuff'); }}typeof Point // "function"Point === Point.prototype.constructor // true上面的代码表明,类的数据类型就是函数,类本身指向构造函数。类的所有方法都定义在类的prototype属性上。在类的实例上调用方法,就是在调用原型上的方法。constructor方法是类的默认方法,通过new命令生成对象实例时自动调用该方法。如果没有显示定义,一个空的construtor方法会被默认添加。 ...

June 3, 2019 · 2 min · jiezi

前端漫谈1-从-for-of-聊到-Generator

聊聊 for of说起 for of 相信每个写过 JavaScript 的人都用过 for of ,平时我们用它做什么呢?大多数情况应该就是遍历数组了,当然,更多时候,我们也会用 map() 或者 filer() 来遍历一个数组。 但是就像我们标题里面说的,它跟 Generator 能扯上什么关系呢? 首先我们想一个问题,为什么使用 for of 或者 map()/filer() 方法就可以遍历一个数组 (或者类数组对象: Strings , Maps , Sets , arguments ) 呢? 为什么不能用他们来遍历一个对象呢? 你能学到什么对 for of 更深入的理解iterator 到底是何方神圣?数组也是对象,为什么不能用 for of 来遍历对象呢?如何实现对象的 for of?Generator 又是何方神圣?他有什么用呢?类数组对象的玄机在真正揭开谜底之前,站在 for of 的角度想一下,现在让你去遍历一个数组,你需要知道什么信息呢? 对应下标的值是否遍历结束的标志带着这样的思考,我们打印一个数组来看看这里面的玄机: const numbersArray = [1, 2, 3];console.dir(numbersArray); 数组 (或者类数组对象: Strings , Maps , Sets , arguments ) 的原型中都实现了一个方法 Symbol.iterator,问题来了,那么这个 Symbol.iterator 又有什么用呢? 拿出来试一下就知道了: ...

May 31, 2019 · 3 min · jiezi

ES6学习3-箭头函数

1 基本用法ES6新增了箭头函数,简化了函数声明过程。 var f = value => value;//等同于下面函数var f = function (value) { return value;};如果不需要要参数或者多个参数,使用圆括号代表参数部分。如果箭头函数的代码块部分多于一条语句,需要使用大括号,并使用return返回。如果直接返回一个对象,必须在对象外面加括号。 var f = {} => 1;var sum = (num1, num2) => { return num1 + num2;};//直接返回一个对象var getName = name => ({ name: 'jack' });箭头函数可以与变量解构结合使用。 let person = ({ name, age }) => name + '今年' + age;let obj = { name: '小王', age: 25 };person(obj); // "小王今年25"箭头函数可以使得表达更加简洁。 let arr = [1, 3, 4, 2, 5];var result = value => value.sort((a, b) => a - b);result(arr); // [1, 2, 3, 4, 5]2 this指向箭头函数的this指向定义时所在对象,而不是使用时所在的对象。因此,在箭头函数中this对象的指向是固定的。 ...

May 21, 2019 · 1 min · jiezi

ES6之常量和扩展运算符

1. ES5定义常量ES5中通过改变属性描述符来达到常量的效果,我们可以将属性定义为只读。Object.defineProperty(window,'PI',{ value:'3.14', writable:false})2. ES6定义常量使用const定义 const PI = 3;3. ES5复制数组数组是复合的数据类型,若直接复制,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。 var arr = [1,2];var arr2 = arr;arr2[0] = 90;console.log(arr); //[90,2]console.log(arr2) //[90,2]所以在ES5中,只能变通用concat方法复制 var arr = [1,2];var arr2 = arr.concat();arr2[0] = 10;console.log(arr); //[1,2]console.log(arr2); //[10,2]4. ES6扩展运算符 ... (三个点)复制数组对象中的扩展运算符可以这样理解:对象中的扩展运算符(...)用于取出参数对象中所有可遍历的属性,拷贝到当前对象中。let arr = [1,2,3];let arr2 = [...arr]; // [1,2,3]假如给拷贝arr2时push一个元素: let arr2 = [...arr,5];console.log(arr2); //[1,2,3,5]console.log(arr); //[1,2,3]可以看到只会改变arr2的值,而不会影响arr的值。同样改变arr的值也不会影响到arr2的值。 arr.push(12);console.log(arr); //[1,2,3,12]console.log(arr2); //[1,2,3,5]所以,上述方法实际上等价于: let arr = [1,2,3];let arr2 = Object.assign([],arr);console.log(arr2); //[1,2,3]arr2.push(12);console.log(arr2); //[1,2,3,12]console.log(arr); //[1,2,3]Object.assign方法用于对象的合并,将源对象所有的可枚举属性复制到目标对象。注:Object.assign和扩展运算符(...),对一级属于属于深拷贝,对后面对级别属于浅拷贝。

May 15, 2019 · 1 min · jiezi

ES6中Arrayfind和findIndex函数用法详解

ES6为Array增加了find(),findIndex函数。find()函数用来查找目标元素,找到就返回该元素,找不到返回undefined,而findIndex()函数也是查找目标元素,找到就返回元素的位置,找不到就返回-1。他们的都是一个查找回调函数。查找函数有三个参数。value:每一次迭代查找的数组元素。index:每一次迭代查找的数组元素索引。arr:被查找的数组。假如我们给vue组件绑定了一个班级的学生列表数据。其数据结构可能如下格式,如果你想从以下数据中查找出姓名为李四的学生的信息。 var stu = [ { name: '张三', gender: '男', age: 20 }, { name: '王小毛', gender: '男', age: 20 }, { name: '李四', gender: '男', age: 20 }]关于find()的使用find()方法返回数组中符合测试函数条件的第一个元素。否则返回undefined在这儿需要注意的几个点:①、第一个元素②、测试函数function getStu(element){ return element.name == '李四'}stu.find(getStu)//返回结果为 {name: "李四", gender: "男", age: 20}结合es6的改进stu.find((element) => (element.name == '李四')); //返回的是{name: "李四", gender: "男", age: 20}这个元素stu.findIndex((element)=>(element.name =='李四')); //返回的是索引下标:2

May 11, 2019 · 1 min · jiezi

箭头函数你想知道的都在这里

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;};由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。// 报错let getTempItem = id => { id: id, name: “Temp” };// 不报错let getTempItem = id => ({ id: id, name: “Temp” });下面是一种特殊情况,虽然可以运行,但会得到错误的结果。let foo = () => { a: 1 };foo() // undefined上面代码中,原始意图是返回一个对象{ a: 1 },但是由于引擎认为大括号是代码块,所以执行了一行语句a: 1。这时,a可以被解释为语句的标签,因此实际执行的语句是1;,然后函数就结束了,没有返回值。2、关于this2.1、默认绑定外层this箭头函数没有 this,所以需要通过查找作用域链来确定 this 的值。这就意味着如果箭头函数被非箭头函数包含,this 绑定的就是最近一层非箭头函数的 this。function foo() { setTimeout(() => { console.log(‘id:’, this.id); }, 100);}var id = 21;foo.call({ id: 42 });// id: 42上面代码中,setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以输出的是42。箭头函数可以让setTimeout里面的this,绑定定义时所在的作用域,而不是指向运行时所在的作用域。所以,箭头函数转成 ES5 的代码如下。// ES6function foo() { setTimeout(() => { console.log(‘id:’, this.id); }, 100);}// ES5function foo() { var _this = this; setTimeout(function () { console.log(‘id:’, this.id); }, 100);}2.2、 不能用call()、apply()、bind()方法修改里面的this(function() { return [ (() => this.x).bind({ x: ‘inner’ })() // 无效的bind,最终this还是指向外层 ];}).call({ x: ‘outer’ });// [‘outer’]上面代码中,箭头函数没有自己的this,所以bind方法无效,内部的this指向外部的this。3、没有 arguments箭头函数没有自己的 arguments 对象,这不一定是件坏事,因为箭头函数可以访问外围函数的 arguments 对象:function constant() { return () => arguments[0]}var result = constant(1);console.log(result()); // 1那如果我们就是要访问箭头函数的参数呢?你可以通过命名参数或者 rest 参数的形式访问参数:let nums = (…nums) => nums;4、 不能通过 new 关键字调用JavaScript 函数有两个内部方法:[[Call]] 和 [[Construct]]。当通过new调用函数时,执行[Construct]]方法,创建一个实例对象,然后再执行函数体,将 this 绑定到实例上。当直接调用的时候,执行[[Call]]方法,直接执行函数体。箭头函数并没有[[Construct]]方法,不能被用作构造函数,如果通过 new 的方式调用,会报错。var Foo = () => {};var foo = new Foo(); // TypeError: Foo is not a constructor5、没有原型由于不能使用new调用箭头函数,所以也没有构建原型的需求,于是箭头函数也不存在prototype这个属性。var Foo = () => {};console.log(Foo.prototype); // undefined5、不适用场合第一个场合是定义函数的方法,且该方法内部包括this。const cat = { lives: 9, jumps: () => { this.lives–; }}上面代码中,cat.jumps()方法是一个箭头函数,这是错误的。调用cat.jumps()时,如果是普通函数,该方法内部的this指向cat;如果写成上面那样的箭头函数,使得this指向全局对象,因此不会得到预期结果。第二个场合是需要动态this的时候,也不应使用箭头函数。var button = document.getElementById(‘press’);button.addEventListener(‘click’, () => { this.classList.toggle(‘on’);});上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。6、使用场景下面这个是我们开发经常遇到的。我们一般会通过this赋值给一个变量,然后再通过变量访问。class Test { constructor() { this.birth = 10; } submit(){ let self = this; $.ajax({ type: “POST”, dataType: “json”, url: “xxxxx” ,//url data: “xxxxx”, success: function (result) { console.log(self.birth);//10 }, error : function() {} }); }}let test = new Test();test.submit();//undefined 这里我们就可以通过箭头函数来解决…success: (result)=> { console.log(this.birth);//10},…箭头函数在react中的运用场景class Foo extends Component { constructor(props) { super(props); } handleClick() { console.log(‘Click happened’, this); this.setState({a: 1}); } render() { return <button onClick={this.handleClick}>Click Me</button>; }}在react中我们这样直接调用方法是有问题的,在handleClick函数中的this是有问题,我们平时需要这么做class Foo extends Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { console.log(‘Click happened’, this); this.setState({a: 1}); } render() { return <button onClick={this.handleClick}>Click Me</button>; }}这里通过this.handleClick.bind(this)给函数绑定this。但是这样写起来有些麻烦,有没有简单的方法呢?这时候我们的箭头函数就出场了class Foo extends Component { // Note: this syntax is experimental and not standardized yet. handleClick = () => { console.log(‘Click happened’, this); this.setState({a: 1}); } render() { return <button onClick={this.handleClick}>Click Me</button>; }}箭头函数中 this 的值是继承自 外围作用域,很好的解决了这个问题。除此之外我们还可以用箭头函数传参(这个不是必须的),而且会有性能问题。更多信息请查看const A = 65 // ASCII character codeclass Alphabet extends React.Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); this.state = { justClicked: null, letters: Array.from({length: 26}, (, i) => String.fromCharCode(A + i)). }; } handleClick(letter) { this.setState({ justClicked: letter }); } render() { return ( <div> Just clicked: {this.state.justClicked} <ul> {this.state.letters.map(letter => <li key={letter} onClick={() => this.handleClick(letter)}> {letter} </li> )} </ul> </div> ) }}最后更多系列文章请看ES6学习(一)之var、let、constES6学习(二)之解构赋值及其原理ES6学习(三)之Set的模拟实现ES6学习(四)之Promise的模拟实现如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star对作者也是一种鼓励。 ...

April 4, 2019 · 2 min · jiezi

es6之js的类

起源JS 从创建之初就不支持类,也没有把类继承作为定义相似对象以及关联对象的主要方式,这让不少开发者感到困惑。而从 ES1 诞生之前直到ES5 时期,很多库都创建了一些工具,让 JS 显得貌似能支持类。尽管一些 JS 开发者强烈认为这门语言不需要类,但为处理类而创建的代码库如此之多,导致 ES6 最终引入了类。ES5 中的仿类结构JS 在 ES5 及更早版本中都不存在类。与类最接近的是:创建一个构造器,然后将方法指派到该构造器的原型上。这种方式通常被称为创建一个自定义类型。例如:function PersonType(name) {this.name = name;}PersonType.prototype.sayName = function() {console.log(this.name);};let person = new PersonType(“Nicholas”);person.sayName(); // 输出 “Nicholas"console.log(person instanceof PersonType); // trueconsole.log(person instanceof Object); // true此代码中的 PersonType 是一个构造器函数,并创建了单个属性 name 。 sayName() 方法被指派到原型上,因此在 PersonType 对象的所有实例上都共享了此方法。接下来,使用 new运算符创建了 PersonType 的一个新实例 person ,此对象会被认为是一个通过原型继承了PersonType 与 Object 的实例。这种基本模式在许多对类进行模拟的 JS 库中都存在,而这也是 ES6 类的出发点。es6基本的类的声明类声明以 class 关键字开始,其后是类的名称;剩余部分的语法看起来就像对象字面量中的方法简写,并且在方法之间不需要使用逗号。作为范例,此处有个简单的类声明:class PersonClass {// 等价于 PersonType 构造器constructor(name) {this.name = name;}// 等价于 PersonType.prototype.sayNamesayName() {console.log(this.name);}}let person = new PersonClass(“Nicholas”);person.sayName(); // 输出 “Nicholas"console.log(person instanceof PersonClass); // trueconsole.log(person instanceof Object); // trueconsole.log(typeof PersonClass); // “function"console.log(typeof PersonClass.prototype.sayName); // “function"es6中类关键字class本质是一种语法糖,而使用类实现的继承其本质上就是原型的继承.为何要使用类的语法类声明不会被提升,这与函数定义不同。类声明的行为与 let 相似,因此在程序的执行到达声明处之前,类会存在于暂时性死区内。类声明中的所有代码会自动运行在严格模式下,并且也无法退出严格模式。调用类构造器时不使用 new ,会抛出错误。试图在类的方法内部重写类名,会抛出错误。作为一级公民的类在编程中,能被当作值来使用的就称为一级公民( first-class citizen ),意味着它能作为参数传给函数、能作为函数返回值、能用来给变量赋值。 JS的函数就是一级公民(它们有时又被称为一级函数),此特性让 JS 独一无二ES6 延续了传统,让类同样成为一级公民。这就使得类可以被多种方式所使用。例如,它能作为参数传入函数:function createObject(classDef) {return new classDef();}let obj = createObject(class {sayHi() {console.log(“Hi!”);}});obj.sayHi(); // “Hi!使用派生类进行继承ES6 之前,实现自定义类型的继承是个繁琐的过程。严格的继承要求有多个步骤。例如,研究以下范例:function Rectangle(length, width) {this.length = length;this.width = width;}Rectangle.prototype.getArea = function() {return this.length * this.width;};function Square(length) {Rectangle.call(this, length, length);}Square.prototype = Object.create(Rectangle.prototype, {constructor: {value:Square,enumerable: true,writable: true,configurable: true}});var square = new Square(3);console.log(square.getArea()); // 9console.log(square instanceof Square); // trueconsole.log(square instanceof Rectangle); // trueSquare 继承了 Rectangle ,为此它必须使用 Rectangle.prototype 所创建的一个新对象来重写 Square.prototype ,并且还要调用 Rectangle.call() 方法。这些步骤常常会搞晕 JS的新手,并会成为有经验开发者出错的根源之一。类让继承工作变得更轻易,使用熟悉的 extends 关键字来指定当前类所需要继承的函数,即可。生成的类的原型会被自动调整,而你还能调用 super() 方法来访问基类的构造器。此处是与上个例子等价的 ES6 代码:class Rectangle {constructor(length, width) {this.length = length;this.width = width;}getArea() {return this.length * this.width;}}class Square extends Rectangle {constructor(length) {// 与 Rectangle.call(this, length, length) 相同super(length, length);}}var square = new Square(3);console.log(square.getArea()); // 9console.log(square instanceof Square); // trueconsole.log(square instanceof Rectangle); // true使用 super() 时需牢记以下几点:你只能在派生类中使用 super() 。若尝试在非派生的类(即:没有使用 extends关键字的类)或函数中使用它,就会抛出错误。在构造器中,你必须在访问 this 之前调用 super() 。由于 super() 负责初始化this ,因此试图先访问 this 自然就会造成错误。唯一能避免调用 super() 的办法,是从类构造器中返回一个对象。总结ES6 的类让 JS 中的继承变得更简单,因此对于你已从其他语言学习到的类知识,你无须将其丢弃。 ES6 的类起初是作为 ES5 传统继承模型的语法糖,但添加了许多特性来减少错误。 ...

April 1, 2019 · 2 min · jiezi

es6之迭代器

起源何为迭代器?迭代器是被设计专用于迭代的对象,带有特定接口。所有的迭代器对象都拥有 next() 方法,会返回一个结果对象。该结果对象有两个属性:对应下一个值的 value ,以及一个布尔类型的 done ,其值为 true 时表示没有更多值可供使用。迭代器持有一个指向集合位置的内部指针,每当调用了 next() 方法,迭代器就会返回相应的下一个值。记住这些后,在 ES5 中创建一个迭代器就相当简单了:function ceateIterator(items) { var i = 0; return { next: function() { var done = (i >= items.length); var value = !done ? items[i++]: undefined; return { done: done, value: value }; } };}var iterator = createIterator([1, 2, 3]);console.log(iterator.next()); // “{ value: 1, done: false }“console.log(iterator.next()); // “{ value: 2, done: false }“console.log(iterator.next()); // “{ value: 3, done: false }“console.log(iterator.next()); // “{ value: undefined, done: true }”// 之后的所有调用console.log(iterator.next()); // “{ value: undefined, done: true }“可迭代对象 与 for-of循环与迭代器紧密相关的是,可迭代对象( iterable )是包含 Symbol.iterator 属性的对象。这个 Symbol.iterator 知名符号定义了为指定对象返回迭代器的函数。在 ES6 中,所有的集合对象(数组、 Set 与 Map )以及字符串都是可迭代对象,因此它们都被指定了默认的迭代器。可迭代对象被设计用于与 ES 新增的 for-of 循环配合使用。for-of在循环每次执行时会调用可迭代对象next()方法,并将结果value值保存在一个变量上,循环过程到done变成true时停止。let values = [1, 2, 3];for (let num of values) {console.log(num);}此代码输出如下123这个 for-of 循环首先调用了 values 数组的 Symbol.iterator 方法,获取了一个迭代器(对 Symbol.iterator 的调用发生在 JS 引擎后台)。接下来 iterator.next() 被调用,迭代器结果对象的 value 属性被读出并放入了 num 变量。 num 变量的值开始为 1 ,接下来是 2 ,最后变成 3 。当结果对象的 done 变成 true ,循环就退出了,因此 num 绝不会被赋值为 undefined 。访问默认迭代器你可以使用 Symbol.iterator 来访问对象上的默认迭代器,就像这样:let values = [1, 2, 3];let iterator = valuesSymbol.iterator;console.log(iterator.next()); // “{ value: 1, done: false }“console.log(iterator.next()); // “{ value: 2, done: false }“console.log(iterator.next()); // “{ value: 3, done: false }“console.log(iterator.next()); // “{ value: undefined, done: true }“既然 Symbol.iterator 指定了默认迭代器,你就可以使用它来检测一个对象是否能进行迭代,正如下例:function isIterable(object) {return typeof object[Symbol.iterator] === “function”;}console.log(isIterable([1, 2, 3])); // trueconsole.log(isIterable(“Hello”)); // trueconsole.log(isIterable(new Map())); // trueconsole.log(isIterable(new Set())); // trueconsole.log(isIterable(new WeakMap())); // falseconsole.log(isIterable(new WeakSet())); // false集合的迭代器ES6 具有三种集合对象类型:数组、 Map 与 Set 。这三种类型都拥有如下的迭代器,有助于探索它们的内容:entries() :返回一个包含键值对的迭代器;values() :返回一个包含集合中的值的迭代器;keys() :返回一个包含集合中的键的迭代器;entries()迭代器entries() 迭代器会在每次 next() 被调用时返回一个双项数组,此数组代表了集合中每个元素的键与值:对于数组来说,第一项是数值索引;对于 Set ,第一项也是值(因为它的值也会被视为键);对于 Map ,第一项就就是键。let colors = [ “red”, “green”, “blue” ];let tracking = new Set([1234, 5678, 9012]);let data = new Map();data.set(“title”, “Understanding ES6”);data.set(“format”, “ebook”);for (let entry of colors.entries()) {console.log(entry);}for (let entry of tracking.entries()) {console.log(entry);}for (let entry of data.entries()) {console.log(entry);}输出内容如下:[0, “red”][1, “green”][2, “blue”][1234, 1234][5678, 5678][9012, 9012][“title”, “Understanding ES6”][“format”, “ebook”]values() 迭代器values() 迭代器仅仅能返回存储在集合内的值,例如:let colors = [ “red”, “green”, “blue” ];let tracking = new Set([1234, 5678, 9012]);let data = new Map();data.set(“title”, “Understanding ES6”);data.set(“format”, “ebook”);for (let value of colors.values()) {console.log(value);}for (let value of tracking.values()) {console.log(value);}for (let value of data.values()) {console.log(value);}此代码输出了如下内容:“red"“green"“blue"123456789012"Understanding ES6"“ebook"keys() 迭代器keys() 迭代器能返回集合中的每一个键。对于数组来说,它只返回了数值类型的键,永不返回数组的其他自有属性; Set 的键与值是相同的,因此它的 keys() 与 values() 返回了相同的迭代器;对于 Map , keys() 迭代器返回了每个不重复的键。这里有个例子演示了这三种情况:let colors = [ “red”, “green”, “blue” ];let tracking = new Set([1234, 5678, 9012]);let data = new Map();data.set(“title”, “Understanding ES6”);data.set(“format”, “ebook”);for (let key of colors.keys()) {console.log(key);}for (let key of tracking.keys()) {console.log(key);}for (let key of data.keys()) {console.log(key);}本例输出了如下内容:012123456789012"title"“format” ...

April 1, 2019 · 2 min · jiezi

前端培训-初级阶段(13、18)

前端最基础的就是 HTML+CSS+Javascript。掌握了这三门技术就算入门,但也仅仅是入门,现在前端开发的定义已经远远不止这些。前端小课堂(HTML/CSS/JS),本着提升技术水平,打牢基础知识的中心思想,我们开课啦(每周四)。先来说一下为什么这节课跳课了,不是说中间的我们不讲了,而且上节课主讲人讲了 18,没办法我这节课补一下。收集上几周的反馈,普遍觉得内容多的超乎想象,所以之后的培训计划会根据内容适当调整。我们要讲什么上下左右居中的几种实现。ECMAScript核心语法结构上下左右居中的几种实现这个问题比较常见,咱们也简单说说吧。其实分为两种,一种行内结构,一种块结构。行内结构居中行内结构可以理解为文本,文本居中可以通过设置父元素的属性来实现。text-align: center 水平居中line-height: height; 垂直居中。行高和高设置为一样的值。vertical-align: middle; 垂直居中。这个属性是用来设置对齐方式的,通过伪元素构建一个 height:100% 然后设置居中就ok了。块级结构居中块结构的特点,占满整行,所以设置要点是设置自己的属性来实现。margin: auto; 水平居中,自动分配剩余空间,但是正常情况下,只有水平方向有剩余空间。position:fixed;top:0;right:0;bottom:0;left:0; 垂直水平居中,这个方法有个要点,就是定宽定高,不然就占满了。当然还有要 margin:auto 来分配剩余空间才可以。position:absolute;left:50%;margin-left:一半宽度 垂直水平居中,left 是基于父级来设置的,所以需要用 margin 再拉回来,也需要定宽高。position:absolute;left:50%;top:50%;transform: translate(-50%,-50%); 垂直水平居中,这个方案是上一个方案的优化版本,translate是基于自己的宽高来现实,所以可以用 -50% 来拉回。特殊的盒子实现居中这个东西就是说一个特殊模型,所以他自身就支持完成水平垂直居中table-cell vertical-align: middle;text-align:centerflex 就不用多说了吧,不懂的去看看上节课。还不懂就要挨锤了。grid margin: auto;ECMAScript 核心语法结构ECMAScript 是一种由 Ecma国际(前身为欧洲计算机制造商协会,英文名称是 European Computer Manufacturers Association)通过 ECMA-262 标准化的脚本程序设计语言。可以理解为是JavaScript的一个标准,但实际上 JS 是 ECMA-262 标准的实现和扩展。版本时间简述ECMAScript 11997年06月首版ECMAScript 21998年06月格式修正,以使得其形式与ISO/IEC16262国际标准一致ECMAScript 31999年12月强大的正则表达式,更好的文字链处理,新的控制指令,异常处理,错误定义更加明确,数输出的格式化及其它改变ECMAScript 4未完成更明确的类的定义,命名空间等等。2004年6月欧洲计算机制造商协会发表了ECMA-357标准,它是ECMAScript的一个扩延,它也被称为E4X(ECMAScript for XML)ECMAScript 52009年12月首版ECMAScript 2015 (ES6/ES2015)2015年6月17日截止发布日期,JavaScript的官方名称是ECMAScript 2015,Ecma国际意在更频繁地发布包含小规模增量更新的新版本,下一版本将于2016年发布,命名为ECMAScript 2016。从现在开始,新版本将按照ECMAScript+年份的形式发布。ECMAScript 2016 (ES7/ES2016)2016年 ECMAScript 2017 (ES8/ES2017)2017年 ECMAScript 2018 (ES9/ES2018)2018年 ECMAScript 20192019年 这一课我真的觉得 ruanyifeng大佬的就很棒 ,这里我先大体介绍一下,之后有时间会开单张来介绍一些常规用法。如:Array数组对象的forEach、map、filter、reduce –之前写的一篇,这样的章节。下面的介绍不全,只是其中的一部分let/var/const 的区别关键字绑定到顶层对象(特殊情况)变量提升块级作用域(if、for)描述varyesyesno会变量提升,可多次赋值,无块级概念(function、try 的块有)letnonoyes只可声明一次,可多次赋值constnonoyes只可以赋值一次字符串扩展repeat(n),重复字符串多少次,padStart(n,s),padEnd(n,s),字符串补全长度的功能,比如前面补 0模板字符串 反引号标识标签模板,其实也是一个偶然机会碰到这个东西的。有个prompt(1) to win,做这个题的时候发现了这种办法。 alert123 // 等同于 alert(123)正则的扩展数值的扩展isNaN() ,NaN是唯一一个自己不等于自己的。函数的扩展默认值 ,fucntion(a = 1){}默认值解构,// 写法一function m1({x = 0, y = 0} = {}) { return [x, y];}// 写法二function m2({x, y} = { x: 0, y: 0 }) { return [x, y];}rest 参数 ,代替 arguments 对象=> 函数()=>console.log(1) 等同于 function(){return console.log(1)}()=>{console.log(1)} 等同于 function(){console.log(1)}this对象绑定为定义时候的对象不可以当作构造函数不可以使用arguments对象数组的扩展扩展运算符,…[1,2,3]分开插入,可以用来替代 apply()Array.from(),将类数组转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。Array.of() 用来修复new Array(3)的异常find() 和 findIndex(),查找元素或者下标fill() 填充一个数组entries(),keys() 和 values() 遍历includes() 判断是否存在,用来替代~indexOfflat(),flatMap() 将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。还可以传入深度对象的扩展ProxyPromise 对象async 函数课后作业(能写几种写几种,越多越好)一行居中,多行居左,怎么实现?(水平居中)一行居中,多行居中,怎么实现?(垂直居中)实现一个重复字符串的函数。往期内容前端培训-初级阶段(1 - 4)前端培训-初级阶段(5 - 8)前端培训-初级阶段(9 - 12)参考资料ECMAScript –百度百科 介绍了一些历史JavaScript 实现 –w3school.com.cn介绍了JS由什么构成,值得一看JavaScript 高级教程 –w3school.com.cn一些语法基础ECMAScript 6 入门 –ruanyifeng如果你想学 ES6,这本书一定不要错过 ...

March 25, 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

ES6系列之目录

ES6系列之Promise, Generator, Async比较ES6系列之EslintES6系列之数值常用方法总结ES6系列之Iterator &Generator & AsyncES6系列之Set& MapES6系列之promiseES6系列之对象方法总结ES6系列之小知识点ES6系列之解构赋值ES5与ES6字符串方法总结ES5与ES6数组方法总结ES6系列之声明变量let与const

January 10, 2019 · 1 min · jiezi

js生成及下载浏览器端的文件-Eric

使用js生成及下载浏览器端的文件-Eric前言之前写了一篇文章一次下载多个文件的解决方案中写了如何下载服务器端的文件(支持连续下载多个),今天和大家分享下如何在浏览器端生成文件并下载。场景如果线上经常出现一些偶发性问题,本地又不好排查,我们只好添加一些追踪日志,当出现问题的时候我们能根据日志进行排查。解决方案首先我们先明确一点,IE浏览器不支持直接下载文件(禁用了反向和正向缓存),我们需要使用另存为功能,google浏览器我们可以直接下载。如果是为了记录日志,我们一般选择txt文件或者html文件(可以加样式,排版)。代码const downloadErrorLog = (fileName, content) => { // IE if(!!window.ActiveXObject || “ActiveXObject” in window) { var winname = window.open(’’, ‘_blank’); winname.document.open(’text/html’, ‘replace’); winname.document.writeln(content);// 如果是jsx代码,记得转化为html winname.document.execCommand(‘saveas’,’’, fileName); winname.close(); }else{ let aLink = document.createElement(‘a’); aLink.download = fileName; aLink.href = “data:text/plain,” + content; // 切记,content只能是字符串,如果是html元素,记得使用.innerHTML转换 aLink.click(); }}

January 9, 2019 · 1 min · jiezi

const & let

前世在 const & let 还未出现前,JS 的世界一直是 var的统治var:在JS中用于变量声明的关键字。特点:变量提升只有函数作用域或者全局作用域,没有块级作用域重复声明变量循环体重的闭包会出现问题…….变量提升function test(tag) { console.log(a,b); // a,b在下面声明,但是会打印出undefined,不会报错 if(10 >= tag) { var a = tag + 1; } else { var b = tag - 1; }}在浏览器预解析机制中,加载函数的时候,此时的作用域为函数作用域,函数作用域中JS会先将所有的声明置顶。function test(tag) { var a,b; // 将声明置顶,但是赋值并不会 console.log(a,b); // a,b在下面声明,但是会打印出undefined,不会报错 if(10 >= tag) { var a = tag + 1; } else { var b = tag - 1; }}只有函数作用域以及全局作用域,没有块级作用域function test(tag) { console.log(a,b); // a,b在下面声明,但是会打印出undefined,不会报错 if(10 >= tag) { var a = tag + 1; } else { var b = tag - 1; } console.log(a); // 9}test(8) // 9按照其他语言规则 if 是一个程序块,在 if 中声明的变量作用域只能在 if 中,但是 JS 因为只有函数作用域和全局作用域,所以才会导致在 if 判断外还可以访问 if 的变量重复声明变量var a = 1;var a = 2;console.log(a); // 2在使用var的时候允许重复声明变量也是令人头痛的事情,也许因为这个机制,可能就会出现bug循环体重的闭包会出现问题var arr = [];for(var i = 0; i < 3; i++) { arr.push(function () { return i; })}for(var j = 0; j < 3; j++) { console.log(arrj); // 3,3,3}将var -> let将会打印出 0,1,2因为缺乏块作用域所以导致问题出现今生如今距离ES6规范的出现已经过去了4年多了,在项目中也早已开始大量使用ES6规范编写代码了。var也不再是JS世界的唯一了,JS 世界出现了const & let。const & let 的出现给JS带来了块级作用域,解决了变量提升,禁止了重复声明变量,让JS少了很多疑惑的地方。let & const相同点:具有块级作用域禁止重复声明变量不会产生变量提升区别:let使用let声明的基本类型变量是可以改变值let a = 12;a = 13;return a; // 13使用let声明引用类型的变量是可以改变引用的let info = { name: “ming995”, age: 25, sayHi: function() { console.log(Hi I'm ${this.name}) }};let heInfo = {};heInfo = info;heInfo.name = “Jack”;console.log(heInfo);const使用const声明的基本类型变量是不可以改变值const a = 13;a = 14;return a; // 报错使用const声明引用类型的变量是不可以改变引用的const info = { name: “ming995”, age: 25, sayHi: function() { console.log(Hi I'm ${this.name}) }};const heInfo = {};heInfo = info; // 报错 heInfo.name = “Jack”;console.log(heInfo);但是我们可以操作const声明的引用类型的属性值const info = { name: “ming995”, age: 25, sayHi: function() { console.log(Hi I'm ${this.name}) }};info.language = “js”;console.log(info);总结之前对于const的理解有偏差,所以就写这篇文章。var时代已经过去了,ES6各种特性用起来。 ...

January 7, 2019 · 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

图学ES6-2.let与const命令

November 25, 2018 · 0 min · jiezi

1.ECMAScript 6简介

November 23, 2018 · 0 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

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 系列之 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

ES6 系列之 Generator 的自动执行

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

October 19, 2018 · 4 min · jiezi

ES6 系列之我们来聊聊 Promise

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

October 17, 2018 · 4 min · jiezi

express中使用es6

express官网上给的javascript标准为es5,是不能直接跑es6的,想要在express中使用es6写法,可以用转码器Babel进行转码。开发环境中express项目中安装babel-cli$ npm install –save-dev babel-cli安装presetsnpm install –save-dev babel-preset-es2015 babel-preset-stage-2在package.json里添加运行的脚本{ … “scripts”: { “start”: “babel-node index.js –presets es2015,stage-2” } …}到此就可以使用es6的写法了,写一段es6运行npm start刚开始学习express的时候,会遇到一个问题:每次改一点点代码,都需要重启服务。我们希望能够实现“热更新”的效果,接下来我们就可以使用nodemon监视文件修改,达到热更新效果,而不比每次都重启服务安装nodemonnpm install –save-dev nodemon修改脚本{ … “scripts”: { “start”: “nodemon index.js –exec babel-node –presets es2015,stage-2” } …}运行 npm start现在更改js代码,不需要重启服务,就可以实现效果了

October 17, 2018 · 1 min · jiezi

你会使用ES6 ,但这几点你可能不知道,感觉身体被掏空!

let 的使用我们知道 let 主要有三个特性:let 声明的变量的作用域是块级的 (es5中只有全局和函数作用域)let 不能重复声明已存在的变量;let 有暂时死区,不会被提升。我们早期面试的时候,经常会考的一道题目是:// 代码段1var liList = document.querySelectorAll(’li’) // 共5个lifor( var i=0; i<liList.length; i++){ liList[i].onclick = function(){ console.log(i) }}大家都知道依次点击 li 会打印出 5 个 5。如果把 var i 改成 let i,就会分别打印出 0、1、2、3、4:// 代码段2var liList = document.querySelectorAll(’li’) // 共5个lifor( let i=0; i<liList.length; i++){ liList[i].onclick = function(){ console.log(i) }}然而,你会发现 for( let i=0; i<liList.length; i++),这样声明 i 的作用域是哪个?这边直接跟你说吧,i 的作用域只在for()括号里面,所以代码中依然只声明了一个 i,在 for 循环结束后,i 的值还是会变成 5 才对。这里可能就说明有一部分的人对 let 理解有误了。于是我们方方老师大佬去看 MDN 的例子,发现鸡贼的 MDN 巧妙地避开了这个问题,它的例子是这样的:它直接在另外声明了一个 j 来保存, 为什么不直接用 i 呢?MDN 为了简化知识,隐瞒了什么?经过我们方方老师大佬饥渴精神,不断的查资料,最后他总结这么几句自己的理解:for( let i = 0; i< 5; i++) 这句话的圆括号之间,有一个隐藏的作用域for( let i = 0; i< 5; i++) { 循环体 } 在每次执行循环体之前,JS 引擎会把 i 在循环体的上下文中重新声明及初始化一次。也就是说上面的代码段2可以近似近似近似地理解为:// 代码段3var liList = document.querySelectorAll(’li’) // 共5个lifor( let i=0; i<liList.length; i++){ let i = 隐藏作用域中的i // 看这里看这里看这里 liList[i].onclick = function(){ console.log(i) }}那样的话,5 次循环,就会有 5 个不同的 i,console.log 出来的 i 当然也是不同的值。再加上隐藏作用域里的 i,一共有 6 个 i。这就是 MDN 加那句 let j = i 的原因:方便新人理解。总得来说就是 let/const 在与 for 一起用时,会有一个 perIterationBindings 的概念(一种语法糖)。let 到底有没有提升之前我写过一篇 《你一度模糊的javascript执行上下文详解》, 说到js在一段<script>或者一个函数,都会生成一个执行上下文,就是js会先把声明的变量都 拿出来,并初始化为 undefined,这就解释了为什么在 var x = 1 之前 console.log(x) 会得到 undefined。那let 的执行上下文,会是怎么样的呢?假设代码如下:{ let x = 1 x = 2}我们只看 {} 里面的过程:找到所有用 let 声明的变量,在环境中「创建」这些变量开始执行代码(注意现在还没有初始化)执行 x = 1,将 x 「初始化」为 1(这并不是一次赋值,如果代码是 let x,就将 x 初始化为 undefined)执行 x = 2,对 x 进行「赋值」这就解释了为什么在 let x 之前使用 x 会报错:let x = ‘global’{ console.log(x) // Uncaught ReferenceError: x is not defined let x = 1}原因有两个:console.log(x) 中的 x 指的是下面的 x,而不是全局的 x执行 log 时 x 还没「初始化」,所以不能使用(也就是所谓的暂时死区)看到这里,你应该明白了 let 到底有没有提升:let 的「创建」过程被提升了,但是初始化没有提升。var 的「创建」和「初始化」都被提升了。function 的「创建」「初始化」和「赋值」都被提升了。get 与 set 的方法Get指读取属性时调用的函数Set指写入属性时调用的函数。有这么一道题是这样的:有没有可能存在一个变量同时满足下列条件:a === 1 && a === 2 && a ===3有的人可能会觉得这怎么可能变成立呢,一个变量两个值?这边就要引入我们set的方法了,我们可以在window,设置一个属性a ,如下定义:var i = 0;Object.defineProperty(window, ‘a’,{ get:function() { i += 1; return i; },})通过在window定义一个a,然后每次访问a的 i 的值会加作为a的值返回,所以我们在控制台输入:当然这个除了面试感觉没啥软用,但这能告诉你 get, set有什么用。Symbol的巧用我们知道:ES 6 引入了一个新的数据类型 Symbol, Symbol 可以创建一个独一无二的值(但并不是字符串)。假如现在有一个对象,里面有一个私有属性,只能自己内部使用,外部调用不到,这时我们可以利用 Symbol来做:如下:{ let a = Symbol(); let obj = { name: ‘小智’, age: 18, [a]: ‘这个是私有属性’ } window.obj = obj;}如下,我有在外部无语你怎么用,都是调用不到 “这个是私有属性”,你可能会说这个有啥用,我只能回答你装x用的,面试可能也会问你 我们如何弄一个隐藏属性呢?答案就是 Symbol ,满婚满婚满婚。以上,就是 Symbol 的简述,更详细更权威的知识参考下面的自学链接。MDN: Symbol - JavaScript阮一峰:ECMAScript 6入门参考:我用了两个月的时间才理解 let一个笨笨的码农,我的世界只能终身学习 ...

October 4, 2018 · 2 min · jiezi

那些必会用到的 ES6 精粹

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

September 20, 2018 · 7 min · jiezi

es6--->数组

常见的伪数组对象伪数组对象本质:必须有length属性。1、DOM操作返回的NodeList集合。下面的nodeList就是一个伪数组对象。 var nodeList = document.querySelectorAll(’li’);2、函数内部的arguments对象。将伪数据对象转换为真正数据的两种方式方法一:扩展运算符。使用扩展运算符的前提是:使用者必须是已经部署了Iterator接口。例如,字符串,Set结构是都具有Iterator接口的。(扩展运算符背后调用的是遍历器接口)从下图可以看出扩展运算符真的把伪数组对象转换成了真正的数组。方法二:Array.from()可以将类数组对象和可遍历对象都转换为真正的数组,任何有length属性的对象,都可以通过Array.from()方法转为数组。下图含有es6和es5的转换数组的方法。注意:Array.from()还有一个用途:将字符串转为数组,然后返回字符串的长度。因为它能正确的处理各种Unicode字符,可以避免JavaScript将大于uFFFF的Unicode字符算作两个字符的bug。Array.of()用途:将一组值转换为数组。优点:弥补了Array()的不足。例子:从下面例子可以看出:Array()如果只有一个参数的时候,这个参数实际上是指定数组的长度。和多参的时候表现不一致。Array.of()总是返回参数值组成的数组。copyWithin()用途:将指定位置的成员复制到其他位置(会覆盖所有的成员),并返回当前数组。Array.prototype.copyWithin(a,b,c);其中参数a是必选的。参数b,c是可选的,可以为负数。find()和findIndex()find():找出第一个符合条件的成员。findIndex():找出第一个符合条件成员的位置。优点:这两个方法都可以发现NaN。find,findIndex实例有关NaN的实例fill()作用:用来初始化空数组比较方便。他会把数组中已有的元素会被全部抹去。fill(a,b,c)a:填充的数据。b:指定填充的起始位置。c:指定填充的结束位置。es6新增遍历数组的方法entries():对键值对的遍历。keys():对键名的遍历。values():对键值的遍历。这三个方法都能返回一个遍历器对象。可以用for…of循环遍历。keys的用法:entries的用法:values的用法:includes()用途:表示某个数组是否包含给定的值。[].includes(a)a:测试数据。b:起始位置,可以是负数。优点:弥补了indexOf的两个缺点,(1)不够语义化.(2)indexof内部必须使用严格运算符(===)(3)indexOf不能识别NaN,但是include可以。关于NaN的查找查看某个元素是否存在注意:很多数组的方法,对空位的处理规则都不一样,所以最好避免空位的出现。

August 30, 2018 · 1 min · jiezi