[ 學習筆記系列 ] 很基礎的 JavaScript 入門 (一) - 基礎程式


Posted by ClayGao on 2020-03-20

前言

這是將之前的筆記整理上部落格的第二篇。

其實要放上筆記,心臟也要有點大顆,因為不知道自己紀錄的過程中,哪裡可能有錯。有時候也會覺得,筆記寫這麼多,以後要是在職場上犯了自己筆記過的錯誤,搞不好會被同事吐槽。

但我想這就是寫筆記的目的,因為不是要拿筆記來教人,而是幫自己建立一個小資料庫,只是它剛好是公開的而已。

嗯,這樣想就好。

這篇主要內容為 Lidemy 實驗導師計畫 Week2 的學習筆記,是很基礎的程式入門,也就是變數宣告、迴圈與判斷式等等。


基礎 JavaScript 入門 (一)

變數與型別 ( varible )

變數名稱

  1. 不可以用數字開頭
  2. ( 淺規定 ) 命名請與該變數用途有所相關
  3. ( 淺規定 ) 使用駝峰式命名較為普遍
  4. 如果變數沒有賦值 ( = ),會是 undefined (letconst 會有特例)

型別 ( typeof() )

  1. number
  2. string
  3. boolean
  4. null
  5. undefinded
  6. symbol ( 自己找資料補充的 )
  7. object ( 囊括 object / array / function / date)

其中 1 - 6 又屬於基本型別 ( Primitives ),7 屬於物件型別 ( Object )

( null 的類型使用 typeof() 會顯示為 object,這個錯誤算是 JS 的老生常談 )

型別的轉換 ( 使用範例:設立一個變數 a )

  1. 轉換為 number:使用 Number(a) / 使用 parseInt(a , 10) // 10 為 10 進位之意
  2. 轉換為 String:使用 toString(a),或者 a + ''( 空字串 ),原理是在 JS 裡,數字加字串會被變成字串

浮點數誤差

浮點數誤差:JS 沒辦法準確的儲存小數,至少不是每次都有辦法,所以假設:

var a = 0.1

但實際上很可能會是

a = 0.100000000000000000003

所以 a 在做基本運算時可能無法準確,比如 a + 0.2 === 0.3 會是 false

補充連結:http://blog.dcview.com/article.php?a=VmhQNVY%2BCzo%3D

基本型別(Primitives)不可變

var a = 'origin'

a = 'changed'

a 雖然被重新賦予字串 'changed',但 'origin' 並沒有被抹除。
'change''origin' 都有各自的記憶體位置,這邊的關鍵點是「=」 賦值

a = 'change' 這個動作,是我將 a 重新指向了 'change',原本的 'origin' 是依舊存在的,'origin' 並沒有被更改

所以不可變的意思是 'origin' 這個字串,既然已經存在,我們就無法對它進行更動

var a = 'origin'

a.toUpperCase()

console.log(a) // 仍然印出 origin ,而非 ORIGIN

假設 'origin' 的記憶體位置是 0x01,那 0x01 所指的 'origin' 就永遠只能是 'origin',無法使用方法對其做改變。

toUpperCase() 方法的確生成了 'ORIGIN',但需要有一個變數接住生成的 'ORIGIN',由於基本型別不可變,所以 'origin' 沒有被改變,a 也沒有被重新賦值,下面這個寫法差別就是在我讓生成的 'ORIGIN' 重新賦值給 a,所以 a 才改變。

var a = 'origin'

a = a.toUpperCase() // 習慣這樣寫

console.log(a) // 印出 ORIGIN

看到 a = a.toUpperCase() 這一行的「=」了嗎 ? 上述的做法是將 'origin' 轉變為大寫之後的樣子,也就是 'ORIGIN' 重新賦值給 a

而按照之前所描述的,'origin' 與其所屬的記憶體位置 0x01 並沒有消失,也就是說小寫 'origin' 與大寫 'ORIGIN' 此時都是存在的。

a 被賦值字串 'origin' 或其他的基本型別,你當然也可以更改 a 指向的值,而你只能通過重新賦值「=」將 a.toUpperCase 回傳的值放入 a 之中,而原本的 'origin' 沒有被改變,記憶體位置也沒有變。

再提醒一次,這是基本型別(Primitives),物件型別不在此討論範圍內

物件型別的內建函式有些可能會改變原本的物件或陣列,有些不會改變而回傳新的值,有些會改變原本的物件或陣列,同時也會回傳新的值。

而基本型別一律不改變原來的值,因為基本型別(Primitives) 不可變,所以相關的方法一律都是回傳新的值,這就是為何需要寫成 a = a.UpperToCase() 的原因

