手把手教你ES6語法知識點補充


手把手教你ES6語法知識點補充


前言

ECMAScript 6.0(簡稱ES6),作為下一代JavaScript的語言標準正式發佈於2015 年 6 月,至今已經發布3年多了,但是因為蘊含的語法之廣,完全消化需要一定的時間,這裡我總結了部分ES6,以及ES6以後新語法的知識點,使用場景,希望對各位有所幫助

本文講著重是對ES6語法特性的補充,不會講解一些API層面的語法,更多的是發掘背後的原理,以及ES6到底解決了什麼問題

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

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

let/const(常用)

let,const用於聲明變量,用來替代老語法的var關鍵字,與var不同的是,let/const會創建一個塊級作用域(通俗講就是一個花括號內是一個新的作用域)

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

手把手教你ES6語法知識點補充


在日常開發中多存在於使用if/for關鍵字結合let/const創建的塊級作用域,值得注意的是使用let/const關鍵字聲明變量的for循環和var聲明的有些不同


手把手教你ES6語法知識點補充


for循環分為3部分,第一部分包含一個變量聲明,第二部分包含一個循環的退出條件,第三部分包含每次循環最後要執行的表達式,也就是說第一部分在這個for循環中只會執行一次var i = 0,而後面的兩個部分在每次循環的時候都會執行一遍


手把手教你ES6語法知識點補充


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

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

給每次循環創建一個塊級作用域:

手把手教你ES6語法知識點補充


暫時性死區

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


手把手教你ES6語法知識點補充


剖析暫時性死區的原理,

其實let/const同樣也有提升的作用,但是和var的區別在於

  • var在創建時就被初始化,並且賦值為undefined
  • let/const在進入塊級作用域後,會因為提升的原因先創建,但不會被初始化,直到聲明語句執行的時候才被初始化,初始化的時候如果使用let聲明的變量沒有賦值,則會默認賦值為undefined,而const必須在初始化的時候賦值。而創建到初始化之間的代碼片段就形成了暫時性死區

引用一篇博客對於ES6標準翻譯出來的一段話

由let/const聲明的變量,當它們包含的詞法環境(Lexical Environment)被實例化時會被創建,但只有在變量的詞法綁定(LexicalBinding)已經被求值運算後,才能夠被訪問

回到例子,這裡因為使用了let聲明瞭變量name,在代碼執行到if語句的時候會先進入預編譯階段,依次創建塊級作用域,詞法環境,name變量(沒有初始化),隨後進入代碼執行階段,只有在運行到let name語句的時候變量才被初始化並且默認賦值為undefined,但是因為暫時性死區導致在運行到聲明語句之前使用到了name變量,所以報錯了


手把手教你ES6語法知識點補充


上面這個例子,因為使用var聲明變量,會有變量提升,同樣也是發生在預編譯階段,var會提升到當前函數作用域的頂部並且默認賦值為undefined,如果這幾行代碼是在全局作用域下,則name變量會直接提升到全局作用域,隨後進入執行階段執行代碼,name被賦值為"abc",並且可以成功打印出字符串abc

相當於這樣


手把手教你ES6語法知識點補充


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

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

const

使用const關鍵字聲明一個常量,常量的意思是不會改變的變量,const和let的一些區別是

  1. const聲明變量的時候必須賦值,否則會報錯,同樣使用const聲明的變量被修改了也會報錯


手把手教你ES6語法知識點補充


  1. const聲明變量不能改變,如果聲明的是一個引用類型,則不能改變它的內存地址(這裡牽扯到JS引用類型的特點,有興趣可以看我另一篇博客對象深拷貝和淺拷貝)


手把手教你ES6語法知識點補充


有些人會有疑問,為什麼日常開發中沒有顯式的聲明塊級作用域,let/const聲明的變量卻沒有變為全局變量


手把手教你ES6語法知識點補充


