近一萬字的ES6語法知識點補充

原文鏈接分享在我的掘金賬號上近一萬字的ES6語法知識點補充

前言

ECMAScript 6.0(簡稱ES6)嗜侮,作為下一代JavaScript的語言標準正式發(fā)布于2015 年 6 月居凶,至今已經(jīng)發(fā)布3年多了,但是因為蘊含的語法之廣斟赚,完全消化需要一定的時間奕巍,這里我總結了部分ES6呵燕,以及ES6以后新語法的知識點,使用場景貌夕,希望對各位有所幫助

本文講著重是對ES6語法特性的補充律歼,不會講解一些API層面的語法,更多的是發(fā)掘背后的原理啡专,以及ES6到底解決了什么問題

如有錯誤,歡迎指出,將在第一時間修改,歡迎提出修改意見和建議

話不多說開始ES6之旅吧~~~

let/const(常用)

let,const用于聲明變量险毁,用來替代老語法的var關鍵字,與var不同的是们童,let/const會創(chuàng)建一個塊級作用域(通俗講就是一個花括號內(nèi)是一個新的作用域)

這里外部的console.log(x)拿不到前面2個塊級作用域聲明的let:


image

在日常開發(fā)中多存在于使用if/for關鍵字結合let/const創(chuàng)建的塊級作用域畔况,值得注意的是使用let/const關鍵字聲明變量的for循環(huán)和var聲明的有些不同

image

for循環(huán)分為3部分,第一部分包含一個變量聲明慧库,第二部分包含一個循環(huán)的退出條件跷跪,第三部分包含每次循環(huán)最后要執(zhí)行的表達式,也就是說第一部分在這個for循環(huán)中只會執(zhí)行一次var i = 0齐板,而后面的兩個部分在每次循環(huán)的時候都會執(zhí)行一遍

image

而使用使用let/const關鍵字聲明變量的for循環(huán)吵瞻,除了會創(chuàng)建塊級作用域,let/const還會將它綁定到每個循環(huán)中甘磨,確保對上個循環(huán)結束時候的值進行重新賦值

什么意思呢橡羞?簡而言之就是每次循環(huán)都會聲明一次(對比var聲明的for循環(huán)只會聲明一次),可以這么理解let/const中的for循環(huán)

給每次循環(huán)創(chuàng)建一個塊級作用域:


image

暫時性死區(qū)

使用let/const聲明的變量济舆,從一開始就形成了封閉作用域卿泽,在聲明變量之前是無法使用這個變量的,這個特點也是為了彌補var的缺陷(var聲明的變量有變量提升)

image

在預編譯的階段,JS編譯器會先解析一遍判斷是否有l(wèi)et/const聲明的變量,如果在一個花括號中存在使用let/const聲明的變量,則ES6規(guī)定這些變量在沒聲明前是無法使用的,隨后再是進入執(zhí)行階段執(zhí)行代碼

這里當滿足if的條件時,進入true的邏輯,這里因為使用了let聲明了變量name,在一開始就"劫持了這個作用域",使得任何在let聲明之前使用name的操作都會報錯

image

使用var聲明的變量,因為會有變量提升,同樣也是發(fā)生在預編譯階段,var會提升到當前函數(shù)作用域的頂部并且默認賦值為undefined,如果這幾行代碼是在全局作用域下,則name變量會直接提升到全局作用域,隨后進入執(zhí)行階段執(zhí)行代碼,name被賦值為"abc",并且可以成功打印出字符串a(chǎn)bc

相當于這樣

image

暫時性死區(qū)其實是為了防止ES5以前在變量聲明前就使用這個變量,這是因為var的變量提升的特性導致一些不熟悉var原理的開發(fā)者習以為常的以為變量可以先使用在聲明,從而埋下一些隱患

關于JS預編譯和JS的3種作用域(全局,函數(shù),塊級)這里也不贅述了,否則又能寫出幾千字的博客,有興趣的朋友自行了解一下,同樣也有助于了解JavaScript這門語言

const

使用const關鍵字聲明一個常量滋觉,常量的意思是不會改變的變量签夭,const和let的一些區(qū)別是

  1. const聲明變量的時候必須賦值,否則會報錯椎侠,同樣使用const聲明的變量被修改了也會報錯
image
  1. const聲明變量不能改變第租,如果聲明的是一個引用類型,則不能改變它的內(nèi)存地址(這里牽扯到JS引用類型的特點我纪,有興趣可以看我另一篇博客對象深拷貝和淺拷貝
image

有些人會有疑問慎宾,為什么日常開發(fā)中沒有顯式的聲明塊級作用域儡羔,let/const聲明的變量卻沒有變?yōu)槿肿兞?/p>

image

這個其實也是let/const的特點,ES6規(guī)定它們不屬于頂層全局變量的屬性璧诵,這里用chrome調(diào)試一下

image

可以看到使用let聲明的變量x是在一個叫script作用域下的,而var聲明的變量因為變量提升所以提升到了全局變量window對象中仇冯,這使我們能放心的使用新語法之宿,不用擔心污染全局的window對象

建議

在日常開發(fā)中,我的建議是全面擁抱let/const苛坚,一般的變量聲明使用let關鍵字比被,而當聲明一些配置項(類似接口地址,npm依賴包泼舱,分頁器默認頁數(shù)等一些一旦聲明后就不會改變的變量)的時候可以使用const等缀,來顯式的告訴項目其他開發(fā)者,這個變量是不能改變的(const聲明的常量建議使用全大寫字母標識,單詞間用下劃線)娇昙,同時也建議了解var關鍵字的缺陷(變量提升尺迂,污染全局變量等),這樣才能更好的使用新語法

箭頭函數(shù)(常用)

ES6 允許使用箭頭(=>)定義函數(shù)

