let && const
let與var的聲明用法相同,但是多了一個臨時死區(Temporal Distonrtion Zone)的概念。
console.log(a)// -> undefinedvara =1console.log(b)// -> Uncaught ReferenceError: b is not definedletb =1
可以發現在聲明前使用let聲明的變量會導致報錯,這解決了 JS 很多奇怪的問題。并且使用let會生成一個塊級作用域,作用域外不能訪問該變量。
{leta =1;varb =1;}console.log(b);// -> 1console.log(a);//? -> Uncaught ReferenceError: b is not defined
在 JS 中,聲明變量都會提升,不論用什么關鍵字聲明。當使用let時變量也會被提升至塊級作用域的頂部,但是只提升聲明,不提升初始化。并且會產生臨時死區,該區域會存放變量,直到執行過聲明語句后,方可使用該變量。
在循環中let會與前面有些不同,每次迭代都會產生一個新的變量,并用之前的值初始化,如何理解這句話呢,請看以下代碼。
for(leti =0; i <10; i++) {console.log(i)// -> 輸入 0 - 9}// 上面的循環代碼可以這樣看{// 形成塊級作用域leti =0{letii = iconsole.log(ii)? ? }? ? i++? ? {letii = iconsole.log(ii)? ? }? ? i++? ? {letii = iconsole.log(ii)? ? }? ? ...}
const與let基本類似,只是用const聲明必須賦值,并且不得修改綁定,什么意思呢,請看代碼。
consta =1;a =2// -> Uncaught TypeError: Assignment to constant variable// butconstb = {a:1};b.a =2// 起效
當然了,有辦法讓這個不能改變
constb =Object.freeze({a:1})b.a =2// 沒有報錯,但是 b.a 沒有被改變
但是Object.freeze只能在這里有效,對于數組這些可以看看這個提案。
這兩個新的聲明方式在全局作用域下不會自動加上window
字符串相關
部分新增的字符串函數
letstring ='startend'string.includes('a')// -> true 是否包含string.endsWith('end')// -> true 是否由 end 結尾string.startsWith('start')// -> true 是否由 start 開頭
模板字面量
很棒的新功能,解決了之前很多麻煩的寫法。
// 語法就是 `` 代替之前的引號,在 `` 中使用引號不需要轉義lets =`it's string`
多行字符串
// 這樣在語法中就可以換行了lets =`start \
end`// 注意在模板字面量中的任何空白符都是起效的lets =`start \
? ? end`// ->? start? ? end
占位符和標簽模板
lets ='string'letmessage =`start${s}`// -> startstring// ${} 就是占位符語法,可以更簡便的實現字符串插入// 定義一個 tag 函數,然后直接在 `` 前使用就可以letm = tag`s${s}e${message}`// strings 是一個數組,value 是模板字面量中所有的占位符的值functiontag(strings, ...value){// -> ['s', 'e', '']console.log(strings)// -> ['string', 'startstring']console.log(value)}// 上面的 ...value 也是 ES6新出的擴展語句,在這里代表不定參數的寫法,用于替換 arguments// 不定參數使用上也是有限制的,必須放在所有參數的末尾,并且在每個函數中只能聲明一次// 擴展語句和 arguments 區別就是代表了 strings 參數后面的所有參數// 除了上面的寫法,還可以用于展開可以迭代(有Symbol.iterator屬性)的對象letarray = [1,2,3]console.log(...array)// 該語法可以解決之前很多地方只能傳入單個參數,只能使用 apply 解決的問題Array.prototype.unshift.apply([4,5], array)// -> [1, 2, 3, 4, 5]// 現在可以直接這樣寫[4,5].unshift(...array)// 展開運算不受不定參數的條件限制,可以一起用
函數
默認參數
ES6 允許給函數增加默認參數
functionfn(a =1, b =2){}// 默認值也可以通過調用函數獲得,注意必須調用函數functionfn1(a =1, b = fn()){}
新增函數內部方法
在 JS 中,函數有多種用法,可以直接調用,也可以通過new構造函數。
在 ES6中,函數內部新增了 [[Call]] 和 [[Construct]] 兩個方法。后者會在使用new構造函數時執行,其他情況會執行前者方法。
當一個函數必須使用new構造時,你可以使用這個新屬性new.target判斷
// new.target 只能在函數中使用functionFn(){if(typeofnew.target ==='underfined') {throw....... }}
箭頭函數
這個特性真的很棒,先介紹下他的幾種語法
// 最簡單的寫法,只有一個參數,單行表達式value => value// 多個參數需要使用小括號包裹(v1, v2) => v2 + v1// 沒有參數需要使用小括號包裹() =>"balabala"http:// 多行表達式需要大括號包裹(v1, v2) => {returnv1 + v2}// 返回一個對象,需要用小括號包裹() => ({a:1})// 立即執行函數,注意普通的立即執行函數的小括號包裹在最外面,箭頭函數不需要((value) =>value)("balabala")
箭頭函數和普通函數區別還是蠻大的,說幾個常用的
沒有this,不能改變this綁定
不能通過new調用,當然也沒有原型
沒有arguments對象,不能有相同命名參數
箭頭函數雖然沒有this,但是還是可以在內部使用this的
this的綁定取決于定義函數時的上下文環境
一旦函數調用,任何改變this的方法都無效
// let 有個細節letx =11111leta = {x:1,? ? init() {// 箭頭函數的 this 取決于 init,所以可以打印出 1document.addEventListener('click', () =>console.log(this.x))? ? },allowInit:()=>{// allowInit 直接是個箭頭函數,所以這時的 this 變成了 window// 但是并不會打印出 11111,忘了 let 的一個細節的可以回到上面看看console.log(this.x))? ? }? ? otherInit() {// 普通函數的 this 取決于調用函數的位置,this 指向 document// 如果想打印出 x,可以使用 binddocument.addEventListener('click',function(){console.log(this.x)? ? ? ? })? ? }}a.init()// -> 1a.allowInit()// -> undefineda.otherInit()// -> undefined
對象相關
leta =1// 當 key 和 value 名字相同時可以簡寫letb = { a }// 對象中的方法也可以簡寫leta = {? ? init() {}}// 對象屬性名也可以計算 letname ='name'b[name +'1'] =2// === b['name1'] = 2
ES6 也新增了幾個對象方法
Object.is(NaN,NaN)// ->true// 結果基本于 === 相似,除了 NaN 和 +0 -0Object.is(+0,-0)// -> falseleto = {a:1}leta =Object.assign({}, o)// -> {a: 1}// 第一個參數為目標參數,后面的參數是不定的,參數屬性名如果有重復,后面的會覆蓋之前的
原型相關
ES6 之前改變對象原型很麻煩
letobj = {a:1}letobj1 = {a:2}// 已 obj 為原型leta =Object.create(obj)// 改變 a 的原型為 obj1Object.setPrototypeOf(a, obj1)// a.a === 2
訪問原型
Object.getPrototypeOf(a)// 訪問原型// ES6 中可以直接通過 super 代表原型leta = {? ? init() {return'Hello'}}letb = {? ? init() {// 不能在 super 之前訪問 thisreturnsuper.init() +'World'}}Object.setPrototypeOf(b, a)b.init()// -> 'HelloWorld'
但是 super 不是每個函數都可以使用的,只有在函數的簡寫語法中方可使用。因為在 ES6中新增了一個函數內部屬性 [[HomeObject]],這個屬性決定了是否可以訪問到super。首先在該屬性上調用Object.getPrototypeOf(綁定的對象),然后找到原型中的同名函數,在設置this綁定并且調用函數,其實就是一個新增的語法糖。
解構賦值
該特性可以用于對象,數組和傳參。
letobj = {a:1,b:2}// 對象解構使用 {},數組解構使用 [],因為這里是對象解構,c 不是 obj 的屬性,所以 underfined// 數組解構中,如果需要解構的變量大于數組索引,多出來的變量也是 undefined// 解構必須賦值,否則報錯。不能 let {a, b, c};// 賦值不能為 null 或者 undefined,會報錯let{a, b, c} = obj// 等于 let a = obj.a,可以看做之前介紹的對象屬性簡寫console.log(a, b, c)// -> 1, 2, underfined// 如果已經聲明了變量并且想使用解構,必須最外面是小括號({a, b} = obj)// 如果不想使用 obj 中的對象名,又想使用解構賦值let{x: a} = obj// 如果想使用默認值let{a =2, c=3} = obj// -> 1, 3// 因為 a 是 obj 中的對象,所以默認值被覆蓋// 解構也可以嵌套letobj = {data: {code:1},message: [1,2]}// 這個寫法在 json 中很好用// 注意在這個寫法中,data 和 message 都是指代了 obj 的屬性,并沒有被聲明變量let{data: {code},message: [a] } = objconsole.log(code, a)// 數組解構和對象解構基本相似,并且簡單多了letmessage = [1,2,3,4]// 因為數組取值只能索引取,所以想跳過某幾個索引,就用逗號代替// 同樣,數組解構也可以使用默認值和嵌套解構,和對象解構一模一樣就不贅述了let[a, , b] = message// -> 1, 3// 在上面章節介紹了擴展語法,同樣也可以使用在數組解構中// 可以看到 b 變成了一個數組let[a, ...b] = message// -> 1, [2, 3, 4]// 傳參使用解構可以讓要傳的參數更加清晰functionfn(name, {key, value}){console.log(name, key, value)}// 使用,注意:傳參解構必須起碼傳入一個值,否則報錯fn(1, {key:2,value:3})// 因為傳參解構類似以下寫法functionfn(name, {key, value}){let{key, value} =null// 這個上面講過不能這樣寫}
Symbol
ES6 新出的第六個原始類型。多用于避免代碼沖突,作為一個私有屬性使用,不會被屬性遍歷出來。可以使用Object.getOwnPropertySymbols()檢索 Symbol 屬性。
創建和使用
// 創建leta =Symbol()// 更推薦這種寫法,可以更加明確這個Symbol的用途// 并且有函數可以通過這個字符串取到相應的Symbolletb =Symbol('is b')// 使用,一般作為可計算屬性使用leta = {}letb =Symbol('is b') a[b] =1// 可以在全局注冊表中共享同一個 Symbol,但不推薦使用// 不存在 is a 會自動創建leta =Symbol.for('is a')
暴露內部操作
Symbol 中預定義了一些 well-know Symbol,這些 Symbol 定義了一些語言的內部實現
Symbol.hasinstance,用于執行instanceof時檢測對象的繼承信息
Symbol.isConcatSpreadable,布爾值,用于判斷當使用concat函數時是否將數組展開
Symbol.iterator,迭代器,后面會講到
Symbol.match,Symbol.replace,Symbol.search,Symbol.split,字符串相關方法的對應內部實現
Symbol.toPrimitive,返回對象原始值
Symbol.toStringTag,調用toString
Set 和 Map
Set
Set 是新增的無重復的有序集合,多用于集合去重或者判斷集合中是否含有某個元素。
// 創建letset =newSet()// 添加元素set.add(1)set.add('1')// 重復的元素不會被添加set.add(1)// 判斷是否包含元素set.has(1)// -> true// 判斷長度set.size()// -> 2// 刪除某個元素set.delete()// 移除所有元素set.clear()
Map
Map 是新增的有序鍵值對列表,鍵值可以是任何類型。
// 創建letmap =newMap()// 設置鍵值對map.set('year',"2017")map.set({},'obj')// 取值map.get('year')// -> '2017'// 判斷是否有該鍵值map.has('year')// -> true// 獲得長度map.size()// -> 2// 刪除某個鍵值map.delete('year')// 移除所有鍵值map.clear()
迭代器和 Generator 函數
迭代器
顧名思義,用來迭代的。之前介紹過Symbol.iterator,可以迭代的對象都有這個屬性,包括數組,Set,Map,字符串和 NodeList。ES6新增的for-of就用到了迭代器的功能,但是默認只有上面這些對象能使用。
leta = [1,2]for(letvalueofa) {console.log(value)// -> 1, 2}// 上面的代碼其實就是調用了數組的默認迭代器letiterator = a[Symbol.iterator]()// 當調用 next 時會輸出這次迭代的 value 和是否迭代完成console.log(iterator.next())// {value: 1, done: false}console.log(iterator.next())// {value: 2, done: false}// 已經沒元素可以迭代了console.log(iterator.next())// {value: undefined, done: true}// 數組的默認迭代器只會輸出 value,如果想同時輸出索引的話// 這里可以使用新特性數組解構 let [index, value]for(letvalueofa.entries()) {console.log(value)// -> [0, 1]? [1, 2]}
對于自己創建的對象都是不可迭代的,當然我們也可以讓他變成迭代的
leta = {array: [],// 這是一個 Generator 函數,馬上就會講到*[Symbol.iterator]() {for(letiteminthis.array) {yielditem? ? ? ? }? ? }}a.array.push(...[1,2,3])for(letitemofa) {console.log(item)}
Generator 函數
用于異步編程。該函數可以暫停和恢復執行,和同步寫法很像。
// 星號表示這是一個 Generator 函數function*gen(){// 第一次 next 只執行到等號右邊letfirst =yield1// 第二次 next 執行 let first = 和 yield 2letsecond =yield2// 不執行接下來的 next 就卡在上一步了letthrid =yield3}letg = gen()g.next()// -> {value: 1, done: false}g.next()// -> {value: 2, done: false
接下來看下 Generator 函數如何用于異步
functiongetFirstName(){? ? setTimeout(function(){? ? ? ? gen.next('alex')? ? },1000);}functiongetSecondName(){? ? setTimeout(function(){? ? ? ? gen.next('perry')? ? },2000);}function*sayHello(){vara =yieldgetFirstName();varb =yieldgetSecondName();// settimeout 本來是異步的,通過 Generator 函數寫成了同步寫法console.log(a, b);// ->alex perry}vargen = sayHello();gen.next();
類
JS 中的類不是其他語言中的類,只是個語法糖,寫法如下。
classPerson{// 構造函數constructor() {this.name = name? ? }? ? sayName() {console.log(this.name)? ? }}letp =newPerson('name')p.sayName()// -> 'name'// class 就是以下代碼的語法糖// 對應 constructorfunctionPerson(name){this.name = name}// 對應 sayNamePerson.prototype.sayName =function(){console.log(this.name)}
類聲明相比之前的寫法有以下幾點優點
類聲明和 let 聲明一樣,有臨時死區
類聲明中的代碼全部運行在嚴格模式下
必須使用 new 調用
繼承
在 ES6 之前寫繼承很麻煩,既然有個類,那么必然也可以繼承類了
classPerson{// 構造函數constructor() {this.name = name? ? }? ? sayName() {console.log(this.name)? ? }}// extends 代表繼承自PersonclassStudentextendsPerson{constructor(name, age) {// super 的注意事項之前有說過super(name)// 必須在 super 之后調用 thisthis.age = age? ? }? ? sayName() {// 如果像使用父類的方法就使用這個方法使用// 不像使用的話就不寫 super,會覆蓋掉父類的方法super.sayName(this.name)console.log(this.age)? ? }}
Promise
概念
用于異步編程。
// 你可以使用 new 創建一個 Promise 對象letpromise =newPromise(function(resolve, reject)){}resole()// 代表成功reject()// 代表失敗promise.then(onFulfilled, onRejected)// 當調用 resole 或者 reject ,then 可以監聽到promise.catch()// reject 或者 throw時可以監聽到
Promise 有三個狀態
pending,等待狀態,也就是既不是 resolve 也不是 reject 狀態
fulfilled,resolve 以后進入這個狀態
reject,reject 以后進入這個狀態
一般使用情況
functiondelay(){// 創建一個 promisereturnnewPromise((resolve, reject) =>{// 當調用 promise 時,里面的內容會立即執行console.log('in delay')? ? ? ? setTimeout(()=>{? ? ? ? ? resolve(1)? ? ? ? },1000)? ? });}functionotherDelay(){returnnewPromise((resolve, reject) =>{? ? ? ? setTimeout(()=>{? ? ? ? ? reject(1)? ? ? ? },1000)? ? });}// 這里會先輸出 delay 函數中的 log,然后再輸出 outer,接下來1秒以后輸出3個1delay()// then 可以捕獲 resolve 和 reject.then((value) =>{console.log(value)? ? })console.log('outer')otherDelay()// 捕獲 reject時,如果不需要捕獲 resolve 時可以這樣寫.then(null, (value) => {console.log(value)? ? })// 捕獲 reject 或者 throw 時推薦使用這個寫法,原因后面會說otherDelay()? ? .catch((value) =>{console.log(value);? ? })
以上是最常用的 Promise 寫法,現在介紹 Promise 鏈
delay()// then 會返回一個新的 promise 對象.then((value) =>{// 這樣就可以傳參了returnvalue +1}).then((value) =>{console.log(value)// -> 2// then 里面可以也可以傳入一個函數名,會自動調用// 如果傳入的函數有參數會自動傳入}).then(delay).then((value) =>{console.log(value)// -> 1// 如果在then 中拋出錯誤,只有 catch 才能監聽到,所以推薦使用 catch 監聽錯誤thrownewError('error')? ? }).then((value) =>{console.log(value)// 這個then 不會執行}).catch((error) =>{console.log('catch'+ error)// -> catch Error})
Promise 高級用法
開發中可能會有需求,需要一次上傳幾張圖片,全部上傳成功以后有個提示,這時候就可以用到Promise.all()
functionupdateOne(){returnnewPromise((resolve, reject) =>{? ? ? ? setTimeout(()=>{? ? ? ? ? ? resolve('one')? ? ? ? ? ? },1000)? ? });}functionupdateTwo(){returnnewPromise((resolve, reject) =>{? ? ? ? setTimeout(()=>{? ? ? ? ? ? resolve('two')? ? ? ? ? ? },2000)? ? });}// all 函數接收一個可迭代對象,注意這里傳入函數必須調用letpromise =Promise.all([updateOne(), updateTwo()])// 只有當 all 中的異步全部完成了才會調用 thenpromise? ? .then((value) =>{// value 是個函數,順序按照 all 里的迭代對象的順序console.log(value)// -> ["one", "two"]})
如果一個異步任務超時了,你想直接取消,可以通過Promise.race()
// 假設該任務執行時間超過1秒就算超時,應該 cancelfunctiondelay(){returnnewPromise((resolve, reject) =>{? ? ? ? setTimeout(function(){? ? ? ? ? ? resolve('finish')? ? ? ? },1500);? ? });}functioncancel(){returnnewPromise((resolve, reject) =>{? ? ? ? setTimeout(function(){? ? ? ? ? ? resolve('cancel')? ? ? ? },1000);? ? });}// 接收的參數和 all 相同letpromise =Promise.race([delay(), cancel()])// race 中只要有一個任務完成,then 就會被調用,這樣就可以 cancel 掉所有超時任務promise? ? .then((value) =>{console.log(value)// -> cancel})
Proxy
Proxy 可以創建一個代替目標對象的代理,攔截語言內部的操作。
lethandle = {}lettarget = {}// 這樣就創建了target 對象的代理,但是這個代理其實沒有任何用處letp =newProxy(target, handle)
上面的代碼中可以看到傳入了一個handle的對象,只有當這個對象中包含一些代理行為的函數時,這個代理才有用。具有的代理行為函數可以去MDN查看,這里舉例幾個用法。
lethandle = {// 改變 set 的內部操作set(target, key, value) {// 當給 age 屬性賦值小于19時報錯console.log(value)if(key ==='age') {if(value <19) {thrownewError('未成年')? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? }lettarget = {}letp =newProxy(target, handle)p.age =1// -> 報錯p.age =19// -> 沒問題
模塊化
ES6 引入了原生的模塊化,這樣就可以拋棄之前的 AMD 或者 CMD 規范了,如果對模塊化還沒什么了解,可以看下我之前的文章明白 JS 模塊化。
// example.js 文件下// export 可以導出任何變量,函數或者類exportvarage =14exportfunctionsum(n1, n2){returnn1 + n2}exportclassPerson{constructor(age) {this.age = age? ? }}// 別的 JS 文件中導入// 如果想導入整個模塊并且自己命名,就可以這樣使用// import 后面代表模塊名,from 后面代表要導入的文件地址import*asExamplefrom'./example'console.log(Example.age)// -> 14// 當然你也可以只使用模塊中的一個功能// 這里使用了對象解構的方法拿到需要的功能,注意這里名字必須相同,否則使用會報錯import{ age, sum }from'./example'console.log(age)// -> 14console.log(sum(1,2))// -> 3// 現在我只想導出一個功能,并且外部可以隨便命名該如何做呢?// default 一個文件中只能使用一次exportdefaultvarage =14// MyAge 可以隨便自己喜歡命名importMyAgefrom'./example'console.log(MyAge)// -> 14