Day 14:來挖挖恐龍骨 —— with 語法

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

事實上連 W3Schools 的 JavaScript 教材 都沒有 with 語法教學!查了一下網路討論,with 曾經也是有人認為很好用的語法。那麼究竟發生什麼事,讓 with 像個黑歷史一樣,被 W3Schools 刻意遺忘?

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

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

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

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

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

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

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

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

with 的語法

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

語法模板如下:

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

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

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

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

with 使用於內建物件的範例

下面是一個使用 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 物件,也可以用在自定義的物件上嗎?

當然可以。

with 使用於自訂物件的範例

以下想對自訂物件 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 語法的疑慮:如果我另外有同名的變數會發生什麼事?

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

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

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 區塊之內被使用

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

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

例如下面這樣:

"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

Last updated