2015年12月10日 星期四

JavaScript 的嚴格模式

  ECMAScript 5的 嚴格模式是JavaScript中的一種限制性更強的變種方式。嚴格模式不是一個子集:它在語義上與正常程式碼有著明顯的差異。不支持嚴格模式的瀏覽器與 同支持嚴格模式的瀏覽器行為上也不一樣, 所以不要在未經嚴格模式特性測試情況下使用嚴格模式。嚴格模式可以與非嚴格模式共存,所以scripts可以逐漸的選擇性加入嚴格模式。
        嚴格模式在語義上與正常的JavaScript有一些不同。 首先,嚴格模式會將JavaScript陷阱直接變成明顯的錯誤。其次,嚴格模式修正了一些引擎難以優化的錯誤:同樣的程式碼有些時候嚴格模式會比非嚴格 模式下更快。 第三,嚴格模式禁用了一些有可能在未來版本中定義的語法。
        如果你想讓你的JavaScript程式碼在嚴格模式下執行,可以參考轉換成嚴格模式

開啟嚴格模式

        嚴格模式可以套用到整個script標籤或某個別函式中。不要在封閉大括弧( {} )內這樣做;在這樣的上下文中這麼做是沒有效果的。 例如,eval 程式碼,Function 程式碼,事件處理屬性,傳入 setTimeout方法的字串 等等包括整個scripts中開啟嚴格模式都會如預期一樣工作。

為某個script標籤開啟嚴格模式

        為整個script標籤開啟嚴格模式, 需要在所有語句之前放一個特定語句 "use strict"; (或 'use strict';)
// 整個語句都開啟嚴格模式的語法
"use strict";
var v = "Hi!  I'm a strict mode script!";
        這種語法存在陷阱,有一個大型網站已經被它坑倒了: 不能盲目的拼合非衝突程式碼。如果串聯一個嚴格模式的scripts和一個非嚴格模式的scripts:整個拼合後的scripts程式碼看起來成了嚴格 模式。反之亦然:非嚴格串聯嚴格看起來像是非嚴格的。拼合全為嚴格模式的scripts或全為非嚴格模式的都沒問題,只有在拼合嚴格模式與非嚴格模式有可 能有問題。建議按一個個函式去開啟嚴格模式(至少在學習的過渡期要這樣做).
        您也可以將整個scripts的內容用一個函式包裹起來,然後在這個外部函式中使用嚴格模式。這樣做就可以消除拼接的問題,但是這就意味著您必須要在函式作用域外聲明一個全域變數。

為某個函式開啟嚴格模式

        同樣的,要給某個函式開啟嚴格模式,得把 "use strict";  (或 'use strict'; )聲明一字不漏地放在函式體所有語句之前。
function strict()
{
  // 函式級別嚴格模式語法
  'use strict';
  function nested() { return "And so am I!"; }
  return "Hi!  I'm a strict mode function!  " + nested();
}
function notStrict() { return "I'm not strict."; }

嚴格模式有哪些不同

嚴格模式同時改變了語法及執行時行為。變化通常分為這幾類:將問題直接轉化為錯誤(如語法錯誤或執行時錯誤), 簡化了如何為給定名稱的特定變數計算,簡化了 eval 以及 arguments, 將寫"安全「JavaScript的步驟變得更簡單,以及改變了預測未來ECMAScript行為的方式。

將拼寫錯轉成異常

在嚴格模式下, 先前被接受的拼寫錯誤將會被認為是異常. JavaScript被設計為能使新人開發者更易於上手, 所以有時候會給本來錯誤操作賦予新的不報錯誤的語義(non-error semantics). 有時候這可以解決當前的問題, 但有時候卻會給以後留下更大的問題. 嚴格模式則把這些失誤當成錯誤, 以便可以發現並立即將其改正.
首先,嚴格模式下無法再意外新增全域變數。在普通的JavaScript裡面給一個拼寫錯誤的變數名賦值會使全域物件新增一個屬性並繼續「工作」(僅管後面可能出錯:在現在的JavaScript中有可能)。嚴格模式中意外新增全域變數被拋出錯誤替代:
"use strict";
                       // 假如有一個全域變數叫做mistypedVariable