參考資料 :


變數的基本運算

基本運算子

運算子 意義
+
-
*
/
% 除以並取餘數
++ 相當於+1
-- 相當於-1
> / >= 大於 / 大於等於
< / <= 小於 / 小於等於
=== 等於

補充 a++ / ++a 的差異( 以變數 a 為例 )

var num;
var a = 1;
num = a++ // 先運算 num = a ,再運算 a = a + 1 ( a++ ),所以 num 為 1
num = ++a // 先運算 a = a + 1 ( ++a ),再運算 num = a,所以 num 為 2

參考資料:https://blog.csdn.net/qq_34471736/article/details/54599901

關於 = 與 == 和 ===

類型 意義
= 將 = 右邊的值賦予給 = 左邊的變數
== 比較運算子,比較內容是否相等
=== 比較運算子,比較內容與「型別」是否相等

總結:永遠使用 === 做比較運算,Eslint 也會強迫使用之

  • 將物件放入變數 - 理解值與記憶體位置 ( 重要觀念 )

      1 === 1 // ture
      'abc' === 'abc' // true
    
      [] === []  // false
      [1] === [1] // false
      {} === {} // false
      {a:1} === {a:1} // false
    

    上述陣列和物件不相等的原因,乃是當陣列和物件在做比較判斷時,比較的是「記憶體位置」,而非值

當你在程式碼輸入一個物件或陣列時(即使還沒放入 key / value),電腦會替你輸入的這個 Object,指派一個「記憶體位置」,假設為 0x01(儘管在 JS 中你無法得知這個記憶體位置為多少),因此在你比較兩個相同值的 Object,實際上你比較的是 0x01 與 0x02,所以會不同。

[1] === [1] // 等同 0x01 === 0x02 這樣的比較
{a:1} === {a:1} // 等同 0x03 === 0x04 這樣的比較

那指定入變數之後,會發生什麼事 ?

先假設 {a:1} 的記憶體位置為 0x01

var abc = {a:1} 
// 電腦分配一個記憶體位置給 {a:1},假設其為 0x01,你將 0x01 賦予 abc

所以,實際上的意義為:

abc 存放的記憶體位置 0x01 == 指向 ==> {a:1}

我們來看第二個例子:

var abc = {a:1} 
// 電腦分配一個記憶體位置給 {a:1},假設其為 0x01,你將 0x01 賦予 abc

var abc2 = abc
// 你是將 abc 內中的 0x01 賦予 abc,而非 0x01 所放置的值 {a:1}

abc2.a = 2 
//abc2 的記憶體位置和 abc 同樣都是 0x01,而你也知道 0x01 這個位置放著值 {a:1},你透過 0x01 修改 {a:1} 的內容使 a:2

console.log(abc) 
// 回傳 {a:2},如果懂前面三個步驟你可以知道即使你修改的 abc2.a,但實際上你也是透過 0x01 做修改

console.log(abc === abc2)
// 印出 true,因為記憶體位置相同

第三個例子,如果這時候 abc2 = {b:2},那 {a:1} 會不會被更動 ?

var abc = {a:1} // 和第四行是一樣意思,記得我是「電腦分配一個記憶體位置給 {a:1},假設其為 0x01,你將 0x01 賦予 abc」
var abc2 = abc 
abc2 = {b:2} // 請回去看第一行
console.log(abc) // 印出 {a:1},代表沒事

{a:1} 當然不會被更動 ! 因為 {b:2} 本身就有一個新的記憶體位置,假設其為 0x05,當 abc2 = {b:2} 的時候其實是代表「電腦分配一個記憶體位置給 {b:2},假設其為 0x05,那麼你是將 0x05 賦予 abc2 這個變數之中」

所以當然完全不關 0x01 和 {a:1} 的事情了

補充我自己想到的第四個例子,如果 abc2 被賦予的不是 {b:2} 而一樣是值 {a:1} 呢?

var abc = {a:1} 
var abc2 = abc 
abc2 = {a:1} 
console.log(abc2)  // 回傳 {a:1}
console.log(abc === abc2) // 回傳 false,這是當然,因為 abc2 內中的 {a:1} 是另外一個 {a:1},不是 0x01 的那個 {a:1}

總結:當你指派一個物件放入一個變數的時候,你放入的是一個記憶體位置。而該物件的記憶體位置是一個物件對上一個位置,再透過這個位置去找到這個物件內的值,假設你要 call 一個物件,過程大概會是這樣:

call => 物件的記憶體位置 => 物件裡的值