箭頭函數(shù)對于使用function關鍵字創(chuàng)建的函數(shù)有以下區(qū)別

  1. 箭頭函數(shù)沒有arguments(建議使用更好的語法冒掌,剩余運算符替代)

  2. 箭頭函數(shù)沒有prototype屬性噪裕,不能用作構造函數(shù)(不能用new關鍵字調(diào)用)

  3. 箭頭函數(shù)沒有自己this,它的this是詞法的股毫,引用的是上下文的this膳音,即在你寫這行代碼的時候就箭頭函數(shù)的this就已經(jīng)和外層執(zhí)行上下文的this綁定了(這里個人認為并不代表完全是靜態(tài)的,因為外層的上下文仍是動態(tài)的可以使用call,apply,bind修改,這里只是說明了箭頭函數(shù)的this始終等于它上層上下文中的this)

image

因為setTimeout會將一個匿名的回調(diào)函數(shù)推入異步隊列,而回調(diào)函數(shù)是具有全局性的铃诬,即在非嚴格模式下this會指向window祭陷,就會存在丟失變量a的問題,而如果使用箭頭函數(shù)趣席,在書寫的時候就已經(jīng)確定它的this等于它的上下文(這里是makeRequest的函數(shù)執(zhí)行上下文兵志,相當于將箭頭函數(shù)中的this綁定了makeRequest函數(shù)執(zhí)行上下文中的this)因為是controller對象調(diào)用的makeRequest函數(shù),所以this就指向了controller對象中的a變量

箭頭函數(shù)的this指向即使使用call,apply,bind也無法改變(這里也驗證了為什么ECMAScript規(guī)定不能使用箭頭函數(shù)作為構造函數(shù)吩坝,因為它的this已經(jīng)確定好了無法改變)

建議

箭頭函數(shù)替代了以前需要顯式的聲明一個變量保存this的操作毒姨,使得代碼更加的簡潔

ES5寫法:

image

ES6箭頭函數(shù):

image

再來看一個例子

image

值得注意的是makeRequest后面的function不能使用箭頭函數(shù),因為這樣它就會再使用上層的this钉寝,而再上層是全局的執(zhí)行上下文弧呐,它的this的值會指向window,所以找不到變量a返回undefined

在數(shù)組的迭代中使用箭頭函數(shù)更加簡潔,并且省略了return關鍵字

image

不要在可能改變this指向的函數(shù)中使用箭頭函數(shù)嵌纲,類似Vue中的methods,computed中的方法,生命周期函數(shù)俘枫,Vue將這些函數(shù)的this綁定了當前組件的vm實例,如果使用箭頭函數(shù)會強行改變this逮走,因為箭頭函數(shù)優(yōu)先級最高(無法再使用call,apply,bind改變指向)

image

在把箭頭函數(shù)作為日常開發(fā)的語法之前,個人建議是去了解一下箭頭函數(shù)的是如何綁定this的,而不只是當做省略function這幾個單詞拼寫,畢竟那才是ECMAScript真正希望解決的問題

iterator迭代器

iterator迭代器是ES6非常重要的概念鸠蚪,但是很多人對它了解的不多,但是它卻是另外4個ES6常用特性的實現(xiàn)基礎(解構賦值,剩余/擴展運算符茅信,生成器盾舌,for of循環(huán)),了解迭代器的概念有助于了解另外4個核心語法的原理蘸鲸,另外ES6新增的Map,Set數(shù)據(jù)結構也有使用到它妖谴,所以我放到前面來講

對于可迭代的數(shù)據(jù)解構,ES6在內(nèi)部部署了一個[Symbol.iterator]屬性酌摇,它是一個函數(shù)膝舅,執(zhí)行后會返回iterator對象(也叫迭代器對象,也叫iterator接口)窑多,擁有[Symbol.iterator]屬性的對象即被視為可迭代的

數(shù)組中的Symbol.iterator方法默認部署在數(shù)組原型上:


image

默認具有iterator接口的數(shù)據(jù)結構有以下幾個仍稀,注意普通對象默認是沒有iterator接口的(可以自己創(chuàng)建iterator接口讓普通對象也可以迭代)

  • Array
  • Map
  • Set
  • String
  • TypedArray(類數(shù)組)
  • 函數(shù)的 arguments 對象
  • NodeList 對象

iterator迭代器是一個對象,它具有一個next方法所以可以這么調(diào)用

image

next方法返回又會返回一個對象埂息,有value和done兩個屬性技潘,value即每次迭代之后返回的值,而done表示是否還需要再次循環(huán)千康,可以看到當value為undefined時崭篡,done為true表示循環(huán)終止

梳理一下

  • 可迭代的數(shù)據(jù)結構會有一個[Symbol.iterator]方法
  • [Symbol.iterator]執(zhí)行后返回一個iterator對象
  • iterator對象有一個next方法
  • next方法執(zhí)行后返回一個有value,done屬性的對象

這里簡要概述了以下iterator的概念,有興趣可以去看阮一峰老師的《ECMAScript 6 入門》

解構賦值(常用)

解構賦值可以直接使用對象的某個屬性吧秕,而不需要通過屬性訪問的形式使用琉闪,對象解構原理個人認為是通過尋找相同的屬性名,然后原對象的這個屬性名的值賦值給新對象對應的屬性

image

這里左邊真正聲明的其實是titleOne,titleTwo這兩個變量砸彬,然后會根據(jù)左邊這2個變量的位置尋找右邊對象中title和test[0]中的title對應的值颠毙,找到字符串a(chǎn)bc和test賦值給titleOne,titleTwo(如果沒有找到會返回undefined)

數(shù)組解構的原理其實是消耗數(shù)組的迭代器,把生成對象的value屬性的值賦值給對應的變量

數(shù)組解構的一個用途是交換變量砂碉,避免以前要聲明一個臨時變量值存儲值

ES6交換變量:


image

建議

同樣建議使用蛀蜜,因為解構賦值語意化更強,對于作為對象的函數(shù)參數(shù)來說增蹭,可以減少形參的聲明滴某,直接使用對象的屬性(如果嵌套層數(shù)過多我個人認為不適合用對象解構,不太優(yōu)雅)

