JavaScript 概念三明治(六)

Event Table

Event Table 是與Event Queue 互相搭配的資料集合,它負責記錄在非同步目的達成後,有哪些函式或者事件要被執行,這裡指的非同步目的指的是像計時完畢、API資料獲取完畢、事件被觸發。當我們執行setTimeout這個函式時,JavaScript會把給定的函式與像是倒數的秒數之類的附帶資訊(meta data)推送到Event Table裡面,等到一秒過後(目的達成)該函式教會被正式推送到事件除列等待執行,而在這之前JavaScript就是透過Event Table知道有那些事件要被送到事件儲列中。

Event Loop

那麼,什麼又是事件迴圈Event Loop呢?可以把Event Loop想成是另外一個幾乎無時無刻、每一毫秒都在執行的程式,它負責檢查現在主執行環境堆疊是否是空的。如果是空的,再去檢查Event Queue,若Event Queue有函式等待執行,則將這些函式從Event Queue依序到主執行環境並執行。
也因為有事件迴圈與事件儲列的機制,像是事件監聽與透過Ajax拉取資料這類行為才有辦法被達成。例如在監聽網頁點時,一旦使用者做了點擊的動作,對應的邏輯就會被推送到事件儲列內,而事件迴圈看到儲列內有待執行的任務,就會負責去執行。
那麼,現在了解了整個事件儲列的概念,讓我們再度回到setTimeout的例子,來看看一道經典的面試問題。
for (var i=0; i<3; i++){
setTimeout(function(){
console.log(i)
}, 1000)
}
各位可以先想一下,在一秒之後,setTiomeout函式內console.log的輸出的結果是甚麼。這邊用到的概念跟前面講閉包的時候類似,都與先後順序有關。若沒有非同步概念的話,通常會下意識的認為結果會是0、1、2,不過這段程式碼最後其實是會印出3、3、3,不知道有沒有猜對。
搭配識見儲列的概念,現在我們知道setTimeout對應的函式會在主執行環境結束之後才被執行,而等到該函式被執行時,for迴圈裡面的i早就已經因為迴圈而被修改為3了。
要改變三個3的結果,想要看到0、1、2的話該怎麼辦呢?這邊要結合一點前面說到的範疇與閉包的觀念,可以看到for迴圈裡面的i是利用var所宣告,而既然var具有的範疇是根據函式來界定,那麼我只要利用函式,是不是就是夠透過產生閉包,把每個迴圈的i值保留下來呢?當然可以,我們可以利用立即執行函式(IIFE),並把i導入這個立即執行函式,就能夠產生閉包了。