mistypedVaraible = 17; // 因為變數名拼寫錯誤
                       // 這一行程式碼就會拋出 ReferenceError
其次, 嚴格模式會使引起靜默失敗(silently fail,注:不報錯也沒有任何效果)的賦值操作拋出異常. 例如, NaN 是一個不可寫的全域變數. 在正常模式下, 給 NaN 賦值不會產生任何作用; 開發者也不會受到任何錯誤反饋. 但在嚴格模式下, 給 NaN 賦值會拋出一個異常. 任何在正常模式下引起靜默失敗的賦值操作 (給不可寫屬性賦值, 給只讀屬性(getter-only)賦值賦值, 給不可擴展物件(non-extensible object)的新屬性賦值) 都會拋出異常:
"use strict";

// 給不可寫屬性賦值
var obj1 = {};
Object.defineProperty(obj1, "x", { value: 42, writable: false });
obj1.x = 9; // 拋出TypeError錯誤

// 給只讀屬性賦值
var obj2 = { get x() { return 17; } };
obj2.x = 5; // 拋出TypeError錯誤

// 給不可擴展物件的新屬性賦值
var fixed = {};
Object.preventExtensions(fixed);
fixed.newProp = "ohai"; // 拋出TypeError錯誤
第三, 在嚴格模式下, 試圖刪除不可刪除的屬性時會拋出異常(之前這種操作不會產生任何效果):
"use strict";
delete Object.prototype; // 拋出TypeError錯誤
第四,在Gecko版本34之前,嚴格模式要求一個物件內的所有屬性名在物件內必須唯一。正常模式下重名屬性是允許的,最後一個重名的屬性決定其屬 性值。因為只有最後一個屬性起作用,當程式碼是要改變屬性值而卻不是修改的最後一個重名屬性的時候,複製這個物件就產生一連串的bug。在嚴格模式下,重 名屬性被認為是語法錯誤:
這個問題在ECMAScript6中已經不復存在(bug 1041128)。
"use strict";
var o = { p: 1, p: 2 }; // !!! 語法錯誤
第五, 嚴格模式要求函式的參數名唯一. 在正常模式下, 最後一個重名參數名會掩蓋之前的重名參數. 之前的參數仍然可以通過 arguments[i] 來訪問, 還不是完全無法訪問. 然而, 這種隱藏毫無意義而且可能是意料之外的 (比如它可能本來是打錯了), 所以在嚴格模式下重名參數被認為是語法錯誤:
function sum(a, a, c){ // !!! 語法錯誤
  "use strict";
  return a + b + c; // 程式碼執行到這裡會出錯
}
第六, 嚴格模式禁止八進制數字語法. ECMAScript並不包含八進制語法, 但所有的瀏覽器都支持這種以零(0)開頭的八進制語法: 0644 === 420 還有 "\045" === "%". 有些新手開發者認為數字的前導零沒有語法意義, 所以他們會用作對齊措施 — 但其實這會改變數字的意義! 八進制語法很少有用並且可能會錯誤使用, 所以嚴格模式下八進制語法會引起語法錯誤:
"use strict";
var sum = 015 + // !!! 語法錯誤
          197 +
          142;

簡化變數的使用