將物件放入變數,是將物件的記憶體位置放入變數,所以 calling 過程會變以下

call => 變數 => 變數裡的物件的記憶體位置 => 物件裡的值

陣列與物件 ( 這邊的心得將簡易帶過已經懂的部分,標註重點 )

陣列

  • 屬於物件型別,所以其屬於可變的,其相關的內建函式有些會改變原陣列,有些不會,也不一定都會回傳值
  • 儲存於變數時,實際儲存的是記憶體位置
  • 陣列內部可以再放入陣列
  • arr.indexOf() 方法可以可以回傳 () 內的值首次在陣列中出現的位置索引,若沒有出現,回傳 -1
  • 承上,陣列 arr 中最後一位索引值將會等於 arr.length - 1

物件

  • 注意形式 -{ key : value, key2 : value }
  • 屬於物件型別,所以其屬於可變的
  • 儲存於變數時,實際儲存的是記憶體位置
  • 物件內部可以再放入物件或放入陣列
  • 呼叫方法為 obj.key 或 obj['key'],注意不是 obj[key]
  • 物件裡面有物件時,可以 obj.key 後面再接 .name 呼叫之

判斷式

if / else

if (true) {} // 若 () 的結果為 true,執行 {} 內部 

else if (true) {} 
// 接在 if 之後,的結果為 true,執行 {} 內部,如還有更多的附加條件,則一樣使用 else if

else {} 
// 前述 if 和 else if 中的 () 內容為 false 時,執行 {} 中的內容

ififif ,與 if 後面接 else ifelse if 的差別在於:

以各個 if 判斷式為一個單位,只要有符合該 if 判斷式括號內的內容,皆會執行其 {} 中的內容,不會按照順序判斷。

若是 ifelse if ,那會按照順序,逐步判斷。只要先遇到符合的判斷式就直接印出該 {},不會再繼續判斷後面的 else if 與 else

最好的例子如下

var age = 17

if ( age >= 15 ) {
    console.log('高中生')
}
if (age >= 7) {
    console.log('小學生')
}
if (age >= 4) {
    console.log('幼兒園生')
}


/*
高中生
小學生
幼兒園生
*/
var age = 17

if ( age >= 15 ) {
    console.log('高中生')
} else if (age >= 7) {
    console.log('小學生')
} else if (age >= 4) {
    console.log('幼兒園生')
}

/*
高中生
*/

switch case

使用時機:對同一個變數進行多次條件判斷的時候,可以使用 switch

var a = 1

switch(a) {
    case : 1  // 判斷 a 的值是否為 1
        console.log('a 為 1')
        break // 要加入,否則會接續著印出
    case : 2
        console.log('a 為 2')
        break
    case : 3
        console.log('a 為 3')
        break
    default:   // 想像成 else ,即上述 case 都為 false
        console.log(' a 超過 3 或非 number')
}

// a 為 1

你也可以將兩個 case 合在一起,如果他們接下來的動作是一樣的話:

var a = 1

switch(a) {
    case : 1  // 判斷 a 的值是否為 1
    case : 2
        console.log('a 為 1 或 2')
        break
    case : 3
        console.log('a 為 3')
        break
    default:   // 想像成 else ,即上述 case 都為 false
        console.log(' a 超過 3 或非 number')
}

// a 為 1 或 2

但如果是要做類似的判斷,也可以考慮下面這種作法

var a = 1

var a_number = ['a 為 1', 'a 為 2', 'a 為 3']
console.log(a_number[a - 1])

即 a 若是多少,就印出什麼樣的內容,即該索引值的值,而值的內容可以表現出 a 為何。

ternary 三元運算子

condition 為 true ? 若為 true 則回傳這裡 : 若為 false 則回傳這裡

如下:

console.log(answer = 10 > 8 ? 'yes' : 'no')
// 由於 10 > 8 為 true,所以回傳 yes
//回傳 yes

也可以做巢狀判斷 (感謝 huli 修正),但不推薦,因為可讀性很差 :

var a = 9;
var answer = a > 8 ?  ( a === 10  ? "Yes, a === 10" : "No, it isnt" ) : "no"

console.log(answer); // No, it isnt

建議還是使用 if/else 做巢狀判斷

var a = 9;
var answer;

if(a > 8) {
    if( a === 10) {
        answer = "Yes, a === 10"
    } else {
        answer = "No, it isnt"
    }
} else {
    answer = "NO!"
}

console.log(answer);

雖然用一行三元運算子解決問題很潮,不過為了保護其他人的雙眼,寫得長一點但好懂一點也是好選擇。


