DIV CSS 佈局教程網

 DIV+CSS佈局教程網 >> 網頁腳本 >> JavaScript入門知識 >> 關於JavaScript >> 元素的內聯事件處理函數的特殊作用域在各浏覽器中存在差異
元素的內聯事件處理函數的特殊作用域在各浏覽器中存在差異
編輯:關於JavaScript     

標准參考

無。

問題描述

在一個元素的屬性中綁定事件,實際上就創建了一個內聯事件處理函數(如<h1 onclick="alert(this);"...>...</h1>),內聯事件處理函數有其特殊的作用域鏈,並且各浏覽器的實現細節也有差異。

造成的影響

如果在元素的內聯事件處理函數中使用的變量或調用的方法不當,將導致腳本運行出錯。

受影響的浏覽器

所有浏覽器

問題分析

1. 內聯事件處理函數的作用域鏈

與其他函數不同,內聯事件處理函數的作用域鏈從頭部開始依次是:調用對象、該元素的 DOM 對象、該元素所屬 FORM 的 DOM 對象(如果有)、document 對象、window 對象(全局對象)。

如以下代碼:

<form action="." method="get">
	<input type="button" value="compatMode" onclick="alert(compatMode);">
</form>

相當於1

<form action="." method="get">
	<input type="button" value="compatMode">
</form>
<script>
document.getElementsByTagName("input")[0].onclick=function(){
	with(document){
		with(this2.form)3{
			with(this2){
				alert(compatMode);
			}
		}
	}
}
</script>

以上兩種寫法的代碼在所有浏覽器中都將彈出 document.compatMode 的值。

將上述代碼中的 'compatMode' 替換為 'method',則在各浏覽器中都將彈出 'get',即 INPUT 元素所在表單對象的 method 屬性值。

注:
1. 這段代碼僅為說明問題而模擬各浏覽器的行為,並非表示所有浏覽器都是如此實現的。
2. 是使用 this 關鍵字還是直接使用這個 DOM 對象,在各浏覽器中有差異,詳情請看本文 2.1 中的內容。
3. 是否添加 FORM 對象到作用域鏈中,各浏覽器在實現上也有差異,詳情請看本文 2.2 中的內容。

2. 內聯事件處理函數的作用域鏈在各浏覽器中的差異

參考 WebKit 的源碼:

void V8LazyEventListener::prepareListenerObject(ScriptExecutionContext* context)
{
  if (hasExistingListenerObject())
    return;

  v8::HandleScope handleScope;

  V8Proxy* proxy = V8Proxy::retrieve(context);
  if (!proxy)
    return;

  // Use the outer scope to hold context.
  v8::Local<v8::Context> v8Context = worldContext().adjustedContext(proxy);
  // Bail out if we cannot get the context.
  if (v8Context.IsEmpty())
    return;

  v8::Context::Scope scope(v8Context);

  // FIXME: cache the wrapper function.

  // Nodes other than the document object, when executing inline event handlers push document, form, and the target node on the scope chain.
  // We do this by using 'with' statement.
  // See chrome/fast/forms/form-action.html
  //   chrome/fast/forms/selected-index-value.html
  //   base/fast/overflow/onscroll-layer-self-destruct.html
  //
  // Don't use new lines so that lines in the modified handler
  // have the same numbers as in the original code.
  String code = "(function (evt) {" \
      "with (this.ownerDocument ? this.ownerDocument : {}) {" \
      "with (this.form ? this.form : {}) {" \
      "with (this) {" \
      "return (function(evt){";
  code.append(m_code);
  // Insert '\n' otherwise //-style comments could break the handler.
  code.append( "\n}).call(this, evt);}}}})");
  v8::Handle<v8::String> codeExternalString = v8ExternalString(code);
  v8::Handle<v8::Script> script = V8Proxy::compileScript(codeExternalString, m_sourceURL, m_lineNumber);
  if (!script.IsEmpty()) {
    v8::Local<v8::Value> value = proxy->runScript(script, false);
    if (!value.IsEmpty()) {
      ASSERT(value->IsFunction());

      v8::Local<v8::Function> wrappedFunction = v8::Local<v8::Function>::Cast(value);

      // Change the toString function on the wrapper function to avoid it
      // returning the source for the actual wrapper function. Instead it
      // returns source for a clean wrapper function with the event
      // argument wrapping the event source code. The reason for this is
      // that some web sites use toString on event functions and eval the
      // source returned (sometimes a RegExp is applied as well) for some
      // other use. That fails miserably if the actual wrapper source is
      // returned.
      DEFINE_STATIC_LOCAL(v8::Persistent<v8::FunctionTemplate>, toStringTemplate, ());
      if (toStringTemplate.IsEmpty())
        toStringTemplate = v8::Persistent<v8::FunctionTemplate>::New(v8::FunctionTemplate::New(V8LazyEventListenerToString));
      v8::Local<v8::Function> toStringFunction;
      if (!toStringTemplate.IsEmpty())
        toStringFunction = toStringTemplate->GetFunction();
      if (!toStringFunction.IsEmpty()) {
        String toStringResult = "function ";
        toStringResult.append(m_functionName);
        toStringResult.append("(");
        toStringResult.append(m_isSVGEvent ? "evt" : "event");
        toStringResult.append(") {\n ");
        toStringResult.append(m_code);
        toStringResult.append("\n}");
        wrappedFunction->SetHiddenValue(V8HiddenPropertyName::toStringString(), v8ExternalString(toStringResult));
        wrappedFunction->Set(v8::String::New("toString"), toStringFunction);
      }

      wrappedFunction->SetName(v8::String::New(fromWebCoreString(m_functionName), m_functionName.length()));

      setListenerObject(wrappedFunction);
    }
  }
}