嚴格模式簡化了程式碼中變數名字映射到變數定義的方式. 很多編譯器的優化是依賴存儲變數X位置的能力:這對全面優化JavaScript程式碼至關重要. JavaScript有些情況會使得程式碼中名字到變數定義的基本映射只在執行時才產生. 嚴格模式移除了大多數這種情況的發生, 所以編譯器可以更好的優化嚴格模式的程式碼.
首先, 嚴格模式禁用 withwith 所引起的問題是塊內的任何名稱可以映射(map)到with傳進來的物件的屬性, 也可以映射到包圍這個塊的作用域內的變數(甚至是全域變數), 這一切都是在執行時決定的: 在程式碼執行之前是無法得知的. 嚴格模式下, 使用 with 會引起語法錯誤, 所以就不會存在 with 塊內的變數在執行是才決定引用到哪裡的情況了:
"use strict";
var x = 17;
with (obj) // !!! 語法錯誤
{
  // 如果沒有開啟嚴格模式,with中的這個x會指向with上面的那個x,還是obj.x?
  // 如果不執行程式碼,我們無法知道,因此,這種程式碼讓引擎無法進行優化,速度也就會變慢。
  x;
}
一種取代 with 的簡單方法是,將目標物件賦給一個短命名變數,然後訪問這個變數上的相應屬性.
第二, 嚴格模式下的 eval 不在為上層範圍(surrounding scope,注:包圍eval程式碼塊的範圍)引入新變數. 在正常模式下,  程式碼 eval("var x;") 會給上層函式(surrounding function)或者全域引入一個新的變數 x . 這意味著, 一般情況下,  在一個包含 eval 呼叫的函式內所有沒有引用到參數或者局部變數的名稱都必須在執行時才能被映射到特定的定義 (因為 eval 可能引入的新變數會覆蓋它的外層變數). 在嚴格模式下 eval 僅僅為被執行的程式碼新增變數, 所以 eval 不會影響到名稱映射到外部變數或者其他局部變數:
var x = 17;
var evalX = eval("'use strict'; var x = 42; x");
assert(x === 17);
assert(evalX === 42);
相應的, 如果函式 eval 被在被嚴格模式下的eval(...)以表達式的形式呼叫時, 其程式碼會被當做嚴格模式下的程式碼執行. 當然也可以在程式碼中顯式開啟嚴格模式, 但這樣做並不是必須的.
function strict1(str)
{
  "use strict";
  return eval(str); // str中的程式碼在嚴格模式下執行
}
function strict2(f, str)
{
  "use strict";
  return f(str); // 沒有直接呼叫eval(...): 當且僅當str中的程式碼開啟了嚴格模式時
                 // 才會在嚴格模式下執行
}
function nonstrict(str)
{
  return eval(str); // 當且僅當str中的程式碼開啟了"use strict",str中的程式碼才會在嚴格模式下執行
}
strict1("'Strict mode code!'");
strict1("'use strict'; 'Strict mode code!'");
strict2(eval, "'Non-strict code.'");
strict2(eval, "'use strict'; 'Strict mode code!'");
nonstrict("'Non-strict code.'");
nonstrict("'use strict'; 'Strict mode code!'");
因此在嚴格模式下eval執行的內容跟在非嚴格模式下eval執行的內容的結果是等同的。
第三, 嚴格模式禁止刪除聲明變數。delete name 在嚴格模式下會引起語法錯誤:
"use strict";

var x;
delete x; // !!! 語法錯誤

eval("var x; delete x;"); // !!! 語法錯誤

eval和arguments變的簡單