這個其實也是let/const的特點,ES6規定它們不屬於頂層全局變量的屬性,這裡用chrome調試一下


手把手教你ES6語法知識點補充


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

建議

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

箭頭函數(常用)

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

箭頭函數對於使用function關鍵字創建的函數有以下區別

  1. 箭頭函數沒有arguments(建議使用更好的語法,剩餘運算符替代)
  2. 箭頭函數沒有prototype屬性,不能用作構造函數(不能用new關鍵字調用)
  3. 箭頭函數沒有自己this,它的this是詞法的,引用的是上下文的this,即在你寫這行代碼的時候就箭頭函數的this就已經和外層執行上下文的this綁定了(這裡個人認為並不代表完全是靜態的,因為外層的上下文仍是動態的可以使用call,apply,bind修改,這裡只是說明了箭頭函數的this始終等於它上層上下文中的this)


手把手教你ES6語法知識點補充


因為setTimeout會將一個匿名的回調函數推入異步隊列,而回調函數是具有全局性的,即在非嚴格模式下this會指向window,就會存在丟失變量a的問題,而如果使用箭頭函數,在書寫的時候就已經確定它的this等於它的上下文(這裡是makeRequest的函數執行上下文,相當於將箭頭函數中的this綁定了makeRequest函數執行上下文中的this)因為是controller對象調用的makeRequest函數,所以this就指向了controller對象中的a變量

箭頭函數的this指向即使使用call,apply,bind也無法改變(這裡也驗證了為什麼ECMAScript規定不能使用箭頭函數作為構造函數,因為它的this已經確定好了無法改變)

建議

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

ES5寫法:


手把手教你ES6語法知識點補充


ES6箭頭函數:


手把手教你ES6語法知識點補充


再來看一個例子


手把手教你ES6語法知識點補充


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

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


手把手教你ES6語法知識點補充


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


手把手教你ES6語法知識點補充


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

iterator迭代器

iterator迭代器是ES6非常重要的概念,但是很多人對它瞭解的不多,但是它卻是另外4個ES6常用特性的實現基礎(解構賦值,剩餘/擴展運算符,生成器,for of循環),瞭解迭代器的概念有助於瞭解另外4個核心語法的原理,另外ES6新增的Map,Set數據結構也有使用到它,所以我放到前面來講

對於可迭代的數據解構,ES6在內部部署了一個[Symbol.iterator]屬性,它是一個函數,執行後會返回iterator對象(也叫迭代器對象),而生成iterator對象[Symbol.iterator]屬性叫iterator接口,有這個接口的數據結構即被視為可迭代的

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

手把手教你ES6語法知識點補充


默認部署iterator接口的數據結構有以下幾個,注意普通對象默認是沒有iterator接口的(可以自己創建iterator接口讓普通對象也可以迭代)

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

iterator迭代器是一個對象,它具有一個next方法所以可以這麼調用


手把手教你ES6語法知識點補充

next方法返回又會返回一個對象,有value和done兩個屬性,value即每次迭代之後返回的值,而done表示是否還需要再次循環,可以看到當value為undefined時,done為true表示循環終止

梳理一下

  • 可迭代的數據結構會有一個[Symbol.iterator]方法
  • [Symbol.iterator]執行後返回一個iterator對象
  • iterator對象有一個next方法
  • 執行一次next方法(消耗一次迭代器)會返回一個有value,done屬性的對象

借用冴羽博客中ES5實現的迭代器可以更加深刻的理解迭代器是如何生成和消耗的


手把手教你ES6語法知識點補充


解構賦值(常用)

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


手把手教你ES6語法知識點補充


這裡左邊真正聲明的其實是titleOne,titleTwo這兩個變量,然後會根據左邊這2個變量的位置尋找右邊對象中title和test[0]中的title對應的值,找到字符串abc和test賦值給titleOne,titleTwo(如果沒有找到會返回undefined)

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

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

ES6交換變量:

