Day 14:來挖挖恐龍骨 —— with 語法
JavaScript 有一個語法 with 似乎相對冷門,比較少看到被使用。
事實上連 W3Schools 的 JavaScript 教材 都沒有 with 語法教學!查了一下網路討論,with 曾經也是有人認為很好用的語法。那麼究竟發生什麼事,讓 with 像個黑歷史一樣,被 W3Schools 刻意遺忘?
Day13 介紹嚴謹模式 (Strict Mode) 的例子時提到,with 語法甚至已經在 ES5 導入嚴謹模式後被禁止使用。
那本篇去研究一個已經被淘汰的語法有什麼意義?
對,語法沒意義,你知道了也不能用 (毆)。
(Source: 豆卡頻道貼圖)
這一篇文章的定位確實有點像考古文,瞭解一下 JavaScript 曾有過這個語法。
但想要探討的不是這個語法怎麼寫,而是它為什麼會被摒棄?
(Source: 網路圖片)
一個語法或特性被特地發展出來,然後又被淘汰,一定有其缺點或原因。
瞭解這些缺點,相當於學到什麼是較被建議的程式思維。這種概念性上的收穫就像內功一樣,可能不會直接轉化成某種語法的顯性應用,但有助於隱性的程式撰寫思維。
當然,要想探討 with 的缺點,首先要先知道語法怎麼寫。

with 的語法

with 語法可以為一段程式敘述指定預設物件,用來簡化特定情形下必須撰寫的程式碼量。
語法模板如下:
1
with(<object>){
2
// statement
3
// ....
4
// ....
5
}
Copied!
以上是 with 標準的語法說明。
如果光看這種論文式的說明就知道怎麼用,你一定是百年一見的練武奇才。
(Source: 網路圖片)
沒學過如來神掌的人,請跟著導遊繼續往下看實際舉例。

with 使用於內建物件的範例

下面是一個使用 JavaScript 內建數學運算物件 Math 的例子:
1
var x = Math.cos(3 * Math.PI) + Math.sin(Math.LN10);
2
var y = Math.tan(14 * Math.E);
3
console.log(x); // -0.2560196630425069
4
console.log(y); // 0.37279230719931067
Copied!
可以發現 Math 被不斷重複呼叫,使得這一段程式碼看起來很累贅。
這時候可以利用 with,讓程式碼變得較簡潔易讀:
1
var x, y;
2
with (Math){
3
x = cos(3 * PI) + sin(LN10);
4
y = tan(14 * E);
5
};
6
console.log(x); // -0.2560196630425069
7
console.log(y); // 0.37279230719931067
Copied!
可以看到,在 with(Math){....} 區塊內,不用再逐一指定每個函式或屬性的呼叫物件,因為已經在 with 的小括號內指定了 Math 作為預設的呼叫物件。
簡單來說,當你需要對同一個物件的多個屬性或函式作操作時,就可以使用 with 來簡化你的程式碼
除了用在內建的 JavaScript 物件,也可以用在自定義的物件上嗎?
當然可以。

with 使用於自訂物件的範例

以下想對自訂物件 player 的多個屬性作操作,印出想要的資訊:
1
function showHeroStatus(hero){
2
console.log("Name: " + hero.name);
3
console.log("Level: " + hero.level);
4
console.log("Exp: " + hero.currentExp);
5
console.log("You need more " + (hero.nextLevelNeededExp - hero.currentExp) + " Exp points for Level " + (hero.level + 1) + ".");
6
}
7
8
var player = {
9
name: "OneJar",
10
level: 1,
11
currentExp: 50,
12
nextLevelNeededExp: 200
13
};
14
15
showHeroStatus(player);
Copied!
執行結果:
1
Name: OneJar
2
Level: 1
3
Exp: 50
4
You need more 150 Exp points for Level 2.
Copied!
可以看到 showHeroStatus() 的內容有點囉嗦,不斷重複對 hero 的呼叫。
可以利用 with 語法讓這段程式碼更簡潔:
1
function showHeroStatus(hero){
2
with(hero){
3
console.log("Name: " + name);
4
console.log("Level: " + level);
5
console.log("Exp: " + currentExp);
6
console.log("You need more " + (nextLevelNeededExp - currentExp) + " Exp points for Level " + (level + 1) + ".");
7
}
8
}
Copied!

撞名會發生什麼事?

看到這邊,應該可以隱約感覺到 with 語法的疑慮:如果我另外有同名的變數會發生什麼事?

1. 變數是在 with 區塊之外宣告

