ES7+ES8 前言 本篇文章主要介紹ES7+ES8的一些新功能,並結合ES6的一些API做出了相應的比較。 ES7 1.Array.prototype.includes() includes()作用,是查找一個值在不在數組里,若是存在則返回true,不存在返回false. 1.基本用法: ['a', 'b', 'c'].includes('a') // true ['a', 'b', 'c'].includes('d') // false 2.接收倆個參數:要搜索的值和搜索的開始索引 ['a', 'b', 'c', 'd'].includes('b') // true ['a', 'b', 'c', 'd'].includes('b', 1) // true ['a', 'b', 'c', 'd'].includes('b', 2) // false 3.與ES6中的indexOf()比較 有些時候是等效的 ['a', 'b', 'c'].includes('a') //true ['a', 'b', 'c'].indexOf('a') > -1 //true var arr = [1, 2, 3] var a = 1; arr.includes(a) //true arr.indexOf(a) //0 在判斷 +0 與 -0 時,被認為是相同的。 [1, +0, 3, 4].includes(-0) //true [1, +0, 3, 4].indexOf(-0) //1 只能判斷簡單類型的數據,對於復雜類型的數據,比如對象類型的數組,二維數組,這些,是無法判斷的. var arr = [1, [2, 3], 4] arr.includes([2, 3]) //false arr.indexOf([2, 3]) //-1 優缺點比較 簡便性 includes()返回的是布爾值,能直接判斷數組中存不存在這個值,而indexOf()返回的是索引,這一點上前者更加方便。 精確性 兩者都是采用===的操作符來作比較的,不同之處在於:對於NaN的處理結果不同。 我們知道js中 NaN === NaN 的結果是false,indexOf()也是這樣處理的,但是includes()不是這樣的。 let demo = [1, NaN, 2, 3] demo.indexOf(NaN) //-1 demo.includes(NaN) //true 總結: 由於它對NaN的處理方式與indexOf不同,假如你只想知道某個值是否在數組中而並不關心它的索引位置,建議使用includes()。如果你想獲取一個值在數組中的位置,那么你只能使用indexOf方法。 2.求冪運算符 基本用法: 3 ** 2 //9 效果同 Math.pow(3, 2) //9 由於是運算符,所以可以和 +=一樣的用法 var b = 3; b **= 2; console.log(b); //9 ES8 1.async await 異步函數async function() 1.1作用 避免有更多的請求操作,出現多重嵌套,也就是俗稱的“回調地獄” this.$http.jsonp('/login', (res) => { this.$http.jsonp('/getInfo', (info) => { // do something }) }) 因此提出了ES6的Promise,將回調函數的嵌套,改為了鏈式調用: var promise = new Promise((resolve, reject) => { this.login(resolve); }) .then(() => { this.getInfo() }) .catch(() => { console.log('Error') }) 1.2聲明方式 異步函數存在以下四種使用形式: 函數聲明: async function foo() {} 函數表達式: const foo = async function() {} 對象的方式: let obj = { async foo() {} } 箭頭函數: const foo = async () => {} 1.3支持返回Promise和同步的值 async用於定義一個異步函數,該函數返回一個Promise。 如果async函數返回的是一個同步的值,這個值將被包裝成一個理解resolve的Promise,等同於return Promise.resolve(value)。 await用於一個異步操作之前,表示要“等待”這個異步操作的返回值。await也可以用於一個同步的值。 //async await //返回Promise let timer = async function timer() { return new Promise((reslove, reject) => { setTimeout(() => { reslove('a'); }, 1000); }) } timer().then(result => { console.log(result); }).catch(err => { console.log(err.message); }) //返回同步的值 let sayHello = async function sayHello() { let hi = 'hello world'//等同於return Promise.resolve(hi); return hi } sayHello().then(res => { console.log(res) }).catch(err => { console.log(err.message); }) 1.4對異常的處理 首先來看下Promise中對異常的處理 1.使用reject let promise = new Promise((reslove, reject) => { setTimeout(() => { reject('promise使用reject拋出異常') }, 1000) }) promise().then(res => { console.log(res) }) .catch(err => { console.log(err) //'promise使用reject拋出異常' }) 2.使用new Error() let promise = new Promise((reslove, reject) => { throw new Error('promise使用Error拋出異常') //使用throw異常不支持放在定時器中 }) promise().then(res => { console.log(res) }) .catch(err => { console.log(err.message) //'promise使用Error拋出異常' }) 3.reject一個new Error() let promise = new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('promise拋出異常')); }, 1000); }) promise.then(res => { console.log(res); }) .catch(err => { console.log(err.message); //'promise拋出異常' }) async對異常的處理也可以直接用.catch()捕捉到 //async拋出異常 let sayHi = async sayHi => { throw new Error('async拋出異常'); } sayHi().then(res => { console.log(res); }) .catch(err => { console.log(err.message); }) 和Promise鏈的對比: 我們的async函數中可以包含多個異步操作,其異常和Promise鏈有相同之處,如果有一個Promise被reject()那么后面的將不會再進行。 let count = () => { return new Promise((resolve, reject) => { setTimeout(() => { reject('promise故意拋出異常') }, 1000); }) } let list = () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve([1, 2, 3]) }, 1000); }) } let getList = async () => { let c = await count() console.log('async') //此段代碼並沒有執行 let l = await list() return { count: c, list: l } } console.time('start'); getList().then(res => { console.log(res) }) .catch(err => { console.timeEnd('start') console.log(err) }) //start: 1000.81494140625ms //promise故意拋出異常 可以看到上面的案例,async捕獲到了一個錯誤之后就會立馬進入.catch()中,不執行之后的代碼 1.5並行 上面的案例中,async采用的是串行處理 count()和list()是有先后順序的 let c = await count() let l = await list() 實際用法中,若是請求的兩個異步操作沒有關聯和先后順序性可以采用下面的做法 let res = await Promise.all([count(), list()]) return res //res的結果為 //[ 100, [ 1, 2, 3 ] ] 案例詳情為: let count = ()=>{ return new Promise((resolve,reject) => { setTimeout(()=>{ resolve(100); },500); }); } let list = ()=>{ return new Promise((resolve,reject)=>{ setTimeout(()=>{ resolve([1,2,3]); },500); }); } let getList = async ()=>{ let result = await Promise.all([count(),list()]); return result; } console.time('begin'); getList().then(result => { console.timeEnd('begin'); //begin: 505.557ms console.log(result); //[ 100, [ 1, 2, 3 ] ] }).catch(err => { console.timeEnd('begin'); console.log(err); }); 我們將count()和list()使用Promise.all()“同時”執行,這里count()和list()可以看作是“並行”執行的,所耗時間將是兩個異步操作中耗時最長的耗時。 最后得到的結果是兩個操作的結果組成的數組。我們只需要按照順序取出數組中的值即可。 1.6與Generator的關系 先來回顧一下ES6中Generator函數的用法: function* getList() { const c = yield count() const l = yield list() return 'end' } var gl = getList() console.log(gl.next()) // {value: Promise, done: false} console.log(gl.next()) // {value: Promise, done: false} console.log(gl.next()) // {value: 'end', done: true} 雖然Generator將異步操作表示得很簡潔,但是流程管理卻不方便(即何時執行第一階段、何時執行第二階段)。此時,我們便希望能出現一種能自動執行Generator函數的方法。我們的主角來了:async/await。 ES8引入了async函數,使得異步操作變得更加方便。簡單說來,它就是Generator函數的語法糖。 let getList = async () => { const c = await count() const l = await list() } 2.Object.entries() 2.1作用 作用:將一個對象中可枚舉屬性的鍵名和鍵值按照二維數組的方式返回。 若對象是數組,則會將數組的下標作為鍵值返回。 Object.entries({ one: 1, two: 2 }) //[['one', 1], ['two', 2]] Object.entries([1, 2]) //[['0', 1], ['1', 2]] 2.2要點 1.若是鍵名是Symbol,編譯時會被自動忽略 Object.entries({[Symbol()]:1, two: 2}) //[['two', 2]] 2.entries()返回的數組順序和for循環一樣,即如果對象的key值是數字,則返回值會對key值進行排序,返回的是排序后的結果 Object.entries({ 3: 'a', 4: 'b', 1: 'c' }) //[['1', 'c'], ['3', 'a'], ['4', 'b']] 3.利用Object.entries()創建一個真正的Map var obj = { foo: 'bar', baz: 42 }; var map1 = new Map([['foo', 'bar'], ['baz', 42]]); //原本的創建方式 var map2 = new Map(Object.entries(obj)); //等同於map1 console.log(map1);// Map { foo: "bar", baz: 42 } console.log(map2);// Map { foo: "bar", baz: 42 } 2.3自定義Object.entries() Object.entries的原理其實就是將對象中的鍵名和值分別取出來然后推進同一個數組中 //自定義entries() var obj = { foo: 'bar', baz: 42 }; function myEntries(obj) { var arr = [] for (var key of Object.keys(obj)) { arr.push([key, obj[key]]) } return arr } console.log(myEntries(obj)) //Generator版本 function* genEntryies(obj) { for (let key of Object.keys(obj)) { yield [key, obj[key]] } } var entryArr = genEntryies(obj); console.log(entryArr.next().value) //["foo", "bar"] console.log(entryArr.next().value) //["baz", 42] 3.Object.values() 3.1作用 作用:只返回自己的鍵值對中屬性的值。它返回的數組順序,也跟Object.entries()保持一致 Object.values({ one: 1, two: 2 }) //[1, 2] Object.values({ 3: 'a', 4: 'b', 1: 'c' }) //['c', 'a', 'b'] 3.2與Object.keys()比較 ES6中的Object.keys()返回的是鍵名 var obj = { foo: 'bar', baz: 42 }; console.log(Object.keys(obj)) //["foo", "baz"] console.log(Object.values(obj)) //["bar", 42] //Object.keys()的作用就類似於for...in function myKeys() { let keyArr = [] for (let key in obj1) { keyArr.push(key) console.log(key) } return keyArr } console.log(myKeys(obj1)) //["foo", "baz"] 3.3entries()、values()總結 var obj = { foo: 'bar', baz: 42 }; console.log(Object.keys(obj)) //["foo", "baz"] console.log(Object.values(obj)) //["bar", 42] console.log(Object.entries(obj)) //[["foo", "bar"], ["baz", 42]] 4.字符串填充 4.1padStart()和padEnd() 字符串填充padStart()和padEnd() 用法 String.padStart(targetLength, padding) 參數:字符串目標長度和填充字段 'Vue'.padStart(10) //' Vue' 'React'.padStart(10) //' React' 'JavaScript'.padStart(10) //'JavaScript' 4.2要點 1.填充函數只有在字符長度小於目標長度時才有效,而且目標長度如果小於字符串本身長度時,字符串也不會做截斷處理,只會原樣輸出 'Vue'.padEnd(10, '_*') //'Vue_*_*_*_' 'React'.padEnd(10, 'Hello') //'ReactHello' 'JavaScript'.padEnd(10, 'Hi') //'JavaScript' 'JavaScript'.padEnd(8, 'Hi') //'JavaScript' 5.Object.getOwnPropertyDescriptors() 5.1作用 該方法會返回目標對象中所有屬性的屬性描述符,該屬性必須是對象自己定義的,不能是從原型鏈繼承來的。 var obj = { id: 1, name: '草泥馬', get gender() { console.log('gender') }, set grad(d) { console.log(d) } } console.log(Object.getOwnPropertyDescriptors(obj)) //輸出 { gender: { configurable: true, enumerable: true, get: f gender(), set: undefined }, grade: { configurable: true, enumerable: true, get: undefined, set: f grade(g) }, id: { configurable: true, enumerable: true, value: 1, writable: true }, name: { configurable: true, enumerable: true, value: '草泥馬', writable: true } } 5.2與getOwnPropertyDescriptor()比較 ES6中也有一個返回目標對象可枚舉屬性的方法 var obj = { id: 1, name: '草泥馬', get gender() { console.log('gender') }, set grad(d) { console.log(d) } } console.log(Object.getOwnPropertyDescriptor(obj, 'id')) //輸出結果 { id: { configurable: true, enumerable: true, value: 1, writable: true } } 兩者的區別:一個是只返回知道屬性名的描述對象,一個返回目標對象所有自身屬性的描述對象 5.3自定義該方法 function myDescriptors(obj) { let descriptors = {} for (let key in obj) { descriptors[key] = Object.getOwnPropertyDescriptor(obj, key) } return descriptors } console.log(myDescriptors(obj)) //返回的結果和該方法一樣 //其中上面自定義方法的for...in也可以換成,效果也是一樣的 for (let key of Object.keys(obj)) { descriptors[key] = Object.getOwnPropertyDescriptor(obj, key) } 6.函數參數支持尾部逗號 該特性允許我們在定義或者調用函數時添加尾部逗號而不報錯 let foo = function ( a, b, c, ) { console.log('a:', a) console.log('b:', b) console.log('c:', c) } foo(1, 3, 4, ) //輸出結果為: a: 1 b: 3 c: 4 它適用於那種多行參數並且參數名很長的情況,開發過程中,如果忘記刪除尾部逗號也沒關系,ES8已經支持這種寫法。 7.修飾器Decorator ES8神器Decorator,修飾器,也稱修飾器模式 7.1 偽Decorator 在介紹Decorator之前,我們先來實現這樣一個功能: 定義一個函數,在調用這個函數時,能夠執行一些其他額外操作 如下代碼,定義doSometing(),在調用它時再執行其他代碼 function doSometing(name) { console.log('Hello' + name) } function myDecorator(fn) { return function() { console.log('start') const res = fn.apply(this, arguments) console.log('end') return res } } const wrapped = myDecorator(doSometing) doSometing('caonima') //Hellowcaonima wrapped('caonima') //start //Hellowcaonima //end 可以看到上面的操作:其實就是一個函數包裝成另一個函數,這樣的方式我們稱之為“修飾器” 同理,我們是不是能用一個什么東西附着在我們的類或者類的屬性上,讓它們也有一些附加的屬性或者功能呢,比如這樣: @addSkill class Person { } function addSkill(target) { target.say = "hello world"; } 在Person這個類中,開始定義的時候是什么屬性都沒有的,在其上面使用@來附着上一個函數,這個函數的功能是給目標對象添加額外的屬性say。 這樣Person這個類就有了say這個屬性了。 此時控制台輸出: console.log(Person['say']) //'hello world' 同樣的,如果想使用Person這個類創建出來的對象也能附加上一些屬性,可以在目標對象的原型對象中進行添加: @addSkill class Person { } function addSkill(target) { target.say = "hello world"; //直接添加到類中 target.prototype.eat = "apple"; //添加到類的原型對象中 } var personOne = new Person() console.log(Person['say']) // 'hello world' console.log(personOne['eat']) // 'apple' 上面案例中的@addSkill其實就是一個最簡單的修飾器。 當然,如果你將上面案例中的代碼復制到你html文件中,會發現它並不能如願的執行: image.png 那是因為decorator是es7提供的方法,在瀏覽器中是無法直接運行的,如果你想要使用它,我們需要提前做一些准備,對它進行編譯。 如果你不想深入其中,只是想單純的了解並使用它可以參考下面的簡易教程。 7.2 快速使用 網上使用Decorator的教材有很多,大多都是要需要使用插件來讓瀏覽器支持Decorator。這里長話短說,貼上一個最精簡的使用教程: 1.創建一個名為:Decorator的文件夾 2.在文件夾目錄下執行命令行 npm i babel-plugin-transform-decorators-legacy babel-register --save-dev 此時文件夾下會出現倆個文件: node_modules 依賴文件夾和package.json-lock.json 3.創建文件 complie.js require('babel-register')({ plugins: ['transform-decorators-legacy'] }); require("./app.js") 4.創建文件 app.js @addSkill class Person { } function addSkill(target) { target.say = "hello world"; } console.log(Person.say) //'hello world' 5.在根目錄下執行指令: node complie.js 此時可以看到命令行中打印出了 hello world 簡單介紹下上面步驟的原理: 第二步中使用了倆個基礎插件: transform-decorators-legacy: //是第三方插件,用於支持decorators babel-register: //用於接入node api 第三步、第四步創建的倆個文件 complie.js //用來編譯app app.js //使用了裝飾器的js文件 第五步: 原理: 1,node執行complie.js文件; 2,complie文件改寫了node的require方法; 3,complie在引用app.js,使用了新的require方法; 4,app.js在加載過程中被編譯,並執行。 當然你也可以將app.js替換為app.ts 不過別忘了把complie.js中的app.js修改為app.ts // app.ts @addSkill class Person { } function addSkill(target) { target.say = "hello world"; } console.log(Person['say']) //這里如果直接使用Person.say會提示say屬性不存在,如我使用的vscode編輯器就會報錯,是因為ts的原因,只需要用[]的形式獲取對象屬性即可。 注:ts中有些語法是和js中不一樣的,比如有些對象上提示沒有屬性的時候,只需要換一種獲取對象屬性的方式即可。 7.3 類修飾器 直接作用在類上面的修飾器,我們可以稱之為類修飾器。 如上面案例中的@addSkill就是一個類修飾器,它修改了Person這個類的行為,為它加上了靜態屬性say。 addSkill函數的參數target是Person這個類本身。 1.修飾器的執行原理基本就是這樣: @decorator class A {} // 等同於 class A {} A = decorator(A) || A; 換句話說,類修飾器是一個對類進行處理的函數。 它的第一個參數target就是函數要處理的目標類。 2.多參數 當然如果你想要有多個參數也是可以的,我們可以在修飾器外面再封裝一層函數: @addSkill("hello world") class Person { } function addSkill(text) { return function(target) { target.say = text; } } console.log(Person.say) //'hello world' 上面代碼中,修飾器addSkill可以接受參數,這就等於可以修改修飾器的行為。 3.修飾器在什么時候執行。 先來看一個案例: @looks class Person { } function looks(target) { console.log('I am handsome') target.looks = 'handsome' } console.log(Person['looks']) //I am handsome //handsome 在修飾器@looks中添加一個console.log()語句,卻發現它是最早執行的,其次才打印出handsome。 這是因為裝飾器對類的行為的改變,是代碼編譯時發生的,而不是在運行時。這意味着,裝飾器能在編譯階段運行代碼。也就是說,裝飾器本質就是編譯時執行的函數。 裝飾器是在編譯時就執行的函數 7.4 方法修飾器 上面的案例中,修飾器作用的對象是類本身。 當然修飾器不僅僅這么簡單,它也可以作用在類里的某個方法或者屬性上,這樣的修飾器我們稱它為方法修飾器。 如下面的案例: class Person { constructor() {} @myname //方法修飾器 name() { console.log('草泥馬') } } function myname(target, key, descriptor) { console.log(target); console.log(key); console.log(descriptor); descriptor.value = function() { console.log('草泥馬') } } var personOne = new Person() //實例化 personOne.name() //調用name()方法 //打印結果: Person {} name { value: [Function: name], writable: true, enumerable: false, configurable: true } 草泥馬 上面案例中的修飾器@myname是放在name()方法上的,myname函數有三個參數: target: 類的原型對象,上例是Person.prototype key: 所要修飾的屬性名 name descriptor: 該屬性的描述對象 我們改變了descriptor中的value,使之打印出草泥馬。 7.5 多個修飾器的執行順序 若是同一個方法上有多個修飾器,會像剝洋蔥一樣,先從外到內進入,然后由內向外執行。 class Person { constructor() {} @dec(1) @dec(2) name() { console.log('草泥馬') } } function dec(id) { console.log('out', id); return function(target, key, descriptor) { console.log(id); } } var person = new Person() person.name() //結果 out 1 out 2 2 1 草泥馬 如上所屬,外層修飾器dec(1)先進入,但是內層修飾器dec(2)先執行。 7.6 不能作用於函數 修飾器不能作用於函數之上,這是因為函數和變量一樣都會提升 var counter = 0; var add = function () { counter++; }; @add function foo() { } 如上面的例子所示,給函數foo()定義了修飾器@add,作用是想將counter++ 預計的結果counter為1,但實際上卻還是為0 原因: 定義的函數foo()會被提升至最上層,定義的變量counter和add也會被提升,效果如下: @add function foo() { } var counter; var add; counter = 0; add = function () { counter++; }; 總之,由於存在函數提升,使得修飾器不能用於函數。類是不會提升的,所以就沒有這方面的問題。 另一方面,如果一定要修飾函數,可以采用高階函數的形式直接執行。 如在7.1中的例子所示: function doSometing(name) { console.log('Hello' + name) } function myDecorator(fn) { return function() { console.log('start') const res = fn.apply(this, arguments) console.log('end') return res } } const wrapped = myDecorator(doSometing) doSometing('caonima') //Hellowcaonima wrapped('caonima') //start //Hellowcaonima //end