一個常用的例子是Vuex中actions中的方法會傳入2個參數(shù)滋迈,第一個參數(shù)是個對象霎奢,你可以隨意命名,然后使用<名字>.commit的方法調(diào)用commit函數(shù)饼灿,或者使用對象解構直接使用commit

不使用對象解構:


image

使用對象解構:


image

另外可以給使用axios的響應結果進行解構(axios默認會把真正的響應結果放在data屬性中)

image

剩余/擴展運算符(常用)

剩余/擴展運算符同樣也是ES6一個非常重要的語法幕侠,使用3個點(...),后面跟著一個數(shù)組碍彭,它使得可以"展開"這個數(shù)組晤硕,可以這么理解悼潭,數(shù)組是存放元素集合的一個容器,而使用剩余/擴展運算符可以將這個容器拆開舞箍,這樣就只剩下元素集合舰褪,你可以把這些元素集合放到另外一個數(shù)組里面

image

擴展運算符

只要含有iterator接口的數(shù)據(jù)結構都可以使用擴展運算符

擴展運算符可以和數(shù)組的解構賦值一起使用,但是必須放在最后一個疏橄,因為剩余/擴展運算符的原理其實是利用了數(shù)組的迭代器抵知,它會消耗3個點后面的數(shù)組的所有迭代器,讀取所有迭代器生成對象的value屬性软族,剩余/擴展運算符后不能在有解構賦值,因為剩余/擴展運算符已經(jīng)消耗了所有迭代器残制,而數(shù)組的解構賦值也是消耗迭代器立砸,但是這個時候已經(jīng)沒有迭代器了,所以會報錯

image

這里first會消耗右邊數(shù)組的一個迭代器初茶,...arr會消耗剩余所有的迭代器颗祝,而第二個例子...arr直接消耗了所有迭代器,導致last沒有迭代器可供消耗了恼布,所以會報錯螺戳,因為這是毫無意義的操作

剩余運算符

剩余運算符最重要的一個特點就是替代了以前的arguments

訪問函數(shù)的arguments對象是一個很昂貴的操作,以前的arguments.callee,arguments.caller都被廢止了折汞,建議在支持ES6語法的環(huán)境下不要在使用arguments對象倔幼,使用剩余運算符替代(箭頭函數(shù)沒有arguments,必須使用剩余運算符才能訪問參數(shù)集合)

image

剩余運算符和擴展運算符的區(qū)別就是爽待,剩余運算符會收集這些集合损同,放到右邊的數(shù)組中,擴展運算符是將右邊的數(shù)組拆分成元素的集合鸟款,它們是相反的

在對象中使用擴展運算符

這個是ES9的語法膏燃,ES9中支持在對象中使用擴展運算符,之前說過數(shù)組的擴展運算符原理是消耗所有迭代器何什,但對象中并沒有迭代器组哩,我個人認為可能是實現(xiàn)原理不同,但是仍可以理解為將鍵值對從對象中拆開处渣,它可以放到另外一個普通對象中

image

其實它和另外一個ES6新增的API相似伶贰,即Object.assign,它們都可以合并對象罐栈,但是還是有一些不同Object.assign會觸發(fā)目標對象的setter函數(shù)幕袱,而對象擴展運算符不會,這個我們放到后面討論

建議

使用擴展運算符可以快速的將類數(shù)組轉(zhuǎn)為一個真正的數(shù)組

image

合并多個數(shù)組

image

函數(shù)柯里化

image

對象屬性/方法簡寫(常用)

對象屬性簡寫

es6允許當對象的屬性和值相同時悠瞬,省略屬性名

image

需要注意的是

  • 省略的是屬性名而不是值
  • 值必須是一個變量

對象屬性簡寫經(jīng)常與解構賦值一起使用

image

結合上文的解構賦值们豌,這里的代碼會其實是聲明了x,y,z變量涯捻,因為bar函數(shù)會返回一個對象,這個對象有x,y,z這3個屬性望迎,解構賦值會尋找等號右邊表達式的x,y,z屬性障癌,找到后賦值給聲明的x,y,z變量

方法簡寫

es6允許當一個對象的屬性的值是一個函數(shù)(即是一個方法),可以使用簡寫的形式

image

在Vue中因為都是在vm對象中書寫方法辩尊,完全可以使用方法簡寫的方式書寫函數(shù)

image

for ... of循環(huán)

for ... of是作為ES6新增的遍歷方式,允許遍歷一個含有iterator接口的數(shù)據(jù)結構并且返回各項的值,和ES3中的for ... in的區(qū)別如下

  1. for ... of遍歷獲取的是對象的鍵值,for ... in 獲取的是對象的鍵名

  2. for ... in會遍歷對象的整個原型鏈,性能非常差不推薦使用,而for ... of只遍歷當前對象不會遍歷原型鏈

  3. 對于數(shù)組的遍歷,for ... in會返回數(shù)組中所有可枚舉的屬性(包括原型鏈上可枚舉的屬性),for ... of只返回數(shù)組的下標對應的屬性值

for ... of循環(huán)的原理其實也是利用了遍歷對象內(nèi)部的iterator接口,將for ... of循環(huán)分解成最原始的for循環(huán),內(nèi)部實現(xiàn)的機制可以這么理解

image

可以看到只要滿足第二個條件(iterator.next()存在且res.done為true)就可以一直循環(huán)下去,并且每次把迭代器的next方法生成的對象賦值給res,然后將res的value屬性賦值給for ... of第一個條件中聲明的變量即可,res的done屬性控制是否繼續(xù)遍歷下去

for... of循環(huán)同時支持break,continue,return(在函數(shù)中調(diào)用的話)并且可以和對象解構賦值一起使用

image

arr數(shù)組每次使用for ... of循環(huán)都返回一對象({a:1},{a:2},{a:3}),然后會經(jīng)過對象解構,尋找屬性為a的值,賦值給obj.a,所以在每輪循環(huán)的時候obj.a會分別賦值為1,2,3

Promise(常用)

