# Day 14：來挖挖恐龍骨 —— with 語法

JavaScript 有一個語法 `with` 似乎相對冷門，比較少看到被使用。

事實上連 [W3Schools 的 JavaScript 教材](https://www.w3schools.com/js/) 都沒有 `with` 語法教學！查了一下網路討論，`with` 曾經也是有人認為很好用的語法。那麼究竟發生什麼事，讓 `with` 像個黑歷史一樣，被 W3Schools 刻意遺忘？

Day13 介紹嚴謹模式 (Strict Mode) 的例子時提到，`with` 語法甚至已經在 ES5 導入嚴謹模式後被禁止使用。

那本篇去研究一個已經被淘汰的語法有什麼意義？

對，語法沒意義，你知道了也不能用 (毆)。

![](https://i.imgur.com/OWsV1lC.png)\
(Source: [豆卡頻道貼圖](https://store.line.me/stickershop/product/1140900/zh-Hant?from=sticker))

這一篇文章的定位確實有點像考古文，瞭解一下 JavaScript 曾有過這個語法。

但想要探討的**不是這個語法怎麼寫，而是它為什麼會被摒棄？**

![](https://i.imgur.com/wWManCT.png)\
(Source: [網路圖片](https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTiDBrJIeJcp-GaSZI42hma6hssYcdsJsTQMnuntRtUAkZT4vljGA))

一個語法或特性被特地發展出來，然後又被淘汰，一定有其缺點或原因。

**瞭解這些缺點，相當於學到什麼是較被建議的程式思維**。這種概念性上的收穫就像內功一樣，可能不會直接轉化成某種語法的顯性應用，但有助於隱性的程式撰寫思維。

當然，要想探討 `with` 的缺點，首先要先知道語法怎麼寫。

## `with` 的語法

`with` 語法可以為一段程式敘述指定預設物件，用來簡化特定情形下必須撰寫的程式碼量。

語法模板如下：

```javascript
with(<object>){
    // statement
    // ....
    // ....
}
```

以上是 `with` 標準的語法說明。

如果光看這種論文式的說明就知道怎麼用，你一定是百年一見的練武奇才。

![](https://i.imgur.com/2ee8sjQ.jpg)\
(Source: [網路圖片](https://s9.rr.itc.cn/r/wapChange/20164_15_14/a0sisi1715111647352.JPEG))

沒學過如來神掌的人，請跟著導遊繼續往下看實際舉例。

## `with` 使用於內建物件的範例

下面是一個使用 JavaScript 內建數學運算物件 `Math` 的例子：

```javascript
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`，讓程式碼變得較簡潔易讀：

```javascript
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 物件，也可以用在自定義的物件上嗎？

當然可以。

## `with` 使用於自訂物件的範例

以下想對自訂物件 `player` 的多個屬性作操作，印出想要的資訊：

```javascript
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` 語法讓這段程式碼更簡潔：

```javascript
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` 語法的疑慮：**如果我另外有同名的變數會發生什麼事？**

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

```javascript
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` 變數。

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

```javascript
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` 優先。

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

```javascript
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` 區塊之外，而 `hero` 有 `hero.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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with) 建議：

> 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 的建議是「將需要重複呼叫的物件暫存於一個名稱簡短的變數」就好。

例如下面這樣：

```javascript
"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
```

## References

* [with - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with)
* [【转载】JavaScript中with、this用法小结](https://king39461.pixnet.net/blog/post/361793370-%E3%80%90%E8%BD%AC%E8%BD%BD%E3%80%91javascript%E4%B8%ADwith%E3%80%81this%E7%94%A8%E6%B3%95%E5%B0%8F%E7%BB%93)
* [\[JavaScript\] with的用法](https://stannotes.blogspot.com/2014/10/javascript-with.html)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://something-about-js-book.onejar99.com/day14.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