手把手教你ES6語法知識點補充


建議

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

一個常用的例子是Vuex中actions中的方法會傳入2個參數,第一個參數是個對象,你可以隨意命名,然後使用.commit的方法調用commit函數,或者使用對象解構直接使用commit

不使用對象解構:

手把手教你ES6語法知識點補充


使用對象解構:

手把手教你ES6語法知識點補充


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


手把手教你ES6語法知識點補充


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

剩餘/擴展運算符同樣也是ES6一個非常重要的語法,使用3個點(...),後面跟著一個含有iterator接口的數據結構

擴展運算符

以數組為例,使用擴展運算符使得可以"展開"這個數組,可以這麼理解,數組是存放元素集合的一個容器,而使用擴展運算符可以將這個容器拆開,這樣就只剩下元素集合,你可以把這些元素集合放到另外一個數組裡面


手把手教你ES6語法知識點補充


擴展運算符可以代替ES3中數組原型的concat方法


手把手教你ES6語法知識點補充


這裡將arr1,arr2通過擴展運算符展開,隨後將這些元素放到一個新的數組中,相對於concat方法語義化更強

剩餘運算符

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

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


手把手教你ES6語法知識點補充


剩餘運算符可以和數組的解構賦值一起使用,但是必須放在最後一個,因為剩餘運算符的原理其實是利用了數組的迭代器,它會消耗3個點後面的數組的所有迭代器,讀取所有迭代器生成對象的value屬性,剩運算符後不能在有解構賦值,因為剩餘運算符已經消耗了所有迭代器,而數組的解構賦值也是消耗迭代器,但是這個時候已經沒有迭代器了,所以會報錯


手把手教你ES6語法知識點補充


這裡first會消耗右邊數組的一個迭代器,...arr會消耗剩餘所有的迭代器,而第二個例子...arr直接消耗了所有迭代器,導致last沒有迭代器可供消耗了,所以會報錯,因為這是毫無意義的操作

剩餘運算符和擴展運算符的區別就是,剩餘運算符會收集這些集合,放到右邊的數組中,擴展運算符是將右邊的數組拆分成元素的集合,它們是相反的

在對象中使用擴展運算符

這個是ES9的語法,ES9中支持在對象中使用擴展運算符,之前說過數組的擴展運算符原理是消耗所有迭代器,但對象中並沒有迭代器,我個人認為可能是實現原理不同,但是仍可以理解為將鍵值對從對象中拆開,它可以放到另外一個普通對象中


手把手教你ES6語法知識點補充


其實它和另外一個ES6新增的API相似,即Object.assign,它們都可以合併對象,但是還是有一些不同Object.assign會觸發目標對象的setter函數,而對象擴展運算符不會,這個我們放到後面討論

建議

使用擴展運算符可以快速的將類數組轉為一個真正的數組


手把手教你ES6語法知識點補充


合併多個數組


手把手教你ES6語法知識點補充


函數柯里化


手把手教你ES6語法知識點補充


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

對象屬性簡寫

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


手把手教你ES6語法知識點補充


需要注意的是

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

對象屬性簡寫經常與解構賦值一起使用


手把手教你ES6語法知識點補充


結合上文的解構賦值,這裡的代碼會其實是聲明瞭x,y,z變量,因為bar函數會返回一個對象,這個對象有x,y,z這3個屬性,解構賦值會尋找等號右邊表達式的x,y,z屬性,找到後賦值給聲明的x,y,z變量

方法簡寫

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


手把手教你ES6語法知識點補充


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


手把手教你ES6語法知識點補充


for ... of循環

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

  1. for ... of只能用在可迭代對象上,獲取的是迭代器返回的value值,for ... in 可以獲取所有對象的鍵名
  2. for ... in會遍歷對象的整個原型鏈,性能非常差不推薦使用,而for ... of只遍歷當前對象不會遍歷它的原型鏈
  3. 對於數組的遍歷,for ... in會返回數組中所有可枚舉的屬性(包括原型鏈上可枚舉的屬性),for ... of只返回數組的下標對應的屬性值