Promise作為ES6中推出的新的概念涛浙,改變了JS的異步編程,現(xiàn)代前端大部分的異步請求都是使用Promise實現(xiàn)摄欲,fetch這個web api也是基于Promise的轿亮,這里不得簡述一下之前統(tǒng)治JS異步編程的回調(diào)函數(shù),回調(diào)函數(shù)有什么缺點胸墙,Promise又是怎么改善這些缺點

回調(diào)函數(shù)

眾所周知我注,JS是單線程的,因為多個線程改變DOM的話會導致頁面紊亂迟隅,所以設計為一個單線程的語言但骨,但是瀏覽器是多線程的,這使得JS同時具有異步的操作智袭,即定時器奔缠,請求,事件監(jiān)聽等吼野,而這個時候就需要一套事件的處理機制去決定這些事件的順序校哎,即Event Loop(事件循環(huán)),這里不會詳細講解事件循環(huán)瞳步,只需要知道贬蛙,前端發(fā)出的請求,一般都是會進入瀏覽器的http請求線程谚攒,等到收到響應的時候會通過回調(diào)函數(shù)推入異步隊列阳准,等處理完主線程的任務會讀取異步隊列中任務,執(zhí)行回調(diào)

在《你不知道的JavaScript》下卷中馏臭,這么介紹

使用回調(diào)函數(shù)處理異步請求相當于把你的回調(diào)函數(shù)置于了一個黑盒野蝇,使用第三方的請求庫你可能會這么寫

image

收到響應后,執(zhí)行后面的回調(diào)打印字符串括儒,但是如果這個第三方庫有類似超時重試的功能绕沈,可能會執(zhí)行多次你的回調(diào)函數(shù),如果是一個支付功能帮寻,你就會發(fā)現(xiàn)你扣的錢可能就不止1000元了-.-

另外一個眾所周知的問題就是乍狐,在回調(diào)函數(shù)中再嵌套回調(diào)函數(shù)會導致代碼非常難以維護,這是人們常說的“回調(diào)地獄”

image

你使用的第三方ajax庫還有可能并沒有提供一些錯誤的回調(diào)固逗,請求失敗的一些錯誤信息可能會被吞掉浅蚪,而你確完全不知情

總結一下回調(diào)函數(shù)的一些缺點

  1. 多重嵌套藕帜,導致回調(diào)地獄

  2. 代碼跳躍,并非人類習慣的思維模式

  3. 信任問題惜傲,你不能把你的回調(diào)完全寄托與第三方庫洽故,因為你不知道第三方庫到底會怎么執(zhí)行回調(diào)(多次執(zhí)行)

  4. 第三方庫可能沒有提供錯誤處理

  5. 不清楚回調(diào)是否都是異步調(diào)用的(可以同步調(diào)用ajax,在收到響應前會阻塞整個線程盗誊,會陷入假死狀態(tài)时甚,非常不推薦)

xhr.open("GET","/try/ajax/ajax_info.txt",false); //通過設置第三個async為false可以同步調(diào)用ajax

Promise

針對回調(diào)函數(shù)這么多缺點,ES6中引入了一個新的概念Promise哈踱,Promise是一個構造函數(shù)荒适,通過new關鍵字創(chuàng)建一個Promise的實例,來看看Promise是怎么解決回調(diào)函數(shù)的這些問題

image

Promise并不是回調(diào)函數(shù)的衍生版本开镣,而是2個概念刀诬,所以需要將之前的回調(diào)函數(shù)改為支持Promise的版本,這個過程成為"提升"哑子,或者"promisory",現(xiàn)代MVVM框架常用的第三方請求庫axios就是一個典型的例子肌割,另外nodejs中也有bluebird卧蜓,Q等

  1. 多重嵌套,導致回調(diào)地獄

Promise在設計的時候引入了鏈式調(diào)用的概念把敞,每個then方法同樣也是一個Promise弥奸,因此可以無限鏈式調(diào)用下去

image

配合箭頭函數(shù),明顯的比之前回調(diào)函數(shù)的多層嵌套優(yōu)雅很多

  1. 代碼跳躍奋早,并非人類習慣的思維模式

Promise使得能夠同步思維書寫代碼盛霎,上述的代碼就是先請求3000端口,得到響應后再請求3001耽装,再請求3002愤炸,再請求3003,而書寫的格式也是符合人類的思維掉奄,從先到后

  1. 信任問題规个,你不能把你的回調(diào)完全寄托與第三方庫,因為你不知道第三方庫到底會怎么執(zhí)行回調(diào)(多次執(zhí)行)

Promise本身是一個狀態(tài)機姓建,具有pending(等待)诞仓,resolve(決議),reject(拒絕)這3個狀態(tài)速兔,當請求發(fā)送沒有得到響應的時候會pending狀態(tài)墅拭,并且一個Promise實例的狀態(tài)只能從pending => resolve 或者從 pending => reject,即當一個Promise實例從pending狀態(tài)改變后涣狗,就不會再改變了(不存在resolve => reject 或 reject => resolve)

而Promise實例必須主動調(diào)用then方法谍婉,才能將值從Promise實例中取出來(前提是Promise不是pending狀態(tài))舒憾,這一個“主動”的操作就是解決這個問題的關鍵,即第三方庫做的只是把改變Promise的狀態(tài)屡萤,而響應的值怎么處理珍剑,這是開發(fā)者主動控制的,這里就實現(xiàn)了控制反轉(zhuǎn)死陆,將原來第三方庫的控制權轉(zhuǎn)移到了開發(fā)者上

image
  1. 第三方庫可能沒有提供錯誤處理

Promise的then方法會接受2個函數(shù)招拙,第一個函數(shù)是這個Promise實例被resolve時執(zhí)行的回調(diào),第二個函數(shù)是這個Promise實例被reject時執(zhí)行的回調(diào)措译,而這個也是開發(fā)者主動調(diào)用的

使用Promise在異步請求發(fā)送錯誤的時候别凤,即使沒有捕獲錯誤,也不會阻塞主線程的代碼