for (var i=0; i<3; i++)
(function(x) {
setTimeout(function(){
console.log(x)
})(i)
}

或者是把var宣告改成let來做宣告
for (let i=0; i<3; i++){
setTimeout(function(){
console.log(i)
}, 1000)
}

Promise

我們在前一個段落提到,為了防止網頁的主程式因為等待某些邏輯運算的回應而停擺,有時候JavaScript的行為需要透過非同步的方式來執行,包括一些瀏覽器的API和對外部伺服器拉取資料的動作,而這些動作是利用瀏覽器的Event Queue來達成非同步的行為。
這的確減少不必要的等待,進而增加了使用流程上的順暢度。不過就程式碼的撰寫方式來看,由於在做這些非同步行為的時候,若不做任何的處理,一般都是以回呼函式的方式來進行,才能確保某一段邏輯在非同步行為完成之後才被執行,若這類邏輯開始複雜的時候,可能會變得難以閱讀。
舉前例講到的例子來說,若以setTimeout來模擬一個非同步的行為,想要確保這件事情的話,我們就必須把一個非同步行為放到另外一個非同步的行為的回呼函式裡面。當這樣子的需求越來越多時,你可能就會看到這樣的程式碼。
const asynActionA = (fn)=>setTimeout(fn, 10000);
const asynActionB = (fn)=>setTimeout(fn, 10000);
const asynActionC = (fn)=>setTimeout(fn, 10000);
const asynActionD = (fn)=>setTimeout(fn, 10000);

asyncActionA(()=>{
console.log("asynActionA");
asynActionB(()=>{
console.log("asynActionB");
asynActionC(()=>{
console.log("asynActionC");
asynActionD(()=>{
console.log("asynActionD");
});
});
});
});
這種巢狀的回呼函式一旦多了起來,就容易造成維護上的困難。一般在正式的專案中都非常不樂見這樣子的程式碼。為了解決這樣的問題,我們就必須認識這個Promise
>Promise簡介
Promise是什麼呢?Promise是JavaScript版本E86以後出現的新語法,這個詞以字面上的意義來看,用比較白話的方式解釋的話有一種:我承諾幫你做某件事情,能不能成功還不一定,但是我做完之後會把結果告訴你的意思。官方的文件描述則是:
Promise是一個代表非同步運作的最終狀態的物件(成功或失敗)
A Promise is an object repressnting the eventual completion
or failure of an asynchronous operation.(MDN)

從技術文件的角度來解釋就顯得比較抽象,不過你應該大致能夠看出一點頭緒,只要抓住幾個關鍵字--也就是「成功」與「失敗」兩種狀態。
>Promise的狀態
進一步總結以上的論點,一個Promise以時間順序來看是有狀態之分的。除了前面講的成功與失敗兩種結果,一般以Pending來描述在執行中,懸而未決的Promise,一個Promise總共會有三種可能的狀態,分別代表進行中、成功與失敗。而相對於Pending這個代表處理中的狀態,不管是進入成功或失敗狀態的Promise,我們都能夠用Settled來表示這個Promise已經被解決了。

*Pending:還在執行中的狀態,表示還沒有特定結果。
*Fulfilled:成功的狀態,代表Promise被實現,對應的回呼函式為resolve。
*Rejected:失敗的狀態,代表Promise被拒絕,對應的回呼函式為reject。
*Settled:表示Promise已經被解決,結果已經確定。

>Promise基本使用方式
從前面文件的描述應該也可以看得出來Promise在JavaScript裡面是以物件的方式存在。這個物件又是怎麼產生呢?接下來我們就來看看要怎麼使用Promise吧!基本的Promise宣告方式如下:
我們進一步仔細看一下這個傳進Promise建構函式,它接收了兩個參數:resolvereject來命名。

resolvereject 其實分別是兩個具有不同目的的函式。resolve(解析)被用於在認為Promise內的行為成功時呼叫reject(拒絕)則用於被認為失敗的邏輯發生時呼叫。使用這兩個詞最為函式的名稱只因為這是一種約定俗成,許多人都用這些詞來稱呼它們。

JavaScript 概念三明治(五)

同步/非同步

「同步」聽起來就是指一次做好幾件事情,其實並不是,同步指的是:
同一時間只做一件事
套用到JavaScript裡面就是指一次只跑一段程式碼,就像JavaScript語法解析器做的,從上到下一行一行執行那樣。當JavaScript載入全域執行環境,也許呼叫了某個函式創造出一個函式執行環境,然後回到全域環境後結束,這一切都是同步來做處裡的,也就是說,在JavaScript裡面不可能同時執行兩個函式、執行多段程式碼邏輯。
而「非同步」呢?就很好理解了,相較於同步,它的概念則相反:
同一時間內處理不只一件事
必須透過非同步的處理方式,才能讓JavaScript在進入全域執行環境到結束的這中間,能夠額外處理一些需要時間進行的邏輯,而又不影響原來的主程式。在非同步的處理方式下,主程式的運行還是會繼續,而非同步進行的工作也還是會進行,只是不是馬上,而是等到主程式執行完畢,JavaScript引擎有空檔後才會被執行。
非同步的行為在瀏覽器前端很常見,像是部落格網站,當網站一打開,JavaScript必須處理畫面上的一些行為,卻又必須跟後端取文章資料。若以同步的方式來進行這個拉取文章的動作,那麼在拉取道完整文章之前,JavaScript就會無法執行任何其他任務。
也就是說,即便JavaScript是一門同步的語言,但為了符合瀏覽器的使用情境,某些時候它必須具有非同步執行邏輯的能力,至於它怎麼處理這些互動的非同步動作呢?就是透過瀏覽器內的事件迴圈的幫助,關於事件迴圈在後面馬上就會看到。
瀏覽器內可不是只有JavaScript引擎而已,還有其他許多部份在JavaScript引擎之外執行的其他程式,像是渲染引擎負責畫面的構築、以及負責處理網路溝通如Http請求以獲得資料的部分。而JavaScript可能在某些時候必須要要求瀏覽器變更畫面的內容,或是觸發網路溝通的流程,以與其他伺服器溝通,但是這些工作可能會是非同步的,只有在JavaScript引擎裡面事同步的運行。

Event Queue 與 Event Loop

Event Queue、Event Loop這兩個部分並不存在於JavaScript內,而是瀏覽器的其中一部份。它們雖然不屬於JavaScript,卻是前端產品開發裡面最重要的概念之一。因此,需要了解它們為前端互動方面帶來什麼樣的幫助和影響。
>JavaScript引擎的組成

讓我們回想一下前面提到的基礎。之前提到過「執行環境堆疊」,函式呼叫時會產生執行環境,若在這個函式執行環境內含有其他函式被呼叫,就會在之上產生另一個執行環境,形成堆疊。而在上層的執行環境結束之前,下層部分的其他程式碼事無法被執行的,包含全域執行環境。                                                                                                

因此,只要在這之中某個堆疊過久,就算只有一個函式執行環境的堆疊,都有可能影響整個主程式(全域執行環境)的運行。不過應用程式裡面總是會有某些功能需要時間來提取/運算,這時候為了不讓整個主程式停下來等待太久,我們可以,而且其實我們很常把這些比較耗時的工作放在主程式以外的某一個部分去執行。
要討論下一段之前,必須先複習一下,前面我們提到的,JavaScript引擎底下根據功能大致上可以分為三個部分:

#全域執行環境
#執行環境堆疊
#記憶體堆疊

然而瀏覽器內可不只有JavaScript引擎,接下來我們要提到一個很重要的概念--Queue(又稱Message/Event/Callback Queue)。
整個瀏覽器的運行並非只有JavaScript引擎組成。前面說到過,因為JavaSript屬於同步執行的語言,同時又為了讓網頁具有像「監聽事件」、「計時」、「拉第三方API」這些類似「背景作業」的功能,瀏覽器提供了另外一些部份來達成,分別是:
#

#Event Queue
#Web API
#Event Table
#Event Loop

整個由上述部分,包含JavaScript引擎所組成的整體,稱為JavaScript Runtime Environment(JRE),瀏覽器內並不只有JavaScript語言本身而已,我們先來看一下瀏覽器內的事件處理。
>Event Queue
Queue(儲列)是什麼樣的概念呢?我們先來看一下,在寫網頁程式的時候,有一些所謂的「內建的」API如setTimeout、setInterval,這些API不存在於JavaScript原始碼內,但你仍然可以在開發時直接使用。因為這些API是屬於瀏覽器所提供的WebAPI。WebAPI並非JavaScript引擎的一部份,但它屬於瀏覽器運行流程的一環。
關於Web API,舉一些例子:
*操作DOM節點的API:document.getElementeById
*AJAX相關API,像是:XMLttpRequest
*計時類型的API,就像剛剛提到的setTimeout
這類Web API在與JavaScript原始碼一起執行的時候,通常不會直接影響JavaScript主執行環境的運行。否則的話若網頁需要與後端伺服器溝通,拉取外部資料時,就只有乾等,無法執行任何其他事情了!
>Event Queue 運行流程
要了解Event Queue的運行,我們必須先找到一件會放到Event Queue執行的事情。所以這邊要先介紹其中幾個瀏覽器的API:setTimeout與setInterval。setTimeout是一個全域的函式,用於將想做的事情延後幾秒進行。將想要執行的邏輯以函式的方式傳入第一個參數,並在第二個參數傳入想要等待的時間,JavaScript就會為你在若干秒之後,呼叫你傳入的函式。
而setInterval呢?與setTimeout相似,只不過setTimeout只會在你給定的秒數之後執行一次,而setInterval則是根據你給定的時間,固定幾秒執行一次。
function executeAfterDelay(){
console.log("一秒之後才會執行");
}
setTimeout(executerAfterDelay, 1000);
console.log("我會先被被執行");
如果了解了這兩個方法的用途,來觀察上圖的程式碼應該不會太難理解,函式內的程式碼會在一秒之後才被呼叫。不過意外的事情總是會發生,這段程式碼裡面,setTimeout可不像一般的函式那樣運行起來那麼單純。
我們可以試試看把setTimeout的時間調整為0,那是不是就會馬上執行對應的函式內容了呢?
function executeAfterDalay() {
console.log("應該要馬上執行");
}
setTimeout(excuteAfterDalay,0);
console.log("應該要最後執行");

整個流程看起來像這樣:
1.JavaScript 引擎執行到瀏覽器提供的setTimeout函式。
2.JavaScript 引擎繼續運行,同時瀏覽器開始根據給定的秒數計時。
3.等待計時完成後,把剛才接收到的函式推送到Event Queue內。
4.等待JavaScript引擎運行完畢,主執行環境結束後,將Event Queue內的函式推送到JavaScript主執行環境,產生堆疊(執行該函式)。

JavaScript 概念三明治(四)

函式陳述式與表達式

JavaScript有兩種語法分類:陳述式與表達式。而了解這兩種語法分類之後,我們才有辦法區別函式的兩種使用方法:函式宣告式、函式表達式在運用上有什麼不同。
陳述式
陳述式Statement就像生活中我們所描述事情時所說的某句話像是「今天出門天氣不好的話記得帶傘」,像是一種聲明、或是對邏輯的描寫。陳述式會影響程式的運行流程,並且一定會做某一些事情,向邏輯判斷就是一種陳述式。
但是陳述式在JavaScript並不會產生數值,可以以此看出它跟表達式的區別。所以不能放在JavaScript內預期會產生數值的地方,像是函式的參數、函式的回傳值、或是宣告變數時等號的右邊(不能被用來指派給另一變數)。舉例來說,常見的陳述式有:

*if/switch判斷式
*for/while迴圈
*變數宣告
*一般函式宣告

表達式
就像你每個月上班或打工,做完該做的事情之後會拿到薪水作為回報一樣。表達式也是一段JavaScript在執行完後會得到一個結果的程式碼。它可以是很長,也可以簡短,只要執行後會回傳結果的一段程式碼,就是表達式。
//函式呼叫
functionInvocation();
//變數指派
a = 3;
//運算式
1 + 2;
a ===3;
ture || false;
true && true;
Array.isArray([]) ? doSomeThing() : doOtherThing();
這些都是表達式。
函式陳述式與函式表達式
在JavaScript裡面,要創造一個函式,可以透過宣告式,也能透過表達式來達成。這兩種方法就很值觀則分別稱成「函式陳述式」跟「函式表達式」。
函式陳述式
函式陳述式是藉由直接給函式名稱來直接宣告一個函式,其實它就是一般的函式宣告。之前有說過變數宣告時,JavaScript會幫你保留記憶體空間,所以它屬於陳述式。而像這樣子一般的函式宣告,跟變數宣告會產生的行為是一樣的,因此當然也屬於陳述式。
//可以在宣告函式的程式碼之前就先呼叫
functionStatement();
function functionStatement(){
//do something
}
函式表達式
另外一種宣告函式的方法是函式表達式,函式表達式的寫法是直接一個把函式指派給另一個變數,之所以能夠這麼做是因為函式本身在JavaScript裡面也是一個物件,所以除了一般的宣告方式之外,函式也能夠作為一個被指派的值。
functionExpression() //functionExpression is not a function
var functionExpression = function(){
//do somthing
}

上方是 functionStatement
下方是函式表達式,有 do somthing

回呼函式

在JavaScript裡面很常聽到這個值,那麼什麼是回呼函式Callback Function?其實它指的就是在函式裡面執行的另外一個函式。它有著一種「等某段邏輯執行完畢之後,在告訴我」的意思。

其餘參數以及物件參數

這邊介紹兩種在函式上非常實用的語法:其餘參數物件作為參數時的展開。

>其餘參數
當一個函式有可能接收一個以上但不確定數量的參數,我們就能夠使用ES6之後提供的其餘參數(Rest Parameter)。其餘參數以...加上變數名稱來表示,且只能擺在最後一個位置,它會將所有後來未在函式裡面定義的變數,蒐集成一個陣列傳入函式。

function addAll(a,b,…rest){
let sum =a+b;
rest.forEach((num)=>{ //參數 rest會是一個陣列
sum = sum +num;
})
return sum;
}
AddAll=addAll(1,2,3,4,5,6);
console.log(AddAll);
>物件參數的展開
當一個物件參數傳入函式時,我們可以把這個物件「展開」,快速地取得物件裡面的屬性作為參數給函式內的邏輯使用。
var userInfo = {
name:'Anakin',
nickName: 'King'
}
function displayUser({name,nickName}){
//do something
}
displayUser(userInfo);

JavaScript 概念三明治(三)

箭頭函式

E86之後,出現一種新的創造函式的方法,普遍被稱為箭頭函式,使得創造函式變得簡潔許多。相對於以前使用function關鍵字來宣告的用法,箭頭函式只需要用=>搭配括號跟區塊,就可以簡單地創造出函式,而因為這個等於大於的符號看起來很像箭頭,所以才被稱為箭頭函式。

const arrowFunction =() =>{
//do something.
};
箭頭函式的用法如上,可以看到這個用法已經不需要function關鍵字了,取而代之的是在括號跟區塊中間的箭頭。而你應該也可以注意到,箭頭函式的用法就與前面提到的函式表達式非常相近。沒錯,箭頭函式本身是匿名的,這意味它只可以被使用在函式表達式裡。
善用函式表達式
我們不妨試試看直接創造一個箭頭函式,但不將它指派給任何變數試試看。

()=>{
console.log("this is an arrow function");
};
你會發現,因為它是表達式,所以直接執行並不會出錯,就像我們直接在全域環境創造一個數字、一個字串或一個物件一樣。它會回傳一個函式物件,不過JavaScript看到這裡只會知道,「喔,這裡有一個函式」,所以該函式不會被執行,這段程式碼對JavaScript來講其實沒有什麼意義。
接下來,直覺敏銳的讀者應該可以猜到我想要說什麼了,其實轉個彎想一下,既然它只能用在表達式,那麼也許我也能把它用在前面立即執行函式(IIFE)的寫法上。
(()=>{
console.log("arrow function has been invoked");
})() //加上()會先去執行()的函式

一樣用括號,並把函式放進括號裡面,清楚告訴JavaScript這是一個表達式,而且我要優先運算,然後得到運算結果所回傳的函式物件之後,直接執行它。這樣一來,我就可以用簡潔的方法來撰寫IIFE了。

簡潔的回傳值
使用箭頭函式時,若所要回傳的內容單純只有一個表達式的時候,這個時候我們可以省略區塊{},而直接接在箭頭符號後面,這麼一來在函式被呼叫之後,這個表達式會直接被回傳。
當然,箭頭既然函式本身也是表達式,而JavaScript裡面函式又可以被傳給另為一個函式,那麼我應該有可以這麼做:
有看到特別的地方嗎?上方呼叫add函式的時候連續使用的兩個括號,這可不是連續呼叫兩次的意思!而是因為add函式本身回傳的是另外一個函式,會有兩次的呼叫是說:在執行第一個函式完收到第二個函式之後就直接執行了。

與function關鍵字的差異

箭頭函式相較於一般函式function關鍵字的用法,一般來說的優點都是語法簡潔、好閱讀等等,實際上也真的是這樣,它的確能夠讓開發者增加不少開發速度,不過它與使用function關鍵字創造出來的函式有些微的不同,甚至在某些點上有非常大的不一樣。這些差別不一定就是好或是壞,端看你如何使用而已,所以在這個段落的後面篇幅將會列舉一些較為明顯的差異來說明。
>>箭頭函式沒有arguments物件
什麼是arguments物件?arguments這個字代表了引數,另外一個與隻非常相近的詞稱為參數,引數用在函式宣告時,而呼叫函式時所傳進去的值則叫做參數。
換句話說,宣告一個函式時要先定義好參數,使函式時必須傳入引數。這兩個詞蠻常被搞混的,在別的地方搞錯沒有關係,不過在這邊為了介紹arguments物件,必須要釐清他們的差異才行。
   function add(a,b){  //a,b是參數parameters
return a+b;
}
add(1,3); //引數
所以函式裡面的arguments物件其實就是用來接收傳入函式的引數。一個使用function關鍵字來創造的函式,在函式被呼叫時,除了直接透過函式的參數名稱來拿到對應傳入的引數值,但是也可以用物件來取用個別的引數內容,這是JavaScript提供的預設物件。
arguments物件是一個很像陣列的資料,你可以透過索引來拿到個別的引數值。
但它沒有陣列上那些方便的方法像是foreach或是map,真的單純只是長得像陣列而已,有些人會把它稱為是一個「類陣列」Array-Liked的資料結構。而一個跟陣列比較像的屬性只有length,所以如果要依序拿到每個引數值,則必須用一般的for迴圈。
function add(a, b) {
for (var i=0; i<arguments.length; i++){
if (typeof arguments[i] !== "number"){
return false;
}
}
return a+b;
}
add(1,2); //3
add(1,"some thing else"); //false
而arguments物件在箭頭函式內可不存在的,所以如果想要利用它來組合一些邏輯的話,千萬記得這一點。
const arrowFunction =() =>{
console.log(arguments);
}
arrowFunction();
//ReferenceError:arguments is not defined

JavaScript 概念三明治(二)

物件型別

   「物件」指的就是物件。恩,但其實有很多東西本身也算是物件,例如陣列和函式,不相信嗎?讓我們繼續看下去。你可以在JavaScript裡面宣告一個陣列,然後用typeof去得到這個陣列的型別,就可以看出一些端倪:
JavaScript提供了Array這個所有地方都可以使用全域物件,而在這個Array.isArray的方法能夠讓我們使用,來知道某數值是不是一個陣列。
好,那麼剛剛說函式也是物件型別,是這樣嗎?你會發現,當你一樣用typeof去觀察一個function的型別時,所得到的結果不是object,而是function。
先別急著反駁,讓我先用一個最簡單的方式證明你看。前面應該提到過,一個物件型別的數值,可以用.或是[]來做存取。那麼首先我宣告一個函式,並把這個函式當作物件使用,在上面新增一個屬性之後,再取出這個屬性來看看,是不是得到同樣的值:
    函式真的是像物件一樣的東西,事實上,它真的就是。在JavaScript裡面,函式算是一個比較物件,稱為「函式物件」(Function Object),它除了可以被當作函式來呼叫,也能夠當作物件使用。

一級函式  First-Class Function

    當我們說一個語言具有一級函式的特性時,代表這個語言把函式當作變數值一樣看待,也因此可以把函式當作參數一樣傳入另外一個函式裡面,也能夠以函式作為另一個函式的回傳值。在Function Programing 裡面,也是因為這個特性,才有辦法做到複合函式(Function Composition)。
在JavaScript內,函式本身也是一個特殊的物件(就是函式物件),所以才有辦法把函式當作參數傳給另外一個函式來做使用。

高階函式 High Order Function

    高階函式有人稱為HOF。只要是可以接收函式作為參數,或是回傳函式作為輸出的函式,我們就能夠把它稱為高階函式,例如,JavaScript裡面的陣列這個類別,上面有許多讓你很方便能夠對陣列做操作的方法,例如:map方法讓你能夠依序對每個陣列做一些修改、而filter則讓你能夠過濾掉不符合條件的元素。
我們可以試著思考看看他們是怎麼被實做出來的,下面就以Array.map為例,一起來模擬這個方法、邊思考它的運作方式。

試著實作陣列上的map方法

map是在任何陣列上都可以取用的一個方法,它接收一個能夠修改陣列元素的函式。並在運算之後將每個元素被修改後的數值,以另外一個全新陣列最為回傳值。
   由於map其實就是對陣列元素個別巡訪,然後做某些操作之後回傳,所以可以推敲出可能的步驟如下:
1.將包含開發者修改的邏輯函式傳入map方法內。
2.先創造一個全新的陣列,用以存放運算之後的結果。
3.執行一個以陣列長度為執行次數的迴圈。
4.利用迴圈的計數值作為陣列的索引,來找到每個陣列元素。
5.取得陣列元素、逐個取得修改函式運算完的結果。
6.逐個放入運算結果值的陣列並待迴圈節結束後回傳。
有了這些運作的邏輯,我們也能夠嘗試自己實作類似map方法的邏輯,看起來可能會像是這樣:
function arrayMap(fn, array){
 let newArray = [];
  for (let i=0; i < array.length; i++){
   let result = fn(array[i]);
    newArray.push(result);
   }
   return newArray;
}
let array =[1,2,3];
let newArray = arrayMap(function(item){
   return item*2;
},array)
console.log(newArray);

試著實作陣列上的filter方法

    既然都講到這裡了,我們就來看看另一類似的filter方法。filter一樣接受一個函式來進行運算,只不過相對於map是變更陣列的個別元素,filter的目的是過濾不需要的元素,你應該也能從字面上看出來。
傳進filter的函式,會以回傳值的條件判斷,來作為陣列元素會不會被過濾掉的依據,若回傳值與布林值的true等價、可被轉型為true,那麼就保留這個元素,反之這個元素就不會出現在運算結果的陣列裡面。
而我們同樣的能夠推測filter的運作流程,預期上會與map相似:
1.將包含開發者判斷邏輯的函式傳入filter方法內。
2.先創造一個全新的陣列,用以存放運算的結果。
3.執行一個以陣列長度為執行次數的迴圈。
4.利用迴圈的計數值作為陣列的索引,來找到每個陣列元素。
5.取得陣列元素、並逐個取得判斷函式運算文成的結果。
6.依照判斷函式的結果決定是否要放入結果值的陣列,並待迴圈結束後回傳。
function arrayFilter(fn, array){
   let newArray=[];

   for (let i=0; i < array.length; i++){
      let arrayElement = array[i];
      let shouldkeep = fn(arrayElement);

      if (Boolean(shouldkeep)){
         newArray.push(arrayElement);
      }
   }

   return newArray;
}

let array = [1,2,3];
let resultArray = arrayFilter(function (element){
   return element > 1;
},array);

console.log(resultArray);

JavaScript 概念三明治(一)

物件 Object

物件是JavaScript 裡面很重要的型別,在其他語言裡的物件可能有不一樣的意思,但在JavaScript裡面,物件就是「一連串鍵與值配對(key-value pairs)的組合」。換句話說,這意味著物件是由一個個的名稱與一個個的對應內容聚集在一起後形成的結果,這個物件的鍵名又被稱為物件的屬性(property),屬性的用途跟變數有一點相像,都是讓開發者在存取時有一個獨特的名稱,用以區別不同內容。

創造一個物件

一個物件最簡單的宣告方式,就是在賦予變數時,用兩個大括號,將即將宣告的物件包裹起來,並用冒號來區分物件內的屬性名稱與想要存放的數值內容。範例如下:

物件屬性的存取

一般的變數在宣告並被賦值後,我們就用該變數名稱來表示要存取對應內容。而這個變數如果存取的內容是物件,則可以搭配中括號「[]」,或是小數點符號「.」來拿到物件裡面某個屬性的內容:

anObject["key1"] --> 用中括號來存取物件屬性的內容
abObject.key1 --> 用小數點符號來存取物件屬性的內容

陣列 Array

陣列在許多程式語言都會出現。它跟物件有一點點像,但是使用的方式有些差異。如同前面提到的,如果物件是一連串的名稱與對應內容組合而成的型別,那麼陣列就是「把一系列的數值放在一起形成的組合」。意即可以先把陣列看成沒有屬性的物件,只是陣列並非像物件那樣是用大括號來宣告。
創造一個陣列
使用中括號就能簡單宣告出一個陣列,並在中括號內用逗點區隔想要存放的各個值,與物件一樣,陣列可以存放多於一個以上的數值。

var array = ["內容一", "內容二"];

與物件屬性可以存放的內容相同,對於陣列裡面可以存放的內容,沒有型別上的限制,想要在陣列裡面放數字、字串、布林、物件或甚至是另外一個陣列,都是可以的。通常存放陣列裡面的數值,我們會用「元素」(element)來稱呼。
存取陣列的元素
存取陣列元素的方法跟物件一樣,可以使用中括號來表示。但是陣列不像物件那樣有明確的屬性可以知道要對哪一個內容作存取,該可麼辦呢?在陣列裡面的處理方式是,既然沒有明確的名稱,那麼就從第一個元素開始,給每一個元素一個足以區別內容的號碼,我們稱之為「索引」(index),陣列的這個號碼,是從0開始算的。
存取陣列的長度
陣列元素的長度可以用.length來取得,它的用法就跟存取物件屬性一模一樣。事實上,陣列背後的實作也是以物件來達成,只是使用的方式不一樣而已。

var array = ["1", "2", "3"];
console.log(array.length);
//3

函式宣告

函式最簡單的使用方式是透過function這個關鍵字來告訴JavaScript你想要宣告的函式。與變數一樣,函式在一般情況下也必須具有名稱才能讓JavaScript知道你想呼叫(執行)哪一個函式。
至於函式執行後回傳的結果則透過return來決定要回傳內容是什麼,而就像前面提到的一樣,return不一定要存在於函式的內容中,只不過如果在函式內沒有任何return被使用,或是有return但後面沒有任何要回傳的值,JavaScript都會自動幫你將回傳的值設置為undefined,所以可以把一個函式回傳的預設值當作是undefined

變數宣告

ES6出現了兩個新的變數宣告的關鍵字,也是目前實際產品開發中最常使用到的兩個語法,它們分別是let以及const。在前面則是用var來處理。
let
let其實與var差不多,與var不同是宣告變數的範疇、以及不能被重複宣告。  var是函式範疇,而let是區塊範疇 

let 是區塊範疇
區塊範疇相比於var的函式範疇,究竟有什麼優缺點呢?一個很明顯的好處就是,能夠更有效避免變數意外地被修改或覆蓋。
var是函式範疇
當我在全域宣告一個變數i,並在後面使用了for迴圈,for迴圈裡面的條件判斷也用到另外一個新的宣告的i,但是你會發現在for條件判斷用i其實在迴圈進行時也無意間對外面的全域變數進行了修改,最後,外面的變數i的內容就跟著變成10。而若使用const和let,這個問題就不會發生。
const和let的範疇則是一樣的,那麼const和let相較起來又有什麼差異呢?
const
const這個字本身意味著常數。它的範疇與let相同,都屬於區塊範疇,但是與let布一樣的是,使用const宣告變數在第一次宣告,並被指派後,就不能修改。也就是說,使用const所宣告的變數,只能夠被讀取。
使用const宣告變數時,一定要跟著一起指派內容。它與var或let不一樣,可以指宣告變數名稱而不給值。若你使用const時這麼做的話,就會出現錯誤。
而上面講到const所宣告的變數無法再重新指派的規則,其實有一個看起來像是例外的情況,那就是當const與物件、陣列一起使用時,會有這樣的情形發生:
看到了嗎?使用const所宣告的變數內容若是物件,則該物件裡的屬性還是能夠被存取跟修改,這看來很像是在對該變數內容作修改,其實並沒有。當變數被宣告並且存放的數值是一個物件時,所帶給變數的並不是物件本身的內容,而是物件存放的記憶體位置。
所以只要該變數所參考的記憶體位置保持一致,就不會有問題。而換另外一個角度來看,若我重新對const所宣告的變數指派另一個物件,那是會報錯的。