嚴格模式讓argumentseval少了一些奇怪的行為。兩者在通常的程式碼中都包含了很多奇怪的行為: eval會增加刪除綁定,改變綁定好的值,還會通過用它索引過的屬性給形參取別名的方式修改形參. 雖然在未來的ECMAScript版本解決這個問題之前,是不會有補丁來完全修復這個問題,但嚴格模式下將eval和arguments作為關鍵字對於此問題的解決是很有幫助的。
首先, 名稱 eval 和 arguments 不能通過程式語法被綁定(be bound)或賦值. 以下的所有嘗試將一起語法錯誤:
"use strict";
eval = 17;
arguments++;
++eval;
var obj = { set p(arguments) { } };
var eval;
try { } catch (arguments) { }
function x(eval) { }
function arguments() { }
var y = function eval() { };
var f = new Function("arguments", "'use strict'; return 17;");
第二,嚴格模式下,參數的值不會隨 arguments 物件的值的改變而變化。在正常模式下,對於第一個參數是 arg 的函式,對 arg 賦值時會同時賦值給 arguments[0],反之亦然(除非沒有參數,或者 arguments[0] 被刪除)。嚴格模式下,函式的 arguments 物件會存檔函式被呼叫時的原始參數。arguments[i] 的值不會隨與之相應的參數的值的改變而變化,同名參數的值也不會隨與之相應的 arguments[i] 的值的改變而變化。
function f(a)
{
  "use strict";
  a = 42;
  return [a, arguments[0]];
}
var pair = f(17);
console.assert(pair[0] === 42);
console.assert(pair[1] === 17);
第三,不再支持 arguments.callee。正常模式下,arguments.callee 指向當前正在執行的函式。這個作用很小:直接給執行函式命名就可以了!此外,arguments.callee 十分不利於優化,例如內聯函式,因為 arguments.callee 會依賴對非內聯函式的引用。在嚴格模式下,arguments.callee 是一個不可刪除屬性,而且賦值和讀取時都會拋出異常:
"use strict";
var f = function() { return arguments.callee; };
f(); // 拋出類型錯誤

"安全的" JavaScript

嚴格模式下更容易寫出「安全」的JavaScript。現在有些網站提供了方式給用戶編寫能夠被網站其他用戶執行的JavaScript程式碼。在 瀏覽器環境下,JavaScript能夠獲取用戶的隱私訊息,因此這類Javascript必須在執行前部分被轉換成需要申請訪問禁用功能的權限。沒有很 多的執行時檢查的情況,Javascript的靈活性讓它無法有效率地做這件事。一些語言中的函式普遍出現,以至於執行時檢查他們會引起嚴重的性能損耗。 做一些在嚴格模式下發生的小改動,要求用戶提交的JavaScript開啟嚴格模式並且用特定的方式呼叫,就會大大減少在執行時進行檢查的必要。
第一,在嚴格模式下通過this傳遞給一個函式的值不會被強制轉換為一個物件。對一個普通的函式來說,this總會是一個物件:不管呼叫時this它本來就是一個物件;還是用布爾值,字串或者數字呼叫函式時函式裡面被封裝成物件的this;還是使用undefined或者null呼叫函式式this代表的全域物件(使用callapply或者bind方法來指定一個確定的this)。這種自動轉化為物件的過程不僅是一種性能上的損耗,同時在瀏覽器中暴露出全域物件也會成為安全隱患,因為全域物件提供了訪問那些所謂安全的JavaScript環境必須限制的功能的途徑。所以對於一個開啟嚴格模式的函式,指定的this不再被封裝為物件,而且如果沒有指定this的話它值是undefined
"use strict";
function fun() { return this; }
assert(fun() === undefined);
assert(fun.call(2) === 2);
assert(fun.apply(null) === null);
assert(fun.call(undefined) === undefined);
assert(fun.bind(true)() === true);
第二,在嚴格模式中再也不能通過廣泛實現的ECMAScript擴展「遊走於」JavaScript的棧中。在普通模式下用這些擴展的話,當一個叫fun的函式正在被呼叫的時候,fun.caller是最後一個呼叫fun的函式,而且fun.arguments包含呼叫fun時用的形參。這兩個擴展介面對於「安全」JavaScript而言都是有問題的,因為他們允許「安全的」程式碼訪問"專有"函式和他們的(通常是沒有經過保護的)形參。如果fun在嚴格模式下,那麼fun.callerfun.arguments都是不可刪除的屬性而且在存值、取值時都會報錯:
function restricted()
{
  "use strict";
  restricted.caller;    // 拋出類型錯誤
  restricted.arguments; // 拋出類型錯誤
}
function privilegedInvoker()
{
  return restricted();
}
privilegedInvoker();
第三,嚴格模式下的arguments不會再提供訪問與呼叫這個函式相關的變數的途徑。在一些舊時的ECMAScript實現中arguments.caller曾經是一個物件,裡面存儲的屬性指向那個函式的變數。這是一個安全隱患,因為它通過函式抽象打破了本來被隱藏起來的保留值;它同時也是引起大量優化工作的原因。出於這些原因,現在的瀏覽器沒有實現它。但是因為它這種歷史遺留的功能,arguments.caller在嚴格模式下同樣是一個不可被刪除的屬性,在賦值或者取值時會報錯:
"use strict";
function fun(a, b)
{
  "use strict";
  var v = 12;
  return arguments.caller; // 拋出類型錯誤
}
fun(1, 2); // 不會暴露v(或者a,或者b)