從以上代碼可以看出,WebKit 在向作用域鏈中添加對象時,使用了 'this' 關鍵字,並且通過判斷 'this.form' 是否存在來決定是否添加 FORM 對象到作用域鏈中。

其他浏覽器中也有類似的實現方式,但在各浏覽器中,將目標對象(即綁定了此內聯事件處理函數的對象)添加到作用域鏈中的方式有差異,判斷並決定是否在作用域鏈中添加 FORM 對象的方法也不相同。

2.1. 各浏覽器在生成這個特殊的作用域鏈時添加目標對象時使用的方法不同

各浏覽器都會將內聯事件處理函數所屬的元素的 DOM 對象加入到作用域鏈中,但加入的方式卻是不同的。

如以下代碼:

<input type="button" value="hello" onclick="alert(value);">

在所有浏覽器中,都將彈出 'hello'。

再修改代碼以變更 INPUT 元素的內聯事件處理函數的執行上下文:

<input type="button" value="hello" onclick="alert(value);">
<script>
var $target=document.getElementsByTagName("input")[0];
var o={
	onclick:$target.onclick,
	value:"Hi, I'm here!"
};
o.onclick();
</script>

在各浏覽器中運行的結果如下:

IE Chrome Hi, I'm here! Firefox Safari Opera hello

可見,各浏覽器將內聯事件處理函數所屬的元素的 DOM 對象加入到作用域鏈中的方式是不同的。

在 IE Chrome 中的添加方式類似以下代碼:

<input type="button" value="hello">
<script>
var $target=document.getElementsByTagName("input")[0];
$target.onclick=function(){
	with(document){
		with(this){
			alert(value);
		}
	}
}
</script>

而在 Firefox Safari Opera 中的添加方式則類似以下代碼:

<input type="button" value="hello">
<script>
var $target=document.getElementsByTagName("input")[0];
$target.onclick=function(){
	with(document){
		with($target){
			alert(value);
		}
	}
}
</script>

由於極少需要改變內聯事件處理函數的執行上下文,這個差異造成的影響並不多見。

2.2. 各浏覽器在生成這個特殊的作用域鏈時對於在何種情況下添加 FORM 對象有不同理解

各浏覽器都會將內聯事件處理函數所屬的 FORM 對象加入到作用域鏈中,但如何判斷該元素是否“屬於”一個表單對象,各浏覽器的處理方式則不相同。

如以下代碼:

<form action="." method="get">
	<div>
		<span onclick="alert(method);">click</span>
	</div>
</form>
<script>
document.method="document.method";
</script>

在各浏覽器中,點擊 SPAN 元素後彈出的信息如下:

IE Safari Opera get Chrome Firefox document.method

可見:

  • IE Safari Opera 將 FORM 對象加入到了內聯事件處理函數的作用域鏈中,是否加入 FORM 對象看起來是由這個元素是否是一個 FORM 的子孫級元素來決定的。因此在這些浏覽器中,函數內的變量 'method' 最終得到的是 FORM 的 'method' 的值。
  • Chrome Firefox 沒有將 FORM 對象加入到內聯事件處理函數的作用域鏈中,判斷是否加入 FORM 對象是看該函數綁定的目標對象的 'form' 屬性是否存在。從上文中的 WebKit 的源碼中可以看到 Chrome 正是使用了 'this.form' 來判斷,只有目標元素是一個 FORM 的子孫級元素並且該目標元素是一個表單元素時,'form' 屬性才會存在。本例中的 SPAN 元素並不是表單元素,因此變量 'method' 最終得到的是 'document.method' 的值。

如果將以上代碼中的 SPAN 元素更換為 INPUT 元素或其他表單元素,則在所有浏覽器中的表現將一致。

3. 由於內聯事件處理函數的這種特殊的作用域鏈而產生問題的實例

3.1. 在元素的內聯事件處理函數中訪問的變量意外的與該該函數作用域鏈中非全局對象的其他對象的屬性重名時出現的問題

當一個內聯事件處理函數中訪問的變量意外的與該函數作用域鏈中非全局對象(window)的其他對象的屬性重名,將導致該變量的實際值不是預期值。

假設有以下代碼:

<button onclick="onsearch()"> click here </button>
<script>
function onsearch(){
	alert("Click!");
}
</script>

作者本意為點擊按鈕即彈出“Click!”信息,但 WebKit 引擎浏覽器的 HTMLElement 對象都有一個名為 onsearch 的事件監聽器,這將導致上述代碼在 Chrome Safari 中不能按照預期執行。本例中由於該監聽器未定義(為 null),因此將報 “Uncaught TypeError: object is not a function” 的錯誤。

附:在上述代碼中,追加以下代碼確認 'onsearch' 的位置:

<script>
var o=document.getElementsByTagName("button")[0];
if("onsearch" in o)alert("當前對象有 onsearch 屬性。");
if(o.hasOwnProperty("onsearch"))alert("onsearch 屬性是當前對象私有。");
</script>

3.2. 在表單內的子孫級非表單元素的內聯事件處理函數中試圖調用表單的屬性或方法時出現的問題

假設有以下代碼:

<form action="xxx" method="get">
	...
	<a href="#" onclick="submit();">click</a>
</form>

作者本意為點擊 A 元素後調用 FORM 的 'submit' 方法,但 Chrome Firefox 並未將 FORM 對象加入到該內聯事件處理函數的作用域鏈中,因此以上代碼在 Chrome Firefox 中並不能正常運行。

解決方案

1. 盡量不要使用內聯事件處理函數,使用 DOM 標准的事件注冊方式為該元素注冊事件處理函數,如:

<button> click here </button>
<script>
function onsearch(){
	alert("Click!");
}
function bind($target,eventName,onEvent){
	$target.addEventListener?$target.addEventListener(eventName,onEvent,false):$target.attachEvent("on"+eventName,onEvent);
}
bind(document.getElementsByTagName("button")[0],"click",onsearch);
</script>

2. 必須使用內聯事件處理函數時,要保證該函數內試圖訪問的變量是位於全局作用域內的,而不會因該函數獨特的作用域鏈而引用到非預期的對象。最簡單的辦法是使用前綴,如 'my_onsearch'。

XML學習教程| jQuery入門知識| AJAX入門| Dreamweaver教程| Fireworks入門知識| SEO技巧| SEO優化集錦|
Copyright © DIV+CSS佈局教程網 All Rights Reserved