image
  1. 不清楚回調(diào)是否都是異步調(diào)用的

Promise在設計的時候保證所有響應的處理回調(diào)都是異步調(diào)用的领虹,不會阻塞代碼的執(zhí)行规哪,Promise將then方法的回調(diào)放入一個叫微任務的隊列中(MicroTask),保證這些回調(diào)任務都在同步任務執(zhí)行完再執(zhí)行塌衰,這部分同樣也是事件循環(huán)的知識點诉稍,有興趣的朋友可以深入研究一下

建議

在日常開發(fā)中,建議全面擁抱新的Promise語法最疆,其實現(xiàn)在的異步編程基本也都使用的是Promise

建議使用ES7的async/await進一步的優(yōu)化Promise的寫法杯巨,async函數(shù)始終返回一個Promise,await可以實現(xiàn)一個"等待"的功能努酸,async/await被成為異步編程的終極解決方案服爷,即用同步的形式書寫異步代碼,并且能夠更優(yōu)雅的實現(xiàn)異步代碼順序執(zhí)行获诈,詳情可以看阮老師的ES6標準入門

關于Promise還有很多很多需要講的仍源,包括它的靜態(tài)方法all,race舔涎,resolve笼踩,reject,Promise的執(zhí)行順序亡嫌,Promise嵌套Promise戳表,thenable對象的處理等,礙于篇幅這里只介紹了一下為什么需要使用Promise昼伴。但很多開發(fā)者在日常使用中只是了解這些API匾旭,卻不知道Promise內(nèi)部具體是怎么實現(xiàn)的,遇到復雜的異步代碼就無從下手圃郊,非常建議去了解一下Promise A+的規(guī)范价涝,自己實現(xiàn)一個Promise

ES6 Module(常用)

在ES6 Module出現(xiàn)之前,模塊化一直是前端開發(fā)者討論的重點持舆,面對日益增長的需求和代碼色瘩,需要一種方案來將臃腫的代碼拆分成一個個小模塊伪窖,從而推出了AMD,CMD和CommonJs這3種模塊化方案,前者用在瀏覽器端居兆,后面2種用在服務端覆山,直到ES6 Module出現(xiàn)

ES6 Module默認目前還沒有被瀏覽器支持,需要使用babel泥栖,在日常寫demo的時候經(jīng)常會顯示這個錯誤

image

可以在script標簽中使用tpye="module"在同域的情況下可以解決(非同域情況會被同源策略攔截簇宽,webstorm會開啟一個同域的服務器沒有這個問題,vscode貌似不行)

image

ES6 Module使用import關鍵字導入模塊吧享,export關鍵字導出模塊禾酱,它還有以下特點

  1. ES6 Module是靜態(tài)的呻袭,也就是說它是在編譯階段運行搬男,和var以及function一樣具有提升效果(這個特點使得它支持tree shaking)

  2. 自動采用嚴格模式(頂層的this返回undefined)

  3. ES6 Module支持使用export {<變量>}導出具名的接口募谎,或者export default導出匿名的接口

module.js導出:


image

a.js導入:


image

這兩者的區(qū)別是,export {<變量>}導出的是一個變量的引用殊鞭,export default導出的是一個值

什么意思呢遭垛,就是說在a.js中使用import導入這2個變量的后,在module.js中因為某些原因x變量被改變了操灿,那么會立刻反映到a.js锯仪,而module.js中的y變量改變后,a.js中的y還是原來的值

module.js:


image

a.js:


image

可以看到給module.js設置了一個一秒后改變x,y變量的定時器,在一秒后同時觀察導入時候變量的值,可以發(fā)現(xiàn)x被改變了,但y的值仍是20,因為y是通過export default導出的,在導入的時候的值相當于只是導入數(shù)字20,而x是通過export {<變量>}導出的,它導出的是一個變量的引用,即a.js導入的是當前x的值,只關心當前x變量的值是什么,可以理解為一個"活鏈接"

export default這種導出的語法其實只是指定了一個命名導出,而它的名字叫default,換句話說,將模塊的導出的名字重命名為default,也可以使用import <變量> from <路徑> 這種語法導入

module.js導出:


image

a.js導入:


image

但是由于是使用export {<變量>}這種形式導出的模塊,即使被重命名為default,仍然導出的是一個變量的引用

這里再來說一下目前為止主流的模塊化方案ES6 Module和CommonJs的一些區(qū)別

  1. CommonJs輸出的是一個值的拷貝,ES6 Module通過export {<變量>}輸出的是一個變量的引用,export default輸出的是一個值

  2. CommonJs運行在服務器上,被設計為運行時加載,即代碼執(zhí)行到那一行才回去加載模塊,而ES6 Module是靜態(tài)的輸出一個接口,發(fā)生在編譯的階段

  3. CommonJs在第一次加載的時候運行一次并且會生成一個緩存,之后加載返回的都是緩存中的內(nèi)容

import()

關于ES6 Module靜態(tài)編譯的特點,導致了無法動態(tài)加載,但是總是會有一些需要動態(tài)加載模塊的需求,所以現(xiàn)在有一個提案,使用把import作為一個函數(shù)可以實現(xiàn)動態(tài)加載模塊,它返回一個Promise,Promise被resolve時的值為輸出的模塊

image
image

使用import方法改寫上面的a.js使得它可以動態(tài)加載(使用靜態(tài)編譯的ES6 Module放在條件語句會報錯,因為會有提升的效果,并且也是不允許的),可以看到輸出了module.js的一個變量x和一個默認輸出

Vue中路由的懶加載的ES6寫法就是使用了這個技術,使得在路由切換的時候能夠動態(tài)的加載組件渲染視圖

函數(shù)默認值

ES6允許在函數(shù)的參數(shù)中設置默認值

ES5寫法:


image

ES6寫法:


image

相比ES5,ES6函數(shù)默認值直接寫在參數(shù)上,更加的直觀