迴圈

do while

do {
    執行
} while(判斷)

也就是先執行一次 do {},執行完後再判斷 while() 的內容,若為 true,再執行 do {} 中的內容

var i = 1 

do {
    console.log(i)
    i++
} while ( i <= 5 )

/*
1
2
3
4
5
*/

另一種方法:

var i = 1 

do {
    console.log(i)
    i++
    if ( i === 5 ) {
        break
    }
} while ( true )

/*
1
2
3
4
5
*/
  • break : 中斷,並跳出該 loop
  • continue : 中斷,直接判斷 while,若為 true,執行 do {};若為 false,跳出該 false

    continue 範例:找出陣列中的值的索引值

      var arr = [1,5,8,12,50,99,7,20]
      var i = 0
    
      do {
          i++
          if ( arr[i] === 50 ) {
              console.log('抓到了 50 ! 他在第' + i + '個 !')
              continue
          }
          console.log('不是第' + i + '個,第' + i + '個是' + arr[i])
    
      } while ( i < arr.length - 1 )
    

    印出

      不是第1個,第1個為5
      不是第2個,第2個為8
      不是第3個,第3個為12
      抓到了 50 ! 他在第4個 !
      不是第5個,第5個為99
      不是第6個,第6個為7
      不是第7個,第7個為20
    

可以看到當抓到 50 這個數之後,就不會印出接下來的 ('不是第' + i + '個,第' + i + '個是' + arr[i] ),而是中斷並直接繼續判斷 while(),繼續執行 do{}往下尋找。

while do

while(判斷) {
    執行
}

即先判斷 while,再做 do {},這個算是比較常用的,其餘與上述 do while 的部分相同。

for loop

for ( 0. 建立初始值; 1. 執行條件; 3. 執行完後做) {
    2. 執行
}

執行順序:0 -> 1 -> 2 -> -3 -> 1 -> 2 -> 3 -> 1 -> 2 ....

注意要點:

  • 步驟 1 的執行條件也有人說是終止條件,但只要理解為該條件為 true 則執行 {} 即可
  • 也可以使用 break 或 continue,continue 的運行同 while : 跳出然後直接去判斷 1. 執行條件
  • 步驟 0 的建立初始值 不一定要具備,但後面的 ; 一樣要存在。 ( ; 1. 執行條件; 3. 執行完後做)

函式

結構

function test() {

}

// function 名稱為 test
// () 中為參數
// {} 中為執行區塊
// 每個 function 一定會有一個 return,但不一定要寫

函式觀念與注意事項:

  • 變數可以用來接收函式的回傳值,使用 = 連接
  • 關於 return
  • 易犯錯誤 : 假設你用 return 回傳物件,但 return 正後方一定要接上 {},以下為錯誤示範
function a(x) {
    return 
    {
        x*2
    }
}

//undefined
  • 每個函式一定都有一個 return 值,若沒有寫 return,會預設該函式 return undeined。
  • 個人見解 : return 與 function 的關係,function 一定會有 return,但不一定有關聯
    也就是說,你在一個 function 中對參數 x 做了千百種運算,但最後可以 return 'I dont want to talk anything',也是可以的。

想像函數是一個機器,這個機器會有一個取物口輸出東西,每一次運算,這個取物口一定會輸出東西,但要輸出什麼東西,是你去決定的 ( 沒決定就會回傳 undefined )。你可以利用機器運算的結果,然後輸出跟這個結果有關的值;你當然也可以輸出一個和機器運轉過程中毫無相關的東西,如一句話 'Haha , there is nothing !'

只是當你呼叫函式機器的時候,由於呼叫的定義是要取「該函式機器的回傳值」,所以一定會取得該函式的 return,那如果你有加就會回傳你所寫的,如果沒加就會回傳 undefined

return 與 console.log 的差異

這樣說來,console.logreturn 就沒有一定的關係,這也是新手 ( 包括我以前 ) 常犯的錯,以為要看到函式運算的結果,就一定要在函式裡面放入 console.log() 印出。

事實上,當你在該函式裡面放入 console.log() 印出了你要的東西,並不是這個函式沒有 return ( 你已經知道函式一定會有回傳值了 ),單純只是我們用不到該函式的回傳值而已。

所以, console.log() 實際上就真的只是印出其 () 內部的東西罷了,並不是函式內部用 console.log() 印出結果後,就沒有 return 了,你也可以 return 運算結果,然後在外面用 console.log(func()),印出呼叫這個函式而回傳的值

