你懂 JavaScript 嗎?#2 暖身 (๑•̀ㅂ•́)و✧ Part 1 - 運算子、運算式、值與型別、變數、條件式、迴圈
09 Oct 2018本文主要內容為程式設計簡介,在此可看到在初學階段所必須理解的各種專有名詞。
以下一一仔細跟大家說明 ( ゚ ∀ ゚)o 彡
程式碼(Code)
程式(program)又稱原始碼(source code)、程式碼(code),用來表示一群執行特定工作的指令,也可說是述句組成的集合。
語法(Syntax)
規範有效指令與組合的規則,稱為電腦語言(computer language)或語法(syntax)。
可想成若希望能編寫電腦可懂的語言,就必須遵循一套規則來撰寫,而這個規則就是語法,就和我們平常溝通所說的語言的文法是一樣的。
述句(Statement)
會執行特定工作的字詞、數字或運算子(operator)組合,即是述句(statement),例如:a = b + 1
就會執行 b + 1
並將結果指定給 a。
字面值(Literal Value)
獨立存在的值,沒有存在於任何變數中,例如:a = b + 1
中的 1。
直譯器(Interpreter)與編譯器(Compiler)
直譯器與編譯器可將程式碼由上到下逐行轉為電腦可懂的命令。
其差別在於時機點
- 直譯(interpret):在程式執行「時」做轉換。
- 編譯(compile):在程式執行「前」做轉換,然後會產出編譯後的指令,因此之後執行的是這個編譯後的結果。注意,JavaScript 引擎會在每次執行前即時編譯程式碼,接著立刻執行編譯後的指令。
運算子(Operators)
對變數或值進行操作的字元,例如:a = b + 1
中的 =
和 +
。
JavaScript 有以下幾種運算子。
- 指定運算子(assignment):其實就是等號運算子(
=
)來進行「指定」的工作,當計算完畢等號右邊的值後,接著將結果放進等號左邊的變數,這個放進去的動作就是指定。例如:a = b + 1
,就是將b + 1
的結果放到 a。 - 算數運算子(math):進行加(
+
)減(-
)乘(*
)除(/
)的運算,例如:b + 1
。 - 複合指定運算子(compound assignment):
+=
、-=
、*=
和/=
,將算術運算子和指定運算子結合在一起,例如:a += 1
,等同於a = a + 1
。 - 遞增運算子(increment/decrement):
++
(遞增)與--
(遞減),例如a++
,等同於a = a + 1
。 - 物件特性的存取運算子(object property access:):利用
.
(點記號法,dot notation) 或[ ]
(方括號記號法,bracket notation) 的方式存取物件的特性,例如:obj.a
或obj['a']
,.
因為簡單便利較常使用,但[ ]
卻可在索引值是變數或有特殊字元時能保證完成值的存取,例如:obj['h e l l o']
(有空白)、obj['#$%^&']
(特殊字元)、obj['123']
(開頭為數字)。若想了解命名規則,待變數命名的部份會再詳述。 - 相等性運算子(equality):可分為
==
(寬鬆相等)、===
(嚴格相等)、!=
(寬鬆不相等)、!==
(嚴格不相等),主要差異是在做值的比較時是否會做強制轉型,待下文相等性的部份再做解釋和舉例。 - 比較運算子(comparison):
<
(小於)、>
(大於)、<=
(小於等於)、>=
(大於等於),例如:a > b
表示比較 a 是否大於 b。注意,比較的結果一定會是布林值,待下文不等性的部份再做解釋和舉例。 - 邏輯運算子(logical):
&&
(and)、||
(or),例如:a || b
表示選擇 a 或 b,常用於表達複合條件、設定初始值。 - 位元運算子(bitwise):將運算元當成 32 位元的 0 和 1 來看待,位元運算子將運算元以二進位的方式處理,接著以 JavaScript 數字型態回傳結果。例如:
5 & 1
會被看成0101 & 0001
,得到結果 0001,回傳 1 。點此看更多範例。
5 & 1; // 1
- 字串運算子(string):
+
可串接兩字元,並回傳結果,通常用於連接變數與字串。不過目前都改用 ES6 的字串模板(string template)了,使用${ variable_name }
即可代入變數,而不需再用+
與雙/單引號拼湊字串,方便許多,範例如下。
const name = 'Summer';
// 使用字串運算子
const greetings_1 = 'Hello ' + name + '!'; // "Hello Summer!"
// 使用字串模板
const greetings_2 = `Hello ${name}!`; // "Hello Summer!"
- 條件(三元)運算子(conditional / ternary):條件(三元)運算子接受兩個運算元作為值且一個運算元作為條件。語法是:
條件 ? 值1 : 值2
。若「條件」為 true,運算子回傳「值 1」,否則回傳「值 2」。如下,條件count <= 0
得到 false,因此得到 prompt 為「還有存貨」。
const count = 10;
const prompt = count <= 0 ? '全部賣完了' : '還有存貨';
prompt; // "還有存貨"
- 逗點運算子(comma):
,
用來隔開多個運算式並由左至右循序執行,最後會回傳最右邊的運算式的結果,通常用於 (1) for 迴圈內部,讓多個變數能在每次迴圈中被更新;(2) 變數宣告。
for (let i = 0, j = 10; i < j; i++, j--) {
console.log(`i: ${i}, j: ${j}`);
}
// i: 0, j: 10
// i: 1, j: 9
// i: 2, j: 8
// i: 3, j: 7
// i: 4, j: 6
- 一元運算子(unary):一元運算是只需要一個運算元的運算。例如:delete 運算子可刪除(隱式宣告的)物件、物件的(非內建)屬性或陣列中經由指定索引而找到的物件。其他的一元運算子還有 typeof 等。
const x = 1;
y = 2;
const product = {
name: 'apple',
count: 100,
};
delete x; // false
x; // 1
delete y; // true
y; // Uncaught ReferenceError: y is not defined
delete product.count; // true
product; // {name: "apple"}
更多如下圖,圖片來源:Summary of all unary operators。
- 關係運算子(relational):關係運算子比較兩運算元並根據比較結果回傳布林值。例如:in 運算子可得知特定屬性是否存在於物件中、instanceof 可用來判斷是否為指定的物件型別。
in 運算子的範例。
const product = {
name: 'apple',
count: 100,
};
'name' in product; // true
'valid' in product; // false
instanceof 運算子的範例。
product instanceof Object; // true
product instanceof Array; // false
運算式(Expression)
對「某個變數或值,或以運算子結合起來的一組變數或值」的參考(reference)。例如:a = b + 1
這個述句中有四個運算式,分別是 1、b、b + 1
、a = b + 1
,其中 1 是字面值運算式(literal value expression)、b 是變數運算式(variable expression)用來取得變數的值、b + 1
是算術運算式(arithmetic expression)用來進行加法運算、a = b + 1
是指定運算式(assignment expression)用來將結果指定給變數存起來。這裡順道一提「呼叫運算式(call expression)」,意即函式呼叫運算式本身,例如:alert(a)
。
題外話,曾幾何時我以為當工程師超酷炫的,應該都像 Daisy Johnson 一樣這麼殺吧。
當年 Daisy Johnson 只是個駭客不打壞人只打電腦時就已經這麼迷人了 (๑╹◡╹๑)
圖片來源:5 reasons why Skye from ‘Agents of S.H.I.E.L.D.’ is a Mary Sue and why it hurts the show
好了好了,夢醒了,趕快繼續幹活 (  ̄ 3  ̄)y▂ξ
值與型別(Values & Types)
型別,指的是「值的不同的表示法」,主要分為兩種「基本型別」(primitives,即 number、string、boolean、null、undefined、symbol)和「物件型別」(object)。
基本型別(Primitive Types)
- number 數字,例如:12345。
- string 字串,例如:
'Hello World'
。 - boolean 布林,例如:true、false。
- null
- undefined
- symbol
物件型別(Object Types)
除了基本型別外的資料型別都是物件,物件型別又分以下子型別
- array 陣列:使用數值化索引來儲存值,而非如物件是使用屬性來儲存值。
- function 函式:一個函式是指一段具名的程式碼片段,我們可藉由呼叫其名稱來執行它,可簡化重複進行的工作會包裝特定功能的程式碼,並且,函式可接受參數、回傳值。這裡會牽涉到另一個概念「範疇(Scope)」,範疇是指一群變數或這些變數如何透過名稱來存取的規範而組成的一個集合,關於範疇之後會再詳述。
function sayHi(name) {
console.log(`Hi, I am ${name}`);
}
sayHi('Jack'); // Hi, I am Jack
- date 日期
在型別之間進行轉換(Converting Between Types)
我們有時候會需要將資料在不同型別間轉換,例如,遇到在表單中輸入一連串的金額(此時是字串),接著計算總金額時就會希望將這些字串轉成數字來做加減乘除的操作,這時候就可能需要做轉型,從字串轉成數字。
例如,小明在蝦 X 買了一件商品準備在母親節送給媽媽,並選擇貨到付款。
圖片來源:母親節送地方媽媽小黃瓜「煮菜啦!」
來看看商品金額、運費和總金額,商品金額(product)是 100(以字串型態存在),運費(shipment)也同樣是 100(以數字型態存在),這時候總金額 total 是?
const product = '100';
const shipment = 100;
咦?怎麼會是 100100!
const total = product + shipment; // 100100
原來是因為若兩值資料型別不同,當其中一方是字串時,+
所代表的就是字串運算子,而將數字強制轉型為字串,並連接兩個字串。解法就是使用 Number 強制轉型(coerce),將字串的部份轉為數字就可以做數學運算了。
const product = '100';
const shipment = 100;
const total = Number(product) + shipment; // 200
又,除了強制轉型,來看在比較兩個非相同型別的值的時候會發生隱含的(implicit)轉型。
例如,小明想要比較買這個商品付這個運費划算嗎?運費該不會比商品還貴吧?
product === shipment; // false
product == shipment; // true
咦?一個是字串、一個是數字,是怎麼能做比較?這是由於 JavaScript 偷偷做了(隱含的)轉型的原因。那…到底做了什麼呢?
product === shipment
:不做轉型,因此型別對比較是有影響的。product == shipment
:會強制轉型,規則是 (1) 布林轉數字;(2) 字串轉數字;(3) 使用valueOf()
將物件取得基本型別的值,再做比較。關於強制轉型的詳細說明,之後(/2018/10/15/coercion/)會再詳述,或參考規格。
小明看到商品價格與運費居然相等,還是先湊個免運再買吧!
typeof
typeof 可用於檢測值的型別是什麼。
typeof 'Hello World!'; // 'string'
typeof true; // 'boolean'
typeof 1234567; // 'number'
typeof null; // 'object'
typeof undefined; // 'undefined'
typeof { name: 'Jack' }; // 'object'
typeof Symbol(); // 'symbol'
typeof function () {}; // 'function'
typeof [1, 2, 3]; // 'object'
typeof NaN; // 'number'
這裡會看到幾個有趣的(奇怪的)地方…
- null 是基本型別之一,但
typeof null
卻得到 object,而非 null!這可說是一個 bug,可是若因為修正了這個 bug 則可能會導致很多網站壞掉,因此就不修了! - 雖然說 function 是物件的子型別,但
typeof function() {}
是得到 function 而非 object,和陣列依舊得到 object 是不一樣的。 - NaN 表示是無效的數字,但依舊還是數字,因此在資料型別的檢測
typeof NaN
結果就是 number,不要被字面上的意思「不是數字」(not a number)給弄糊塗了。另外,NaN 與任何數字運算都會得到 NaN,並且 NaN 不大於、不小於也不等於任何數字,包含 NaN 它自己。
看到這裡是不是覺得 JavaScript 很難懂呢? (/‵Д′)/~ ╧╧
內建方法(Built-In Type Methods)
意即物件以屬性(或稱方法)的形式對外提供的行為,例如
const prompt = 'Hello World';
prompt.length; // 11
這背後的原因是每個型別基本上都會有其物件包裹器(object wrapper,又稱原生 natives)的型態做對應使用,例如資料型別 string 的物件包裹器型態就是 String,而就是這個包裹器型態在其原型(prototype)上定義了許多屬性和方法,因此這些資料型態就能如物件般擁有屬性和方法以供使用。
相等性與不等性
相等性(Equality)
關於相等性的運算子有四個「==
」(寬鬆相等性 loose equality)、「===
」(嚴格相等性 strict equality)、「!=
」和「!==
」。寬鬆與嚴格的差異在於檢查值相等時,是否會做強制轉型,==
會做強制轉型,而 ===
不會。
const a = '100';
const b = 100;
a == b; // true,強制轉型,將字串 '100' 轉為數字 100
a === b; // false
另外,關於值的儲存方式有傳值「pass by value」和傳址/參考「pass by referece」兩種,其中 pass by value 又可再細分是否為「pass by sharing」(可參考這篇),基本型別是傳值,而物件型別是傳址。當比較兩物件時,比較的是儲存的位置,因此看起來是相同的物件,但比較結果卻是不相同的。
const a = [1, 2, 3, 4, 5];
const b = [1, 2, 3, 4, 5];
a == b; // false
不等性(Inequality)
關於不等性的比較運算子有 >
、<
、>=
、<=
共四種,在這裡有幾種狀況需要注意
- 若比較的值都是字串,則以字典的字母順序為主。
'ab' < 'cd'; // true
- 若比較的值型別不同,由於值的不等性比較沒有嚴格不相等這種情況,因此,無論什麼樣的比較都會被強制轉型為數字,無法轉為數字的就會變成 NaN。
'99' > 98; // true,字串 '99' 被強制轉型為數字 99
'Hello World' > 1; // false,字串 'Hello World' 無法轉為數字,變成 NaN
'Hello World' < 1; // false
'Hello World' = 1; // false
- NaN 不大於、不小於、不等於任何值,當然也不等於自己。
NaN > NaN; // false
NaN < NaN; // false
NaN === NaN; // false
NaN == NaN; // false
看到這裡如果覺得 JavaScript 很難學,莫慌莫急莫害怕,推薦一本好書給你。
轉行賣雞排吧你!
程式碼註解(Code Comments)
//
和 /* ... */
。
這陣子我們都看到一則新聞「碼農槍擊了 4 名同事導致一人情況危急」,程式碼註解有多重要就不用再提了吧!
變數(Variables)
變數是儲存值的地方,又稱為符號佔位器(symbolic placeholder),例如:a = b + 1
中的 a 和 b。變數的作用是「管理程式的狀態」,讓我們能將程式中各種會變動的狀態(也就是值)存起來並搭配運算子組成運算式做一些運算。注意,JavaScript 是弱型別的語言,意即宣告變數、賦值後仍可改變值的資料型別。
// 用 var 宣告一個物件
var product = {
name: 'apple',
count: 100,
};
// 用 let 宣告一個有作用域限制的變數,範圍限於大括號內
let name = 'Nina';
常數(Constants)
ES6 使用 const
來宣告常數,代表這變數的值不會改變,在嚴格模式下還會報錯。
const PI = 3.14;
命名規則
變數的命名必須要為有效的識別字,何謂有效?就是必須以 a-z、A-Z、$
(錢字號)或 _
(底線)開頭,之後可加上 a-z、A-Z、$
(錢字號)、_
(底線) 和數字 0-9,並且不可以是關鍵字或保留字。變數的命名規則同樣也適用於物件特性的命名,只是物件特性的名稱可為關鍵字或保留字。
var happy = 'happy'; // 這是合法的
var @@ = 'sad'; // 這是不合法的,報錯「Uncaught SyntaxError: Invalid or unexpected token」
var obj = {
// 這是合法的,物件的特性可使用關鍵字或保留字命名
this: 'this is an object',
};
什麼是關鍵字?又什麼是保留字?
「關鍵字」(keyword)是指在目前 ECMAScript 中有特定用途的英文字詞,而「保留字」(reserved word)則是系統留用,雖然目前尚未用到但未來可能有其他用途的字彙。
再次強調,不管是關鍵字或保留字都不能做變數名稱使用。
區塊(Blocks)
區塊是由一對大括號(curly-brace pair,{ ... }
)所規範出來的範圍。
if (count > 10) {
// 區塊範圍在此...
}
條件式(Conditionals)
表達條件式的方法有 if 述句、switch 述句、條件(三元)運算子和迴圈,以下分別述之。
- if 述句,括號內的即是條件,若條件為真,則做指定的事情。括號內的條件放置運算式,運算結果是布林值 true 或 false,若非布林值就會強制轉型(例如:0 或空字串會被轉為 false,而其他就會轉為 true,規則下文會再詳述)。範例如下,若商品金額大於運費,就買;否則就不買。
const product = '100';
const shipment = 100;
const total = Number(product) + shipment; // 200
if (product > shipment) {
console.log('Buy it!');
} else {
console.log('Do not buy it!');
}
結果得到「Do not buy it!」。
- switch 述句等同於 if-else 的縮寫,依靠 break 來決定是否要持續進行下一個 case 述句,若沒有 break 就會「落穿而過」。範例如下,這裡有一個檢測庫存的簡易範例,假設目前庫存數量為 50,當庫存為 0 ~ 2 時提示要趕快進貨補庫存,庫存到達 50 時顯示庫存充裕,庫存到達 100 時提示貨品是不是賣不掉,其他狀況都顯示為運作正常。
const count = 50;
switch (count) {
case 0:
case 1:
case 2:
console.log('快賣完了!趕快進貨!');
case 50:
console.log('庫存充裕');
case 100:
console.log('是不是賣不掉了!?');
default:
console.log('運作正常');
}
但出乎意料的是,結果印出「庫存充裕、是不是賣不掉了!?、運作正常」。
庫存充裕
是不是賣不掉了!?
運作正常
這是因為如果沒有加入 break,一旦某個符合條件了,接下來的 case 無論符合與否都會被執行,也就是剛才所提到的「落穿而過」。
加入 break 修正一下。
const count = 50;
switch (count) {
case 0:
case 1:
case 2:
console.log('快賣完了!趕快進貨!');
break;
case 50:
console.log('庫存充裕');
break;
case 100:
console.log('是不是賣不掉了!?');
break;
default:
console.log('運作正常');
}
結果印出
庫存充裕
- 條件(三元)運算子(conditional / ternary)。
- 迴圈(loops)使用條件式來判斷迴圈是否繼續或停止。
Truthy & Falsy
在 JavaScript 中會被轉為 false 的值有
""
空字串- 0, -0, NaN
- null
- undefined
- false
而除了以上之外,都會被轉為 true,舉例如下
'Hello World'
非空字串- 42 非零的有效數字
[], [1, 2, 3]
陣列,不管是不是空的{}, { name: 'Jack' }
物件,不管是不是空的function foo() {}
函式- true
如果真的很不確定到底會轉成什麼,可以使用 !!
做測試
!![]; // true
!!{}; // true
!!NaN; // false
迴圈(Loops)
重複一組動作,直到檢測條件不成立為止。迴圈的形式有很多種,最常用的就是 while 迴圈(while 或 do…while)和 for 迴圈兩種。
while 迴圈
while 迴圈的構成有以下要素:測試條件和區塊,而每次執行區塊時就稱為一次迭代(iteration)。
while
vs do...while
兩者的差異在於 while
是先測後跑,而 do...while
是先跑後測。
來看第一個簡單的例子,。假設商品數量目前有五個,每賣掉一個就將庫存減一,當全賣完(及庫存為零)的時候就跳出迴圈,並印出「全部賣完了」的訊息。
let product = 5;
while (product > 0) {
console.log('買一個!');
product--;
console.log(`現在還剩 ${product} 個。`);
}
console.log('全部賣完了');
但下面這個例子就超有不同了,此時更改商品數量為零,剛剛提到 while 是「先測後跑」,因此當檢驗測試條件時,發現 product > 0
得到 false,也就不會進入區塊了,直接印出「全部賣完了」的訊息。
let product = 0;
while (product > 0) {
console.log('買一個!');
product--;
console.log(`現在還剩 ${product} 個。`);
}
console.log('全部賣完了');
再來看 while...loop
,這個例子並無異狀,跟第一個例子所得到的結果完全相同。
let product = 5;
do {
console.log('買一個!');
product--;
console.log(`現在還剩 ${product} 個。`);
} while (product > 0);
console.log('全部賣完了');
但是…剛剛提到 while...loop
是「先跑後測」,我們又將商品商品數量改為零,
此時會先進入區塊,依序印出「買一個!」、商品數量減一、顯示「現在還剩 -1 個。」,最後才檢驗測試條件,終止迴圈的執行,印出「全部賣完了」的訊息。
let product = 0;
do {
console.log('買一個!');
product--;
console.log(`現在還剩 ${product} 個。`);
} while (product > 0);
console.log('全部賣完了');
break
使用 break 跳出迴圈。
範例如下,在 product 為 2 的時候跳出迴圈。
let product = 5;
while (product > 0) {
console.log('買一個!');
product--;
console.log(`現在還剩 ${product} 個。`);
if (product === 2) {
console.log(`停停停,不要賣了!快進貨啊。`);
break;
}
}
console.log(`停賣後還剩 ${product} 個。`);
continue
使用 continue 跳過本次迭代,迴圈依舊持續進行。
範例如下,在 product 為 2 的時候忽略之後要執行的 console.log(
現在還剩 ${product} 個。);
,直接進入下一次迭代。
let product = 5;
while (product > 0) {
console.log('買一個!');
product--;
if (product === 2) {
console.log(`第二個我要暗摃起來`);
continue;
}
console.log(`現在還剩 ${product} 個。`);
}
console.log('全部賣完了');
for 迴圈
for 迴圈有三個子句-初始化子句、條件測試子句、更新子句。
- 初始化子句,例如:
let product = 5
- 條件測試子句,例如:
product > 0
- 更新子句,例如:
product--
for (let product = 5; product > 0; product--) {
console.log('買一個!');
console.log(`現在還剩 ${product} 個。`);
}
console.log('全部賣完了');
回顧
看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到…
- 基本名詞解釋,例如:什麼是程式碼、語法、述句、字面值、直譯器與編譯器的差異、區塊、註解。
- 運算子(operator)可用來進行操作,而運算式是對「某個變數或值,或以運算子結合起來的一組變數或值」的參考。
- 值(value)與型別(type)可用來進行不同種類的動作,例如:使用 number 的數學運算、使用 string 來顯示提示訊息、函式(function)可用來組織程式碼成為可重用的片段。。
- 變數用來保存程式的狀態,也就是儲存值。
- 條件式(condition)可用來做判斷,例如:if、switch。
- 迴圈(loop)可用來進行重複的工作,直到特定條件不成立為止。
References
- You Don’t Know JS: Up & Going, Chapter 1: Into Programming
- You Don’t Know JS: Up & Going, Chapter 2: Into JavaScript
- 運算式與運算子
- 深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?
同步發表於2019 鐵人賽。