let,const用於聲明變量,用來替代老語法的var關鍵字,與var不一樣的是,let/const會建立一個塊級做用域(通俗講就是一個花括號內是一個新的做用域)前端
這裏外部的console.log(x)拿不到前面2個塊級做用域聲明的let:node
在平常開發中多存在於使用if/for關鍵字結合let/const建立的塊級做用域,值得注意的是使用let/const關鍵字聲明變量的for循環和var聲明的有些不一樣ios
for循環分爲3部分,第一部分包含一個變量聲明,第二部分包含一個循環的退出條件,第三部分包含每次循環最後要執行的表達式,也就是說第一部分在這個for循環中只會執行一次var i = 0,然後面的兩個部分在每次循環的時候都會執行一遍es6
而使用使用let/const關鍵字聲明變量的for循環,除了會建立塊級做用域,let/const還會將它綁定到每一個循環中,確保對上個循環結束時候的值進行從新賦值web
什麼意思呢?簡而言之就是每次循環都會聲明一次(對比var聲明的for循環只會聲明一次),能夠這麼理解let/const中的for循環ajax
給每次循環建立一個塊級做用域:chrome
使用let/const聲明的變量,從一開始就造成了封閉做用域,在聲明變量以前是沒法使用這個變量的,這個特色也是爲了彌補var的缺陷(var聲明的變量有變量提高)npm
在預編譯的階段,JS編譯器會先解析一遍判斷是否有let/const聲明的變量,若是在一個花括號中存在使用let/const聲明的變量,則ES6規定這些變量在沒聲明前是沒法使用的,隨後再是進入執行階段執行代碼編程
這裏當知足if的條件時,進入true的邏輯,這裏由於使用了let聲明瞭變量name,在一開始就"劫持了這個做用域",使得任何在let聲明以前使用name的操做都會報錯axios
使用var聲明的變量,由於會有變量提高,一樣也是發生在預編譯階段,var會提高到當前函數做用域的頂部而且默認賦值爲undefined,若是這幾行代碼是在全局做用域下,則name變量會直接提高到全局做用域,隨後進入執行階段執行代碼,name被賦值爲"abc",而且能夠成功打印出字符串abc
至關於這樣
暫時性死區實際上是爲了防止ES5之前在變量聲明前就使用這個變量,這是由於var的變量提高的特性致使一些不熟悉var原理的開發者習覺得常的覺得變量能夠先使用在聲明,從而埋下一些隱患
關於JS預編譯和JS的3種做用域(全局,函數,塊級)這裏也不贅述了,不然又能寫出幾千字的博客,有興趣的朋友自行了解一下,一樣也有助於瞭解JavaScript這門語言
使用const關鍵字聲明一個常量,常量的意思是不會改變的變量,const和let的一些區別是
const聲明變量的時候必須賦值,不然會報錯,一樣使用const聲明的變量被修改了也會報錯
const聲明變量不能改變,若是聲明的是一個引用類型,則不能改變它的內存地址(這裏牽扯到JS引用類型的特色,有興趣能夠看我另外一篇博客對象深拷貝和淺拷貝)
有些人會有疑問,爲何平常開發中沒有顯式的聲明塊級做用域,let/const聲明的變量卻沒有變爲全局變量
這個其實也是let/const的特色,ES6規定它們不屬於頂層全局變量的屬性,這裏用chrome調試一下
能夠看到使用let聲明的變量x是在一個叫script做用域下的,而var聲明的變量由於變量提高因此提高到了全局變量window對象中,這使咱們能放心的使用新語法,不用擔憂污染全局的window對象
在平常開發中,個人建議是全面擁抱let/const,通常的變量聲明使用let關鍵字,而當聲明一些配置項(相似接口地址,npm依賴包,分頁器默認頁數等一些一旦聲明後就不會改變的變量)的時候可使用const,來顯式的告訴項目其餘開發者,這個變量是不能改變的(const聲明的常量建議使用全大寫字母標識,單詞間用下劃線),同時也建議瞭解var關鍵字的缺陷(變量提高,污染全局變量等),這樣才能更好的使用新語法
ES6 容許使用箭頭(=>)定義函數
箭頭函數對於使用function關鍵字建立的函數有如下區別
箭頭函數沒有arguments(建議使用更好的語法,剩餘運算符替代)
箭頭函數沒有prototype屬性,沒有constructor,即不能用做與構造函數(不能用new關鍵字調用)
箭頭函數沒有本身this,它的this是詞法的,引用的是上下文的this,即在你寫這行代碼的時候就箭頭函數的this就已經和外層執行上下文的this綁定了(這裏我的認爲並不表明徹底是靜態的,由於外層的上下文還是動態的可使用call,apply,bind修改,這裏只是說明了箭頭函數的this始終等於它上層上下文中的this)
由於setTimeout會將一個匿名的回調函數推入異步隊列,而回調函數是具備全局性的,即在非嚴格模式下this會指向window,就會存在丟失變量a的問題,而若是使用箭頭函數,在書寫的時候就已經肯定它的this等於它的上下文(這裏是makeRequest的函數執行上下文,至關於講箭頭函數中的this綁定了makeRequest函數執行上下文中的this),因此this就指向了makeRequest中的a變量
箭頭函數中的this即便使用call,apply,bind也沒法改變指向(這裏也驗證了爲何ECMAScript規定不能使用箭頭函數做爲構造函數,由於它的this已經肯定好了沒法改變)
箭頭函數替代了之前須要顯式的聲明一個變量保存this的操做,使得代碼更加的簡潔
ES5寫法不推薦:
ES6箭頭函數:
值得注意的是makeRequest後面的function不能使用箭頭函數,由於這樣它就會再使用上層的this,而再上層是全局的執行上下文,它的this的值會指向window
setTimeout第一個參數使用了箭頭函數,它會引用上下文的this,而它的外層也是一個箭頭函數,又會引用再上層的this,最上層就是整個全局上下文,即this的值爲window對象,因此沒有變量a
在數組的迭代中使用箭頭函數更加簡潔,而且省略了return關鍵字
不要在可能改變this指向的函數中使用箭頭函數,相似Vue中的methods,computed中的方法,生命週期函數,Vue將這些函數的this綁定了當前組件的vm實例,若是使用箭頭函數會強行改變this,由於箭頭函數優先級最高(沒法再使用call,apply,bind改變指向)
在把箭頭函數做爲平常開發的語法以前,我的建議是去了解一下箭頭函數的是如何綁定this的,而不僅是當作省略function這幾個單詞拼寫,畢竟那纔是ECMAScript真正但願解決的問題
iterator迭代器是ES6很是重要的概念,可是不少人對它瞭解的很少,可是它倒是另外4個ES6經常使用特性的實現基礎(解構賦值,剩餘/擴展運算符,生成器,for of循環),瞭解迭代器的概念有助於瞭解另外4個核心語法的原理,另外ES6新增的Map,Set數據結構也有使用到它,因此我放到前面來說
對於可迭代的數據解構,ES6在內部部署了一個[Symbol.iterator]屬性,它是一個函數,執行後會返回iterator對象(也叫迭代器對象,也叫iterator接口),擁有[Symbol.iterator]屬性的對象即被視爲可迭代的
數組中的Symbol.iterator方法默認部署在數組原型上:
默認具備iterator接口的數據結構有如下幾個,注意普通對象默認是沒有iterator接口的(能夠本身建立iterator接口讓普通對象也能夠迭代)
Array
Map
Set
String
TypedArray(類數組)
函數的 arguments 對象
NodeList 對象
iterator迭代器是一個對象,它具備一個next方法因此能夠這麼調用
next方法返回又會返回一個對象,有value和done兩個屬性,value即每次迭代以後返回的值,而done表示是否還須要再次循環,能夠看到當value爲undefined時,done爲true表示循環終止
梳理一下
可迭代的數據結構會有一個[Symbol.iterator]方法
[Symbol.iterator]執行後返回一個iterator對象
iterator對象有一個next方法
next方法執行後返回一個有value,done屬性的對象
這裏簡要概述瞭如下iterator的概念,有興趣能夠去看阮一峯老師的《ECMAScript 6 入門》
解構賦值能夠直接使用對象的某個屬性,而不須要經過屬性訪問的形式使用,對象解構原理我的認爲是經過尋找相同的屬性名,而後原對象的這個屬性名的值賦值給新對象對應的屬性
這裏左邊真正聲明的實際上是titleOne,titleTwo這兩個變量,而後會根據左邊這2個變量的位置尋找右邊對象中title和test[0]中的title對應的值,找到字符串abc和test賦值給titleOne,titleTwo(若是沒有找到會返回undefined)
數組解構的原理實際上是消耗數組的迭代器,把生成對象的value屬性的值賦值給對應的變量
數組解構的一個用途是交換變量,避免之前要聲明一個臨時變量值存儲值
ES6交換變量:
一樣建議使用,由於解構賦值語意化更強,對於做爲對象的函數參數來講,能夠減小形參的聲明,直接使用對象的屬性(若是嵌套層數過多我我的認爲不適合用對象解構,不太優雅)
一個經常使用的例子是Vuex中actions中的方法會傳入2個參數,第一個參數是個對象,你能夠隨意命名,而後使用<名字>.commit的方法調用commit函數,或者使用對象解構直接使用commit
不使用對象解構:
使用對象解構:
另外能夠給使用axios的響應結果進行解構(axios默認會把真正的響應結果放在data屬性中)
剩餘/擴展運算符一樣也是ES6一個很是重要的語法,使用3個點(...),後面跟着一個數組,它使得能夠"展開"這個數組,能夠這麼理解,數組是存放元素集合的一個容器,而使用剩餘/擴展運算符能夠將這個容器拆開,這樣就只剩下元素集合,你能夠把這些元素集合放到另一個數組裏面
只要含有iterator接口的數據結構均可以使用擴展運算符
擴展運算符能夠和數組的解構賦值一塊兒使用,可是必須放在最後一個,由於剩餘/擴展運算符的原理實際上是利用了數組的迭代器,它會消耗3個點後面的數組的全部迭代器,讀取全部迭代器的value屬性,剩餘/擴展運算符後不能在有解構賦值,由於剩餘/擴展運算符已經消耗了全部迭代器,而數組的解構賦值也是消耗迭代器,可是這個時候已經沒有迭代器了,因此會報錯
這裏first會消耗右邊數組的一個迭代器,...arr會消耗剩餘全部的迭代器,而第二個例子...arr直接消耗了全部迭代器,致使last沒有迭代器可供消耗了,因此會報錯,由於這是毫無心義的操做
剩餘運算符最重要的一個特色就是替代了之前的arguments
訪問函數的arguments對象是一個很昂貴的操做,之前的arguments.callee也被廢止了,建議在支持ES6語法的環境下不要在使用arguments,使用剩餘運算符替代(箭頭函數沒有arguments,必須使用剩餘運算符才能訪問參數集合)
剩餘運算符和擴展運算符的區別就是,剩餘運算符會收集這些集合,放到右邊的數組中,擴展運算符是將右邊的數組拆分紅元素的集合,它們是相反的
這個是ES9的語法,ES9中支持在對象中使用擴展運算符,以前說過數組的擴展運算符原理是消耗全部迭代器,但對象中並無迭代器,我我的認爲多是實現原理不一樣,可是仍能夠理解爲將鍵值對從對象中拆開,它能夠放到另一個普通對象中
其實它和另一個ES6新增的API類似,即Object.assign,它們均可以合併對象,可是仍是有一些不一樣Object.assign會觸發目標對象的setter函數,而對象擴展運算符不會,這個咱們放到後面討論
使用擴展運算符能夠快速的將類數組轉爲一個真正的數組
合併多個數組
函數柯里化
es6容許當對象的屬性和值相同時,省略屬性名
須要注意的是
對象屬性簡寫常常與解構賦值一塊兒使用
結合上文的解構賦值,這裏的代碼會實際上是聲明瞭x,y,z變量,由於bar函數會返回一個對象,這個對象有x,y,z這3個屬性,解構賦值會尋找等號右邊表達式的x,y,z屬性,找到後賦值給聲明的x,y,z變量
es6容許當一個對象的屬性的值是一個函數(便是一個方法),可使用簡寫的形式
在Vue中由於都是在vm對象中書寫方法,徹底可使用方法簡寫的方式書寫函數
for ... of是做爲ES6新增的遍歷方式,容許遍歷一個含有iterator接口的數據結構而且返回各項的值,和ES3中的for ... in的區別以下
for ... of遍歷獲取的是對象的鍵值,for ... in 獲取的是對象的鍵名
for ... in會遍歷對象的整個原型鏈,性能很是差不推薦使用,而for ... of只遍歷當前對象不會遍歷原型鏈
對於數組的遍歷,for ... in會返回數組中全部可枚舉的屬性(包括原型鏈),for ... of只返回數組的下標對於的屬性值
for ... of循環的原理其實也是利用了遍歷對象內部的iterator接口,將for ... of循環分解成最原始的for循環,內部實現的機制能夠這麼理解
能夠看到只要知足第二個條件(iterator.next()存在且res.done爲true)就能夠一直循環下去,而且每次把迭代器的next方法生成的對象賦值給res,而後將res的value屬性賦值給for ... of第一個條件中聲明的變量便可,res的done屬性控制是否繼續遍歷下去
for... of循環同時支持break,continue,return(在函數中調用的話)而且能夠和對象解構賦值一塊兒使用
arr數組每次使用for ... of循環都返回一對象({a:1},{a:2},{a:3}),而後會通過對象解構,尋找屬性爲a的值,賦值給obj.a,因此在每輪循環的時候obj.a會分別賦值爲1,2,3
Promise做爲ES6中推出的新的概念,改變了JS的異步編程,現代前端大部分的異步請求都是使用Promise實現,fetch這個web api也是基於Promise的,這裏不得簡述一下以前統治JS異步編程的回調函數,回調函數有什麼缺點,Promise又是怎麼改善這些缺點
衆所周知,JS是單線程的,由於多個線程改變DOM的話會致使頁面紊亂,因此設計爲一個單線程的語言,可是瀏覽器是多線程的,這使得JS同時具備異步的操做,即定時器,請求,事件監聽等,而這個時候就須要一套事件的處理機制去決定這些事件的順序,即Event Loop(事件循環),這裏不會詳細講解事件循環,只須要知道,前端發出的請求,通常都是會進入瀏覽器的http請求線程,等到收到響應的時候會經過回調函數推入異步隊列,等處理完主線程的任務會讀取異步隊列中任務,執行回調
在《你不知道的JavaScript》下卷中,這麼介紹
使用回調函數處理異步請求至關於把你的回調函數置於了一個黑盒,使用第三方的請求庫你可能會這麼寫
收到響應後,執行後面的回調打印字符串,可是若是這個第三方庫有相似超時重試的功能,可能會執行屢次你的回調函數,若是是一個支付功能,你就會發現你扣的錢可能就不止1000元了-.-
另一個衆所周知的問題就是,在回調函數中再嵌套回調函數會致使代碼很是難以維護,這是人們常說的「回調地獄」
你使用的第三方ajax庫還有可能並無提供一些錯誤的回調,請求失敗的一些錯誤信息可能會被吞掉,而你確徹底不知情
總結一下回調函數的一些缺點
多重嵌套,致使回調地獄
代碼跳躍,並不是人類習慣的思惟模式
信任問題,你不能把你的回調徹底寄託與第三方庫,由於你不知道第三方庫到底會怎麼執行回調(屢次執行)
第三方庫可能沒有提供錯誤處理
不清楚回調是否都是異步調用的(能夠同步調用ajax,在收到響應前會阻塞整個線程,會陷入假死狀態,很是不推薦)
xhr.open("GET","/try/ajax/ajax_info.txt",false); //經過設置第三個async爲false能夠同步調用ajax
針對回調函數這麼多缺點,ES6中引入了一個新的概念,Promise,Promise是一個構造函數,經過new關鍵字建立一個Promise的實例,來看看Promise是怎麼解決回調函數的這些問題
Promise並非回調函數的衍生版本,而是2個概念,因此須要將以前的回調函數改成支持Promise的版本,這個過程成爲"提高",或者"promisory",現代MVVM框架經常使用的第三方請求庫axios就是一個典型的例子,另外nodejs中也有bluebird,Q等
多重嵌套,致使回調地獄
Promise在設計的時候引入了鏈式調用的概念,每一個then方法一樣也是一個Promise,所以能夠無限鏈式調用下去
配合箭頭函數,明顯的比以前回調函數的多層嵌套優雅不少
代碼跳躍,並不是人類習慣的思惟模式
Promise使得可以同步思惟書寫代碼,上述的代碼就是先請求3000端口,獲得響應後再請求3001,再請求3002,再請求3003,而書寫的格式也是符合人類的思惟,從先到後
信任問題,你不能把你的回調徹底寄託與第三方庫,由於你不知道第三方庫到底會怎麼執行回調(屢次執行)
Promise自己是一個狀態機,具備pending(等待),resolve(決議),reject(拒絕)這3個狀態,當請求發送沒有獲得響應的時候會pending狀態,而且一個Promise實例的狀態只能從pending => resolve 或者從 pending => reject,即當一個Promise實例從pending狀態改變後,就不會再改變了(不存在resolve => reject 或 reject => resolve)
而Promise實例必須主動調用then方法,才能將值從Promise實例中取出來(前提是Promise不是pending狀態),這一個「主動」的操做就是解決這個問題的關鍵,即第三方庫作的只是把改變Promise的狀態,而響應的值怎麼處理,這是開發者主動控制的,這裏就實現了控制反轉,將原來第三方庫的控制權轉移到了開發者上
第三方庫可能沒有提供錯誤處理
Promise的then方法會接受2個函數,第一個函數是這個Promise實例被resolve時執行的回調,第二個函數是這個Promise實例被reject時執行的回調,而這個也是開發者主動調用的
使用Promise在異步請求發送錯誤的時候,即便沒有捕獲錯誤,也不會阻塞主線程的代碼
不清楚回調是否都是異步調用的
Promise在設計的時候保證全部響應的處理回調都是異步調用的,不會阻塞代碼的執行,Promise將then方法的回調放入一個叫微任務的隊列中(MicroTask),保證這些回調任務都在同步任務執行完再執行,這部分一樣也是事件循環的知識點,有興趣的朋友能夠深刻研究一下
在平常開發中,建議全面擁抱新的Promise語法,其實如今的異步編程基本也都使用的是Promise
建議使用ES7的async/await進一步的優化Promise的寫法,async函數始終返回一個Promise,await能夠實現一個"等待"的功能,async/await被成爲異步編程的終極解決方案,即用同步的形式書寫異步代碼,而且可以更優雅的實現異步代碼順序執行,詳情能夠看阮老師的ES6標準入門
關於Promise還有不少不少須要講的,包括它的靜態方法all,race,resolve,reject,Promise的執行順序,Promise嵌套Promise,thenable對象的處理等,礙於篇幅這裏只介紹了一下爲何須要使用Promise。但不少開發者在平常使用中只是瞭解這些API,殊不知道Promise內部具體是怎麼實現的,遇到複雜的異步代碼就無從下手,很是建議去了解一下Promise A+的規範,本身實現一個Promise
在ES6 Module出現以前,模塊化一直是前端開發者討論的重點,面對日益增加的需求和代碼,須要一種方案來將臃腫的代碼拆分紅一個個小模塊,從而推出了AMD,CMD和CommonJs這3種模塊化方案,前者用在瀏覽器端,後面2種用在服務端,直到ES6 Module出現
ES6 Module默認目前尚未被瀏覽器支持,須要使用babel,在平常寫demo的時候常常會顯示這個錯誤
能夠在script標籤中使用tpye="module"在同域的狀況下能夠解決(非同域狀況會被同源策略攔截,webstorm會開啓一個同域的服務器沒有這個問題,vscode貌似不行)
ES6 Module使用import關鍵字導入模塊,export關鍵字導出模塊,它還有如下特色
ES6 Module是靜態的,也就是說它是在編譯階段運行,和var以及function同樣具備提高效果(這個特色使得它支持tree shaking)
自動採用嚴格模式(頂層的this返回undefined)
ES6 Module支持使用export {<變量>}導出具名的接口,或者export default導出匿名的接口
module.js導出:
a.js導入:
這二者的區別是,export {<變量>}導出的是一個變量的引用,export default導出的是一個值
什麼意思呢,就是說在a.js中使用import導入這2個變量的後,在module.js中由於某些緣由x變量被改變了,那麼會馬上反映到a.js,而module.js中的y變量改變後,a.js中的y仍是原來的值
module.js:
a.js:
能夠看到給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導出:
a.js導入:
可是因爲是使用export {<變量>}這種形式導出的模塊,即便被重命名爲default,仍然導出的是一個變量的引用
這裏再來講一下目前爲止主流的模塊化方案ES6 Module和CommonJs的一些區別
CommonJs輸出的是一個值的拷貝,ES6 Module經過export {<變量>}輸出的是一個變量的引用,export default輸出的是一個值的拷貝
CommonJs運行在服務器上,被設計爲運行時加載,即代碼執行到那一行纔回去加載模塊,而ES6 Module是靜態的輸出一個接口,發生在編譯的階段
CommonJs在第一次加載的時候運行一次,以後加載返回的都是第一次的結果,具備緩存的效果,ES6 Module則沒有
關於ES6 Module靜態編譯的特色,致使了沒法動態加載,可是老是會有一些須要動態加載模塊的需求,因此如今有一個提案,使用把import做爲一個函數能夠實現動態加載模塊,它返回一個Promise,Promise被resolve時的值爲輸出的模塊
使用import方法改寫上面的a.js使得它能夠動態加載(使用靜態編譯的ES6 Module放在條件語句會報錯,由於會有提高的效果,而且也是不容許的),能夠看到輸出了module.js的一個變量x和一個默認輸出
Vue中路由的懶加載的ES6寫法就是使用了這個技術,使得在路由切換的時候可以動態的加載組件渲染視圖
ES6容許在函數的參數中設置默認值
ES5寫法:
ES6寫法:
相比ES5,ES6函數默認值直接寫在參數上,更加的直觀
若是使用了函數默認參數,在函數的參數的區域(括號裏面),它會做爲一個單獨的做用域,而且擁有let/const方法的一些特性,好比暫時性死區,塊級做用域,沒有變量提高等,而這個做用域在函數內部代碼執行前
這裏當運行func的時候,由於沒有傳參數,使用函數默認參數,y就會去尋找x的值,在沿着詞法做用域在外層找到了值爲1的變量x
再來看一個例子
這裏一樣沒有傳參數,使用函數的默認賦值,x經過詞法做用域找到了變量w,因此x默認值爲2,y一樣經過詞法做用域找到了剛剛定義的x變量,y的默認值爲3,可是在解析到z = z + 1這一行的時候,JS解釋器先會去解析z+1找到相應的值後再賦給變量z,可是由於暫時性死區的緣由(let/const"劫持"了這個塊級做用域,沒法在聲明以前使用這個變量,上文有解釋),致使在let聲明以前就使用了變量z,因此會報錯
這樣理解函數的默認值會相對容易一些
當傳入的參數爲undefined時才使用函數的默認值(顯式傳入undefined也會觸發使用函數默認值,傳入null則不會觸發)
在舉個例子:
這裏借用阮一峯老師書中的一個例子,func的默認值爲一個函數,執行後返回foo變量,而在函數內部執行的時候,至關於對foo變量的一次變量查詢(LHS查詢),而查詢的起點是在這個單獨的做用域中,即JS解釋器不會去查詢去函數內部查詢變量foo,而是沿着詞法做用域先查看同一做用域(前面的函數參數)中有沒有foo變量,再往函數的外部尋找foo變量,最終找不到因此報錯了,這個也是函數默認值的一個特色
第一行給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一樣是一個構造函數,使用new關鍵字生成一個攔截對象的實例,ES6提供了很是多對象攔截的操做,幾乎覆蓋了全部可能修改目標對象的狀況(Proxy通常和Reflect配套使用,前者攔截對象,後者返回攔截的結果,Proxy上有的的攔截方法Reflect都有)
提到Proxy就不得不提一下ES5中的Object.defineProperty,這個api能夠給一個對象添加屬性以及這個屬性的屬性描述符/訪問器(這2個不能共存,同一屬性只能有其中一個),屬性描述符有configurable,writable,enumerable,value這4個屬性,分別表明是否可配置,是否只讀,是否可枚舉和屬性的值,訪問器有configurable,enumerable,get,set,前2個和屬性描述符功能相同,後2個都是函數,定義了get,set後對元素的讀寫操做都會執行這個函數,而且覆蓋默認的讀寫行爲
定義了obj中a屬性的表示爲只讀,且不可枚舉,obj2定義了get,但沒有定義set表示只讀,而且讀取obj2的b屬性返回的值是get函數的返回值
ES5中的Object.defineProperty這和Proxy有什麼關係呢?我的理解Proxy是Object.defineProperty的加強版,ES5只規定可以定義屬性的屬性描述符或訪問器.而Proxy加強到了13種,具體太多了我就不一一放出來了,這裏我舉幾個比較有意思的例子
apply可讓咱們攔截一個函數(JS中函數也是對象,Proxy也能夠攔截函數)的執行,咱們能夠把它用在函數節流中
調用攔截後的函數:
contruct能夠攔截經過new關鍵字調用這個函數的操做,咱們能夠把它用在單例模式中
這裏經過一個閉包保存了instance變量,每次使用new關鍵字調用被攔截的函數後都會查看這個instance變量,若是存在就返回閉包中保存的instance變量,不然就新建一個實例,這樣能夠實現全局只有一個實例
defineProperty能夠攔截對這個對象的Object.defineProerty操做
注意對象內部的默認的[[SET]]函數(即對這個對象的屬性賦值)會間接觸發defineProperty和getOwnPropertyDescriptor這2個攔截方法
這裏有幾個知識點
這裏使用了遞歸的操做,當須要訪問對象的屬性時候,會判斷代理的對象屬性的值還是一個能夠代理的對象就遞歸的進行代理,不然經過錯誤捕獲執行默認的get函數
定義了defineProperty的攔截方法,當對這個代理對象的某個屬性進行賦值的時候會執行對象內部的[[SET]]函數進行賦值,這個操做會間接觸發defineProperty這個方法,隨後會執行定義的callback函數
這樣就實現了不管對象嵌套多少層,只要有屬性進行賦值就會觸發get方法,對這層對象進行代理,隨後觸發defineProperty執行callback回調函數
Proxy另外還有不少功能,好比在實現驗證器的時候,能夠將業務邏輯和驗證器分離達到解耦,經過defineProperty設置一些私有變量,攔截對象作日誌記錄等
尤大預計2019年下半年發佈Vue3.0,其中一個核心的功能就是使用Proxy替代Object.defineProperty
我相信瞭解過一點Vue響應式原理的人都知道Vue框架在對象攔截上的一些不足
<template>
<div>
<div>{{arr}}</div>
<div>{{obj}}</div>
<button @click="handleClick">修改arr下標</button>
<button @click="handleClick2">建立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>
能夠看到這裏數據改變了,控制檯打印出了新的值,可是視圖沒有更新,這是由於Vue內部使用Object.defineProperty進行的數據劫持,而這個API沒法探測到對象根屬性的添加和刪除,以及直接給數組下標進行賦值,因此不會通知渲染watcher進行視圖更新,而理論上這個API也沒法探測到數組的一系列方法(push,splice,pop),可是Vue框架修改了數組的原型,使得在調用這些方法修改數據後會執行視圖更新的操做
//源碼位置: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
});
});
在掘金翻譯的尤大Vue3.0計劃中寫到
3.0 將帶來一個基於 Proxy 的 observer 實現,它能夠提供覆蓋語言 (JavaScript——譯註) 全範圍的響應式能力,消除了當前 Vue 2 系列中基於 Object.defineProperty 所存在的一些侷限,如: 對屬性的添加、刪除動做的監測 對數組基於下標的修改、對於 .length 修改的監測 對 Map、Set、WeakMap 和 WeakSet 的支持
Proxy就沒有這個問題,而且還提供了更多的攔截方法,徹底能夠替代Object.defineProperty,惟一不足的也就是瀏覽器的支持程度了(IE:誰在說我?)
因此要想深刻了解Vue3.0實現機制,學會Proxy是必不可少的
這個ES6新增的Object靜態方法容許咱們進行多個對象的合併
能夠這麼理解,Object.assign遍歷須要合併給target的對象(即sourece對象的集合)的屬性,用等號進行賦值,這裏遍歷{a:1}將屬性a和值數字1賦值給target對象,而後再遍歷{b:2}將屬性b和值數字2賦值給target對象
這裏羅列了一些這個API的須要注意的知識點
Object.assign是淺拷貝,對於值是引用類型的屬性拷貝扔的是它的引用
對於Symbol屬性一樣能夠拷貝
不可枚舉的屬性沒法拷貝
target必須是一個對象,若是傳入一個基本類型,會變成基本包裝類型,null/undefined沒有基本包裝類型,因此傳入會報錯
source參數若是是不可枚舉的會忽略合併(字符串類型被認爲是可枚舉的,由於內部有iterator接口)
由於是用等號進行賦值,若是被賦值的對象的屬性有setter函數會觸發setter函數,同理若是有getter函數,也會調用賦值對象的屬性的getter(這就是爲何Object.assign沒法合併對象屬性的訪問器,由於它會直接執行對應的getter/setter函數而不是合併它們,在ES7中可使用Object.defineOwnPropertyDescriptors實現複製屬性訪問器的操做)
這裏爲了加深瞭解我本身模擬了Object.assign的實現,可供參考
ES9支持在對象上使用擴展運算符,實現的功能和Object.assign類似,惟一的區別就是在含有getter/setter函數的對象有所區別
能夠看到,ES9在合併2個對象的時候觸發了合併對象的getter,而ES6中觸發了target對象的setter而不會觸發getter,除此以外,Object.assgin和對象擴展運算符功能是相同的,二者均可以使用,二者都是淺拷貝,使用ES9的方法相對簡潔一點
Vue中重置data中的數據
這個是我最經常使用的小技巧,使用Object.assign能夠將你目前組件中的data對象和組件默認初始化狀態的data對象中的數據合併,這樣能夠達到初始化data對象的效果
在當前組件的實例中$data屬性保存了當前組件的data對象,而$options是當前組件實例初始化時候的對象,其中有個data方法,即在組件中寫的data函數,執行後會返回一個初始化的data對象,而後將這個初始化的data對象合併到當前的data來初始化全部數據
給對象合併須要的默認屬性
能夠封裝一個函數,外層聲明一個DEFAULTS常量,options爲每次傳入的動態配置,這樣每次執行後會合併一些默認的配置項
在傳參的時候能夠多個數據合併成一個對象傳給後端