最後當函式運行到 return 時,會立刻回傳值,而該函式內部 return 之後的程式碼都不會執行了,跟之前的 break 概念上是類似的。

  • 參數的命名盡量取有意義的,非必要,但相當重要,比如在框架中我們習慣以動詞開頭
  • 函式內什麼都可以放,當然也可以放其他 function
  • 呼叫函式的方法為 FunctionName()
function test(x) {
    return x*2
}

var a = test(2)

函式中的參數也可以是代入函式作為引數(極重要)

function transform(arr, transformType) {
    var result = []
    for (var i = 0; i < arr.length; i++) {
        result.push(transformType(arr[i]))
    }
    return result
}

function triple(x) {
    return x * 3
}

console.log(transform([2,5,8],triple)) // [ 6, 15, 24 ]

可以看到我使用的函式為 triple,依此類推,我可以在主函式中代入各式各樣我想要用的工具函式

用匿名函式的方式寫則是

function transform(arr, transformType) {
    var result = []
    for (var i = 0; i < arr.length; i++) {
        result.push(transformType(arr[i]))
    }
    return result
}

console.log(transform([2,5,8],function triple(x) {
    return x * 3
})) // [ 6, 15, 24 ]
  • 引數(Argument)與參數(Parameter)
  • 函式內 () 為參數
  • 呼叫內 () 為引數
  • 函式中的 arguments[],個人覺得很酷,代表函數的處理可以看引數,參數無格式限制
function abc(){
return arguments[0] +  arguments[1] +  arguments[2]
}

console.log(abc(5,1,2)) // 8

需要注意的是,arguments 是 Object 物件,而非 Array,所以在方法的使用上你沒辦法對其使用 Array 的方法。但可以對其使用索引值 ( arguments[index] ),也能用 length 方法操作。

傳參數的運作機制 ( 極重要,可與前述之將物件放入變數 - 理解值與記憶體位置做觀念連結 )

參數運作外在的變數時,不會直接改變變數,具體而言,JS 會複製一份你看不到的變數進入參數做運算,但運算之後的結果不會改變該變數

function abc(x){
    return x * 2
}

var a = 2

console.log(abc(a)) //4
console.log(a) //2

那如果 a 是物件呢 ?

function abc(x){
  x.num++
  return x
}

var a = {
    num : 2
}

console.log(abc(a)) // { num : 3 }

可以看到 num 的值被改變了,這是因為即使你是複製一份進去參數做運算,但本質上變數 a 與參數內的 x.one++ 都是基於同一個記憶體位置對上同一個值,也就是 2,所以你在參數內的運算,也會對該值做更動。

綜合觀念而言,如果重新賦予一個物件,那這個物件本身都是全新的,與現有的物件並沒有關聯,即使他們內中的屬性與值一模一樣,但由於記憶體位置不同,所指向的也不會一樣。

介紹幾種常見的宣告函式的方式

  • 基本方式
function test(x) {
    return x*2
}

var a = test(2)
console.log(a) // 4
  • 放入變數之中
var a = function (x){
    console.log(x*2)
}

a(2) // a 即 該函式,印出 4
  • 箭頭函式(ES6)
var a = (x) => {
    console.log(x*2)
}

a(2) // a 即 該函式,印出 4

第一種和第二種的宣告方式在底層似乎有些微妙的不同,這邊我先做個註記,之後有機會另開一篇研究。

而箭頭函式也會影響到 this 的指向,這邊也不在本次的討論範圍。

結語

上述的筆記是自己在很萌新的時候寫的,所以其實當時知道基本型別不可變時,還沒有想到深拷貝與淺拷貝的概念,所以在一些用詞上,難免有失精準。

但我認為初學者可以先去明白最根本的原因是什麼,才去知道說,哦,就是這個原因所以我們不好做到深拷貝,才會需要使用到 Object.assignJSON.parse(JSON.stringify),甚至是 Lodash 的 cloneDeep 來達成。

當然這邊要討論得更深也是可以的,只是好像超出自己原本預設的內容了,有機會以後再做一些小研究。

至於其他的部分都是比較基礎的程式入門,今天的筆記就到這裡了,如果有描述錯誤的地方還望讀者指正,感謝 !


#javascript #新手 #初心者 #入門







Related Posts

[BE201] 、超簡易留言板(下)

[BE201] 、超簡易留言板(下)

MTR04_1011

MTR04_1011

AppWorks School Batch #16 Front-End Class 學習筆記&心得(駐點階段四:個人專案~重構)

AppWorks School Batch #16 Front-End Class 學習筆記&心得(駐點階段四:個人專案~重構)


Comments