如果使用了函數(shù)默認參數(shù),在函數(shù)的參數(shù)的區(qū)域(括號里面),它會作為一個單獨的作用域,并且擁有l(wèi)et/const方法的一些特性,比如暫時性死區(qū),塊級作用域,沒有變量提升等,而這個作用域在函數(shù)內(nèi)部代碼執(zhí)行前

image

這里當運行func的時候,因為沒有傳參數(shù),使用函數(shù)默認參數(shù),y就會去尋找x的值,在沿著詞法作用域在外層找到了值為1的變量x

再來看一個例子


image

這里同樣沒有傳參數(shù),使用函數(shù)的默認賦值,x通過詞法作用域找到了變量w,所以x默認值為2,y同樣通過詞法作用域找到了剛剛定義的x變量,y的默認值為3,但是在解析到z = z + 1這一行的時候,JS解釋器先會去解析z+1找到相應的值后再賦給變量z,但是因為暫時性死區(qū)的原因(let/const"劫持"了這個塊級作用域,無法在聲明之前使用這個變量,上文有解釋),導致在let聲明之前就使用了變量z,所以會報錯

這樣理解函數(shù)的默認值會相對容易一些

image

當傳入的參數(shù)為undefined時才使用函數(shù)的默認值(顯式傳入undefined也會觸發(fā)使用函數(shù)默認值,傳入null則不會觸發(fā))

在舉個例子:


image

這里借用阮一峰老師書中的一個例子,func的默認值為一個函數(shù),執(zhí)行后返回foo變量,而在函數(shù)內(nèi)部執(zhí)行的時候,相當于對foo變量的一次變量查詢(LHS查詢),而查詢的起點是在這個單獨的作用域中,即JS解釋器不會去查詢?nèi)ズ瘮?shù)內(nèi)部查詢變量foo,而是沿著詞法作用域先查看同一作用域(前面的函數(shù)參數(shù))中有沒有foo變量,再往函數(shù)的外部尋找foo變量,最終找不到所以報錯了,這個也是函數(shù)默認值的一個特點

函數(shù)默認值配合解構賦值

image

第一行給func函數(shù)傳入了2個空對象,所以函數(shù)的第一第二個參數(shù)都不會使用函數(shù)默認值,然后函數(shù)的第一個參數(shù)會嘗試解構對象,提取變量x,因為第一個參數(shù)傳入了一個空對象,所以解構不出變量x,但是這里又在內(nèi)層設置了一個默認值,所以x的值為10,而第二個參數(shù)同樣傳了一個空對象,不會使用函數(shù)默認值,然后會嘗試解構出變量y,發(fā)現(xiàn)空對象中也沒有變量y,但是y沒有設置默認值所以解構后y的值為undefined

第二行第一個參數(shù)顯式的傳入了一個undefined,所以會使用函數(shù)默認值為一個空對象,隨后和第一行一樣嘗試解構x發(fā)現(xiàn)x為undefined,但是設置了默認值所以x的值為10,而y和上文一樣為undefined

第三行2個參數(shù)都會undefined,第一個參數(shù)和上文一樣,第二個參數(shù)會調(diào)用函數(shù)默認值,賦值為{y:10},然后嘗試解構出變量y,即y為10

第四行和第三行相同,一個是顯式傳入undefined,一個是隱式不傳參數(shù)

第五行直接使用傳入的參數(shù),不會使用函數(shù)默認值,并且能夠順利的解構出變量x,y

Proxy

Proxy作為一個"攔截器",可以在目標對象前架設一個攔截器,他人訪問對象,必須先經(jīng)過這層攔截器,Proxy同樣是一個構造函數(shù),使用new關鍵字生成一個攔截對象的實例,ES6提供了非常多對象攔截的操作,幾乎覆蓋了所有可能修改目標對象的情況(Proxy一般和Reflect配套使用,前者攔截對象,后者返回攔截的結果,Proxy上有的的攔截方法Reflect都有)


image

Object.definePropery

提到Proxy就不得不提一下ES5中的Object.defineProperty,這個api可以給一個對象添加屬性以及這個屬性的屬性描述符/訪問器(這2個不能共存,同一屬性只能有其中一個),屬性描述符有configurable,writable,enumerable,value這4個屬性,分別代表是否可配置,是否只讀,是否可枚舉和屬性的值,訪問器有configurable,enumerable,get,set,前2個和屬性描述符功能相同,后2個都是函數(shù),定義了get,set后對元素的讀寫操作都會執(zhí)行后面的getter/setter函數(shù),并且覆蓋默認的讀寫行為

image

定義了obj中a屬性的表示為只讀,且不可枚舉,obj2定義了get,但沒有定義set表示只讀,并且讀取obj2的b屬性返回的值是getter函數(shù)的返回值

ES5中的Object.defineProperty這和Proxy有什么關系呢?個人理解Proxy是Object.defineProperty的增強版,ES5只規(guī)定能夠定義屬性的屬性描述符或訪問器.而Proxy增強到了13種,具體太多了我就不一一放出來了,這里我舉幾個比較有意思的例子

handler.apply

apply可以讓我們攔截一個函數(shù)(JS中函數(shù)也是對象,Proxy也可以攔截函數(shù))的執(zhí)行,我們可以把它用在函數(shù)節(jié)流中

image

調(diào)用攔截后的函數(shù):

image

handler.contruct

contruct可以攔截通過new關鍵字調(diào)用這個函數(shù)的操作,我們可以把它用在單例模式中

image

這里通過一個閉包保存了instance變量,每次使用new關鍵字調(diào)用被攔截的函數(shù)后都會查看這個instance變量,如果存在就返回閉包中保存的instance變量,否則就新建一個實例,這樣可以實現(xiàn)全局只有一個實例

handler.defineProperty

defineProperty可以攔截對這個對象的Object.defineProerty操作

注意對象內(nèi)部的默認的[[SET]]操作(即對這個對象的屬性賦值)會間接觸發(fā)defineProperty和getOwnPropertyDescriptor這2個攔截方法