for ... of循環的原理其實也是利用了可迭代對象內部部署的iterator接口,如果將for ... of循環分解成最原始的for循環,內部實現的機制可以這麼理解


手把手教你ES6語法知識點補充


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

for... of循環同時支持break,continue,return(在函數中調用的話)並且可以和對象解構賦值一起使用


手把手教你ES6語法知識點補充


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

Promise(常用)

Promise作為ES6中推出的新的概念,改變了JS的異步編程,現代前端大部分的異步請求都是使用Promise實現,fetch這個web api也是基於Promise的,這裡不得簡述一下之前統治JS異步編程的回調函數,回調函數有什麼缺點,Promise又是怎麼改善這些缺點

回調函數

眾所周知,JS是單線程的,因為多個線程改變DOM的話會導致頁面紊亂,所以設計為一個單線程的語言,但是瀏覽器是多線程的,這使得JS同時具有異步的操作,即定時器,請求,事件監聽等,而這個時候就需要一套事件的處理機制去決定這些事件的順序,即Event Loop(事件循環),這裡不會詳細講解事件循環,只需要知道,前端發出的請求,一般都是會進入瀏覽器的http請求線程,等到收到響應的時候會通過回調函數推入異步隊列,等處理完主線程的任務會讀取異步隊列中任務,執行回調

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

使用回調函數處理異步請求相當於把你的回調函數置於了一個黑盒,雖然你聲明瞭等到收到響應後執行你提供的回調函數,可是你並不知道這個第三方庫會在什麼具體會怎麼執行回調函數

使用第三方的請求庫你可能會這麼寫:

手把手教你ES6語法知識點補充


收到響應後,執行後面的回調打印字符串,但是如果這個第三方庫有類似超時重試的功能,可能會執行多次你的回調函數,如果是一個支付功能,你就會發現你扣的錢可能就不止1000元了-.-

第二個眾所周知的問題就是,在回調函數中再嵌套回調函數會導致代碼非常難以維護,這是人們常說的“回調地獄”


手把手教你ES6語法知識點補充


另外你使用的第三方ajax庫還有可能並沒有提供一些錯誤的回調,請求失敗的一些錯誤信息可能會被吞掉,而你確完全不知情(nodejs提供了err-first風格的回調,即異步操作的第一個回調永遠是錯誤的回調處理,但是你還是不能保證所有的庫都提供了發送錯誤時的執行的回調函數)

總結一下回調函數的一些缺點

  1. 多重嵌套,導致回調地獄
  2. 代碼跳躍,並非人類習慣的思維模式
  3. 信任問題,你不能把你的回調完全寄託與第三方庫,因為你不知道第三方庫到底會怎麼執行回調(多次執行)
  4. 第三方庫可能沒有提供錯誤處理
  5. 不清楚回調是否都是異步調用的(可以同步調用ajax,在收到響應前會阻塞整個線程,會陷入假死狀態,非常不推薦)
<code>xhr.open("GET","/try/ajax/ajax_info.txt",false); //通過設置第三個async為false可以同步調用ajax
複製代碼/<code>

Promise

針對回調函數這麼多缺點,ES6中引入了一個新的概念Promise,Promise是一個構造函數,通過new關鍵字創建一個Promise的實例,來看看Promise是怎麼解決回調函數的這些問題


手把手教你ES6語法知識點補充


Promise並不是回調函數的衍生版本,而是2個概念,所以需要將之前的回調函數改為支持Promise的版本,這個過程成為"提升",或者"promisory",現代MVVM框架常用的第三方請求庫axios就是一個典型的例子,另外nodejs中也有bluebird,Q等

  1. 多重嵌套,導致回調地獄

