ES的解构赋值数组对象

什么是解构?按照一定的模式从数组或者对象中取值,对变量进行赋值的过程称为「解构」 在ES5中,为变量赋值只能直接指定值: var a=1,b=2,c=3a; // 1b; // 2c; // 3但是在ES6中,我们可以被允许写成: var [a,b,c]=[1,2,3];a; // 1b; // 2c; // 3ES6中可以很明显看出来,我们可以在数组中取数据,按照位置的对应关系对变量赋值。 [默认值]解构赋值允许使用默认值var [foo = true] = [];foo; // true[x,y = 'b'] = ['a']x; // "a"y; // "b"[x,y = 'b'] = ['a','c']x; // "a"y; // "c"ES6内部使用的是严格相等运算符(===)判断一个位置是否有值。所以,如果一个数组成员不严格等于undefind,默认值是不会生效的。 var [x = 1] = [undefined];x; // 1null == undefined // truevar [x = 1] = [null];x; // null上述代码中,一个数组成员是null,因此默认值不生效。因为null不严格等于undefined。 function f(){ console,log('aaa');}let [x = f()] = [1]; // undefined ,不执行 f()x; // 1对象的解构赋值var {foo,bar}={foo:"aaa",bar:"bbb"};foo; // "aaa"bar; // "bbb"对象的解构赋值和数组有一个不同,数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。 ...

June 5, 2019 · 1 min · jiezi

前端漫谈3-从-filter-聊到-Promise

前言在学习前端的时候,我总是能听到很多高级词汇,比如今天会聊到的 函数式编程(Functional Programming) & 高阶函数 (Higher-order function) 。但是当你真正的理解什么是 函数式编程 & 高阶函数 的时候,也许会发现,你几乎每天都会用到它,只是你不知道那就是高阶函数 / 函数式编程。 JavaScript 中的函数在 javascript 中,函数是一种值,举个例子: const double = function (x) { return x * 2}我们把一个函数作为值,赋给了变量 double ,这在我们的代码中很常见对吗? 你是不是经常会听到或者看到这样一句话:“在 JavaScript 中函数是一等公民” 粗看很不好理解,但是它的意思很简单:函数和 字符串/number 没有什么不一样,它可以声明为变量,也可以作为参数传入到其他函数中。 什么是高阶函数?什么是高阶函数?其实上一段我们已经说过了,我们可以把函数A作为参数传入到另一个函数B中,那么接收函数作为参数的函数B,就是 高阶函数 ,这只是方便大家理解,高阶函数的定义是: "一个函数的参数是另一个函数,或者一个函数的返回值是另一个函数" 高阶函数的例子filter说到 filter() 你肯定不陌生,他接收一个回调函数作为它的参数,所以它是一个典型的高阶函数,举个例子: 我们有这么一个数组,要筛选出对应 category 为 html&css 的书籍。 const books = [ {name:'gitbook',category:'git'}, {name:'reactbook',category:'react'}, {name:'vuebook',category:'vue'}, {name:'cssbook',category:'html&css'}, {name:'htmlbook',category:'html&css'}, ]传统的写法是这样: let html_css_books = []for (let i = 0; i < books.length; i++) { if(books[i].category === 'html&css'){ html_css_books.push(books[i]) }}console.log(html_css_books)我相信几乎没有人会选择上面的方式,大部分人都会用 filter ...

June 4, 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

拆弹时刻小程序canvas生成海报二优化方案

海报生成速度缓慢问题的优化 微信头像在app.js中预先加载缓存多图片异步加载流程中断处理 二次授权失败的处理请求或者下载图片失败处理保存图片可被压缩海报生成速度缓慢问题的优化原因分析: 主要的时间消耗在于getImageInfo网络请求获取头像和下载图片获得临时地址的过程,可以看到海报中有3张图片(微信头像、主图、动态二维码(对应不同新闻的ID))需要下载,接下来主要就是对这3张图的优化 微信头像在app.js中预先加载缓存//app.js//可以在app.js中使用小程序默认的全局变量,将头像在加载的时候预先缓存App({ onLaunch: function () { // 获取用户信息 wx.getSetting({ success: res => { if (res.authSetting['scope.userInfo']) { // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框 wx.getUserInfo({ success: res => { this.globalData.userInfo = res.userInfo; //从返回值中获取微信头像地址 let WxHeader = res.userInfo.avatarUrl; wx.getImageInfo({ src: WxHeader,//下载微信头像获得临时地址 success: res => { //将头像缓存在全局变量里 this.globalData.avatarUrlTempPath = res.path; }, fail: res => { //失败回调 } }); } }) } } }) }, globalData: { userInfo: null, //如果用户没有授权,无法在加载小程序的时候获取头像,就使用默认头像 avatarUrlTempPath: "./images/defaultHeader.jpg" }})大致思路是: 加载App.js的时候==> getSetting(判断是否授权) ==> getUserInfo(获取头像) ==> getImageInfo(生成临时地址) 将需要的网络请求在加载小程序的时候就异步完成,提前将临时地址缓存在全局变量globalData中,这样当用户进入新闻页面,点击生成海报的时候就不需要在请求微信头像,缩短了不少时间。 注意: 如果用户一开始没有微信授权,生成海报时又必须要用户头像不能使用默认的话,那就只能老老实实走之前的流程了。 ...

May 31, 2019 · 2 min · jiezi

详解ES6中的class基本概念

用构造函数,生成对象实例: 使用构造函数,并且new 构造函数(), 后台会隐式执行new Object() 创建对象将构造函数的作用域给新对象,(即new Object() 创建出的对象),函数体内的this代表new Object() 出来的对象执行构造函数的代码返回新对象( 后台直接返回)function Person1(name, age) { this.name = name this.age = age}Person1.prototype.say = function () { return "My name is " + this.name + ", I'm " + this.age + " years old."}var obj = new Person1("Simon", 28);console.log(obj.say()); // My name is Simon, I'm 28 years old.用class改写上述代码: 通过class关键字定义类,使得在对象写法上更清晰,让javascript更像一种面向对象的语言在类中声明方法的时,不可给方法加function关键字class Person2 { // 用constructor构造方法接收参数 constructor(name, age) { this.name = name; // this代表的是实例对象 this.age = age; } // 类的方法,此处不能加function say() { return "My name is " + this.name + ", I'm " + this.age + " years old." }}var obj = new Person2("Coco", 26);console.log(obj.say()); // My name is Coco, I'm 26 years old.ES6中的类,实质上就是一个函数类自身指向的就是构造函数类其实就是构造函数的另外一种写法console.log(typeof Person2); // functionconsole.log(Person1 === Person1.prototype.constructor); // trueconsole.log(Person2 === Person2.prototype.constructor); // true构造函数的prototype属性,在ES6的class中依然存在:// 构造1个与类同名的方法 -> 成功实现覆盖Person2.prototype.say = function () { return "证明一下:My name is " + this.name + ", I'm " + this.age + " years old."}var obj = new Person2("Coco", 26);console.log(obj.say()); // 证明一下:My name is Coco, I'm 26 years old.// 通过prototype属性对类添加方法Person2.prototype.addFn = function () { return "通过prototype新增加的方法addFn"}var obj = new Person2("Coco", 26);console.log(obj.addFn()); // 通过prototype新增加的方法addFn通过Object.assign方法来为对象动态增加方法:Object.assign(Person2.prototype, { getName: function () { return this.name; }, getAge: function () { return this.age; }})var obj = new Person2("Coco", 26);console.log(obj.getName()); // Cococonsole.log(obj.getAge()); // 26constructor方法是类的构造函数的默认方法new生成对象实例时,自动调用该方法class Box { constructor() { console.log("自动调用constructor方法"); // 实例化对象时,该行代码自动执行 }}var obj = new Box();若没有定义constructor方法,将隐式生成一个constructor方法: ...

May 30, 2019 · 2 min · jiezi

Promiseasyncawait深入了解

1、Promise 的含义Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。 Promise对象有以下两个特点。 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、resolved(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。 Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

May 30, 2019 · 1 min · jiezi

前端培训初级阶段场景实战20190606下载文件下载进度

前端最基础的就是 HTML+CSS+Javascript。掌握了这三门技术就算入门,但也仅仅是入门,现在前端开发的定义已经远远不止这些。前端小课堂(HTML/CSS/JS),本着提升技术水平,打牢基础知识的中心思想,我们开课啦(每周四)。 这两天,碰到了不止一次前端下载的的问题。其实之前我写过一篇文章 download使用浅析,主要依靠 download 属性来实现浏览器端下载,因为是走浏览器的下载,所以没有进度条。今天我们就来说说我的解决方案。 sf 的一个问题,需要显示进度条。答案地址,问题地址一个朋友的问题,下载的文件需要 headers 验证,无奈只能 ajax 拿数据,但是拉回来的还是字符串,需要自己处理。一个朋友的问题,监测下载进度。今天我们要讲什么?如何使用 download 属性,下载文件。这节主要是讲如何使用,以及前端下载的核心操作。下载文件,并显示进度条。这节是正常操作,如果你只为了解原理,看到这里就够了其他数据类型如何互相转换这节就不一样了。因为之前的 api 是使用 blob 实现,但是 ajax 传回来的数据有好多种类型,我们如何将他们相互转换?如何使用 download 属性,下载文件。download使用浅析 这一文中已经介绍了,可以去看看。我这里简单说一下。<a>标签如果设置了 download 属性,他就会去下载这个地址。测试地址-原生 download 属性测试。 下载文件,并显示进度条下载文件上面已经实现了,那我们先说说如何显示进度条。 显示进度条其实浏览器也是有进度条的,但是咱们拿不到。那我们就来模拟一下载,然后显示进度条。 ajax 实现下载进度条,测试地址-显示进度条 xhr = new XMLHttpRequest();xhr.open('get', file1.url);xhr.onprogress = (e)=>console.log(e)//e 就是一个 ProgressEvent 对象,其中 loaded 是已下载的, total 是总大小。xhr.send()fetch 实现下载进度条,测试地址-fetch显示进度条并下载fetch 的实现上来说有一些功能是没有的,比如 abort、进度等。那我们就需要去通过一些别的手段来模拟实现。实现代码如下,我们操作成读流,然后统计长度。下载文件进度条已经显示好了,那我们可以下载文件了。首先我们要分几种情况 缓存下载(一个资源如果已经下载完了,再次去访问)本地下载(资源已经在浏览器中) blob url 下载 如这种地址 blob:https://www.lilnong.top/deb4c297-821c-4545-9b23-0fbdd76890c7base64 url 下载 如这种地址 data:application/octet-stream;base64,aGVsbG8gbGlub25n blob = new Blob(['hello linong']) freader = new FileReader() freader.readAsDataURL(blob)//将 blob 读成 dataurl freader.onload=e=>console.log(freader.result)// 异步的,所以需要回调里面拿 ...

May 28, 2019 · 1 min · jiezi

彻底弄懂ES6中的Map和Set

摘要: 2个很有用的数据结构。 原文:彻底弄懂ES6中的Map和Set作者:pubdreamccFundebug经授权转载,版权归原作者所有。 MapMap对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。构造函数Map可以接受一个数组作为参数。 Map和Object的区别 一个Object 的键只能是字符串或者 Symbols,但一个Map 的键可以是任意值。Map中的键值是有序的(FIFO 原则),而添加到对象中的键则不是。Map的键值对个数可以从 size 属性获取,而 Object 的键值对个数只能手动计算。Object 都有自己的原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。Map对象的属性 size:返回Map对象中所包含的键值对个数Map对象的方法 set(key, val): 向Map中添加新元素get(key): 通过键值查找特定的数值并返回has(key): 判断Map对象中是否有Key所对应的值,有返回true,否则返回falsedelete(key): 通过键值从Map中移除对应的数据clear(): 将这个Map中的所有元素删除const m1 = new Map([['a', 111], ['b', 222]])console.log(m1) // {"a" => 111, "b" => 222}m1.get('a') // 111const m2 = new Map([['c', 3]])const m3 = new Map(m2)m3.get('c') // 3m3.has('c') // truem3.set('d', 555)m3.get('d') // 555遍历方法 keys():返回键名的遍历器values():返回键值的遍历器entries():返回键值对的遍历器forEach():使用回调函数遍历每个成员const map = new Map([['a', 1], ['b', 2]])for (let key of map.keys()) { console.log(key)}// "a"// "b"for (let value of map.values()) { console.log(value)}// 1// 2for (let item of map.entries()) { console.log(item)}// ["a", 1]// ["b", 2]// 或者for (let [key, value] of map.entries()) { console.log(key, value)}// "a" 1// "b" 2// for...of...遍历map等同于使用map.entries()for (let [key, value] of map) { console.log(key, value)}// "a" 1// "b" 2map与其他数据结构的互相转换 ...

May 28, 2019 · 2 min · jiezi

数据类型js的数据类型

一、js的数据类型有基本数据类型和引用类型基本数据类型包括:undefined, null,boolean,number,string 引用类型是object:包括function,array,Date... 【es6新增类型】1. set 集合,区别于数组的就是 set中不可以有重复的数据,常可以用来做去重操作 含有size属性,集合里元素的个数add 方法,返回的是原对象改变后的值delete方法,返回值是true/falsehas('data1') 返回值是true/falsemyset.keys(); myset.value() 2. Map类型,与对象的区别是:对象的键值只能是字符串,不可以是对象类型,使用Map类型可以避免键值必须是字符串的限制,可以是对象,数组等 创建的时候一个大的数组,每一项元素是小数组,小数组有两个元素,分别是一组对应的key,value。方法:set(key,value); get(key);delete(key); has(key); //返回值均是true/falsemymap.forEach(function (value,key) { console.log(key+":"+value);}) // 遍历,参数是value,key 【注意顺序】mymap.set({},"obj1");mymap.set({},"obj2"); 这是两个不同的键值,不会被覆盖 3.Symbol类型 用相同的字符串做属性名命名的时候会发生命名冲突,使用Symbol产生的名字是不同的 Symbol 即便传入相同的参数,两者的值也是不一样的 例:Symbol('foo') !== Symbol('foo');如果想让两者相等,可以使用Symbol.for() 例 Symbol.for('bar') === Symbol.for('bar')但是 Symbol('bar') !== Symbol.for('bar');如果Symbol作为属性名,不会被Object.keys() Object.getOwnPropertyNames()、JSON.stringify()返回;该属性也不会出现在for...in、for...of循环中。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有 Symbol 属性名。typeof null返回值是 “object”,null会被认为是一个空对象的引用null == undefined //true当定义一个变量用来保存对象,就可以将这个变量初始化为null 实现继承function Person(){}function Student(){}Student.prototype = new Person()Student.prototype.constructor= Studentvar bosn= new Student()bosn instanceof Student //truebosn instanceof Person // true 二、判断类型的方式typeof | instanceof | Object.prototype.toString.apply() ...

May 28, 2019 · 1 min · jiezi

js异步从入门到放弃四-Generator-封装异步任务

在之前的文章介绍了传统异步的实现方案,本文将介绍ES6中的一种全新的异步方案--Generator函数。 generator简介简单介绍一下generator的原理和语法,(更详细内容请看ECMAScript 6 入门,本文只介绍和异步相关的核心内容) 基本语法通过一个简单的例子来了解generator函数 function* MyGenerator() { yield 'yield的含义是:执行此处时,暂停执行当前函数' yield '暂停之后的函数可以用next方法继续执行' return '遇到return之后会真正结束,done会变成true'}const gen = MyGenerator() //执行后返回的是一个指针,可以利用该指针执行next方法console.log(gen.next()) // {value: "yield的含义是:执行此处时,暂停执行当前函数“, done: false}console.log(gen.next())// {value: "暂停之后的函数可以用next方法继续执行", done: false}console.log(gen.next())// {value: "遇到return之后会真正结束,done会变成true", done: true}数据交互数据交互指的是可以通过yield把当前执行的结果传出,也可以使用next把外部参数传入 function* MyGenerator(){ const result = yield '传出的数据' return result } const gen = MyGenerator() console.log(gen.next())// generatorAndAsync2.html:19 {value: "传出的数据", done: false} console.log(gen.next('传入的数据'))// {value: "传入的数据", done: true}交互的参数类型当然也可以是函数: function sayHi(){ console.log('hi'); } function* MyGenerator(){ const result = yield sayHi() return } const gen = MyGenerator() const result = gen.next()// {value: sayHi, done: false} result.value() // 正常输出'hi'具备以上最核心的两个特性之后,generator就可以进行异步操作封装。 ...

May 26, 2019 · 2 min · jiezi

ECMASCRIPT-6-实战之-扩展运算符

扩展运算符(spreading)是 ECMASCRIPT 6(以下简称ES 6) 中又一非常好用的实战技术, 它的写法只需要三个点(...),作用则顾名思义,用来展开你想要使用的任意变量,本质上是对所有拥有迭代器接口(Iterator)的对象进行迭代。 典型应用用于展开(迭代)数组元素 const labels = ['ES 5', 'ES 6', 'React.js', 'Vue.js', 'Node.js']; console.log('label elements: ', ...labels); // ES 5 ES 6 React.js Vue.js Node.js展开未使用的键值, 并放到剩余参数对象中去 数组中的扩展剩余参数 只取想要使用的第一个变量 const labels = ['javascript', 'ES 5', 'ES 6', 'React.js', 'Vue.js', 'Node.js', 'React-Native']; const [main, ...rest] = labels; main // 'javascript' rest // ["ES 5", "ES 6", "React.js", "Vue.js", "Node.js", "React-Native"]; 对象中的剩余参数 const editor = { id: '1688', name: 'handome_boy', age: 18, male: 1 } 只想要使用 id 字段, 其它字段需要另作它用时: const { id, ...rest } = editor; console.log(id); // 1688 console.log(rest); // { name: 'handome_boy', age: 18, male: 1 }经典实战复制数组 const labels = ['segementfault', '好看', '实用']; const copy_array = [...labels]; 把扩展后的 labels 的元素又放在一个新的数组字面量中, 即可得到一个新数组, 新数组与旧数组是使用不同的内存空间 labels === copy_array // false 效果类似 slice, 但用法简洁, 谁用谁喜欢 : )合并数组 const tag1 = ['前端', '设计', '产品']; const tag2 = ['后端', '数据库', '缓存']; const merge_tags = [...tag1, ...tag2]; console.log(merge_tags); // ['前端', '设计', '产品', '后端', '数据库', '缓存']; 相比于 tag1.concat(tag2); 扩展运算符的用法简直简约到极致, 除了理解容易, 也具有几何的对称美合并对象 const response = { itemid: 1068, name: 'segementfault', tags: ['前端', '设计', '产品'], pv: 8888 } const merged_response = { ...response, name: '掘银', tags: ['后端', '数据库', '缓存'] }; console.log(merged_response); // { itemid: 1068, name: 'segementAdult', tags: ['后端', '数据库', '缓存'], pv: 8888 } 效果形同 Object.assign, 也是右边的同名字段会覆盖左边的同名字段, 但谁更简洁, 一目了然合并剩余参数 在定义函数时, 把用不到的参数合并到一个对象中, 集中管理 const calcalute_date = (mode = 'fullDate', ...rest) => { if(mode === 'timestamp') { return Date.now(); } return rest.join('-'); } calcalute_date('fullDate', '2019', '05', '26'); // 2019-05-26在 React 中 透传 props const Button = props => { const { title = '确定', style = {}, ...rest } = props; return ( <div {...rest} style={{ ...confirmButtonStyle, ...style }}>{ title }</div> ) } 组件的某些属性不需要特意从 props 中解构出来, 那就使用 rest 从组件最外层透传进来, 例如 onClick, 或是某些自定义事件, 这样即使组件定义 简单优雅, 也达到了支持透传任意多的属性/方法的目的 另外本例也在style中使用扩展运算符作了 合并样式组件(style)的操作三个小点,身材小巧,功能强大,还犹豫什么,实际开发中, 走你!!! ...

May 26, 2019 · 2 min · jiezi

填坑手册小程序Canvas生成海报完整版

海报生成示例 最近智酷君在做[小程序]canvas生成海报的项目中遇到一些棘手的问题,在网上查阅了各种资料,也踩扁了各种坑,智酷君希望把这些“填坑”经验整理一下分享出来,避免后来的兄弟重复“掉坑”。 原型图 这是一个大致的原型图,下面来看下如何制作这个海报,以及整体的思路。 <center>海报生成流程</center> 下面分享下主要的代码内容和“填坑现场”:一、添加字体https://developers.weixin.qq.com/miniprogram/dev/api/canvas/font.html canvasContext.font = value //示例ctx.font = `normal bold 20px sans-serif`//设置字体大小,默认10ctx.setTextAlign('left');ctx.setTextBaseline("top");ctx.fillText("《智酷方程式》专注研究和分享前端技术", 50, 15, 250)//绘制文本符合 CSS font 语法的 DOMString 字符串,至少需要提供字体大小和字体族名。默认值为 10px sans-serif 文字过长在canvas下换行问题处理(最多两行,超过“...”代替)ctx.setTextAlign('left');ctx.setFillStyle('#000');//文字颜色:默认黑色ctx.font = `normal bold 18px sans-serif`//设置字体大小,默认10let canvasTitleArray = canvasTitle.split("");let firstTitle = ""; //第一行字let secondTitle = ""; //第二行字for (let i = 0; i < canvasTitleArray.length; i++) { let element = canvasTitleArray[i]; let firstWidth = ctx.measureText(firstTitle).width; //console.log(ctx.measureText(firstTitle).width); if (firstWidth > 260) { let secondWidth = ctx.measureText(secondTitle).width; //第二行字数超过,变为... if (secondWidth > 260) { secondTitle += "..."; break; } else { secondTitle += element; } } else { firstTitle += element; }}//第一行文字ctx.fillText(firstTitle, 20, 278, 280)//绘制文本//第二行问题if (secondTitle) { ctx.fillText(secondTitle, 20, 300, 280)//绘制文本}通过 ctx.measureText 这个方法可以判断文字的宽度,然后进行切割。(一行字允许宽度为280时,判断需要写小点,比如260) ...

May 23, 2019 · 2 min · jiezi

30分钟用Nodejs构建一个API服务器

翻译:疯狂的技术宅原文:https://medium.freecodecamp.o...本文首发微信公众号:前端先锋欢迎关注,每天都给你推送新鲜的前端技术文章 Node.js 对初学者来说可能是令人望而却步的,其灵活的结构和缺乏严格的规范使它看起来很复杂。 本教程是 Node.js,Express 框架和 MongoDB 的快速指南,重点介绍基本的 REST 路由和基本的数据库交互。你将构建一个简单的 API 框架模版,然后可以将其用作任何应用。 本教程适用于:你应该对 REST API 和 CRUD 操作有基本的了解,还有基本的 JavaScript 知识。我用的是 ES6(主要是箭头函数),但并不是很复杂。 在本教程中,我们将为创建一个网络笔记应用的后端骨架 —— 类似于Google Keep,能够执行所有的四个CRUD操作:创建、读取、更新和删除。 配置如果你没有安装Node,请参阅此处。 创建一个新目录,运行 npm init,然后按照提示操作,把你的应用程序命名为“notable”(或者你可能喜欢的其他名字)。 npm init一旦完成,在你的目录中会有一个 package.json 文件。你可以开始安装项目所需的依赖项了。 我们将使用 Express 作为自己的框架,MongoDB 作为数据库,还有一个名为 body-parser 的包来帮助处理 JSON 请求。 npm install --save express mongodb@2.2.16 body-parser我还强烈建议将 Nodemon 安装为 dev 依赖项。这是一个非常简单的小包,可在文件被更改时自动重启服务器。 如果你运行: npm install --save-dev nodemon然后将以下脚本添加到 package.json: // package.json "scripts": { "dev": "nodemon server.js" },完整的 package.json 应如下所示: ...

May 23, 2019 · 5 min · jiezi

ES6入门之对象的新增方法

1. Object.is()用来解决在ES5中 两种相等运算符的缺点。用来比较两个值是否严格相等,行为和(===)基本一致。在ES5中判断两个值是否相等,只能用(==)相等运算符和(===)严格相等运算符,但是这两货都有缺点,前者 两边的值都会转换数据类型,后者 NaN不等于自身还有 +0 == -0。 Object.is('foo', 'foo') // trueObject.is({}, {}) // false// 在 Object.is()+0 === -0 // trueNaN === NaN // falseObject.is(+0, -0) // falseObject.is(NaN, NaN) // true 2. Object.assign()用于对象的合并,将源对象的所有可枚举属性,复制到目标对象,现在常用来进行浅拷贝。const t = {a: 1}const s1 = {b: 2}const s2= {c: 3}Object.assign(t, s2, s2)// t {a:1, b:2, c:3}// 在上面中第一个参数t是目标对象,s1 s2是源对象注意:如果目标对象和源对象有同名属性,或者多个同名,那么在后面的属性会将前面的覆盖。 const t = {a: 1, b: 2}const s1 = {b: 3, c: 4}const s2 = {c: 5}Object.assign(t, s1, s2)t // {a:1, b:3, c:5}如果Object.assign的参数只有一个,那么就返回该参数,另外如果该参数不是对象,那么Object.assign会将其转为对象在返回const t = {a: 2}Object.assign(t)t // {a: 2}Object.assigin(2)// "object"另外由于null 和 undefined 无法转换位对象,那么如果他们作为第一个参数就会报错,如果不是在第一个参数则不会有这个问题Object.assign(undefined) // 报错Object.assign(null) //报错这样就不会报错:如下const t = {a: 2}Object.assign(t, undefined) // trueObject.assign(t, null) // true其他(数值,字符串,布尔值)数值不再第一个也不会报错,但是字符串会以数组的形式被拷入目标对象,两外两个则不会做任何操作。const a = 'abc'const b = trueconst c = 12const o = Object.assign({}, a, b, c)o // {"0": "a", "1": "b", "2": "c"}// 因为布尔值number 的原始值都在对象的内部属性[[PrimitiveValue]]上面,这个属性不能别Object.assign拷贝Obeject.assign 拷贝的属性是有限制的,只能拷贝源对象的自身属性,也不能拷贝不可枚举的属性,另外 Symbol值的属性也能被拷贝 ...

May 22, 2019 · 2 min · jiezi

ES6入门之对象的扩展

1. 属性的简洁表示法在ES6中 允许直接写入变量和函数,作为对象的属性和方法,使得代码的书写更为简洁。const f = 'a'const b = {f}b // {f: 'a'}等同于const b = {f: f}在ES6中允许在对象内直接写变量,这时候属性名为变量名,属性值就是变量值 function u(x, y){ return {x, y}}// ====function u(x, y){ return {x: x, y: y }}u(1, 2) // {x:1, y: 2}或者一下写法:function o() { const x = 1; const x = 2; return {x, y}}o() // {x:1, y:2}2. 属性名表达式在JavaScript中属性名的表达式的方法有两种,一种 直接用标识符作为属性名,第二种用表达式作为属性名。第二种写的时候表达式要放在方括号之内//一obj.foo = true//二obj['a' + 'bc'] = 123//三let t = 'm'let obj = { [t]: true, ['a' + 'bc']: 123}表达式还可以用来定义方法名(注意:属性名表达式不能和简洁表示法同时使用) ...

May 22, 2019 · 2 min · jiezi

es6模板字符串

es6引入模板字符串来解决传统拼接字符串1.插入变量不友好2.多行不能正常换行的问题。 1.解决插入变量不友好 这是传统拼接字符串写法: 模板字符串写法:用反引号把字符串括起来,变量写在${}里就ok了。 2.解决多行不能正常换行

May 22, 2019 · 1 min · jiezi

ES6项目小练习TodoList15

ES6技术本身非常容易,相信大家也体会到了。各种新特性都不难,但是为什么很多人学习起来困难呢? 其实ES6难的不是技术,而是实际工作环境的搭建。比如我们想写一个简单的ES6版本的TodoList. 很多同学学生是这么放弃的: 通过搜索引擎,发现很多教程都是直接引入Traceur.js 然后讲解ES6各种功能和语法的,但是好像实际并不是直接引入Traceur.js ,而是用babel。 使用babel的话好像需要安装node.js,还有会用npm 安装好npm 以后我是该使用gulp还是webpack呢? 嗯,应该webpack吧,这个好像目前更主流。好,就选它了。 webpack怎么安装?是不是需要webpack-cli?另外webpack 4.0好像更好用,但是好像安装又有兼容性问题? 纠结ing…… 考虑了半天后,终于下定决心,就用最新版本,学老的没用。 安装好webpack 4.0,对了是不是要配置工作流? 对配置吧,这才是“工作的方式”,js需要压缩,装个压缩插件,webpack怎么用啊?有各种rule,plugins,还有entry. 开始头疼,耐着性子把网上的教程配置完,这个插件怎么报错了啊? 继续查,原来是webpack 4.0和它不兼容,换webpack 3.0还是换插件? 纠结半天后,终于鼓起勇气,换插件! 又配置出错了…… 开始进入暴走模式,又气又恼。 好吧,折腾半天,请教大牛总算通过了。 那么问题来了,学了这么久css和js,我居然不知道往哪里写css…… 好吧,听说得用sass…… 配置完了,sass语法不会…… 我就想写一个ES6的todoList,太费劲了,咋得学这么东西啊…… 还是放弃吧,我感觉我不适合做前端。 虽然夸张了些,但是大部分前端都有类似的经历。 今天我就让大家彻底的学会如何工作中写ES6,我们依然用todoList举例,对了我们并不需要学习webpack,sass,插件等等。我们只学习ES6,对其它的统统不用学,你会用就可以,也不过几个命令而已。 ok,我们开始。 首先,拿到我配置好的工作流,直接在根目录下进入命令行,然后 npm install安装完成后,使用 npm run dev然后就可以用了, 就这几个文件,对应写html,js和css。 首先我们先写 html文件 。 <!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0,minimum-scale=1.0,user-scalable=no"> <title>TODOList</title></head><body> <div class="container"> <div class="item-area"> <h1 class="title">TODO-LIST</h1> <ul class="item"> <li class="item-li-default">输入任务...</li> </ul> </div> <div class="input-area"> <form class="add-item"> <input maxlength="15" type="text" name="item" placeholder="待办事件..." class="add-text" required> <input type="submit" value="+ 添加" class="add-button"> </form> </div> </div></body></html>接着,写css美化一下 * { padding: 0; margin: 0; } li { list-style: none; } html { display: flex; justify-content: center; align-items: center; text-align: center; min-height: 100vh; box-sizing: border-box; font-family: Futura, "Trebuchet MS", Arial, sans-serif; background: #ffc600; } svg { fill: #fff; background: rgba(0,0,0,0.1); padding: 20px; border-radius: 50%; width: 100px; margin-bottom: 50px; } .container { padding: 20px; max-width: 350px; box-shadow: 0 0 0 10px rgba(0,0,0,0.1); background: #fff; } .container .item-area .title { text-align: center; margin: 10px 0; color: #aaa; font-weight: 200; } .container .item-area .item { background: #fff; border-radius: 4px; } .container .item-area .item .item-li-default { color: #46b7b9; } .container .item-area .item .item-li { width: 100%; border-bottom: 1px solid #eee; height: 30px; line-height: 30px; text-align: left; color: #f95959; } .container .item-area .item .item-li .delete-item { float: right; outline: none; width: 44px; height: 26px; border-radius: 4px; color: #f05941; background: #fafafa; } .container .item-area .item .item-li input:checked + .item-text { color: #196b69; text-decoration: line-through; } .container .input-area { padding-top: 10px; } .container .input-area .add-text { outline: 0; border: 1px solid rgba(0,0,0,0.1); height: 40px; border-radius: 4px; text-indent: 10px; } .container .input-area .add-button { outline: 0; height: 40px; border-radius: 4px; width: 60px; padding: 4px; background-color: #fff; border: 1px solid rgba(0,0,0,0.1); }ok,使用 ...

May 21, 2019 · 3 min · jiezi

js-调用栈机制与ES6尾调用优化介绍

调用栈的英文名叫做Call Stack,大家或多或少是有听过的,但是对于js调用栈的工作方式以及如何在工作中利用这一特性,大部分人可能没有进行过更深入的研究,这块内容可以说对我们前端来说就是所谓的基础知识,咋一看好像用处并没有很大,但掌握好这个知识点,就可以让我们在以后可以走的更远,走的更快! 博客、前端积累文档、公众号、GitHub目录数据结构:栈调用栈是什么?用来做什么?调用栈的运行机制调用栈优化内存调用栈debug大法数据结构:栈栈是一种遵从后进先出(LIFO)原则的有序集合,新元素都靠近栈顶,旧元素都接近栈底。 生活中的栗子,帮助一下理解: 餐厅里面堆放的盘子(栈),一开始放的都在下面(先进),后面放的都在上面(后进),洗盘子的时候先从上面开始洗(先出)。 调用栈是什么?用来做什么?调用栈是一种栈结构的数据,它是由调用侦组成的。调用栈记录了函数的执行顺序和函数内部变量等信息。调用栈的运行机制机制: 程序运行到一个函数,它就会将其添加到调用栈中,当从这个函数返回的时候,就会将这个函数从调用栈中删掉。 看一下例子帮助理解: // 调用栈中的执行步骤用数字表示printSquare(5); // 1 添加function printSquare(x) { var s = multiply(x, x); // 2 添加 => 3 运行完成,内部没有再调用其他函数,删掉 console.log(s); // 4 添加 => 5 删掉 // 运行完成 删掉printSquare}function multiply(x, y) { return x * y;}调用栈中的执行步骤如下(删除multiply的步骤被省略了): 调用侦: 每个进入到调用栈中的函数,都会分配到一个单独的栈空间,称为“调用侦”。 在调用栈中每个“调用侦”都对应一个函数,最上方的调用帧称为“当前帧”,调用栈是由所有的调用侦形成的。 找到一张图片,调用侦: 调用栈优化内存调用栈的内存消耗: 如上图,函数的变量等信息会被调用侦保存起来,所以调用侦中的变量不会被垃圾收集器回收。 当函数嵌套的层级比较深了,调用栈中的调用侦比较多的时候,这些信息对内存消耗是非常大的。 针对这种情况除了我们要尽量避免函数层级嵌套的比较深之外,ES6提供了“尾调用优化”来解决调用侦过多,引起的内存消耗过大的问题。 何谓尾调用: 尾调用指的是:函数的最后一步是调用另一个函数。 function f(x){ return g(x); // 最后一步调用另一个函数并且使用return}function f(x){ g(x); // 没有return 不算尾调用 因为不知道后面还有没有操作 // return undefined; // 隐式的return}尾调用优化优化了什么? ...

May 21, 2019 · 1 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面试常见ES6问题集锦14

通过对ES6系列文章的学习,相信大家对ES6已结有一定程度的了解。 所以本节的目的在于通过测试,便于让大家了解在学习中的知识掌握情况,查缺补漏,一步一个脚印。 1、选择题 1.1 下面不属于关键字let的特点的是:( ) A、只在 let 命令所在的代码块内有效 B、会产生变量提升现象 C、同一个作用域,不能重复声明同一个变量 D、不能在函数内部重新声明参数 答案:B 解析:使用var关键字才会产生变量提升的现象。关键字let不会产生变量提升现象,所以必须先声明定义后使用,否则程序抛出异常。 1.2 关于定义常量的关键字const,定义一个Object对象{“name”:”Jack”},再对属性name 的值进行修改,如:obj.name = “John”。下列说法正确的:() A、修改常量,程序跑出异常 B、程序不抛出异常,修改无效 C、修改成功,name的值为John D、程序不抛出异常,name的值为undefined 答案:C 解析:用const来声明一个对象类型的常量,就是传址赋值。而不可修改的是对象在内存中的地址,而不是对象本身。所以修改name并不是修改对象的内存地址,所以可以成功修改。 1.3 在对象的解构赋值中,var {a,b,c} = { “c”:10, ”b”:9, ”a”:8 } 结果中,a、b、c的值分别是:() A、10 9 8 B、8 9 10 C、undefined 9 undefined D、null 9 null 答案:B 解析:对象的解构赋值不会受到属性的排列次序影响。 1.4 关于模板字符串,下列说法不正确的是:() A、使用反引号标识 B、插入变量的时候使用${ } C、所有的空格和缩进都会被保留在输出中 D、${ }中的表达式不能是函数的调用 答案:D 解析:${ }中可以放任意的JavaScript表达式,包括运算表达式、对象属性、函数调用等。 1.5 关于字符串扩展的新函数,下面描述错误的是:() A、includes函数用于判断字符串中是否含有指定的子字符串 B、repeat函数将目标字符串重复N次,目标字符串被修改 C、startsWidth函数判断指定的子字符串是否出现在目标字符串头部位置 D、endWidth函数判断指定的子字符串是否出现在目标字符串尾部位置 答案:B 解析:repeat函数将目标字符串重复N次,会返回一个新的字符串,不影响目标字符串。 1.6 数组扩展的fill( )函数,[1,2,3].fill(4)的结果是:() ...

May 18, 2019 · 2 min · jiezi

ES6Proxy-Reflect

Proxy定义:Proxy 可以理解为在目标对象之外加一层“拦截”,外界对该对象的访问,都必须会先经过这一层拦截,因些我们可以对外界的访问做一些改写与过滤,可译为“代理器”。 var proxy = new Proxy(target, handler);target 就是想要代理的目标对象,handler 则是一个方法,在其中定义想要代理的一些操作。常用可代理操作列表如下: get(target, property, receiver),拦截对象属性的读取set(target, property, value, receiver),拦截对象属性的设置has(target, property),拦截 property in target 的操作,返回一个布尔值deleteProperty(target, property),拦截 delete target[property]的操作,返回一个布尔值defineProperty(target, property, prop),拦截 Object.defineProperty 的操作,返回一个布尔值apply(target, object, args),拦截 函数的调用 、call 和 apply 操作更多操作可查看MDN:https://developer.mozilla.org... 实际使用场景: 实现私有变量的访问拦截let person = { name: 'test', age: 23, _privateName: 'private name', _phone: '13888888888'}let personProxy = new Proxy(person, { get(target, prop) { if(prop.startsWith('_')){ console.log('私有变量禁止访问'); return false; } return target[prop]; }, set(target, prop, value) { if(prop.startsWith('_')) { console.log('私有变量禁止修改'); return false; } target[prop] = value; }, has(target, prop) { return prop.startsWith('_') ? false : (prop in target); }})personProxy._phone; // 私有变量禁止访问personProxy._phone = '13999999999'; // 私有变量禁止修改'_phone' in personProxy; // false'name' in personProxy; // true设置对象属性前进行校验let person = { phone: ''}let personProxy = new Proxt(person, { set(target, prop, value) { if(prop === 'phone') { let reg = 'xxx'; if(!reg.test(value)){ throw Error('validate error!') } } target[prop] = value; }})Observe Functionfunction observe(obj, callback) { return new Proxy(obj, { set(target, prop, value) { callback(prop, value); target[prop] = value; } })}const bar = { open: false}const barObserve = observe(bar, (prop, value) => { prop === 'open' && value ? alert('Opening!') : console.log('Waiting');})barObserve.open = true; // 'Opening!'ReflectReflect的方法与Proxy的方法一一对应 ,它可以让对 Object 的一切操作都变成函数形为;比如 key in object => Reflect.has(key)、delect object[key] => Reflect.deleteProperty(object, key)。 ...

May 15, 2019 · 2 min · jiezi

ES6Arrow-Functions

基本语法(参数1, 参数2..., 参数n) => {函数声明}单一参数 => { 函数声明 } (参数1, 参数2...) => 单一表达式() => { 函数声明 }与一般 function 的区别箭头函数中的 this 指向的是定义时的对象,而不是使用时的对象// 事件绑定document.body.addEventListener('click', () => { console.log(this) // Window Object})document.body.addEventListener('click', function(){ console.log(this) // body object})// 对象中的方法var a = { name: 'a', getname: () => { console.log(this) }}a.getname() // Windowvar a = { name: 'a', getname: function(){ console.log(this) }}a.getname() // {name: "a", getname: ƒ}通过 call 或 apply 调用由于 箭头函数没有自己的this指针,通过 call() 或 apply() 方法调用一个函数时,只能传递参数,他们的第一个参数会被忽略let f = (val) => { console.log(this); console.log(val);}let obj = { count: 1};f.call(obj, 'test')输出: Window test不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替let fn = (a, b) => { console.log(arguments) // 报错}// rest参数替代let fn = (...rest) => { console.log(rest);}fn(1, 2); // [1,2]不能用箭头函数定义构造函数,箭头函数不存在 prototype;因此也不能通过 new 关键字调用let FN = () => {}console.log(FN.prototype) // undefinedlet fn = new FN(); // Uncaught TypeError: A is not a constructor原文链接:https://arronf2e.github.io/es6-arrow-functions.html ...

May 15, 2019 · 1 min · jiezi

ES6Async与异步编程11

单线程是Javascript语言最本质的特性之一,Javascript引擎在运行js代码的时候,同一个时间只能执行单个任务。 这种模式的好处是实现起来比较简单,执行环境相对单纯。 坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。 所以异步编程对JavaScript语言太重要。 有些小伙伴可能还不太理解"异步"。 所谓的"异步",就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。 例如,有一个任务是读取文件进行处理,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫做异步。 相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。 讲的通俗点: 朱自清的《背影》中,父亲对朱自清说 :“我买几个橘子去。你就在此地,不要走动。” 朱自清没有走动,等着买完橘子的父亲一起吃橘子,就叫同步。 如果朱自清没有等父亲,独自走了,那就不能和父亲一起吃橘子,就叫异步。 1、异步编程 我们就以用户注册这个特别常见的场景为例,讲讲异步编程。 第一步,验证用户是否注册 第二步,没有注册,发送验证码 第三步,填写验证码、密码,检验验证码是否正确 这个过程是有一定的顺序的,你必须保证上一步完成,才能顺利进行下一步。 1.1 回调函数 function testRegister(){} // 验证用户是否注册function sendMessage(){} // 给手机发送验证码xfunction testMessage(){} // 检验验证码是否正确function doRegister(){ //开始注册 testRegister(data){ if(data===false){ //已注册 }else{ //未注册 sendMessage(data){ if(data===true){ //发送验证码成功 testMessage(data){ if(data===true){ //验证码正确 }else{ //验证码不正确 } } } } } }}代码中就已经有许多问题,比如杂乱的 if 判断语句 、层层嵌套的函数,造成代码的可读性差,难于维护。 另外,如果在层层回调函数中出现异常,调试起来是非常让人奔溃的 —— 由于 try-catch 无法捕获异步的异常,我们只能不断不断的写 debugger 去追踪,简直步步惊心。 这种层层嵌套被称为回调地狱。 1.2 Promise方式 Promise就是为了解决回调地狱问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。采用Promise,连续读取多个文件,写法如下。 ...

May 15, 2019 · 2 min · jiezi

ES6let-const

es5 声明变量var variable = value;存在的问题: 变量提升引起的问题function get(condition) { if(condition) { var value = 'test'; return value; }else { return null; }}在JS的预编译阶段,JS引擎会将上面的函数修改如下: function get(condition) { var value; if(condition) { value = 'test'; return value; }else { return null; }}因此,ES6引入了块级作用域,强化对变量生命周期的控制,块级作用域:声明在指定块作用域内的变量不能被该作用域之外来访问 let用法与 var 相同,但可以把变量的作用域限制在当前代码块,变量声明不会被提升 function get(condition) { if(condition) { <!-- value 作用域开始 let value = 'test'; return value; 作用域结束 --> }else { return null; }}同一作用域内不能用 let 重复定义变量 var name = 'test';let name = 'test';// Uncaught SyntaxError: Identifier 'name' has already been declaredconst声明常量,值不可更改(如定义的是对象,则可以修改对象的值),声明的变量必须初始化;与 let 一样,不存在变量提升并且同一作用域内不能用 const 重复定义变量 ...

May 15, 2019 · 1 min · jiezi

优雅的异步编程和JavaScript的类

简单介绍分享内容可分为两块:优雅的异步编程 和 JavaScript的类 Generator 是 ES6 的一种异步编程的方案, ES2017 引入了 async 函数,它是 Generator 函数的语法糖,它让异步操作更加方便。 JavaScript 在 ES6 之前一直没有类的概念,生成实例的方法就是通过构造函数。但是 类(Class) 只是一个语法糖,它实际上是构造函数和对象原型写法的优化。 ????Generator 函数的语法最基本的使用Generator 可以理解为一个状态机,它封装了多个内部状态执行 Generator 会得到一个遍历器对象,所以它也是遍历器生成函数。 定义一个 Generator 函数了解一下 Generator 函数的特征 function* helloWorld() { yield 'hello' yield 'world' return 'ending'}一是 * : 在关键词 function 和 函数名之间有一个 *二是 yield: 函数体内部使用yield表达式,定义不同的状态 调用Generator 函数的调用和普通函数一样,比如 helloWrold(),不同的是,调用 Generator 函数并不会立即执行,它会返回一个遍历器对象,然后通过它的next方法来获取内部状态 通过不断的调用遍历器的 next 方法,来获取下一个状态,每次调用 next 方法,都会执行到下一个 yield表达式。 hello.next() // {value: "hello", done: false}hello.next() // {value: "world", done: false}hello.next() // {value: "ending", done: true}hello.next() // {value: undefined, done: true}返回一个有 value 和 done 两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。 ...

May 14, 2019 · 13 min · jiezi

ES6class与模块化9

JavaScript语言自创立之初,一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。 很多编程语言都有这项功能,比如 Python的import、Ruby的require,甚至就连CSS都有@import,但是JavaScript没有这方面的支持,这增加了开发大型的、复杂的项目时的难度。 于是前端开发者们开始想办法,为了防止命名空间被污染,采用的是命名空间的方式。 在ES6之前,一些前端社区制定了模块加载方案,最主要的有CommonJS和AMD两种。前者用于服务器,后者用于浏览器。 但这两种规范都由开源社区制定,没有统一,而ES6中引入了模块(Module)体系,从语言层在实现了模块机制,实现了模块功能,而且实现得相当简单,为JavaScript开发大型的、复杂的项目扫清了障碍。 ES6中的模块功能主要由两个命令构成:export和import。 export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能,二者属于相辅相成、一一对应关系。 一、什么是模块 模块可以理解为函数代码块的功能,是封装对象的属性和方法的javascript代码,它可以是某单个文件、变量或者函数。 模块实质上是对业务逻辑分离实现低耦合高内聚,也便于代码管理而不是所有功能代码堆叠在一起,模块真正的魔力所在是仅导出和导入你需要的绑定,而不是将所有的东西都放到一个文件。 在理想状态下我们只需要完成自己部分的核心业务逻辑代码,其他方面的依赖可以通过直接加载被人已经写好模块进行使用即可。 二、export 导出 命令 一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取。如果想从外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。分为以下几种情况: (1)在需要导出的lib.js文件中, 使用 export{接口} 导出接口, 大括号中的接口名字为上面定义的变量, import和引入的main.js文件中的export是对应的: //lib.js 文件let bar = "stringBar";let foo = "stringFoo";let fn0 = function() { console.log("fn0");};let fn1 = function() { console.log("fn1");};export{ bar , foo, fn0, fn1}//main.js文件import {bar,foo, fn0, fn1} from "./lib";console.log(bar+"_"+foo);fn0();fn1();(2)在export接口的时候, 我们可以使用 XX as YY, 把导出的接口名字改了, 比如: xiaoming as haoren, 这样做的目的是为了让接口字段更加语义化。 //lib.js文件let fn0 = function() { console.log("fn0");};let obj0 = {}export { fn0 as foo, obj0 as bar};//main.js文件import {foo, bar} from "./lib";foo();console.log(bar); ...

May 12, 2019 · 2 min · jiezi

javascript中filter的用法

filterfilter用于把Array的某些元素过滤掉,然后返回剩下的元素。和map()类似,Array的filter()也接收一个函数。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是true还是false决定保留还是丢弃该元素。例如,在一个Array中,删掉偶数,只保留奇数,可以这么写: var arr = [1, 2, 4, 5, 6, 9, 10, 15];var r = arr.filter(function (x) { return x % 2 !== 0;});r; // [1, 5, 9, 15]把一个Array中的空字符串删掉,可以这么写: var arr = ['A', '', 'B', null, undefined, 'C', ' ']; var r = arr.filter(function (s) { return s && s.trim(); // 注意:IE9以下的版本没有trim()方法 }); r; // ['A', 'B', 'C']

May 11, 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

Objectkeys方法之详解

一、语法Object.keys(obj)参数:要返回其枚举自身属性的对象返回值:一个表示给定对象的所有可枚举属性的字符串数组二、处理对象,返回可枚举的属性数组let person = {name:"张三",age:25,address:"深圳",getName:function(){}}Object.keys(person) // ["name", "age", "address","getName"] 三、处理数组,返回索引值数组let arr = [1,2,3,4,5,6]Object.keys(arr) // ["0", "1", "2", "3", "4", "5"] 四、处理字符串,返回索引值数组let str = "saasd字符串"Object.keys(str) // ["0", "1", "2", "3", "4", "5", "6", "7"] 五、常用技巧let person = {name:"张三",age:25,address:"深圳",getName:function(){}}Object.keys(person).map((key)=>{ person[key] // 获取到属性对应的值,做一些处理}) 六、Object.values()和Object.keys()是相反的操作,把一个对象的值转换为数组

May 11, 2019 · 1 min · jiezi

ES6专题-class与面向对象编程

在ES5中,我们经常使用方法或者对象去模拟类的使用,并基于原型实现继承,虽然可以实现功能,但是代码并不优雅,很多人还是倾向于用 class 来组织代码,很多类库、框架创造了自己的 API 来实现 class 的功能。 ES6 时代终于有了 class (类)语法,能让我们可以用更简明的语法实现继承,也使代码的可读性变得更高,同时为以后的JavaScript语言版本添加更多的面向对象特征打下基础。有了ES6的class 以后妈妈再也不用担心我们的代码乱七八糟了,这简直是喜大普奔的事情。ok,我们看看神奇的class. 一、 类的定义 1.1 ES5 模拟类定义 function Person( name , age ) { this.name = name; this.age = age;}Person.prototype.say = function(){ return '我叫' + this.name + ',今年' + this.age + '岁';}var p = new Person('大彬哥',18); // Person {name: "大彬哥", age: 18}p.say() //"我叫大彬哥,今年18岁"使用ES5语法定义了一个Person类,该类有name和age两个属性和一个原型say方法。 这种写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大。接下来我们看下ES6 类的写法,这个就很接近于传统面向对象语言了。如果你想了解传统面向对象语言,这里是一个好切入点。 1.2 ES6 class类定义 class Person { constructor( name , age ) { this.name = name; this.age = age; } say() { return '我叫' + this.name + ',今年' + this.age + '岁'; }}var p = new Person('大彬哥',18); // Person {name: "大彬哥", age: 18}p.say() //"我叫大彬哥,今年18岁"上面代码定义了一个同样的Person类,constructor方法就是构造方法,而this关键字则代表实例对象,这更接近传统语言的写法。 ...

May 10, 2019 · 2 min · jiezi

前端培训初级阶段17-数据存储cookiesessionstroage

前端最基础的就是 HTML+CSS+Javascript。掌握了这三门技术就算入门,但也仅仅是入门,现在前端开发的定义已经远远不止这些。前端小课堂(HTML/CSS/JS),本着提升技术水平,打牢基础知识的中心思想,我们开课啦(每周四)。 这是初级阶段的最后一堂了。之后的内容插入了一些实际场景和review 我们要讲什么cookie是什么?用来解决问题?有什么注意点?session是什么?用来解决问题?有什么注意点?stroage是什么?用来解决问题?有什么注意点?其他内容(IndexedDB、WebSQL)cookiecookie 是什么?cookie是一个存放在浏览器端的内容,可以在请求服务端的时候时候带在header中,下图可以看到关键词有name,value,Domain,path,Expires/max-age,http,secure,可以打开自己的浏览器研究一下。 name 就是key获取的,value 就是值,和name是对应的。domain 就是所属域名,比如 sf.gg 的就不能被 baidu.com 获取。path 是所属路径 /im 就不能获取 /live 下面的,所以一般公共的都放在根目录expires 是失效时间,有会话级别的-关闭浏览器就失效。有时间级别的-到点失效。httponly 只有服务端能获取到,接口访问的时候也会自动带上。但是 document.cookie 拿不到 cookie 解决了什么问题浏览器的访问是无状态,意味着服务器不理解两次请求是不是同一个人。所以他可以通过 cookie 做一个唯一标识。然后每次访问都带上,这样服务器就可以知道这是同一个人。所以说 cookie 是重要的,如果别人拿到了你的 cookie,他就是你。 cookie 使用中有什么要注意的各个浏览器的容量是不一样的(条数)。cookie 的不要放大量数据,因为这些数据会用在每次请求上。敏感数据要设置 httponly ,防止意外的被他人获取。sessionsession是什么?服务器端存放数据。一般来说生成一个sessionID,放在cookie里面。浏览器的请求来了之后,拿着sessionID去查到详细信息。一般来说都是使用过期时间 session用来解决什么问题?cookie不适合存放大量数据、敏感数据。比如说userid,不能说用户改啥就是啥。比如说一些内部的判断条件。就给浏览器端一个id,来服务器端查就ok了。 session 使用时需要注意什么?服务器的事情,咱们前端就不管了吧。爱咋用咋用。 stroagestroage是什么?浏览器支持两种 localstroage 和 sessionStroage。都是用来做浏览器端存储的。 localStroage 是用来跨页面使用的,可以长久存储。当然是同源的页面。sessionStroage 是用来存放本页面的数据的,关闭页面就清空了。stroage解决的问题cookie的存储大小问题。页面通信的问题。真正提供了前端存储能力 stroage使用的时候需要注意什么?存储的值都为String。存储是同步的。localstroage的改变会通知给其他页面stroage事件支持大小是5MB,当然也不准咯,看浏览器厂商的实现。其他内容(IndexedDB、WebSQL)IndexedDB IndexedDB 是一种低级API,用于客户端存储大量结构化数据(包括, 文件/ blobs)。该API使用索引来实现对该数据的高性能搜索。虽然 Web Storage 对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用。IndexedDB提供了一个解决方案。WebSQL 将要废弃的方案。其他的封装库 对于简单的情况可能看起来太复杂。如果你更喜欢一个简单的API,尝试二次封装的类库如localForage、dexie.js、ZangoDB。后记主讲人文章-2019-04-25

May 10, 2019 · 1 min · jiezi

利用-es6-newtarget-来对模拟抽象类

起源最近在使用 Symbol 来做为唯一值,发现 Symbol 无法进行 new 操作,只能当作函数使用,只要进行了new 就会发生类型错误 new Symbol()// errorUncaught TypeError: Symbol is not a constructor at new Symbol (<anonymous>) at <anonymous>:1:1在不考虑底层实现的情况下,在代码层面是否能够实现一个函数只可以进行调用而不可以进行 new 操作呢?思考之后如下写出: function disConstructor() { if (this !== window) { throw new TypeError(' disConstructor is not a constructor') } console.log('gogo go')}// 测试结果如下disConstructor() // gogo gonew disConstructor()// errorUncaught TypeError: disConstructor is not a constructor at new disConstructor (<anonymous>:3:15) at <anonymous>:1:1如果使用 nodejs,window 可以切换为 global, 代码运行结果不变,因为对于个人而言没有适用场景。于是就没有继续研究下去,可是最近在从新翻阅 es6 时候发现 new.target这个属性。 ...

May 10, 2019 · 2 min · jiezi

ES6数组新方法7

在javascript中,数组是最重要的数据结构,没有之一,因为所有的数据结构都可以使用数组模拟和表达。可以说掌握了数组,就掌握了js与数据操作的大部分核心功能。 ES6给数组添加了一些新特性,而这些新特性到目前为止完全可以运用到自己的业务层。 但是,开发者初次接触ES6,很多东西都看得云里来雾里,无从下手。在这一节中将总结有关于ES6给数组提供一些新特性的使用方法,让用户能够听得懂,用的起来。 1、新增数组创建方法 1.1 Array.from Array.from的设计目的是快速便捷把一个类似数组的可迭代对象创建成一个新的数组实例。 通俗的讲,只要一个对象有length,Array.from就能把它变成一个数组,返回新的数组,而不改变原对象。 let likeArr = { '0': 'a', '1': 'b', '2': 'c', length: 3};// ES5的写法var arr1 = [].slice.call(likeArr); // ['a', 'b', 'c']// ES6的写法let arr2 = Array.from(likeArr); // ['a', 'b', 'c']常见的类似数组的对象还有 DOM 操作返回的 NodeList 集合,以及函数内部的 arguments 对象。Array.from都可以将它们转为真正的数组。 // NodeList对象let div = document.querySelectorAll('div');console.log(div); // NodeList(8) [div#cst, div, div.gb_3, …]console.log(Array.from(div)); //(8) [div#cst, div, div.gb_3, …]// arguments对象function foo() { var args = Array.from(arguments); console.log(args);}foo(1,2,34,666,333,663); // [1, 2, 34, 666, 333, 663]Array.from 对 String,Set,Map 等拥有迭代器的对象也可以进行转换。 ...

May 10, 2019 · 3 min · jiezi

吃鸡吗和平精英来了感觉不一般

本博客 猫叔的博客,转载请申明出阅读本文约“3分钟”适读人群:IT/互联网工作者、游戏爱好者 吃鸡吗? 本文部分素材摘抄自“36Kr-《最前线 | 腾讯“吃鸡”游戏或借壳变现,《绝地求生》“成为”《和平精英》》”。玩过吃鸡类手游的朋友,应该都大致了解过各类大厂出的吃鸡游戏吧。今日,腾讯旗下已过审游戏《和平精英》测试服务器安卓端已开启,用腾讯社交账号体系登陆后,继承了《绝地求生:刺激战场》的游戏数据。此外,除了游戏的世界观不太一样,《和平精英》的美术、UI都与《绝地求生:刺激战场》极为相似。 商业变现?高度保密?国家军队机构宣传广告!经济价值我是一个LOL的老玩家了(一脚盲僧),不过吃鸡真的玩不过来,毕竟3D眩晕的感觉一次就足够我体会了。 在TalkingData免费版搜了一下,查询手机游戏的射击类排行,3月份的前10是这样的,前3还是依旧是大厂。 我比较少玩吃鸡(几乎没有),不过对于游戏的好奇感一直高于其他行业。 今天来和大家说点游戏技术上的事情吧。 就说简单的吧,毕竟我本身也不是做专业游戏出身的。说一举例说,就说一下射击子弹与玩家角色之间的关系与基本实现思路。 吃鸡是3D的,我举例就用微信小游戏的模版来说吧,是2D的,不过大同小异。 这个是微信小游戏,官方的Demo,使用快速模板就可以生成运行,是一个很传统的飞机射击游戏,其实原理也很简单,精灵(所有运动角色的简称)是通过x/y轴改变位置来实现移动,过程中精灵的动作可以通过一系列的帧图片实现走动效果或者爆炸。 让我们玩玩这个游戏看看。 我们来说说这个子弹击中敌军飞机,然后爆炸,分数+1的过程。 简单的说,就是判断子弹的坐标与敌军飞机是否重叠或者“接触”,如果是,那么就执行击中音乐、飞机消失、子弹爆炸动画、分数+1等子函数的动作。 如果我们想要让其在判断后,不执行哪一部分的操作,那么也可以将该部分的动作代码注释掉。 如上的游戏效果,就是我把飞机消失的动画效果去除后的游戏体验,因为敌军没有消失,所以当它接触到我方的时候,游戏就结束了。 太久没有玩小程序,一些ES6的语法也记不太清楚了~ 公众号:Java猫说学习交流群:728698035 现架构设计(码农)兼创业技术顾问,不羁平庸,热爱开源,杂谈程序人生与不定期干货。

May 9, 2019 · 1 min · jiezi

你不知道的newtarget

前几天在复看es6的时候看到这个东西(new.target),个人觉得挺好的,然后想跟大家分享一下 什么是new.targer?它有什么用?它能干什么?new.target是es6新增加的一个东西,它返回的是某个构造函数或者是undefined。new.target可以判断构造函数是new实例化的还是直接调用的,如果是通过new实例化的,会返回这个构造函数,如果直接调用的,返回的就是undefined。如果通过es5写构造函数的时候,可以通过new.target返回的值,确保构造函数是通过new实例化,而不是直接调用;如果通过es6写构造类的时候,也可以通过new.target返回的值,确保祖先类只能被继承,不猛被实例化。es5 function Person(name){ if(new.target === undefined){ throw new Error("必须通过new来实例化") } this.name = name; } //如果不通过new实例化的话,直接报错 var Jack = Person("su")es6(这里说一下,Person被继承之后,new.target不再指向Peoson,会指向子类的构造函数) class Person{ constructor(name) { if(new.target === Person){ throw new Error("该类只能继承不能被实例化") } this.name = name }}// Person实例化直接报错class July extends Person{ constructor(name,age){ super(name), this.age = age } eat(){ console.log(this.age,this.name) }}var Jack = new July("su",21)——end——

May 9, 2019 · 1 min · jiezi

Symbol-类型

Symbol 类型根据规范,对象的属性键只能是 String 类型或者 Symbol 类型。不是 Number,也不是 Boolean,只有 String 或 Symbol 这两种类型。 到目前为止,我们只见过 String。现在我们来看看 Symbol 能给我们带来什么好处。 Symbol"Symbol" 值表示唯一的标识符。 可以使用 Symbol() 来创建这种类型的值: // id 是 symbol 的一个实例化对象let id = Symbol();我们可以给 Symbol 一个描述(也称为 Symbol 名),这对于调试非常有用: // id 是描述为 "id" 的 Symbollet id = Symbol("id");Symbol 保证是唯一的。即使我们创建了许多具有相同描述的 Symbol,它们的值也是不同。描述只是一个不影响任何东西的标签。 例如,这里有两个描述相同的 Symbol —— 它们不相等: let id1 = Symbol("id");let id2 = Symbol("id");*!*alert(id1 == id2); // false*/!*如果您熟悉 Ruby 或者其他有 "Symbol" 的语言 —— 别被误导。JavaScript 的 Symbol 与众不同。 ...

May 9, 2019 · 3 min · jiezi

ES6新特性varletconst

1、 var、let、const var不存在块级作用域,具有变量提升机制。let和const存在块级作用域,不存在变量提升。在同一作用域内只能声明一次。## var的说明 ##function a(flag){ if(flag){ console.log(value); //结果为undefined,已声明但未初始化 var value=1; return value; }else{ console.log(value); //仍旧可以访问到value;值为undefined }}//上述代码等价于function a(flag){ var value; if(flag){ value=1; return value; }else{ console.log(value); //仍旧可以访问到value;值为undefined }}## let说明 ##function a(flag){ if(flag){ console.log(value); // 报错,因为value此时位于TDZ(后面讲)内,无法访问 let value=1; return value; }else{ console.log(value); //value is not defined }}## const说明 ##function a(flag){ if(flag){ console.log(value); // 报错,因为value此时位于TDZ(后面讲)内,无法访问 const value=1; //const必须在声明的时候初始化,并且其值无法被重新赋值 return value; }else{ console.log(value); //value is not defined }}2、const在声明时需要赋值且无法修改,但如果常量是对象,则对象的属性可以修改 const obj={ name:'Alisa', age:25}obj=23; //报错obj.age=23; //允许修改3、TDZ暂存性死区JavaScript引擎在扫描代码发现变量声明时,如果是使用var声明的变量,则讲变量提升至作用域顶部;如果是采用let和const声明的变量,则将其放到TDZ中。访问TDZ中的变量会触发运行时错误,只有执行过变量声明语句后,变量才会从TDZ中移出,然后才可以正常访问。注意:其针对的是let和const所在的块级作用域。 ...

May 7, 2019 · 1 min · jiezi

ES6箭头函数5

0.为什么会出现箭头函数? 1.传统的javascript函数语法并没有提供任何的灵活性,每一次你需要定义一个函数时,你都必须输入function () {},这至少会出现两个问题,ES6箭头函数都圆满解决了它, 第一个问题:代码输入快了容易输错成 funciton或者functoin或者其它,但是=>这个玩意你要是再写错就只能说有点过分了。 第二个问题:节省大量代码,我们先不用管下面的ES6代码为什么这样的语法能实现同样的功能,我们就直观的感受一下代码量。 ES5写法: function addFive(num){ return num+5; }alert(addFive(10));ES6写法: var addFive = num=>num+5;alert(addFive(5));没有function、没有return,没有(),没有{},这些全变成了浮云,世界好清静。 从上面我们就可以看到,使用箭头函数不仅仅能够避免错误,同时还能让我们少一丢丢代码,当然实际工作中远比这个代码量节省更多。一方面是因为积累效应,每一部分少一丢丢合起来就多了,一方面是它还有更能节省代码和大幅提高工作效率的场景。 接下来我们就说说今天的主角--箭头函数。 ES6标准新增了一种新的函数:Arrow Function(箭头函数),也称“胖箭头函数”, 允许 使用“箭头”(=>)定义函数,是一种简写的函数表达式。 1、箭头函数语法 在ES5中我们实现一个求和的函数: var sum = function(x, y) { return x + y}要使用箭头函数,可以分两步实现同样的函数功能: 首先使用=>来替代关键词function var sum = (x, y) => { return x + y}这个特性非常好!!! 前面我们已经说过用=>来替代关键词function就意味着不会写错function了,这真是一个绝妙的设计思想! 其次,函数体只有一条返回语句时, 我们可以省略括号{}和return关键词: var sum = (x, y) => x + y再夸张一点点,如果只有一个参数时,()可省略。 这是箭头函数最简洁的形式,常用于作用简单的处理函数,比如过滤: // ES5var array = ['1', '2345', '567', '89'];array = array.filter(function (item) { return item.length > 2;});// ["2345", "567"]// ES6let array = ['1', '2345', '567', '89'];array = array.filter(item => item.length > 2); // ["2345", "567"]箭头函数的主要使用模式如下: ...

May 7, 2019 · 3 min · jiezi

探索怎样让-JS-API-具有更好的实用性

程序员的精神,不应不止于实现,更要注重优化。不应止于表面,更要研究内部机制,方能青出于蓝而胜于蓝。1.前言在上家公司开发后台管理系统的时候,频繁要处理各种数据显示的问题,一开始是实现就好。后来写多了,自己看得也难受了。就想着怎么优化代码和复用了。下面就通过一个简单的例子,怎么让 API 更加的实用,更好的复用。 1.代码的实用性,只能尽量,尽量再尽量。不会出现完美的API,或者是一次编写,永不修改的 API 。2.关于实用性,API 命名和扩展性也很重要。但之前写过文章,在这里就不重复了。[[前端开发]--分享个人习惯的命名方式](https://juejin.im/post/5b6ad6...,重构 - 设计API的扩展机制 2.举个例子比如有一个需求,有这样的数据 { cashAmount: 236700,//回款金额(分) cashDate: "2018-05-26 10:25:28",//回款时间 cashId: "SM2018022800020692",//回款ID cashStatus: 0,//回款状态 createTime: "2018-05-23 10:26:25",//创建时间 custoName: "广州测试有限公司",//回款公司名称 id: "SM2018022800020692",//回款ID merchandisers: "守候",//回款公司联系人 ordId: "SO2018022800020692",//订单ID payChannel: null,//支付方式 remark: "",//备注 userMobile: "18819222363",//回款公司联系人电话}需要对数据进行以下处理,再渲染到页面 1.cashAmount 转换成元,并保留两位小数 2.cashStatus 进行解析(0-未回款 1-已回款) 3.payChannel 进行解析 ('zfb'-支付宝,'wx'-微信支付,'cash'-现金支付,'bankTransfer'-银行转账) 4.所有值为 '' , null , undefined 的字段,全部设置为:'--' 面对这样的需要,很简单,顺手就来 let obj = { cashAmount: 236700,//回款金额(分) cashDate: "2018-05-26 10:25:28",//回款时间 cashId: "SM2018022800020692",//回款ID cashStatus: 0,//回款状态 createTime: "2018-05-23 10:26:25",//创建时间 custoName: "广州测试有限公司",//回款公司名称 id: "SM2018022800020692",//回款ID merchandisers: "守候",//回款公司联系人 ordId: "SO2018022800020692",//订单ID payChannel: null,//支付方式 remark: "",//备注 userMobile: "13226452474",//回款公司联系人电话}function setValue(obj) { let _obj=JSON.parse(JSON.stringify(obj)); //设置金额 _obj.cashAmount = (_obj.cashAmount / 100).toFixed(2); //解析回款状态 _obj.cashStatus = _obj.cashStatus === 0 ? '未回款' : '已回款'; //解析支付方式 let payChannelLabel = { 'zfb': '支付宝', 'wx': '微信支付', 'cash': '现金支付', 'bankTransfer': '银行转账' } _obj.payChannel=payChannelLabel[_obj.payChannel]; //设置默认值 for (let key in _obj){ if(_obj[key]===''||_obj[key]===null||_obj[key]===undefined){ _obj[key]='--' } } return _obj;}obj=setValue(obj);console.log(obj)结果也正确,如下图 ...

May 6, 2019 · 3 min · jiezi

ES6字符串模板引擎4

字符串模板引擎 ES5中的字符串缺乏多行字符串、字符串格式化、HTML转义等特性。 而ES6通过模板字面量的方式进行了填补,模板字面量试着跳出JS已有的字符串体系,通过一些全新的方法来 解决问题。 1.基本用法 ES5字符串写法: let message = "我的宠物狗叫黑子,今年16岁了"将其转化成ES6写法,其实非常简单: 只需把最外围的双引号(")或者单引号(') 转化成反引号(`)即可。 let message = `我的宠物狗叫黑子,今年16岁了`如果想在字符串内部使用反引号,只需使用反斜杠( )转义即可 let message = `我的宠物狗叫\`黑子\`,今年16岁了`;console.log(message); // "我的宠物狗叫`黑子`,今年16岁了" 2.多行字符串 传统的JavaScript语言,输出模板通常是这样写的: var name = '黑子';var age = 8;$('#result').append( '我的宠物狗叫 <b>' + name + '</b>\n' + '今年\n' + '<em>' + age+ '</em>岁,\n'+ '十分可爱!');但是在ES6中,要获得同样效果的多行字符串,只需使用如下代码: let name = '黑子';let age = 8;$('#result').append( `我的宠物狗叫 <b>${name}</b> 今年 <em>${age}</em>岁, 十分可爱!`);对比两段拼接的代码,模板字符串使得我们不再需要反复使用双引号(或者单引号)了;而是改用反引号标识符 (`),插入变量的时候也不需要再使用加号(+)了,而是把变量放入${ }即可。 也不用再通过写 n 进行换行了,ES6 的模板字面量使多行字符串更易创建,因为它不需要特殊的语法,只需在想 要的位置直接换行即可,此处的换行会同步出现在结果中。简单、清晰、明了。 注意:如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。因此需要特别留意缩进。 ...

May 5, 2019 · 2 min · jiezi

摘自ES6标准入门第3版阮一峰著经典案例持续更新中

1.模拟next方法返回值的例子function makeIterator(array){ var nextIndex = 0; return { next: function(){ return nextIndex < array.length ? {value: array[nextIndex++],done:false} : {value: undefined, done: true}; } }}var it = makeIterator(['a','b']);it.next(); // {value: "a", done: false}it.next(); // {value: "b", done: true}it.next(); // {value: undefined, done: true}

April 29, 2019 · 1 min · jiezi

gulp-gulpbetterrollup-rollup-构建-ES6-开发环境

gulp + gulp-better-rollup + rollup 构建 ES6 开发环境关于 Gulp 就不过多啰嗦了。常用的 js 模块打包工具主要有 webpack、rollup 和 browserify 三个,Gulp 构建 ES6 开发环境通常需要借助这三者之一来合并打包 ES6 模块代码。因此,Gulp 构建 ES6 开发环境的方案有很多,例如:webpack-stream、rollup-stream 、browserify等,本文讲述使用 gulp-better-rollup 的构建过程。gulp-better-rollup 可以将 rollup 更深入地集成到Gulps管道链中。 GitHub地址:https://github.com/JofunLiang/gulp-translation-es6-demo 构建基础的 ES6 语法转译环境首先,安装 gulp 工具,命令如下: $ npm install --save-dev gulp安装 gulp-better-rollup 插件,由于 gulp-better-rollup 需要 rollup 作为依赖,因此,还要安装 rollup 模块和 rollup-plugin-babel(rollup 和 babel 之间的无缝集成插件): $ npm install --save-dev gulp-better-rollup rollup rollup-plugin-babel安装 babel 核心插件: $ npm install --save-dev @babel/core @babel/preset-env安装完成后,配置 .babelrc 文件和 gulpfile.js文件,将这两个文件放在项目根目录下。 ...

April 29, 2019 · 2 min · jiezi

你对项目里的依赖包了解吗

注意:本文所有依赖包是目前最新版本的 现在很多开发朋友对于使用webapck、babel搭建开发环境已经不陌生,但很少去系统性的了解项目依赖。 本文从环境依赖包说起,让你对自己的开发环境有更深的了解。 为了简单,我们将依赖分个类:Babel相关????、Webpack相关????、可选的依赖包。注意:带???? 是指必需的依赖, 下面我们一个一个来说。 Babel相关????要使用最新的ES6+语法,必须少不了Babel转码,那么要搭建一个完全体的环境,应该使用哪些依赖呢? 首先,我们安装最核心的依赖: @babel/cli、@babel/core、@babel/polyfill、@babel/register、core-js 下面是他们的一些简单解释: { /* Babel 自带了一个内置的 CLI 命令行工具,可通过命令行编译文件。 */ "@babel/cli": "^7.4.3", /* 看到`core`就知道它是`babel`的核心,一些转码操作都是基于它完成的, 所以它是必须的依赖。 */ "@babel/core": "^7.4.3", /* Babel默认只转换新的JavaScript语法,但是不转换新的API,比如 `Iterator`、`Generator`、`Set`、`Maps`、`Proxy`、`Reflect`、 `Symbol`、`Promise` 等全局对象,以及一些定义在全局对象上的方法(比 如 `Object.assign` )都不会转码。而`@babel/polyfill`就可以做到。 */ "@babel/polyfill": "^7.4.3", /* 让webpack.config.babel.js也支持ES6语法 */ "@babel/register": "^7.4.0", /* 通俗说就是动态polyfill,它可以动态加载需要的新API,具体可以看https://github.com/zloirock/core-js#readme */ "core-js": "3", }下面我们安装必需的preset和plugin:@babel/preset-env、@babel/plugin-proposal-class-properties、@babel/plugin-proposal-decorators、@babel/plugin-proposal-object-rest-spread、@babel/plugin-syntax-dynamic-import 下面是它们的一些解释: { /* 根据指定环境来转码,这个不用说,必装 */ "@babel/preset-env": "^7.4.3", /* 对class中属性初始化语法、static等语法进行处理 */ "@babel/plugin-proposal-class-properties": "^7.4.0", /* 装饰器语法处理 */ "@babel/plugin-proposal-decorators": "^7.4.0", /* 对象rest、spread语法处理 */ "@babel/plugin-proposal-object-rest-spread": "^7.4.3", /* import()语法处理 */ "@babel/plugin-syntax-dynamic-import": "^7.2.0",}安装好了以上preset和plugins,我们需要新建一个.babelrc文件来使用它们: ...

April 27, 2019 · 2 min · jiezi

对asyncawait的理解

async 函数先看看MDN上怎么介绍的: async function 声明用于定义一个返回 AsyncFunction 对象的异步函数。异步函数是指通过事件循环异步执行的函数,它会通过一个隐式的 Promise 返回其结果。但是如果你的代码使用了异步函数,它的语法和结构会更像是标准的同步函数。如果async函数中是return一个值,这个值就是Promise对象中resolve的值;如果async函数中是throw一个值,这个值就是Promise对象中reject的值。async函数的写法async function imAsync(num) { if (num > 0) { return num // 这里相当于resolve(num) } else { throw num // 这里相当于reject(num) }}imAsync(1).then(function (v) { console.log(v); // 1});// 注意这里是catchimAsync(0).catch(function (v) { console.log(v); // 0})Promise的写法function imPromise(num) { return new Promise(function (resolve, reject) { if (num > 0) { resolve(num); } else { reject(num); } })}imPromise(1).then(function (v) { console.log(v); // 1})imPromise(0).then(function (v) { console.log(v); // 0})awaitawait会暂停当前async函数的执行,等待后面的Promise的计算结果返回以后再继续执行当前的async函数。如果单纯的 await setTimeout(...)是行不通的,await 不是什么都等,它等待的只是Promise,你如果没有给他返回个Promise,那它还是会继续向下执行。所以 await 等待的不是所有的异步操作,等待的只是Promise。 ...

April 27, 2019 · 1 min · jiezi

常见的JavaScript陷阱

随着ES6标准的普及,JavaScript已经拥有许多新的语法糖,这让我们编写可读的,高质量的代码变得更加方便,但即使这样仍然会遇到一些潜在的"陷阱"。 箭头函数与对象字面量箭头函数提供了更简洁和更短的语法,其中一个可用功能是您可以将函数编写为具有隐式返回值的lambda表达式。 例如: const numbers = [1, 2, 3, 4];numbers.map(function(n) { return n * n;});可以使用箭头函数简化 const numbers = [1, 2, 3, 4];numbers.map(n => n * n);但如果我们希望映射到对象,可能并不会像我们期望的那样。 const numbers = [1, 2, 3, 4];numbers.map(n => { value: n });上面的结果并非映射到对象,而是映射到undefined。花括号被解释为箭头函数的块范围,值语句实际上最终成为标签,上面的代码将被Javascript解释器翻译成: const numbers = [1, 2, 3, 4];numbers.map(function(n) { value: n return;});解决方法非常微妙,我们只需要将对象包装在括号中,将其转换为表达式而不是块语句,如下所示: const numbers = [1, 2, 3, 4];numbers.map(n => ({ value: n }));箭头函数与this指针箭头函数没有自己的this绑定,这意味着它们的this值将this与封闭的词法范围的值相同。尽管语法可以说是“更光滑”,但箭头函数功能并不能适用于所有的情况,比如你可能会遇到: let calculator = { value: 0, add: (values) => { this.value = values.reduce((a, v) => a + v, this.value); },};calculator.add([1, 2, 3]); console.log(calculator.value); // 0我们期望这里的this绑定的是计算器对象,但它实际上会导致this未定义或指向全局对象。而常规函数是有一个this绑定,当在一个对象上调用时,它将指向该对象,因此在添加对象方法时仍然应该使用常规函数。 ...

April 27, 2019 · 2 min · jiezi

理解和使用Promiseall和Promiserace

JavaScript的世界中,所有代码都是单线程执行的。异步执行可以用回调函数实现,但是某些场景并不好用,且不易复用。Promise对象这种链式写法的好处在于,先统一执行逻辑,不关心如何处理结果,然后,根据结果是成功还是失败,在将来的某个时候调用success函数或fail函数。Pomise.all的使用Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。Promise.all中传入的是数组,返回的也是是数组,并且会将进行映射,传入的promise对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的,除非可迭代对象为空。 可迭代对象:遍历Array可以采用下标循环,遍历Map和Set就无法使用下标。为了统一集合类型,ES6标准引入了新的iterable类型,Array、Map和Set都属于iterable类型。具有iterable类型的集合可以通过新的for ... of循环来遍历。 示例代码如下:let p1 = new Promise((resolve, reject) => { resolve('成功了')})let p2 = new Promise((resolve, reject) => { resolve('success')})let p3 = Promse.reject('失败')Promise.all([p1, p2]).then((result) => { console.log(result) //['成功了', 'success']}).catch((error) => { console.log(error)})Promise.all([p1,p3,p2]).then((result) => { console.log(result)}).catch((error) => { console.log(error) // 失败了,打出 '失败'})Promse.all在处理多个异步处理时非常有用,比如说一个页面上需要等两个或多个ajax的数据回来以后才正常显示,在此之前只显示loading图标。 代码模拟:let wake = (time) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(`${time / 1000}秒后醒来`) }, time) })}let p1 = wake(3000)let p2 = wake(2000)Promise.all([p1, p2]).then((result) => { console.log(result) // [ '3秒后醒来', '2秒后醒来' ]}).catch((error) => { console.log(error)})需要特别注意的是,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,即p1的结果在前,即便p1的结果获取的比p2要晚。这带来了一个绝大的好处:在前端开发请求数据的过程中,偶尔会遇到发送多个请求并根据请求顺序获取和使用数据的场景,使用Promise.all毫无疑问可以解决这个问题。 ...

April 27, 2019 · 1 min · jiezi

ES6入门之数值的扩展

1. 二进制和八进制表示法ES6提供了二进制和八进制数值的新写法,分别用前缀 0b(0B) 和 0o(0O)表示 如下:0b111110111 === 503 // true0o767 === 503 // true2. Number.isFinite(), Number.isNaN()ES6在number上提供了 Number.isFinite(), Number.isNaN()两个方法,前者用来检查数值是否为有限的,后者用来检查一个值是否为NAN,如下:Number.isFinite(15); // trueNumber.isFinite(0.8); // trueNumber.isFinite(NaN); // falseNumber.isFinite(Infinity); // falseNumber.isFinite(-Infinity); // falseNumber.isFinite('foo'); // falseNumber.isFinite('15'); // falseNumber.isFinite(true); // false参数类型如果不是数字就返回falseNumber.isNaN(NaN) // trueNumber.isNaN(15) // falseNumber.isNaN('15') // falseNumber.isNaN(true) // falseNumber.isNaN(9/NaN) // trueNumber.isNaN('true' / 0) // trueNumber.isNaN('true' / 'true') // true与传统的isFinite() 和 isNaN() 的区别在于,传统方法优先调用Number()将非数值的值转为数值,在进行判断。而Number.isFinite()对于非数值一律返回false, Number.isNaN()只有对于NaN才返回true,非NaN一律返回false。 3. Number.parseInt(), Number.parseFloat()ES6 将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。减少全局性方法,使得语言逐渐模块化。如下:// ES5的写法parseInt('12.34') // 12parseFloat('123.45#') // 123.45// ES6的写法Number.parseInt('12.34') // 12Number.parseFloat('123.45#') // 123.454. Number.isInteger()Number.isInteger()用来判断一个数值是否为整数,如下:Number.isInteger(25) // trueNumber.isInteger(25.1) // falseNumber.isInteger(25.0) // true 整数和浮点数采用相同的存储方式但是Number.isInteger() 对于数据精度要求比较高的情况不能很好的判断,不建议使用。 ...

April 26, 2019 · 2 min · jiezi

ES6入门之函数的扩展

1. 函数参数的默认值1.1 用法在ES6之前是不能为函数的参数指定默认值的,要想实现默认值只能通过判断赋值的方式来实现,在ES6中允许函数为参数设置默认值,主要是为了提高代码的可阅读性,有利于代码的优化。另外注意的是在参数赋值的时候,该参数不能重复使用,不能使用let const 进行定义。// ES6 之前实现function log(x, y) { y = y || 'World'; if (typeof y === 'undefined') { y = 'World'; } console.log(x, y);}log('Hello') // Hello Worldlog('Hello', 'China') // Hello Chinalog('Hello', '') // Hello World// ES6 中实现function log(x, y = 'World') { console.log(x, y);}log('Hello') // Hello Worldlog('Hello', 'China') // Hello Chinalog('Hello', '') // Hellofunction Point(x = 0, y = 0) { this.x = x; this.y = y;}const p = new Point();p // { x: 0, y: 0 }function foo(x = 5,x) { let x = 1; // 报错,不能同名参数,不能对参数进行let const 定义 const x = 2;}1.2 与解构赋值一起使用如果函数在调用的时候没有提供参数,内部变量就不会产生,就会产生错误,通过提供函数的默认值可以解决这种问题,如下:function foo({x, y = 5}) { console.log(x, y);}foo() // 报错foo({x:1}) // 1 5foo({x:2,y:3) // 2 3foo({}) // undefined 5function foo ({x,y = 5} = {}){ console.log(x,y)}foo() // undefined 5 这样就是如果没有在调用的时候传值 就默认赋空对象。如下例子:function post(url, {b = '',type='get',h={}}){ console.log(type)}post('w.b.c',{}) // getpost('w.b.c') // 报错// 改成这样就可以了function post(url, {b = '',type='get',h={}} = {}){ console.log(type)}post('w.b.c',{}) // getpost('w.b.c') // get下面例子的区别// 写法一function m1({x = 0, y = 0} = {}) { return [x, y];}// 写法二function m2({x, y} = { x: 0, y: 0 }) { return [x, y];}两个都是有默认值在调用的时候都传值或者都不传值的时候情况是一样的。但是如果传空值,或者不传值的情况会有差异如下:m1({}) // 因为本身有默认值 所以为 [0,0]m2({}) // 默认值为空 解构赋值没有传值 所以 [undefined,undefined]// 其他情况同上m1({x: 3}) // [3, 0]m2({x: 3}) // [3, undefined]m1({z: 3}) // [0, 0]m2({z: 3}) // [undefined, undefined]1.3 参数默认值的位置如果定义了默认值的参数,应该是函数的尾参数。而且这个参数是无法省略的,除非输入undefined1.4 函数的 length 属性函数参数指定了默认值之后,函数的length属性将会减去指定了默认值的参数个数。因为该属性认为,指定了默认值的参数将不包含在预期参数个数中。如下:(function (a) {}).length // 1(function (a = 5) {}).length // 0(function (a, b, c = 5) {}).length // 21.5 作用域如果函数中的参数设置了默认值,那么函数在声明初始化的时候,参数会形成一个单独的作用域,初始化完成后这个作用域就会消失,这种情况只在参数设置了默认值的情况下。如下:var x = 1;function f(x, y = x) { console.log(y);}f(2) // 2// 因为 设置了默认值 所以在调用 f 的时候就形成了作用域,这时候因为将x赋值给y 传入的x 为 2 所以y是2,如果这时候 调用的时候不传值,那么x将指向全局,所以y = 11.6 应用利用参数默认值,可以指定某一个参数不得省略,如果省略就报错,如下function throwIfMissing() { throw new Error('Missing parameter');}function foo(mustBeProvided = throwIfMissing()) { return mustBeProvided;}foo()// Error: Missing parameterfoo(2) // 22. rest 参数ES6 中 增加了 rest 参数(...变量名),用于获取函数多余的参数,rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。function add(...values) { let sum = 0; for (var val of values) { sum += val; } return sum;}add(2, 5, 3) // 10// 注意:rest 参数之后不能再有其他参数,另外rest参数也不计算在函数的length属性中。3. 严格模式ES6 中,如果函数参数使用了默认值,解构赋值,或者扩展运算符,那么函数内部将不能显式设定为严格模式,否则会报错。因为函数执行的时候 先执行函数参数,在执行函数体,但是因为只有在函数体中才能知道参数是否以严格模式执行,但是参数却应该先于函数执行。有两种方法可以规避:一、 设置全局严格模式,二、把函数包在一个无参数的立即执行函数里面。4. name属性返回函数的函数名,如下:function foo(){}foo.name // foovar f = function(){}// ES5f.name // ''// ES6f.name // fvar f = function c(){}f.name // c5. 箭头函数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; //rest 参数与箭头函数结合的例子。const numbers = (...nums) => nums;numbers(1, 2, 3, 4, 5)// [1,2,3,4,5]const headAndTail = (head, ...tail) => [head, tail];headAndTail(1, 2, 3, 4, 5)// [1,[2,3,4,5]]注意点1. 函数体内的this对象,就是在定义时所在的对象,而不是使用时所在的对象。2. 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。3. 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。4. 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。5. 由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。不适用场景1. 定义对象的方法,且该方法内部包括this2. 动态定义this 的场合,如点击事件中this 的指向嵌套的箭头函数箭头函数内部可以在嵌套使用箭头函数。6. 尾调用优化什么是尾调用函数式编程的一个重要概念,指某个函数的最后一步是调用另一个函数function f(x){ return g(x);}// 一下都不属于// 情况一function f(x){ let y = g(x); return y;}// 情况二function f(x){ return g(x) + 1;}// 情况三function f(x){ g(x);}尾调用优化只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。function f() { let m = 1; let n = 2; return g(m + n);}f();// 等同于function f() { return g(3);}f();// 等同于g(3);注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。 ...

April 26, 2019 · 3 min · jiezi

gulp-4-前后端不分离模板切图仔的最爱

gulp 4 前后端不分离模板(切图仔的最爱)前后端不分离的多页应用, 而且前端只是负责做静态图 本脚手架基于gulp 4, 主要功能包括:less以及css的处理: less转css, autoprefixhtml处理: gulp-file-include模板, 对应后端jsp的include语法img压缩热启动js语法检查(因为我是切图仔, 写的js是基本效果, 必须让后端xx看明白)es6 写配置文件TODO压缩与不压缩; 检查完善demo, 例如: 模板demo细节, 比如min文件不压缩, lib库(包含img css js)的处理性能优化运行运行gulp, 查看页面 http://localhost:3000/page/about.html github 下载地址: https://github.com/phoenix-yassin/gulp4-demos

April 25, 2019 · 1 min · jiezi

ES6

Symbol概述ES6引进了一种新的原始数据类型Symbol表示独一无二的值。它是JavaScript语言的第七种类型。Symbol值是通过Symbol函数生成。这就是说,对象的属性名吸纳在可以有两种类型,一种是原来的字符串,另一种就是新增的Symbol类型。凡是属性名属于Symbol类型,就都是独一无二的,可以保证不会与其他属性名冲突。 let s = Symbol();alert(typeof s) //Symbol注意: 1.symbol函数不能使用new命令,否则会报错2.Symnol是一个原始类型的值,不是对象(不能添加属性)3.它是一种类似于字符串的数据类型Symbol函数可以接收一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时比较容易区分。 Symbol函数的参数只是表示对当前Symbol值的描述,因此相同参数的Symbol函数的返回值是不相等的。 // 没有参数的情况let s1 = Symbol();let s2 = Symbol();s1 === s2 // false//有参数的情况let s1 = Symbol('foo')let s1 = Symbol('bar')s1 === s2 // false //如果参数是一个对象,就会调用改对象的toString方法,将其转为字符串,然后才生成一个Symbol值const obj = { toString() { return 'abc'; }};const sym = Symbol(obj);sym // Symbol(abc)作为属性名的Symbollet mySymbol = Symbol();// 第一种写法let a = {};a[mySymbol] = 'Hello!';// 第二种写法let a = { [mySymbol]: 'Hello!'};// 第三种写法let a = {};Object.defineProperty(a, mySymbol, { value: 'Hello!' });// 以上写法都得到同样结果a[mySymbol] // "Hello!"a.mySymbol // undefined //作为对象属性名时,不能用点运算符。a.mySymbol = 'Hello!';a[mySymbol] // undefineda['mySymbol'] // "Hello!"//因为点运算符后面是字符串,所以不会读取mySymbol作为标识名所指代的那个值,导致a的属性名实际上是一个字符串,而不是一个 Symbol 值。属性名的遍历Symbol作为属性名,该属性不会出现在for...in、for...of也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是它也不是私有属性。有一个Object。getOwnPropertySymbols方法,可以获取指定对象的所有Symbol属性名。 ...

April 23, 2019 · 1 min · jiezi

生成器与迭代器

之前的文章 写到了 Generator 与异步编程的关系,其实简化异步编程只是 Generator 的“副业”,Generator 本身却不是为异步编程而存在。 生成器函数我们看 Generator 自身的含义——生成器,就是产生序列用的。比如有如下函数: function* range(start, stop) { for (let item = start; item < stop; ++item) { yield item; }}range 就是一个生成器函数,它自身是函数可以调用(typeof range === 'function' // true),但又与普通函数不同,生成器函数(GeneratorFunction)永远返回一个生成器(Generator) 注:我们通常所说的 Generator 实际上指生成器函数(GeneratorFunction),而把生成器函数返回的对象称作迭代器(Iterator)。由于感觉“生成器函数”返回“生成器”这句话有些拗口,下文沿用生成器和迭代器的说法。 迭代器初次调用生成器实际上不执行生成器函数的函数体,它只是返回一个迭代器,当用户调用迭代器的 next 函数时,程序才开始真正执行生成器的函数体。当程序运行到 yield 表达式时,会将 yield 后面表达式的值作为 next 函数的返回值(的一部分)返回,函数本身暂停执行。 const iterator = range(0, 10); // 获取迭代器const value1 = iterator.next().value; // 获取第一个值 => 0const value2 = iterator.next().value; // 获取第二个值 => 1next 返回值是一个对象,包含两个属性 value 和 done。value 即为 yield 后面表达式的值,done 表示函数是否已经结束(return)。如果函数 return(或执行到函数尾退出,相当于 return undefined),则 done 为 true,value 为 return 的值。 ...

April 22, 2019 · 2 min · jiezi

前端异步解决方案-4.2(generator+promise)

为什么要实现generator和promise的结合呢?大部分网上的文章都说是为了更简单的处理错误,不过这个我暂时还没有感悟,我反而觉得其实差不多;但是还是先学习一下用法吧;先从简单的用法讲起: //最简单的用法function request() { return new Promise(function (resolve, reject) { setTimeout(function () { let num = Math.random(); if (num > 0.9) { reject(num + "request err") } else { resolve(num * 100); } }, 100) })}let it = gen();let p = it.next().value;// 在yield处被返回的Promise 被赋给了 pp.then(res => it.next(res).value, err => it.throw(err));// 发生错误时,将错误抛入生成器(gen)中function* gen() { try { let response = yield request(); console.log(response.text); } catch (error) { console.log('Ooops, ', error.message); // 可以捕获Promise抛进来的错误!}} }}这里request的写法就是普通的Promise异步的写法,而gen中异步的写法就已经非常像同步了,唯一一个缺点就是中间这一段代码在我们多次异步的时候,我们需要不断的加长就像这样 ...

April 21, 2019 · 2 min · jiezi

ES6入门之字符串的扩展

字符的 Unicode 表示法Javascript 中允许采用 uxxxx的形式表示一个字符,其中xxxx表示字符的Unicode码点。如下"\u0061"这种 表示有区间 只能在 0000 ~ FFFF之间,如果超出则需要用双字节的的形式表示"\u20BB7\uDFB1"在ES6中,对字符串的写法做出了改变。改为将码点放入大括号中,就能正确读取该字符,如下:"\u{20BB7}" // 吉2. codePointAt()Javascript内部,字符以 UTF-16的格式存储,每个字符固定为2个字节。对于需要4个字节储存的字符,JavaScript会认为它们是两个字符。如下:var s = “吉"s.length // 2s.charAt(0) // ‘’s.charAt(1) // ‘’s.charCodeAt(0) // 55362s.charCodeAt(1) // 57271上面代码中汉字吉 需要4个字节存储,但是JavaScript不能正确处理,字符串长度误判为2,而且charAt 方法无法读取字符,charCodeAt 只能返回前面两个字节和后面两个字节的值,ES6 提供了 codePointAt 方法能够正确处理4个字节存储的字符,返回一个字符的码点,如下:let s = ‘吉a’;s.codePointAt(0) // 134071s.codePointAt(1) // 57271codePointAt() 方法是测试一个字符由两个字节还是四个字节组成的最简单的方法3. String.fromCodePoint()ES5 提供了 String.fromCharCode 方法 用于从码点返回对应字符,但是不能识别32位的UTR-16字符,ES6提供了 Sring.fromCodePoint() 方法 可以识别大于32位的。如果有多个参数则将合并。如下:String.fromCodePoint(0x20BB7)// “????“String.fromCodePoint(0x78, 0x1f680, 0x79) === ‘x\uD83D\uDE80y’// true注意:fromCodePoint 方法定义在 String 对象上,而codePointAt 方法定义在字符串实例对象上4. 字符串的遍历器接口ES6为字符串添加了遍历器接口,字符串可以被for of 遍历5. normalize()用来将字符的不同表示方法统一为同样的形式,6. includes(), startsWith(), endsWith()传统上,JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。includes():返回布尔值,表示是否找到了参数字符串。startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。7. repeat()用来返回一个新字符串,表示将原字符串重复N次’x’.repeat(2) // ‘xx’‘cx’.repeat(3) // ‘cxcxcx’参数NaN等同于 0。’na’.repeat(NaN) // ““如果repeat的参数是字符串,则会先转换成数字。’na’.repeat(’na’) // “"’na’.repeat(‘3’) // “nanana"8. padStart(),padEnd()用于字符串的补全,接受两个参数,第一个字符串补全生效的最大长度,第二个是用来补全的字符串,如下:‘x’.padStart(5, ‘ab’) // ‘ababx’‘x’.padStart(4, ‘ab’) // ‘abax’‘x’.padEnd(5, ‘ab’) // ‘xabab’‘x’.padEnd(4, ‘ab’) // ‘xaba’注意:如果原字符串的长度等于或大于最大长度,则补全不生效,返回原字符串如果补全字符串和原字符串,两者长度超过最大长度,则截取超出位数的补全字符串9. matchAll()matchAll方法返回一个正则表达式在当前字符串的所有匹配10. 模板字符串用来简化之前模板的写法,写法如下:$(’#result’).append( ‘There are <b>’ + basket.count + ‘</b> ’ + ‘items in your basket, ’ + ‘<em>’ + basket.onSale + ‘</em> are on sale!’);上面这种写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题。$(’#result’).append( There are &lt;b&gt;${basket.count}&lt;/b&gt; items in your basket, &lt;em&gt;${basket.onSale}&lt;/em&gt; are on sale!);大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。let x = 1;let y = 2;${x} + ${y} = ${x + y}// “1 + 2 = 3”${x} + ${y * 2} = ${x + y * 2}// “1 + 4 = 5"let obj = {x: 1, y: 2};${obj.x + obj.y}// “3"11. 模板编译使用<%…%>放置 JavaScript 代码,使用<%= … %>输出 JavaScript 表达式,如下:let template = &lt;ul&gt; &lt;% for(let i=0; i &lt; data.supplies.length; i++) { %&gt; &lt;li&gt;&lt;%= data.supplies[i] %&gt;&lt;/li&gt; &lt;% } %&gt;&lt;/ul&gt;;12. 标签模板模板字符串可以跟在一个函数名后面,该函数将被调用来处理这个模板字符串,这被称为 标签模板。 如下:alert(123)alert123标签模板其实不是模板,而是函数的一种特殊形式。“标签”就是函数,跟在后面的模板字符串就变成了它的参数。但是如果模板字符串里面有变量,就会将模板字符串先处理成多个参数,再调用函数。如下:let a = 5;let b = 10;tagHello ${ a + b } world ${ a * b };// 等同于tag([‘Hello ‘, ’ world ‘, ‘’], 15, 50);String.raw()ES6 为原生 String 对象,提供了一个raw方法。用来充当模板字符串的处理函数,返回一个斜杠都被转义的字符串,对应于替换变量后的模板字符串。如下:String.rawHi\n${2+3}!;// 返回 “Hi\n5!“String.rawHi\u000A!;// 返回 “Hi\u000A!“模板字符串的限制模板字符串默认会将字符串转义,导致无法嵌入其他语言。为了解决这个问题,ES2018 放松了对标签模板里面的字符串转义的限制。如果遇到不合法的字符串转义,就返回undefined,而不是报错,并且从raw属性上面可以得到原始字符串。欢迎关注 公众号【小夭同学】ES6入门系列ES6入门之let、contES6入门之变量的解构赋值Git教程前端Git基础教程

April 19, 2019 · 2 min · jiezi

《前端面试手记》之ES6重难点整理

???? 内容速览 ????let和constSet和MapGenerator和yieldPromise、async/await介绍Proxy代理器…????查看全部教程 / 阅读原文????let和constES6新增了let和const,它们声明的变量,都处于“块级作用域”。并且不存在“变量提升”,不允许重复声明。同时,const声明的变量所指向的内存地址保存的数据不得改变:对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),不能保证指向的数据结构不可变。如果要保证指向的数据结构也不可变,需要自行封装:/** * 冻结对象 * @param {Object} obj * @return {Object} */function constantize(obj) { if(Object.isFrozen(obj)) { return obj } Reflect.ownKeys(obj).forEach(key => { // 如果属性是对象,递归冻结 typeof obj[key] === ‘object’ && (obj[key] = constantize(obj[key])) }); return Object.freeze(obj)}/********测试代码 ***/const obj = { a: 1, b: { c: 2, d: { a: 1 } }, d: [ 1, 2 ]}const fronzenObj = constantize(obj)try { fronzenObj.d = [] fronzenObj.b.c = 3} catch(error) { console.log(error.message)}Set和Map题目:解释下Set和Map。Set元素不允许重复Map类似对象,但是它的键(key)可以是任意数据类型①Set常用方法// 实例化一个setconst set = new Set([1, 2, 3, 4]);// 遍历setfor (let item of set) { console.log(item);}// 添加元素,返回Set本身set.add(5).add(6);// Set大小console.log(set.size);// 检查元素存在console.log(set.has(0));// 删除指定元素,返回boollet success = set.delete(1);console.log(success);set.clear();其他遍历方法:由于没有键名,values()和keys()返回同样结果。for (let item of set.keys()) { console.log(item);}for (let item of set.values()) { console.log(item);}for (let item of set.entries()) { console.log(item);}②Map常用方法Map接口基本和Set一致。不同的是增加新元素的API是:set(key, value)const map = new Map();// 以任意对象为 Key 值// 这里以 Date 对象为例let key = new Date(); map.set(key, “today”);console.log(map.get(key));Generator与yieldgenerator函数是es6提供的新特性,它的最大特点是:控制函数的执行。让我们从网上最火的一个例子来看:function foo(x) { var y = 2 * (yield x + 1); var z = yield y / 3; return x + y + z;}var b = foo(5);b.next(); // { value:6, done:false }b.next(12); // { value:8, done:false }b.next(13); // { value:42, done:true }通俗的解释下为什么会有这种输出:给函数foo传入参数5,但由于它是generator,所以执行到第一个yield前就停止了。第一次调用next(),这次传入的参数会被忽略暂停。第二次调用next(12),传入的参数会被当作上一个yield表达式的返回值。因此,y = 2 * 12 = 24。执行到第二个yield,返回其后的表达式的值 24 / 3 = 8。然后函数在此处暂停。第三次调用next(13),没有yield,只剩return了,按照正常函数那样返回return的表达式的值,并且done为true。难点:在于为什么最后的value是42呢?首先,x的值是刚开始调用foo函数传入的5。而最后传入的13被当作第二个yield的返回值,所以z的值是13。对于y的值,我们在前面第三步中已经计算出来了,就是24。所以,x + y + z = 5 + 24 + 13 = 42看懂了上面的分析,再看下面这段代码就很好理解了:function foo(x) { var y = 2 * (yield x + 1); var z = yield y / 3; return x + y + z;}var a = foo(5);a.next(); // Object{value:6, done:false}a.next(); // Object{value:NaN, done:false}a.next(); // Object{value:NaN, done:true}只有第一次调用next函数的时候,输出的value是6。其他时候由于没有给next传入参数,因此yield的返回值都是undefined,进行运算后自然是NaN。Promise介绍简单归纳下 Promise:三个状态、两个过程、一个方法三个状态:pending、fulfilled、rejected两个过程(单向不可逆):pending->fulfilledpending->rejected一个方法then:Promise本质上只有一个方法,catch和all方法都是基于then方法实现的。请看下面这段代码:// 构造 Promise 时候, 内部函数立即执行new Promise((resolve, reject) => { console.log(“new Promise”); resolve(“success”);});console.log(“finifsh”);// then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装Promise.resolve(1) .then(res => { console.log(res); // => 1 return 2; // 包装成 Promise.resolve(2) }) .then(res => { console.log(res); // => 2 });async/await介绍async函数返回一个Promise对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。这也是它最受欢迎的地方:能让异步代码写起来像同步代码,并且方便控制顺序。可以利用它实现一个sleep函数阻塞进程:function sleep(millisecond) { return new Promise(resolve => { setTimeout(() => resolve, millisecond) })}/ * 以下是测试代码 /async function test() { console.log(‘start’) await sleep(1000) // 睡眠1秒 console.log(’end’)}test() // 执行测试函数虽然方便,但是它也不能取代Promise,尤其是我们可以很方便地用Promise.all()来实现并发,而async/await只能实现串行。function sleep(second) { return new Promise(resolve => { setTimeout(() => { console.log(Math.random()); resolve(); }, second); });}async function chuanXingDemo() { await sleep(1000); await sleep(1000); await sleep(1000);}async function bingXingDemo() { var tasks = []; for (let i = 0; i < 3; ++i) { tasks.push(sleep(1000)); } await Promise.all(tasks);}运行bingXingDemo(),几乎同时输出,它是并发执行;运行chuanXingDemo(),每个输出间隔1s,它是串行执行。ES6对象和ES5对象题目:es6 class 的new实例和es5的new实例有什么区别?在ES6中(和ES5相比),class的new实例有以下特点:class的构造参数必须是new来调用,不可以将其作为普通函数执行es6 的class不存在变量提升最重要的是:es6内部方法不可以枚举。es5的prototype上的方法可以枚举。为此我做了以下测试代码进行验证:console.log(ES5Class()) // es5:可以直接作为函数运行// console.log(new ES6Class()) // 会报错:不存在变量提升function ES5Class(){ console.log(“hello”)}ES5Class.prototype.func = function(){ console.log(“Hello world”) }class ES6Class{ constructor(){} func(){ console.log(“Hello world”) }}let es5 = new ES5Class()let es6 = new ES6Class()// 推荐在循环对象属性的时候,使用for…in// 在遍历数组的时候的时候,使用for…ofconsole.log(“ES5 :")for(let _ in es5){ console.log()}// es6:不可枚举console.log(“ES6 :")for(let _ in es6){ console.log()}参考/推荐:《JavaScript创建对象—从es5到es6》Proxy代理器他可以实现js中的“元编程”:在目标对象之前架设拦截,可以过滤和修改外部的访问。它支持多达13种拦截操作,例如下面代码展示的set和get方法,分别可以在设置对象属性和访问对象属性时候进行拦截。const handler = { // receiver 指向 proxy 实例 get(target, property, receiver) { console.log(GET: target is ${target}, property is ${property}) return Reflect.get(target, property, receiver) }, set(target, property, value, receiver) { console.log(SET: target is ${target}, property is ${property}) return Reflect.set(target, property, value) }}const obj = { a: 1 , b: {c: 0, d: {e: -1}}}const newObj = new Proxy(obj, handler)/ * 以下是测试代码 */newObj.a // output: GET…newObj.b.c // output: GET…newObj.a = 123 // output: SET…newObj.b.c = -1 // output: GET…运行这段代码,会发现最后一行的输出是 GET …。也就是说它触发的是get拦截器,而不是期望的set拦截器。这是因为对于对象的深层属性,需要专门对其设置Proxy。更多请见:《阮一峰ES6入门:Proxy》EsModule和CommonJS的比较目前js社区有4种模块管理规范:AMD、CMD、CommonJS和EsModule。 ES Module 是原生实现的模块化方案,与 CommonJS 有以下几个区别:CommonJS 支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案:import(xxx)CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响commonJs输出的是值的浅拷贝,esModule输出值的引用ES Module 会编译成 require/exports 来执行的更多系列文章⭐在GitHub上收藏/订阅⭐《前端知识体系》JavaScript基础知识梳理(上)JavaScript基础知识梳理(下)ES6重难点整理谈谈promise/async/await的执行顺序与V8引擎的BUG前端面试中常考的源码实现Flex上手与实战……《设计模式手册》单例模式策略模式代理模式迭代器模式订阅-发布模式桥接模式备忘录模式模板模式抽象工厂模式……《Webpack4渐进式教程》webpack4 系列教程(二): 编译 ES6webpack4 系列教程(三): 多页面解决方案–提取公共代码webpack4 系列教程(四): 单页面解决方案–代码分割和懒加载webpack4 系列教程(五): 处理 CSSwebpack4 系列教程(八): JS Tree Shakingwebpack4 系列教程(十二):处理第三方 JavaScript 库webpack4 系列教程(十五):开发模式与 webpack-dev-server……⭐在GitHub上收藏/订阅⭐ ...

April 19, 2019 · 3 min · jiezi

7个javascript实用小技巧

每种编程语言都有一些“黑魔法”或者说小技巧,JS也不例外,大部分是借助ES6或者浏览器新特性实现。下面介绍的7个实用小技巧,相信其中有些你一定用过。数组去重const j = […new Set([1, 2, 3, 3])]>> [1, 2, 3]数组清洗洗掉数组中一些无用的值,如 0, undefined, null, false 等myArray .map(item => { // … }) // Get rid of bad values .filter(Boolean);创建空对象我们可以使用对象字面量{}来创建空对象,但这样创建的对象有隐式原型__proto__和一些对象方法比如常见的hasOwnProperty,下面这个方法可以创建一个纯对象。let dict = Object.create(null);// dict.proto === “undefined”// No object properties exist until you add them该方法创建的对象没有任何的属性及方法合并对象JS中我们经常会有合并对象的需求,比如常见的给用传入配置覆盖默认配置,通过ES6扩展运算符就能快速实现。const person = { name: ‘David Walsh’, gender: ‘Male’ };const tools = { computer: ‘Mac’, editor: ‘Atom’ };const attributes = { handsomeness: ‘Extreme’, hair: ‘Brown’, eyes: ‘Blue’ };const summary = {…person, …tools, …attributes};/Object { “computer”: “Mac”, “editor”: “Atom”, “eyes”: “Blue”, “gender”: “Male”, “hair”: “Brown”, “handsomeness”: “Extreme”, “name”: “David Walsh”,}/设置函数必传参数借助ES6支持的默认参数特性,我们可以将默认参数设置为一个执行抛出异常代码函数返回的值,这样当我们没有传参时就会抛出异常终止后面的代码运行。const isRequired = () => { throw new Error(‘param is required’); };const hello = (name = isRequired()) => { console.log(hello ${name}) };// This will throw an error because no name is providedhello();// This will also throw an errorhello(undefined);// These are good!hello(null);hello(‘David’);解构别名ES6中我们经常会使用对象结构获取其中的属性,但有时候会想重命名属性名,以避免和作用域中存在的变量名冲突,这时候可以为解构属性名添加别名。const obj = { x: 1 };// Grabs obj.x as { x }const { x } = obj;// Grabs obj.x as as { otherName }const { x: otherName } = obj;获取查询字符串参数以前获取URL中的字符串参数我们需要通过函数写正则匹配,现在通过URLSearchParamsAPI即可实现。// Assuming “?post=1234&action=edit"var urlParams = new URLSearchParams(window.location.search);console.log(urlParams.has(‘post’)); // trueconsole.log(urlParams.get(‘action’)); // “edit"console.log(urlParams.getAll(‘action’)); // [“edit”]console.log(urlParams.toString()); // “?post=1234&action=edit"console.log(urlParams.append(‘active’, ‘1’)); // “?post=1234&action=edit&active=1"随着Javascript的不断发展,很多语言层面上的不良特性都在逐渐移除或者改进,如今的一行ES6代码可能等于当年的几行代码。拥抱JS的这些新变化意味着前端开发效率的不断提升,以及良好的编码体验。当然不管语言如何变化,我们总能在编程中总结一些小技巧来精简代码。本文首发于公众号「前端新视界」,如果你也有一些Javascript的小技巧,欢迎在下方留言,和大家一起分享哦! ...

April 17, 2019 · 1 min · jiezi

前端异步解决方案-3(Promise)

又有好些天没有动笔了,这几天一直在断断续续的学习Promise和generator,今天终于能够把着两个玩意结合起来了解决异步问题了。今天我先把promise相关的用法和对异步的处理分享给大家。老样子,还是先模拟一个Promise。//咳咳这样就实现了嘛let MyPromise = Promise;开个玩笑,其实这两天我也一直在看Promise的实现,但是还是没有怎么理解。所以Promise的代码实现我暂时先放一放,等我完全理解再来新开一篇分享。这里先给大家推荐几篇我觉得比较好的Promise实现的博客,想要学习的小伙伴可以先到那边一睹为快,当然等我更新了之后大家还是要来给我文章点赞的哈。彻底理解Promise对象——用es5语法实现一个自己的Promise(上篇)这一篇文章用的es5的语法,对es6不是很熟悉的同学可以用这篇文章顶一下Promise实现原理(附源码)这一篇呢是用到了class的,所以需要大家对es6有所了解,也是一篇好文章接下来我介绍一下Promise最常用的几个部分:首先先介绍最简单的只有一个Promise对象的用法//Promise()接收一个函数,并且在创建这个Promise对象的同时,接收的这个函数就会被立刻执行var promise = new Promise(function (resolve, reject) { //resolve用来接收成功的返回,reject用来接收失败的返回 setTimeout(function () { //这里我们生成一个随机数,并在接下来根据这个随机数的大小来判断这个异步是否成功 let num = Math.random(); if (num > 0.8) { //接收失败的原因 console.log(“reject”) reject(num + “大于0.8,这次异步操作失败了”) } else { //接收成功的数据 console.log(“resolve”) resolve(num + “小于0.8,这次异步操作成功了”) } }, 100)});console.log(“一个Promise对象生成了”);//Promise对象的.then()方法接收两个回调,第一个为成功回调,第二个为失败回调//这两个回调会在上面的resolve或者reject函数生效后被调用promise.then(function (data) { //这个函数会在上面的resolve函数被调用后生效,这里的data就是上面resolve()中接收的值 console.log(data)}, function (err) { ///这个函数会在上面的reject函数被调用后生效,这里的err就是上面reject()中接收的值 console.error(err)});console.log(“promise的.then方法被调用了”)大家可以按F12唤起控制台,然后把这段代码运行几次。看看会有什么结果; 多运行几次以后,大家应该可以看到这两类结果: 其中的undefind是console.log(“promise的.then方法被调用了”)这行代码的返回,大家可以不用关注。在这里可以看到不论是成功的结果还是失败的结果都是在定时器执行后再打印出来的。这种写法可以帮助我们实现简单的异步编程。接下介绍多个Promise对象同时使用的用法, 先介绍最常见的.then()链式调用的方法 //用来快速生成一个Promise对象,接收一个日志列表,不论成功还是失败都会往日志列表中添加一条日志 function promise(log) { return new Promise(function (resolve, reject) { setTimeout(function () { log = log || []; //和上次的例子一样,利用随机数来随机失败和成功 let num = Math.random(); if (num > 0.5) { log.push(num + “大于0.5,这次异步操作失败了”); reject(log) } else { log.push(num + “小于0.5,这次异步操作成功了”); resolve(log) } }, 100) }) }.then()中返回了Promise对象的情况var promise1 = promise();//promise1.then()方法会返回一个Promise对象//如果我们在.then()生效的那个 !!!回调方法!!! 中有返回一个Promise对象的话,该对象会被 !!!.then()方法!!! 返回//先看返回了Promise对象的方式promise1.then(function (data) { console.log(data); return promise(data)}, function (err) { console.error(err); return promise(err)}).then(function (data) { console.log(data)}, function (err) { console.error(err)});这段代码运行后一共会有四种结果: 两次都成功 两次都失败 第一次失败,第二次成功 第一次成功,第二次失败通过这种方法我们可以用比较清晰的方式来书写我们的异步代码。特别是多个异步操作嵌套的时候,可以链式调用.then()来实现,这样的代码看起来逻辑更清晰; 刚刚看完了返回了Promise对象的场景,再来看看没有返回Promise的场景//如果我们没有返回Promise对象,.then()就会将我们返回的东西包装成一个Promise对象(没有返回就相当于返回了undefined)//可以等同于我们写了 return new Promise((resolve,reject)=>{resolve(/原先的返回值/)})promise1.then(function (data) { console.log(data); return data;}, function (err) { console.error(err); return err;}).then(function (data) { console.log(data)}, function (err) { //这里是永远不会被触发的,原因是上一个.then() 返回的是new Promise((resolve,reject)=>{resolve(/原先的返回值/)}) //返回的Promise对象的reject方法永远都不会被触发,所以这个里也就永远都不会触发了 console.error(err)});讲解都写在注释里面了,接下里我就贴运行图吧,这段代码会运行出以下两类结果:需要所有的请求都返回后才可以执行某个动作//改造promise,让其可以接收一个定时器等待时间参数function promise(log, time) { return new Promise(function (resolve, reject) { setTimeout(function () { log = log || []; //和上次的例子一样,利用随机数来随机失败和成功 let num = Math.random(); if (num > 0.5) { log.push(“等待时长” + time + “,” + num + “大于0.5,这次异步操作失败了”); console.error(log); reject(log) } else { log.push(“等待时长” + time + “,” + num + “小于0.5,这次异步操作成功了”); console.log(log); resolve(log) } }, time) })}//Promise.all()可以接收一个Promise对象的数组,返回一个Promise对象//该Promise对象会在数组中所有Promise成功返回后执行成功回调,在任意一个Promise失败后立刻执行失败回调var promise1 = promise(null, 10), promise2 = promise(null, 100), promise3 = Promise.all([promise1, promise2]);promise3.then((data) => { //这里的data为promise1,和promise2的返回值的数组 console.log(“promise3”, data)}, (err, err2) => { //报错信息 console.error(“promise3”, err)});这段代码一共有四种可能的结果 如果两次都成功的话 如果两次都成的话,promise3会执行成功回调,并且回调中的data就是promise1和promise2返回值的数组(数组顺序和.all()中的顺序一致)任意一个promise失败的话,promise3会立刻执行失败回调,并且回调中的err就是失败的那个promise在reject中返回的值文章写到这里,我认为Promise常用的一些用法都已经讲完了,更详细的Promise的教程请参考 MDN中对promise的讲解 ...

April 16, 2019 · 2 min · jiezi

ES6基础

一、块级作用域1. var首先看看ES5中得变量声明方式if (true) { var a = 2}console.log(a) // 2以上代码等同于var aif (true) { a = 2}console.log(a)以上可知 :在块内部定义变量 变量提升,到函数最顶部通过var声明的变量,无论在何处声明,均为全局作用域2.let 和 const再来看看ES6中的let和constletif (true) { let b = 2}console.log(b) // b is not defined此时在{} 外部访问b 将会报错,因为 let 的作用域仅为 { } 的内部,及块级作用域constif (true) { const c = 2}console.log(c) // c is not defined从上面可知 const 也是也仅为块级作用域const的作用域与let作用域相同:只在声明所在的块级作用域内有效让我们看看const 更多的特性:const 表示常量:const d = 2d = 3 // Assignment to constant variable.此时,当 d 为 基本数据类型的时候,改变其值,将会报错!!!但是它的常量仅仅表示的是地址常量 对象的成员可以改变值看看下面的例子:const people = {name: ‘张三’, age: 23}people.age = 25console.log(people) // {name: “张三”, age: 25}看看此时 people已经被改变了why?对象是复杂的数据类型 它的地址保存在 栈里面, 值保存在堆里面cosnt仅仅是保证这个地址不改变,至于地址对应的数据,是可以进行改变的基本类型值在内存中占据固定大小的空间 因此被保存在栈内存中。比如 const a = 1 ; 这时候其直接保存在栈里面结合网上的内容,个人理解而得 欢迎指正 ...

April 16, 2019 · 1 min · jiezi

JavaScript ES6相关的一些知识(/let、const/箭头函数/Promise/generate)

ES6是个啥ECMAScript是国际通过的标准化脚本语言JavaScript由ES,BOM,DOM组成ES是JavaScript的语言规范,同时JavaScript是ES的实现和扩展6就是JavaScript语言的下一代标准关于ES6的一些知识1.let、constES5中的作用域有:函数作用域,全局作用域ES6新增了块级作用域。由{}包括(if和for语句也属于块级作用域){ var a = 1; let b = 2; }console.log(a)//1console.log(b)//undefinedlet、const、var的区别var:可以跨块级作用域访问,不能跨函数作用域访问let:只能在块级作用域访问,不能跨函数使用const:定义常量,必须初始化且不能修改,只能在块级作用域内使用关于变量提升:var不论声明在何处都会莫默认提升到函数/全局最顶部,但是let和const不会进行变量提升2.arrow function 箭头函数箭头函数相当于匿名函数,简化了函数的定义定义就用=> 一个箭头箭头函数有两种格式:第一种:只包含一个表达式x=>x++相当于function(x)}{ return x++;}第二种:包含多条语句://包含判断等x=>{ if(x>0){ return x++; } else{ x–; }}//多个参数(x,y,z….)=>x+y+z+…+//无参数()=>1//返回对象x=>({obj:x})//注意符号,避免和函数体的{}冲突使用箭头函数时,函数体内的this对象,就是定义时所在的对象3.Promise定义:Promise对象用于异步操作,它表示一个尚未完成且预计在未来完成的异步操作关于同步&异步JavaScript是基于事件驱动的单线程运行机制(why: 浏览器中至少有三个线程:js引擎线程,gui渲染线程,浏览器事件触发线程js操作dom,浏览器事件触发都会影响gui渲染效果,因此他们之间存在互斥的关系,使用多线程会带来非常复杂的同步问题(a线程在某个DOM节点添加内容。b线程删除了该节点)同步:即单线程模式,所有的任务都在主线程上执行,形成一个执行栈*,如函数调用后需要等待函数执行结束后才能进行下一个任务,如果某个任务执行时间过长(如死循环),容易造成线程阻塞,影响下面任务的正常进行异步:可以一起执行多个任务,函数调用后不会立刻就返回执行结果,异步任务会在当前脚本所有的同步任务执行结束后再执行。异步任务不进入主线程,而是进入任务队列,在某个任务可以执行时,等待主线程读取任务队列,随后该任务将进入主线程执行异步任务的实现,最经典的就是setTimeout()/setInterval()他们的内部运行机制完全一样,前者指定的代码只执行一次,后者为反复执行setTimeout(function(){ console.log(“taskA,yibu”);},0)console.log(“taskB,tongbu”);//taskB,tongbu//taskA,yibu即使延时事件为0,但由于它属于异步任务,仍需要等待同步任务执行结束后再执行综合看,整体的执行顺序为:先执行执行栈中的内容,执行完毕后,读取任务队列,寻找对应的异步任务,结束等待状态,进入执行栈执行,不断循环(Event Loop)只要主线程空了,就会去读取任务队列关于回调函数,callback()回调函数即是会被主线程挂起的代码异步任务必须指定回调函数,当主线程开始读取任务队列,执行异步任务的时候,执行的就是对应的回调函数言归正传,继续理解Promisepromise的三种状态:pending:初始状态fulfilled:操作成功rejected:操作失败Promise可以由1->2/1->3一旦状态变化,便会一直保持这个状态,不再改变。当状态改变Promise.then绑定的函数就会被调用构建Promisevar promise = new Promise(function(resolve,reject){ if(/操作成功/) resolve(data); else reject(error);});异步操作成功调用resolve,将结果作为参数传递出去异步操作失败调用reject,将报出的错误作为参数传递出去Promise构建完成后,使用then方法指定resolve状态和reject状态的回调函数promise.then(成功回调函数,失败的回调函数(非必要))//这两个函数都接受promise传出的值作为参数promise.then(function(data){do xxxx for success},function(error){do xxxx for failure});Promise新建后就执行,then方法指定的回调函数会在当前脚本的所有同步任务执行结束后再执行例子:var promise = new Promise(function(resolve, reject) { console.log(‘before resolved’); resolve(); console.log(‘after resolved’);});promise.then(function() { console.log(‘resolved’);});console.log(‘outer’);//before resolved//after resolved//outer//resolvedPromise的优势在于,可以在then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作。能够简化层层回调的写法。Promise的精髓在于,用维护状态、传递状态的方式使得回调函数能够及时调用,比传递callback要简单、灵活Promise的其他方法.catch()用于指定发生错误时的回调函数,等同于reject部分和reject的区别:promise.then(onFulfilled,onRejected)在onFulfilled发生异常,在onRejected中捕获不到promise.then(onFulfilled).catch(onRejected)能够捕获异常。也可以用then替换,只是写法不同。本质上没有区别.all()用于将多个Promise实例包装成一个新的Promise实例var p = Promise.all([p1, p2, p3]);p1p2p3都需为promise实例当p1p2p3都为fulfilled时,p才会变为fulfilled只要有一个变为rejected,p就会变成rejected.race()用于将多个Promise实例包装成一个新的Promise实例与all()的区别类似于 AND 和 ORp1p2p3有一个状态发生改变,p的状态就发生改变,并返回第一个改变状态的promsie返回值,传递给p.resolve()看作new Promise()的快捷方式实例:Promise.resolve(‘Success’);/等同于/new Promise(function (resolve) { resolve(‘Success’);});让对象立刻进入resolved状态4.generate可以将generate理解为一个能够多次返回的“函数”function* foo(x){ yield x++; yield x+2; yield x+3; return xx;}由function定义,并且yield也可以返回数据调用generate的方法:不断的使用next()var f = foo(0);console.log(f.next());// 0 falseconsole.log(f.next());// 1 falseconsole.log(f.next());// 3 falseconsole.log(f.next());// 4 true使用for of结构for (var x of foo(0)) { console.log(x); // 依次输出0 1 3 (没有输出4原因不详)}每执行一次后就暂停,返回的值就是yield的返回值,每次返回一个值,直到done为true,这个generate对象已经全部执行完毕generate更像一个能够记住执行状态的函数实际上generate不算是一个函数,它的返回值不是变量也不是函数,而是一个可迭代的对象该对象类似一个元素被定义好的数组,保存的是一种规则而不元素本身,不能够随机访问,遍历也只能够遍历一次,因为规则只保存了上次的状态参考文档1:讲解JavaScript的线程运作参考文档2:讲解Promise参考文档3:关于generate ...

April 16, 2019 · 1 min · jiezi

ES6系列之 let 和 const

为什么需要块级作用域ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。通过var声明变量存在变量提升:if (condition) { var value = 1}console.log(value)初学者可能会认为当变量condition为true时,才会创建value。当condition为false时,不会创建value,结果应该是报错。然而因为JavaScript存在变量提升的概念,代码等同于:var valueif (condition) { value = 1}console.log(value) // undefined所有当condition为false,输入结果为undefined。ES5 只有全局作用域和函数作用域,其中变量提升也分成两种情况:一种全局声明的变量,提升会在全局最上面,上面就属于全局变量声明;一种是函数中声明的变量,提升在函数的最上方:function fn() { var value if (condition) { value = 1 } console.log(value) // undefined}console.log(value) // Uncaught ReferenceError: value is not defined所有当condition为false,函数内输入结果为undefined,函数输入就会报错Uncaught ReferenceError: value is not defined。函数的变量提升是根据最近的外层函数提升,没有函数就为全局下提升。为规范变量使用控制,ECMAScript 6 引入了块级作用域。块级作用域就是 {} 之间的区域let 和 const我们来整理一下 let 和 const 的特点:不存在变量提升if(condition) { let value = 1}console.log(value) // Uncaught ReferenceError: value is not defined不管 conditon 为 true 或者 false ,都无法输出value,结果为 Uncaught ReferenceError: value is not defined重复声明报错let value = 1let value = 2重复声明同一个变量,会直接报错 Uncaught SyntaxError: Identifier ‘value’ has already been declared不绑定在全局作用域上var value = 1console.log(window.value) // 1在来看一下let声明:let value = 1console.log(window.value) // undefinedlet 和 const 的区别:const声明一个只读的常量。一旦声明,常量的值就不能改变。const value = 1value = 2 // Uncaught TypeError: Assignment to constant variable.上面代码表明改变常量的值会报错。const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。const foo;// SyntaxError: Missing initializer in const declaration上面代码表示,对于const来说,只声明不赋值,就会报错。对于对象的变量,变量指向是数据指向的内存地址。const只能保证数据指向内存地址不能改变,并不能保证该地址下数据不变。const data = { value: 1}// 更改数据data.value = 2console.log(data.value) // 2// 更改地址data = {} // Uncaught TypeError: Assignment to constant variable.上述代码中,常量 data 存储的是一个地址,这里地址指向一个对象。不可变的只是这个地址,即不能将 data 指向另一个地址,但是对象本身是可以变的,所以依然为其更改或添加新属性。暂时性死区在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。let 和 const 声明的变量不会被提升到作用域顶部,如果在声明之前访问这些变量,会导致报错:console.log(typeof value); // Uncaught ReferenceError: value is not definedlet value = 1;这是因为 JavaScript 引擎在扫描代码发现变量声明时,要么将它们提升到作用域顶部(遇到 var 声明),要么将声明放在 TDZ 中(遇到 let 和 const 声明)。访问 TDZ 中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从 TDZ 中移出,然后方可访问。看似很好理解,不保证你不犯错:var value = ‘global’;// 例子1(function() { console.log(value); let value = ’local’;}());// 例子2{ console.log(value); const value = ’local’;};两个例子中,结果并不会打印 “global”,而是报错 Uncaught ReferenceError: value is not defined,就是因为 TDZ 的缘故。常见面试题for(var i = 0; i < 3; i++) { setTimeout(() => { console.log(i) })}// 3// 3// 3上述代码中,我们期望输出0,1,2三个值,但是输出结果是 3,3,3 ,不符合我们的预期。解决方案如下:使用闭包解法for(var i = 0; i < 3; i++) { (function(i) { setTimeout(() => { console.log(i) }) })(i)}// 0// 1// 2ES6 的 let 解法for(let i = 0; i < 3; i++) { setTimeout(() => { console.log(i) })}// 0// 1// 2上述代码中,变量 i 是 let 声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是0,1,2。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值。这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算.另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。for (let i = 0; i < 3; i++) { let i = ‘abc’; console.log(i);}// abc// abc// abc上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。如果尝试将 let 改成 const 定义:for (const i = 0; i < 3; i++) { console.log(i);}// 0// Uncaught TypeError: Assignment to constant variable.上述代码中,会先输出一次 0,然后代码就会报错。这是由于for循环的执行顺序造成的,i 定义为 0,然后执行 i < 3比较,符合条件执行循环主体,输出一次 0, 然后执行 i++,由于 i 使用const定义的只读变量,代码执行报错。说完了普通的for循环,我们还有for…in循环呢~那下面的结果是什么呢?const object = {a: 1, b: 1, c: 1};for (const key in object) { console.log(key)}// a// b// c上述代码中,虽然使用 const 定义 key 值,但是代码中并没有尝试修改 key 值,代码正常执行,这也是普通for循环和for…in循环的区别。Babel编译在 Babel 中是如何编译 let 和 const 的呢?我们来看看编译后的代码:let value = 1;编译为:var value = 1;我们可以看到 Babel 直接将 let 编译成了 var,如果是这样的话,那么我们来写个例子:if (false) { let value = 1;}console.log(value); // Uncaught ReferenceError: value is not defined如果还是直接编译成 var,打印的结果肯定是 undefined,然而 Babel 很聪明,它编译成了:if (false) { var _value = 1;}console.log(value);我们再写个直观的例子:let value = 1;{ let value = 2;}value = 3;var value = 1;{ var _value = 2;}value = 3;本质是一样的,就是改变量名,使内外层的变量名称不一样。那像 const 的修改值时报错,以及重复声明报错怎么实现的呢?其实就是在编译的时候直接给你报错……那循环中的 let 声明呢?var funcs = [];for (let i = 0; i < 10; i++) { funcs[i] = function () { console.log(i); };}funcs0; // 0Babel 巧妙的编译成了:var funcs = [];var _loop = function _loop(i) { funcs[i] = function () { console.log(i); };};for (var i = 0; i < 10; i++) { _loop(i);}funcs0; // 0项目实践在我们实际项目开发过程中,应该默认使用 let 定义可变的变量,使用 const 定义不可变的变量,而不是都使用 var 来定义变量。同时,变量定义位置也有一定区别,使用var 定义变量都会在全局顶部或者函数顶部定义,防止变量提升造成的问题,对于使用 let 和 const 定义遵循就近原则,即变量定义在使用的最近的块级作用域中。ES6系列文章ES6系列文章地址:https://github.com/changming-… ...

April 14, 2019 · 3 min · jiezi

es6 String 字符串新特性

1,字符串可以被for of 遍历2,es5判断一个字符串是否在另一个字符串里,通过indexOf()。es6 includes():返回布尔值,表示是否找到了参数字符串。3,模板字符串 ``

April 11, 2019 · 1 min · jiezi

????JavaScript设计模式实践:18份笔记、例子和源码????

背景介绍之前在阅读《JavaScript设计模式和开发实践》这本书的时候,收货颇丰,学习了设计模式在很多场景下的应用。但也是因为书上场景过多,所以当记不清某一种设计模式的时候,翻书温习复杂案例的成本是相对较高的。有时候,只需要一段经典、简洁的demo就可以迅速回顾起精髓,在快速业务开发中,这是个比较经济的做法。除此之外,当主要工作语言发生变化的时候(例如:js -> python),简洁的demo更能帮助开发者快速回忆某种设计模式的精髓和实现思路,方便开发者根据语言特性再做实现。因此,对于比较重要的18种设计模式,我都挑选了它的一种经典应用,并且尽量使用ES6的语法和编程习惯来进行实现。 前10个设计模式还提供了Python3的实现版本(后来比较忙,遂放弃)。文章地址一共记录了18个设计模式,部分文章发到了掘金,由于精力有限,后面几篇文章就直接放在了Github仓库 / 个人博客单例模式:https://godbmw.com/passages/2018-10-23-singleton-pattern/策略模式: https://godbmw.com/passages/2018-10-25-stragegy-pattern/代理模式: https://godbmw.com/passages/2018-11-01-proxy-pattern/迭代器模式: https://godbmw.com/passages/2018-11-06-iter-pattern/订阅-发布模式: https://godbmw.com/passages/2018-11-18-publish-subscribe-pattern/命令模式: https://godbmw.com/passages/2018-11-25-command-pattern/组合模式: https://godbmw.com/passages/2018-12-12-composite-pattern/享元模式:https://godbmw.com/passages/2018-12-16-flyweight-pattern/责任链模式: https://godbmw.com/passages/2019-01-07-chain-of-responsibility-pattern/装饰者模式: https://godbmw.com/passages/2019-01-12-decorator-pattern/状态模式: https://godbmw.com/passages/2019-01-16-state-pattern/适配器模式: https://godbmw.com/passages/2019-01-17-adapter-pattern/桥接模式: https://godbmw.com/passages/2019-01-19-bridge-pattern/解释器模式: https://godbmw.com/passages/2019-01-25-interpreter-pattern/备忘录模式: https://godbmw.com/passages/2019-01-26-memento-pattern/模板模式: https://godbmw.com/passages/2019-01-31-template-pattern/工厂模式: https://godbmw.com/passages/2019-03-31-factory-pattern/抽象工厂模式: https://godbmw.com/passages/2019-04-01-abstract-factory-pattern/放在最后其实整理这些的原因还有一个,就是为了准备今年春招的面试。然后过了腾讯的校招和阿里的前三面发现,竟然没有专门问到设计模式相关知识!但回首看,系统地学习、理智地使用设计模式(不是为了用而用),确实能提升代码的可读性,实现业务解耦。而在写这些文章的过程中,每种设计模式自己也是会找很多的实现(包括不限于python、java、c++)来参考,探索式学习还是蛮有趣的。尽管如此,有2篇文章的瑕疵还是很多,希望您抱着交流的心态来阅读,如有不当,欢迎指出、共同提升。

April 11, 2019 · 1 min · jiezi

ES6入门之变量的解构赋值

数组的解构赋值基本用法ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为结构。在ES6之前想要为变量赋值,只能指定其值,如下:let a = 1;let b = 2而在ES6中可以写成这样,如下:let [a,b] = [1,2]// a = 1, b = 2值得注意的是,等式两边的值要对等,这样左边的变量才会被赋上右边对应的值,如果不对等左边的值将会出现undefined,如下写法:let [foo,[[bar],baz]] = [1,[[2],3]]foo // 1bar // 2baz // 3注意:只有左右两边的 格式一定要对等,数量可以不对等。let [a,b,c] = [1,2]a = 1, b = 2, c = undefinedlet [a,,c] = [1,2,3]a = 1, c = 3let [a, …b] = [1,2,3]a = 1, b = [2,3]let [a] = []let [b,a] = [1]a = undefined还有一种情况,等号左边为数组,但是等号右边为其他值,将会报错。如下:let [a] = 1;let [a] = false;let [a] = NaN;let [a] = undefined;let [a] = null;let [a] = {};以上都会报错但是如果左边为数组,右边为字符串,将会取字符串的第一个下标的值let [a] = ‘121321’ a = ‘1’let [a] = ‘adgasg’ a = ‘a’对于Set结构,同样可以使用数组的解构赋值。let [x,y,z] = new Set([1,2,3])x = 1, y = 2, z = 3默认值解构赋值允许指定默认值let [a = 3] = [] // a:3let [a = 3,b] = [,4] // a:3 b:4let [a = 3,b] = [5,4] // a:5 b:4特殊let [a = 3] = [undefined] // a:3let [a = 3] = [null] // a:nullTips: 在es6中使用严格相等运算符,在结构赋值中如果需要默认值生效,则应对等的值为undefined的时候才会采用默认值,否则还是使用赋值。上面中null 不严格等于undefined++如果默认值的赋值为一个表达式,只有当等号右边的赋值没有值为undefined的时候,才会取表达式中的值,如下:function demo(){ console.log(‘demo’)}let [a = demo()] = [] // a: demolet [a = demo()] = [1] // a : 1对象的解构赋值与数组的不同点是,数组的元素必须和赋值的元素要位置一致才能正确的赋值,而对象的解构赋值则是等号两边的变量和属性同名即可取到正确的值。否则值为 undefinedlet {a,b} = {a:‘23’,b:‘3’}let {a,b} = {b:‘3’,a:‘23’}// 上面两个的值 都是 a: 23 b: 3let {a,b} = {a:‘3’,c:’d’}//a: 3 b: undefined对象的解构赋值还有将某一个现有对象的方法赋值到一个新的变量,如下:let {sin,cos} = Math// Math 中的sin cos 方法将赋值给变量 sin coslet {log} = console// log(2) === console.log(2)如果等号左边的变量名不能和等号右边的对象的属性名一致,则必须写成如下格式:let {a:b} = {a:‘ss’} // b:ss//a是属性名,b才是实际赋值的变量名对象的解构赋值一样是可以嵌套解构的,如下:第一种:let obj = { p:[ ‘Hello’, {y:‘world’} ]}let {p:[x,{y}]} = obj // x: Hello, y: world这边的p只是属性不是变量,如果p想赋值可以写成:let {p,:[x,{y}]} = obj第二种:const a = { loc: { t :1, b :{ c:1, d:2 } }}let {loc:{t,b:{c,d}}} = a或者let {loc,loc:{t,b,b:{c,d}}} = a嵌套赋值let o = {}, arr = []({foo:o.prop,bar: arr[0]} = {foo:123,bar:true})//o: 123, arr = [true]如果解构模式 是嵌套的对象,如果子对象所在的父属性不存在,则会报错,如下:let {foo:{bar}} = {baz:‘baz’} //报错默认值let {x = 3} = {}// x: 3let {x,y = 5} = {x : 1}// x: 1, y: 5let {x: y = 5} = {}// y = 5let {x: y = 5} = {x : 4}// y = 4let {x: y = ‘hhhh’} = {}//y = ‘hhhh’Tips:以上左边 x为属性名,y为变量let {x = 5} = {x: undefined}// x: 5let {x = 4} = {x: null}// x: null同数组一样遵循 严格等于 只有右边为undefined的时候默认值才会生效注意点:1)不能将已声明的变量用于解构赋值,因为已经是一个代码块。字符串的解构赋值如果赋值的对象是数组,字符串将被分割为数组的格式,一一对应赋值let [a,b] = ‘ha’// a = h , b = a同时可以获得字符串的长度:let {length:len} = ‘12121’// len = 5数值和布尔值的解构赋值如果等号右边是数字或者布尔值 则转换成对象或者说,除了是数组和对象,其他值都将转换成对象,null 和 undefined 除外。如下:let {t:s} = 123let {t: s} = true函数参数的解构赋值function add([x,y]){ return x+y}add([3,5]) // 8[[3,5],[6,7]].map(([a,b]) => a + b)//8 , 13function m({x=3, y=4} = {}){ return [x,y]}m({x:33,y:8}) // [33,8]m({x:32}) // [32,4]m({}) // [3,4]m() // [3,4]function m({x,y} = {x=0,y=0}){ return [x,y]}m({x:33,y:8}) // [33,8]// 代替右边的 x:0, y:0 所以是传值 33 8m({x:32}) // [32,undefined]//因为传值替代右边的赋值,但是只有x没有y//所以y是取 左边y的默认值,因为你没有赋值 为undefinedm({}) // [undefined,undefined] // 取左边x,y的默认值,因为没有赋值 为undefinedm() // [0,0]// 没有传值,使用本身的赋值 都是0其他不能使用圆括号的情况变量声明语句函数参数赋值语句的模式可以使用圆括号的情况赋值语句的非模式部分,可以使用圆括号解构赋值的用途交换变量的值从函数返回多个值函数参数的定义提取JOSN数据函数参数的默认值遍历Map解构输入模块的指定方法欢迎关注 公众号【小夭同学】ES6入门系列ES6入门之let、cont ...

April 10, 2019 · 2 min · jiezi

ES6+ 常用语法整理

箭头函数// 5function greet (name) { return ‘Hello, ‘+ name;}// 6const greet = (name) => { return ‘Hello, ‘+ name;}// 由于函数内容只有一句,可以去掉{},并且省略return关键字const greet = (name) => console.log(‘Hello, ‘+ name);// 进一步简化,由于参数也只有一个,参数的括号也可以省略。参数多于一个时必须用括号包裹const greet = name => console.log(‘Hello, ‘+ name);所以当看到一句的函数如果不理解,可以一步一步的反推,还原成你熟悉的代码// 回调 5const names = [‘Tom’,‘Jerry’,‘Dog’];names.map(function(name){ console.log(name);});// 6names.map(name=> console.log(name));// 当需要直接返回一个对象怎么办?直接写{}发现是错的。这是需要把要返回的对象用()包裹。这个用法比较生僻,不好找。const test = (a,b) => ({a,b});test(1,2); // {a:1, b:2}扩展运算符// …的一个通用的用法就是把对象展开// 展开数组console.log(…[1,2,3]) // 1 2 3// 展开对象const people = {name:‘Tome’, age: 18};const coder = {…people, scope:‘javascript’};console.log(coder); // {name: “Tome”, age: 18, scope: “javascript”}// 展开字符串console.log(…‘hello’); // h e l l o变量声明// 6中不建议继续使用var来声明变量,推荐使用let和const声明,以此避免var声明存在的弊端,看下例子// 5, 不卖关子,输出10个10for (var i = 0; i < 10; i++) { setTimeout(() => console.log(i));}// 6, 输出0 … 9for (let i = 0; i < 10; i++) { setTimeout(() => console.log(i));}// 差别一目了然,如果你不理解,简单来讲,let能避免var的一些不必要的麻烦// const用来声明常量,也就是声明了之后不能再被赋值。底层的优点不说了,用const能避免误操作,如下这种误操作会报错,如果用var则不会。const MAX = 20;if(MAX = number){ // …}// 还有一些简单的声明方式,或者是语法糖// 直接使用对象的属性,简化掉冗长的点调用const student = { name: ‘Tom’, age: 18, grade: 3, score: 100 };const {name, age, grade, score} = student;console.log(name, age, grade, score); // Tom 18 3 100// 如果是多层的嵌套的对象,比如http请求中的req对象req={body:{name:’test name’},headers:{…}};const {body:{name}}=req;console.log(name); // 反向追踪到要用的属性即可获得// 一次性声明多个变量并赋值let [x,y,z] = [1,2,3];console.log(x,y,z); // 1 2 3ES6+ 对于书写来说提供了极大的方便,可能有些地方不那么好理解,多练习就好! ...

April 10, 2019 · 1 min · jiezi

普通函数和箭头函数

我常常的使用箭头函数,却还没有对箭头函数有个深入的了解,现在找一下这2个函数的不同点1. 箭头函数本身没有prototype(原型)由于箭头函数没有原型,因此箭头函数本身没有thislet a = () => {}console.log(a.prototype) // undefinedlet b = function () {}console.log(b.prototype) // Object2. 箭头函数的this指向在定义的时候继承自外层第一个普通函数的thislet a;let barObj = { msg: ‘bar的this指向’}let fooObj = { msg: ‘foo的this指向’}bar.call(barObj)foo.call(fooObj) // { msg: ‘bar的this指向’ }bar.call(fooObj)a() // { msg: ‘foo的this指向’ }function foo() { a()}function bar () { a = () => { console.log(this) }}从上面例子中可以得出2点:箭头函数的this指向定义时所在的外层第一个普通函数,跟使用位置没有没有关系被继承的普通函数的this指向改变,箭头函数的this也会跟着改变。不能直接修改箭头函数的this可以通过修改被继承的普通函数的this指向,然后箭头函数的this也会跟着改变3. 箭头函数使用argumentslet b = () => { console.log(arguments); } b(1,2,3,4) // arguments is not defined function bar () { console.log(arguments); // 完成第二个普通函数 bb(‘完成第一个普通函数’) function bb() { console.log(arguments); // 完成第一个普通函数 let a = () => { console.log(arguments); // 完成第一个普通函数 } a(‘箭头函数’) } } bar(‘完成第二个普通函数’)从上面可以得出以下2点箭头函数指向window时,arguments会报未定义的错误如果不是window,那么就是外层第一个普通函数的arguments4. 箭头函数不可以使用new无论箭头函数的this指向哪里,使用new调用箭头函数都会报错,箭头函数没有构造函数let a = () => {} let b = new a() // a is not a constructor ...

April 10, 2019 · 1 min · jiezi

es6学习之let和const命令

let和const命令let命令是在它所在的代码块有效,它属于块级作用域,es6新增。es5只有全局作用域和函数作用域。let命令存在暂时性死区(TDZ),即在申明前使用就会报错,不存在变量提升console.log(a); // 报错let a = 111;==let不允许在相同作用域中,重复申明同一变量==块级作用域和函数声明es6中明确规定在块级作用域中可以声明函数,在块级作用域中,函数声明语句的行为类似于let,在块级作用域外不可引用。以下是es6中对函数声明做的规定允许在块级作用域内声明函数。函数声明类似于var,即会提升到全局作用域或函数作用域的头部。同时,函数声明还会提升到所在的块级作用域的头部。看下面一段代码// ES6 环境function f() { console.log(‘I am outside!’); }(function () { if (false) { function f() { console.log(‘I am inside!’); } } f();}());// Uncaught TypeError: f is not a function会报错,应该块级作用域中的函数声明类似于var,会被提升到块级作用域头部,下面是实际运行代码//ES6 环境function f() { console.log(‘I am outside!’); }(function () { var f = undefined; if (false) { function f() { console.log(‘I am inside!’); } } f();}());// Uncaught TypeError: f is not a function所以要避免在块级作用域中声明函数,如果必须的话使用函数表达式const命令const 声明一个只读的常量,声明后不可改变,需要注意的是const只是保证声明的变量指向的那块内存空间保存的数据不能改动,对于复合型数据(对象和数组),变量指向的内存地址,保存的是一个指向实际数据的指针,const 只能保证这个指针是固定的,指针指向的数据结构是可以变的,因此对象可以添加属性,数组可以添加数据,但是他们不能赋给另一个变量const foo = {};// 为 foo 添加一个属性,可以成功foo.prop = 123;foo.prop // 123// 将 foo 指向另一个对象,就会报错foo = {}; // TypeError: “foo” is read-onlyes6中有六中声明变量的方法:var 命令和function 命令,let 和const ,还有import 命令和class命令es5中顶层对象和全局变量是挂钩的,全局声明的变量是顶层对象的属性,es6中let,const,class命令声明的全局变量不在是全局变量的属性 ...

April 9, 2019 · 1 min · jiezi

知识点小记

这是一些小问题的记录和总结:1. vue serve和build在vue-cli3.0中可以快速的开发原型。通过全局安全@vue/cli-service-globalnpm i -g @vue/cli-service-global那么就可以使用vue serve xx.vue起服务和vue build xx.vue打包。当然打包的话还有更多的配置项:命名,打包模式等。2. es6箭头函数我们来看一下箭头函数的效果:var num = 10var getNum = function() { return this.num; }var getNum2 = () => {return this.num;}var c = { num: 11}console.log(getNum.bind(c)()) // 11console.log(getNum2.bind(c)()) // 10箭头函数是无法通过bind、apply、call来修改作用域的这个需要切记。因为我们习惯使用箭头后,会忘记为什么使用。在有些场景,我们自定义的函数,可能会因为习惯而使用箭头函数;而使得作用域错误。所以切记在需要的时候使用箭头函数。如果我们在vue的生命周期使用箭头函数会怎么样?<template> <div id=“app”> </div></template><script>export default { name: ‘app’, data () { return { } }, created: () => { console.log(this) // undefined }, created () { console.log(this) // vue实例 }, methods: { }}</script>很明显,如果使用箭头函数的话,那么this也无法被修改,那么就会undefined。3. 控件库中多语言是如何实现的以前我开发控件库的时候,是把设计中文相关的内容都通过外部传入,那么这样就不会设计多语言了。但是如果是一些固定的词汇,那么每次通过外面传入是会比较繁琐的。这边hui或hui-pro是通过自己定义一个工具库。这个工具库的内容就是先判断this存在不存在,存在的话检测this对象中是否有对语言对象i18n,如果存在那么就使用i18n的语言进行转换。如果不存在。那么直接使用本地控件库的键值对的形式读取。

April 8, 2019 · 1 min · jiezi

es6学习之map和set

Map和SetMap 定义:一组键值对的集合它是一组键值对的数据结构,由于之前javascript的键(key)必须是字符串。为了使键可以是多种类型的于是在最新的es6中引入了map这种数据结构,这样可以加快查询速度,类似于查字典方法和属性var m = map() //空mapm.set(‘学生年龄’,6 ) //添加一对新的key-valuem.has(‘学生年龄’) //是否存在key学生年龄 truem.get(‘学生年龄’) //拿到值 6m.delete(‘学生年龄’) // 删除key学生年龄m.clear() //清除所有成员 没有返回值m.size() // 返回map结构的成员总数 1需要注意的是如果多次对相同key值插入value,前一次的value值会被覆盖map中的键实际是对内存地址的引用,举例说明var k1 = 222;var k2 = 222;map.set(k1, 123);.set(k2, 222);map.get(k1); // 123 map.get(k2); // 222因此如果键值是对象的话就算值相同也是两个不同的键,如果是简单类型的话(number,boolean,string),只要值相等,就是同一个键,0和-0也被map视为同一键,NaN也是同一键遍历map的方法keys(): 返回键名的遍历器values(): 返回键值的遍历器entries():返回所有成员的遍历器forEach(): 遍历所有map成员map遍历的顺序就是插入的顺序使用实例var m = new map( [‘F’, ’no’], [’t’, ‘yes’]);for(let key of map.keys()) { console.log(key);}// f,tfor(let key of map.values()) { console.log(key);}// no, yesfor(let key of map) { console.log(key[0], key[1]);}// f no// t yesmap.forEach( res => { console.log(res[0], res[1]);});// f no// t yesforEach方法还可以接受第二个参数,用来绑定this。类型转换[ ] map如何转换为数组运用扩展运算符可以很方便的转换var m = map;m.set(‘a’, 1);m.set(‘b’, 2);[…map];// [[‘a’,1], [‘b’,2]];[ ] 数组转换为mapnew Map([[true, 7], [{foo: 3}, [‘abc’]]])// Map {true => 7, Object {foo: 3} => [‘abc’]}[ ] map转化为对象如果键都是字符串function mapToObj(strMap) { let obj = Object.create(null); for(let [k,v] of strMap) { obj[k] = v } return obj;}let map = new Map().set(‘yes’,111);mapToObj(map);// {yes: 111};[ ] 对象转为mapfunction objToMap(obj) { let map = new Map(); for(let k of obj.keys()) { map.set(k.obj[k]); } return map;}objToStrMap({yes: true, no: false});// [ [ ‘yes’, true ], [ ’no’, false ] ][ ] map转化为json如果map键名都是字符串的话,可以转化为对象jsonfunction mapToJson(strMap) { return JSON.stringify(mapToObj(strMap)); // map转化为对象在转化为json }let map = new Map().set(‘yes’, 111);strMapToJson(myMap)// ‘{“yes”:true,“no”:false}‘如果键名时非字符串的话,转化为数组jsonfunction mapToJson1(strMap) { return JSON.stringify([…strMap]); //map转化为数组在转化为数组json}let myMap = new Map().set(true, 7).set({foo: 3}, [‘abc’]);mapToArrayJson(myMap)// ‘[[true,7],[{“foo”:3},[“abc”]]]‘json转化为map同理只是return的是JSON.parse()方法 也分为键名是字符串和非字符串Set特点类型于数组。但是它的成员值都是唯一的,可以用来做数组去重构造函数var a = new Set([1,2,2,3,4,555,5,5,6]);[…a];// [1,2,3,4,555,6];这是一个数组去重的例子在set内部如果有两个NAN会去掉一个方法add(value): 添加值 返回set结构本身delete(value): 删除某个值 返回布尔值 表示是否删除成功has(value): 返回布尔值 表示该值是否为set的成员clear(): 清除所有成员数组去重的两种方法var arr = [1,2,3,4,4,5];var aSet = new Set(arr);[…aSet];//[1,2,3,4,5] var bSet = new Set(arr);arr = Array.from(bSet); // [1,2,3,4,5]由于set的keys()和values()完全一样所以无法通过set[xxx]的方式找到其内部的值,遍历set的方法keys(): 返回键名和遍历器values(): 返回键值和遍历器entires(): 返回键值对的遍历器forEach(): 使用回调函数遍历每个成员 let set = new Set([‘red’, ‘green’, ‘blue’]); for (let item of set.keys()) { console.log(item); } // red // green // blue for (let item of set.values()) { console.log(item); } // red // green // blue for (let item of set.entries()) { console.log(item); } // [“red”, “red”] // [“green”, “green”] // [“blue”, “blue”]forEach对空数组不会执行回调函数 ...

April 8, 2019 · 2 min · jiezi

js 中的Generator 函数

语法上首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。形式上Generator 函数是一个普通函数,但是有两个特征。 一是,function关键字与函数名之间有一个星号; 二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。调用上Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。我们必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行function* helloWorldGenerator() { yield ‘hello’; yield ‘world’; return ’ending’;}var hw = helloWorldGenerator();hw.next()// { value: ‘hello’, done: false }hw.next()// { value: ‘world’, done: false }hw.next()// { value: ’ending’, done: true }hw.next()// { value: undefined, done: true }调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。yield表达式yield表达式与return语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield表达式。正常函数只能返回一个值,因为只能执行一次return;Generator 函数可以返回一系列的值,因为可以有任意多个yield。从另一个角度看,也可以说 Generator 生成了一系列的值,这也就是它的名称的来历(英语中,generator 这个词是“生成器”的意思)。语法注意点:1.yield表达式只能用在 Generator 函数里面2.yield表达式如果用在另一个表达式之中,必须放在圆括号里面3.yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。例如:function* demo() { foo(yield ‘a’, yield ‘b’); // OK let input = yield; // OK}next 方法的参数yield表达式本身没有返回值(就是说let a=yield ;会返回undefined),或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值 (注意,是表达式的返回值,例如 let a=yield………. 参数会是a 的值并且会覆盖表达式之前的值)。function* f() { for(var i = 0; true; i++) { var reset = yield i; console.log(reset); if(reset) { i = -1; } }}var g = f();g.next() 由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。 ...

April 7, 2019 · 1 min · jiezi

ES6-变量的解构赋值(3)

1、解构赋值简介官方解释:按照一定的模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。举个例子,想获取数组中的前三个元素,通常会这么写:var arr =[111,222,333];var first = arr[0];var second = arr[1];var third = arr[2];如果使用解构赋值的特性,将会使等效的代码变得更加简洁并且可读性更高:let [first, second, third] = arr;本质上,这种写法属于“模式匹配”、“映射关系”。只要等号两边的模式相同,一一对应,左边的变量就会被赋予右边对应的值。这种赋值语法极度简洁,同时还比传统的属性访问方法更为条理清晰。当然,世间万物并不是完美的,例如下面的例子:let [x, y] = [‘a’];x // “a"y // undefined注意:左边数组中的 y 没有找到右边数组中对应值,解构不成功,变量 y 的值就等于undefined。我们也可以给解构的对象设置一个默认值let [x, y=‘b’] = [‘a’];x // “a"y // “b"左边数组中的 y 的有了默认值 “b”。把解构赋值说的更通俗点,有点类似于“庖丁解牛” 。庖丁心里先设想把牛(Array、Object等)分解成很多块,然后按照规划好的想法,一刀刀对应起来,就把牛分解了。2、数组的解构赋值2.1 数组解构赋值特点// ES6 之前var a=1; var b=2; var c=3;// ES6 之后let [a,b,c] = [1,2,3];这样可以用简单的方式将数组的值分别赋值到多个变量中。数组的解构赋值特点:根据数据的下标来赋值的,有次序。本质上,只要等号两边模式一致,左边变量即可获取右边对应位置的值。2.2 可以对任意深度的嵌套数组进行解构能够非常迅速的获取二维数组、三维数组,甚至多维数组中的值let [foo, [[bar], baz]] = [1, [[2], 3]];console.log(foo); // 1console.log(bar); // 2console.log(baz); // 32.3 不需要匹配的位置可以置空[,,third] = [1, 2, 3]; console.log(third); // 32.4 使用…扩展运算符,匹配余下的所以值,形成一个数组var [head, …body] = [1, 2, 3, 4]; console.log(body); // [2, 3, 4]这个’三点式’运算符我们用的比较多,比如用在数组合并上,//ES5var arr1 = [8]var arr2 = [9,11,12,13]arr1.push(arr2);//[8,[9,11,12,13]]Array.prototype.push.apply(arr1,arr2);//[8,9,11,12,13]// ES6arr1.push(…arr2);console.log(arr1)//[8,9,11,12,13]大家可以看到ES6明显写起来简洁很多。3、对象的解构赋值3.1 对象的解构赋值特点数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。let { a, b } = { a: “111”, z: “222” };a // “111"b // undefined上面的例子中,变量名与属性名不一致,可以改写成下面这样:let { a, z:b } = { a: “111”, z: “222” };a // “111"b // “222"对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。3.2 可以对任意深度的嵌套对象进行解构let itemObj = { arr: [ “aaa”, { secondLevel: “bbb” } ] }; let { arr: [firstLevel, { secondLevel }] } = itemObj; console.log(firstLevel); // “aaa” console.log(secondLevel); // “bbb"3.3 可以自定义属性名称var {name, id: ID} = { name: ‘jack’, id: 1 };ID // 1id // Uncaught ReferenceError: id is not defined但要注意的是被赋值的只是我们自定义的属性名称,匹配的模式(项)并未被赋值4、字符串解构字符串也可以解构赋值,字符串被转换成了一个类似数组的对象。模式能够匹配起来,如:const [a, b, c, d, e] = ‘hello’;a // “h"b // “e"c // “l"d // “l"e // “o"let { length:len } = ‘hello’;console.log(len); //5 (长度为5)5、数值和布尔值的解构赋值解构赋值的规则是:只要等号右边的值不是对象或数组,就先将其转为对象。如果转换之后的对象或原对象拥有Iterator接口,则可以进行解构赋值,否则会报错。// 数值和布尔值的包装对象都有toString属性let {toString: str} = 111;str === Number.prototype.toString // truelet {toString: str} = true;str === Boolean.prototype.toString // truelet { prop: x } = undefined; // TypeErrorlet { prop: y } = null; // TypeError以上的数组和布尔值会转换成对象,toString模式匹配上了对象的toString属性,所以解构成功。而null或undefined却不能转换成此类对象,所以报错。ES6中引入了Iterator迭代器,集合Set或Generator生成器函数等都部署了这个Iterator接口,所以也可以用来进行解构赋值。Set的解构赋值例子如下:var [a, b, c] = new Set([1, 2, 3]);a // 1b // 2c // 36、圆括号的用法如果在解构之前就已经定义了对象let obj;{obj}={obj:‘James’};console.log(‘James’); //报错原因:大括号{位于行首,匹配了}之后 JS引擎 就会认为 { obj } 是一个代码块,所以等号就出问题了,解决方式是在行首放个括号(,即外包裹一层括号()let obj;({obj}={obj:‘James’});console.log(‘James’); //James括号的出现,让整个解构赋值的结构被看做一个代码块,而内部的 { obj } 模式则可以正常匹配到。7、实际用途7.1 交换变量的值let x = 1;let y = 2;[x, y] = [y, x];console.log(x); //2console.log(y); //17.2 函数参数定义// 参数是一组有次序的值function foo([width,height,left,right]) { //… }foo([100, 200, 300, 300])// 参数是一组无次序的值function foo({width,height,left,right}){ // …}foo([left:300, width:100, right:300, height:200,])为了实现设计良好的 API,通常的做法是为函数为函数设计一个对象作为参数,然后将不同的实际参数作为对象属性,以避免让 API 使用者记住多个参数的使用顺序。我们可以使用解构特性来避免这种问题,当我们想要引用它的其中一个属性时,大可不必反复使用这种单一参数对象。7.3 配置对象参数jQuery.ajax = function (url, { async = true, beforeSend = noop, cache = true, complete = noop, crossDomain = false, global = true, // … 更多配置 }) { // … };如此一来,我们可以避免对配置对象的每个属性都重复var foo = config.foo || theDefaultFoo;这样的操作。7.4从函数返回多个值// 返回一个数组function foo() { return [1, 2, 3];}let [a, b, c] = foo();// 返回一个对象function foo2() { return { a: 1, b: 2 };}let { a, b } = foo2();7.5 引入模块的指定方法加载模块时,往往需要指定输入那些方法。解构赋值使得输入语句非常清晰。如果项目中只用到了element-ui中的Loading模块,可以这么写:import { Loading} from ’element-ui'8、总结8.1 解构赋值的定义解构赋值,即对某种结构进行解析,然后将解析出来的值赋值给相关的变量,常见的有数组、对象、字符串的解构赋值等。在语法上,就是赋值的作用,解构为(左边一种解构。右边一种解构,左右一一对应进入赋值)。8.2 解构赋值的分类1.左右为数组即为数组解构赋值。2.左右为对象即为对象解构赋值。3.左边是数组,右边是字符串,为字符串解构赋值。4.布尔值解构赋值为字符串的一种。5.函数参数解构赋值即为数组解构赋值在函数参数的一种应用。6.数值解构赋值为字符串解构赋值的一种。8.3 解构赋值的优势变量的解构赋值就是一种写法,掌握了这种写法可以让我们在书写 javascript 代码时可以更加的简单,迅捷。在很多独立细小的方面,解构赋值都非常有用。这些新的特性和微小的改进结合起来,它终将会影响你工作中的每一个项目。 ...

April 5, 2019 · 2 min · jiezi

Promise 对象的理解

Promise 含义Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise 对象。所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。Promise 对象有以下两个特点:对象的状态不受外界影响。有三种状态,分别为 pending(进行中)、fulfilled(已成功)和 rejected(已失败)。一旦状态改变,就不会再变,任何时候都可以得到这个结果。状态改变只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected。使用 Promise 对象的好处在于:可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。Promise 对象提供统一的接口,使得控制异步操作更加容易。Promise 缺点:无法取消 Promise,一旦新建它就会立即执行,无法中途取消。如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。基本用法ES6 规定,Promise 对象是一个构造函数,用来生成 Promise 实例。const promise = new Promise((resolve, reject) => { setTimeout(() => { const num = Math.random(); if (num > 0.5) { resolve(num); } else { reject(num); } }, 500);});promise.then( res => { console.log(“成功:” + res); }, err => { console.log(“失败:” + err); });Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。resolve 函数的作用:将 Promise 对象的状态从“未完成(pending)”变为“成功(resolved)”,在异步操作成功时调用,并将异步操作的结果作为参数传递出去。reject 函数的作用:将 Promise 对象的状态从“未完成(pending)”变为“失败(rejected)”在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。then 方法作用:接受两个回调函数作为参数。第一个回调函数是 Promise 对象的状态变为 resolved 时调用,第二个回调函数是 Promise 对象的状态变为 rejected 时调用。第二个函数可选,不一定要提供,也可以将第二个函数作为 catch 方法的参数。catch 方法作用:用于指定发生错误时的回调函数。Promise 对象异步操作抛出错误,状态就会变为 rejected,就会调用 catch 方法指定的回调函数处理这个错误。另外,then 方法指定的回调函数,如果运行中抛出错误,也会被 catch 方法捕获。const promise = new Promise((resolve, reject) => { setTimeout(() => { const num = Math.random(); if (num > 0.5) { resolve(num); } else { reject(num); } }, 500);});promise .then(res => { console.log(“成功:” + res); }) .catch(err => { console.log(“失败:” + err); });promise .then(res => { console.log(“成功:” + res); throw new Error(“test”); }) .catch(err => { // num > 0.5时打印 “失败:Error: test” console.log(“失败:” + err); });Promise 执行顺序Promise 新建后立即执行,then 方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,catch 同理。调用 resolve 或 reject 并不会终结 Promise 的参数函数的执行。const promise = new Promise((resolve, reject) => { console.log(“我是第一个执行的”); resolve();});promise.then(res => { console.log(“我是第三个执行的”);});console.log(“我是第二个执行的”);resolve 函数和 reject 函数的参数reject 函数的参数通常是 Error 对象的实例,表示抛出的错误;resolve 函数的参数除了正常的值以外,还可能是另一个 Promise 实例。如果一个 Promise(P2) 的 resolve 参数是另一个 Promise(P1),此时 P1 的状态就会传给 P2,P1 的状态决定了 P2 的状态,P1 的状态改变,P2 的回调函数才会执行。const p1 = new Promise(function(resolve, reject) { setTimeout(() => reject(new Error(“fail”)), 3000);});const p2 = new Promise(function(resolve, reject) { setTimeout(() => resolve(p1), 1000);});p2.then(result => console.log(result)).catch(error => console.log(error));// Error: fail上面代码中,p1 是一个 Promise,3 秒之后变为 rejected。p2 的状态在 1 秒之后改变,resolve 方法返回的是 p1。由于 p2 返回的是另一个 Promise,导致 p2 自己的状态无效了,由 p1 的状态决定 p2 的状态。所以,后面的 then 语句都变成针对后者(p1)。又过了 2 秒,p1 变为 rejected,导致触发 catch 方法指定的回调函数。Promise 链式调用then 方法可以返回一个新的 Promise 实例(注意,不是原来那个 Promise 实例)。因此可以采用链式写法,即 then 方法后面再调用另一个 then 方法。const promise = new Promise((resolve, reject) => { resolve(“promise”);}) .then(res => { console.log(res); // promise return “promise1”; }) .then(res => { console.log(res); // promise1 return “promise2”; }) .then(res => { console.log(res); // promise2 });注意:只要一个 Promise 中抛出错误,将执行 catch 方法,then 链终止。const promise = new Promise((resolve, reject) => { resolve(“promise”);}) .then(res => { console.log(res); // promise throw new Error(“中止”); return “promise1”; }) .then(res => { console.log(res); return “promise2”; }) .then(res => { console.log(res); }) .catch(err => { console.log(err); // Error: 中止 });主动终止 then 链,通过 catch 方法来中止 promise chainconst promise = new Promise((resolve, reject) => { resolve(“promise”);}) .then(res => { console.log(res); // promise return Promise.reject({ notRealPromiseException: true }); }) .then(res => { console.log(res); return “promise2”; }) .then(res => { console.log(res); }) .catch(err => { if (err.notRealPromiseException) { return true; } console.log(err); });Promise.prototype.finally()finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。finally 本质上是 then 方法的特例,不接受任何参数,不依赖于 Promise 的执行结果promise.finally(() => { // 语句});// 等同于promise.then( result => { // 语句 return result; }, error => { // 语句 throw error; });Promise.all()Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。const promise = Promise.all([promise1, promise2, promise3])Promise.all 方法接受一个数组作为参数,promise、pro 米色、promise3 都是 Promise 实例,如果不是,就会先调用下面讲到的 Promise.resolve 方法,将参数转为 Promise 实例,再进一步处理。(Promise.all 方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。了解 Iterator 接口)promise 的状态由 promise1、promise2、promise3 决定,分成两种情况。只有 promise1、promise2、promise3 的状态都变成 fulfilled,p 的状态才会变成 fulfilled,此时 promise1、promise2、promise3 的返回值组成一个数组,传递给 p 的回调函数。只要 promise1、promise2、promise3 之中有一个被 rejected,promise 的状态就变成 rejected,此时第一个被 reject 的实例的返回值,会传递给 promise 的回调函数。const p1 = new Promise((resolve, reject) => { resolve(“hello”);}) .then(result => result) .catch(e => e);const p2 = new Promise((resolve, reject) => { throw new Error(“报错了”);}) .then(result => result) .catch(e => e);Promise.all([p1, p2]) .then(result => console.log(result)) .catch(e => console.log(e));// [“hello”, Error: 报错了]上面代码中,p1 会 resolved,p2 首先会 rejected,但是 p2 有自己的 catch 方法,该方法返回的是一个新的 Promise 实例,p2 指向的实际上是这个实例。该实例执行完 catch 方法后,也会变成 resolved,导致 Promise.all()方法参数里面的两个实例都会 resolved,因此会调用 then 方法指定的回调函数,而不会调用 catch 方法指定的回调函数。如果 p2 没有自己的 catch 方法,就会调用 Promise.all()的 catch 方法。Promise.race()Promise.race 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。const p = Promise.race([p1, p2, p3])只要 p1、p2、p3 之中有一个实例率先改变状态,p 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函数。Promise.race 方法的参数与 Promise.all 方法一样,如果不是 Promise 实例,就会先调用下面讲到的 Promise.resolve 方法,将参数转为 Promise 实例,再进一步处理。Promise.resolve()将现有对象转化为 Promise 对象。const promise = Promise.resolve(‘Hello world’)参数是 Promise 实例,该方法不做任何改变。参数是一个 thenable 对象,先将对象转为 Promise 对象,然后立即执行 thenable 方法。相当于将 thenable 对象中的 then 方法处理的值作为参数传给 promise then 方法。let thenObj = { then(resolve, reject) { resolve(“Hello”); }};const promise = Promise.resolve(thenObj);promise.then(res => { console.log(res); // Hello});参数不是具有 then 方法的对象,或根本就不是对象,则 Promise.resolve 方法返回一个新的 Promise 对象,状态为 resolved。Promise.reject()Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为 rejected。 ...

April 4, 2019 · 3 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

前端异步解决方案-1(callback,事件监听);

今天在学习koa2的时候发现自己对async的使用方法和原理都不是很熟悉,连带着发现自己对前端异步的解决方案并了解并不如何深刻,所以干脆把前端现有的异步解决方案都复习了一遍。今天先把将用callback和事件监听思想解决异步问题的代码贴出来,明天再补充代码的解析及其他解决方案;CallBackfunction f1(f2) { setTimeout(function () { let b = 10; f2(b) }, 1000)}function f2(data) { console.log(“callback_type_demo”, data)}f1(f2);事件监听//实现事件监听let DOM = function () { //被监听的事件的集合 this.eventList = {}; //绑定事件监听的方法 this.on = function (eventName, fun) { if (typeof eventName !== “string”) { console.error(“监听的事件名应为字符串”); return; } if (typeof fun !== “function”) { console.error(“被触发的必须是事件”); return; } this.eventList[eventName] = this.eventList[eventName] || []; this.eventList[eventName].push(fun); }; //移除事件监听的方法 this.removeOn = function (eventName, fun) { let onList = this.eventList[eventName]; if (onList) { for (let i = 0; i < onList.length; i++) { if (onList[i] === fun) { onList.splice(i, 1); return } } } }; //触发事件监听的方法 this.trigger = function (eventName, param) { let onList = this.eventList[eventName]; if (onList) { for (let i = 0; i < onList.length; i++) { onListi } } };};let dom1 = new DOM();let dom2 = new DOM();let f2 = function () { setTimeout(function () { dom1.trigger(‘done’, 20) }, 100)};let f3 = function (data) { console.log(data)};dom1.on(“done”, f3);f2();setTimeout(function () { console.log(“removeOn”); dom1.removeOn(“done”, f3); f2();}, 200);dom1.trigger(“done”, “123”);dom2.trigger(“done”, “123”);由于时间关系今天不多做解析,明天再来解析我的代码和运行结果 ...

April 4, 2019 · 1 min · jiezi

《前端面试手记》之JavaScript基础知识梳理(下)

???? 内容速览 ????实现ES5继承的4种方法原型和原型链作用域和作用域链Event Loop执行上下文闭包的理解和分析????查看全部教程 / 阅读原文????ES5继承题目:ES5中常用继承方法。方法一:绑定构造函数缺点:不能继承父类原型方法/属性function Animal(){ this.species = ‘动物’}function Cat(){ // 执行父类的构造方法, 上下文为实例对象 Animal.apply(this, arguments)}/** * 测试代码 /var cat = new Cat()console.log(cat.species) // output: 动物方法二:原型链继承缺点:无法向父类构造函数中传递参数;子类原型链上定义的方法有先后顺序问题。注意:js中交换原型链,均需要修复prototype.constructor指向问题。function Animal(species){ this.species = species}Animal.prototype.func = function(){ console.log(‘Animal’)}function Cat(){}/* * func方法是无效的, 因为后面原型链被重新指向了Animal实例 /Cat.prototype.func = function() { console.log(‘Cat’)}Cat.prototype = new Animal()Cat.prototype.constructor = Cat // 修复: 将Cat.prototype.constructor重新指向本身/* * 测试代码 /var cat = new Cat()cat.func() // output: Animalconsole.log(cat.species) // undefined方法3:组合继承结合绑定构造函数和原型链继承2种方式,缺点是:调用了2次父类的构造函数。function Animal(species){ this.species = species}Animal.prototype.func = function(){ console.log(‘Animal’)}function Cat(){ Animal.apply(this, arguments)}Cat.prototype = new Animal()Cat.prototype.constructor = Cat /* * 测试代码 /var cat = new Cat(‘cat’)cat.func() // output: Animalconsole.log(cat.species) // output: cat方法4:寄生组合继承改进了组合继承的缺点,只需要调用1次父类的构造函数。它是引用类型最理想的继承范式。(引自:《JavaScript高级程序设计》)/* * 寄生组合继承的核心代码 * @param {Function} sub 子类 * @param {Function} parent 父类 /function inheritPrototype(sub, parent) { // 拿到父类的原型 var prototype = Object(parent.prototype) // 改变constructor指向 prototype.constructor = sub // 父类原型赋给子类 sub.prototype = prototype}function Animal(species){ this.species = species}Animal.prototype.func = function(){ console.log(‘Animal’)}function Cat(){ Animal.apply(this, arguments) // 只调用了1次构造函数}inheritPrototype(Cat, Animal)/* * 测试代码 */var cat = new Cat(‘cat’)cat.func() // output: Animalconsole.log(cat.species) // output: cat原型和原型链所有的引用类型(数组、对象、函数),都有一个__proto__属性,属性值是一个普通的对象所有的函数,都有一个prototype属性,属性值也是一个普通的对象所有的引用类型(数组、对象、函数),proto__属性值指向它的构造函数的prototype属性值注:ES6的箭头函数没有prototype属性,但是有__proto__属性。const obj = {};// 引用类型的 proto 属性值指向它的构造函数的 prototype 属性值console.log(obj.proto === Object.prototype); // output: true原型题目:如何JS中的原型?// 构造函数function Foo(name, age) { this.name = name}Foo.prototype.alertName = function () { alert(this.name)}// 创建示例var f = new Foo(‘zhangsan’)f.printName = function () { console.log(this.name)}// 测试f.printName()f.alertName()但是执行alertName时发生了什么?这里再记住一个重点 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的__proto(即它的构造函数的prototype)中寻找,因此f.alertName就会找到Foo.prototype.alertName。原型链题目:如何JS中的原型链?以上一题为基础,如果调用f.toString()。f试图从__proto__中寻找(即Foo.prototype),还是没找到toString()方法。继续向上找,从f.proto.__proto__中寻找(即Foo.prototype.__proto__中)。因为Foo.prototype就是一个普通对象,因此Foo.prototype.proto = Object.prototype最终对应到了Object.prototype.toString这是对深度遍历的过程,寻找的依据就是一个链式结构,所以叫做“原型链”。作用域和作用域链题目:如何理解 JS 的作用域和作用域链。①作用域ES5有”全局作用域“和”函数作用域“。ES6的let和const使得JS用了”块级作用域“。为了解决ES5的全局冲突,一般都是闭包编写:(function(){ … })()。将变量封装到函数作用域。②作用域链当前作用域没有找到定义,继续向父级作用域寻找,直至全局作用域。这种层级关系,就是作用域链。Event Loop单线程题目:讲解下面代码的执行过程和结果。var a = true;setTimeout(function(){ a = false;}, 100)while(a){ console.log(‘while执行了’)}这段代码会一直执行并且输出"while…"。JS是单线程的,先跑执行栈里的同步任务,然后再跑任务队列的异步任务。执行栈和任务队列题目:说一下JS的Event Loop。简单总结如下:JS是单线程的,其上面的所有任务都是在两个地方执行:执行栈和任务队列。前者是存放同步任务;后者是异步任务有结果后,就在其中放入一个事件。当执行栈的任务都执行完了(栈空),js会读取任务队列,并将可以执行的任务从任务队列丢到执行栈中执行。这个过程是循环进行,所以称作Loop。执行上下文题目:解释下“全局执行上下文“和“函数执行上下文”。①全局执行上下文解析JS时候,创建一个 全局执行上下文 环境。把代码中即将执行的(内部函数的不算,因为你不知道函数何时执行)变量、函数声明都拿出来。未赋值的变量就是undefined。下面这段代码输出:undefined;而不是抛出Error。因为在解析JS的时候,变量a已经存入了全局执行上下文中了。console.log(a);var a = 1;②函数执行上下文和全局执行上下文差不多,但是多了this和arguments和参数。在JS中,this是关键字,它作为内置变量,其值是在执行的时候确定(不是定义的时候确定)。闭包的理解和分析题目:解释下js的闭包直接上MDN的解释:闭包是函数和声明该函数的词法环境的组合。而在JavaScript中,函数是被作为一级对象使用的,它既可以本当作值返回,还可以当作参数传递。理解了:“Js中的函数运行在它们被定义的作用域,而不是它们被执行的作用域”(摘自《JavaScript语言精粹》) 这句话即可。题目:闭包优缺点闭包封住了变量作用域,有效地防止了全局污染;但同时,它也存在内存泄漏的风险:在浏览器端可以通过强制刷新解决,对用户体验影响不大在服务端,由于node的内存限制和累积效应,可能会造成进程退出甚至服务器沓机解决方法是显式对外暴露一个接口,专门用以清理变量:function mockData() { const mem = {} return { clear: () => mem = null, // 显式暴露清理接口 get: (page) => { if(page in mem) { return mem[page] } mem[page] = Math.random() } }}更多系列教程⭐在GitHub上收藏/订阅⭐《前端知识体系》JavaScript基础知识梳理(上)JavaScript基础知识梳理(下)谈谈promise/async/await的执行顺序与V8引擎的BUG前端面试中常考的源码实现Flex上手与实战……《设计模式手册》单例模式策略模式代理模式迭代器模式订阅-发布模式桥接模式备忘录模式模板模式抽象工厂模式……《Webpack4渐进式教程》webpack4 系列教程(二): 编译 ES6webpack4 系列教程(三): 多页面解决方案–提取公共代码webpack4 系列教程(四): 单页面解决方案–代码分割和懒加载webpack4 系列教程(五): 处理 CSSwebpack4 系列教程(八): JS Tree Shakingwebpack4 系列教程(十二):处理第三方 JavaScript 库webpack4 系列教程(十五):开发模式与 webpack-dev-server……⭐在GitHub上收藏/订阅⭐ ...

April 3, 2019 · 2 min · jiezi

ES6入门之let、cont

一、前提解决ES5中只有全局作用域和函数作用域,没有块级作用域而带来的不合理的场景。let基本用法用法和var 一样,只是let声明的变量只有在let命令所在的代码块有效{ let a = 10; var b = 1;}a // ReferenceError: a is not defined.b // 1可以看出var 声明的变量在代码块之外也是可以调用,而let声明的则调用报错。所以let 声明只在它声明的当前代码块中才能调用。变量提升在使用 var 的时候会出现 “变量提升”的现象,即变量可以在声明之前使用,值为undefined。let 改变了这种现状,但是必须先声明在使用,如果在声明之前使用则会出现报错。如下:// var 的情况console.log(foo); // 输出undefinedvar foo = 2;// let 的情况console.log(bar); // 报错ReferenceErrorlet bar = 2;暂时性死区只要块级作用域内部存在 let 或者 const 命令,它所声明的变量就“绑定”在这个区域,不会受外部影响。且暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。如下:var tmp = 123;if (true) { tmp = ‘abc’; // ReferenceError let tmp;}ES6 明确规定,如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。如下:if (true) { // TDZ开始 tmp = ‘abc’; // ReferenceError console.log(tmp); // ReferenceError let tmp; // TDZ结束 console.log(tmp); // undefined tmp = 123; console.log(tmp); // 123}注意:++使用let声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量x的声明语句还没有执行完成前,就去取x的值,导致报错”x 未定义“。++不允许重复声明let 不允许在同一个作用域内声明同一个变量,如下:// 报错function func() { let a = 10; var a = 1;}// 报错function func() { let a = 10; let a = 1;}或者如下:function func(arg) { let arg;}func() // 报错function func(arg) { { let arg; }}func() // 不报错块级作用域上面也提到过在es5中没有块级作用域的概念,只有函数作用域和全局作用域,那么就带来了一些问题,如下:var tmp = new Date();function f() { console.log(tmp); if (false) { var tmp = ‘hello world’; }}f(); // undefined外层声明被内层声明所覆盖,内层使用的是外层的声明,内层变量提升导致 undefinded第二种:计数循环全局泄露,如下:var s = ‘hello’;for (var i = 0; i < s.length; i++) { console.log(s[i]);}console.log(i); // 5常见的面试题,最后输出不是预料中的 1 2 3 4 5 而全部是 5ES6的块级作用域,实际上就是let 新增的,如下:function f1() { let n = 5; if (true) { let n = 10; } console.log(n); // 5}内外层的 n 互不干扰ES6中 允许作用域任意嵌套,并且互不干扰,如下:内外层可以同名{{{{ let insane = ‘Hello World’; {let insane = ‘Hello World’}}}}};或者{{{{ {let insane = ‘Hello World’} console.log(insane); // 报错}}}};块级作用域的出现可以让以下立即执行函数的写法不必要,如下:// IIFE 写法(function () { var tmp = …; …}());// 块级作用域写法{ let tmp = …; …}块级作用域和函数声明在ES5中,函数只能在顶层作用域和函数作用域中声明,不能在块级作用域中声明,但是浏览器为了兼容性,还是可以在块级作用域中声明,理论上在ES6中 块级作用域中声明的函数,在外部调用会报错,考虑环境的问题,应当避免在块级作用域中声明函数,如果需要也应当写成函数表达式的方式,而不是函数声明语句,如下:// 函数声明语句{ let a = ‘secret’; function f() { return a; }}// 函数表达式{ let a = ‘secret’; let f = function () { return a; };}constconst声明的是一个常量 如下:const PI = 3.1415;PI // 3.1415PI = 3;// TypeError: Assignment to constant variable.声明之后如果在赋值,将会报错,同时因为声明的是常量,即const声明后即要赋值不然也会报错const 和 let 相同,声明也只在当前的块级作用域生效。同样也不会声明提升,也存在暂时死区,只能在声明之后使用,且和 let 一样不得重复声明,不能重新赋值。重要:const 所不能改变的并不是值,而是变量指向的那个内存地址所保存的值不能变动,对于简单类型(数值、字符串、布尔值),值就保存在变量所指向的内存地址中,因此等同于常量。而对于复合类型(数组、对象),变量指向的内存地址,保存的只是一个指向实际数据的指针,const 只能保证这个指针是固定的(即总指向一个固定的地址)。至于它指向的数据结构则是不能控制的 ,如下:const foo = {};// 为 foo 添加一个属性,可以成功foo.prop = 123;foo.prop // 123// 将 foo 指向另一个对象,就会报错foo = {}; // TypeError: “foo” is read-only常量foo储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。或者const a = [];a.push(‘Hello’); // 可执行a.length = 0; // 可执行a = [‘Dave’]; // 报错常量a是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a,就会报错如果真的想将声明对象冻结,不能在改变 则应该使用object.freeze()const foo = Object.freeze({});// 常规模式时,下面一行不起作用;// 严格模式时,该行会报错foo.prop = 123;对象也可以冻结var constantize = (obj) => { Object.freeze(obj); Object.keys(obj).forEach( (key, i) => { if ( typeof obj[key] === ‘object’ ) { constantize( obj[key] ); } });};ES6 声明变量的六种方法1. function2. var3. let4. const5. import 6. class顶层对象的属性在浏览器环境指的是window对象,在 Node 指的是global对象,ES5 之中,顶层对象的属性与全局变量是等价的。window.a = 1;a // 1a = 2;window.a // 2global 对象ES5 的顶层对象,本身也是一个问题,因为它在各种实现里面是不统一的。浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window。浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self。Node 里面,顶层对象是global,但其他环境都不支持。同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this变量,但是有局限性。全局环境中,this会返回顶层对象。但是,Node 模块和 ES6 模块中,this返回的是当前模块。函数里面的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined。不管是严格模式,还是普通模式,new Function(‘return this’)(),总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么eval、new Function这些方法都可能无法使用。欢迎关注 公众号【前端开发小白】 ...

April 2, 2019 · 2 min · jiezi

【webpack4】用不同语言语法编写webpack配置文件

写在前面webpack配置文件默认情况下为webpack.config.js,即用原生js语法书写的配置文件。然而,在我们的项目中,有时候是使用的是其他语言语法进行编程,例如:es6、coffeeScript、typeScript等语言语法。为此,webpack为我们提供了可以采用不同语言语法类型书写配置文件的功能。具体可以支持的文件拓展可以看这里:https://github.com/gulpjs/int…可以看到,webpack为我们提供了丰富多样可供选择的文件拓展。下面介绍一下最常见的webpack配置文件类型:TypeScript1、安装依赖如果想要使用TypeScript来书写webpack配置文件,首先要先安装依赖:npm install –save-dev typescript ts-node @types/node @types/webpack如果需要用到webpack-dev-server,还需要安装:npm install –save-dev @types/webpack-dev-server2、编写webpack配置文件(1)把webpack配置文件的文件名改为:webpack.config.tsTypeScript的文件拓展名为.ts,所以我们需要同时把webpack配置文件的文件名改为.ts拓展名(原来默认为webpack.config.js)当我们把webpack配置文件名拓展改为.ts时,webpack也会自动读取该拓展名下的文件。即不需要这样设置:>> webpack –config webpack.config.tswebpack会自动帮我们读取webpack.config.ts文件,不需要我们再去设置了(2)编写webpack.config.ts配置文件利用TypeScript编写webpack配置文件时,webpack配置文件的结构同以前一样,只不过语言变为Typescript而已。//webpack.config.tsimport path from ‘path’import webpack from ‘webpack’?const config: webpack.Configuration = { entry: ‘./src/index.js’, output: { filename: ‘bundle.js’, path: path.resolve(__dirname, ‘dist’), publicPath: path.resolve(__dirname, ‘dist’), },}export default config在本webpack配置文件webpack.config.ts中,我们把require语法改为ts中的import、export静态模块引入导出的语法,以便我们测试。3、编写TypeScript配置文件通常来说,大多情况下,我们使用某种语法、插件等,它都会有自己一份默认的配置,在比较简单的项目中,毋需我们编写配置文件。但是,在利用TypeScript书写webpack配置文件时,我们还需要额外编写TypeScript配置文件tsconfig.json:{ “compilerOptions”: { /* ts-node只支持commonjs规范 */ “module”: “commonjs”, “target”: “es5”, “esModuleInterop”: true, }}这是因为我们在前面安装的依赖ts-node不支持commonjs之外的模块语法,所以我们必须在TypeScript的配置文件tsconfig.json配置compilerOptions中module字段为:commonjs,否则,webpack会报错。ps:在安装依赖的时候,我们也需要考虑依赖的局限性。比如某些依赖依赖于其他的依赖,在项目开发的时候,我们需要把其涉及到的其他依赖一同安装。另外,依赖不是万能的,在确定安装依赖的时候,需要额外去学习该依赖相关知识。coffeeScript1、安装依赖与上面的内容相似,首先我们需要安装相关依赖:npm install –save-dev coffee-script2、编写webpack配置文件(1)把webpack配置文件的文件名改为:webpack.config.coffeeCoffeeScript的文件拓展名为.coffee,所以我们需要同时把webpack配置文件的文件名改为.coffee拓展名(原来默认为webpack.config.js)当我们把webpack配置文件名拓展改为.coffee时,webpack也会自动读取该拓展名下的文件。即不需要这样设置:webpack –config webpack.config.coffeewebpack会自动帮我们读取webpack.config.coffee文件,不需要我们再去设置(2)利用coffeeScript重新编写webpack.config.coffee文件//webpack.config.coffeewebpack = require(‘webpack’)path = require(‘path’)config = mode: ‘production’ entry: ‘./src/index.js’ output: path: path.resolve(__dirname, ‘dist’) filename: ‘bundle.js’module.exports = config用coffeeScript编写webpack配置文件时,毋需向TypeScript一样编写ts配置文件,因为coffeeScript安装的依赖没有其他的兼容性问题出现。ES6利用es6写webpack配置文件的原理同上面一样,都是把其他类型的语言语法编译成原生js。把es6编译成原生js可以使用babel进行编译(也有其他选择)。1、安装依赖npm install –save-dev babel-core babel-loader babel-preset-es2015 babel-preset-stage-3其中,要使用babel编译器,我们需要安装的依赖有babel-core。babel-core包中囊括了babel的核心方法。2、编写webpack配置文件由于es6语法写的文件名拓展也是.js,那么webpack如何识别该js文件,并把它交予babel进行编译呢?(1)webpack.config.[loader].js把webpack配置文件的文件名改为webpack.config.babel.js,其中babel字段表示需要优先使用babel-loader对该webpack配置文件进行编译。同样地,我们可以把webpack.config.[loader].js中的[loader]替换成我们需要的loader名。这也是我们需要安装babel-loader的原因。(2)编写webpack.config.babel.js为了体现es6语法,我们把webpack配置文件改写成:import path from ‘path’// example of an imported pluginconst CustomPlugin = config => ({ …config, name: ‘custom-plugin’});?export default { entry: ‘./src/index.js’, output: { filename: ‘bundle.js’, path: path.resolve(__dirname, ‘dist’) },}其中,import、export、… 语法为es6语法3、编写babel配置文件.babelrcbabel实质是一个支持众多语法编译转化的编译器,为了保证babel的可拓展性,作者把babel设计成可以灵活配置,支持安装插件的模式。因此,我们需要自行配置babel,使之支持es6编译。{ “presets”: [ “es2015”, “stage-3”],}其中,我们需要安装babel-preset-es2015的包,使得babel支持es6编译。另外,使用…config语法需要安装babel-preset-stage-3包,否则会编译错误。总之,我们可以使用各种各样的语言语法来编写webpack配置文件,它们的原理都是使用对应的编译器编译成原生的js。所以我们在编程的时候,都需要安装编译器的相关依赖,并且在必要的时候,还需要对编译器进行配置。 ...

April 2, 2019 · 1 min · jiezi

ES6的前世今生(0)

1、ECMAScript是什么? 和 JavaScript 有着怎样的关系?1996 年 11 月,Netscape 创造了javascript并将其提交给了标准化组织 ECMA,次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript,这个版本就是 1.0 版。ECMAScript更新了6个版本,最新正式版 ES6(ECMAScript 6)是 JavaScript 语言的下一代标准,早已在 2015 年 6 月正式发布。要问两者之间的关系,可以用 ECMAScript 是 JavaScript 语言的国际标准,JavaScript 是 ECMAScript 的实现这句话来形容。说的通俗易懂点:如果说设计图是标准,盖好的房子是实现,那么 ECMAScript就是设计图,JavaScript是盖好的房子。2、历史进化过程感悟:长路漫漫,吾将上下而求索!3、ES6兼容性分析3.1 横向对比(1)桌面端浏览器对ES2015的支持情况Chrome:51 版起便可以支持 97% 的 ES6 新特性。Firefox:53 版起便可以支持 97% 的 ES6 新特性。Safari:10 版起便可以支持 99% 的 ES6 新特性。IE:Edge 15可以支持 96% 的 ES6 新特性。Edge 14 可以支持 93% 的 ES6 新特性。(IE7~11 基本不支持 ES6)(2)移动端浏览器对ES2015的支持情况iOS:10.0 版起便可以支持 99% 的 ES6 新特性。Android:基本不支持 ES6 新特性(5.1 仅支持 25%)(3)服务器对ES2015的支持情况Node.js:6.5 版起便可以支持 97% 的 ES6 新特性。(6.0 支持 92%)3.2 纵向对比引用地址 https://caniuse.com/#search=es6引用地址 https://caniuse.com/#search=es5结论:现在的Chrome浏览器对ES6的支持已经做的相当棒了,但是有些低版本的浏览器还是不支持ES6的语法,例如IE8及其以下,说的就是你,不用再怀疑。4、为什么学习ES6?如果把前端开发比作成伐木头,那么ES3是斧头,ES5是钢锯,而ES6则是电锯,随着前端项目日趋复杂和移动端越来越主流,Vue、React、Angular等技术栈的大行其道,ES6 成为前端开发人员必须掌握的基本技能。掌握了ES6 不仅仅能够更加便捷的开发、大幅度的提高工作效率,更能够为学习Vue、React、Angular等技术栈甚至是NodeJS打下坚实的基础。说的这么666,那么……4.1 使用ES6编程,到底好在哪里?例一:在ES5中,我们不得不使用以下方法来表示多行字符串: var str =’<div id=“ul1”>’+ ‘<li>青年问禅师:</li>’+ ‘<li>“大师终日答疑解惑、普渡众生,如何不为俗物所扰,静心修行?”</li>’+ ‘<li>禅师微微一笑:“我每天晚上睡觉前都关机!”</li>’+ ‘</div>’;然而在ES6中,仅仅用反引号就可以解决了: var str = &lt;div id="ul1"&gt; &lt;li&gt;青年问禅师:&lt;/li&gt; &lt;li&gt;“大师终日答疑解惑、普渡众生,如何不为俗物所扰,静心修行?”&lt;/li&gt; &lt;li&gt;禅师微微一笑:“我每天晚上睡觉前都关机!”&lt;/li&gt; &lt;/div&gt;;例二:在ES5中实现对象拷贝效果:var createAssigner = function(keysFunc, undefinedOnly) { return function(obj) { var length = arguments.length; if (length < 2 || obj == null) return obj; for (var index = 1; index < length; index++) { var source = arguments[index], keys = keysFunc(source), l = keys.length; for (var i = 0; i < l; i++) { var key = keys[i]; if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key]; } } return obj; }; };var allKeys = function(obj){ var keys = []; for(var key in obj) keys.push(key); return keys;}var extend = createAssigner(allKeys);extend({a:111},{b:222});在ES6中实现对象拷贝效果:Object.assign({a:111},{b:222});同样实现一个对象拷贝效果,用ES5写需要20多行代码,但是用ES6写,只需要 1 行代码!!!当然,ES6还有很多强大的新特性等着我们去学习,ES6引入的新特性是ES5无法比拟的!4.2 ES6的新功能简介ES6过渡版本,ES4激进被废掉,ES5遗留很多问题,而ES6 兼容性还好,代码简洁,易用。(1)块级作用域绑定1 let声明2 const声明Constant Declarations3 循环中的块级绑定4 循环中的函数(2)函数的新增特性1 带默认参数的函数2 默认参数对 arguments 对象的影响3 默认参数表达式 Default Parameter Expressions4 未命名参数问题5 函数中的扩展运算符(3)全新的函数箭头函数1 箭头函数语法2 使用箭头函数实现函数自执行3 箭头函数中无this绑定No this Binding4 无arguments绑定(4)对象功能的扩展1 对象类别2 对象字面量的语法扩展2.1 简写的属性初始化2.2 简写的方法声明2.3 在字面量中动态计算属性名3 新增的方法3.1 Objectis3.2 Object assign(5)字符串功能的增强1 查找子字符串2 repeat方法3 字符串模板字面量3.1 基本语法3.2 多行字符串3.3 字符串置换3.4 模板标签3.4.1 什么是模板标签3.4.2 定义模板标签(6)解构1 解构的实用性2 对象解构2.1 对象解构的基本形式2.2 解构赋值表达式2.3 对象解构时的默认值2.4 赋值给不同的变量名3 数组解构3.1 数组解构基本语法3.2 解构表达式(7)新的基本类型Symbol1 创建Symbol2 识别Symbol3 Symbol作为属性名4 Symbol属性名的遍历5 Symbolfor字符串和SymbolkeyForsymbol类型的值(8)Set数据结构1 创建Set和并添加元素2 Set中不能添加重复元素3 使用数组初始化Set4 判断一个值是否在Set中5 移除Set中的元素6 遍历Set7 将Set转换为数组(9)Map数据结构1 创建Map对象和Map的基本的存取操作2 Map与Set类似的3个方法3 初始化Map4 Map的forEach方法(10)迭代器和forof循环1 循环问题2 什么是迭代器3 生成器函数4 生成器函数表达式5 可迭代类型和for-of迭代循环6 访问可迭代类型的默认迭代器7 自定义可迭代类型(11)类1 ES5之前的模拟的类2 ES6中基本的类声明2 匿名类表达式3 具名类表达式 4 作为一等公民的类型5 动态计算类成员的命名6 静态成员7 ES6中的继承7.1 继承的基本写法7.2 在子类中屏蔽父类的方法7.3 静态方法也可以继承使用ES6之后,可以节约很多开发时间,用来。。。5、 如何使用ES6的新特性,又能保证浏览器的兼容?针对 ES6 的兼容性问题,很多团队为此开发出了多种语法解析转换工具,把我们写的 ES6 语法转换成 ES5,相当于在 ES6 和浏览器之间做了一个翻译官。比较通用的工具方案有 babel,jsx,traceur,es6-shim 等。下一节,我们将具体讲解该部分的知识。6、总结通过本节,我们了解了ECMAScript的发展进化史,以及ES6的一些新特性。随着JavaScript应用领域越来越广, 以及ES6 优雅的编程风格和模式、强大的功能,越来越多的程序正在使用ES6更好地实现。是不是对学习ES6充满了动力?OK,下节课开始,我们就讲讲如何搭建ES6的开发环境搭建,进行ES6开发。 ...

April 1, 2019 · 2 min · jiezi

js导入导出总结与实践

在上一篇文章中JavaScript中AMD和ES6模块的导入导出对比,偏向于理论层面,还有一些同学在微信群里或是私下里针对一些问题进行了沟通,所以有了这一篇文章,对js的导入导出进行总结和实践当直接给 module.exports时,exports会失效这个问题其实已经和导入导出没什么关系了,我们看一个知乎上的问题(详细地址阅读原文可以查看)我们以此为突破点js 数组赋值问题 :值传递还是引用?var a = [1,2,3];var b = a;a = [4,5,6];console.log(b); //=>[1,2,3]继续看var a = [1,2,3];var b = a;a.pop();console.log(b); //=>[1,2]为什么会出现这种情况?数组和对象的赋值操作都是引用传递看下这个(留意注释部分)var a = [1,2,3];// a指向了数组 [1,2,3];var b = a;//b 指向 a 所指向的数组[1,2,3];a = [4,5,6];//a 指向了新的数组 [4,5,6],(a的指向发生了变化,改变的是a引用本身,没有改变数组对象,所以b没有变)console.log(b); //b没有改变,还是指向数组 [1,2,3];再看下这个(留意注释部分)var a = [1,2,3];// a指向了数组 [1,2,3];var b = a;//b 指向 a 所指向的数组[1,2,3];a.pop();// a 指向的数组实例发生了 pop 操作console.log(b); //=>a和b都是指向同一个数组,a变量,所以b也变量,最后输出=>[1,2]看一张图片,很形象的描述数组如此,对象也是大同小异看一个群友@ZSing提供的demovar test = { “name”: “zhangshuo”}var demo = test;demo.name = “want you”//你认为test是什么?console.log(test)//=>{ name: ‘want you’ }下面通过注释解释一下(如出一辙)var test = { “name”: “zhangshuo”}//test指向了一个对象 { “name”: “zhangshuo”}var demo = test;//demo 指向 test 所指向的对象 { “name”: “zhangshuo”}demo.name = “want you”//对象的属性发生了改变 { “name”: “want you”}//你认为test是什么?console.log(test)//=>{ name: ‘want you’ }test和demo指向了同一个对象,一个变了,就都变了同样的,我们对上面的demo做一下改造var test = { “name”: “zhangshuo”}var demo = test; test={ “name”: “更改了这个name” }demo.name = “want you”//你认为test是什么?console.log(test)//=>{ name: ‘更改了这个name’ }还需要对此进行赘述吗?还是通过注释对此进行解释说明var test = { “name”: “zhangshuo”}//test指向了一个对象 { “name”: “zhangshuo”}var demo = test;//demo 指向 test 所指向的对象 { “name”: “zhangshuo”} test={ “name”: “更改了这个name” }//test的指向发生了变化,指向了一个新对象{ “name”: “更改了这个name” }demo.name = “want you”//demo的指向没有变,改变了原对象的属性 { “name”: “want you”}//你认为test是什么?console.log(test)//=>{ name: ‘更改了这个name’ }我相信,上面的两个栗子你已经看懂了,即将进入正题先来一个过渡再看一个栗子,用来模拟exports和 module.exports的关联关系 let md = {exps:{}}//md指向一个对象 {exps:{}} let exps = md.exps//exps指向了md.exps所指向的对象 ,这个空对象{} md.exps = {a: 1, b: 2}//md.exps指向了一个新对象 {a: 1, b: 2} exps.c=3//exps,属性赋值 {c: 3} console.log(md.exps); //新对象{ a: 1, b: 2 }上面栗子中的md就是module,md.exps就是module.exports,exps就是exports在每一个模块的头部都有一行这样的命令var exports = module.exports;当直接给module.exports赋值时(module.exports={…..}),module.exports就指向了一个新对象,exports会失效直接给exports赋值会切断exports和 module.exports的关联关系还是这样的一个前提var exports = module.exports;exports是来自于module,exports指向 module.exports所指向的对象当直接给exports赋值,即 exports = {a:1}exports指向了一个新对象,不再是 module.exports所指向的对象,所以不要给 exports 直接赋值( exports =。。。)实践=>导出exportsexports的output.jsexports.str=‘string字符串’//导出字符串exports.bool=true//导出布尔exports.num=123//导出numberexports.foo=(r)=>{//导出函数 console.log(导出函数为:${r});}exports.arr=[1,2,3]//导出数组exports.obj={ a:1, b:2}//导出对象input.js const iptObj= require(’./output.js’) console.log(iptObj.str);//=>string字符串 console.log(iptObj.bool);//=>true console.log(iptObj.num);//=>123 console.log(iptObj.arr);//=>[ 1, 2, 3 ] console.log(iptObj.obj);//=>{ a: 1, b: 2 } iptObj.foo(‘参数’)//=>导出函数为:参数module.exportsmodule.exports的output.jsmodule.exports={ str:‘string字符串’, bool:true, num:123, foo:(r)=>{ console.log(导出函数为:${r}); }, arr:[1,2,3], obj:{ a:1, b:2}}input.js const iptObj= require(’./output.js’) console.log(iptObj.str);//=>string字符串 console.log(iptObj.bool);//=>true console.log(iptObj.num);//=>123 console.log(iptObj.arr);//=>[ 1, 2, 3 ] console.log(iptObj.obj);//=>{ a: 1, b: 2 } iptObj.foo(‘参数’)//=>导出函数为:参数module.exports的output.js同时支持如下写法module.exports.str=‘string字符串’module.exports.bool=truemodule.exports.num=123module.exports.foo=(r)=>{ console.log(导出函数为:${r});}module.exports.arr=[1,2,3]module.exports.obj={ a:1, b:2}input.js不变exportexport的output.jsexport const srt = ‘string字符串’export const bool = trueexport const num = 123export const arr = [1, 2, 3]export const obj = { a: 1, b: 2}export function foo(r) { console.log(导出函数为:${r});}input.jsimport {str,arr,obj,bool,num,foo} from ‘./output’console.log(str)console.log(arr)console.log(obj)console.log(bool)console.log(num)foo(‘参数’)export的output.js同时支持如下写法const str = ‘string字符串’ const bool = trueconst num = 123const arr = [1, 2, 3]const obj = { a: 1, b: 2}function foo(r) { console.log(导出函数为:${r});}export { str,bool,num,arr,obj,foo}input.js 导入支持重命名import {str as STR,arr,obj,bool,num,foo as FOO} from ‘./output’console.log(STR)console.log(arr)console.log(obj)console.log(bool)console.log(num)FOO(‘参数’)继续重命名import * as newName from ‘./output’console.log(newName.str)console.log(newName.arr)console.log(newName.obj)console.log(newName.bool)console.log(newName.num)newName.foo(‘参数’)export defaultexport default的output.jsexport default { str: ‘string字符串’, bool: true, num: 123, foo: (r) => { console.log(导出函数为:${r}); }, arr: [1, 2, 3], obj: { a: 1, b: 2 }}input.jsimport defaultObj from ‘./output’console.log(defaultObj.str)console.log(defaultObj.arr)console.log(defaultObj.bool)console.log(defaultObj.num)console.log(defaultObj.obj)defaultObj.foo(’ef’)//=>导出函数为:efexport default的output.js同时支持如下写法const str = ‘string字符串’const bool = trueconst num = 123const arr = [1, 2, 3]const obj = {a: 1, b: 2}function foo(r) { console.log(导出函数为:${r});}export default { str, bool, num, arr, obj, foo}input.js不变总结这篇文章是对上一篇文章的总结和实践当直接给 module.exports时,exports会失效直接给exports赋值会切断exports和 module.exports的关联关系export,export default,exports,module.exports具体的使用方法实例更多前端资源请关注微信公众号“前端陌上寒”原文链接参考链接js 数组赋值问题 :值传递还是引用? ...

March 31, 2019 · 2 min · jiezi

ES6 - Generator函数

本文已同步至我的个人主页。欢迎访问查看更多内容!如有错误或遗漏,欢迎随时指正探讨!谢谢大家的关注与支持!一、什么是Generator函数Generator函数是ES6标准中提出的一种异步编程的解决方案。这种函数与普通函数最大的区别在于它可以暂停执行,又可以从暂停的位置恢复继续执行。从语法上看,Generator函数就是一个状态机,封装了许多内部状态。从实质上看,Generator函数就是一个遍历器对象生成器。(关于遍历器对象,可以参考阮一峰老师的这篇文章)Generator函数返回一个遍历器对象,遍历这个对象,就可以依次得到函数内部的每一个状态。二、基本语法1、定义Generator函数定义一个Generator函数和定义一个普通函数的区别在于:function关键字和函数名之间有一个 (星号)。函数内部使用yield来定义每一个函数内部的状态。如果函数内部有return语句,那么他就是函数内部的最后一个状态。来看一个简单的例子:// 定义function sayHello() { yield ‘hello’; yield ‘world’; return ’ending’;}// 调用// 注意,hw获取到的值是一个遍历器对象let g = sayHello();上面的例子,定义了一个名为sayHello的Generator函数,它内部有两个yield表达式和一个return表达式。所以,该函数内部有三个状态:hello,world 和 return语句(结束执行)。最后,调用这个函数,得到一个遍历器对象并赋值给变量g。Generator函数的调用方法与普通函数完全一样,函数名()。不同的是:函数调用后,内部代码(从第一行开始)都不会立即执行。函数调用后会有一个返回值,这个值是一个指向内部状态的指针对象,实质就是一个包含函数内部状态的遍历器对象。Generator函数调用后不会立即执行,那么,我们如何让它开始执行内部的代码呢?又如何获取它内部的每一个状态呢?此时,我们必须调用返回的生成器对象的.next()方法,才能开始代码的执行,并且使得指针移向下一个状态。以上面的例子为例:g.next();// { value: ‘hello’, done: false }g.next();// { value: ‘world’, done: false }g.next();// { value: ’ending’, done: true }g.next();// { value: undefined, done: true }上面的代码中,一共调用了四次g这个遍历器对象的.next()方法。第一次调用,sayHello这个Generator函数开始执行,直到遇到第一个yield表达式就会暂停执行。.next()方法会返回一个对象,它的value属性就是当前yield表达式的值hello,done属性的值false,表示遍历还没有结束。第二次再调用.next(),就会执行到第二个yield表达式处,并暂停执行,返回对应的对象。第三次调用.next(),函数执行到最后的return语句,此时标志着遍历器对象g遍历结束,所以返回的对象中value属性值就是return后面所跟的值ending,done属性值为true,表示遍历已经结束。第四次以及后面在调用.next()方法,返回的都会是{value: undefined, done: true }。2、yield表达式由Generator函数返回的遍历器对象,只有调用.next()方法才会遍历到下一个内部状态,所以这其实是提供了一种可以暂停执行的函数,yield表达式就是暂停标志。遍历器对象的.next()方法的运行逻辑如下。遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。下一次调用.next()方法时,再继续往下执行,直到遇到下一个yield表达式。如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。如果该函数没有return语句,则返回的对象的value属性值为undefined。值得注意的是:yield关键字只能出现在Generator函数中,出现在别的函数中会报错。 // 出现在普通函数中,报错 (function () { yield ‘hello’; })() // forEach不是Generator函数,报错 [1, 2, 3, 4, 5].forEach(val => { yield val });yield关键字后面跟的表达式,是惰性求值的。 只有当调用.next()方法、内部状态暂停到当前yield时,才会计算其后面跟的表达式的值。这等于为JavaScript提供了手动的“惰性求值”的语法功能。function* step() { yield ‘step1’; // 下面的yield后面的表达式不会立即求值, // 只有暂停到这一行时,才会计算表达式的值。 yield ‘step’ + 2; yield ‘setp3’; return ’end’;}yield表达式本身是没有返回值的,或者说它的返回值为undefined。使用.next()传参可以为其设置返回值。(后面会讲到)function* gen() { for (let i = 0; i < 5; i++) { let res = yield; // yield表达式本身没有返回值 console.log(res); // undefined }}let g = gen();g.next(); // {value: 0, done: false}g.next(); // {value: 1, done: false}g.next(); // {value: 2, done: false}yield与return的异同:相同点:两者都能返回跟在其后面的表达式的值。不同点:yield表达式只是暂停函数向后执行,return是直接结束函数执行。yield表达式可以出现多次,后面还可以有代码。return只能出现一次,后面的代码不会执行,在一些情况下还会报错。正常函数只能返回一个值,因为只能执行一次return。Generator函数可以返回一系列的值,因为可以有任意多个yield。3、.next()方法传参前面我们说到过,yield表达式自身没有返回值,或者说返回值永远是undefined。但是,我们可以通过给.next()方法传入一个参数,来设置上一个(是上一个)yield表达式返回值。来看一个例子:function* conoleNum() { console.log(‘Started’); console.log(data: ${yield}); console.log(data: ${yield}); return ‘Ending’;}let g = conoleNum();g.next(); // 控制台输出:‘Started’g.next(‘a’); // 控制台输出:‘data: a’// 不传入参数’a’,就会输出’data: undefined’g.next(‘b’); // 控制台输出:‘data: b’// 不传入参数’a’,就会输出’data: undefined’上面的例子,需要强调一个不易理解的地方。第一次调用.next(),此时函数暂停在代码第三行的yield表达式处。记得吗?yield会暂停函数执行,此时打印它的console.log(),也就是代码第三行的console,由于暂停并没有被执行,所以不会打印出结果,只输出了代码第二行的’Started’。当第二次调用.next()方法时,传入参数’a’,函数暂停在代码第四行的yield语句处。此时参数’a’会被当做上一个yield表达式的返回值,也就是代码第三行的yiled表达式的返回值,所以此时控制台输出’data: a’。而代码第四行的console.log()由于暂停,没有被输出。第三次调用,同理。所以输出’data: b’。4、Generator.prototype.throw()Generator函数返回的遍历器对象,都有一个.throw()方法,可以在函数体外抛出错误,然后在Generator函数体内捕获。function* gen() { try { yield; } catch (e) { console.log(‘内部捕获’, e); }};var g = gen();// 下面执行一次.next()// 是为了让gen函数体执行进入try语句中的yield处// 这样抛出错误,gen函数内部的catch语句才能捕获错误g.next();try { g.throw(‘a’); g.throw(‘b’);} catch (e) { console.log(‘外部捕获’, e);}上面例子中,遍历器对象g在gen函数体外连续抛出两个错误。第一个错误被gen函数体内的catch语句捕获。g第二次抛出错误,由于gen函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就会被抛出gen函数体,被函数体外的catch语句捕获。值得注意的是:如果Generator函数内部没有部署try…catch代码块,那么遍历器对象的throw方法抛出的错误,将被外部try…catch代码块捕获。如果Generator函数内部和外部都没有部署try…catch代码块,那么程序将报错,直接中断执行。遍历器对象的throw方法被捕获以后,会附带执行一次.next()方法,代码执行会暂停到下一条yield表达式处。看下面这个例子:function* gen(){ try { yield console.log(‘a’); } catch (e) { console.log(e); // ‘Error’ } yield console.log(‘b’); yield console.log(‘c’);}var g = gen();g.next(); // 控制台输出:‘a’g.throw(‘Error’); // 控制台输出:‘b’// throw的错误被内部catch语句捕获,// 会自动在执行一次g.next()g.next(); // 控制台输出:‘c'5、Generator.prototype.return()Generator函数返回的遍历器对象,还有一个.return()方法,可以返回给定的值,并且直接结束对遍历器对象的遍历。function* gen() { yield 1; yield 2; yield 3;}var g = gen();g.next(); // { value: 1, done: false }// 提前结束对g的遍历。尽管yield还没有执行完// 此时done属性值为true,说明遍历结束g.return(‘foo’); // { value: “foo”, done: true }g.next(); // { value: undefined, done: true }如果.return()方法调用时,不提供参数,则返回值的value属性为undefined。6、yield* 表达式yield* 用来在一个Generator函数里面执行另一个Generator函数。如果在一个Generator函数内部,直接调用另一个Generator函数,默认情况下是没有效果的。function* gen1() { yield ‘a’; yield ‘b’;}function* gen2() { yield ‘x’; // 直接调用gen1() gen1(); yield ‘y’;}// 遍历器对象可以使用for…of遍历所有状态for (let v of gen2()){ 只输出了gen1的状态 console.log(v); // ‘x’ ‘y’}上面的例子中,gen1和gen2都是Generator函数,在gen2里面直接调用gen1,是不会有效果的。这个就需要用到 yield* 表达式。function* gen1() { yield ‘a’; yield ‘b’;}function* gen2() { yield ‘x’; // 用 yield* 调用gen1() yield* gen1(); yield ‘y’;}for (let v of gen2()){ 输出了gen1、gen2的状态 console.log(v); // ‘x’ ‘a’ ‘b’ ‘y’}小节本文主要讲解Generator函数的基本语法和一些细节,Generator函数的定义、yield表达式、.next()方法及传参、.throw()方法、.return()方法以及 yield* 表达式。文章开头讲到,Generator函数时ES6提出的异步编程的一种解决方案。在实际应用中,一般在yield关键字后面会跟随一个异步操作,当异步操作成功返回后调用.next()方法,将异步流程交给下一个yield表达式。具体关于Generator函数的异步应用,大家可以参考阮一峰老师的这篇文章,或参考其他网上资料,继续深入学习。 ...

March 29, 2019 · 2 min · jiezi

ES6 - 箭头函数、箭头函数与普通函数的区别总结

这篇文章我们来了解一下ES6中的箭头函数。首先会介绍一下箭头函数的基本语法,因为基本语法比较好理解,我们用示例做简单介绍即可。之后,我们重点来讨论一下箭头函数与普通函数之间的区别。本文已同步至我的个人主页。欢迎访问查看更多内容!如有错误或遗漏,欢迎随时指正探讨!谢谢大家的关注与支持!一、基本语法ES6中允许使用箭头=>来定义箭头函数,具体语法,我们来看一个简单的例子:// 箭头函数let fun = (name) => { // 函数体 return Hello ${name} !;};// 等同于let fun = function (name) { // 函数体 return Hello ${name} !;};可以看出,定义箭头函在数语法上要比普通函数简洁得多。箭头函数省去了function关键字,采用箭头=>来定义函数。函数的参数放在=>前面的括号中,函数体跟在=>后的花括号中。关于箭头函数的参数:① 如果箭头函数没有参数,直接写一个空括号即可。② 如果箭头函数的参数只有一个,也可以省去包裹参数的括号。③ 如果箭头函数有多个参数,将参数依次用逗号(,)分隔,包裹在括号中即可。// 没有参数let fun1 = () => { console.log(111);};// 只有一个参数,可以省去参数括号let fun2 = name => { console.log(Hello ${name} !)};// 有多个参数let fun3 = (val1, val2, val3) => { return [val1, val2, val3];};关于箭头函数的函数体:① 如果箭头函数的函数体只有一句代码,就是简单返回某个变量或者返回一个简单的JS表达式,可以省去函数体的大括号{ }。let f = val => val;// 等同于let f = function (val) { return val };let sum = (num1, num2) => num1 + num2;// 等同于let sum = function(num1, num2) { return num1 + num2;};② 如果箭头函数的函数体只有一句代码,就是返回一个对象,可以像下面这样写:// 用小括号包裹要返回的对象,不报错let getTempItem = id => ({ id: id, name: “Temp” });// 但绝不能这样写,会报错。// 因为对象的大括号会被解释为函数体的大括号let getTempItem = id => { id: id, name: “Temp” };③ 如果箭头函数的函数体只有一条语句并且不需要返回值(最常见是调用一个函数),可以给这条语句前面加一个void关键字let fn = () => void doesNotReturn();箭头函数最常见的用处就是简化回调函数。// 例子一// 正常函数写法[1,2,3].map(function (x) { return x * x;});// 箭头函数写法[1,2,3].map(x => x * x);// 例子二// 正常函数写法var result = [2, 5, 1, 4, 3].sort(function (a, b) { return a - b;});// 箭头函数写法var result = [2, 5, 1, 4, 3].sort((a, b) => a - b);二、箭头函数与普通函数的区别1、语法更加简洁、清晰从上面的基本语法示例中可以看出,箭头函数的定义要比普通函数定义简洁、清晰得多,很快捷。2、箭头函数不会创建自己的this(重要!!深入理解!!)我们先来看看MDN上对箭头函数this的解释。箭头函数不会创建自己的this,所以它没有自己的this,它只会从自己的作用域链的上一层继承this。箭头函数没有自己的this,它会捕获自己在定义时(注意,是定义时,不是调用时)所处的外层执行环境的this,并继承这个this值。所以,箭头函数中this的指向在它被定义的时候就已经确定了,之后永远不会改变。来看个例子:var id = ‘Global’;function fun1() { // setTimeout中使用普通函数 setTimeout(function(){ console.log(this.id); }, 2000);}function fun2() { // setTimeout中使用箭头函数 setTimeout(() => { console.log(this.id); }, 2000)}fun1.call({id: ‘Obj’}); // ‘Global’fun2.call({id: ‘Obj’}); // ‘Obj’上面这个例子,函数fun1中的setTimeout中使用普通函数,2秒后函数执行时,这时函数其实是在全局作用域执行的,所以this指向Window对象,this.id就指向全局变量id,所以输出’Global’。但是函数fun2中的setTimeout中使用的是箭头函数,这个箭头函数的this在定义时就确定了,它继承了它外层fun2的执行环境中的this,而fun2调用时this被call方法改变到了对象{id: ‘Obj’}中,所以输出’Obj’。再来看另一个例子:var id = ‘GLOBAL’;var obj = { id: ‘OBJ’, a: function(){ console.log(this.id); }, b: () => { console.log(this.id); }};obj.a(); // ‘OBJ’obj.b(); // ‘GLOBAL’上面这个例子,对象obj的方法a使用普通函数定义的,普通函数作为对象的方法调用时,this指向它所属的对象。所以,this.id就是obj.id,所以输出’OBJ’。但是方法b是使用箭头函数定义的,箭头函数中的this实际是继承的它定义时所处的全局执行环境中的this,所以指向Window对象,所以输出’GLOBAL’。(这里要注意,定义对象的大括号{}是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中!!)3、箭头函数继承而来的this指向永远不变(重要!!深入理解!!)上面的例子,就完全可以说明箭头函数继承而来的this指向永远不变。对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。4、.call()/.apply()/.bind()无法改变箭头函数中this的指向.call()/.apply()/.bind()方法可以用来动态修改函数执行时this的指向,但由于箭头函数的this定义时就已经确定且永远不会改变。所以使用这些方法永远也改变不了箭头函数this的指向,虽然这么做代码不会报错。var id = ‘Global’;// 箭头函数定义在全局作用域let fun1 = () => { console.log(this.id)};fun1(); // ‘Global’// this的指向不会改变,永远指向Window对象fun1.call({id: ‘Obj’}); // ‘Global’fun1.apply({id: ‘Obj’}); // ‘Global’fun1.bind({id: ‘Obj’})(); // ‘Global'5、箭头函数不能作为构造函数使用我们先了解一下构造函数的new都做了些什么?简单来说,分为四步: ① JS内部首先会先生成一个对象; ② 再把函数中的this指向该对象; ③ 然后执行构造函数中的语句; ④ 最终返回该对象实例。但是!!因为箭头函数没有自己的this,它的this其实是继承了外层执行环境中的this,且this指向永远不会随在哪里调用、被谁调用而改变,所以箭头函数不能作为构造函数使用,或者说构造函数不能定义成箭头函数,否则用new调用时会报错!let Fun = (name, age) => { this.name = name; this.age = age;};// 报错let p = new Fun(‘cao’, 24);6、箭头函数没有自己的arguments箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是外层局部(函数)执行环境中的值。// 例子一let fun = (val) => { console.log(val); // 111 // 下面一行会报错 // Uncaught ReferenceError: arguments is not defined // 因为外层全局环境没有arguments对象 console.log(arguments); };fun(111);// 例子二function outer(val1, val2) { let argOut = arguments; console.log(argOut); // ① let fun = () => { let argIn = arguments; console.log(argIn); // ② console.log(argOut === argIn); // ③ }; fun();}outer(111, 222);上面例子二,①②③处的输出结果如下:很明显,普通函数outer内部的箭头函数fun中的arguments对象,其实是沿作用域链向上访问的外层outer函数的arguments对象。可以在箭头函数中使用rest参数代替arguments对象,来访问箭头函数的参数列表!!7、箭头函数没有原型prototypelet sayHi = () => { console.log(‘Hello World !’)};console.log(sayHi.prototype); // undefined8、箭头函数不能用作Generator函数,不能使用yeild关键字这一点等到讲解Generator函数的时候我们在深入讨论。 ...

March 29, 2019 · 2 min · jiezi

手把手教你实现一个Promise

1、constructor首先我们都知道Promise 有三个状态,为了方便我们把它定义成常量const PENDING = ‘pending’;const FULFILLED = ‘fulfilled’;const REJECTED = ‘rejected’;接下来我们来定义一个类class MyPromise { constructor(executor) { //控制状态,使用了一次之后,接下来的都不被使用 this.state = PENDING; this.value = null; this.reason = null; // 定义resolve函数 const resolve = value => { if (this.state === PENDING) { this.value = value; this.state = FULFILLED; } } // 定义reject函数 const reject = value => { if (this.state === PENDING) { this.reason = value; this.state = REJECTED; } } // executor方法可能会抛出异常,需要捕获 try { // 将resolve和reject函数给使用者 executor(resolve, reject); } catch (error) { // 如果在函数中抛出异常则将它注入reject中 reject(error); } }}到这基本比较好理解我简单说明一下executor:这是实例Promise对象时在构造器中传入的参数,一般是一个function(resolve,reject){}state:Promise的状态,一开始是默认的pendding状态,每当调用道resolve和reject方法时,就会改变其值,在后面的then方法中会用到value:resolve回调成功后,调用resolve方法里面的参数值reason:reject回调成功后,调用reject方法里面的参数值resolve:声明resolve方法在构造器内,通过传入的executor方法传入其中,用以给使用者回调reject:声明reject方法在构造器内,通过传入的executor方法传入其中,用以给使用者回调2、thenthen就是将Promise中的resolve或者reject的结果拿到,那么我们就能知道这里的then方法需要两个参数,成功回调和失败回调,上代码!then(onFulfilled, onRejected) { if (this.state === FULFILLED) { onFulfilled(this.value) } if (this.state === REJECTED) { onRejected(this.reason) }}我们来简单的运行一下测试代码const mp = new MyPromise((resolve, reject)=> { resolve(’******* i love you ’);})mp.then((suc)=> {console.log(11111, suc);}, (err)=> {console.log(’ 你不爱我了******’, err)})// 11111 ‘******* i love you ‘这样看着好像没有问题,那么我们来试试异步函数呢?const mp = new MyPromise((resolve, reject)=> { setTimeout(()=> { resolve(’ i love you ’); }, 0)})mp.then((suc)=> {console.log(11111, suc);}, (err)=> {console.log(’ 你不爱我了******’, err)})我们会发现什么也没有打印,哪里出问题了呢?原来是由于异步的原因,当我们执行到then的时候this. state的值还没发生改变,所以then里面的判断就失效了。那么我们该怎么解决呢?这就要说回经典得callback 了。来上源码// 存放成功回调的函数this.onFulfilledCallbacks = [];// 存放失败回调的函数this.onRejectedCallbacks = [];const resolve = value => { if (this.state === PENDING) { this.value = value; this.state = FULFILLED; this.onFulfilledCallbacks.map(fn => fn()); }}const reject = value => { if (this.state === PENDING) { this.value = value; this.reason = REJECTED; this.onRejectedCallbacks.map(fn => fn()); }}在then里面添加then(onFulfilled, onRejected) { // … if(this.state === PENDING) { this.onFulfilledCallbacks.push(()=> { onFulfilled(this.value); }); this.onRejectedCallbacks.push(()=> { onRejected(this.value); }) }}好了,到这异步的问题解决了,我们再来执行一下刚才的测试代码。结果就出来了。到这我们还缺什么呢?链式调用当我们不传参数时应当什么运行这二个的思路也都很简单,链式调用也就是说我们再返回一个promise的实例就好了。而不传参则就是默认值的问题了。下面来看源码then(onFulfilled, onRejected) { let self = this; let promise2 = null; //解决onFulfilled,onRejected没有传值的问题 onFulfilled = typeof onFulfilled === ‘function’ ? onFulfilled : y => y //因为错误的值要让后面访问到,所以这里也要跑出个错误,不然会在之后then的resolve中捕获 onRejected = typeof onRejected === ‘function’ ? onRejected : err => { throw err; } promise2 = new MyPromise((resolve, reject) => { if (self.state === PENDING) { console.log(’then PENDING’) self.onFulfilledCallbacks.push(() => { setTimeout(() => { try { let x = onFulfilled(self.value); console.log(333333, x, typeof x); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }, 0) }); self.onRejectedCallbacks.push(() => { setTimeout(() => { try { let x = onRejected(self.reason); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }, 0); }); } if (self.state === FULFILLED) { console.log(’then FULFILLED’) setTimeout(() => { try { let x = onFulfilled(self.value); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }, 0); } if (self.state === REJECTED) { console.log(’then REJECTED’) setTimeout(() => { try { let x = onRejected(self.reason); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }) } }); return promise2;}为什么外面要包一层setTimeout?:因为Promise本身是一个异步方法,属于微任务一列,必须得在执行栈执行完了在去取他的值,所以所有的返回值都得包一层异步setTimeout。resolvePromise是什么?:这其实是官方Promise/A+的需求。因为你的then可以返回任何职,当然包括Promise对象,而如果是Promise对象,我们就需要将他拆解,直到它不是一个Promise对象,取其中的值。3、resolvePromise我们直接看代码resolvePromise(promise2, x, resolve, reject) { let self = this; let called = false; // called 防止多次调用 //因为promise2是上一个promise.then后的返回结果,所以如果相同,会导致下面的.then会是同一个promise2,一直都是,没有尽头 //相当于promise.then之后return了自己,因为then会等待return后的promise,导致自己等待自己,一直处于等待 if (promise2 === x) { return reject(new TypeError(‘循环引用’)); } //如果x不是null,是对象或者方法 if (x !== null && (Object.prototype.toString.call(x) === ‘[object Object]’ || Object.prototype.toString.call(x) === ‘[object Function]’)) { // x是对象或者函数 try { let then = x.then; if (typeof then === ‘function’) { then.call(x, (y) => { // 别人的Promise的then方法可能设置了getter等,使用called防止多次调用then方法 if (called) return; called = true; // 成功值y有可能还是promise或者是具有then方法等,再次resolvePromise,直到成功值为基本类型或者非thenable self.resolvePromise(promise2, y, resolve, reject); }, (reason) => { if (called) return; called = true; reject(reason); }); } else { if (called) return; called = true; resolve(x); } } catch (reason) { if (called) return; called = true; reject(reason); } } else { // x是普通值,直接resolve resolve(x); }}为什么要在一开始判断promise2和x?:首先在Promise/A+中写了需要判断这两者如果相等,需要抛出异常,我就来解释一下为什么,如果这两者相等,我们可以看下下面的例子,第一次p2是p1.then出来的结果是个Promise对象,这个Promise对象在被创建的时候调用了resolvePromise(promise2,x,resolve,reject)函数,又因为x等于其本身,是个Promise,就需要then方法递归它,直到他不是Promise对象,但是x(p2)的结果还在等待,他却想执行自己的then方法,就会导致等待。为什么要递归去调用resolvePromise函数?:相信细心的人已经发现了,我这里使用了递归调用法,首先这是Promise/A+中要求的,其次是业务场景的需求,当我们碰到那种Promise的resolve里的Promise的resolve里又包了一个Promise的话,就需要递归取值,直到x不是Promise对象。4、catch//catch方法catch(onRejected){ return this.then(null,onRejected)}5、finallyfinally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。finally(fn) { return this.then(value => { fn(); return value; }, reason => { fn(); throw reason; });};6、resolve/reject大家一定都看到过Promise.resolve()、Promise.reject()这两种用法,它们的作用其实就是返回一个Promise对象,我们来实现一下。static resolve(val) { return new MyPromise((resolve, reject) => { resolve(val) })}//reject方法static reject(val) { return new MyPromise((resolve, reject) => { reject(val) })}7、allall方法可以说是Promise中很常用的方法了,它的作用就是将一个数组的Promise对象放在其中,当全部resolve的时候就会执行then方法,当有一个reject的时候就会执行catch,并且他们的结果也是按着数组中的顺序来排放的,那么我们来实现一下。static all(promiseArr) { return new MyPromise((resolve, reject) => { let result = []; promiseArr.forEach((promise, index) => { promise.then((value) => { result[index] = value; if (result.length === promiseArr.length) { resolve(result); } }, reject); }); });}8、racerace方法虽然不常用,但是在Promise方法中也是一个能用得上的方法,它的作用是将一个Promise数组放入race中,哪个先执行完,race就直接执行完,并从then中取值。我们来实现一下吧。static race(promiseArr) { return new MyPromise((resolve, reject) => { promiseArr.forEach(promise => { promise.then((value) => { resolve(value); }, reject); }); });}9、deferredstatic deferred() { let dfd = {}; dfd.promies = new MyPromise((resolve, reject) => { dfd.resolve = resolve; dfd.rfeject = reject; }); return dfd;};什么作用呢?看下面代码你就知道了let fs = require(‘fs’)let MyPromise = require(’./MyPromise’)//Promise上的语法糖,为了防止嵌套,方便调用//坏处 错误处理不方便function read(){ let defer = MyPromise.defer() fs.readFile(’./1.txt’,‘utf8’,(err,data)=>{ if(err)defer.reject(err) defer.resolve(data) }) return defer.Promise}10、测试const mp1 = MyPromise.resolve(1);const mp2 = MyPromise.resolve(2);const mp3 = MyPromise.resolve(3);const mp4 = MyPromise.reject(4);MyPromise.all([mp1, mp2, mp3]).then(x => { console.log(x);}, (err) => { console.log(’err1’, err);})MyPromise.race([mp1, mp4, mp2, mp3]).then(x => { console.log(x);}, (err) => { console.log(’err2’, err);})var mp = new MyPromise((resolve, reject) => { console.log(11111); setTimeout(() => { resolve(22222); console.log(3333); }, 1000);});mp.then(x => { console.log(x);}, (err) => { console.log(’err2’, err);})//11111//[ 1, 2, 3 ]//1//3333//22222完整源码请查看如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ...

March 29, 2019 · 4 min · jiezi

Promise入门之基本用法

Promise入门之基本用法背景在我们使用异步函数比如ajax进行编写代码,如果我们需要很多个ajax请求不同的接口,而下一个接口需要依赖上一个接口的返回值,这样,我们的代码则需要在各种回调函数中嵌套,这样一层一层地下去,就形成了回调地狱。 但是promise的出现则不需要嵌套就能解决这个问题。什么是promise?promise本质其实是一个对象,用于传递异步操作的信息。并且promise这个对象提供了相对应的API,满足我们的需求开发。创建promise对象let pro = new Promise(function(resolve, reject){ // 异步处理逻辑 // 处理完毕之后调用resolve或者reject})promise对象跟其他普通对象的创建方法一样,只需要new一个新的对象即可,接受一个函数作为参数,并且该函数中的参数分别为两个回调函数,用于进行不同的逻辑处理。 在定完一个promise对象之后,我们可以通过调用then方法来执行其对应的逻辑pro.then(function(res){ // 如果promise对象调用了resolve方法,则进入该函数逻辑}, function(err){ // 如果promise对象调用了reject方法,则进入该函数逻辑})promise的状态promise的实例主要有以下三种状态:①pending: 处理中②fulfilled: 成功③rejected: 失败pending状态的promise可以转化为fulfilled状态或者rejected状态,该转化不可逆且只能转化一次。同时,fulfilled状态和rejected状态只能由pending状态转化,相互不能转化。如图pending状态下的promise在处理完业务逻辑,且能正常退出时便可以执行resolve方法,从而进入fulfilled状态; 若pending状态下的promise在处理业务逻辑的过程中出现异常错误,或者主动调用reject方法,则进入rejected状态。let pro = new Promise(function(resolve, reject){ if(// 逻辑处理完毕且能正常退出){ resolve() } else{ // 异常错误抛出 reject() }})pro.then(function(res){ // 如果promise对象调用了resolve方法,则进入该函数逻辑}, function(err){ // 如果promise对象调用了reject方法,则进入该函数逻辑})链式调用因为promise对象中的then方法的返回值是一个新的promise对象,因此可以实现链式调用。但后一个then方法的执行必须等待前一个then方法返回的promise对象状态转为fulfilled或者rejected,若promise对象处于pending状态下,则后一个then方法只能等待。pro.then(function(res){ // 第一个promise对象逻辑执行 return newPro;// 返回一个新promise}).then(function(res){ // 对newPro这个对象进行处理})// …可以一直链式调用下去异常捕捉promise中的catch方法其实就是pro.then(null, rejection),用户捕捉代码运行中的错误异常。pro.then(function(res){ // 逻辑处理,但存在异常}).catch(function(err){ // 捕捉上一个then函数中所出现的异常错误})此外,catch方法的所捕捉的异常不仅仅局限于上一个then方法内,而是可以把错误一直传递下来,直至遇到的第一个catch,然后被捕捉。如链式调用中:pro.then(function(res){ // 逻辑处理,但存在异常}).then({ // 逻辑处理}).catch(function(err){ // 捕捉上面第一个出现异常的then函数中所出现的错误})promise.allpromise.all方法可以接受一个由promise组成的数组作为参数,包装成一个新的promise对象,并且按照数组中promise的顺序进行异步执行。如:let pro1 = new Promise(function(resolve, reject){});let pro2 = new Promise(function(resolve, reject){});let pro3 = new Promise(function(resolve, reject){});let proAll = promise.all([pro1, pro2, pro3]);proAll的状态由pro1,pro2,pro3三者共同决定:①pending: 处理中,pro1,pro2,pro3中无rejected且存在pending状态。②rejected: pro1,pro2,pro3中存在一个rejected。③fulfilled:pro1,pro2,pro3三者均为fulfilled。当proAll的状态为fulfilled时,会返回一个数组,该数组中的元素则是上面由promise组成的数组相对应执行后的结果。promise.racepromise.race所接受的参数与promise.all一致,但promise.race的返回值则是由pro1,pro2,pro3三者中最先完成的promise对象决定,并且返回值为该最早完成的promise对象的返回值。let proAll = promise.race([pro1, pro2, pro3]);promise.resolvepromise.resolve方法能将一个对象转换成promise对象let newPro = promise.resolve(obj);①若obj不具有then方法,则newPro直接变为fulfilled状态let newPro = promise.resolve(‘i am not a promise’);newPro.then(function(str){ console.log(str) // 输出 i am not a promise})②如果obj本就是一个Promise对象,则promise.resolve会将obj直接返回。promise.rejectpromise.reject方法与promise.resolve一样,能将一个对象转换成promise对象,但返回的promise对象的状态为rejected。async/awaitasync是关键词,在一个函数名之前加上该关键词,表明该函数内部有异步操作,而这异步操作应该返回一个promise对象,并且在这异步操作之前添加await关键词。当函数执行的时候,遇到await则会先进行Promise的逻辑处理,带promise的状态不再为pending时再执行该函数后面的语句。let pro = new Promise(function(resolve, reject){ // 异步处理逻辑 resolve();})async function asyncFunc(){ // 正常执行的语句 await pro; // 等待promise处理完之后再执行的语句}asyncFunc();总结promise的出现,为我们编写异步函数定下不少规则,在优化我们代码的同时也能减少代码量,并增强可读性,但也需严格遵守promise/A+的规范进行promise的开发。 ...

March 28, 2019 · 1 min · jiezi

在最新版Node中使用ES6语法-2019-03-27

描述node中使用ES6语法,很简单,网上的文章写的太复杂,我根据网上的经验折腾了一下午,最后终结了下,几乎装个babel就能用的。下面是我的使用过程,分享如何使用及遇到的问题。配置环境首先的有node环境,这个不介绍,当前我的版本是8.11.41.初始化npm init -y2.安装babel安装官网上介绍的操作就行,https://www.babeljs.cn/npm install –save-dev babel-cli babel-preset-env创建 .babelrc 文件{ “presets”: [“env”]}这时候已经配置完了,可以执行ES6语法了,但是import和export还是不支持的。检测对es6的支持情况安装es-checker来帮助我们查看对es6的支持情况。npm install –save-dev es-checker借助npx工具来运行es-checkernpx的介绍可以看这篇文章:http://www.ruanyifeng.com/blo…npx es-checker结果如下:ECMAScript 6 Feature Detection (v1.4.1)Variables √ let and const √ TDZ error for too-early access of let or const declarations √ Redefinition of const declarations not allowed √ destructuring assignments/declarations for arrays and objects √ … operator…省略内容Module × Module export command × Module import command=========================================Passes 39 feature DetectionsYour runtime supports 92% of ECMAScript 6=========================================可以看到还是有一些不支持的。测试code.babelrc{ “presets”: [“env”]}上面的配置其实就行了;但是我在看babel的文档时说设置node环境需要设置targets,于是我的配置如下,但是我试了上面的配置也是可以的,下面的仅供参考。{ “presets”: [ [ “env”, { “targets”: { “node”: “current” } } ] ]}package.json{ “name”: “node”, “version”: “1.0.0”, “description”: “”, “main”: “index.js”, “scripts”: { “test”: “echo "Error: no test specified" && exit 1” }, “keywords”: [], “author”: “”, “license”: “ISC”, “devDependencies”: { “babel-cli”: “^6.26.0”, “babel-preset-env”: “^1.7.0”, “es-checker”: “^1.4.1” }}Stack.jsconst Stack = (function() { const items = new WeakMap(); class Stack { constructor() { items.set(this, []); } push(value) { let stack = items.get(this); stack.push(value); } pop() { let stack = items.get(this); return stack.pop(); } isEmpty() { let stack = items.get(this); return stack.length === 0; } size() { let stack = items.get(this); return stack.length; } print() { let stack = items.get(this); console.log(stack.toString()); } } return Stack;})();module.exports.Stack = Stack;index.jsconst { Stack } = require("./Stack.js");//import { Stack } from “./Stack”;let stack = new Stack();stack.push(“aaa”);stack.print();在控制台中执行node index.js输出aaa解决import和export不能用其实node版本9以上就已经支持了,但是需要把文件名改成*.mjs,并且加上–experimental-modules 选项。升级node介绍一个node升级的好工具,名字就叫n,具体可以去npm上查看。npm install -g n执行如下命令进行升级n stable或n 10.15.3结果 install : node-v11.12.0 mkdir : /usr/local/n/versions/node/11.12.0 fetch : https://nodejs.org/dist/v11.12.0/node-v11.12.0-darwin-x64.tar.gz######################################################################## 100.0% installed : v11.12.0升级成功后,①把文件都改成*.mjs,②并把代码改成import和export的方式,执行node –experimental-modules arithmetic/index.mjs 上面两步都不能少。不然就执行不成功。 ...

March 27, 2019 · 2 min · jiezi

大话javascript 4期:事件循环(3)

一、定时器除了放置异步任务的事件,“任务队列"还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做"定时器”(timer)功能,也就是定时执行的代码。定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。以下主要讨论setTimeout()。setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。console.log(1);setTimeout(function(){console.log(2);},1000);console.log(3);上面代码的执行结果是1,3,2,因为setTimeout()将第二行推迟到1000毫秒之后执行。如果将setTimeout()的第二个参数设为0,就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。setTimeout(function(){console.log(1);}, 0);console.log(2);上面代码的执行结果总是2,1,因为只有在执行完第二行以后,系统才会去执行"任务队列"中的回调函数。总之,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()。需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。二、Node.js的Event LoopNode.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。这里需要注意一下,node新加了一个微任务(process.nextTick)和一个宏任务(setImmediate)简单的来说,就是node在处理一个执行队列的时候不管怎样都会先执行完当前队列,然后再清空微任务队列,再去执行下一个队列。请看下面的示意图(作者@BusyRich)。根据上图,Node.js的运行机制如下。(1)V8引擎解析JavaScript脚本。(2)解析后的代码,调用Node API。(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。(4)V8引擎再将结果返回给用户。除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与"任务队列"有关的方法:process.nextTick和setImmediate。它们可以帮助我们加深对"任务队列"的理解。process.nextTick方法可以在当前"执行栈"的尾部—-下一次Event Loop(主线程读取"任务队列")之前—-触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像。请看下面的例子(via StackOverflow)。process.nextTick(function A() { console.log(1); process.nextTick(function B(){console.log(2);});});setTimeout(function timeout() { console.log(‘TIMEOUT FIRED’);}, 0)// 1// 2// TIMEOUT FIRED上面代码中,由于process.nextTick方法指定的回调函数,总是在当前"执行栈"的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前"执行栈"执行。现在,再看setImmediate。setImmediate(function A() { console.log(1); setImmediate(function B(){console.log(2);});});setTimeout(function timeout() { console.log(‘TIMEOUT FIRED’);}, 0);上面代码中,setImmediate与setTimeout(fn,0)各自添加了一个回调函数A和timeout,都是在下一次Event Loop触发。那么,哪个回调函数先执行呢?答案是不确定。运行结果可能是1–TIMEOUT FIRED–2,也可能是TIMEOUT FIRED–1–2。令人困惑的是,Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面。实际上,这种情况只发生在递归调用的时候。setImmediate(function (){ setImmediate(function A() { console.log(1); setImmediate(function B(){console.log(2);}); }); setTimeout(function timeout() { console.log(‘TIMEOUT FIRED’); }, 0);});// 1// TIMEOUT FIRED// 2上面代码中,setImmediate和setTimeout被封装在一个setImmediate里面,它的运行结果总是1–TIMEOUT FIRED–2,这时函数A一定在timeout前面触发。至于2排在TIMEOUT FIRED的后面(即函数B在timeout后面触发),是因为setImmediate总是将事件注册到下一轮Event Loop,所以函数A和timeout是在同一轮Loop执行,而函数B在下一轮Loop执行。我们由此得到了process.nextTick和setImmediate的一个重要区别:多个process.nextTick语句总是在当前"执行栈"一次执行完,多个setImmediate可能则需要多次loop才能执行完。事实上,这正是Node.js 10.0版添加setImmediate方法的原因,否则像下面这样的递归调用process.nextTick,将会没完没了,主线程根本不会去读取"事件队列"!process.nextTick(function foo() { process.nextTick(foo);});事实上,现在要是你写出递归的process.nextTick,Node.js会抛出一个警告,要求你改成setImmediate。另外,由于process.nextTick指定的回调函数是在本次"事件循环"触发,而setImmediate指定的是在下次"事件循环"触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查"任务队列")。最后注意一下,微任务中process.nextTick比promise.then快

March 27, 2019 · 1 min · jiezi

大话javascript 4期:事件循环(2)

一、任务队列同步任务与异步任务的由来单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务与异步任务的定义同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。异步执行的运行机制具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。(4)主线程不断重复上面的第三步。只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。举个例子:console.log(“1”);setTimeout(()=>{ console.log(“2”);},0);console.log(“3”);// 1// 3// 2运行结果是:1、3、2setTimeout里的函数并没有立即执行,而是延迟一段时间,符合特定的条件才开始执行,这就是异步执行操作。console.log(“1”) //是同步任务,放入主线程,setTimeout() //是异步任务,被放入事件列表Event table中,0秒后被推入任务队列task queue里,console.log(“3”) //是同步任务,放入主线程//当1、3任务先执行完后,主线程去task queue(事件队列)里查看是否有可执行的函数,执行setTimeout里的函数。二、事件和回调函数"任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。“任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列”,等待主线程读取。所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。“任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,“任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。三、Event Loop主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。主线程运行的时候,产生堆(heap)和栈(stack),heap(堆):是用户主动请求而划分出来的内存区域,比如你new Object(),就是将一个对象存入堆中,可以理解为heap存对象。stack(栈):是由于函数运行而临时占用的内存区域,函数都存放在栈里。栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。(当满足触发条件后才加入队列,如ajax请求完毕)而当栈中的代码执行完毕,主线程就会去读取"任务队列”,依次执行那些事件所对应的回调函数。如此循环【注意,总是要等待栈中的代码执行完毕后才会去读取事件队列中的事件】四、宏任务和微任务JS中分为两种任务类型:宏任务macro task和微任务micro task,在ECMAScript中,micro task称为jobs,macro task可称为task宏任务与微任务的定义1)宏任务(macro task),可以理解为每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)每一个task会从头到尾将这个任务执行完毕,不会执行其它浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染(task->渲染->task->…)2)微任务(micro task),可以理解为在当前 task 执行结束后立即执行的任务也就是说,在当前task任务后,下一个task之前,在渲染之前所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染也就是说,在某一个macro task执行完后,就会将在它执行期间产生的所有micro task都执行完毕(在渲染前)常见的宏任务和微任务:1)宏任务(macro task):主代码块,setTimeout,setInterval,I/O、UI交互事件、postMessage、MessageChannel、setImmediate(node.js 环境)等(可以看到,事件队列中的每一个事件都是一个宏任务)2)微任务(micro task):Promise.then、MutaionObserver、MessageChannel、process.nextTick(node.js 环境)等__补充:在node环境下,process.nextTick的优先级高于Promise,也就是可以简单理解为:在宏任务结束后会先执行微任务队列中的nextTickQueue部分,然后才会执行微任务中的Promise部分。再根据线程来理解下:宏任务(macro task)中的事件都是放在一个事件队列中的,而这个队列由事件触发线程维护微任务(micro task)中的所有微任务都是添加到微任务队列(Job Queues)中,等待当前宏任务执行完毕后执行,而这个队列由JS引擎线程维护所以,总结下运行机制:执行一个宏任务(栈中没有就从事件队列中获取)执行过程中如果遇到微任务,就将它添加到微任务的任务队列中宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)举个例子:setTimeout(()=>{console.log(“定时器开始执行”);}) new Promise(function(resolve){ console.log(“准备执行for循环了”); for(var i=0;i<100;i++){ i==22&&resolve(); }}).then(()=>console.log(“执行then函数”)); console.log(“代码执行完毕”);//首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的【队列】里//遇到 new Promise直接执行,打印"准备执行for循环” //遇到then方法,是微任务,将其放到微任务的【队列里】 //打印 “代码执行完毕” //本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数, 打印"执行then函数" //到此,本轮的event loop 全部完成。 //下一轮的循环里,先执行一个宏任务,发现宏任务的【队列】里有一个 setTimeout里的函数,执行打印"定时器开始执行"所以最后的执行顺序就是:【准备执行for循环–>代码执行完毕–>执行then函数–>定时器开始执行】

March 27, 2019 · 1 min · jiezi

大话javascript 4期:事件循环(1)

一、进程与线程现代操作系统比如Mac OS X,UNIX,Linux,Windows等,都是支持“多任务”的操作系统。什么叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用浏览器上网,一边在听MP3,一边在用Word赶作业,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。由于每个进程至少要干一件事,所以,一个进程至少有一个线程。当然,像Word这种复杂的进程可以有多个线程,多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。当然,真正地同时执行多线程需要多核CPU才可能实现。如果我们要同时执行多个任务怎么办?有两种解决方案:一种是启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务。还有一种方法是启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务。当然还有第三种方法,就是启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,当然这种模型更复杂,实际很少采用。总结一下就是,多任务的实现有3种方式:多进程模式;多线程模式;多进程+多线程模式。同时执行多个任务通常各个任务之间并不是没有关联的,而是需要相互通信和协调,有时,任务1必须暂停等待任务2完成后才能继续执行,有时,任务3和任务4又不能同时执行,所以,多进程和多线程的程序的复杂度要远远高于我们前面写的单进程单线程的程序。因为复杂度高,调试困难,所以,不是迫不得已,我们也不想编写多任务。但是,有很多时候,没有多任务还真不行。想想在电脑上看电影,就必须由一个线程播放视频,另一个线程播放音频,否则,单线程实现的话就只能先把视频播放完再播放音频,或者先把音频播放完再播放视频,这显然是不行的。总结:1) 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)2) 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)二、浏览器是多进程的浏览器包含哪些进程1) Browser进程:浏览器的主进程(负责协调、主控),只有一个。作用有负责浏览器界面显示,与用户交互。如前进,后退等负责各个页面的管理,创建和销毁其他进程将Renderer进程得到的内存中的Bitmap,绘制到用户界面上网络资源的管理,下载等2) 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建3) GPU进程:最多一个,用于3D绘制等4) 浏览器渲染进程(浏览器内核)(Renderer进程,内部是多线程的):默认每个Tab页面一个进程,互不影响。主要作用为页面渲染,脚本执行,事件处理等关于浏览器进程问题可以简单基础三点:1) 浏览器是多进程的。2) 浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)。3) 简单点理解,每打开一个Tab页,就相当于创建了一个独立的浏览器进程。平时 coding 接触到最多的一个浏览器进程是浏览器渲染进程(浏览器内核),它管理着页面渲染。脚本执行,事件处理等。要同时处理这么多事情,渲染进程显然是多线程的,它主要包括以下5个常驻线程:GUI渲染线程,负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。JS引擎线程,也称为JS内核,负责处理Javascript脚本程序,(例如V8引擎)。事件触发线程,用来控制事件循环(可以理解为,JS引擎线程自己都忙不过来,需要浏览器另开线程协助)。定时触发器线程,浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确),JS中常用的setInterval和setTimeout就归这个线程管理。异步http请求线程,也就是ajax发出http请求后,接收响应、检测状态变更等都是这个线程管理的。三、Javascript是单线程的我们常说的JavaScript是单线程的,其实就是说的JS引擎是单线程的,它仅仅是浏览器渲染进程种的一个线程。为什么呢?因为JavaScript的主要作用是与用户互动,以及操作DOM,如果JavaScript有两个线程,一个线程对一个DOM节点执行 A 操作,另一个线程这个DOM节点执行 B 操作,那么就会起冲突,所以JavaScript在前端的应用就注定了它是单线程的。然而JavaScript的单线程特性就注定我们不用它去完成密集的 cpu 运算,因为密集 cpu 运算耗时过长,阻塞页面渲染。为了解决这个问题,HTML5提出 Web Worker 标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。

March 27, 2019 · 1 min · jiezi

js 处理异步操作的几种方式

概论由于 JavaScript 是一门单线程执行的语言,所以在我们处理耗时较长的任务时,异步编程就显得尤为重要。js 处理异步操作最传统的方式是回调函数,基本上所有的异步操作都可以用回调函数来处理;为了使代码更优雅,人们又想到了用事件监听、发布/订阅模式和 Promise 等来处理异步操作;之后在 ES2015 语言标准中终于引入了Promise,从此浏览器原生支持 Promise ;此外,ES2015 中的生成器generator因其中断/恢复执行和传值等优秀功能也被人们用于异步处理;之后,ES2017 语言标准又引入了更优秀的异步处理方法async/await……异步处理方式为了更直观地发现这些异步处理方式的优势和不足,我们将分别使用不同的方式解决同一个异步问题。问题:假设我们需要用原生 XMLHttpRequest 获取两个 json 数据 —— 首先异步获取广州的天气,等成功后再异步获取番禺的天气,最后一起输出获取到的两个 json 数据。前提:假设我们已经了解了Promise,generator和async。回调函数我们首先用最传统的回调函数来处理:var xhr1 = new XMLHttpRequest();xhr1.open(‘GET’, ‘https://www.apiopen.top/weatherApi?city=广州’);xhr1.send();xhr1.onreadystatechange = function() { if(this.readyState !== 4) return; if(this.status === 200) { data1 = JSON.parse(this.response); var xhr2 = new XMLHttpRequest(); xhr2.open(‘GET’, ‘https://www.apiopen.top/weatherApi?city=番禺’); xhr2.send(); xhr2.onreadystatechange = function() { if(this.readyState !== 4) return; if(this.status === 200) { data2 = JSON.parse(this.response); console.log(data1, data2); } } }};优点:简单、方便、实用。缺点:易形成回调函数地狱。如果我们只有一个异步操作,用回调函数来处理是完全没有任何问题的。如果我们在回调函数中再嵌套一个回调函数,问题也不大。但是如果我们要嵌套很多个回调函数,问题就很大了,因为多个异步操作形成了强耦合,代码将乱作一团,无法管理。这种情况被称为"回调函数地狱"(callback hell)。事件监听使用事件监听的方式:var events = new Events();events.addEvent(‘done’, function(data1) { var xhr = new XMLHttpRequest(); xhr.open(‘GET’, ‘https://www.apiopen.top/weatherApi?city=番禺’); xhr.send(); xhr.onreadystatechange = function() { if(this.readyState !== 4) return; if(this.status === 200) { data1 = JSON.parse(data1); var data2 = JSON.parse(this.response); console.log(data1, data2); } }});var xhr = new XMLHttpRequest();xhr.open(‘GET’, ‘https://www.apiopen.top/weatherApi?city=广州’);xhr.send();xhr.onreadystatechange = function() { if(this.readyState !== 4) return; if(this.status === 200) { events.fireEvent(‘done’, this.response); }};上述代码需要实现一个事件监听器 Events。优点:与回调函数相比,事件监听方式实现了代码的解耦,将两个回调函数分离了开来,更方便进行代码的管理。缺点:使用起来不方便,每次都要手动地绑定和触发事件。而发布/订阅模式与其类似,就不多说了。Promise使用 ES6 Promise 的方式:new Promise(function(resolve, reject) { const xhr = new XMLHttpRequest(); xhr.open(‘GET’, ‘https://www.apiopen.top/weatherApi?city=广州’); xhr.send(); xhr.onreadystatechange = function() { if(this.readyState !== 4) return; if(this.status === 200) return resolve(this.response); reject(this.statusText); };}).then(function(value) { const xhr = new XMLHttpRequest(); xhr.open(‘GET’, ‘https://www.apiopen.top/weatherApi?city=番禺’); xhr.send(); xhr.onreadystatechange = function() { if(this.readyState !== 4) return; if(this.status === 200) { const data1 = JSON.parse(value); const data2 = JSON.parse(this.response); console.log(data1, data2); } };});优点:使用Promise的方式,我们成功地将回调函数嵌套调用变成了链式调用,与前两种方式相比逻辑更强,执行顺序更清楚。缺点:代码冗余,异步操作都被包裹在Promise构造函数和then方法中,主体代码不明显,语义变得不清楚。generator + 回调函数接下来,我们使用 generator 和回调函数来实现。首先用一个 generator function 封装异步操作的逻辑代码:function* gen() { const data1 = yield getJSON_TH(‘https://www.apiopen.top/weatherApi?city=广州’); const data2 = yield getJSON_TH(‘https://www.apiopen.top/weatherApi?city=番禺’); console.log(data1, data2);}看了这段代码,是不是感觉它很直观、很优雅。实际上,除去星号和yield关键字,这段代码就变得和同步代码一样了。当然,只有这个 gen 函数是没有用的,直接执行它只会得到一个generator对象。我们需要用它返回的 generator 对象来恢复/暂停 gen 函数的执行,同时传递数据到 gen 函数中。用getJSON_TH函数封装异步操作的主体代码:function getJSON_TH(url) { return function(fn) { const xhr = new XMLHttpRequest(); xhr.open(‘GET’, url); xhr.responseType = “json”; xhr.setRequestHeader(“Accept”, “application/json”); xhr.send(); xhr.onreadystatechange = function() { if(this.readyState !== 4) return; let err, data; if(this.status === 200) { data = this.response; } else { err = new Error(this.statusText); } fn(err, data); } }}有的同学可能觉得直接给getJSON_TH函数传入 url 和 fn 两个参数不就行了吗,为什么非要返回一个函数。其实这正是奥妙所在,getJSON_TH函数返回的函数是一个Thunk函数,它只接收一个回调函数作为参数。通过Thunk函数或者说Thunk函数的回调函数,我们可以在 gen 函数外部向其内部传入数据,同时恢复 gen 函数的执行。在 node.js 中,我们可以通过 Thunkify 模块将带回调参数的函数转化为 Thunk 函数。接下来,我们手动执行 gen 函数:const g = gen();g.next().value((err, data) => { if(err) return g.throw(err); g.next(data).value((err, data) => { if(err) return g.throw(err); g.next(data); })});其中,g.next().value 就是 gen 函数中yield输出的值,也就是我们之前提到的Thunk函数,我们在它的回调函数中,通过 g.next(data) 方法将 data 传给 gen 函数中的 data1,并且恢复 gen 函数的执行(将 gen 函数的执行上下文再次压入调用栈中)。方便起见,我们还可以将自动执行 gen 函数的操作封装起来:function run(gen) { const g = gen(); function next(err, data) { if(err) return g.throw(err); const res = g.next(data); if(res.done) return; res.value(next); } next();}run(gen);优点:generator 方式使得异步操作很接近同步操作,十分的简洁明了。另外,gen 执行 yield 语句时,只是将执行上下文暂时弹出,并不会销毁,这使得上下文状态被保存。缺点:流程管理不方便,需要一个执行器来执行 generator 函数。generator + Promise除了Thunk函数,我们还可以借助Promise对象来执行 generator 函数。同样优雅的逻辑代码:function* gen() { const data1 = yield getJSON_PM(‘https://www.apiopen.top/weatherApi?city=广州’); const data2 = yield getJSON_PM(‘https://www.apiopen.top/weatherApi?city=番禺’); console.log(data1, data2);}getJSON_PM函数返回一个 Promise 对象:function getJSON_PM(url) { return new Promise((resolve, rejext) => { const xhr = new XMLHttpRequest(); xhr.open(‘GET’, url); xhr.responseType = “json”; xhr.setRequestHeader(“Accept”, “application/json”); xhr.send(); xhr.onreadystatechange = function() { if(this.readyState !== 4) return; if(this.status === 200) return resolve(this.response); reject(new Error(this.statusText)); }; });}手动执行 generator 函数:const g = gen();g.next().value.then(data => { g.next(data).value.then(data => g.next(data), err => g.throw(err));}, err => g.throw(err));自动执行 generator 函数:function run(gen) { const g = gen(); function next(data) { const res = g.next(data); if(res.done) return; res.value.then(next); } next();}run(gen);generator + co 模块node.js 中的co模块是一个用来自动执行generator函数的模块,它的入口是一个co(gen)函数,它预期接收一个 generator 对象或者 generator 函数作为参数,返回一个Promise对象。在参数 gen 函数中,yield语句预期接收一个 generator 对象,generator 函数,thunk 函数,Promise 对象,数组或者对象。co模块的主要实现原理是将 yield 接收的值统一转换成一个Promise对象,然后用类似上述 generator + Promise 的方法来自动执行 generator 函数。下面是我根据 node.js co 模块源码修改的 es6 co 模块,让它更适合自己使用:https://github.com/lyl123321/…yield接收thunk函数:import co from ‘./co.mjs’function* gen() { const data1 = yield getJSON_TH(‘https://www.apiopen.top/weatherApi?city=广州’); const data2 = yield getJSON_TH(‘https://www.apiopen.top/weatherApi?city=番禺’); console.log(data1, data2);}co(gen);yield接收Promise对象:function* gen() { const data1 = yield getJSON_PM(‘https://www.apiopen.top/weatherApi?city=广州’); const data2 = yield getJSON_PM(‘https://www.apiopen.top/weatherApi?city=番禺’); console.log(data1, data2);}co(gen);async/awaitasync函数是generator函数的语法糖,它相对于一个自带执行器(如 co 模块)的generator函数。async函数中的await关键字预期接收一个Promise对象,如果不是 Promise 对象则返回原值,这使得它的适用性比 co 执行器更广。async函数返回一个Promise对象,这点与 co 执行器一样,这使得async函数比返回generator对象的generator函数更实用。如果 async 函数顺利执行完,则返回的 Promise 对象状态变为 fulfilled,且 value 值为 async 函数中 return 关键字的返回值;如果 async 函数执行时遇到错误且没有在 async 内部捕获错误,则返回的 Promise 对象状态变为 rejected,且 reason 值为 async 函数中的错误。await只处理Promise对象:async function azc() { const data1 = await getJSON_PM(‘https://www.apiopen.top/weatherApi?city=广州’); const data2 = await getJSON_PM(‘https://www.apiopen.top/weatherApi?city=番禺’); console.log(data1, data2);}azc();async函数将generator函数的自动执行器,改在语言层面提供,不暴露给用户。async function fn(args) { // …}相当于:function fn(args) { return exec(function* () { // … });}优点:最简洁,最符合语义,最接近同步代码,最适合处理多个 Promise 异步操作。相比 generator 方式,async 方式省掉了自动执行器,减少了代码量。缺点:js 语言自带的 async 执行器功能性可能没有 co 模块等执行器强。你可以根据自己的需求定义自己的 generator 函数执行器。我只是整理了一下阮神的文章,参考链接:http://es6.ruanyifeng.com/#do… ...

March 27, 2019 · 3 min · jiezi

大话javascript 3期:闭包

一、什么是闭包1.闭包的定义闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境(包含自由变量)。环境由闭包创建时在作用域中的任何局部变量组成。闭包是指有权访问另外一个函数作用域中的变量的函数闭包是函数以及函数声明所在的词法环境的组合。由此,我们可以看出闭包共有两部分组成:闭包 = 函数 + 函数能够访问的自由变量举个例子:var a = 1;function foo() { console.log(a);}foo();foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以 a 就是自由变量。那么,函数 foo + foo 函数访问的自由变量 a 不就是构成了一个闭包嘛2.闭包的概念function fa(){ var va = “this is fa”; function fb(){ console.log(va); } return fb;}var fc = fa();fc();//“this is fa"其实,简单点说,就是在 A 函数内部,存在 B 函数, B函数 在 A 函数 执行完毕后再执行。B执行时,访问了已经执行完毕的 A函数内部的变量和函数。由此可知:闭包是函数A的执行环境以及执行环境中的函数B组合而构成的。变量都储存在其所在执行环境的活动对象中,所以说是函数A的执行环境。当函数A执行完毕后,函数B再执行,B的作用域中就保留着函数A的活动对象,因此B中可以访问A中的变量,函数,arguments对象。此时产生了闭包。大部分书中,都把函数B称为闭包,而在谷歌浏览器中,把A函数称为闭包。3.闭包的本质之前说过,当函数执行完毕后,局部活动对象就会被销毁。其中保存的变量,函数都会被销毁。内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况就不同了。以上面的例子来说,函数fb和其所在的环境函数fa,就组成了闭包。函数fa执行完毕后,按道理说, 函数fa执行环境中的 活动对象就应该被销毁了。但是,因为函数fa执行时,其中的函数fb被返回,被变量fc引用着。导致,函数fa的活动对象没有被销毁。而在其后fc()执行,就是函数fb执行时,构建的作用域中保存着函数fa的活动对象,因此,函数fb中可以通过作用域链访问函数fa中的变量。其实,简单的说:就是fa函数执行完毕了,其内部的fb函数没有执行,并返回fb的引用,当fb再次执行时,fb的作用域中保留着fa函数的活动对象。二、闭包的作用闭包的特点是读取函数内部局部变量,并将局部变量保存在内存,延长其生命周期。作用通过闭包,在外部环境访问内部环境的变量。使得这些变量一直保存在内存中,不会被垃圾回收。以使用闭包实现以下功能:1.解决类似循环绑定事件的问题在实际开发中,经常会遇到需要循环绑定事件的需求,比如在id为container的元素中添加5个按钮,每个按钮的文案是相应序号,点击打印输出对应序号。其中第一个方法很容易错误写成:var container = document.getElementById(‘container’);for(var i = 1; i <= 5; i++) { var btn = document.createElement(‘button’), text = document.createTextNode(i); btn.appendChild(text); btn.addEventListener(‘click’, function(){ console.log(i); }) container.appendChild(btn);}虽然给不同的按钮分别绑定了事件函数,但是5个函数其实共享了一个变量 i。由于点击事件在 js 代码执行完成之后发生,此时的变量 i 值为6,所以每个按钮点击打印输出都是6。为了解决这个问题,我们可以修改代码,给各个点击事件函数建立独立的闭包,保持不同状态的i。var container = document.getElementById(‘container’);for(var i = 1; i <= 5; i++) { (function(_i) { var btn = document.createElement(‘button’), text = document.createTextNode(_i); btn.appendChild(text); btn.addEventListener(‘click’, function(){ console.log(_i); }) container.appendChild(btn); })(i);}注:解决这个问题更好的方法是使用 ES6 的 let,声明块级的局部变量。2.封装私有变量(1) 经典的计数器例子:function makeCounter() { var value = 0; return { getValue: function() { return value; }, increment: function() { value++; }, decrement: function() { value–; } }}var a = makeCounter();var b = makeCounter();b.increment();b.increment();b.decrement();b.getValue(); // 1a.getValue(); // 0a.value; // undefined每次调用makeCounter函数,环境是不相同的,所以对b进行的increment/decrement操作不会影响a的value属性。同时,对value属性,只能通过getValue方法进行访问,而不能直接通过value属性进行访问。(2) 经典的循环闭包面试题for (var i=1;i<=5;i++){ setTimeout(function timer(){ console.log(i); },i1000);}正常预想下,上面这段代码我们以为是分别输出数字1-5,每秒一个。但实际上,运行时输出的却是每秒输出一个6,一共五次。Why?for循环有一个特点,就是“i判断失败一次才停止”。所以,i在不断的自加1的时候,直到i等于5,i才失败,这时候循环体不再执行,会跳出,所以i等于5没错。那么为什么5次循环的i都等于5?原因就是setTimeout()的回调,也就是console.log(i);被压到任务队列的最后,for循环是同步任务,所以先执行,等于是空跑了5次循环。于是,i都等于5之后,console.log(i);刚开始第一次执行,当然输出全是5。根据setTimeout定义的操作在函数调用栈清空之后才会执行的特点,for循环里定义了5个setTimeout操作。而当这些操作开始执行时,for循环的i值,已经先一步变成了6。因此输出结果总为6。而我们想要让输出结果依次执行,我们就必须借助闭包的特性,每次循环时,将i值保存在一个闭包中,当setTimeout中定义的操作执行时,则访问对应闭包保存的i值即可。简单来说,原因是,延迟函数的回调会在循环结束时才执行。根据作用域的工作原理,循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,实际上只有一个i。解决办法方法1:用立即执行函数模拟块级作用域利用立即执行函数和函数作用域来解决,用自执行函数传参,这样自执行函数内部形成了局部作用域,不受外部变量变化的影响。我们可以通过立即执行函数创建作用域。(立即执行函数会通过声明并立即执行一个函数来创建作用域)。 for (var i=1; i<=5; i++) { (function(i) { setTimeout( function timer() { console.log(i); }, i1000 ); })(i) }// 1// 2// 3// 4// 5方法2:利用闭包function makeClosures(i){ //这里就和 内部的匿名函数构成闭包了 var i = i; //这步是不需要的,为了让看客们看的轻松点 return function(){ console.log(i); //匿名没有执行,它可以访问i的值,保存着这个i的值。 }}for (var i=1; i<=5; i++) { setTimeout(makeClosures(i),i1000); //这里简单说下,这里makeClosures(i), 是函数执行,并不是传参,不是一个概念 //每次循环时,都执行了makeClosures函数,都返回了一个没有被执行的匿名函数 //(这里就是返回了5个匿名函数),每个匿名函数都是一个局部作用域,保存着每次传进来的i值 //因此,每个匿名函数执行时,读取i值,都是自己作用域内保存的值,是不一样的。所以,就得到了想要的结果}//1//2//3//4//5方法3:利用块级作用域ES6引入的let在循环中不止会被声明一次,在每次迭代都会声明:for (let i=1;i<=5;i++){ setTimeout(function timer(){ console.log(i); },i1000);}因为使用let,导致每次循环都会创建一个新的块级作用域,这样,虽然setTimeout 中的匿名函数内没有 i 值,但它向上作用域读取i 值,就读到了块级作用域内 i 的值。三、闭包的问题使用闭包会将局部变量保持在内存中,所以会占用大量内存,影响性能。所以在不再需要使用这些局部变量的时候,应该手动将这些变量设置为null, 使变量能被回收。当闭包的作用域中保存一些DOM节点时,较容易出现循环引用,可能会造成内存泄漏。原因是在IE9以下的浏览器中,由于BOM 和DOM中的对象是使用C++以COM 对象的方式实现的,而COM对象的垃圾收集机制采用的是引用计数策略,当出现循环引用时,会导致对象无法被回收。当然,同样可以通过设置变量为null解决。举例如下:function func() { var element = document.getElementById(’test’); element.onClick = function() { console.log(element.id); };}func 函数为 element 添加了闭包点击事件,匿名函数中又对element进行了引用,使得 element 的引用始终不为0。解决办法是使用变量保存所需内容,并在退出函数时将 element 置为 null。function func() { var element = document.getElementById(’test’), id = element.id; element.onClick = function() { console.log(id); }; element = null;}四、应用场景:模块与柯里化模块也是利用了闭包的一个强大的代码模式。function CoolModule(){ var something=“cool”; var anothor=[1,2,3]; function doSomething(){ console.log(something); } function doAnthor(){ console.log(anothor.join(”!")); } return{ doSomethig:doSomething, doAnothor:doAnother };}var foo=CoolMOdule();foo.doSomething();//coolfoo.doAnother();//1!2!3模块有2个主要特征:为创建内部作用域而调用了一个包装函数;包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。关于模块的引入import可以将一个模块中的一个或多个API导入到当前作用域中,并分别绑定在一个变量上;module会将整个模块的API导入并绑定到一个变量上;export会将当前模块的一个标识符导出为公共API。 ...

March 26, 2019 · 2 min · jiezi

ES6--浅析Promise内部结构

一、前言什么是promise?promsie的核心是什么?promise如何解决回调地狱的?等问题1、什么是promise?promise是表示异步操作的最终结果;可以用来解决回调地狱和并发IO操作的问题A promise represents the eventual result of an asynchronous operation.2、promise 的核心是什么?promise的核心就是链式调用3、采用什么方法可以实现链式调用?通过使用then的方法,then方法是用来注册在这个Promise状态确定后的回调,很明显,then方法需要写在原型链上。4、promise是如何解决回调地狱的问题?(1)如果一个promise返回的是一个promise,会把这个promise传递结果传递到下一次的then中;(2)如果一个promise返回的是一个普通的值,会把这个普通值作为下一次then的成功回调结果;(3)如果当前promise失败了,会走下一个then的回调函数;(4)如果then不返回值,就会有一个默认值为undefined,作为普通值,会作为下一个then的成功回调;(5)catch是错误没有处理的情况才会执行;(6)then中可以不写东西二、promise的标准解读1、只有一个then方法,没有catch,race,all等方法,甚至没有构造函数;Promise标准中仅指定了Promise对象的then方法的行为,其它一切我们常见的方法/函数都并没有指定,包括catch,race,all等常用方法,甚至也没有指定该如何构造出一个Promise对象,另外then也没有一般实现中(Q, $q等)所支持的第三个参数,一般称onProgress2、then方法返回一个新的Promise;Promise的then方法返回一个新的Promise,而不是返回this,此处在下文会有更多解释promise2 = promise1.then(alert)promise2 != promise1 // true3、不同Promise的实现需要可以相互调用(interoperable)4、Promise的初始状态为pending,它可以由此状态转换为fulfilled(本文为了一致把此状态叫做resolved)或者rejected,一旦状态确定,就不可以再次转换为其它状态,状态确定的过程称为settle三、实现一个promise1、构造函数因为标准并没有指定如何构造一个Promise对象,所以我们同样以目前一般Promise实现中通用的方法来构造一个Promise对象,也是ES6原生Promise里所使用的方式,即:// Promise构造函数接收一个executor函数,executor函数执行完同步或异步操作后,调用它的两个参数resolve和rejectvar promise = new Promise(function(resolve, reject) { /* 如果操作成功,调用resolve并传入value 如果操作失败,调用reject并传入reason */})我们先实现构造函数的框架如下:function Promise(executor) { var self = this self.status = ‘pending’ // Promise当前的状态 self.data = undefined // Promise的值 self.onResolvedCallback = [] // Promise resolve时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面 self.onRejectedCallback = [] // Promise reject时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面 executor(resolve, reject) // 执行executor并传入相应的参数}上面的代码基本实现了Promise构造函数的主体,但目前还有两个问题:1、我们给executor函数传了两个参数:resolve和reject,这两个参数目前还没有定义2、executor有可能会出错(throw),类似下面这样,而如果executor出错,Promise应该被其throw出的值reject:new Promise(function(resolve, reject) { throw 2})所以我们需要在构造函数里定义resolve和reject这两个函数:function Promise(executor) { var self = this self.status = ‘pending’ // Promise当前的状态 self.data = undefined // Promise的值 self.onResolvedCallback = [] // Promise resolve时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面 self.onRejectedCallback = [] // Promise reject时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面 function resolve(value) { // TODO } function reject(reason) { // TODO } try { // 考虑到执行executor的过程中有可能出错,所以我们用try/catch块给包起来,并且在出错后以catch到的值reject掉这个Promise executor(resolve, reject) // 执行executor } catch(e) { reject(e) }}有人可能会问,resolve和reject这两个函数能不能不定义在构造函数里呢?考虑到我们在executor函数里是以resolve(value),reject(reason)的形式调用的这两个函数,而不是以resolve.call(promise, value),reject.call(promise, reason)这种形式调用的,所以这两个函数在调用时的内部也必然有一个隐含的this,也就是说,要么这两个函数是经过bind后传给了executor,要么它们定义在构造函数的内部,使用self来访问所属的Promise对象。所以如果我们想把这两个函数定义在构造函数的外部,确实是可以这么写的:function resolve() { // TODO}function reject() { // TODO}function Promise(executor) { try { executor(resolve.bind(this), reject.bind(this)) } catch(e) { reject.bind(this)(e) }}但是众所周知,bind也会返回一个新的函数,这么一来还是相当于每个Promise对象都有一对属于自己的resolve和reject函数,就跟写在构造函数内部没什么区别了,所以我们就直接把这两个函数定义在构造函数里面了。不过话说回来,如果浏览器对bind的所优化,使用后一种形式应该可以提升一下内存使用效率。另外我们这里的实现并没有考虑隐藏this上的变量,这使得这个Promise的状态可以在executor函数外部被改变,在一个靠谱的实现里,构造出的Promise对象的状态和最终结果应当是无法从外部更改的。接下来,我们实现resolve和reject这两个函数function Promise(executor) { // … function resolve(value) { if (self.status === ‘pending’) { self.status = ‘resolved’ self.data = value for(var i = 0; i < self.onResolvedCallback.length; i++) { self.onResolvedCallbacki } } } function reject(reason) { if (self.status === ‘pending’) { self.status = ‘rejected’ self.data = reason for(var i = 0; i < self.onRejectedCallback.length; i++) { self.onRejectedCallbacki } } } // …}基本上就是在判断状态为pending之后把状态改为相应的值,并把对应的value和reason存在self的data属性上面,之后执行相应的回调函数,逻辑很简单,这里就不多解释了。2、then方法then方法是用来注册这个promise确定状态后的回调,then方法是需要写在原型链上。自然约束:then方法会返回一个Promise,关于这一点,Promise/A+标准并没有要求返回的这个Promise是一个新的对象,但在Promise/A标准中,明确规定了then要返回一个新的对象,目前的Promise实现中then几乎都是返回一个新的Promise(https://promisesaplus.com/dif…,所以在我们的实现中,也让then返回一个新的Promise对象。下面我们来实现then方法:// then方法接收两个参数,onResolved,onRejected,分别为Promise成功或失败后的回调Promise.prototype.then = function(onResolved, onRejected) { var self = this var promise2 // 根据标准,如果then的参数不是function,则我们需要忽略它,此处以如下方式处理 onResolved = typeof onResolved === ‘function’ ? onResolved : function(v) {} onRejected = typeof onRejected === ‘function’ ? onRejected : function(r) {} if (self.status === ‘resolved’) { return promise2 = new Promise(function(resolve, reject) { }) } if (self.status === ‘rejected’) { return promise2 = new Promise(function(resolve, reject) { }) } if (self.status === ‘pending’) { return promise2 = new Promise(function(resolve, reject) { }) }}Promise总共有三种可能的状态,我们分三个if块来处理,在里面分别都返回一个new Promise。根据标准,我们知道,对于如下代码,promise2的值取决于then里面函数的返回值:promise2 = promise1.then(function(value) { return 4}, function(reason) { throw new Error(‘sth went wrong’)})如果promise1被resolve了,promise2的将被4 resolve,如果promise1被reject了,promise2将被new Error(‘sth went wrong’) reject,更多复杂的情况不再详述。3、完整的promisetry { module.exports = Promise} catch (e) {}function Promise(executor) { var self = this self.status = ‘pending’ self.onResolvedCallback = [] self.onRejectedCallback = [] function resolve(value) { if (value instanceof Promise) { return value.then(resolve, reject) } setTimeout(function() { // 异步执行所有的回调函数 if (self.status === ‘pending’) { self.status = ‘resolved’ self.data = value for (var i = 0; i < self.onResolvedCallback.length; i++) { self.onResolvedCallbacki } } }) } function reject(reason) { setTimeout(function() { // 异步执行所有的回调函数 if (self.status === ‘pending’) { self.status = ‘rejected’ self.data = reason for (var i = 0; i < self.onRejectedCallback.length; i++) { self.onRejectedCallbacki } } }) } try { executor(resolve, reject) } catch (reason) { reject(reason) }}function resolvePromise(promise2, x, resolve, reject) { var then var thenCalledOrThrow = false if (promise2 === x) { return reject(new TypeError(‘Chaining cycle detected for promise!’)) } if (x instanceof Promise) { if (x.status === ‘pending’) { //because x could resolved by a Promise Object x.then(function(v) { resolvePromise(promise2, v, resolve, reject) }, reject) } else { //but if it is resolved, it will never resolved by a Promise Object but a static value; x.then(resolve, reject) } return } if ((x !== null) && ((typeof x === ‘object’) || (typeof x === ‘function’))) { try { then = x.then //because x.then could be a getter if (typeof then === ‘function’) { then.call(x, function rs(y) { if (thenCalledOrThrow) return thenCalledOrThrow = true return resolvePromise(promise2, y, resolve, reject) }, function rj(r) { if (thenCalledOrThrow) return thenCalledOrThrow = true return reject(r) }) } else { resolve(x) } } catch (e) { if (thenCalledOrThrow) return thenCalledOrThrow = true return reject(e) } } else { resolve(x) }}Promise.prototype.then = function(onResolved, onRejected) { var self = this var promise2 onResolved = typeof onResolved === ‘function’ ? onResolved : function(v) { return v } onRejected = typeof onRejected === ‘function’ ? onRejected : function(r) { throw r } if (self.status === ‘resolved’) { return promise2 = new Promise(function(resolve, reject) { setTimeout(function() { // 异步执行onResolved try { var x = onResolved(self.data) resolvePromise(promise2, x, resolve, reject) } catch (reason) { reject(reason) } }) }) } if (self.status === ‘rejected’) { return promise2 = new Promise(function(resolve, reject) { setTimeout(function() { // 异步执行onRejected try { var x = onRejected(self.data) resolvePromise(promise2, x, resolve, reject) } catch (reason) { reject(reason) } }) }) } if (self.status === ‘pending’) { // 这里之所以没有异步执行,是因为这些函数必然会被resolve或reject调用,而resolve或reject函数里的内容已是异步执行,构造函数里的定义 return promise2 = new Promise(function(resolve, reject) { self.onResolvedCallback.push(function(value) { try { var x = onResolved(value) resolvePromise(promise2, x, resolve, reject) } catch (r) { reject(r) } }) self.onRejectedCallback.push(function(reason) { try { var x = onRejected(reason) resolvePromise(promise2, x, resolve, reject) } catch (r) { reject(r) } }) }) }}Promise.prototype.catch = function(onRejected) { return this.then(null, onRejected)}Promise.deferred = Promise.defer = function() { var dfd = {} dfd.promise = new Promise(function(resolve, reject) { dfd.resolve = resolve dfd.reject = reject }) return dfd}四、promise的常用方法是如何实现1、Promise.resolve / Promise.reject 实现// 原生的Promise.resolve使用Promise.resolve(‘hello swr’).then((data)=>{ // 直接把成功的值传递给下一个then console.log(data) // hello swr})// 那么Promise.resolve内部是怎么实现的呢?Promise.resolve = function(value){ return new Promise((resolve,reject)=>{ // 在内部new一个Promise对象 resolve(value) })}// 同理,Promise.reject内部也是类似实现的Promise.reject = function(reason){ return new Promise((resolve,reject)=>{ reject(reason) })}2、catch的实现// 原生Promise的catch使用Promise.reject(‘hello swr’).catch((e)=>{ console.log(e) // hello swr})// 上面这段代码相当于下面这段代码Promise.reject(‘hello swr’).then(null,(e)=>{ // then里直接走了失败的回调 console.log(e) // hello swr})// 内部实现Promise.prototype.catch = function(onRejected){ return this.then(null,onRejected) // 相当于then里的成功回调只传个null}3、promise.all的实现同时执行多个异步,并且返回一个新的promise,成功的值是一个数组,该数组的成员的顺序是传参给promise.all的顺序// 原生Promise.all的使用// 假设1.txt内容为hello 2.txt内容为swrlet fs = require(‘fs’)function read(filePath,encoding){ return new Promise((resolve,reject)=>{ fs.readFile(filePath,encoding,(err,data)=>{ if(err) reject(err) resolve(data) }) })}Promise.all([read(’./1.txt’,‘utf8’),read(’./2.txt’,‘utf8’)]).then((data)=>{ console.log(data) // 全部读取成功后返回 [‘hello’,‘swr’] // 需要注意的是,当其中某个失败的话,则会走失败的回调函数})promise.all内部实现Promise.all = function(promises){ // promises 是一个数组 return new Promise((resolve,reject)=>{ let arr = [] let i = 0 function processData(index,data){ arr[index] = data // 5.我们能用arr.length === promises.length来判断请求是否全部完成吗? // 答案是不行的,假设arr[2] = ‘hello swr’ // 那么打印这个arr,将是[empty × 2, “hello swr”], // 此时数组长度也是为3,而数组arr[0] arr[1]则为空 // 那么换成以下的办法 if(++i === promises.length){ // 6.利用i自增来判断是否都成功执行 resolve(arr) // 此时arr 为[‘hello’,‘swr’] } } for(let i = 0;i < promises.length;i++){ // 1.在此处遍历执行 promises[i].then((data)=>{ // 2.data是成功后返回的结果 processData(i,data) // 4.因为Promise.all最终返回的是一个数组成员按照顺序排序的数组 // 而且异步执行,返回并不一定按照顺序 // 所以需要传当前的i },reject) // 3.如果其中有一个失败的话,则调用reject } })}4、promise.race方法实现,同时执行多个异步,然后那个快,就用那个的结果,race是赛跑// 原生Promise.race的使用// 一个成功就走成功的回调,一个失败就走失败的回调Promise.race([read(’./1.txt’,‘utf8’),read(’./2.txt’,‘utf8’)]).then((data)=>{ console.log(data) // 可能返回 ‘hello’ 也可能返回 ‘swr’ 看哪个返回快就用哪个作为结果})// 内部实现Promise.race = function(promises){ // promises 是一个数组 return new Promise((resolve,reject)=>{ for(let i = 0;i < promises.length;i++){ promises[i].then(resolve,reject) // 和上面Promise.all有点类似 } })}5、promise.defer = promise.deferred这个语法糖怎么理解呢?这个语法糖可以简化一些操作,比如:let fs = require(‘fs’)// 写法一:function read(filePath,encoding){ // 这里的new Promise依然是传递了一个executor回调函数 // 我们该怎样减少回调函数嵌套呢? return new Promise((resolve,reject)=>{ fs.readFile(filePath,encoding,(err,data)=>{ if(err) reject(err) resolve(data) }) })}// 写法二:// 这样的写法减少了一层回调函数的嵌套function read(filePath,encoding){ let dfd = Promise.defer() fs.readFile(filePath,encoding,(err,data)=>{ if(err) dfd.reject(err) dfd.resolve(data) }) return dfd.promise}read(’./1.txt’,‘utf8’).then((data)=>{ console.log(data)})五、promise的链式调用promise的核心在于:链式调用。promise主要解决两个问题:(1)回调地狱(2)并发的异步IO操作,同一时间内把这个结果拿到,比如有两个异步io操作,当这2个获取完毕后,才执行相应的代码1、回调地狱怎么解决那么我们来看下面的代码,并且改为promise。// 回调函数let fs = require(‘fs’)fs.readFile(’./a.txt’,‘utf8’,(err,data)=>{ // 往fs.readFile方法传递了第三个为函数的参数 if(err){ console.log(err) return } console.log(data)})// 改写为Promiselet fs = require(‘fs’)function read(filePath,encoding){ return new Promise((resolve,reject)=>{ fs.readFile(filePath,encoding,(err,data)=>{ if(err) reject(err) resolve() }) })}read(’./a.txt’,‘utf8’).then((data)=>{ // 在这里则不再需要传回调函数进去,而是采用then来达到链式调用 console.log(data)},(err)=>{ console.log(err)})这样看好像Promise也没什么优势,那么接下来我们对比一下// 假设有3个文件// - 1.txt 文本内容为'2.txt’// - 2.txt 文本内容为'3.txt’// - 3.txt 文本内容为’hello swr’// 用回调函数fs.readFile(’./1.txt’,‘utf8’,(err,data)=>{ fs.readFile(data,‘utf8’,(err,data)=>{ fs.readFile(data,‘utf8’,(err,data)=>{ console.log(data) // hello swr }) })})// 用Promiseread(’./1.txt’,‘utf8’).then((data)=>{ // 1.如果一个promise执行完后,返回的还是一个promise, // 会把这个promise的执行结果会传递给下一次then中 return read(data,‘utf8’)}).then((data)=>{ return read(data,‘utf8’)}).then((data)=>{ // 2.如果在then中返回的不是一个promise, // 而是一个普通值,会将这个普通值作为下次then的成功的结果 return data.split(’’).reverse().join(’’)}).then((data)=>{ console.log(data) // rws olleh // 3.如果当前then中失败了,会走下一个then的失败回调 throw new Error(‘出错’)}).then(null,(err)=>{ console.log(err) // Error:出错 报错了 // 4.如果在then中不返回值,虽然没有显式返回, // 但是默认是返回undefined,是属于普通值,依然会把这个普通值传到 // 下一个then的成功回调中}).then((data)=>{ console.log(data) // undefined})从上面可以看得出,改写为Promise的代码,更好阅读和维护,从用Promise方式可以得出结论:(1)如果一个promise执行完后,返回的是一个promise,会将这个promise的执行结果传递给下一个then回调成功中;(2)如果在then中返回的不是一个promise,而是一个普通的值,会将这个普通的值传到下一个then成功回调中;(3)如果当时then中失败了,会走下一个then的回调失败;(4)如果then不返回值,但是默认是返回undefined的,属于普通值,会将这个普通值传到下一个then成功回调中。如果在then中抛出错误,会怎么样呢?情况1:会被下一个then中的失败回调捕获// 情景一,会被下一个then中的失败回调捕获read(’./1.txt’,‘utf8’).then((data)=>{ throw new Error(‘出错了’)}).then(null,(err)=>{ console.log(err) // Error:出错了 报错})情况2:没有被失败回调捕获,抛出错误最终会变成异常read(’./1.txt’,‘utf8’).then((data)=>{ throw new Error(‘出错了’)})情况3:如果没有被失败的回调捕获,那么最终会被catch捕获到read(’./1.txt’,‘utf8’).then((data)=>{ throw new Error(‘出错了’)}).then((data)=>{ }).catch((err)=>{ console.log(err) // Error:出错了 报错})情况4:如果被失败的回调捕获,那么就不会被catch捕获到read(’./1.txt’,‘utf8’).then((data)=>{ throw new Error(‘出错了’)}).then(null,(err)=>{ console.log(err) // Error:出错了 报错}).catch((err)=>{ console.log(err) // 不会执行到这里})(5)catch是错误没有处理的情况下才会执行(6)then回调中可以不写六、关于promise的其他问题1、性能问题可能各位看官会觉得奇怪,Promise能有什么性能问题呢?并没有大量的计算啊,几乎都是处理逻辑的代码。理论上说,不能叫做“性能问题”,而只是有可能出现的延迟问题。什么意思呢,记得刚刚我们说需要把4块代码包在setTimeout里吧,先考虑如下代码:var start = +new Date()function foo() { setTimeout(function() { console.log(‘setTimeout’) if((+new Date) - start < 1000) { foo() } })}foo()运行上面的代码,会打印出多少次’setTimeout’呢,各位可以自己试一下,不出意外的话,应该是250次左右,我刚刚运行了一次,是241次。这说明,上述代码中两次setTimeout运行的时间间隔约是4ms(另外,setInterval也是一样的),实事上,这正是浏览器两次Event Loop之间的时间间隔,相关标准各位可以自行查阅。另外,在Node中,这个时间间隔跟浏览器不一样,经过我的测试,是1ms。单单一个4ms的延迟可能在一般的web应用中并不会有什么问题,但是考虑极端情况,我们有20个Promise链式调用,加上代码运行的时间,那么这个链式调用的第一行代码跟最后一行代码的运行很可能会超过100ms,如果这之间没有对UI有任何更新的话,虽然本质上没有什么性能问题,但可能会造成一定的卡顿或者闪烁,虽然在web应用中这种情形并不常见,但是在Node应用中,确实是有可能出现这样的case的,所以一个能够应用于生产环境的实现有必要把这个延迟消除掉。在Node中,我们可以调用process.nextTick或者setImmediate(Q就是这么做的),在浏览器中具体如何做,已经超出了本文的讨论范围,总的来说,就是我们需要实现一个函数,行为跟setTimeout一样,但它需要异步且尽早的调用所有已经加入队列的函数,这里有一个实现。2、如何停止一个promise链?在一些场景里,我们会遇到一个较长的promise的链式调用,在某一步出现的错误让我们没有必要去运行链式调用后面所有的代码,类似于下面这样的(此处省略then/catch里的函数):new Promise(function(resolve, reject) { resolve(42)}) .then(function(value) { // “Big ERROR!!!” }) .catch() .then() .then() .catch() .then()假设这个Big ERROR!!!的出现让我们完全没有必要运行后面所有的代码了,但链式调用的后面即有catch,也有then,无论我们是return还是throw,都不可避免的会进入某一个catch或then里面,那有没有办法让这个链式调用在Big ERROR!!!的后面就停掉,完全不去执行链式调用后面所有回调函数呢?从一个实现者的角度看问题时,确实找到了答案,就是在发生Big ERROR后return一个Promise,但这个Promise的executor函数什么也不做,这就意味着这个Promise将永远处于pending状态,由于then返回的Promise会直接取这个永远处于pending状态的Promise的状态,于是返回的这个Promise也将一直处于pending状态,后面的代码也就一直不会执行了,具体代码如下:new Promise(function(resolve, reject) { resolve(42)}) .then(function(value) { // “Big ERROR!!!” return new Promise(function(){}) }) .catch() .then() .then() .catch() .then()这种方式看起来有些山寨,它也确实解决了问题。但它引入的一个新问题就是链式调用后面的所有回调函数都无法被垃圾回收器回收(在一个靠谱的实现里,Promise应该在执行完所有回调后删除对所有回调函数的引用以让它们能被回收,在前文的实现里,为了减少复杂度,并没有做这种处理),但如果我们不使用匿名函数,而是使用函数定义或者函数变量的话,在需要多次执行的Promise链中,这些函数也都只有一份在内存中,不被回收也是可以接受的。将返回一个什么也不做的Promise封装成一个有语义的函数,以增加代码的可读性Promise.cancel = Promise.stop = function() { return new Promise(function(){})}这么使用了:new Promise(function(resolve, reject) { resolve(42)}) .then(function(value) { // “Big ERROR!!!” return Promise.stop() }) .catch() .then() .then() .catch() .then()3、promise的链上返回的最后一个promise出错了怎么办?new Promise(function(resolve) { resolve(42)}) .then(function(value) { alter(value) })但运行这段代码的话你会发现什么现象也不会发生,既不会alert出42,也不会在控制台报错,怎么回事呢。细看最后一行,alert被打成了alter,那为什么控制台也没有报错呢,因为alter所在的函数是被包在try/catch块里的,alter这个变量找不到就直接抛错了,这个错就正好成了then返回的Promise的rejection reason。解决办法:(1)所有的promise链的最后都加上一个catch,这样出错后就会被捕获到,这样违背了DRY原则,而且繁琐;(2)借鉴Q的一个方法done,把这个方法加到promise链的最后,就能够处理捕获最后一个promise出现的错误,其实个catch的思路一样,这个是框架来实现的。(3)在一个Promise被reject的时候检查这个Promise的onRejectedCallback数组,如果它为空,则说明它的错误将没有函数处理,这个时候,我们需要把错误输出到控制台,让开发者可以发现。在Promise被reject但又没有callback时,把错误输出到控制台。4、出错时,使用throw new Error()还是使用return Promise.reject(new Error())呢?从性能和编码的舒适角度考虑:(1)性能方面:throw new Error()会使代码进入catch块里的逻辑(我们把多有的回调都包在try/catch里),传说throw多了会影响性能,因为一旦throw,代码就有可能跳转到不可预知的位置。而使用promise.reject(new Error()),则需要构造一个新的promise对象(包含2个数组,4个函数:resolve/reject,onResolved/onRejected),也会花费一定的时间和内存。因为onResolved/onRejected函数是直接被包在promise实现里的try里,出错后直接进入到这个try对应的catch块,代码的跳跃幅度相对较小,性能应该可以忽略不记。(2)编码的舒适度方面:出错用throw,正常用return,正常可以明显的区分出错和正常综上觉得还是promise里发现显式错误后,用throw抛出错误比较好,而不是显式的构造一个呗reject的promise对象。七、实践注意:1、不要把promise写成嵌套的结构// 错误的写法promise1.then(function(value) { promise1.then(function(value) { promise1.then(function(value) { }) })})2、链式promise要返回一个promise,而不是构造一个promise// 错误的写法Promise.resolve(1).then(function(){ Promise.resolve(2)}).then(function(){ Promise.resolve(3)})八、参考https://github.com/xieranmaya… ...

March 26, 2019 · 6 min · jiezi

ES6系列—新的变量声明方式

在ES5中,变量声明只有var和function以及隐式声明三种,在ES6中则增加了let、const、import和class四种。1. let1.1 块级作用域let声明的变量的作用域是块级作用域(这个特性有点类似于后台语言),ES5 并没有块级作用域,只有函数作用域和全局作用域。{ let a = ‘ES6’; var b = ‘ES5’;}console.log(b) // ES5 console.log(a) // ReferenceError: a is not defined.那么let的块级作用域有什么好处呢?let非常适合用于 for循环内部的块级作用域。JS中的for循环体比较特殊,每次执行都是一个全新的独立的块作用域,用let声明的变量传入到 for循环体的作用域后,不会发生改变,不受外界的影响。看一个常见的面试题目:for (var i = 0; i <10; i++) { setTimeout(function() { // 同步注册回调函数到异步的宏任务队列。 console.log(i); // 执行此代码时,同步代码for循环已经执行完成 }, 0);}// 输出结果10 (共10个)// 这里变量为i的for循环中,i是一个全局变量,在全局范围内都有效,所以每一次循环,新的i值都会覆盖旧值,导致最后输出的是最后一轮i的值,即i的最终结果为10,实际上都是console.log(10)。涉及的知识点:JS的事件循环机制,setTimeout的机制等把 var改成 let声明:for (let i = 0; i < 10; i++) { setTimeout(function() { console.log(i); //当前的i仅在本轮的循环中有效,就是说每一次循环,i其实都是一个新产生的变量。 //用 let 声明的变量 i 只作用于循环体内部,不受外界干扰。 }, 0);}// 输出结果:0 1 2 3 4 5 6 7 8 91.2 暂时性死区(Temporal Dead Zone)在一个块级作用域中,变量唯一存在,一旦在块级作用域中用let声明了一个变量,那么这个变量就唯一属于这个块级作用域,不受外部变量的影响,如下面所示。var tmp = ‘bread and dream’;if(true){ tmp = ‘dream or bread’; //ReferenceError let tmp;}这个例子中tmp = ‘dream or bread’的赋值会报错,因为在if块中的let对tmp变量进行了声明,导致该tmp绑定了这个作用域,而let临时死区导致了并不能在声明前使用,所以在声明前对变量赋值会报错。暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。暂时性死区的意义也是让我们标准化代码,将所有变量的声明放在作用域的最开始。1.3 不允许重复声明(1) 在相同的作用域内,用let声明变量时,只允许声明一遍,但 var是可以多次声明的。大家都知道ES5 多次声明会造成变量覆盖而且不会报错,这就给调试增加了难度,而let能够直接扼杀这个问题在摇篮之中,因为会直接报错。// 不报错function demo() { var a = ‘bread and dream’; var a = ‘dream or bread’;} // 报错,Duplicate declaration “a"function demo() { let a = ‘bread and dream’; var a = ‘dream or bread’;}// 报错,Duplicate declaration “a"function demo() { let a = ‘bread and dream’; let a = ‘dream or bread’;}(2) 不能在函数内部重新声明参数:function demo1(arg) { let arg; // 报错}demo1()function demo2(arg) { { let arg; // 不报错 }}demo2()2. const2.1 用于声明常量const声明的常量是不允许改变的,只读属性,这意味常量声明时必须同时赋值, 只声明不赋值,就会报错,通常常量以大写字母命名。const Person; // 错误,必须初始化 const Person = ‘bread and dream’;// 正确const Person2 = ’no’; Person2 = ‘dream or bread’; //报错,不能重新赋值这样做的两个好处:一是阅读代码的人立刻会意识到不应该修改这个值,二是防止了无意间修改变量值所导致的错误。比如我们使用nodejs的一些模块的时候,我们只是使用对应的模块(如http模块),但是并不需要修改nodejs的模块,这个时候就可以声明成const,增加了代码的可读性和避免错误。2.2 支持块级作用域const和let类似,也是支持块级作用域.if (true) { const MIN = 5;}MIN // Uncaught ReferenceError: MIN is not defined2.3 不支持变量提升,有暂时性死区const声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。if (true) { console.log(MIN); // ReferenceError const MIN = 5;}2.4 特殊情况如果声明的常量是一个对象,那么对于对象本身是不允许重新赋值的,但是对于对象的属性是可以赋值的。const obj = {};obj.a = ‘xiao hua’;console.log(obj.a); //‘xiao hua’实际上const能保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针。至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。如果要彻底将对象冻结(不可修改其属性),应该使用Object.freeze(obj)方法。同理,数组也是一样的。3. importES6采用import来代替node等的require来导入模块。import {$} from ‘./jquery.js’ $对象就是jquery中export暴露的对象。如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。import { JQ as $ } from ‘./jquery.js’;注意,import命令具有提升效果,会提升到整个模块的头部,首先执行。4. classES6引入了类的概念,有了class这个关键字。类的实质还是函数对象。先定义一个类://定义类class Animal { constructor(name, age) { this.name = name; this.age = age; } setSex(_sex) { this.sex=_sex; }} constructor方法,就是构造方法,也就是ES5时代函数对象的主体,而this关键字则代表实例对象。上面的类也可以改成ES5的写法:function Animal(name, age){ this.name = name; this.age = age;}Animal.prototype.setSex = function (_sex) { this.sex=_sex;}其实,大多数类的特性都可以通过之前的函数对象与原型来推导。生成类的实例对象的写法,与ES5通过构造函数生成对象完全一样,也是使用new命令。class Animal {}let dog = new Animal();在类的实例上面调用方法,其实就是调用原型上的方法,因为类上的方法其实都是添加在原型上。Class其实就是一个function,但是有一点不同,Class不存在变量提升,也就是说Class声明定义必须在使用之前。5.总结 在ES6之前,JavaScript是没有块级作用域的,如果在块内使用var声明一个变量,它在代码块外面仍旧是可见的。ES6规范给开发者带来了块级作用域,let和const都添加了块级作用域,使得JS更严谨和规范。let 与 const 相同点:块级作用域有暂时性死区 约束了变量提升 禁止重复声明变量 let 与 const不同点:const声明的变量不能重新赋值,也是由于这个规则,const变量声明时必须初始化,不能留到以后赋值。合理的使用ES6新的声明方式,不管是面试还是工作中都有实际的应用,尤其是工作中,大家一定要尽量的多使用新的声明方式,不但可以让代码更规范,更可以避免不必要的bug,浪费调试时间,进而提高工作效率。 ...

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

js之数组克隆

js主要分基本数据类型及引用数据类型两大类基本数据类型包括:number,string,undefine,null,boolean,Symbol(es6新增)引用数据类型:Object,Array,Function,Data等注意:基本数据类型放在栈空间内,并且是按值存放,可以直接读取和操作。 引用数据类型存放在堆空间内(门),变量的值其实是指向堆空间的地址(钥匙),因此如果克隆这个变量,相当于复制钥匙。let arr = [1,2,3,4,5]let arr1 = arr // 这一步相当于把arr栈空间的地址赋给了arr1,其实arr和arr1操作的是同一个堆空间的对象arr1.push(6) // arr1 = [1,2,3,4,5,6]console.log(arr) //[1,2,3,4,5,6]因此对于引用类型的拷贝,需要拷贝堆空间的对象数组浅拷贝1.运用数组slice与concat方法返回一个新数组的特性let arr = [1,2,3,4,5]let arr1 = arr.slice() //[1,2,3,4,5]let arr2 = arr.concat() //[1,2,3,4,5]2.简单粗暴的方法-遍历let arr = [1,2,3,4,5]let arr2 = []arr.forEach(item=>{ arr2.push(item) })console.log(arr2)3.es6新增方法-拓展运算符let arr = [1,2,3,4,5]let arr1 = […arr] //[1,2,3,4,5]4.es6新增方法-Object.assignlet arr = [1,2,3,4,5]let arr1 = []Object.assign(arr1,arr)console.log(arr1) //[1,2,3.4,5]如果数组里嵌套数组和对象,浅拷贝只会拷贝该数组或者对象存放在栈空间的地址,因此无论在新旧数组中改变此地址指向的对象,两个数组都会发生改变。 因此我们需要深拷贝来拷贝此类数组。数组深拷贝1.普通遍历,遍历到引用类型时候进行引用类型的拷贝let arr = [1,2,3,4,5,{name:‘bob’},[‘a’,‘b’]] function clone (arr) { let arr1 = [] arr.forEach(item=>{ //如果不是object,将该值插入到新数组 if(typeof(item) !== ‘object’) { arr1.push(item) } else { //根据遍历的对象新建一个相同类型的空对象 let obj = item instanceof Array ? [] : {} for(var key in item){ if(item.hasOwnProperty(key)){ obj[key] = item[key] } } arr1.push(obj) } }) return arr1 } let arr1 = clone(arr) arr1[5].name = ‘js’ console.log(arr,‘arr’,arr1,‘arr1’)2.简单粗暴(能拷贝数组和对象,但不能拷贝函数)let arr = [1,2,3,4,5,{name:‘bob’},[‘a’,‘b’]]let arr1 = JSON.parse(JSON.stringify(arr)) ...

March 19, 2019 · 1 min · jiezi

一篇文章彻底搞懂JS深浅拷贝和const

首先要了解的js基础基本数据类型:undefined、null、Boolean、Number、String、Symbol (ES6新加) 引用类型: Object 、Array 、Date 、RegExp 、Function 两者的重要区别在于:基本类型赋值给变量,变量的标识符和变量的值都存放在内存中的栈(Stack)里。引用类型的变量的标识符在栈中,变量的值在内存的堆(Heap)中。举一个通俗的例子:基本类型是你在内存的栈中拥有一个咖啡店和钥匙,引用类型是你只有咖啡店的钥匙,你可以去内存的堆中找到对应的咖啡店地址,钥匙环上有一个标签上面写了,这个钥匙是对应的哪一家店,而这个标签就是指针。图:数据类型图:引用类型深浅拷贝问题不知道什么是深拷贝和浅拷贝的请先去Google并在Chrome调试台自己操作一下,这篇文章只会说明为何JS中会有这种问题。我举个栗子出现这种结果的原因是第二步将a赋给b只是将a的地址给了b(请注意,数组是引用类型),此时改变b其实就是改变了堆中a和b共同指向的地址的值。通俗一点讲:a和b两个人都拿到了一个同一家咖啡店的钥匙,这时候在咖啡店多放一个杯子,自然两人共同的咖啡店都有这个杯子。所以有的时候我们为了避免浅拷贝,会用一些方式实现深拷贝。关于ES6里的const,有些后台人员甚至前端人员误以为const赋值是常量,其实const并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址)(此段引用自阮一峰)const只是把钥匙上的标签(指针)固定,所以:小白第一次写技术文章,如果有错误还请多多指教,多谢!参考资料[深入了解JS引用类型基本类型][5][阮一峰ES6教程][6]

March 19, 2019 · 1 min · jiezi

ES6的基础知识(二)

demo地址函数默认参数function Person(name = “xiaohong”, age = 18) { this.name = name; this.age = age;}let person1 = new Person();console.log(person1.name); // xiaohong省略参数function test(…arg) { let res = arg.reduce(function(sum, item) { return sum + item; }); console.log(res); // 6}test(1, 2, 3);箭头函数let sum = (a, b) => { return a + b;};console.log(sum(1, 2)); // 3// 单个参数可省略小括号,直接返回可省略函数大括号let double = num => num * 2;console.log(double(2)); // 4// 箭头函数没有this,内部的this就是上层代码块的thislet obj = { name: “xiaohong”, say: function() { // 指向上层代码块obj setTimeout(() => { console.log(this.name); // xiaohong }); // 指向window setTimeout(function(){ console.log(this.name); // undefined }); }};obj.say();let fn = () => { console.log(this); // window};fn();Array.from()将类数组转成数组function sum() { return Array.from(arguments).reduce((val, item) => { return val + item; });}console.log(sum(1, 2, 4)); // 7fill填充数组let arr = [, ,];console.log(arr.fill(1)); // [1, 1]find找到数组中第一个满足条件的元素let arr = [1, 2, 3];let item = arr.find(item => { return item > 1;});console.log(item); // 2findIndex找到数组中第一个满足条件的元素索引let arr = [1, 2, 3];let index = arr.findIndex(item => { return item > 1;});console.log(index); // 1some数组中是否存在满足条件的元素let arr = [1, 2, 3];let flag = arr.some(item => { return item >= 3;});console.log(flag); // trueevery数组中是否所有元素都满足条件let arr = [1, 2, 3];let flag = arr.every(item => { return item >= 3;});console.log(flag); // false对象对象的属性和方法可简写let name = ‘xiaohong’;let obj = { // 当key值和变量一致时可省略 name, // 方法可简写成这样 sayName(){}}对象的继承 let obj = { name:‘xiaohong’, sayName(){ console.log(this.name) } }; let newObj = { name:‘xiaoli’ }; Object.setPrototypeOf(newObj, obj); newObj.sayName(); // xiaoliclass类 // 声明类,只能通过new生成对象,不能直接使用 class Parent { // 构造函数 constructor(name) { // 实例私有属性 this.name = name; } // 静态属性 不需要通过实例调用,类直接用 static hello() { return “hello”; } // 实例公共方法 sayName() { return this.name; } } // extends继承 class Child extends Parent { constructor(name) { // super 父级的构造函数 super(name); } } let p = new Child(“xiaoli”); console.log(p.name); // xiaoli console.log(p.sayName()); // xiaoli ...

March 19, 2019 · 2 min · jiezi

es6系列二:你真的会声明变量吗(var,let和const)

ES6新增了二个声明变量的关键字,let和const,再加上ES6之前的var,这样声明变量就有三个关键字了,大有三国鼎立之势。那到底用哪个来声明变量呢?var首先,得说说var的特殊行为,变量提升,来看一个例子:var condition = falseif (condition) { var value = ‘red’ console.log(value)} else { // value 在这里可以访问,值为undefined console.log(value)}在上面代码中,即使condition为false,变量value也是存在的,相当于如下定义:var condition = falsevar valueif (condition) { value = ‘red’ console.log(value)} else { // value 在这里可以访问,值为undefined console.log(value)}就是说,使用var关键字声明的变量,无论其实际声明位置在何处,都会被视为声明于所在函数的顶部(如果声明不在函数内,则视为在全局作用域的顶部),这就是变量提升。对这种特殊行为,如果你不理解,就很可能导致bug。我们来看一个循环的例子:for (var i = 0; i < 6; i++) { console.log(i)}// i 在这里依旧可以访问console.log(i) // 6for循环完了,已经不再需要变量i,但是它依旧可以被访问。所以,ES6引入了块级作用域,让变量的生命周期更加可控,于是,let出现了。块级声明-let我们来看看用let声明的代码:var condition = falseif (condition) { let value = ‘red’ console.log(value)} else { // value 在这里不可以访问,会报错 console.log(value)}由于变量value声明使用的是let,所以就没有被提升到函数定义的顶部,变量value在if代码块外部是无法访问的。当condition的值为false 时,该变量不会被声明并初始化。如果上面的那个for循环中用let声明变量i,那么循环完了,变量i也就随时销毁,不能再被访问。常量声明-const使用const声明的变量会被认为是常量(constant),意味着它们的值在被设置完成后就不能再被改变,常量声明与let声明一样,都是块级声明。来看个小例子:const num = 12num = 13 // 报错const obj = { name: ‘moddx’, age: 28 };obj.age = 26 // 正常上面用const声明了一个变量num,当重新给其赋值时会报错,而变量obj初始化之后,再将其age属性改变,不会报错,因为对象是一个引用类型,其指向的内存中的地址是没有被改变的,除非将其重新赋值给一个新对象,就会报错,如下:const obj = { name: ‘moddx’, age: 28 };obj = { name: ‘foo’ } // 报错总结说了这么多,那到底时候用什么关键字来声明呢?就没有一个标准或者约定俗成的习惯吗?目前,被广泛认可的变量声明方式是:默认情况下应当使用const,当你确定声明的变量需要改变时,用let声明。其依据是大部分变量在初始化之后都不应当被修改,因为预期外的改动是bug的源头之一。(那var呢?好吧,基本被抛弃了) ...

March 18, 2019 · 1 min · jiezi

Javascript处理数组基础知识(含es6新语法)

迭代方法1.every()对数组中每一项进行给定函数,如果每一项都返回true,则返回true。every(callback(element,index,array), thisArg)callback:必需,对数组每一项所要执行的函数。thisArg:可选,规定callback回调函数this所指向的对象。var numbers = [1,2,3,4,5,4,3,2,1];var everyResult = numbers.every(function(item,index,array){ return item>2;});console.log(everyResult); //falsevar numbers = [1,2,3,4,5,4,3,2,1];var obj = {num:2}var everyResult = numbers.every(function(item,index,array){ return item>this.num;},obj);console.log(everyResult); //false特别说明:第一个参数是回调函数,第二个参数是一个对象,它可以重置回调函数中this的指向。那么,上述代码中回调函数的this就指向obj,于是this.num值等于2。2.some()对数组中每一项进行给定函数,如果任意一项都返回true,则返回truesome(callback(element,index,array), thisArg)callback:必需,对数组每一项所要执行的函数。thisArg:可选,规定callback回调函数this所指向的对象。var numbers = [1,2,3,4,5,4,3,2,1];var someResult = numbers.some(function(item,index,array){ return item>2;});console.log(someResult); //truevar numbers = [1,2,3,4,5,4,3,2,1];var obj = {num:2}var someResult = numbers.some(function(item,index,array){ return item>this.num;},obj);console.log(someResult); //true3.filter()对数组中每一项进行给定函数,返回该函数会返回true的项组成新的数组。filter(callback(element,index,array), thisArg)callback:必需,对数组每一项所要执行的函数。thisArg:可选,规定callback回调函数this所指向的对象。var numbers = [1,2,3,4,5,4,3,2,1];var filterResult = numbers.filter(function(item,index,array){ return item>2;});console.log(filterResult); // [3,4,5,4,3]var numbers = [1,2,3,4,5,4,3,2,1];var obj = {num:2}var filterResult = numbers.filter(function(item,index,array){ return item>this.num;},obj);console.log(filterResult); // [3,4,5,4,3]4.map()对数组中每一项进行给定函数,返回每次函数调用的结果组成的新数组。map(callback(element,index,array), thisArg)callback:必需,对数组每一项所要执行的函数。thisArg:可选,规定callback回调函数this所指向的对象。var numbers = [1,2,3,4,5,4,3,2,1];var mapResult = numbers.map(function(item,index,array){ return item2;});alert(mapResult); // [2, 4, 6, 8, 10, 8, 6, 4, 2]var numbers = [1,2,3,4,5,4,3,2,1];var obj = {num:2}var mapResult = numbers.map(function(item,index,array){ return itemthis.num;},obj);alert(mapResult); // [2, 4, 6, 8, 10, 8, 6, 4, 2]5.forEach()对数组中每一项进行给定函数,没有返回值,和for循环类似。forEach(callback(element,index,array), thisArg)callback:必需,对数组每一项所要执行的函数。thisArg:可选,规定callback回调函数this所指向的对象。var numbers = [1,2,3,4,5,4,3,2,1];numbers.forEach(function(item,index,array){ console.log(item2)});//2, 4, 6, 8, 10, 8, 6, 4, 2console.log(numbers) //1,2,3,4,5,4,3,2,1var numbers = [1,2,3,4,5,4,3,2,1];var obj = {num:2}numbers.forEach(function(item,index,array){ console.log(itemthis.num)},obj);//2, 4, 6, 8, 10, 8, 6, 4, 2归并方法ES5新增了两个归并数组的方法:reduce(callback(prev,cur,index,array))和reduceRight(callback(prev,cur,index,array))。这两个方法迭代数组所有项,然后构建一个最终返回的值。reduce从左到右,reduceRight从右到左。prev:前一个值cur:当前值index:下标array:数组对象var numbers = [1,2,3,4,5];var sum = numbers.reduce(function(prev,cur,index,array){ return prev + cur;});console.log(sum); //15这个函数返回的任何值都会作为第一个参数自动传给下一项。第一次迭代发生在第二项上,因此第一个参数是数组的第一项,第二个参数是数组的第二项。检测数组1.instanceofES3的方法var numbers = [1,2,3];if(numbers instanceof Array){ //对数组进行某些操作}2.Array.isArrayES5的方法var numbers = [1,2,3];if(Array.isArray(numbers)){ //对数组进行某些操作}数组转化成字符串1.toLocaleString() toString()var numbers = [1,2,3];console.log(numbers.toLocaleString()) //1,2,3console.log(numbers.toString())//1,2,32.join()将数组转换为字符串,且用分隔符分割var numbers = [1,2,3];alert(numbers.join("|")); // 1|2|3栈方法栈方法是指Last-In-First-Out后进先出push() 从数组末尾添加pop() 从数组末尾移除队列方法队列方法是First-In-First-Out先进先出shift() 从数组前端移除unshift() 从数组前端添加重排序方法1.reverse()反转数组var numbers = [1,2,3];console.log(numbers.reverse()) //3,2,12.sort()var numbers = [0,1,5,10,15];numbers.sort(function(a,b){ return b-a;});console.log(numbers); //[15, 10, 5, 1, 0]操作方法1.concat()用于复制或者从尾部添加,创建新数组。concat(n1,n2,…,nN)n1,n2,…,nN:都可选,既可以是数组,也可以是单个字符。var numbers = [1,2,3];var n1 = values.concat(); //复制var n2 = values.concat(4); //尾部添加var n3 = n1.concat(n2,10,11); //尾部添加console.log(numbers); //[1,2,3]console.log(n1); //[1,2,3]console.log(n2); //[1,2,3,4]console.log(n3); //[1, 2, 3, 1, 2, 3, 4,10,11]2.slice()用于复制或截取数组,创建新数组。slice(start,end)strat:可选,数组其实位置,截取的时候包括其实位置end:可选,结束位置,截取的时候不包括结束位置var numbers = [1,2,3];var n1 = numbers.slice(); //复制var n2 = numbers.slice(1); //截取var n3 = numbers.slice(1,3); //截取console.log(numbers); //[1,2,3]console.log(n1); //[1,2,3]console.log(n2); //[2,3]console.log(n3); //[2,3]3.splice()用于删除、插入、替换,号称最强大的数组方法删除:可以删除任意数量的项,需要两个参数,要删除的第一项的位置和要删除的项数splice(start,num)var values = [1,2,3,4,5,6];var v = values.splice(0,2);console.log(values); //[3,4,5,6]console.log(v); //[1,2]插入和替换:至少三个参数,第一个是起始位置,第二个是要删除的项数,第三个及以后是要插入或替换的值。splice(start,num,n1,n2,…,nN)//插入demovar values = [1,2,3,4,5,6];var v1 = values.splice(1,0,1,1,1);console.log(values); //[1,1,1,1,2,3,4,5,6]console.log(v1); //[]//替换demovar values = [1,2,3,4,5,6];var v1 = values.splice(1,2,1,1,1);console.log(values); //[1,1,1,1,4,5,6]console.log(v1); //[2,3]位置方法indexOf(value,start)lastIndexOf(value,start)都接受两个参数:查找的值、查找起始位置。不存在,返回 -1 ;存在,返回位置。indexOf 是从前往后查找, lastIndexOf 是从后往前查找。var numbers = [1,2,3,4,5,6,7,8,9,5,3];console.log(numbers.indexOf(5)) //4console.log(numbers.lastIndexOf(5)) //9ES6新增新操作数组的方法1.find()传入一个回调函数,找到数组中符合当前搜索规则的第一个元素,返回它,并且终止搜索。find(callback(element,index,array), thisArg)callback:必需,回调函数,它制定了对数组元素的检索规则。thisArg:可选,用来规定回调函数中this所指向的对象。let arr = [2,3,4,5,6,7,8,9,10];let elem=arr.find(function (ele) { if (ele > 4) { return true }})console.log(elem); //5let arr = [2,3,4,5,6,7,8,9,10];let obj = {num: 4};let elem=arr.find(function (ele) { if (ele > this.num) { return true }},obj)console.log(elem); //52.findIndex()传入一个回调函数,找到数组中符合当前搜索规则的第一个元素,返回它的下标,终止搜索。findIndex(callback(element,index,array), thisArg)callback:必需,回调函数,它制定了对数组元素的检索规则。thisArg:可选,用来规定回调函数中this所指向的对象。let arr = [2,3,4,5,6,7,8,9,10];let elem=arr.findIndex(function (ele) { if (ele > 4) { return true }})console.log(elem); //3let arr = [2,3,4,5,6,7,8,9,10];let obj = {num: 4};let elem=arr.findIndex(function (ele) { if (ele > this.num) { return true }},obj)console.log(elem); //33.fill()用新元素替换掉数组内的元素,可以指定替换下标范围。fill(value,start,end)value:必需,用来进行填充的值。start:可选,规定填充开始位置,默认从索引0处开始。end:可选,规定填充结束位置,默认填充到数组结尾。const arr = [‘a’, ‘b’, ‘c’, ’d’]console.log(arr.fill(‘ss’, 1, 3)) // [“a”, “ss”, “ss”, “d”]const arr2 = [‘a’, ‘b’, ‘c’, ’d’]console.log(arr.fill(‘ss’)) // [“ss”, “ss”, “ss”, “ss”]4.includes()判断数组中是否存在该元素,可以替换 ES5 时代的 indexOf 判断方式。includes(value,start)value:必需,要检测的元素。start:可选,规定填充开始位置,默认从索引0处开始。let arr = [‘a’, ‘b’,‘c’, ’d’, NaN]arr.includes(‘a’) //truearr.includes(NaN) //truearr.indexOf(NaN) //-15.Array.from()方法可以将指定的参数转换为真正的数组。当然并不是任意参数都是可以被转换为数组,可以转换的参数如下:类数组对象、具有遍历器接口的对象。Array.from(arrayLike, mapFn, thisArg)arrayLike:必需,将要被转换为真正数组的类数组或者具有遍历器接口的对象。mapFn:可选,对参数arrayLike中的数据进行处理并返回,作为新数组的元素。thisArg:可选,规定mapFn的调用对象,那么mapFn中的this将会指向此调用对象。let obj = { “0”: “蚂蚁部落”, “1”: “www.softwhy.com”, “2”: 6, “3”:“ES2015”, length:4};console.log(Array.from(obj)); // [“蚂蚁部落”, “www.softwhy.com”, 6, “ES2015”]obj = { “a”: “蚂蚁部落”, “b”: “www.softwhy.com”, “c”: 6, “d”:“ES2015”, length:4};console.log(Array.from(obj)); //[undefined, undefined, undefined, undefined]obj = { length:3};console.log(Array.from(obj)); //[undefined, undefined, undefined]上述代码将类数组对象转换为真正的数组,什么是类数组对象:很多材料上对类数组的描述比较严苛,其实很简单,只要一个对象具有length属性就是类数组。当然一个类数组对象,如果属性恰好类似于数组递增式索引,那就更好了,比如上面代码中的对象。上面对象只有一个length属性,他也是一个类数组对象,只不过生成的数组元素都是undefined。let str = “蚂蚁部落”;console.log(Array.from(str)); // [“蚂”, “蚁”, “部”, “落”]因为字符串具有遍历器接口,那么字符串也可以生成数组,数组的每一个元素,就是构成字符串的字符。let obj = { “0”: 1, “1”: 2, “2”: 3, “3”: 4, length:4};let thisObj = { num:2}console.log(Array.from(obj, function (elem, index) { return elem * this.num}, thisObj)); //[2, 4, 6, 8]扩展:展开运算符生成数组let str=“蚂蚁部落”;console.log([…str]); //[“蚂”, “蚁”, “部”, “落”]6.Array.of()Array.of(element0,element1,…,elementN)此方法可以接受任意类型的参数,参数之间用逗号分隔,并且能够将参数转换为数组。console.log(Array.of(2)); //[2]console.log(Array.of(2,3,4)); //[2,3,4]扩展:Array()与Array.of()方法区别console.log(Array(2)) // [undefined, undefined]console.log(Array.of(2)) // [2]console.log(Array(2,3,4)) // [2,3,4]console.log(Array.of(2,3,4)) // [2,3,4]这个方法的主要目的,是弥补数组构造函数 Array() 的不足。7.entries()返回迭代器:返回一个遍历器对象【键值对】//数组const arr = [‘a’, ‘b’, ‘c’];for(let v of arr.entries()) { console.log(v)}// [0, ‘a’] [1, ‘b’] [2, ‘c’]for(let [key, value] of arr.entries()) { console.log(key+’–’+value)}//0–a 1–b 2–c//Setconst arr = new Set([‘a’, ‘b’, ‘c’]);for(let v of arr.entries()) { console.log(v)}// [‘a’, ‘a’] [‘b’, ‘b’] [‘c’, ‘c’]//Mapconst arr = new Map();arr.set(‘a’, ‘a’);arr.set(‘b’, ‘b’);for(let v of arr.entries()) { console.log(v)}// [‘a’, ‘a’] [‘b’, ‘b’]8.keys()返回迭代器:返回键值对的key//数组const arr = [‘a’, ‘b’, ‘c’];for(let v of arr.values()) { console.log(v)}//‘a’ ‘b’ ‘c’//Setconst arr = new Set([‘a’, ‘b’, ‘c’]);for(let v of arr.values()) { console.log(v)}// ‘a’ ‘b’ ‘c’//Mapconst arr = new Map();arr.set(‘a’, ‘a’);arr.set(‘b’, ‘b’);for(let v of arr.values()) { console.log(v)}9.values()返回迭代器:返回键值对的value//数组const arr = [‘a’, ‘b’, ‘c’];for(let v of arr.keys()) { console.log(v)}// 0 1 2//Setconst arr = new Set([‘a’, ‘b’, ‘c’]);for(let v of arr.keys()) { console.log(v)}// ‘a’ ‘b’ ‘c’//Mapconst arr = new Map();arr.set(‘a’, ‘a’);arr.set(‘b’, ‘b’);for(let v of arr.keys()) { console.log(v)}// ‘a’ ‘b’ ...

March 18, 2019 · 3 min · jiezi

JavaScript 为什么要有 Symbol 类型

Symbols 是 ES6 引入了一个新的数据类型 ,它为 JS 带来了一些好处,尤其是对象属性时。 但是,它们能为我们做些字符串不能做的事情呢?在深入探讨 Symbol 之前,让我们先看看一些 JavaScript 特性,许多开发人员可能不知道这些特性。背景js中的数据类型总体来说分为两种,他们分别是:值类型 和 引用类型值类型(基本类型):数值型(Number),字符类型(String),布尔值型(Boolean),null 和 underfined引用类型(类):函数,对象,数组等值类型理解:变量之间的互相赋值,是指开辟一块新的内存空间,将变量值赋给新变量保存到新开辟的内存里面;之后两个变量的值变动互不影响,例如:var a=10; //开辟一块内存空间保存变量a的值“10”;var b=a; //给变量 b 开辟一块新的内存空间,将 a 的值 “10” 赋值一份保存到新的内存里;//a 和 b 的值以后无论如何变化,都不会影响到对方的值;一些语言,比如 C,有引用传递和值传递的概念。JavaScript 也有类似的概念,它是根据传递的数据类型推断的。如果将值传递给函数,则重新分配该值不会修改调用位置中的值。但是,如果你修改的是引用类型,那么修改后的值也将在调用它的地方被修改。引用类型理解:变量之间的互相赋值,只是指针的交换,而并非将对象(普通对象,函数对象,数组对象)复制一份给新的变量,对象依然还是只有一个,只是多了一个指引~~;例如:var a={x:1,y:2} //需要开辟内存空间保存对象,变量 a 的值是一个地址,这个地址指向保存对象的空间;var b=a; // 将a 的指引地址赋值给 b,而并非复制一给对象且新开一块内存空间来保存;// 这个时候通过 a 来修改对象的属性,则通过 b 来查看属性时对象属性已经发生改变;值类型(神秘的 NaN 值除外)将始终与具有相同值的另一个值类型的完全相等,如下:const first = “abc” + “def”;const second = “ab” + “cd” + “ef”;console.log(first === second); // true但是完全相同结构的引用类型是不相等的:const obj1 = { name: “Intrinsic” };const obj2 = { name: “Intrinsic” };console.log(obj1 === obj2); // false// 但是,它们的 .name 属性是基本类型:console.log(obj1.name === obj2.name); // true对象在 JavaScript 语言中扮演重要角色,它们的使用无处不在。对象通常用作键/值对的集合,然而,以这种方式使用它们有一个很大的限制: 在 symbol 出现之前,对象键只能是字符串,如果试图使用非字符串值作为对象的键,那么该值将被强制转换为字符串,如下:const obj = {};obj.foo = ‘foo’;obj[‘bar’] = ‘bar’;obj[2] = 2;obj[{}] = ‘someobj’;console.log(obj);// { ‘2’: 2, foo: ‘foo’, bar: ‘bar’, ‘[object Object]’: ‘someobj’ }Symbol 是什么Symbol() 函数会返回 symbol 类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的 symbol 注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:“new Symbol()"。所以使用 Symbol 生成的值是不相等:const s1 = Symbol();const s2 = Symbol();console.log(s1 === s2); // false实例化 symbol 时,有一个可选的第一个参数,你可以选择为其提供字符串。 此值旨在用于调试代码,否则它不会真正影响symbol 本身。const s1 = Symbol(‘debug’);const str = ‘debug’;const s2 = Symbol(‘xxyy’);console.log(s1 === str); // falseconsole.log(s1 === s2); // falseconsole.log(s1); // Symbol(debug)symbol 作为对象属性symbol 还有另一个重要的用途,它们可以用作对象中的键,如下:const obj = {};const sym = Symbol();obj[sym] = ‘foo’;obj.bar = ‘bar’;console.log(obj); // { bar: ‘bar’ }console.log(sym in obj); // trueconsole.log(obj[sym]); // fooconsole.log(Object.keys(obj)); // [‘bar’]乍一看,这看起来就像可以使用 symbol 在对象上创建私有属性,许多其他编程语言在其类中有自己的私有属性,私有属性遗漏一直被视为 JavaScript 的缺点。不幸的是,与该对象交互的代码仍然可以访问其键为 symbol 的属性。 在调用代码尚不能访问 symbol 本身的情况下,这甚至是可能的。 例如,Reflect.ownKeys() 方法能够获取对象上所有键的列表,包括字符串和 symbol :function tryToAddPrivate(o) { o[Symbol(‘Pseudo Private’)] = 42;}const obj = { prop: ‘hello’ };tryToAddPrivate(obj);console.log(Reflect.ownKeys(obj));// [ ‘prop’, Symbol(Pseudo Private) ]console.log(obj[Reflect.ownKeys(obj)[1]]); // 42注意:目前正在做一些工作来处理在JavaScript中向类添加私有属性的问题。这个特性的名称被称为私有字段,虽然这不会使所有对象受益,但会使类实例的对象受益。私有字段从 Chrome 74开始可用。防止属性名称冲突符号可能不会直接受益于JavaScript为对象提供私有属性。然而,他们是有益的另一个原因。当不同的库希望向对象添加属性而不存在名称冲突的风险时,它们非常有用。Symbol 为 JavaScrit 对象提供私有属性还有点困难,但 Symbol 还有别外一个好处,就是避免当不同的库向对象添加属性存在命名冲突的风险。考虑这样一种情况:两个不同的库想要向一个对象添加基本数据,可能它们都想在对象上设置某种标识符。通过简单地使用 id 作为键,这样存在一个巨大的风险,就是多个库将使用相同的键。function lib1tag(obj) { obj.id = 42;}function lib2tag(obj) { obj.id = 369;}通过使用 Symbol,每个库可以在实例化时生成所需的 Symbol。然后用生成 Symbol 的值做为对象的属性:const library1property = Symbol(’lib1’);function lib1tag(obj) { obj[library1property] = 42;}const library2property = Symbol(’lib2’);function lib2tag(obj) { obj[library2property] = 369;}出于这个原因,Symbol 似乎确实有利于JavaScript。但是,你可能会问,为什么每个库在实例化时不能简单地生成随机字符串或使用命名空间?const library1property = uuid(); // random approachfunction lib1tag(obj) { obj[library1property] = 42;}const library2property = ‘LIB2-NAMESPACE-id’; // namespaced approachfunction lib2tag(obj) { obj[library2property] = 369;}这种方法是没错的,这种方法实际上与 Symbol 的方法非常相似,除非两个库选择使用相同的属性名,否则不会有冲突的风险。在这一点上,聪明的读者会指出,这两种方法并不完全相同。我们使用唯一名称的属性名仍然有一个缺点:它们的键非常容易找到,特别是当运行代码来迭代键或序列化对象时。考虑下面的例子:const library2property = ‘LIB2-NAMESPACE-id’; // namespacedfunction lib2tag(obj) { obj[library2property] = 369;}const user = { name: ‘Thomas Hunter II’, age: 32};lib2tag(user);JSON.stringify(user);// ‘{“name”:“Thomas Hunter II”,“age”:32,“LIB2-NAMESPACE-id”:369}‘如果我们为对象的属性名使用了 Symbol,那么 JSON 输出将不包含它的值。这是为什么呢? 虽然 JavaScript 获得了对 Symbol 的支持,但这并不意味着 JSON 规范已经改变! JSON 只允许字符串作为键,JavaScript 不会尝试在最终 JSON 有效负载中表示 Symbol 属性。const library2property = ‘f468c902-26ed-4b2e-81d6-5775ae7eec5d’; // namespaced approachfunction lib2tag(obj) { Object.defineProperty(obj, library2property, { enumerable: false, value: 369 });}const user = { name: ‘Thomas Hunter II’, age: 32};lib2tag(user);console.log(user); // {name: “Thomas Hunter II”, age: 32, f468c902-26ed-4b2e-81d6-5775ae7eec5d: 369}console.log(JSON.stringify(user)); // {“name”:“Thomas Hunter II”,“age”:32}console.log(user[library2property]); // 369通过将 enumerable 属性设置为 false 而“隐藏”的字符串键的行为非常类似于 Symbol 键。它们通过 Object.keys() 遍历也看不到,但可以通过 Reflect.ownKeys() 显示,如下的示例所示:const obj = {};obj[Symbol()] = 1;Object.defineProperty(obj, ‘foo’, { enumberable: false, value: 2});console.log(Object.keys(obj)); // []console.log(Reflect.ownKeys(obj)); // [ ‘foo’, Symbol() ]console.log(JSON.stringify(obj)); // {}在这点上,我们几乎重新创建了 Symbol。隐藏的字符串属性和 Symbol 都对序列化器隐藏。这两个属性都可以使用Reflect.ownKeys()方法读取,因此它们实际上不是私有的。假设我们为属性名的字符串版本使用某种名称空间/随机值,那么我们就消除了多个库意外发生名称冲突的风险。但是,仍然有一个微小的区别。由于字符串是不可变的,而且 Symbol 总是保证惟一的,所以仍然有可能生成字符串组合会产生冲突。从数学上讲,这意味着 Symbol 确实提供了我们无法从字符串中得到的好处。在 Node.js 中,检查对象时(例如使用 console.log() ),如果遇到名为 inspect 的对象上的方法,将调用该函数,并将打印内容。可以想象,这种行为并不是每个人都期望的,通常命名为 inspect 的方法经常与用户创建的对象发生冲突。现在 Symbol 可用来实现这个功能,并且可以在 equire(‘util’).inspect.custom 中使用。inspect 方法在Node.js v10 中被废弃,在 v1 1中完全被忽略, 现在没有人会偶然改变检查的行为。模拟私有属性这里有一个有趣的方法,我们可以用来模拟对象上的私有属性。这种方法将利用另一个 JavaScript 特性: proxy(代理)。代理本质上封装了一个对象,并允许我们对与该对象的各种操作进行干预。代理提供了许多方法来拦截在对象上执行的操作。我们可以使用代理来说明我们的对象上可用的属性,在这种情况下,我们将制作一个隐藏我们两个已知隐藏属性的代理,一个是字符串 _favColor,另一个是分配给 favBook 的 S ymbol :let proxy;{ const favBook = Symbol(‘fav book’); const obj = { name: ‘Thomas Hunter II’, age: 32, _favColor: ‘blue’, [favBook]: ‘Metro 2033’, [Symbol(‘visible’)]: ‘foo’ }; const handler = { ownKeys: (target) => { const reportedKeys = []; const actualKeys = Reflect.ownKeys(target); for (const key of actualKeys) { if (key === favBook || key === ‘_favColor’) { continue; } reportedKeys.push(key); } return reportedKeys; } }; proxy = new Proxy(obj, handler);}console.log(Object.keys(proxy)); // [ ’name’, ‘age’ ]console.log(Reflect.ownKeys(proxy)); // [ ’name’, ‘age’, Symbol(visible) ]console.log(Object.getOwnPropertyNames(proxy)); // [ ’name’, ‘age’ ]console.log(Object.getOwnPropertySymbols(proxy)); // [Symbol(visible)]console.log(proxy._favColor); // ‘blue’使用 _favColor 字符串很简单:只需阅读库的源代码即可。 另外,通过蛮力找到动态键(例如前面的 uuid 示例)。但是,如果没有对 Symbol 的直接引用,任何人都不能 从proxy 对象访问’Metro 2033’值。Node.js警告:Node.js中有一个功能会破坏代理的隐私。 JavaScript语 言本身不存在此功能,并且不适用于其他情况,例 如Web 浏览器。 它允许在给定代理时获得对底层对象的访问权。 以下是使用此功能打破上述私有属性示例的示例:const [originalObject] = process .binding(‘util’) .getProxyDetails(proxy);const allKeys = Reflect.ownKeys(originalObject);console.log(allKeys[3]); // Symbol(fav book)现在,我们需要修改全局 Reflect 对象,或者修改 util 流程绑定,以防止它们在特定的 Node.js 实例中使用。但这是一个可怕的兔子洞。如果你对掉进这样一个兔子洞感兴趣,请查看我们的其他博客文章: Protecting your JavaScript APIs。代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》! ...

March 18, 2019 · 3 min · jiezi

Promise——从阅读文档到简单实现(二)

前言按照文档说明简单地实现 ES6 Promise的各个方法并不难,但是Promise的一些特殊需求实现起来并不简单,我首先提出一些不好实现或者容易忽略的需求:数据传递回调绑定将回调变成 microtask实现 then/finally 返回的pending promise “跟随”它们的回调返回的pending promise实现 resolve 返回的 promise “跟随”它的thenable对象参数实现框架在解决上述问题前,我们先实现一个框架。首先,我的目的是实现一个Promise插件,它包括:构造函数:Promise静态方法:resolve、reject、all 和 race实例方法:then、catch 和 finally私有函数:identity、thrower 和 isSettled 等如下:;(function() { function Promise(executor) { } Object.defineProperties(Promise, { resolve: { value: resolve, configurable: true, writable: true }, reject: { value: reject, configurable: true, writable: true }, race: { value: race, configurable: true, writable: true }, all: { value: all, configurable: true, writable: true } }); Promise.prototype = { constructor: Promise, then: function(onFulfilled, onRejected) { }, catch: function(onRejected) { }, finally: function(onFinally) { }, } function identity(value) { return value; } function thrower(reason) { throw reason; } function isSettled(pro) { return pro instanceof Promise ? pro.status === ‘fulfilled’ || pro.status === ‘rejected’ : false; } window.Promise = Promise;})();解决问题接下来,我们解决各个问题。数据传递为了传递数据——回调函数需要用到的参数以及 promise 的状态,我们首先在构造函数Promise中添加status、value和reason属性,并且在构造函数中执行 executor 函数:function Promise(executor) { var self = this; this.status = ‘pending’; this.value = undefined; this.reason = undefined; typeof executor === ‘function’ ? executor.call(null, function(value) { self.value = value; self.status = ‘fulfilled’; }, function(reason) { self.reason = reason; self.status = ‘rejected’; }) : false;}按照文档说明,为了实现链式调用,Promise的所有方法都会返回一个 Promise 对象,而且除了Promise.resolve(peomiseObj) 这种情况外都是新生成的 Promise 对象。所以接下来我的大部分方法都会返回一个新的 promise 对象。不生成新对象的特例:var a = Promise.resolve(‘a’), b = Promise.resolve(a);console.log(a === b) //true回调绑定接下来,我们要将then、catch和finally中的回调方法绑定到Promise对象的状态这个事件上。我想到的第一个事件就是onchange事件,但是 promiseObj.status 属性上并没有change事件。但是,我马上想到每次设置accessor属性的值时,就会调用 accessor 属性的setter方法。那么,我只要把status属性设置为存取属性,然后在它的 setter 方法里绑定回调函数就行啦!如下:function Promise(executor) { var self = this; //存储状态的私有属性 this._status = ‘pending’; this.value = undefined; this.reason = undefined; //this.events = new Events(); //存储状态的公开属性 Object.defineProperty(this, ‘status’, { get: function() { return self._status; }, set: function(newValue) { self._status = newValue; //self.events.fireEvent(‘change’); }, configurable: true }); typeof executor === ‘function’ ? executor.call(null, function(value) { self.value = value; self.status = ‘fulfilled’; }, function(reason) { self.reason = reason; self.status = ‘rejected’; }) : false;}为了绑定回调函数,我使用了发布订阅模式。在then、catch和finally方法执行的时候订阅事件change,将自己的回调函数绑定到change事件上,promiseObj.status 属性是发布者,一旦它的值发生改变就发布change事件,执行回调函数。为了节省篇幅,不那么重要的发布者Events() 构造函数及其原型我就不贴代码了,文章末尾我会给出源代码。实现 microtaskthen、catch和finally方法的回调函数都是microtask,当满足条件(promise 对象状态改变)时,这些回调会被放入microtask队列。每当调用栈中的macrotask执行完毕时,立刻执行microtask队列中的所有microtask,这样一次事件循环就结束了,js引擎等待下一次循环。要我实现microtask我是做不到的,我就只能用macrotask模仿一下microtask了,就像有些大人喜欢冒充小学生过儿童节一样,我用宏任务冒充一下微任务应该问题不大(才怪)。我用 setTimeout 发布的macrotask进行模仿:Object.defineProperty(this, ‘status’, { get: function() { return self._status; }, set: function(newValue) { self._status = newValue; setTimeout(() = >{ self.events.fireEvent(‘change’); }, 0); }, configurable: true});实现函数接下来,我们实现各个函数和方法。在知道方法的参数和返回值后再实现方法如有神助,而实现过程中最难处理的就是 pending 状态的 promise 对象,因为我们要等它变成其它状态时,再做真正的处理。下面我拿出两个最具代表性的方法来分析。静态方法all如果忘记了 Promise.all(iterable) 的参数和返回值,可以返回我上一篇文章查看。function all(iterable) { //如果 iterable 不是一个可迭代对象 if (iterable[Symbol.iterator] == undefined) { let err = new TypeError(typeof iterable + iterable + ’ is not iterable (cannot read property Symbol(Symbol.iterator))’); return Promise.reject(err); } //如果 iterable 对象为空 if (iterable.length === 0) { return Promise.resolve([]); } //其它情况用异步处理 var pro = new Promise(), //all 返回的 promise 对象 valueArr = []; //all 返回的 promise 对象的 value 属性 setTimeout(function() { var index = 0, //记录当前索引 count = 0, len = iterable.length; for (let val of iterable) { - function(i) { if (val instanceof Promise) { //当前值为 Promise 对象时 if (val.status === ‘pending’) { val.then(function(value) { valueArr[i] = value; count++; //Promise.all([new Promise(function(resolve){setTimeout(resolve, 100, 1)}), 2, 3, 4]) if (count === len) { pro.value = valueArr; pro.status = ‘fulfilled’; } }, function(reason) { pro.reason = reason; pro.status = ‘rejected’; //当一个pending Promise首先完成时,解除其它 pending Promise的事件,防止之后其它 Promise 改变 pro 的状态 for (let uselessPromise of iterable) { if (uselessPromise instanceof Promise && uselessPromise.status === ‘pending’) { uselessPromise.events.removeEvent(‘change’); } } }); } else if (val.status === ‘rejected’) { pro.reason = val.reason; pro.status = ‘rejected’; return; } else { //val.status === ‘fulfilled’ valueArr[i] = val.value; count++; } } else { valueArr[i] = val; count++; } index++; } (index); } //如果 iterable 对象中的 promise 对象都变为 fulfilled 状态,或者 iterable 对象内没有 promise 对象, //由于我们可能需要等待 pending promise 的结果,所以要额外花费一个变量计数,而不能用valueArr的长度判断。 if (count === len) { pro.value = valueArr; pro.status = ‘fulfilled’; } }, 0); return pro;}这里解释两点:1、如何保证 value 数组中值的顺序如果iterable对象中的 promise 对象都变为 fulfilled 状态,或者 iterable 对象内没有 promise 对象,all 返回一个 fulfilled promise 对象,且其 value 值为 iterable 中各项值组成的数组,数组中的值将会按照 iterable 内的顺序排列,而不是由 pending promise 的完成顺序决定。为了保证 value 数组中值的顺序,最简单的方法是valueArr[iterable.indexOf(val)] = val.value;但是像除 Array、TypedArray 和 String 外的 Map 和 Set 原生 iterabe 对象,以及其它通过myIterable[Symbol.iterator] 创建的自定义的 iterable 对象都没有 indexOf 方法,所以我选择用闭包来保证 value 数组值的顺序。2、处理 pending promise 对象。pending promise 是导致这个函数要额外添加很多变量存储状态,额外做很多判断和处理的罪魁祸首。如果 iterabe 对象中有一个pending状态的 promise(通常为一个异步的 promise),我们就使用then方法来持续关注它的动态。一旦它变成fulfilledpromise,就将它的 value 加入 valueArr 数组。我们添加一个 count 变量记录目前 valueArr 获取到了多少个值,当全部获取到值后,就可以给 pro.value 和pro.status 赋值了。之所以用 count 而不是 valueArr.length 判断,是因为 valueArr = [undefined,undefined,undefined,1] 的长度也为4,这样可能导致还没获取到 pending promise 的值就改变 pro.status 了。而当它变成rejectedpromise 时,我们就更新 all 方法返回的对象的 reason 值,同时改变状态 status 为 rejected,触发绑定的onrejected函数。另外,为了与原生 Promise 表现相同:如果 iterable 对象中任意一个 pending promise 对象状态变为 rejected,将不再持续关注其它 pending promise 的动态。而我早就在所有的 pending promise 上都绑定了 onfulfilled 和 onrejected 函数,用来跟踪它们。所以我需要在某个 pending promise 变为 rejected promise 时,删除它们绑定的回调函数。实例方法thenPromise.prototype.then(onFulfilled, onRejected):Promise.prototype.then = function(onFulfilled, onRejected) { var pro = new Promise(); //绑定回调函数,onFulfilled 和 onRejected 用一个回调函数处理 this.events.addEvent(‘change’, hander.bind(null, this)); function hander(that) { var res; //onFulfilled 或 onRejected 回调函数执行后得到的结果 try { if (that.status === ‘fulfilled’) { //如果onFulfilled不是函数,它会在then方法内部被替换成一个 Identity 函数 typeof onFulfilled !== ‘function’ ? onFulfilled = identity: false; //将参数 this.value 传入 onFulfilled 并执行,将结果赋给 res res = onFulfilled.call(null, that.value); } else if (that.status === ‘rejected’) { //如果onRejected不是函数,它会在then方法内部被替换成一个 Thrower 函数 typeof onRejected !== ‘function’ ? onRejected = thrower: false; res = onRejected.call(null, that.reason); } } catch(err) { //抛出一个错误,情况3 pro.reason = err; pro.status = ‘rejected’; return; } if (res instanceof Promise) { if (res.status === ‘fulfilled’) { //情况4 pro.value = res.value; pro.status = ‘fulfilled’; } else if (res.status === ‘rejected’) { //情况5 pro.reason = res.reason; pro.status = ‘rejected’; } else { //情况6 //res.status === ‘pending’时,pro 跟随 res pro.status = ‘pending’; res.then(function(value) { pro.value = value; pro.status = ‘fulfilled’; }, function(reason) { pro.reason = reason; pro.status = ‘rejected’; }); } } else { //回调函数返回一个值或不返回任何内容,情况1、2 pro.value = res; pro.status = ‘fulfilled’; } } return pro;};我想我已经注释得很清楚了,可以对照我上一篇文章进行阅读。我再说明一下pending promise 的“跟随”情况,和 all 方法的实现方式差不多,这里也是用 res.then来“跟随”的。我相信大家都看得懂代码,下面我举个例子来实践一下:var fromCallback;var fromThen = Promise.resolve(‘done’).then(function onFulfilled(value) { fromCallback = new Promise(function(resolve){ setTimeout(() => resolve(value), 0); //未执行 setTimeout 的回调方法之前 fromCallback 为’pending’状态 }); return fromCallback; //then 方法返回的 fromThen 将跟随 onFulfilled 方法返回的 fromCallback});setTimeout(function() { //目前已执行完 onFulfilled 回调函数,fromCallback 为’pending’状态,fromThen ‘跟随’ fromCallback console.log(fromCallback.status); //fromCallback.status === ‘pending’ console.log(fromThen.status); //fromThen.status === ‘pending’ setTimeout(function() { //目前已执行完 setTimeout 中的回调函数,fromCallback 为’fulfilled’状态,fromThen 也跟着变为’fulfilled’状态 console.log(fromCallback.status + ’ ’ + fromCallback.value); //fromCallback.status === ‘fulfilled’ console.log(fromThen.status + ’ ’ + fromThen.value); //fromThen.status === ‘fulfilled’ console.log(fromCallback === fromThen); //false }, 10); //将这个 delay 参数改为 0 试试}, 0);看完这个例子,我相信大家都搞懂了then的回调函数返回 pending promise 时它会怎么处理了。另外,这个例子也体现出我用 setTimeout 分发的macrotask模拟microtask的不足之处了,如果将倒数第二行的的 delay 参数改为 0,那么 fromThen.status === ‘pending’,这说明修改它状态的代码在 log 它状态的代码之后执行,至于原因大家自己想一下,这涉及到 event loop。测试大侠请点下面的链接进行测试:https://codepen.io/lyl123321/…或者点这里查看源代码:https://github.com/lyl123321/…结语本菜鸡终于做完这个 Promise 的 polyfill 了,各种改 bug 累个半死,还是老老实实滚去做毕设好了:) ...

March 16, 2019 · 5 min · jiezi

Promise——从阅读文档到简单实现(一)

前言最近几周参加笔试面试,总是会遇到实现异步和处理异步的问题,然而作者每次都无法完美地回答。在最近一次笔试因为 Promise 而被刷掉后,我终于下定决心一个个地搞懂它们,就先拿 Promise 开刀吧 :)。用法解析ES6 的Promise对象是一个代理对象,被代理的值在Promise对象创建时可能是未知的,另外它允许你为异步操作的成功和失败分别绑定相应的处理方法。Promise 常用于控制异步操作的执行顺序,而且可以让异步方法像同步方法那样返回值。它不能立即取得异步方法的返回值,但是它可以代理这个值,一旦异步操作完成,就会以及将值传递给相应的处理方法。一个Promise对象有以下几种状态:pending: 初始状态,既不是成功,也不是失败状态。fulfilled: 意味着操作成功完成。rejected: 意味着操作失败。一个Promise对象的状态可以从pending变成fulfilled,同时传递一个值给相应的onfulfilled处理方法;也可以从pending变成rejected,同时传递一个失败信息给相应的onrejected处理方法。一旦一个Promise对象的状态发生改变,就会触发之前通过Promise.prototype.then、 Promise.prototype.catch和 Promise.prototype.finally方法绑定的onfulfilled、onrejected和onFinally处理方法。因为 then、catch和finally方法都会返回一个新的Promise对象, 所以它们可以被链式调用。构造函数构造函数Promise()主要用来包装还未支持 promises 的函数。new Promise( function(resolve, reject) {…} /* executor */ );参数:executorexecutor是带有 resolve 和 reject 两个函数参数的函数。Promise构造函数执行时立即调用executor函数,换句话说,executor函数是在Promise构造函数内执行的,所以它是同步代码。在executor函数内调用 resolve 和 reject 函数,可以传递参数给相应的处理方法,并会分别将 promise 新建对象的状态改为fulfilled(完成)或rejected(失败)。executor 内部通常会执行一些异步操作如ajax,一旦完成,可以调用resolve函数传递参数并将 promise 对象的状态改成 fulfilled,或者在发生错误时调用reject 函数传递参数并将 promise 对象的状态改成 rejected。如下:function myAsyncFunction(url) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open(“GET”, url); xhr.onload = () => resolve(xhr.responseText); xhr.onerror = () => reject(xhr.statusText); xhr.send(); });};如果在executor函数中抛出一个错误,那么该 promise 对象状态将变为rejected,并将错误作为参数传递给对应的onrejected处理方法。如下:function myAsyncFunction(url) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open(“GET”, url); xhr.onerror = () => { throw xhr.statusText; //throw xhr.statusText 效果等同于 reject(xhr.statusText) }; xhr.send(); });};静态方法静态方法是定义在构造函数上的方法,声明静态方法:Func.fn = function() {}调用静态方法:Func.fn(args);ES6中的Promise构造函数有4个静态方法:Promise.resolve(value)Promise.reject(reason)Promise.all(iterable)Promise.race(iterable)Promise.resolve(value):返回一个由参数value解析而来的Promise对象。如果value是一个thenable对象(带有 then 方法的对象),返回的Promise对象会跟随这个thenable对象,状态随之变化;如果传入的value本身就是Promise对象,则直接返回value;其它情况下返回的Promise对象状态为fulfilled,并且将该value作为参数传递给onfulfilled处理方法。通常而言,如果你不知道一个值是否为Promise对象,就可以使用 Promise.resolve(value) 来将value以Promise对象的形式使用。 // resolve一个thenable对象 var p1 = Promise.resolve({ then: function(onFulfill, onReject) { onFulfill(“fulfilled!”); } }); console.log(p1 instanceof Promise) // true, 这是一个Promise对象 p1.then(function(v) { console.log(v); // 输出"fulfilled!" }, function(e) { // 不会被调用 });Promise.reject(reason):返回一个被给定原因reason拒绝的Promise对象。返回的Promise对象的status状态属性为rejected,reason拒绝原因属性(传递给onrejected处理方法的 reason 参数)与参数reason相等。Promise.reject(new Promise((resolve, reject) => resolve(‘done’))).then(function(reason) { // 未被调用}, function(reason) { console.log(reason); // new Promise});Promise.all(iterable):参数:iterable对象为Array对象、Map对象和Set对象等可迭代对象。返回一个Promise对象。如果iterable对象为空,Promise.all会同步地返回一个状态为fulfilled的 promise 对象。如果iterable对象中的 promise 对象都变为fulfilled状态,或者iterable对象内没有 promise 对象,Promise.all返回的 promise 对象将异步地变为fulfilled状态。以上两种情况返回的都是fulfilled状态的 promise 对象,其value值(传递给onfulfilled处理方法的 value 参数)都是一个数组,这个数组包含iterable对象中所有的基本值和 promise 对象value值。如果iterable对象中任意一个 promise 对象状态变为rejected,那么Promise.all就会异步地返回一个状态为rejected的 promise 对象,而且此 promise 对象的reason值(传递给onrejected处理方法的 reason 参数),等于iterable对象中状态为rejected的那一个 promise 对象的reason值。// this will be counted as if the iterable passed is empty, so it gets fulfilledvar p = Promise.all([1,2,3]);// this will be counted as if the iterable passed contains only the resolved promise with value “444”, so it gets fulfilledvar p2 = Promise.all([1,2,3, Promise.resolve(444)]);// this will be counted as if the iterable passed contains only the rejected promise with value “555”, so it gets rejectedvar p3 = Promise.all([1,2,3, Promise.reject(555)]);// using setTimeout we can execute code after the stack is emptysetTimeout(function(){ console.log(p); console.log(p2); console.log(p3);});// logs// Promise { <state>: “fulfilled”, <value>: Array[3] }// Promise { <state>: “fulfilled”, <value>: Array[4] }// Promise { <state>: “rejected”, <reason>: 555 }Promise.race(iterable):返回一个Promise对象。一旦iterable中的某个 promise 对象完成或拒绝,返回的 promise 对象就会完成或拒绝,且返回的 promise 对象的value值(完成时)或reason值(拒绝时)和这个 promise 对象的value值(完成时)或reason值(拒绝时)相等。var promise1 = new Promise(function(resolve, reject) { setTimeout(resolve, 500, ‘one’);}), promise2 = new Promise(function(resolve, reject) { setTimeout(resolve, 100, ’two’);});Promise.race([promise1, promise2]).then(function(value) { console.log(value); // Both resolve, but promise2 is faster});// expected output: “two"实例方法实例方法是定义在原型对象上的方法,声明实例方法:Func.prototype.fn = function() {}调用实例方法需要先创建一个实例:let func = new Func();func.fn(args);Promise的原型对象上有3个实例方法:Promise.prototype.then(onFulfilled, onRejected)Promise.prototype.catch(onRejected)Promise.prototype.finally(onFinally)Promise.prototype.then(onFulfilled, onRejected):then方法接收成功和失败两种情况的回调函数作为参数,返回一个Promise对象。参数:onFulfilled和onRejected回调函数onFulfilled:当 promise 对象变成 fulfilled 状态时被调用。onFulfilled函数有一个参数,即 promise 对象完成时的 value 值。如果onFulfilled不是函数,它会在then方法内部被替换成一个Identity函数,即 (x) => (x) 。onRejected:当 promise 对象变成 rejected 状态时被调用。onRejected函数有一个参数,即 promise 对象失败时的 reason 值。如果onRejected不是函数,它会在then方法内部被替换成一个Thrower函数,即 (reason) => {throw reason} 。返回:一旦调用then方法的 promise 对象被完成或拒绝,将异步调用相应的处理函数(onFulfilled或onRejected),即将处理函数加入microtask队列中。如果onFulfilled或onRejected回调函数:返回一个值,则then返回的 promise 对象的status变为fulfilled,value变为回调函数的返回值;不返回任何内容,则then返回的 promise 对象的status变为fulfilled,value变为undefined;抛出一个错误,则then返回的 promise 对象的status变为rejected,reason变为回调函数抛出的错误;返回一个状态为fulfilled的 promise 对象,则then返回的 promise 对象的status变为fulfilled,value等于回调函数返回的 promise 对象的value值;返回一个状态为rejected的 promise 对象,则then返回的 promise 对象的status变为rejected,reason等于回调函数返回的 promise 对象的reason值;返回一个状态为pending的 promise 对象,则then返回的 promise 对象的status变为pending,且其status将随着回调函数返回的 promise 对象的status变化而变化,之后其value或reason值也会和此 promise 对象的value或reason值相同。这里提一下,这个地方看 MDN 文档中文翻译实在看不懂,之后看英文原文反而稍微理解了,希望之后在实现 Promise 的过程中能理解更深。 var fromCallback; var fromThen = Promise.resolve(‘done’) .then(function() { fromCallback = new Promise(function(){}); return fromCallback; }); setTimeout(function() { console.log(fromCallback); //fromCallback.status === ‘pending’ console.log(fromThen); //fromThen.status === ‘pending’ console.log(fromCallback === fromThen); //false }, 0);Promise.prototype.catch(onRejected):catch方法接收失败情况的回调函数作为参数,返回一个Promise对象。参数:onRejected回调函数,表现同then中的onRejected参数。返回:promiseObj.catch(onRejected) 与 promiseObj.then(undefined, onRejected) 返回值相同。 Promise.resolve() .then( () => { // 返回一个 rejected promise throw ‘Oh no!’; }) .catch( reason => { console.error( ‘onRejected function called: ‘, reason ); }) .then( () => { console.log( “I am always called even if the prior then’s promise rejects” ); });Promise.prototype.finally(onFinally):finally方法接收onFinally回调函数作为参数,返回一个Promise对象。如果你想在 promise 执行完毕后,无论其结果是成功还是失败,都做一些相同的处理时,可以使用finally方法。参数:onFinally回调函数onFinally不接收任何参数,当 promise 对象变成 settled (fulfilled / rejected) 状态时onFinally被调用。返回:如果onFinally回调函数不返回任何内容或者返回一个值或者返回一个状态为fulfilled的 promise 对象,则finally返回的 promise 对象的status、value和reason值与调用这个finally方法的 promise 对象的值相同;抛出一个错误或者返回一个状态为rejected的 promise 对象,则finally返回的 promise 对象的status值变为rejected,reason值变为被抛出的错误或者回调函数返回的 promise 对象的reason值;返回一个状态为pending的 promise 对象,则finally返回的 promise 对象的status值变为pending,且其status值将随着回调函数返回的 promise 对象的status值变化而变化,之后其value或reason值也会和此 promise 对象的value或reason值相同。Promise.reject(‘是我,开心吗’).finally(function() { var pro = new Promise(function(r){r(‘你得不到我’)}); //pro.status === ‘fulfilled’ return pro; //onFinally回调函数返回一个状态为fulfilled的 promise 对象}).catch(function(reason) { console.log(reason);});结语将 MDN 文档整理了一下,加入了一些自己的理解,也花费了一天的时间。现在Promise各个方法的参数、返回值、功能和使用方法已经有个大概的了解了,为了进一步理解其原理,接下来我打算简单地实现一下它。 ...

March 15, 2019 · 3 min · jiezi

ES6 知识整理一(es6快速入门)

ES6 简介ES6, 全称 ECMAScript 6.0 ,是 JaveScript 的下一个版本标准,2015.06 发版。let 和 constlet 命令let 命令,用来声明变量。它的用法类似于 var,区别在于 var 声明的变量全局有效,let 声明的变量只在它所在的代码块内有效。// 变量i储存的值是10,所以执行a2后输出10var a = [];for (var i = 0; i < 10; i++) { a[i] = function () { console.log(i); };}a2; // 10// 修正方法// 闭包会使得函数中的变量都被保存在内存中,所以执行a2后输出2var a = [];for (var i = 0; i < 10; i++) { (function (i) { a[i] = function () { console.log(i) } })(i);}a2; // 2// es6// let声明的i只在当前的代码块有效,所以每次for循环相当于用let重新声明一次ivar a = [];for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); };}a2; // 2// 注:JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。let 不存在变量提升,必须先声明后使用,否则报错;var 存在变量提升,未声明前使用输出 undefined。let 存在暂时性死区,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。let 不允许重复声明。const 命令const 声明一个只读的常量。一旦声明,常量的值就不能改变。不能只声明不赋值。const a = 10;a = 20; // 报错const b; // 报错const 的作用域与 let 相同。if(true) { const num = 5;}console.log(num); // 报错const 声明对象,常量对象内存地址,因此对象本身可改,但是给常量重新赋值就会报错。const obj = {};obj.a = ‘a’;obj = {}; // 报错块级作用域和函数作用域ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。但是在 ES6 中,函数可以在块级作用域中声明。但是,市面上很多浏览器都不支持 ES6,所以应该避免在块级作用与中声明函数。ES6 声明变量的方法varfunctionletconstimportclass变量的解构赋值ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。数组的解构赋值模式匹配赋值,如果解构不成功,变量的值就等于 undefined。let [a, [[b], c]] = [1, [[2], 3]];console.log(a,b,c); // 1, 2, 3let [x, , y, z] = [1, 2, 3];console.log(x); // 1console.log(y); // 2console.log(z); // undefined不完全解构赋值,等号左边的模式,只匹配一部分的等号右边的数组。let [x, [y], z] = [1, [2, 3], 4];console.log(x); // 1console.log(y); // 2console.log(z); // 4数组结构赋值右边必须是数组,模式不匹配则报错。let [a] = {}; // 报错解构赋值可以添加默认值,并且可以引用解构赋值的其他变量。let [a = 1, b = 2] = [, 3];console.log(a); // 1console.log(b); // 3let [x = 1, y = x] = []; // x = 1; y = 1let [x = 1, y = x] = [2]; // x = 1; y = 2数组解构赋值可用于交换变量的值。let [a, b] = [1, 2];console.log(a, b); // 1, 2[b, a] = [a, b];console.log(a, b); // 2, 1对象的解构赋值变量必须与属性同名let { a, b, c } = { a: ‘aaa’, b: ‘bbb’ };console.log(a); // ‘aaa’console.log(b); // ‘bbb’console.log(c); // undefined变量名与属性名不一致let { a: x, b: y } = { a: ‘aaa’, b: ‘bbb’ };console.log(x); // ‘aaa’console.log(y); // ‘bbb’嵌套赋值,如果子对象所在的父属性不存在,会报错,慎用。let { a, a: {x}, b: y } = { a: {x: ‘xxx’,y: ‘yyy’}, b: “bbb” };console.log(a); // { x: ‘xxx’, y: ‘yyy’ }console.log(x); // ‘xxx’let {c: {d: {e}}} = {c: ‘ccc’}; // 报错console.log(e)变量解构赋值也和数组的解构赋值一样,可以赋默认值,变量解构赋值时,不能将大括号写在行首,否者 JavaScript 引擎将会按代码块执行。let x;{x} = {x: 1}; // 报错// 正确写法let x;({x} = {x: 1});字符串解构赋值字符串解构赋值,将字符串转化成数组对象const [a,b,c] = ‘123456789’;const {length} = ‘123456789’;console.log(a, b, c, length); // 1, 2, 3, 9函数解构赋值const arr = [[1, 2], [3, 4]].map(([a, b]) => a + b);console.log(arr); // [ 3, 7 ]解构赋值规则解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefined 和 null 无法转为对象,所以对它们进行解构赋值,都会报错。let {toString: n} = 123;n === Number.prototype.toString // truelet {toString: b} = true;b === Boolean.prototype.toString // truelet { prop: u } = undefined; // 报错let { prop: n } = null; // 报错解构赋值的用途交换变量的值从对象、数组中取值(提取 JSON 数据),或从函数中返回多个值函数解构赋值传参,给定函数参数的默认值输入模块的指定方法遍历 Map 结构const map = new Map();map.set(‘first’, ‘hello’);map.set(‘second’, ‘world’);for (let [key, value] of map) { console.log(key + ’ is ’ + value);}// first is hello// second is world字符串扩展(不含编码)for…of 遍历字符串for(let codePoint of ‘string’){ console.log(codePoint)}// ’s’// ’t’// ‘r’// ‘i’// ’n’// ‘g’includes(),startsWith(),endsWith()三个方法都接收两个参数,第一个参数为检索的值,第二个参数为检索的起始位置,返回布尔值let s = ‘Hello world!’;const [a, b, c] = [ s.startsWith(‘Hello’, 2), s.endsWith(’!’), s.includes(‘o w’)];console.log(a, b, c); // false true truerepeat()repeat 方法返回一个新字符串,表示将原字符串重复 n 次。参数为[-Infinity,-1]或者 Infinity,会报错;参数为(-1,1)时,相当于参数为 0;参数为小数时向下取整;参数 NaN 等同于 0;参数是字符串,则会先转换成数字。‘str’.repeat(‘3’) // ‘strstrstr’padStart(), padEnd()padStart(),padEnd()有两个参数,第一个参数为字符串补全生效的最大长度,第二个参数为补全的字符串。第二个参数默认为空格,省略第二个参数时默认用空格补全。第一个参数小于字符串原长度时,返回原字符串。如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串。常见用途:补全指定位数,提示字符串格式。‘123456’.padStart(10, ‘0’) // “0000123456”‘09-12’.padStart(10, ‘YYYY-MM-DD’) // “YYYY-09-12"模版字符串(``)const str = ‘world’;const template = Hello ${str};console.log(template); // Hello world正则扩展(略)数值扩展二进制、八进制表示法使用二进制表示法,前缀为 0b,使用八进制表示法,前缀为 0o,ES6 不支持使用 00 前缀表示八进制。进制转换使用 toString 方法,使用 Number 方法直接转十进制。0b1100100 === 100; // true0o144 === 100; // true(0b1100100).toString(8); // 144(0b1100100).toString(10); // 100Number(‘0b1100100’); // 100Number.isFinite(),Number.isNaN()Number.isFinite()用来检查一个数值是否为有限的(finite),即不是 Infinity。参数类型不是数值,Number.isFinite 一律返回 false。Number.isNaN()用来检查一个值是否为 NaN。参数类型不是 NaN,Number.isNaN 一律返回 false。Number.isFinite(15); // trueNumber.isFinite(-Infinity); // falseNumber.isNaN(15) // falseNumber.isNaN(9/0) // trueNumber.parseInt(), Number.parseFloat()ES6 将全局方法 parseInt()和 parseFloat(),移植到 Number 对象上面,行为完全保持不变。Number.isInteger()Number.isInteger()用来判断一个数值是否为整数。Number.isInteger(25) // trueNumber.isInteger(25.0) // trueNumber.isInteger(25.1) // falseES6 新增 Number 常量Number.EPSILON 极小常量,浮点数误差小于这个值可以认为不存在误差;Number.MAX_SAFE_INTEGER 安全整数的最大范围;Number.MIN_SAFE_INTEGER 安全整数的最小范围;Number.isSafeInteger() 用来判断一个整数是否落在安全整数范围之内。Number.isSafeInteger(9007199254740993) // falseNumber.isSafeInteger(990) // trueNumber.isSafeInteger(9007199254740993 - 990) // trueMath 对象的扩展Math.trunc() 除去一个数的小数部分,返回整数部分。参数不是数值,内部会先调用 Nunber()专为数值,对于空值和无法截取整数的值,返回 NaN。(Math 对象的扩展的方法对于非数值的处理方法都一样)Math.trunc(5.9) // 5Math.trunc(-4.9) // -4Math.trunc(null) // 0Math.trunc(‘foo’); // NaNMath.sign() 判断一个数是正数、负数、还是零。Math.sign(-5) // -1 负数Math.sign(5) // +1 正数Math.sign(0) // +0 零Math.sign(-0) // -0 零Math.sign(NaN) // NaNMath.cbrt() 计算一个数的立方根。Math.cbrt(2) // 1.2599210498948734// Math.sqrt(x) 计算平方根Math.sqrt(2) // 1.4142135623730951// 幂运算 Math.pow(x,y)Math.pow(2, 3)Math.hypot() 返回所有参数的平方和的平方根。Math.hypot(3, 4); // 5Math.hypot(3, 4, 5); // 7.0710678118654755函数扩展rest 参数ES6 引入 rest 参数(形式为…变量名),用于获取函数的多余参数,rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。只能是最后一个参数,函数的 length 属性,不包括 rest 参数。function sum1(x, y, …args) { let sum = 0; for (let arg of args) { sum += arg; } return sum;}console.log(sum1(1, 2, 3, 4)) // 7function sum2(…args) { return args.reduce((prev, curr) => { return prev + curr }, 0)}console.log(sum2(1, 2, 3)); // 6name 属性函数的 name 属性,返回该函数的函数名。对于匿名函数,ES5 返回’’,ES6 返回变量名;Function 构造函数返回的函数实例,name 属性的值为 anonymous;bind 返回的函数,name 属性值会加上 bound 前缀。function fn() {}fn.name // ‘fn’function foo() {};foo.bind({}).name // ‘bound foo’(function(){}).bind({}).name // ‘bound ‘箭头函数const fn = v => v;// 等同于const fn = function (v) { return v;};注意要点函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象;不可以当作构造函数,即不可以使用 new 命令,否则会抛出一个错误;不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替;不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。尾调用优化尾调用指函数的最后一步是调用另一个函数。function f(x){ ‘use strict’; return g(x);}函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数 A 的内部调用函数 B,那么在 A 的调用帧上方,还会形成一个 B 的调用帧。等到 B 运行结束,将结果返回到 A,B 的调用帧才会消失。如果函数 B 内部还调用函数 C,那就还有一个 C 的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了,这样可以防止内存溢出,达成尾调用优化。ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。数组扩展扩展运算符扩展运算符(spread)是三个点(…)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。const arr = [1, 2, 3];arr.push(…[4, 5, 6]);扩展运算符的应用数组展开const arr = [1, 2, 3];…arr // 1, 2, 3复制数组const a1 = [1, 2];// 写法一const a2 = […a1];// 写法二const […a2] = a1;// 相当于const a1 = [1, 2];const a2 = a1.concat();合并数组。浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。以下的两种方法属于浅拷贝,如果修改了原数组的成员,会同步反映到新数组。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’ ]解构赋值,字符串转数组const list = [1, 2, 3];[a, …b] = list;console.log(a) // 1console.log(b) // [2, 3][…‘hello’] // [‘h’, ’e’, ’l’, ’l’, ‘o’]Array.from()Array.from 方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。常见的类似数组的对象有 DOM 操作返回的 NodeList 集合,以及函数内部的 arguments 对象。let arrayLike = { ‘0’: ‘a’, ‘1’: ‘b’, ‘2’: ‘c’, length: 3};// ES5的写法var arr1 = [].slice.call(arrayLike); // [‘a’, ‘b’, ‘c’]// ES6的写法let arr2 = Array.from(arrayLike); // [‘a’, ‘b’, ‘c’]Array.from(‘hello’);// [‘h’, ’e’, ’l’, ’l’, ‘o’]let namesSet = new Set([‘a’, ‘b’]);Array.from(namesSet); // [‘a’, ‘b’]Array.from 还可以接受第二个参数,作用类似于数组的 map 方法,用来对每个元素进行处理,将处理后的值放入返回的数组。let arrayLike = { ‘0’: 1, ‘1’: 2, ‘2’: 3, length: 3};Array.from(arrayLike, x => x * x); // [ 1, 4, 9 ]Array.of()Array.of 方法用于将一组值,转换为数组。这个方法的主要目的,是弥补数组构造函数 Array()的不足。因为参数个数的不同,会导致 Array()的行为有差异。Array.of() // []Array.of(undefined) // [undefined]Array.of(1) // [1]Array.of(1, 2) // [1, 2]copyWithin()参数:target(必需):从该位置开始替换数据。如果为负值,表示倒数。start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数。end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。这三个参数都应该是数值,如果不是,会自动转为数值。[1, 2, 3, 4, 5].copyWithin(0, 3)find() 和 findIndex()数组实例的 find 方法,用于找出第一个符合条件的数组成员,如果没有符合条件的成员,则返回 undefined。findIndex 方法返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。[1, 4, -5, 10].find(n => n < 0); // -5[1, 4, -5, 10].findIndex(n => n < 0); // 2两个方法都可以接受第二个参数,用来绑定回调函数的 this 对象。function f(v){ return v > this.age;}let person = {name: ‘John’, age: 20};[10, 12, 26, 15].find(f, person); // 26这两个方法都可以发现 NaN,弥补了数组的 indexOf 方法的不足。fill() 填充数组fill 方法使用给定值,填充一个数组。fill 方法可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象,改变数组中的一项,则所有项都改变。let arr = Array.of(1, 2, 3).fill({ num: 20});console.log(arr); // [ { num: 20 }, { num: 20 }, { num: 20 } ]arr[0].num = 10;console.log(arr); // [ { num: 10 }, { num: 10 }, { num: 10 } ]entries(),keys() 和 values() 遍历数组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"includes()includes 方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的 includes 方法类似。该方法的第二个参数表示搜索的起始位置,第二参数是负数,取它的倒数,第二参数大于数组长度,取 0。[1, 2, 3].includes(3, -1); // trueflat(),flatMap()flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将 flat()方法的参数写成一个整数,表示想要拉平的层数,默认为 1。flat()的参数为 2,表示要“拉平”两层的嵌套数组。如果不管有多少层嵌套,都要转成一维数组,可以用 Infinity 关键字作为参数。[1, [2, [3]]].flat(Infinity);// [1, 2, 3]flatMap()先遍历数组,再“拉平”一层,也只能拉平一层。参数鱼 map()方法类似。ß[2, 3, 4].flatMap(x => [x, x * 2]); // [2, 4, 3, 6, 4, 8]// 相当于[2, 3, 4].map(x => [x, x * 2]).flat(); // [2, 4, 3, 6, 4, 8]对象扩展属性简洁表示法const a = 1;const b = 2;const c = {a, b};// 等同于const c = {a: a, b: b};const o = { method() { return “Hello!”; }};// 等同于const o = { method: function() { return “Hello!”; }};function f(x, y) { return {x, y};}// 等同于function f(x, y) { return {x: x, y: y};}对象的扩展运算符对象扩展符类似数组扩展符,主要用于解构赋值。let { x, y, …z } = { x: 1, y: 2, a: 3, b: 4 };x // 1y // 2z // { a: 3, b: 4 }let ab = { …a, …b };// 等同于let ab = Object.assign({}, a, b);Object.is()Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。Object.is(‘str’, ‘str’); // trueObject.is({}, {}); // false不同之处只有两个:一是+0不等于-0,二是NaN等于自身。+0 === -0 //trueNaN === NaN // falseObject.is(+0, -0) // falseObject.is(NaN, NaN) // trueObject.assign()Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。由于undefined和null无法转成对象,所以如果它们作为首参数,就会报错。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}常见用途:为对象添加属性和方法克隆或合并对象给属性指定默认值其他本文参考《ECMAScript 6 入门》,了解更多请点击跳转点击跳转。 ...

March 12, 2019 · 6 min · jiezi

使用 Webpack 与 Babel 配置 ES6 开发环境

使用 Webpack 与 Babel 配置 ES6 开发环境安装 Webpack安装:# 本地安装$ npm install –save-dev webpack webpack-cli# 全局安装$ npm install -g webpack webpack-cli在项目根目录下新建一个配置文件—— webpack.config.js 文件:const path = require(‘path’);module.exports = { mode: ’none’, entry: ‘./src/index.js’, output: { filename: ‘bundle.js’, path: path.resolve(__dirname, ‘dist’) }}在 src 目录下新建 a.js 文件:export const isNull = val => val === nullexport const unique = arr => […new Set(arr)]在 src 目录下新建 index.js 文件:import { isNull, unique } from ‘./a.js’const arr = [1, 1, 2, 3]console.log(unique(arr))console.log(isNull(arr))执行编译打包命令,完成后打开 bundle.js 文件发现 isNull 和 unique 两个函数没有被编译,和 webpack 官方说法一致:webpack 默认支持 ES6 模块语法,要编译 ES6 代码依然需要 babel 编译器。安装配置 Babel 编译器使用 Babel 必须先安装 @babel/core 和 @babel/preset-env 两个模块,其中 @babel/core 是 Babel 的核心存在,Babel 的核心 api 都在这个模块里面,比如:transform。而 @babel/preset-env 是一个智能预设,允许您使用最新的 JavaScript,而无需微观管理您的目标环境需要哪些语法转换(以及可选的浏览器polyfill)。因为这里使用的打包工具是 Webpack,所以还需要安装 babel-loader 插件。安装:$ npm install –save-dev @babel/core @babel/preset-env babel-loader新建 .babelrc 文件:{ “presets”: [ “@babel/preset-env” ]}修改 webpack 配置文件(webpack.config.js):const path = require(‘path’);module.exports = { mode: ’none’, entry: ‘./src/index.js’, output: { filename: ‘bundle.js’, path: path.resolve(__dirname, ‘dist’) }, module: { rules: [ { test: /.js$/, loader: ‘babel-loader’, exclude: /node_modules/ } ] }}由于 babel 默认只转换 ES6 新语法,不转换新的 API,如:Set、Map、Promise等,所以需要安装 @babel/polyfill 转换新 API。安装 @babel/plugin-transform-runtime 优化代码,@babel/plugin-transform-runtime 是一个可以重复使用 Babel 注入的帮助程序代码来节省代码的插件。安装 @babel/polyfill、@babel/plugin-transform-runtime 两个插件:$ npm install –save-dev @babel/polyfill @babel/plugin-transform-runtime修改 .babelrc 配置文件:{ “presets”: [ ["@babel/preset-env", { “useBuiltIns”: “usage”, // 在每个文件中使用polyfill时,为polyfill添加特定导入。利用捆绑器只加载一次相同的polyfill。 “modules”: false // 启用将ES6模块语法转换为其他模块类型,设置为false不会转换模块。 }] ], “plugins”: [ ["@babel/plugin-transform-runtime", { “helpers”: false }] ]}最后,配置兼容的浏览器环境。在 .babelrc 配置文件中设置 targets 属性:{ “presets”: [ ["@babel/preset-env", { “useBuiltIns”: “usage”, “modules”: false, “targets”: { “browsers”: “last 2 versions, not ie <= 9” } }] ], “plugins”: [ ["@babel/plugin-transform-runtime", { “helpers”: false }] ]}执行命令编译代码,完成后检查 bundle.js 文件,是否成功转换新 API 。如果发现以下代码即说明转换成功:// 23.2 Set Objectsmodule.exports = webpack_require(80)(SET, function (get) { return function Set() { return get(this, arguments.length > 0 ? arguments[0] : undefined); };}, { // 23.2.3.1 Set.prototype.add(value) add: function add(value) { return strong.def(validate(this, SET), value = value === 0 ? 0 : value, value); }}, strong);其他关于 js 压缩和 Webpack 启用 tree shaking 功能的设置本文不在赘述。配置文件详情概览package.json 文件:{ “name”: “demo”, “version”: “1.0.0”, “description”: “”, “main”: “index.js”, “scripts”: { “test”: “echo "Error: no test specified" && exit 1”, “dev”: “webpack” }, “keywords”: [], “author”: “”, “license”: “ISC”, “devDependencies”: { “@babel/core”: “^7.3.4”, “@babel/plugin-transform-runtime”: “^7.3.4”, “@babel/polyfill”: “^7.2.5”, “@babel/preset-env”: “^7.3.4”, “babel-loader”: “^8.0.5”, “webpack”: “^4.29.6”, “webpack-cli”: “^3.2.3” }}webpack.config.js 文件:const path = require(‘path’);module.exports = { mode: ’none’, entry: ‘./src/index.js’, output: { filename: ‘bundle.js’, path: path.resolve(__dirname, ‘dist’) }, module: { rules: [ { test: /.js$/, loader: ‘babel-loader’, exclude: /node_modules/ } ] }}.babelrc 文件:{ “presets”: [ ["@babel/preset-env", { “useBuiltIns”: “usage”, “modules”: false, “targets”: { “browsers”: “last 2 versions, not ie <= 9” } }] ], “plugins”: [ ["@babel/plugin-transform-runtime", { “helpers”: false }] ]}符录usuallyjs 项目是本人最近建设的开源项目,欢迎感兴趣的同行交流。usuallyjs: https://github.com/JofunLiang/usuallyjs ...

March 11, 2019 · 2 min · jiezi