image

這里有幾個知識點

  1. 這里使用了遞歸的操作,當需要訪問對象的屬性時候,會判斷代理的對象屬性的值仍是一個可以代理的對象就遞歸的進行代理,否則通過錯誤捕獲執(zhí)行默認的get操作
  2. 定義了defineProperty的攔截方法,當對這個代理對象的某個屬性進行賦值的時候會執(zhí)行對象內(nèi)部默認的[[SET]]操作進行賦值,這個操作會間接觸發(fā)defineProperty這個方法,隨后會執(zhí)行定義的callback函數(shù)

這樣就實現(xiàn)了無論對象嵌套多少層,只要有屬性進行賦值就會觸發(fā)get方法,對這層對象進行代理,隨后觸發(fā)defineProperty執(zhí)行callback回調(diào)函數(shù)

其他的使用場景

Proxy另外還有很多功能,比如在實現(xiàn)驗證器的時候,可以將業(yè)務邏輯和驗證器分離達到解耦,通過defineProperty設置一些私有變量,攔截對象做日志記錄等

Vue

尤大預計2019年下半年發(fā)布Vue3.0,其中一個核心的功能就是使用Proxy替代Object.defineProperty

我相信了解過一點Vue響應式原理的人都知道Vue框架在對象攔截上的一些不足

<template>
   <div>
       <div>{{arr}}</div>
       <div>{{obj}}</div>
       <button @click="handleClick">修改arr下標</button>
       <button @click="handleClick2">創(chuàng)建obj的屬性</button>
   </div>
</template>

<script>

    export default {
        name: "index",
        data() {
            return {
                arr:[1,2,3],
                obj:{
                    a:1,
                    b:2
                }
            }
        },
        methods: {
            handleClick() {
                this.arr[0] = 10
                console.log(this.arr)
            },
            handleClick2() {
                this.obj.c = 3
                console.log(this.obj)
            }
        },
   }
</script>
image

可以看到這里數(shù)據(jù)改變了,控制臺打印出了新的值,但是視圖沒有更新,這是因為Vue內(nèi)部使用Object.defineProperty進行的數(shù)據(jù)劫持,而這個API無法探測到對象根屬性的添加和刪除,以及直接給數(shù)組下標進行賦值,所以不會通知渲染watcher進行視圖更新,而理論上這個API也無法探測到數(shù)組的一系列方法(push,splice,pop),但是Vue框架修改了數(shù)組的原型,使得在調(diào)用這些方法修改數(shù)據(jù)后會執(zhí)行視圖更新的操作

//源碼位置:src/core/observer/array.js
methodsToPatch.forEach(function (method) {
  // cache original method
  var original = arrayProto[method];
  def(arrayMethods, method, function mutator () {
    var args = [], len = arguments.length;
    while ( len-- ) args[ len ] = arguments[ len ];

    var result = original.apply(this, args);
    var ob = this.__ob__;
    var inserted;
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args;
        break
      case 'splice':
        inserted = args.slice(2);
        break
    }
    if (inserted) { ob.observeArray(inserted); }
    // notify change
    ob.dep.notify(); //這一行就會主動調(diào)用notify方法,會通知到渲染watcher進行視圖更新
    return result
  });
});

掘金翻譯的尤大Vue3.0計劃中寫到

3.0 將帶來一個基于 Proxy 的 observer 實現(xiàn)牲尺,它可以提供覆蓋語言 (JavaScript——譯注) 全范圍的響應式能力卵酪,消除了當前 Vue 2 系列中基于 Object.defineProperty 所存在的一些局限幌蚊,如:
對屬性的添加谤碳、刪除動作的監(jiān)測
對數(shù)組基于下標的修改、對于 .length 修改的監(jiān)測
對 Map溢豆、Set蜒简、WeakMap 和 WeakSet 的支持

Proxy就沒有這個問題,并且還提供了更多的攔截方法,完全可以替代Object.defineProperty,唯一不足的也就是瀏覽器的支持程度了(IE:誰在說我?)

所以要想深入了解Vue3.0實現(xiàn)機制,學會Proxy是必不可少的

Object.assign

這個ES6新增的Object靜態(tài)方法允許我們進行多個對象的合并

image

可以這么理解,Object.assign遍歷需要合并給target的對象(即sourece對象的集合)的屬性,用等號進行賦值,這里遍歷{a:1}將屬性a和值數(shù)字1賦值給target對象,然后再遍歷{b:2}將屬性b和值數(shù)字2賦值給target對象

這里羅列了一些這個API的需要注意的知識點

  1. Object.assign是淺拷貝,對于值是引用類型的屬性,拷貝仍舊的是它的引用

  2. 可以拷貝Symbol屬性

  3. 不能拷貝不可枚舉的屬性

  4. Object.assign保證target始終是一個對象,如果傳入一個基本類型,會轉(zhuǎn)為基本包裝類型,null/undefined沒有基本包裝類型,所以傳入會報錯

  5. source參數(shù)如果是不可枚舉的數(shù)據(jù)類型會忽略合并(字符串類型被認為是可枚舉的,因為內(nèi)部有iterator接口)

  6. 因為是用等號進行賦值,如果被賦值的對象的屬性有setter函數(shù)會觸發(fā)setter函數(shù),同理如果有getter函數(shù),也會調(diào)用賦值對象的屬性的getter函數(shù)(這就是為什么Object.assign無法合并對象屬性的訪問器,因為它會直接執(zhí)行對應的getter/setter函數(shù)而不是合并它們,如果需要合并對象屬性的getter/setter函數(shù),可以使用ES7提供的Object.getOwnPropertyDescriptors和Object.defineProperties這2個API實現(xiàn))

image
image

可以看到這里成功的復制了obj對象中a屬性的getter/setter

為了加深了解我自己模擬了Object.assign的實現(xiàn),可供參考

image

