你懂JavaScript 嗎?#29 語法(Syntax) | Summer。桑莫。夏天

文章推薦指數: 80 %
投票人數:10人

本文主要會談到ES6 新增的熱門語法,包含以區塊為範疇的宣告、分散與其餘運算、預設參數值、解構、物件字面值擴充功能、範本字面值、箭號函式。



Summer。

桑莫。

夏天 前端工程師,喜歡蒐集明信片、設計簡單的小物、旅遊和看電影。

這裡紀錄了我的學習和開發筆記,歡迎交流(*´∀`)~♥ 關於我、所有文章和標籤列表 關於我 所有文章 標籤列表 ©2022.Allrightsreserved. 你懂JavaScript嗎?#29語法(Syntax) 05Nov2018 You-Dont-Know-JS javascript 2019鐵人賽 你所不知道的JS 你懂JavaScript嗎? 鐵人賽 ES6 You-Dont-Know-JS-ES6-and-Beyond ReferenceError undefined operator運算子 本文主要會談到ES6新增的熱門語法,包含以區塊為範疇的宣告、分散與其餘運算、預設參數值、解構、物件字面值擴充功能、範本字面值、箭號函式。

以區塊為範疇的宣告(Block-ScopedDeclaration) 在ES6出現以前JavaScript是以函式為範疇宣告的單位,而在ES6之後,可用大括號{...}定義區塊範疇,讓let和const宣告以區塊為範疇的變數。

先總結let與const的特點,都是… 以區塊為範疇的宣告 沒有拉升(hoisting) temporaldeadzone,TDZ:宣告後才能使用 不允許重複宣告 兩者差異在於let可重新賦值,而const在宣告時就要給值,之後也不能更新其值。

let let+for 由於let可宣告以區塊為範疇的變數,因此在與for合用時,能對每次迭代都產生一個新的變數並將上一次迭代的結果作為這一次的初始值。

範例如下,每秒依序印出數字1,2,3,…,5,let會在每次迭代時重新宣告變數i,並將上一次迭代的結果作為這一次的初始值。

for(leti=1;i<=5;i++){ setTimeout(functiontimer(){ console.log(i); },i*1000); } 得到結果 1; 2; 3; 4; 5; 如果不用let而是用var,運作結果就不是我們預期所想的那樣…那是因為var所宣告的i只會有一個被外層範疇宣告包圍,而let會在每次迭代時建立新的變數i,而這些i都會被各自的範疇所包圍。

我們可以想成var的每次迭代都共用同一個i,而let會讓每次迭代都擁有自己獨立的i以供後續運作。

範例如下,由於console.log(i)中的i會存取的範疇是for所在的範疇,而此例為全域範疇,並且var宣告的變數不具區塊範疇的特性,因此當1秒、2秒…5秒後執行console.log(i)時,就會去取i的值,而此時for迴圈已跑完,i變成6,因此就會每隔一秒印出一個「6」。

for(vari=1;i<=5;i++){ setTimeout(functiontimer(){ console.log(i); },i*1000); } 得到結果 6; 6; 6; 6; 6; 6; 暫時死亡區域(TemporalDeadZone,TDZ) 在宣告前使用var所宣告的變數,由於拉升(hoisting)的緣故,變數宣告會被提到所屬範疇的最上方但不賦值,因此會得到undefined。

console.log(x);//undefined varx=1; ES6定義了「暫時死亡區域」(TemporalDeadZone,TDZ),意即程式碼中某個部份的變數的參考動作還不能執行的地方,這是因為該變數尚未被初始化的緣故。

範例如下,在宣告前使用let所宣告的變數會產生TDZ的問題,因此會報錯ReferenceError。

console.log(x);//UncaughtReferenceError:xisnotdefined letx=1; 這在使用typeof檢查變數是否存在時也有同樣的狀況。

如下所示,之前提到typeof對於尚未宣告的變數可有保護機制,但在這裡是無效的。

如下,typeofb會被報錯,得到ReferenceError。

{ typeofa;//undefined typeofb;//ReferenceError!(TDZ) letb; } 垃圾回收 另外值得再次提起的是「垃圾回收」的議題-一但變數用不到了,JavaScript引擎就可能會將它回收,但由於範疇的緣故,仍須保留這些變數存取值的能力,而區塊範疇明確表達資料不再用到,而解決這個不需要被保留的狀況,可釋出更多記憶體空間,點這裡參考先前範例。

const 宣告變數為常數,不可重新賦值。

{ consta=2; console.log(a);//2 a=3;//UncaughtTypeError:Assignmenttoconstantvariable. } 但注意,不可變的不是值本身,而是「參考」。

因此,當將一個物件或陣列設定為常數時,這意味著此常數的語彙範疇在消失前,這個值都無法被垃圾回收,因為這個參考永遠都無法被解除設定。

{ consta=[1,2,3]; a.push(4); console.log(a);//[1,2,3,4] a=123;//UncaughtTypeError:Assignmenttoconstantvariable. } 雖然const比起let對於JavaScript來說來得更容易最佳化,但我們在選擇要使用const或let來宣告變數時,最重要的是考慮用途-是否真的是常數?在這樣的思路下所撰寫的程式碼才是易於閱讀、好維護的。

以區塊為範疇的函式 從ES6之後,在區塊內宣告的函式將會被限制在該區塊內,但函式可被拉升而無TDZ的問題。

如下範例所示,在ES6之前的環境下,不論flag的值是true或false,都會因為函式的拉升且後面的宣告會覆蓋前面的宣告而得到2。

if(flag){ functionfoo(){ console.log('1'); } }else{ functionfoo(){ console.log('2'); } } foo();//2 但若在ES6的環境下,則不論flag的值為何,由於函式在區塊內宣告,因此只能在區塊內存取,因此會得到ReferenceError。

if(flag){ functionfoo(){ console.log('1'); } }else{ functionfoo(){ console.log('2'); } } foo();//ReferenceError 擴展與其餘運算(Spread/Rest) ...可作為擴展或其餘運算子,端看用途而定。

擴展運算子(SpreadOperator) 以...表示,將陣列展開成個別數值,可以想像是展示(展示這個陣列的所有元素)的功能。

以下列出相關應用。

將陣列展開為字串 將list這個陣列展開為字串appleboycat。

letlist=['apple','boy','cat']; console.log(...list);//appleboycat 展開字串成為個別元素 將list陣列內的字串jacket展開為個別字元。

letlist=[...'jacket']; console.log(list);//["j","a","c","k","e","t"] 陣列的複製 單層結構的物件或陣列是以深拷貝(deepcopy)的方式進行複製。

letlist=['apple','boy','cat']; letlist2=[...list]; letlist3=['doll',...list,'fat']; console.log(list2);//["apple","boy","cat"] console.log(list3);//["doll","apple","boy","cat","fat"] list.push('goat'); console.log(list);//["apple","boy","cat","goat"] console.log(list2);//["apple","boy","cat"] console.log(list3);//["doll","apple","boy","cat","fat"] 在多維陣列或有複雜物件結構的情況時,是以淺拷貝(shallowcopy)的方式進行複製。

如下範例所示,更新list中的元素'fat'為'happy',結果也更動到list2了。

letlist=['apple','boy','cat',['doll','fat']]; letlist2=[...list]; list[3][1]='happy'; console.log(list);//["apple","happy","cat",["doll","happy"]] console.log(list2);//["apple","boy","cat",["doll","happy"]] 陣列的合併 範例如下,將陣列list與list2合併為list3。

可取代concat來做陣列的複製。

letlist=['apple','boy','cat']; letlist2=['dog','egg']; letlist3=list.concat(list2);//["apple","boy","cat","dog","egg"] 改用擴展運算子。

letlist=['apple','boy','cat']; letlist2=['dog','egg']; letlist3=[...list,...list2];//["apple","boy","cat","dog","egg"] 當成參數,代入函式中 letstudent=['Nina','girl']; functionsayHi(name,gender){ console.log(`Hi,Iam${name}.Iama${gender}.`); } sayHi(...student);//Hi,IamNina.Iamagirl. 對照ES5的語法,由於apply的第二個參數是陣列(fun.apply(thisArg,[argsArray])),因此上面的例子可改為下面的寫法 sayHi.apply(null,student);//Hi,IamNina.Iamagirl. 其餘運算子(RestOperator) 以...表示,集合剩餘的數值並轉為陣列,可以想像是收納(多個元素至一個陣列)的功能。

其餘參數 範例如下,若對應不到的參數,就會將剩餘的數值蒐集起來轉成陣列(非類陣列)。

因此,x為字串'happy',其餘的字元轉為陣列["f","i","v","e"]。

constconcatenate=(x,...letters)=>{ console.log(x); console.log(letters); }; concatenate('happy','f','i','v','e'); 得到結果。

happy[('f','i','v','e')]; 使用這樣作法的優點是-在不確定要傳入多少參數的時候,是很好用的。

備註:(1)其餘參數只能有一個,並且只能放在最後;(2)其餘參數在沒有傳入值的時候會是空陣列。

陣列的解構賦值 const[a,...b]=[1,2,3,4,5];//a=1,b=[2,3,4,5] 對應不到的時候就是空陣列。

const[a,...b]=[1];//a=1,b=[] 預設參數值(DefaultParameterValue) 在JavaScript中,函式的參數預設值都為undefined,在ES6可為參數設定初始值,好處是再也不用在函式中檢查參數是否為undefined再設定初始值了。

範例如下,像是這樣的檢查,若沒有傳入參數值或傳入undefined,就設定為初始值。

functioncallMe(phone){ returnphone||'0912345678'; } callMe();//"0912345678" callMe(undefined);//"0912345678" callMe('0987654321');//"0987654321" 但若傳入會轉型為false的值,就會出現誤判的狀況,反而用了預設值。

這是因為邏輯運算子||會先將第一個運算元做布林測試或強制轉型為布林以便測試,若為結果true,則取第一個運算元為結果;若結果為false,則取第二個運算元為結果。

因此,如下範例所示,0||'0912345678'檢測0並做轉型得到false,因此選了第二個運算元作為結果,也就是'0912345678',而誤設了初始值。

callMe(0);//"0912345678" 改進如下。

functioncallMe(phone){ vartelNumber=typeofphone!=='undefined'?phone:'0912345678'; returntelNumber; } callMe();//"0912345678" callMe(undefined);//"0912345678" callMe('0987654321');//"0987654321" 或 functioncallMe(phone){ vartelNumber=phone!==undefined?phone:'0912345678'; returntelNumber; } callMe();//"0912345678" callMe(undefined);//"0912345678" callMe('0987654321');//"0987654321" 以上看起來程式碼挺複雜的…那麼改為在參數傳入時設定預設值吧。

constcallMe=(phone='0911111111')=>phone; callMe();//"0911111111" callMe('0922222222');//"0922222222" 預設參數值的功能是不是簡潔有力呢! 預設值運算式(DefaultValueExpression) 預設值不但可以是簡單值,還可以是運算式或函式呼叫。

函式宣告中的形式參數(formalparameter)是形成自己的範疇,當無法在形式範疇中找到時,才會往外面的範疇查找。

因此functionbar(x=y+2,z=foo(x))中的y無法在自己的形式範疇中找到,於是往外的全域範疇找到值為5,而順利運算得到x的值;z=foo(x)中的x可在自己的形式範疇中找到,於是順利計算出z的值。

functionfoo(a){ returna+1; } functionbar(x=y+2,z=foo(x)){ console.log(x,z); } vary=5; bar();//得到78,其中x=7,y=5,z=8 bar(10,20);//得到1020,其中x=10,未宣告y,z=20 再看下面的一個例子,在運算z+1的過程中,z可在自己的形式範疇中找到,但此時z尚未被初始化(TDZ)而無法存取,因此得到ReferrenceError。

varw=1, z=2; functionfoo(x=w+1,y=x+1,z=z+1){ console.log(x,y,z); } foo();//ReferrenceError 解構(Destructure) 物件與陣列專用,是一種有結構的指定方式,可以想像成是類似鏡子映射的概念來指定成員值。

範例如下,經由一一對應的方式,將變數與值對應起來,無法對應到的就會得到undefined。

const[a,b]=[1,2];//a=1,b=2 const{foo,bar}={foo:'12345'};//foo='12345',bar=undefined 物件特性指定模式 在這裡要先說明一下,一般物件屬性的設定與物件解構的模式可能稍微有點不同。

範例如下,在物件a指定屬性aa與bb的值的過程中,aa:x的資料來源與目的地是targettarget,這樣的模式是需要特別注意的。

varx=1, y=2; vara={aa:x,bb:y}; var{aa:AA,bb:BB}=a; console.log(`AA:${AA}`); console.log(`BB:${BB}`); 得到結果 AA:1; BB:2; 我們也可以將宣告的變數提出來,而非隱藏在解構當中。

varx=1, y=2, AA, BB; vara={aa:x,bb:y}; ({aa:AA,bb:BB}=a); console.log(`AA:${AA}`); console.log(`BB:${BB}`); 注意({aa:AA,bb:BB}=a)外必須要包裹小括號,否則{...}的部份會被視為區塊述句而非物件。

另,指定的不一定要是簡單值,也可以是運算式。

範例如下,經由計算得出的屬性名稱與解構方式來指定屬性值。

varwhich='a', o={}; functionfoo(){ return{ a:1, b:2, }; } ({[which]:o[which]}=foo()); console.log(o);//{a:1} 還可做值的交換,範例如下,將x與y的值做交換。

varx=1, y=2; [y,x]=[x,y]; console.log(`x:${x},y:${y}`);//x:2,y:1 重複指定 解構允許將來源值重複指定給目的地。

vara={x:1}; var{x:AA,x:BB}=a; console.log(`AA:${AA},BB:${BB}`);//AA:1,BB:1 巢狀解構 解構子物件或子陣列。

解構子物件。

var{ a:{x:X,x:Y}, a, }={ a:{ x:1, }, }; console.log(a);//{x:1} console.log(X);//1 console.log(Y);//1 解構子陣列。

({ a:X, a:Y, a:[Z], }={a:[1]}); X.push(2); Y[0]=10; console.log(X);//[10,2] console.log(Y);//[10,2] console.log(Z);//1 巢狀預設值:解構與重新結構化 我們常遇到一種狀況,在製作設定檔時,一般條件下,使用預設的設定參數即可,而若有特殊情況,再將部份參數修改,以適應新的環境。

這裡有一份預設的參數設定檔案。

vardefaults={ setupFiles:['setup.js','setUp-another.js'], testUrl:'https://sample.com.tw', support:{ a:true, b:false, }, }; 還有一份應用在特殊情況下的參數設定檔案,若在特殊狀況下沒有設定到的部份,就沿用預設的參數設定即可。

varconfig={ testUrl:'https://sample-special.com.tw', support:{ a:false, c:true, }, }; 若希望能達到目的-以defaults為預設值,並添加或覆蓋上config,大多時候的寫法都是很複雜的。

來看解構可以幫我們做什麼? 將defaults融合到config中,如下所示,先將全部的屬性都解構到頂層變數中,再重新建構為預定的巢狀結構。

{ let{ setupFiles=defaults.setupFiles, testUrl=defaults.testUrl, support=defaults.support, }=config; config={ setupFiles, testUrl, support, }; } 解構可以幫我們重新結構化,好讀易懂。

太多、太少、剛剛好 解構不一定需要一一對應。

不必指定所有出現的值,丟棄a物件的屬性y與z,只取x。

vara={ x:1, y:2, z:3, }; var{x}=a; console.log(x);//1 超過的指定會被設為undefined,範例如下,z並沒有被對應到,因此值為undefined。

vara=[1,2]; var[x,y,z]=a; console.log(`x:${x}`); console.log(`x:${y}`); console.log(`x:${z}`); 與擴展運算子合併使用,如下範例所示,...d會將剩餘的數值聚集成一個陣列並指定給d。

vara=[1,2,3,4,5]; varb=([,c,...d]=a); console.log(c);//2 console.log(d);//[3,4,5] 預設值指定 解構也能當成預設值來做指定,如下範例所示,由於b為undefined,因此給定預設值2。

var{a=3,b=2,c=1}=foo(); functionfoo(){ return{ a:1, b:undefined, c:3, }; } console.log(a);//1 console.log(b);//2 console.log(c);//3 參數解構 函式的參數解構,並搭配指定預設值。

varobj={a:2,b:1,c:'HelloWorld'}; functionfoo({a=1,b=2}){ console.log(`a:${a},b:${b}`); } foo(obj);//a:2,b:1 解構預設值vs參數預設值 解構預設值與函式參數預設值的差異是什麼呢? 範例如下,函式foo使用了兩種方式來設定參數的預設值,其中 {x=10}={}使用解構預設值來設定x的初始值,若傳入的物件沒有與x同名的屬性,則會使用預設值10。

{y}={y:10}使用參數預設值來設定y的初始值,若傳入的物件沒有與y同名的屬性,就會得到undefined。

functionfoo({x=10}={},{y}={y:10}){ console.log(x,y); } foo();//1010 foo(undefined,undefined);//1010 foo({},undefined);//1010 foo({},{});//10undefined foo(undefined,{});//10undefined foo({x:2},{y:3});//23 物件字面值擴充功能(ObjectLiteralExtension) ES6為物件字面值新增了幾個重要且方便的擴充功能。

簡潔特性 若定義的屬性與所使用的語彙識別字同名則可作簡寫。

範例如下,x與y的屬性與所使用的語彙識別字同名,分別寫了兩次x與兩次y。

varx=1,y=2; varobj={ x:x, y:y, }; 簡寫如下,只需要寫一次x和y就可以了。

varx=1,y=2; varobj={ x, y, }; 簡潔方法 方法上不需要function關鍵字,也就是省略function()。

varobj={ a:function(){ //... }, b:function(){ //... }, }; 可簡寫為… varobj={ a(){ //... }, b(){ //... }, }; 計算得出的屬性名稱 ES6新增動態產生的字串作為屬性名稱功能,讓key的值可經由運算得出,並且必須使用鍵值存取[]的方式。

constprefix='fresh-'; constfruits={ [prefix+'apple']:100, [prefix+'orange']:60, }; fruits['fresh-apple'];//100 fruits['fresh-orange'];//60 設定[[Prototype]] 在ES6前若想設定[[Prototype]]內部屬性的值,可直接設定__proto__這個內部屬性來將兩個物件連起來(點這裡複習原型,點那裡複習委派),但現在可以使用ES6的setPrototypeOf來設定[[Prototype]]內部屬性的值了。

varo1={ //... }; varo2={ __proto__:o1, //... }; 可改成 varo1={ //... }; varo2={ //... }; Object.setPrototypeOf(o2,o1); 這個意思就是當在o2無法找到特定的屬性時,就循著鍊結串鏈往o1找就可以了。

物件super super通常與類別有關,但其實也可以只是(也只能)用於普通物件的簡潔方法中(不可用於函式運算式屬性上)。

如下範例所示,o2的foo方法中的super等同於Object.getPrototypeOf(o2)而得到o1,因此會呼叫o1.foo。

varo1={ foo(){ console.log('o1:foo'); }, }; varo2={ foo(){ super.foo(); console.log('o2:foo'); }, }; Object.setPrototypeOf(o2,o1); o2.foo(); 得到結果 o1:foo; o2:foo; 範本字面值(TemplateLiteral) 範本字面值又稱「內插的字串字面值」(InterpolatedStringLiteral),在ES6可使用`(backtick)作為分隔符號來內插運算式,並會自動剖析與估算。

也就是說,backtick的內容會被解讀為字串,而${...}內的運算式會被剖析並在行內被估算。

範例如下,ES6前必須要用+與雙/單引號拼湊字串,但在ES6後使用${variable_name}代入變數即可。

letname='Summer'; letgreetings_1='Hello'+name+'!';//greetings_1="HelloSummer!" letgreetings_2=`Hello${name}!`;//greetings_2="HelloSummer!" 可跨越多行,並且斷行會被保留。

vartext=`HelloWorld HelloWorld HelloWorld HelloWorld`; console.log(text); 得到結果 HelloWorld HelloWorld HelloWorld HelloWorld 除了字串,還可以放任何運算式,包含函式呼叫等。

如下所示,建立一個函式upper負責將字串轉為大寫,接著內插到backtick中。

functionupper(s){ returns.toUpperCase(); } varwho='reader'; vartext=`Avery${upper('warm')}welcome toallofyou${upper(`${who}s`)}!`; console.log(text); //AveryWARMwelcome //toallofyouREADERS! 箭號函式(ArrowFunction) 箭號函式除了提供較精簡的語法外,更重要的是關於this的綁定(點此複習this),箭號函式的this的值是以語彙範疇的方式查找,其值並非源自執行時的綁定,而是定義時包含它的範疇或全域範疇,並不適用於之前在this的篇章所提到的四種規則來做判斷。

範例如下,雖然obj.sayHi()印出預期的「Hi,IamJack」,但setTimeout(obj.sayHi,1000);卻因為前面提過的隱含的失去中的函式是另一個函式的參考而失去了原先this的綁定。

varname='Apple'; varobj={ name:'Jack', sayHi:function(){ console.log(`Hi,Iam${this.name}`); }, }; obj.sayHi();//Hi,IamJack setTimeout(obj.sayHi,1000);//Hi,IamApple 改進方法有很多,像是… 透過變數儲存目前this的值。

使用bind、call或apply,綁定特定的物件作為this的值。

透過變數儲存目前this的值…這其實不算是「改變」,只能說是「保留」。

如果希望存取到外部的this,可用一個變數_this儲存起來,稍後再用。

修改上例如下,一秒後的確是印出「Hi,IamJack」。

varobj={ name:'Jack', sayHi:function(){ var_this=this; setTimeout(function(){ console.log(`Hi,Iam${_this.name}`); },1000); }, }; obj.sayHi();//Hi,IamJack 當然我們也可以用bind、call或apply來明確綁定特定的物件作為this的值。

修改上例如下,一秒後的確是印出「Hi,IamJack」。

varobj={ name:'Jack', sayHi:function(){ setTimeout( function(){ console.log(`Hi,Iam${this.name}`); }.bind(this), 1000, ); }, }; obj.sayHi();//Hi,IamJack 既然箭號函式會自動綁定所在範疇,那也就可以這麼修改了…經由箭號函式就會把this綁在obj上。

varobj={ name:'Jack', sayHi:function(){ setTimeout(()=>{ console.log(`Hi,Iam${this.name}`); },1000); }, }; obj.sayHi();//Hi,IamJack 雖然箭號函式語法簡單又能解決this綁定不明確的問題,但箭號函式不適用於… 箭號函式會將this強制綁定為執行環境,因此,若不希望this被綁定為執行環境,基本上都是不適用的,不需要為了少寫幾個字而硬要使用箭號函式。

箭號函式內的callback是匿名的,匿名函式的缺點請看這裡。

更多關於箭頭函式的this,可參考這裡。

回顧 看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到… 使用大括號{...}定義區塊範疇,讓let和const宣告以區塊為範疇的變數。

擴展運算子以...表示,將陣列展開成個別數值,可以想像是展示(展示這個陣列的所有元素)的功能;其餘運算子以...表示,集合剩餘的數值並轉為陣列,可以想像是收納(多個元素至一個陣列)的功能。

為函式的參數設定初始值。

解構可用於物件特性指定、重複指定、巢狀解構(設定預設值並重新結構化)、調整無法一一對應的狀況、預設值指定、參數解構(比較解構預設值與參數預設值)。

物件字面值擴充功能,像是簡潔的特性與方法、計算得出的屬性名稱、設定[[Prototype]]與物件super。

範本字面值使用`(backtick)作為分隔符號來內插運算式,並會自動剖析與估算。

除了字串,還可以放任何運算式,包含函式呼叫等。

箭號函式除了提供較精簡的語法外,更重要的是關於this的綁定-強制綁定為執行環境。

References YouDon’tKnowJS:ES6&Beyond,Chapter2:Syntax 同步發表於2019鐵人賽。

標籤: You-Dont-Know-JS javascript 2019鐵人賽 你所不知道的JS 你懂JavaScript嗎? 鐵人賽 ES6 You-Dont-Know-JS-ES6-and-Beyond ReferenceError undefined operator運算子 commentspoweredbyDisqus RecentPosts 《美國四星上將教你打造黃金團隊》閱讀筆記–共享意識、賦權與自主成長,終能提升團體的實力 15Jun2022 ArchitectingonAWS筆記:Networking 02Jun2022 ArchitectingonAWS筆記:BackupandRestore 01Jun2022



請為這篇文章評分?