前言
這是將之前的筆記整理上部落格的第二篇。
其實要放上筆記,心臟也要有點大顆,因為不知道自己紀錄的過程中,哪裡可能有錯。有時候也會覺得,筆記寫這麼多,以後要是在職場上犯了自己筆記過的錯誤,搞不好會被同事吐槽。
但我想這就是寫筆記的目的,因為不是要拿筆記來教人,而是幫自己建立一個小資料庫,只是它剛好是公開的而已。
嗯,這樣想就好。
這篇主要內容為 Lidemy 實驗導師計畫 Week2 的學習筆記,是很基礎的程式入門,也就是變數宣告、迴圈與判斷式等等。
基礎 JavaScript 入門 (一)
變數與型別 ( varible )
變數名稱
- 不可以用數字開頭
- ( 淺規定 ) 命名請與該變數用途有所相關
- ( 淺規定 ) 使用駝峰式命名較為普遍
- 如果變數沒有賦值 ( = ),會是 undefined (
let
與const
會有特例)
型別 ( typeof() )
- number
- string
- boolean
- null
- undefinded
- symbol ( 自己找資料補充的 )
- object ( 囊括 object / array / function / date)
其中 1 - 6 又屬於基本型別 ( Primitives ),7 屬於物件型別 ( Object )
( null 的類型使用 typeof() 會顯示為 object,這個錯誤算是 JS 的老生常談 )
型別的轉換 ( 使用範例:設立一個變數 a )
- 轉換為 number:使用 Number(a) / 使用 parseInt(a , 10) // 10 為 10 進位之意
- 轉換為 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 時,執行 {} 中的內容
if
接 if
接 if
,與 if
後面接 else if
接 else if
的差別在於:
以各個 if
判斷式為一個單位,只要有符合該 if
判斷式括號內的內容,皆會執行其 {} 中的內容,不會按照順序判斷。
若是 if
與 else 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
: 中斷,並跳出該 loopcontinue
: 中斷,直接判斷while
,若為true
,執行do {}
;若為false
,跳出該 falsecontinue 範例:找出陣列中的值的索引值
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.log
與 return
就沒有一定的關係,這也是新手 ( 包括我以前 ) 常犯的錯,以為要看到函式運算的結果,就一定要在函式裡面放入 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.assign
、JSON.parse(JSON.stringify)
,甚至是 Lodash 的 cloneDeep
來達成。
當然這邊要討論得更深也是可以的,只是好像超出自己原本預設的內容了,有機會以後再做一些小研究。
至於其他的部分都是比較基礎的程式入門,今天的筆記就到這裡了,如果有描述錯誤的地方還望讀者指正,感謝 !