首頁  >  文章  >  web前端  >  JavaScript探針:SpiderMonkey的怪癖

JavaScript探針:SpiderMonkey的怪癖

高洛峰
高洛峰原創
2016-11-28 14:52:021225瀏覽

大家都知道,命名函數表達式的標識符只在函數的局部作用域中有效。但包含這個識別符的局部作用域又是什麼樣子的嗎?其實非常簡單。在命名函數表達式被求值時,會創建一個特殊的對象,該對象的唯一目的就是保存一個屬性,而這個屬性的名字對應著函數標識符,屬性的值對應著那個函數。這個物件會被注入到目前作用域鏈的前端。然後,被「擴展」的作用域鏈又被用來初始化函數。

在這裡,有一點十分有意思,那就是ECMA-262定義這個(保存函數標識符的)「特殊」物件的方式。標準說「像呼叫new Object()表達式那樣」建立這個物件。如果從字面上來理解這句話,那麼這個物件就應該是全域Object的一個實例。然而,只有一個實作是按照標準字面上的要求這麼做的,這個實作就是SpiderMonkey。因此,在SpiderMonkey中,擴充Object.prototype有可能會幹擾函數的局部作用域:

Object.prototype.x = 'outer';
 
(function(){
   
  var x = 'inner';
   
  /*
    函数foo的作用域链中有一个特殊的对象——用于保存函数的标识符。这个特殊的对象实际上就是{ foo: <function object> }。
    当通过作用域链解析x时,首先解析的是foo的局部环境。如果没有找到x,则继续搜索作用域链中的下一个对象。下一个对象
    就是保存函数标识符的那个对象——{ foo: <function object> },由于该对象继承自Object.prototype,所以在此可以找到x。
    而这个x的值也就是Object.prototype.x的值(outer)。结果,外部函数的作用域(包含x = &#39;inner&#39;的作用域)就不会被解析了。
  */
   
  (function foo(){
     
    alert(x); // 提示框中显示:outer
   
  })();
})();

不過,較高版本的SpiderMonkey改變了上述行為,原因可能是認為那是一個安全漏洞。也就是說,「特殊」物件不再繼承Object.prototype了。不過,如果你使用Firefox 3或更低版本,還可以「重溫」這種行為。

另一個把內部物件實作為全域Object物件的是黑莓(Blackberry)瀏覽器。目前,它的活動物件(Activation Object)仍然繼承Object.prototype。可是,ECMA-262並沒有說活動物件也要「像呼叫new Object()表達式那樣」來創建(或者說像創建保存NFE標識符的物件一樣創建)。 人家規範只說了活動對像是規範中的一種機制。

好,那我們下面就來看看黑莓瀏覽器的行為吧:

Object.prototype.x = &#39;outer&#39;;
 
(function(){
   
  var x = &#39;inner&#39;;
   
  (function(){
     
    /*
    在沿着作用域链解析x的过程中,首先会搜索局部函数的活动对象。当然,在该对象中找不到x。
    可是,由于活动对象继承自Object.prototype,因此搜索x的下一个目标就是Object.prototype;而
    Object.prototype中又确实有x的定义。结果,x的值就被解析为——outer。跟前面的例子差不多,
    包含x = &#39;inner&#39;的外部函数的作用域(活动对象)就不会被解析了。
    */
     
    alert(x); // 显示:outer
     
  })();
})();

不過神奇的還是,函數中的變數甚至會與已有的Object.prototype的成員發生衝突,來看看下面的程式碼:

(function(){
   
  var constructor = function(){ return 1; };
   
  (function(){
     
    constructor(); // 求值结果是{}(即相当于调用了Object.prototype.constructor())而不是1
     
    constructor === Object.prototype.constructor; // true
    toString === Object.prototype.toString; // true
     
    // ……
     
  })();
})();

要避免這個問題,要避免使用Object.prototype裡的屬性名稱,如toString, valueOf, hasOwnProperty等等。

JScript解決方案

var fn = (function(){
 
  // 声明要引用函数的变量
  var f;
 
  // 有条件地创建命名函数
  // 并将其引用赋值给f
  if (true) {
    f = function F(){ }
  }
  else if (false) {
    f = function F(){ }
  }
  else {
    f = function F(){ }
  }
 
  // 声明一个与函数名(标识符)对应的变量,并赋值为null
  // 这实际上是给相应标识符引用的函数对象作了一个标记,
  // 以便垃圾回收器知道可以回收它了
  var F = null;
 
  // 返回根据条件定义的函数
  return f;
})();

最後我們給出一個應用上述技術的應用實例,這是一個跨瀏覽器的addEvent函數代碼:

// 1) 使用独立的作用域包含声明
var addEvent = (function(){
 
  var docEl = document.documentElement;
 
  // 2) 声明要引用函数的变量
  var fn;
 
  if (docEl.addEventListener) {
 
    // 3) 有意给函数一个描述性的标识符
    fn = function addEvent(element, eventName, callback) {
      element.addEventListener(eventName, callback, false);
    }
  }
  else if (docEl.attachEvent) {
    fn = function addEvent(element, eventName, callback) {
      element.attachEvent(&#39;on&#39; + eventName, callback);
    }
  }
  else {
    fn = function addEvent(element, eventName, callback) {
      element[&#39;on&#39; + eventName] = callback;
    }
  }
 
  // 4) 清除由JScript创建的addEvent函数
  //    一定要保证在赋值前使用var关键字
  //    除非函数顶部已经声明了addEvent
  var addEvent = null;
 
  // 5) 最后返回由fn引用的函数
  return fn;
})();


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn