前言
ECMAScript 6.0(簡(jiǎn)稱ES6),作為下一代JavaScript的語(yǔ)言標(biāo)準(zhǔn)正式發(fā)布于2015 年 6 月冰悠,至今已經(jīng)發(fā)布3年多了段誊,但是因?yàn)樘N(yùn)含的語(yǔ)法之廣俯画,完全消化需要一定的時(shí)間,這里我總結(jié)了部分ES6两踏,以及ES6以后新語(yǔ)法的知識(shí)點(diǎn)败京,使用場(chǎng)景,希望對(duì)各位有所幫助
本文講著重是對(duì)ES6語(yǔ)法特性的補(bǔ)充梦染,不會(huì)講解一些API層面的語(yǔ)法赡麦,更多的是發(fā)掘背后的原理,以及ES6到底解決了什么問(wèn)題
如有錯(cuò)誤,歡迎指出,將在第一時(shí)間修改,歡迎提出修改意見和建議
話不多說(shuō)開始ES6之旅吧~~~
let/const(常用)
let,const用于聲明變量帕识,用來(lái)替代老語(yǔ)法的var關(guān)鍵字泛粹,與var不同的是,let/const會(huì)創(chuàng)建一個(gè)塊級(jí)作用域(通俗講就是一個(gè)花括號(hào)內(nèi)是一個(gè)新的作用域)
這里外部的console.log(x)拿不到前面2個(gè)塊級(jí)作用域聲明的let:
在日常開發(fā)中多存在于使用if/for關(guān)鍵字結(jié)合let/const創(chuàng)建的塊級(jí)作用域肮疗,值得注意的是使用let/const關(guān)鍵字聲明變量的for循環(huán)和var聲明的有些不同
for循環(huán)分為3部分晶姊,第一部分包含一個(gè)變量聲明,第二部分包含一個(gè)循環(huán)的退出條件伪货,第三部分包含每次循環(huán)最后要執(zhí)行的表達(dá)式们衙,也就是說(shuō)第一部分在這個(gè)for循環(huán)中只會(huì)執(zhí)行一次var i = 0钾怔,而后面的兩個(gè)部分在每次循環(huán)的時(shí)候都會(huì)執(zhí)行一遍
而使用使用let/const關(guān)鍵字聲明變量的for循環(huán),除了會(huì)創(chuàng)建塊級(jí)作用域蒙挑,let/const還會(huì)將它綁定到每個(gè)循環(huán)中宗侦,確保對(duì)上個(gè)循環(huán)結(jié)束時(shí)候的值進(jìn)行重新賦值
什么意思呢?簡(jiǎn)而言之就是每次循環(huán)都會(huì)聲明一次(對(duì)比var聲明的for循環(huán)只會(huì)聲明一次)忆蚀,可以這么理解let/const中的for循環(huán)
給每次循環(huán)創(chuàng)建一個(gè)塊級(jí)作用域:
暫時(shí)性死區(qū)
使用let/const聲明的變量矾利,從一開始就形成了封閉作用域,在聲明變量之前是無(wú)法使用這個(gè)變量的蜓谋,這個(gè)特點(diǎn)也是為了彌補(bǔ)var的缺陷(var聲明的變量有變量提升)
在預(yù)編譯的階段,JS編譯器會(huì)先解析一遍判斷是否有l(wèi)et/const聲明的變量,如果在一個(gè)花括號(hào)中存在使用let/const聲明的變量,則ES6規(guī)定這些變量在沒聲明前是無(wú)法使用的,隨后再是進(jìn)入執(zhí)行階段執(zhí)行代碼
這里當(dāng)滿足if的條件時(shí),進(jìn)入true的邏輯,這里因?yàn)槭褂昧薼et聲明了變量name,在一開始就"劫持了這個(gè)作用域",使得任何在let聲明之前使用name的操作都會(huì)報(bào)錯(cuò)
使用var聲明的變量,因?yàn)闀?huì)有變量提升,同樣也是發(fā)生在預(yù)編譯階段,var會(huì)提升到當(dāng)前函數(shù)作用域的頂部并且默認(rèn)賦值為undefined,如果這幾行代碼是在全局作用域下,則name變量會(huì)直接提升到全局作用域,隨后進(jìn)入執(zhí)行階段執(zhí)行代碼,name被賦值為"abc",并且可以成功打印出字符串a(chǎn)bc
相當(dāng)于這樣
暫時(shí)性死區(qū)其實(shí)是為了防止ES5以前在變量聲明前就使用這個(gè)變量,這是因?yàn)関ar的變量提升的特性導(dǎo)致一些不熟悉var原理的開發(fā)者習(xí)以為常的以為變量可以先使用在聲明,從而埋下一些隱患
關(guān)于JS預(yù)編譯和JS的3種作用域(全局,函數(shù),塊級(jí))這里也不贅述了,否則又能寫出幾千字的博客,有興趣的朋友自行了解一下,同樣也有助于了解JavaScript這門語(yǔ)言
const
使用const關(guān)鍵字聲明一個(gè)常量,常量的意思是不會(huì)改變的變量炭分,const和let的一些區(qū)別是
- const聲明變量的時(shí)候必須賦值桃焕,否則會(huì)報(bào)錯(cuò),同樣使用const聲明的變量被修改了也會(huì)報(bào)錯(cuò)
- const聲明變量不能改變捧毛,如果聲明的是一個(gè)引用類型观堂,則不能改變它的內(nèi)存地址(這里牽扯到JS引用類型的特點(diǎn),有興趣可以看我另一篇博客對(duì)象深拷貝和淺拷貝)
有些人會(huì)有疑問(wèn)呀忧,為什么日常開發(fā)中沒有顯式的聲明塊級(jí)作用域师痕,let/const聲明的變量卻沒有變?yōu)槿肿兞?/p>
這個(gè)其實(shí)也是let/const的特點(diǎn),ES6規(guī)定它們不屬于頂層全局變量的屬性而账,這里用chrome調(diào)試一下
可以看到使用let聲明的變量x是在一個(gè)叫script作用域下的胰坟,而var聲明的變量因?yàn)樽兞刻嵘蕴嵘搅巳肿兞縲indow對(duì)象中,這使我們能放心的使用新語(yǔ)法泞辐,不用擔(dān)心污染全局的window對(duì)象
建議
在日常開發(fā)中笔横,我的建議是全面擁抱let/const,一般的變量聲明使用let關(guān)鍵字咐吼,而當(dāng)聲明一些配置項(xiàng)(類似接口地址吹缔,npm依賴包,分頁(yè)器默認(rèn)頁(yè)數(shù)等一些一旦聲明后就不會(huì)改變的變量)的時(shí)候可以使用const锯茄,來(lái)顯式的告訴項(xiàng)目其他開發(fā)者厢塘,這個(gè)變量是不能改變的(const聲明的常量建議使用全大寫字母標(biāo)識(shí),單詞間用下劃線),同時(shí)也建議了解var關(guān)鍵字的缺陷(變量提升肌幽,污染全局變量等)晚碾,這樣才能更好的使用新語(yǔ)法
箭頭函數(shù)(常用)
ES6 允許使用箭頭(=>)定義函數(shù)
箭頭函數(shù)對(duì)于使用function關(guān)鍵字創(chuàng)建的函數(shù)有以下區(qū)別
箭頭函數(shù)沒有arguments(建議使用更好的語(yǔ)法,剩余運(yùn)算符替代)
箭頭函數(shù)沒有prototype屬性喂急,沒有constructor迄薄,即不能用作與構(gòu)造函數(shù)(不能用new關(guān)鍵字調(diào)用)
箭頭函數(shù)沒有自己this,它的this是詞法的煮岁,引用的是上下文的this讥蔽,即在你寫這行代碼的時(shí)候就箭頭函數(shù)的this就已經(jīng)和外層執(zhí)行上下文的this綁定了(這里個(gè)人認(rèn)為并不代表完全是靜態(tài)的,因?yàn)橥鈱拥纳舷挛娜允莿?dòng)態(tài)的可以使用call,apply,bind修改,這里只是說(shuō)明了箭頭函數(shù)的this始終等于它上層上下文中的this)
因?yàn)閟etTimeout會(huì)將一個(gè)匿名的回調(diào)函數(shù)推入異步隊(duì)列涣易,而回調(diào)函數(shù)是具有全局性的,即在非嚴(yán)格模式下this會(huì)指向window冶伞,就會(huì)存在丟失變量a的問(wèn)題新症,而如果使用箭頭函數(shù),在書寫的時(shí)候就已經(jīng)確定它的this等于它的上下文(這里是makeRequest的函數(shù)執(zhí)行上下文响禽,相當(dāng)于講箭頭函數(shù)中的this綁定了makeRequest函數(shù)執(zhí)行上下文中的this)徒爹,所以this就指向了makeRequest中的a變量
箭頭函數(shù)中的this即使使用call,apply,bind也無(wú)法改變指向(這里也驗(yàn)證了為什么ECMAScript規(guī)定不能使用箭頭函數(shù)作為構(gòu)造函數(shù),因?yàn)樗膖his已經(jīng)確定好了無(wú)法改變)
建議
箭頭函數(shù)替代了以前需要顯式的聲明一個(gè)變量保存this的操作芋类,使得代碼更加的簡(jiǎn)潔
ES5寫法不推薦:
ES6箭頭函數(shù):
值得注意的是makeRequest后面的function不能使用箭頭函數(shù)隆嗅,因?yàn)檫@樣它就會(huì)再使用上層的this,而再上層是全局的執(zhí)行上下文侯繁,它的this的值會(huì)指向window
setTimeout第一個(gè)參數(shù)使用了箭頭函數(shù),它會(huì)引用上下文的this,而它的外層也是一個(gè)箭頭函數(shù),又會(huì)引用再上層的this,最上層就是整個(gè)全局上下文,即this的值為window對(duì)象,所以沒有變量a
在數(shù)組的迭代中使用箭頭函數(shù)更加簡(jiǎn)潔胖喳,并且省略了return關(guān)鍵字
不要在可能改變this指向的函數(shù)中使用箭頭函數(shù),類似Vue中的methods,computed中的方法,生命周期函數(shù)贮竟,Vue將這些函數(shù)的this綁定了當(dāng)前組件的vm實(shí)例丽焊,如果使用箭頭函數(shù)會(huì)強(qiáng)行改變this,因?yàn)榧^函數(shù)優(yōu)先級(jí)最高(無(wú)法再使用call,apply,bind改變指向)
在把箭頭函數(shù)作為日常開發(fā)的語(yǔ)法之前,個(gè)人建議是去了解一下箭頭函數(shù)的是如何綁定this的,而不只是當(dāng)做省略function這幾個(gè)單詞拼寫,畢竟那才是ECMAScript真正希望解決的問(wèn)題
iterator迭代器
iterator迭代器是ES6非常重要的概念咕别,但是很多人對(duì)它了解的不多技健,但是它卻是另外4個(gè)ES6常用特性的實(shí)現(xiàn)基礎(chǔ)(解構(gòu)賦值,剩余/擴(kuò)展運(yùn)算符惰拱,生成器雌贱,for of循環(huán)),了解迭代器的概念有助于了解另外4個(gè)核心語(yǔ)法的原理偿短,另外ES6新增的Map,Set數(shù)據(jù)結(jié)構(gòu)也有使用到它帽芽,所以我放到前面來(lái)講
對(duì)于可迭代的數(shù)據(jù)解構(gòu),ES6在內(nèi)部部署了一個(gè)[Symbol.iterator]屬性翔冀,它是一個(gè)函數(shù)导街,執(zhí)行后會(huì)返回iterator對(duì)象(也叫迭代器對(duì)象,也叫iterator接口)纤子,擁有[Symbol.iterator]屬性的對(duì)象即被視為可迭代的
數(shù)組中的Symbol.iterator方法默認(rèn)部署在數(shù)組原型上:
默認(rèn)具有iterator接口的數(shù)據(jù)結(jié)構(gòu)有以下幾個(gè)搬瑰,注意普通對(duì)象默認(rèn)是沒有iterator接口的(可以自己創(chuàng)建iterator接口讓普通對(duì)象也可以迭代)
Array
Map
Set
String
TypedArray(類數(shù)組)
函數(shù)的 arguments 對(duì)象
NodeList 對(duì)象
iterator迭代器是一個(gè)對(duì)象,它具有一個(gè)next方法所以可以這么調(diào)用
next方法返回又會(huì)返回一個(gè)對(duì)象控硼,有value和done兩個(gè)屬性泽论,value即每次迭代之后返回的值,而done表示是否還需要再次循環(huán)卡乾,可以看到當(dāng)value為undefined時(shí)翼悴,done為true表示循環(huán)終止
梳理一下
可迭代的數(shù)據(jù)結(jié)構(gòu)會(huì)有一個(gè)[Symbol.iterator]方法
[Symbol.iterator]執(zhí)行后返回一個(gè)iterator對(duì)象
iterator對(duì)象有一個(gè)next方法
next方法執(zhí)行后返回一個(gè)有value,done屬性的對(duì)象
這里簡(jiǎn)要概述了以下iterator的概念,有興趣可以去看阮一峰老師的《ECMAScript 6 入門》
解構(gòu)賦值(常用)
解構(gòu)賦值可以直接使用對(duì)象的某個(gè)屬性幔妨,而不需要通過(guò)屬性訪問(wèn)的形式使用鹦赎,對(duì)象解構(gòu)原理個(gè)人認(rèn)為是通過(guò)尋找相同的屬性名谍椅,然后原對(duì)象的這個(gè)屬性名的值賦值給新對(duì)象對(duì)應(yīng)的屬性
這里左邊真正聲明的其實(shí)是titleOne,titleTwo這兩個(gè)變量,然后會(huì)根據(jù)左邊這2個(gè)變量的位置尋找右邊對(duì)象中title和test[0]中的title對(duì)應(yīng)的值古话,找到字符串a(chǎn)bc和test賦值給titleOne,titleTwo(如果沒有找到會(huì)返回undefined)
數(shù)組解構(gòu)的原理其實(shí)是消耗數(shù)組的迭代器雏吭,把生成對(duì)象的value屬性的值賦值給對(duì)應(yīng)的變量
數(shù)組解構(gòu)的一個(gè)用途是交換變量,避免以前要聲明一個(gè)臨時(shí)變量值存儲(chǔ)值
ES6交換變量:
建議
同樣建議使用陪踩,因?yàn)榻鈽?gòu)賦值語(yǔ)意化更強(qiáng)杖们,對(duì)于作為對(duì)象的函數(shù)參數(shù)來(lái)說(shuō),可以減少形參的聲明肩狂,直接使用對(duì)象的屬性(如果嵌套層數(shù)過(guò)多我個(gè)人認(rèn)為不適合用對(duì)象解構(gòu)摘完,不太優(yōu)雅)
一個(gè)常用的例子是Vuex中actions中的方法會(huì)傳入2個(gè)參數(shù),第一個(gè)參數(shù)是個(gè)對(duì)象傻谁,你可以隨意命名孝治,然后使用<名字>.commit的方法調(diào)用commit函數(shù),或者使用對(duì)象解構(gòu)直接使用commit
不使用對(duì)象解構(gòu):
使用對(duì)象解構(gòu):
另外可以給使用axios的響應(yīng)結(jié)果進(jìn)行解構(gòu)(axios默認(rèn)會(huì)把真正的響應(yīng)結(jié)果放在data屬性中)
剩余/擴(kuò)展運(yùn)算符(常用)
剩余/擴(kuò)展運(yùn)算符同樣也是ES6一個(gè)非常重要的語(yǔ)法栅螟,使用3個(gè)點(diǎn)(...)荆秦,后面跟著一個(gè)數(shù)組篱竭,它使得可以"展開"這個(gè)數(shù)組力图,可以這么理解,數(shù)組是存放元素集合的一個(gè)容器掺逼,而使用剩余/擴(kuò)展運(yùn)算符可以將這個(gè)容器拆開吃媒,這樣就只剩下元素集合,你可以把這些元素集合放到另外一個(gè)數(shù)組里面
擴(kuò)展運(yùn)算符
只要含有iterator接口的數(shù)據(jù)結(jié)構(gòu)都可以使用擴(kuò)展運(yùn)算符
擴(kuò)展運(yùn)算符可以和數(shù)組的解構(gòu)賦值一起使用吕喘,但是必須放在最后一個(gè)赘那,因?yàn)槭S?擴(kuò)展運(yùn)算符的原理其實(shí)是利用了數(shù)組的迭代器,它會(huì)消耗3個(gè)點(diǎn)后面的數(shù)組的所有迭代器氯质,讀取所有迭代器的value屬性募舟,剩余/擴(kuò)展運(yùn)算符后不能在有解構(gòu)賦值,因?yàn)槭S?擴(kuò)展運(yùn)算符已經(jīng)消耗了所有迭代器闻察,而數(shù)組的解構(gòu)賦值也是消耗迭代器拱礁,但是這個(gè)時(shí)候已經(jīng)沒有迭代器了,所以會(huì)報(bào)錯(cuò)
這里first會(huì)消耗右邊數(shù)組的一個(gè)迭代器辕漂,...arr會(huì)消耗剩余所有的迭代器呢灶,而第二個(gè)例子...arr直接消耗了所有迭代器,導(dǎo)致last沒有迭代器可供消耗了钉嘹,所以會(huì)報(bào)錯(cuò)鸯乃,因?yàn)檫@是毫無(wú)意義的操作
剩余運(yùn)算符
剩余運(yùn)算符最重要的一個(gè)特點(diǎn)就是替代了以前的arguments
訪問(wèn)函數(shù)的arguments對(duì)象是一個(gè)很昂貴的操作,以前的arguments.callee也被廢止了跋涣,建議在支持ES6語(yǔ)法的環(huán)境下不要在使用arguments缨睡,使用剩余運(yùn)算符替代(箭頭函數(shù)沒有arguments鸟悴,必須使用剩余運(yùn)算符才能訪問(wèn)參數(shù)集合)
剩余運(yùn)算符和擴(kuò)展運(yùn)算符的區(qū)別就是,剩余運(yùn)算符會(huì)收集這些集合宏蛉,放到右邊的數(shù)組中遣臼,擴(kuò)展運(yùn)算符是將右邊的數(shù)組拆分成元素的集合,它們是相反的
在對(duì)象中使用擴(kuò)展運(yùn)算符
這個(gè)是ES9的語(yǔ)法拾并,ES9中支持在對(duì)象中使用擴(kuò)展運(yùn)算符揍堰,之前說(shuō)過(guò)數(shù)組的擴(kuò)展運(yùn)算符原理是消耗所有迭代器,但對(duì)象中并沒有迭代器嗅义,我個(gè)人認(rèn)為可能是實(shí)現(xiàn)原理不同屏歹,但是仍可以理解為將鍵值對(duì)從對(duì)象中拆開,它可以放到另外一個(gè)普通對(duì)象中
其實(shí)它和另外一個(gè)ES6新增的API相似之碗,即Object.assign蝙眶,它們都可以合并對(duì)象,但是還是有一些不同Object.assign會(huì)觸發(fā)目標(biāo)對(duì)象的setter函數(shù)褪那,而對(duì)象擴(kuò)展運(yùn)算符不會(huì)幽纷,這個(gè)我們放到后面討論
建議
使用擴(kuò)展運(yùn)算符可以快速的將類數(shù)組轉(zhuǎn)為一個(gè)真正的數(shù)組
合并多個(gè)數(shù)組
函數(shù)柯里化
對(duì)象屬性/方法簡(jiǎn)寫(常用)
對(duì)象屬性簡(jiǎn)寫
es6允許當(dāng)對(duì)象的屬性和值相同時(shí),省略屬性名
需要注意的是
對(duì)象屬性簡(jiǎn)寫經(jīng)常與解構(gòu)賦值一起使用
結(jié)合上文的解構(gòu)賦值博敬,這里的代碼會(huì)其實(shí)是聲明了x,y,z變量友浸,因?yàn)閎ar函數(shù)會(huì)返回一個(gè)對(duì)象,這個(gè)對(duì)象有x,y,z這3個(gè)屬性偏窝,解構(gòu)賦值會(huì)尋找等號(hào)右邊表達(dá)式的x,y,z屬性收恢,找到后賦值給聲明的x,y,z變量
方法簡(jiǎn)寫
es6允許當(dāng)一個(gè)對(duì)象的屬性的值是一個(gè)函數(shù)(即是一個(gè)方法),可以使用簡(jiǎn)寫的形式
在Vue中因?yàn)槎际窃趘m對(duì)象中書寫方法祭往,完全可以使用方法簡(jiǎn)寫的方式書寫函數(shù)
for ... of循環(huán)
for ... of是作為ES6新增的遍歷方式,允許遍歷一個(gè)含有iterator接口的數(shù)據(jù)結(jié)構(gòu)并且返回各項(xiàng)的值,和ES3中的for ... in的區(qū)別如下
for ... of遍歷獲取的是對(duì)象的鍵值,for ... in 獲取的是對(duì)象的鍵名
for ... in會(huì)遍歷對(duì)象的整個(gè)原型鏈,性能非常差不推薦使用,而for ... of只遍歷當(dāng)前對(duì)象不會(huì)遍歷原型鏈
對(duì)于數(shù)組的遍歷,for ... in會(huì)返回?cái)?shù)組中所有可枚舉的屬性(包括原型鏈),for ... of只返回?cái)?shù)組的下標(biāo)對(duì)于的屬性值
for ... of循環(huán)的原理其實(shí)也是利用了遍歷對(duì)象內(nèi)部的iterator接口,將for ... of循環(huán)分解成最原始的for循環(huán),內(nèi)部實(shí)現(xiàn)的機(jī)制可以這么理解
可以看到只要滿足第二個(gè)條件(iterator.next()存在且res.done為true)就可以一直循環(huán)下去,并且每次把迭代器的next方法生成的對(duì)象賦值給res,然后將res的value屬性賦值給for ... of第一個(gè)條件中聲明的變量即可,res的done屬性控制是否繼續(xù)遍歷下去
for... of循環(huán)同時(shí)支持break,continue,return(在函數(shù)中調(diào)用的話)并且可以和對(duì)象解構(gòu)賦值一起使用
arr數(shù)組每次使用for ... of循環(huán)都返回一對(duì)象({a:1},{a:2},{a:3}),然后會(huì)經(jīng)過(guò)對(duì)象解構(gòu),尋找屬性為a的值,賦值給obj.a,所以在每輪循環(huán)的時(shí)候obj.a會(huì)分別賦值為1,2,3
Promise(常用)
Promise作為ES6中推出的新的概念伦意,改變了JS的異步編程,現(xiàn)代前端大部分的異步請(qǐng)求都是使用Promise實(shí)現(xiàn)硼补,fetch這個(gè)web api也是基于Promise的驮肉,這里不得簡(jiǎn)述一下之前統(tǒng)治JS異步編程的回調(diào)函數(shù),回調(diào)函數(shù)有什么缺點(diǎn)已骇,Promise又是怎么改善這些缺點(diǎn)
回調(diào)函數(shù)
眾所周知离钝,JS是單線程的,因?yàn)槎鄠€(gè)線程改變DOM的話會(huì)導(dǎo)致頁(yè)面紊亂疾捍,所以設(shè)計(jì)為一個(gè)單線程的語(yǔ)言奈辰,但是瀏覽器是多線程的,這使得JS同時(shí)具有異步的操作乱豆,即定時(shí)器奖恰,請(qǐng)求,事件監(jiān)聽等,而這個(gè)時(shí)候就需要一套事件的處理機(jī)制去決定這些事件的順序瑟啃,即Event Loop(事件循環(huán))论泛,這里不會(huì)詳細(xì)講解事件循環(huán),只需要知道蛹屿,前端發(fā)出的請(qǐng)求屁奏,一般都是會(huì)進(jìn)入瀏覽器的http請(qǐng)求線程,等到收到響應(yīng)的時(shí)候會(huì)通過(guò)回調(diào)函數(shù)推入異步隊(duì)列错负,等處理完主線程的任務(wù)會(huì)讀取異步隊(duì)列中任務(wù)坟瓢,執(zhí)行回調(diào)
在《你不知道的JavaScript》下卷中,這么介紹
使用回調(diào)函數(shù)處理異步請(qǐng)求相當(dāng)于把你的回調(diào)函數(shù)置于了一個(gè)黑盒犹撒,使用第三方的請(qǐng)求庫(kù)你可能會(huì)這么寫
收到響應(yīng)后折联,執(zhí)行后面的回調(diào)打印字符串,但是如果這個(gè)第三方庫(kù)有類似超時(shí)重試的功能识颊,可能會(huì)執(zhí)行多次你的回調(diào)函數(shù)诚镰,如果是一個(gè)支付功能,你就會(huì)發(fā)現(xiàn)你扣的錢可能就不止1000元了-.-
另外一個(gè)眾所周知的問(wèn)題就是祥款,在回調(diào)函數(shù)中再嵌套回調(diào)函數(shù)會(huì)導(dǎo)致代碼非常難以維護(hù)清笨,這是人們常說(shuō)的“回調(diào)地獄”
你使用的第三方ajax庫(kù)還有可能并沒有提供一些錯(cuò)誤的回調(diào)骏掀,請(qǐng)求失敗的一些錯(cuò)誤信息可能會(huì)被吞掉队伟,而你確完全不知情
總結(jié)一下回調(diào)函數(shù)的一些缺點(diǎn)
多重嵌套,導(dǎo)致回調(diào)地獄
代碼跳躍再愈,并非人類習(xí)慣的思維模式
信任問(wèn)題奠伪,你不能把你的回調(diào)完全寄托與第三方庫(kù)跌帐,因?yàn)槟悴恢赖谌綆?kù)到底會(huì)怎么執(zhí)行回調(diào)(多次執(zhí)行)
第三方庫(kù)可能沒有提供錯(cuò)誤處理
不清楚回調(diào)是否都是異步調(diào)用的(可以同步調(diào)用ajax首懈,在收到響應(yīng)前會(huì)阻塞整個(gè)線程绊率,會(huì)陷入假死狀態(tài),非常不推薦)
<pre class="" style="margin: 0px 0px 15px; padding: 15px 5px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; background-color: rgb(246, 248, 250); font-size: 13px; line-height: 1.5; overflow: auto; border-radius: 3px; font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; color: rgb(0, 0, 0); text-align: start;">
xhr.open("GET","/try/ajax/ajax_info.txt",false); //通過(guò)設(shè)置第三個(gè)async為false可以同步調(diào)用ajax
</pre>
Promise
針對(duì)回調(diào)函數(shù)這么多缺點(diǎn)究履,ES6中引入了一個(gè)新的概念滤否,Promise,Promise是一個(gè)構(gòu)造函數(shù)最仑,通過(guò)new關(guān)鍵字創(chuàng)建一個(gè)Promise的實(shí)例藐俺,來(lái)看看Promise是怎么解決回調(diào)函數(shù)的這些問(wèn)題
Promise并不是回調(diào)函數(shù)的衍生版本,而是2個(gè)概念泥彤,所以需要將之前的回調(diào)函數(shù)改為支持Promise的版本欲芹,這個(gè)過(guò)程成為"提升",或者"promisory"吟吝,現(xiàn)代MVVM框架常用的第三方請(qǐng)求庫(kù)axios就是一個(gè)典型的例子菱父,另外nodejs中也有bluebird,Q等
- 多重嵌套,導(dǎo)致回調(diào)地獄
Promise在設(shè)計(jì)的時(shí)候引入了鏈?zhǔn)秸{(diào)用的概念浙宜,每個(gè)then方法同樣也是一個(gè)Promise官辽,因此可以無(wú)限鏈?zhǔn)秸{(diào)用下去
配合箭頭函數(shù),明顯的比之前回調(diào)函數(shù)的多層嵌套優(yōu)雅很多
- 代碼跳躍粟瞬,并非人類習(xí)慣的思維模式
Promise使得能夠同步思維書寫代碼同仆,上述的代碼就是先請(qǐng)求3000端口,得到響應(yīng)后再請(qǐng)求3001裙品,再請(qǐng)求3002俗批,再請(qǐng)求3003,而書寫的格式也是符合人類的思維市怎,從先到后
- 信任問(wèn)題扶镀,你不能把你的回調(diào)完全寄托與第三方庫(kù),因?yàn)槟悴恢赖谌綆?kù)到底會(huì)怎么執(zhí)行回調(diào)(多次執(zhí)行)
Promise本身是一個(gè)狀態(tài)機(jī)焰轻,具有pending(等待)臭觉,resolve(決議),reject(拒絕)這3個(gè)狀態(tài)辱志,當(dāng)請(qǐng)求發(fā)送沒有得到響應(yīng)的時(shí)候會(huì)pending狀態(tài)蝠筑,并且一個(gè)Promise實(shí)例的狀態(tài)只能從pending => resolve 或者從 pending => reject,即當(dāng)一個(gè)Promise實(shí)例從pending狀態(tài)改變后揩懒,就不會(huì)再改變了(不存在resolve => reject 或 reject => resolve)
而Promise實(shí)例必須主動(dòng)調(diào)用then方法什乙,才能將值從Promise實(shí)例中取出來(lái)(前提是Promise不是pending狀態(tài)),這一個(gè)“主動(dòng)”的操作就是解決這個(gè)問(wèn)題的關(guān)鍵已球,即第三方庫(kù)做的只是把改變Promise的狀態(tài)臣镣,而響應(yīng)的值怎么處理,這是開發(fā)者主動(dòng)控制的智亮,這里就實(shí)現(xiàn)了控制反轉(zhuǎn)忆某,將原來(lái)第三方庫(kù)的控制權(quán)轉(zhuǎn)移到了開發(fā)者上
- 第三方庫(kù)可能沒有提供錯(cuò)誤處理
Promise的then方法會(huì)接受2個(gè)函數(shù),第一個(gè)函數(shù)是這個(gè)Promise實(shí)例被resolve時(shí)執(zhí)行的回調(diào)阔蛉,第二個(gè)函數(shù)是這個(gè)Promise實(shí)例被reject時(shí)執(zhí)行的回調(diào)弃舒,而這個(gè)也是開發(fā)者主動(dòng)調(diào)用的
使用Promise在異步請(qǐng)求發(fā)送錯(cuò)誤的時(shí)候,即使沒有捕獲錯(cuò)誤状原,也不會(huì)阻塞主線程的代碼
- 不清楚回調(diào)是否都是異步調(diào)用的
Promise在設(shè)計(jì)的時(shí)候保證所有響應(yīng)的處理回調(diào)都是異步調(diào)用的聋呢,不會(huì)阻塞代碼的執(zhí)行,Promise將then方法的回調(diào)放入一個(gè)叫微任務(wù)的隊(duì)列中(MicroTask)颠区,保證這些回調(diào)任務(wù)都在同步任務(wù)執(zhí)行完再執(zhí)行削锰,這部分同樣也是事件循環(huán)的知識(shí)點(diǎn),有興趣的朋友可以深入研究一下
建議
在日常開發(fā)中毕莱,建議全面擁抱新的Promise語(yǔ)法器贩,其實(shí)現(xiàn)在的異步編程基本也都使用的是Promise
建議使用ES7的async/await進(jìn)一步的優(yōu)化Promise的寫法测暗,async函數(shù)始終返回一個(gè)Promise,await可以實(shí)現(xiàn)一個(gè)"等待"的功能磨澡,async/await被成為異步編程的終極解決方案碗啄,即用同步的形式書寫異步代碼,并且能夠更優(yōu)雅的實(shí)現(xiàn)異步代碼順序執(zhí)行稳摄,詳情可以看阮老師的ES6標(biāo)準(zhǔn)入門
關(guān)于Promise還有很多很多需要講的稚字,包括它的靜態(tài)方法all,race厦酬,resolve胆描,reject,Promise的執(zhí)行順序仗阅,Promise嵌套Promise昌讲,thenable對(duì)象的處理等,礙于篇幅這里只介紹了一下為什么需要使用Promise减噪。但很多開發(fā)者在日常使用中只是了解這些API短绸,卻不知道Promise內(nèi)部具體是怎么實(shí)現(xiàn)的,遇到復(fù)雜的異步代碼就無(wú)從下手筹裕,非常建議去了解一下Promise A+的規(guī)范醋闭,自己實(shí)現(xiàn)一個(gè)Promise
ES6 Module(常用)
在ES6 Module出現(xiàn)之前,模塊化一直是前端開發(fā)者討論的重點(diǎn)朝卒,面對(duì)日益增長(zhǎng)的需求和代碼证逻,需要一種方案來(lái)將臃腫的代碼拆分成一個(gè)個(gè)小模塊,從而推出了AMD,CMD和CommonJs這3種模塊化方案抗斤,前者用在瀏覽器端囚企,后面2種用在服務(wù)端,直到ES6 Module出現(xiàn)
ES6 Module默認(rèn)目前還沒有被瀏覽器支持瑞眼,需要使用babel龙宏,在日常寫demo的時(shí)候經(jīng)常會(huì)顯示這個(gè)錯(cuò)誤
可以在script標(biāo)簽中使用tpye="module"在同域的情況下可以解決(非同域情況會(huì)被同源策略攔截,webstorm會(huì)開啟一個(gè)同域的服務(wù)器沒有這個(gè)問(wèn)題负拟,vscode貌似不行)
ES6 Module使用import關(guān)鍵字導(dǎo)入模塊烦衣,export關(guān)鍵字導(dǎo)出模塊歹河,它還有以下特點(diǎn)
ES6 Module是靜態(tài)的掩浙,也就是說(shuō)它是在編譯階段運(yùn)行,和var以及function一樣具有提升效果(這個(gè)特點(diǎn)使得它支持tree shaking)
自動(dòng)采用嚴(yán)格模式(頂層的this返回undefined)
ES6 Module支持使用export {<變量>}導(dǎo)出具名的接口秸歧,或者export default導(dǎo)出匿名的接口
module.js導(dǎo)出:
a.js導(dǎo)入:
這兩者的區(qū)別是厨姚,export {<變量>}導(dǎo)出的是一個(gè)變量的引用,export default導(dǎo)出的是一個(gè)值
什么意思呢键菱,就是說(shuō)在a.js中使用import導(dǎo)入這2個(gè)變量的后谬墙,在module.js中因?yàn)槟承┰騲變量被改變了今布,那么會(huì)立刻反映到a.js,而module.js中的y變量改變后拭抬,a.js中的y還是原來(lái)的值
module.js:
a.js:
可以看到給module.js設(shè)置了一個(gè)一秒后改變x,y變量的定時(shí)器,在一秒后同時(shí)觀察導(dǎo)入時(shí)候變量的值,可以發(fā)現(xiàn)x被改變了,但y的值仍是20,因?yàn)閥是通過(guò)export default導(dǎo)出的,在導(dǎo)入的時(shí)候的值相當(dāng)于只是導(dǎo)入數(shù)字20,而x是通過(guò)export {<變量>}導(dǎo)出的,它導(dǎo)出的是一個(gè)變量的引用,即a.js導(dǎo)入的是當(dāng)前x的值,只關(guān)心當(dāng)前x變量的值是什么,可以理解為一個(gè)"活鏈接"
export default這種導(dǎo)出的語(yǔ)法其實(shí)只是指定了一個(gè)命名導(dǎo)出,而它的名字叫default,換句話說(shuō),將模塊的導(dǎo)出的名字重命名為default,也可以使用import <變量> from <路徑> 這種語(yǔ)法導(dǎo)入
module.js導(dǎo)出:
a.js導(dǎo)入:
但是由于是使用export {<變量>}這種形式導(dǎo)出的模塊,即使被重命名為default,仍然導(dǎo)出的是一個(gè)變量的引用
這里再來(lái)說(shuō)一下目前為止主流的模塊化方案ES6 Module和CommonJs的一些區(qū)別
CommonJs輸出的是一個(gè)值的拷貝,ES6 Module通過(guò)export {<變量>}輸出的是一個(gè)變量的引用,export default輸出的是一個(gè)值的拷貝
CommonJs運(yùn)行在服務(wù)器上,被設(shè)計(jì)為運(yùn)行時(shí)加載,即代碼執(zhí)行到那一行才回去加載模塊,而ES6 Module是靜態(tài)的輸出一個(gè)接口,發(fā)生在編譯的階段
CommonJs在第一次加載的時(shí)候運(yùn)行一次,之后加載返回的都是第一次的結(jié)果,具有緩存的效果,ES6 Module則沒有
import( )
關(guān)于ES6 Module靜態(tài)編譯的特點(diǎn),導(dǎo)致了無(wú)法動(dòng)態(tài)加載,但是總是會(huì)有一些需要?jiǎng)討B(tài)加載模塊的需求,所以現(xiàn)在有一個(gè)提案,使用把import作為一個(gè)函數(shù)可以實(shí)現(xiàn)動(dòng)態(tài)加載模塊,它返回一個(gè)Promise,Promise被resolve時(shí)的值為輸出的模塊
使用import方法改寫上面的a.js使得它可以動(dòng)態(tài)加載(使用靜態(tài)編譯的ES6 Module放在條件語(yǔ)句會(huì)報(bào)錯(cuò),因?yàn)闀?huì)有提升的效果,并且也是不允許的),可以看到輸出了module.js的一個(gè)變量x和一個(gè)默認(rèn)輸出
Vue中路由的懶加載的ES6寫法就是使用了這個(gè)技術(shù),使得在路由切換的時(shí)候能夠動(dòng)態(tài)的加載組件渲染視圖
函數(shù)默認(rèn)值
ES6允許在函數(shù)的參數(shù)中設(shè)置默認(rèn)值
ES5寫法:
ES6寫法:
相比ES5,ES6函數(shù)默認(rèn)值直接寫在參數(shù)上,更加的直觀
如果使用了函數(shù)默認(rèn)參數(shù),在函數(shù)的參數(shù)的區(qū)域(括號(hào)里面),它會(huì)作為一個(gè)單獨(dú)的作用域,并且擁有l(wèi)et/const方法的一些特性,比如暫時(shí)性死區(qū),塊級(jí)作用域,沒有變量提升等,而這個(gè)作用域在函數(shù)內(nèi)部代碼執(zhí)行前
這里當(dāng)運(yùn)行func的時(shí)候,因?yàn)闆]有傳參數(shù),使用函數(shù)默認(rèn)參數(shù),y就會(huì)去尋找x的值,在沿著詞法作用域在外層找到了值為1的變量x
再來(lái)看一個(gè)例子
這里同樣沒有傳參數(shù),使用函數(shù)的默認(rèn)賦值,x通過(guò)詞法作用域找到了變量w,所以x默認(rèn)值為2,y同樣通過(guò)詞法作用域找到了剛剛定義的x變量,y的默認(rèn)值為3,但是在解析到z = z + 1這一行的時(shí)候,JS解釋器先會(huì)去解析z+1找到相應(yīng)的值后再賦給變量z,但是因?yàn)闀簳r(shí)性死區(qū)的原因(let/const"劫持"了這個(gè)塊級(jí)作用域,無(wú)法在聲明之前使用這個(gè)變量,上文有解釋),導(dǎo)致在let聲明之前就使用了變量z,所以會(huì)報(bào)錯(cuò)
這樣理解函數(shù)的默認(rèn)值會(huì)相對(duì)容易一些
當(dāng)傳入的參數(shù)為undefined時(shí)才使用函數(shù)的默認(rèn)值(顯式傳入undefined也會(huì)觸發(fā)使用函數(shù)默認(rèn)值,傳入null則不會(huì)觸發(fā))
在舉個(gè)例子:
這里借用阮一峰老師書中的一個(gè)例子,func的默認(rèn)值為一個(gè)函數(shù),執(zhí)行后返回foo變量,而在函數(shù)內(nèi)部執(zhí)行的時(shí)候,相當(dāng)于對(duì)foo變量的一次變量查詢(LHS查詢),而查詢的起點(diǎn)是在這個(gè)單獨(dú)的作用域中,即JS解釋器不會(huì)去查詢?nèi)ズ瘮?shù)內(nèi)部查詢變量foo,而是沿著詞法作用域先查看同一作用域(前面的函數(shù)參數(shù))中有沒有foo變量,再往函數(shù)的外部尋找foo變量,最終找不到所以報(bào)錯(cuò)了,這個(gè)也是函數(shù)默認(rèn)值的一個(gè)特點(diǎn)
函數(shù)默認(rèn)值配合解構(gòu)賦值
第一行給func函數(shù)傳入了2個(gè)空對(duì)象,所以函數(shù)的第一第二個(gè)參數(shù)都不會(huì)使用函數(shù)默認(rèn)值,然后函數(shù)的第一個(gè)參數(shù)會(huì)嘗試解構(gòu)對(duì)象,提取變量x,因?yàn)榈谝粋€(gè)參數(shù)傳入了一個(gè)空對(duì)象,所以解構(gòu)不出變量x,但是這里又在內(nèi)層設(shè)置了一個(gè)默認(rèn)值,所以x的值為10,而第二個(gè)參數(shù)同樣傳了一個(gè)空對(duì)象,不會(huì)使用函數(shù)默認(rèn)值,然后會(huì)嘗試解構(gòu)出變量y,發(fā)現(xiàn)空對(duì)象中也沒有變量y,但是y沒有設(shè)置默認(rèn)值所以解構(gòu)后y的值為undefined
第二行第一個(gè)參數(shù)顯式的傳入了一個(gè)undefined,所以會(huì)使用函數(shù)默認(rèn)值為一個(gè)空對(duì)象,隨后和第一行一樣嘗試解構(gòu)x發(fā)現(xiàn)x為undefined,但是設(shè)置了默認(rèn)值所以x的值為10,而y和上文一樣為undefined
第三行2個(gè)參數(shù)都會(huì)undefined,第一個(gè)參數(shù)和上文一樣,第二個(gè)參數(shù)會(huì)調(diào)用函數(shù)默認(rèn)值,賦值為{y:10},然后嘗試解構(gòu)出變量y,即y為10
第四行和第三行相同,一個(gè)是顯式傳入undefined,一個(gè)是隱式不傳參數(shù)
第五行直接使用傳入的參數(shù),不會(huì)使用函數(shù)默認(rèn)值,并且能夠順利的解構(gòu)出變量x,y
Proxy
Proxy作為一個(gè)"攔截器",可以在目標(biāo)對(duì)象前架設(shè)一個(gè)攔截器,他人訪問(wèn)對(duì)象,必須先經(jīng)過(guò)這層攔截器,Proxy同樣是一個(gè)構(gòu)造函數(shù),使用new關(guān)鍵字生成一個(gè)攔截對(duì)象的實(shí)例,ES6提供了非常多對(duì)象攔截的操作,幾乎覆蓋了所有可能修改目標(biāo)對(duì)象的情況(Proxy一般和Reflect配套使用,前者攔截對(duì)象,后者返回?cái)r截的結(jié)果,Proxy上有的的攔截方法Reflect都有)
Object.definePropery
提到Proxy就不得不提一下ES5中的Object.defineProperty,這個(gè)api可以給一個(gè)對(duì)象添加屬性以及這個(gè)屬性的屬性描述符/訪問(wèn)器(這2個(gè)不能共存,同一屬性只能有其中一個(gè)),屬性描述符有configurable,writable,enumerable,value這4個(gè)屬性,分別代表是否可配置,是否只讀,是否可枚舉和屬性的值,訪問(wèn)器有configurable,enumerable,get,set,前2個(gè)和屬性描述符功能相同,后2個(gè)都是函數(shù),定義了get,set后對(duì)元素的讀寫操作都會(huì)執(zhí)行這個(gè)函數(shù),并且覆蓋默認(rèn)的讀寫行為
定義了obj中a屬性的表示為只讀,且不可枚舉,obj2定義了get,但沒有定義set表示只讀,并且讀取obj2的b屬性返回的值是get函數(shù)的返回值
ES5中的Object.defineProperty這和Proxy有什么關(guān)系呢?個(gè)人理解Proxy是Object.defineProperty的增強(qiáng)版,ES5只規(guī)定能夠定義屬性的屬性描述符或訪問(wèn)器.而Proxy增強(qiáng)到了13種,具體太多了我就不一一放出來(lái)了,這里我舉幾個(gè)比較有意思的例子
handler.apply
apply可以讓我們攔截一個(gè)函數(shù)(JS中函數(shù)也是對(duì)象,Proxy也可以攔截函數(shù))的執(zhí)行,我們可以把它用在函數(shù)節(jié)流中
調(diào)用攔截后的函數(shù):
handler.contruct
contruct可以攔截通過(guò)new關(guān)鍵字調(diào)用這個(gè)函數(shù)的操作,我們可以把它用在單例模式中
這里通過(guò)一個(gè)閉包保存了instance變量,每次使用new關(guān)鍵字調(diào)用被攔截的函數(shù)后都會(huì)查看這個(gè)instance變量,如果存在就返回閉包中保存的instance變量,否則就新建一個(gè)實(shí)例,這樣可以實(shí)現(xiàn)全局只有一個(gè)實(shí)例
handler.defineProperty
defineProperty可以攔截對(duì)這個(gè)對(duì)象的Object.defineProerty操作
注意對(duì)象內(nèi)部的默認(rèn)的[[SET]]函數(shù)(即對(duì)這個(gè)對(duì)象的屬性賦值)會(huì)間接觸發(fā)defineProperty和getOwnPropertyDescriptor這2個(gè)攔截方法
這里有幾個(gè)知識(shí)點(diǎn)
這里使用了遞歸的操作,當(dāng)需要訪問(wèn)對(duì)象的屬性時(shí)候,會(huì)判斷代理的對(duì)象屬性的值仍是一個(gè)可以代理的對(duì)象就遞歸的進(jìn)行代理,否則通過(guò)錯(cuò)誤捕獲執(zhí)行默認(rèn)的get函數(shù)
定義了defineProperty的攔截方法,當(dāng)對(duì)這個(gè)代理對(duì)象的某個(gè)屬性進(jìn)行賦值的時(shí)候會(huì)執(zhí)行對(duì)象內(nèi)部的[[SET]]函數(shù)進(jìn)行賦值,這個(gè)操作會(huì)間接觸發(fā)defineProperty這個(gè)方法,隨后會(huì)執(zhí)行定義的callback函數(shù)
這樣就實(shí)現(xiàn)了無(wú)論對(duì)象嵌套多少層,只要有屬性進(jìn)行賦值就會(huì)觸發(fā)get方法,對(duì)這層對(duì)象進(jìn)行代理,隨后觸發(fā)defineProperty執(zhí)行callback回調(diào)函數(shù)
其他的使用場(chǎng)景
Proxy另外還有很多功能,比如在實(shí)現(xiàn)驗(yàn)證器的時(shí)候,可以將業(yè)務(wù)邏輯和驗(yàn)證器分離達(dá)到解耦,通過(guò)defineProperty設(shè)置一些私有變量,攔截對(duì)象做日志記錄等
Vue
尤大預(yù)計(jì)2019年下半年發(fā)布Vue3.0,其中一個(gè)核心的功能就是使用Proxy替代Object.defineProperty
我相信了解過(guò)一點(diǎn)Vue響應(yīng)式原理的人都知道Vue框架在對(duì)象攔截上的一些不足
<pre class="" style="margin: 0px 0px 15px; padding: 15px 5px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; background-color: rgb(246, 248, 250); font-size: 13px; line-height: 1.5; overflow: auto; border-radius: 3px; font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; color: rgb(0, 0, 0); text-align: start;">
<template>
<div>
<div>{{arr}}</div>
<div>{{obj}}</div>
<button @click="handleClick">修改arr下標(biāo)</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>
</pre>
可以看到這里數(shù)據(jù)改變了,控制臺(tái)打印出了新的值,但是視圖沒有更新,這是因?yàn)閂ue內(nèi)部使用Object.defineProperty進(jìn)行的數(shù)據(jù)劫持,而這個(gè)API無(wú)法探測(cè)到對(duì)象根屬性的添加和刪除,以及直接給數(shù)組下標(biāo)進(jìn)行賦值,所以不會(huì)通知渲染watcher進(jìn)行視圖更新,而理論上這個(gè)API也無(wú)法探測(cè)到數(shù)組的一系列方法(push,splice,pop),但是Vue框架修改了數(shù)組的原型,使得在調(diào)用這些方法修改數(shù)據(jù)后會(huì)執(zhí)行視圖更新的操作
<pre class="" style="margin: 0px 0px 15px; padding: 15px 5px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; background-color: rgb(246, 248, 250); font-size: 13px; line-height: 1.5; overflow: auto; border-radius: 3px; font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; color: rgb(0, 0, 0); text-align: start;">
//源碼位置: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(); //這一行就會(huì)主動(dòng)調(diào)用notify方法,會(huì)通知到渲染watcher進(jìn)行視圖更新
return result
});
});
</pre>
在掘金翻譯的尤大Vue3.0計(jì)劃中寫到
3.0 將帶來(lái)一個(gè)基于 Proxy 的 observer 實(shí)現(xiàn)部默,它可以提供覆蓋語(yǔ)言 (JavaScript——譯注) 全范圍的響應(yīng)式能力,消除了當(dāng)前 Vue 2 系列中基于 Object.defineProperty 所存在的一些局限造虎,如: 對(duì)屬性的添加傅蹂、刪除動(dòng)作的監(jiān)測(cè) 對(duì)數(shù)組基于下標(biāo)的修改、對(duì)于 .length 修改的監(jiān)測(cè) 對(duì) Map算凿、Set份蝴、WeakMap 和 WeakSet 的支持
Proxy就沒有這個(gè)問(wèn)題,并且還提供了更多的攔截方法,完全可以替代Object.defineProperty,唯一不足的也就是瀏覽器的支持程度了(IE:誰(shuí)在說(shuō)我?)
所以要想深入了解Vue3.0實(shí)現(xiàn)機(jī)制,學(xué)會(huì)Proxy是必不可少的
Object.assign
這個(gè)ES6新增的Object靜態(tài)方法允許我們進(jìn)行多個(gè)對(duì)象的合并
可以這么理解,Object.assign遍歷需要合并給target的對(duì)象(即sourece對(duì)象的集合)的屬性,用等號(hào)進(jìn)行賦值,這里遍歷{a:1}將屬性a和值數(shù)字1賦值給target對(duì)象,然后再遍歷{b:2}將屬性b和值數(shù)字2賦值給target對(duì)象
這里羅列了一些這個(gè)API的需要注意的知識(shí)點(diǎn)
Object.assign是淺拷貝,對(duì)于值是引用類型的屬性拷貝扔的是它的引用
對(duì)于Symbol屬性同樣可以拷貝
不可枚舉的屬性無(wú)法拷貝
target必須是一個(gè)對(duì)象,如果傳入一個(gè)基本類型,會(huì)變成基本包裝類型,null/undefined沒有基本包裝類型,所以傳入會(huì)報(bào)錯(cuò)
source參數(shù)如果是不可枚舉的會(huì)忽略合并(字符串類型被認(rèn)為是可枚舉的,因?yàn)閮?nèi)部有iterator接口)
因?yàn)槭怯?strong>等號(hào)進(jìn)行賦值,如果被賦值的對(duì)象的屬性有setter函數(shù)會(huì)觸發(fā)setter函數(shù),同理如果有g(shù)etter函數(shù),也會(huì)調(diào)用賦值對(duì)象的屬性的getter(這就是為什么Object.assign無(wú)法合并對(duì)象屬性的訪問(wèn)器,因?yàn)樗鼤?huì)直接執(zhí)行對(duì)應(yīng)的getter/setter函數(shù)而不是合并它們,在ES7中可以使用Object.defineOwnPropertyDescriptors實(shí)現(xiàn)復(fù)制屬性訪問(wèn)器的操作)
這里為了加深了解我自己模擬了Object.assign的實(shí)現(xiàn),可供參考
和ES9的對(duì)象擴(kuò)展運(yùn)算符對(duì)比
ES9支持在對(duì)象上使用擴(kuò)展運(yùn)算符,實(shí)現(xiàn)的功能和Object.assign相似,唯一的區(qū)別就是在含有g(shù)etter/setter函數(shù)的對(duì)象有所區(qū)別
可以看到,ES9在合并2個(gè)對(duì)象的時(shí)候觸發(fā)了合并對(duì)象的getter,而ES6中觸發(fā)了target對(duì)象的setter而不會(huì)觸發(fā)getter,除此之外,Object.assgin和對(duì)象擴(kuò)展運(yùn)算符功能是相同的,兩者都可以使用,兩者都是淺拷貝,使用ES9的方法相對(duì)簡(jiǎn)潔一點(diǎn)
建議
- Vue中重置data中的數(shù)據(jù)
這個(gè)是我最常用的小技巧,使用Object.assign可以將你目前組件中的data對(duì)象和組件默認(rèn)初始化狀態(tài)的data對(duì)象中的數(shù)據(jù)合并,這樣可以達(dá)到初始化data對(duì)象的效果
在當(dāng)前組件的實(shí)例中options是當(dāng)前組件實(shí)例初始化時(shí)候的對(duì)象,其中有個(gè)data方法,即在組件中寫的data函數(shù),執(zhí)行后會(huì)返回一個(gè)初始化的data對(duì)象,然后將這個(gè)初始化的data對(duì)象合并到當(dāng)前的data來(lái)初始化所有數(shù)據(jù)
- 給對(duì)象合并需要的默認(rèn)屬性
可以封裝一個(gè)函數(shù),外層聲明一個(gè)DEFAULTS常量,options為每次傳入的動(dòng)態(tài)配置,這樣每次執(zhí)行后會(huì)合并一些默認(rèn)的配置項(xiàng)
- 在傳參的時(shí)候可以多個(gè)數(shù)據(jù)合并成一個(gè)對(duì)象傳給后端