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

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

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

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

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

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

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

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

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

從上面的例子應該可以體會到,使用 with 固然可以節省一點程式碼,但對於程式的作用域運作可能造成混亂
例如上面的例子 1 和 3,level 都是宣告在 with 之外,卻因為是否曾經在 with 之內被使用而有不同行為,這對於程式維護安全性來說並非好事。
尤其從現實專案風險的角度來看: 1. 使用 with 能節省的程式碼量可能有限;而且簡化程式碼通常屬於「能做到最好,但不能危害到程式運作」的加分項目。 2. 如果使用 with 不慎,造成程式運作的行為不同,屬於「一旦發生,會危害到程式運作」的問題。
專案最大的課題就是權衡 (trade-off)。從以上兩點來看,毫無疑問第 2 點對專案的殺傷力遠大於第 1 點,使用 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 的建議是「將需要重複呼叫的物件暫存於一個名稱簡短的變數」就好。
例如下面這樣:
"use strict";
var m = Math;
var x = m.cos(3 * m.PI) + m.sin(m.LN10);
var y = m.tan(14 * m.E);
console.log(x); // -0.2560196630425069
console.log(y); // 0.37279230719931067

Copy link
On this page
with 的語法
with 使用於內建物件的範例
with 使用於自訂物件的範例
撞名會發生什麼事?
1. 變數是在 with 區塊之外宣告
2. 變數是在 with 區塊之內宣告
3. 變數是在 with 區塊之外宣告,但在 with 區塊之內被使用
總結 with 作用域的優先順序
with 的風險
with 掰掰~
References