這里有一個坑不得不提,對于target參數(shù)傳入一個字符串,內(nèi)部會轉(zhuǎn)換為基本包裝類型,而字符串基本包裝類型的屬性是只讀的(屬性描述符的writable屬性為false),這里感謝木易楊的專欄

image
image

打印對象屬性的屬性描述符可以看到下標屬性的值都是只讀的,即不能再次賦值,所以嘗試以下操作會報錯

image

字符串a(chǎn)bc會轉(zhuǎn)為基本包裝類型,然后將字符串def合并給這個基本包裝類型的時候會將字符串def展開,分別將字符串def賦值給基本包裝類型abc的0,1,2屬性,隨后就會在賦值的時候報錯(非嚴格模式下會只會靜默處理,ES6的Object.assign默認開啟了嚴格模式)

和ES9的對象擴展運算符對比

ES9支持在對象上使用擴展運算符,實現(xiàn)的功能和Object.assign相似,唯一的區(qū)別就是在含有getter/setter函數(shù)的對象的屬性上有所區(qū)別

image
image

(最后一個字符串get可以忽略,這是控制臺為了顯示a變量觸發(fā)的getter函數(shù))

分析一下這個例子

ES9:

  • 會合并2個對象,并且只觸發(fā)2個對象對應屬性的getter函數(shù)
  • 相同屬性的后者覆蓋了前者,所以a屬性的值是第二個getter函數(shù)return的值

ES6:

  • 同樣會合并這2個對象,并且只觸發(fā)了obj上a屬性的setter函數(shù)而不會觸發(fā)它的getter函數(shù)(結合上述Object.assgin的內(nèi)部實現(xiàn)理解會容易一些)
  • obj上a屬性的setter函數(shù)替代默認的賦值行為,導致obj2的a屬性不會被復制過來

除去對象屬性有getter/setter的情況,Object.assgin和對象擴展運算符功能是相同的,兩者都可以使用,兩者都是淺拷貝,使用ES9的方法相對簡潔一點

建議

  1. Vue中重置data中的數(shù)據(jù)

這個是我最常用的小技巧,使用Object.assign可以將你目前組件中的data對象和組件默認初始化狀態(tài)的data對象中的數(shù)據(jù)合并,這樣可以達到初始化data對象的效果

image

在當前組件的實例中$data屬性保存了當前組件的data對象,而$options是當前組件實例初始化時的一些屬性,其中有個data方法,即在在組件中寫的data函數(shù),執(zhí)行后會返回一個初始化的data對象,然后將這個初始化的data對象合并到當前的data來初始化所有數(shù)據(jù)

  1. 給對象合并需要的默認屬性
image

可以封裝一個函數(shù),外層聲明一個DEFAULTS常量,options為每次傳入的動態(tài)配置,這樣每次執(zhí)行后會合并一些默認的配置項

  1. 在傳參的時候可以多個數(shù)據(jù)合并成一個對象傳給后端
image

參考資料

  1. 阮一峰:ES6標準入門

  2. 慕課網(wǎng):ES6零基礎教學

  3. 你不知道的JavaScript下卷

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市漩仙,隨后出現(xiàn)的幾起案子搓茬,更是在濱河造成了極大的恐慌,老刑警劉巖队他,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件卷仑,死亡現(xiàn)場離奇詭異,居然都是意外死亡麸折,警方通過查閱死者的電腦和手機锡凝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來垢啼,“玉大人窜锯,你說我怎么就攤上這事张肾。” “怎么了锚扎?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵吞瞪,是天一觀的道長。 經(jīng)常有香客問我驾孔,道長芍秆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任助币,我火速辦了婚禮浪听,結果婚禮上,老公的妹妹穿的比我還像新娘眉菱。我一直安慰自己迹栓,他們只是感情好,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布俭缓。 她就那樣靜靜地躺著克伊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪华坦。 梳的紋絲不亂的頭發(fā)上愿吹,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機與錄音惜姐,去河邊找鬼犁跪。 笑死,一個胖子當著我的面吹牛歹袁,可吹牛的內(nèi)容都是我干的坷衍。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼条舔,長吁一口氣:“原來是場噩夢啊……” “哼枫耳!你這毒婦竟也來了?” 一聲冷哼從身側響起孟抗,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤迁杨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后凄硼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體铅协,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年摊沉,在試婚紗的時候發(fā)現(xiàn)自己被綠了狐史。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖预皇,靈堂內(nèi)的尸體忽然破棺而出侈玄,到底是詐尸還是另有隱情,我是刑警寧澤吟温,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布序仙,位于F島的核電站,受9級特大地震影響鲁豪,放射性物質(zhì)發(fā)生泄漏潘悼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一爬橡、第九天 我趴在偏房一處隱蔽的房頂上張望治唤。 院中可真熱鬧,春花似錦糙申、人聲如沸宾添。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缕陕。三九已至,卻和暖如春疙挺,著一層夾襖步出監(jiān)牢的瞬間扛邑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工铐然, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蔬崩,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓搀暑,卻偏偏與公主長得像沥阳,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子险掀,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 一沪袭、ES6簡介 ? 歷時將近6年的時間來制定的新 ECMAScript 標準 ECMAScript 6(亦稱 ...
    一歲一枯榮_閱讀 6,078評論 8 25
  • 以下內(nèi)容是我在學習和研究ES6時湾宙,對ES6的特性樟氢、重點和注意事項的提取、精練和總結侠鳄,可以做為ES6特性的字典埠啃;在本...
    科研者閱讀 3,128評論 2 9
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy閱讀 9,519評論 1 51
  • 下午抽空去了一下單位,領導臨時安排了事情伟恶,抓緊時間完成碴开,工作效率蠻高的。 提高工作效率的前提是提高專注力,不說話潦牛,...
    泡沫XY閱讀 155評論 0 0
  • 你還是跟我說了再見巴碗,我只能看著你的背影朴爬,漸行漸遠。 —1— 我從沒想過橡淆,事隔經(jīng)年召噩,再次相見,竟是此情此景逸爵。 “君君...
    姓張的大傻子閱讀 800評論 0 2