1
function showHeroStatus(hero){
2
var level = 99; // Function Scope
3
var money = 1300; // Function Scope
4
5
with(hero){
6
console.log("Name: " + name);
7
console.log("Level: " + level); // `with` 預設物件優先
8
console.log("Exp: " + currentExp);
9
console.log("You need more " + (nextLevelNeededExp - currentExp) + " Exp points for Level " + (level + 1) + ".");
10
console.log("Money: " + money);
11
}
12
}
Copied!
執行結果:
1
Name: OneJar
2
Level: 1
3
Exp: 50
4
You need more 150 Exp points for Level 2.
5
Money: 1300
Copied!
  • with 之內會以預設物件的屬性為優先。
  • 如果該屬性名稱不存在於物件內,會按照作用域鏈 (Scope Chain)的順序,繼續找其他變數定義。
  • 在這個例子裡:
    • level 會以 hero.level 優先。
    • hero 沒有 money 這個屬性,所以 money 會找到 Function Scope 所宣告的 money 變數。

2. 變數是在 with 區塊之內宣告

1
function showHeroStatus(hero){
2
with(hero){
3
var level = 99; // The Same Block
4
5
console.log("Name: " + name);
6
console.log("Level: " + level);
7
console.log("Exp: " + currentExp);
8
console.log("You need more " + (nextLevelNeededExp - currentExp) + " Exp points for Level " + (level + 1) + ".");
9
}
10
}
Copied!
執行結果:
1
Name: OneJar
2
Level: 99
3
Exp: 50
4
You need more 150 Exp points for Level 100.
Copied!
  • 會以with 之內的宣告變數為優先
  • 在這個例子裡:
    • 呼叫 level 時,會以區塊內的 var level = 99 優先。

3. 變數是在 with 區塊之外宣告,但在 with 區塊之內被使用

1
function showHeroStatus(hero){
2
var level = 99; // Function Scope
3
4
with(hero){
5
level = 70;
6
7
console.log("Name: " + name);
8
console.log("Level: " + level);
9
console.log("Exp: " + currentExp);
10
console.log("You need more " + (nextLevelNeededExp - currentExp) + " Exp points for Level " + (level + 1) + ".");
11
}
12
}
Copied!
執行結果:
1
Name: OneJar
2
Level: 70
3
Exp: 50
4
You need more 150 Exp points for Level 71.
Copied!
  • 雖然 level 宣告在 with 區塊之外,而 herohero.level 這個屬性名稱,但在 with 之內還是先找到變數 level

總結 with 作用域的優先順序

當遇到一個呼叫名稱時 (例如 level, money): 1. 先從 with 區塊內尋找,有使用過即可,不必是在區塊內宣告的變數。 2. 如果找不到,再從預設物件 (例如 hero) 的屬性去找同名稱的屬性。 3. 如果以上都找不到,再根據一般的作用域鏈 (Scope Chain) 繼續往外找。

with 的風險

從上面的例子應該可以體會到,使用 with 固然可以節省一點程式碼,但對於程式的作用域運作可能造成混亂
例如上面的例子 1 和 3,level 都是宣告在 with 之外,卻因為是否曾經在 with 之內被使用而有不同行為,這對於程式維護安全性來說並非好事。
尤其從現實專案風險的角度來看: 1. 使用 with 能節省的程式碼量可能有限;而且簡化程式碼通常屬於「能做到最好,但不能危害到程式運作」的加分項目。 2. 如果使用 with 不慎,造成程式運作的行為不同,屬於「一旦發生,會危害到程式運作」的問題。
專案最大的課題就是權衡 (trade-off)。從以上兩點來看,毫無疑問第 2 點對專案的殺傷力遠大於第 1 點,使用 with 所得到的效益可能遠不及它隱含的風險,因此 with 的摒棄是可以被理解。

with 掰掰~

如前面文章介紹到,在 ES5 導入的嚴謹模式已經禁止 with 語法的使用。
MDN 建議:
Using with is not recommended, and is forbidden in ECMAScript 5 strict mode. The recommended alternative is to assign the object whose properties you want to access to a temporary variable.
如果單純希望程式碼可以再簡潔一點,MDN 的建議是「將需要重複呼叫的物件暫存於一個名稱簡短的變數」就好。
例如下面這樣:
1
"use strict";
2
var m = Math;
3
var x = m.cos(3 * m.PI) + m.sin(m.LN10);
4
var y = m.tan(14 * m.E);
5
console.log(x); // -0.2560196630425069
6
console.log(y); // 0.37279230719931067
Copied!

References