Promise在設計的時候引入了鏈式調用的概念,每個then方法同樣也是一個Promise,因此可以無限鏈式調用下去


手把手教你ES6語法知識點補充


配合箭頭函數,明顯的比之前回調函數的多層嵌套優雅很多

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

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

  1. 信任問題,你不能把你的回調完全寄託與第三方庫,因為你不知道第三方庫到底會怎麼執行回調(多次執行)

Promise本身是一個狀態機,具有以下3個狀態

  • pending(等待)
  • fulfilled(成功)
  • rejected(拒絕)

當請求發送沒有得到響應的時候為pending狀態,得到響應後會resolve(決議)當前這個Promise實例,將它變為fulfilled/rejected(大部分情況會變為fulfilled)

當請求發生錯誤後會執行reject(拒絕)將這個Promise實例變為rejected狀態

一個Promise實例的狀態只能從pending => fulfilled 或者從 pending => rejected,即當一個Promise實例從pending狀態改變後,就不會再改變了(不存在fulfilled => rejected 或 rejected => fulfilled)

而Promise實例必須主動調用then方法,才能將值從Promise實例中取出來(前提是Promise不是pending狀態),這一個“主動”的操作就是解決這個問題的關鍵,即第三方庫做的只是改變Promise的狀態,而響應的值怎麼處理,這是開發者主動控制的,這裡就實現了控制反轉,將原來第三方庫的控制權轉移到了開發者上


手把手教你ES6語法知識點補充


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

Promise的then方法會接受2個函數,第一個函數是這個Promise實例被resolve時執行的回調,第二個函數是這個Promise實例被reject時執行的回調,而這個也是開發者主動調用的

使用Promise在異步請求發送錯誤的時候,即使沒有捕獲錯誤,也不會阻塞主線程的代碼(準確的來說,異步的錯誤都不會阻塞主線程的代碼)


手把手教你ES6語法知識點補充


  1. 不清楚回調是否都是異步調用的

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

對於第三個問題中,為什麼說執行了resolve函數後"大部分情況"會進入fulfilled狀態呢?考慮以下情況


手把手教你ES6語法知識點補充


手把手教你ES6語法知識點補充


(這裡用一個定時器在下輪事件循環中打印這個Promise實例的狀態,否則會是pending狀態)

很多人認為promise中調用了resolve函數則這個promise一定會進入fulfilled狀態,但是這裡可以看到,即使調用了resolve函數,仍返回了一個拒絕狀態的Promise,原因是因為如果在一個promise的resolve函數中又傳入了一個Promise,會展開傳入的這個promise

這裡因為傳入了一個拒絕狀態的promise,resolve函數展開這個promise後,就會變成一個拒絕狀態的promise,所以把resolve理解為決議比較好一點

等同於這樣

手把手教你ES6語法知識點補充


建議

在日常開發中,建議全面擁抱新的Promise語法,其實現在的異步編程基本也都使用的是Promise

建議使用ES7的async/await進一步的優化Promise的寫法,async函數始終返回一個Promise,await可以實現一個"等待"的功能,async/await被成為異步編程的終極解決方案,即用同步的形式書寫異步代碼,並且能夠更優雅的實現異步代碼順序執行以及在發生異步的錯誤時提供更精準的錯誤信息,詳細用法可以看阮老師的ES6標準入門


手把手教你ES6語法知識點補充


關於Promise還有很多很多需要講的,包括它的靜態方法all,race,resolve,reject,Promise的執行順序,Promise嵌套Promise,thenable對象的處理等,礙於篇幅這裡只介紹了一下為什麼需要使用Promise。但很多開發者在日常使用中只是瞭解這些API,卻不知道Promise內部具體是怎麼實現的,遇到複雜的異步代碼就無從下手,非常建議去了解一下Promise A+的規範,自己實現一個Promise

ES6 Module(常用)