為未來的ECMAScript版本鋪平道路

未來版本的ECMAScript很有可能會引入新語法,ECMAScript5中的嚴格模式就提早設置了一些限制來減輕之後版本改變產生的影響。如果提早使用了嚴格模式中的保護機制,那麼做出改變就會變得更容易。
首先,在嚴格模式中一部分字元變成了保留的關鍵字。這些字元包括implements, interface, let, package, private, protected, public, staticyield。在嚴格模式下,你不能再用這些名字作為變數名或者形參名。
function package(protected) // !!!
{
  "use strict";
  var implements; // !!!

  interface: // !!!
  while (true)
  {
    break interface; // !!!
  }

  function private() { } // !!!
}
function fun(static) { 'use strict'; } // !!!
兩個針對Mozilla開發的警告:第一,如果你的JavaScript版本在1.7及以上(你的chrome程式碼或者你正確使用了<script type="">)並且開啟了嚴格模式的話,因為letyield是最先引入的關鍵字,所以它們會起作用。但是網路上用<script src="">或者<script>...</script>加載的程式碼,let或者yield都不會作為關鍵字起作用;第二,儘管ES5無條件的保留了class, enum, export, extends, importsuper關鍵字,在Firefox 5之前,Mozilla僅僅在嚴格模式中保留了它們。
其次,嚴格模式禁止了不在scripts或者函式層面上的函式聲明。在瀏覽器的普通程式碼中,在「所有地方」的函式聲明都是合法的。這並不在ES5規範中(甚至是ES3)!這是一種針對不同瀏覽器中不同語義的一種延伸。未來的ECMAScript版本很有希望制定一個新的,針對不在scripts或者函式層面進行函式聲明的語法。在嚴格模式下禁止這樣的函式聲明對於將來ECMAScript版本的推出掃清了障礙:
"use strict";
if (true)
{
  function f() { } // !!! 語法錯誤
  f();
}
for (var i = 0; i < 5; i++)
{
  function f2() { } // !!! 語法錯誤
  f2();
}
function baz() // 合法
{
  function eit() { } // 同樣合法
}
這種禁止放到嚴格模式中並不是很合適,因為這樣的函式聲明方式從ES5中延伸出來的。但這是ECMAScript委員會推薦的做法,瀏覽器就實現了這一點。

瀏覽器的嚴格模式

主流瀏覽器現在實現了嚴格模式。但是不要盲目的依賴它,因為市場上仍然有大量的瀏覽器版本只部分支持嚴格模式或者根本就不支持(比如IE10之前的版本)。嚴格模式改變了語義。依賴這些改變可能會導致沒有實現嚴格模式的瀏覽器中出現問題或者錯誤。謹慎地使用嚴格模式,通過檢測相關程式碼的功能保證嚴格模式不出問題。最後,記得在支持或者不支持嚴格模式的瀏覽器中測試你的程式碼。如果你只在不支持嚴格模式的瀏覽器中測試,那麼在支持的瀏覽器中就很有可能出問題,反之亦然。

沒有留言:

張貼留言