在ES6 Module出現之前,模塊化一直是前端開發者討論的重點,面對日益增長的需求和代碼,需要一種方案來將臃腫的代碼拆分成一個個小模塊,從而推出了AMD,CMD和CommonJs這3種模塊化方案,前者用在瀏覽器端,後面2種用在服務端,直到ES6 Module出現

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

手把手教你ES6語法知識點補充


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


手把手教你ES6語法知識點補充


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

  1. ES6 Module是靜態的,也就是說它是在編譯階段運行,和var以及function一樣具有提升效果(這個特點使得它支持tree shaking)
  2. 自動採用嚴格模式(頂層的this返回undefined)
  3. ES6 Module支持使用export {}導出具名的接口,或者export default導出匿名的接口

module.js導出:

手把手教你ES6語法知識點補充


a.js導入:

手把手教你ES6語法知識點補充


這兩者的區別是,export {}導出的是一個變量的引用,export default導出的是一個值

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

module.js:

手把手教你ES6語法知識點補充


a.js:

手把手教你ES6語法知識點補充


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

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

module.js導出:

手把手教你ES6語法知識點補充


a.js導入:

手把手教你ES6語法知識點補充


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

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

  1. CommonJs輸出的是一個值的拷貝,ES6 Module通過export {}輸出的是一個變量的引用,export default輸出的是一個值
  2. CommonJs運行在服務器上,被設計為運行時加載,即代碼執行到那一行才回去加載模塊,而ES6 Module是靜態的輸出一個接口,發生在編譯的階段
  3. CommonJs在第一次加載的時候運行一次並且會生成一個緩存,之後加載返回的都是緩存中的內容

import()

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


手把手教你ES6語法知識點補充


手把手教你ES6語法知識點補充


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

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

函數默認值

ES6允許在函數的參數中設置默認值

ES5寫法:

手把手教你ES6語法知識點補充


ES6寫法:

手把手教你ES6語法知識點補充


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

如果使用了函數默認參數,在函數的參數的區域(括號裡面),它會作為一個單獨的塊級作用域,並且擁有let/const方法的一些特性,比如暫時性死區


手把手教你ES6語法知識點補充


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

再來看一個例子

手把手教你ES6語法知識點補充


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

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


手把手教你ES6語法知識點補充


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

在舉個例子:

手把手教你ES6語法知識點補充


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


手把手教你ES6語法知識點補充


通過debugger可以更加直觀的發現在這個函數內部可以通過詞法作用域訪問func函數,foo變量,還有this,但是當查看func函數的詞法作用域時,發現它只能訪問到Global,即全局作用域,foo變量並不存在於它的詞法作用域中

函數默認值配合解構賦值


手把手教你ES6語法知識點補充


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

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

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

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

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

Proxy

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

手把手教你ES6語法知識點補充


Object.definePropery

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


手把手教你ES6語法知識點補充


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

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

handler.apply

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


手把手教你ES6語法知識點補充


調用攔截後的函數:


手把手教你ES6語法知識點補充


handler.contruct

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


手把手教你ES6語法知識點補充


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

handler.defineProperty

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

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


手把手教你ES6語法知識點補充


這裡有幾個知識點

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

這樣就實現了無論對象嵌套多少層,只要有屬性進行賦值就會觸發get方法,對這層對象進行代理,隨後觸發defineProperty執行callback回調函數

其他的使用場景

Proxy另外還有很多功能,比如在實現驗證器的時候,可以將業務邏輯和驗證器分離達到解耦,通過get攔截對私有變量的訪問實現私有變量,攔截對象做日誌記錄,實現微信api的promise化等

Vue

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

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

<code><template>

{{arr}}

{{obj}}

<button>修改arr下標/<button>
<button>創建obj的屬性/<button>

/<template>


複製代碼/<code>


手把手教你ES6語法知識點補充


可以看到這裡數據改變了,控制檯打印出了新的值,但是視圖沒有更新,這是因為Vue內部使用Object.defineProperty進行的數據劫持,而這個API無法探測到

對象根屬性的添加和刪除,以及直接給數組下標進行賦值,所以不會通知渲染watcher進行視圖更新,而理論上這個API也無法探測到數組的一系列方法(push,splice,pop),但是Vue框架修改了數組的原型,使得在調用這些方法修改數據後會執行視圖更新的操作

<code>//源碼位置: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(); //這一行就會主動調用notify方法,會通知到渲染watcher進行視圖更新
return result
});
});
複製代碼/<code>

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

3.0 將帶來一個基於 Proxy 的 observer 實現,它可以提供覆蓋語言 (JavaScript——譯註) 全範圍的響應式能力,消除了當前 Vue 2 系列中基於 Object.defineProperty 所存在的一些侷限,如: 對屬性的添加、刪除動作的監測 對數組基於下標的修改、對於 .length 修改的監測 對 Map、Set、WeakMap 和 WeakSet 的支持

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

所以要想深入瞭解Vue3.0實現機制,學會Proxy是必不可少的

Object.assign

這個ES6新增的Object靜態方法允許我們進行多個對象的合併


手把手教你ES6語法知識點補充


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

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

  1. Object.assign是淺拷貝,對於值是引用類型的屬性,拷貝仍舊的是它的引用
  2. 可以拷貝Symbol屬性
  3. 不能拷貝不可枚舉的屬性
  4. Object.assign保證target始終是一個對象,如果傳入一個基本類型,會轉為基本包裝類型,null/undefined沒有基本包裝類型,所以傳入會報錯
  5. source參數如果是不可枚舉的數據類型會忽略合併(字符串類型被認為是可枚舉的,因為內部有iterator接口)
  6. 因為是用等號進行賦值,如果被賦值的對象的屬性有setter函數會觸發setter函數,同理如果有getter函數,也會調用賦值對象的屬性的getter函數(這就是為什麼Object.assign無法合併對象屬性的訪問器,因為它會直接執行對應的getter/setter函數而不是合併它們,如果需要合併對象屬性的getter/setter函數,可以使用ES7提供的Object.getOwnPropertyDescriptors和Object.defineProperties這2個API實現)


手把手教你ES6語法知識點補充


手把手教你ES6語法知識點補充


可以看到這裡成功的複製了obj對象中a屬性的getter/setter

為了加深瞭解我自己模擬了Object.assign的實現,可供參考


手把手教你ES6語法知識點補充


這裡有一個坑不得不提,對於target參數傳入一個字符串,內部會轉換為基本包裝類型,而字符串基本包裝類型的屬性是隻讀的(屬性描述符的writable屬性為false),這裡感謝木易楊的專欄


手把手教你ES6語法知識點補充


手把手教你ES6語法知識點補充


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


手把手教你ES6語法知識點補充


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

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

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


手把手教你ES6語法知識點補充


手把手教你ES6語法知識點補充


(最後一個字符串get可以忽略,這是控制檯為了顯示a變量觸發的getter函數)

分析一下這個例子

ES9:

  • 會合並2個對象,並且只觸發2個對象對應屬性的getter函數
  • 相同屬性的後者覆蓋了前者,所以a屬性的值是第二個getter函數return的值

ES6:

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

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

建議

  1. Vue中重置data中的數據

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


手把手教你ES6語法知識點補充


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

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


手把手教你ES6語法知識點補充


可以封裝一個函數,外層聲明一個DEFAULTS常量,options為每次傳入的動態配置,這樣每次執行後會合併一些默認的配置項

  1. 在傳參的時候可以多個數據合併成一個對象傳給後端


手把手教你ES6語法知識點補充


  1. 阮一峰:ES6標準入門:https://es6.ruanyifeng.com/
  2. 慕課網:ES6零基礎教學:https://coding.imooc.com/class/98.html
  3. 你不知道的JavaScript下卷


分享到:


相關文章: