問題描述
在一個(gè)元素的屬性中綁定事件,實(shí)際上就創(chuàng)建了一個(gè)內(nèi)聯(lián)事件處理函數(shù)(如<h1 onclick="alert(this);"...>...</h1>),內(nèi)聯(lián)事件處理函數(shù)有其特殊的作用域鏈,并且各瀏覽器的實(shí)現(xiàn)細(xì)節(jié)也有差異。
造成的影響
如果在元素的內(nèi)聯(lián)事件處理函數(shù)中使用的變量或調(diào)用的方法不當(dāng),將導(dǎo)致腳本運(yùn)行出錯(cuò)。
受影響的瀏覽器
所有瀏覽器
問題分析
1. 內(nèi)聯(lián)事件處理函數(shù)的作用域鏈
與其他函數(shù)不同,內(nèi)聯(lián)事件處理函數(shù)的作用域鏈從頭部開始依次是:調(diào)用對(duì)象、該元素的 DOM 對(duì)象、該元素所屬 FORM 的 DOM 對(duì)象(如果有)、document 對(duì)象、window 對(duì)象(全局對(duì)象)。
如以下代碼:
<form action="." method="get">
<input type="button" value="compatMode" onclick="alert(compatMode);">
</form>
相當(dāng)于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 元素所在表單對(duì)象的 method 屬性值。
注:
1. 這段代碼僅為說明問題而模擬各瀏覽器的行為,并非表示所有瀏覽器都是如此實(shí)現(xiàn)的。
2. 是使用 this 關(guān)鍵字還是直接使用這個(gè) DOM 對(duì)象,在各瀏覽器中有差異,詳情請(qǐng)看本文 2.1 中的內(nèi)容。
3. 是否添加 FORM 對(duì)象到作用域鏈中,各瀏覽器在實(shí)現(xiàn)上也有差異,詳情請(qǐng)看本文 2.2 中的內(nèi)容。
2. 內(nèi)聯(lián)事件處理函數(shù)的作用域鏈在各瀏覽器中的差異
參考 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 在向作用域鏈中添加對(duì)象時(shí),使用了 'this' 關(guān)鍵字,并且通過判斷 'this.form' 是否存在來決定是否添加 FORM 對(duì)象到作用域鏈中。
其他瀏覽器中也有類似的實(shí)現(xiàn)方式,但在各瀏覽器中,將目標(biāo)對(duì)象(即綁定了此內(nèi)聯(lián)事件處理函數(shù)的對(duì)象)添加到作用域鏈中的方式有差異,判斷并決定是否在作用域鏈中添加 FORM 對(duì)象的方法也不相同。
2.1. 各瀏覽器在生成這個(gè)特殊的作用域鏈時(shí)添加目標(biāo)對(duì)象時(shí)使用的方法不同
各瀏覽器都會(huì)將內(nèi)聯(lián)事件處理函數(shù)所屬的元素的 DOM 對(duì)象加入到作用域鏈中,但加入的方式卻是不同的。
如以下代碼:
<input type="button" value="hello" onclick="alert(value);">
在所有瀏覽器中,都將彈出 'hello'。
再修改代碼以變更 INPUT 元素的內(nèi)聯(lián)事件處理函數(shù)的執(zhí)行上下文:
<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>
在各瀏覽器中運(yùn)行的結(jié)果如下:
IE ChromeHi, I'm here!
Firefox Safari Operahello
可見,各瀏覽器將內(nèi)聯(lián)事件處理函數(shù)所屬的元素的 DOM 對(duì)象加入到作用域鏈中的方式是不同的。
在 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>
由于極少需要改變內(nèi)聯(lián)事件處理函數(shù)的執(zhí)行上下文,這個(gè)差異造成的影響并不多見。
2.2. 各瀏覽器在生成這個(gè)特殊的作用域鏈時(shí)對(duì)于在何種情況下添加 FORM 對(duì)象有不同理解
各瀏覽器都會(huì)將內(nèi)聯(lián)事件處理函數(shù)所屬的 FORM 對(duì)象加入到作用域鏈中,但如何判斷該元素是否“屬于”一個(gè)表單對(duì)象,各瀏覽器的處理方式則不相同。
如以下代碼:
<form action="." method="get">
<div>
<span onclick="alert(method);">click</span>
</div>
</form>
<script>
document.method="document.method";
</script>
在各瀏覽器中,點(diǎn)擊 SPAN 元素后彈出的信息如下:
IE Safari Operaget
Chrome Firefoxdocument.method
可見:
IE Safari Opera 將 FORM 對(duì)象加入到了內(nèi)聯(lián)事件處理函數(shù)的作用域鏈中,是否加入 FORM 對(duì)象看起來是由這個(gè)元素是否是一個(gè) FORM 的子孫級(jí)元素來決定的。因此在這些瀏覽器中,函數(shù)內(nèi)的變量 'method' 最終得到的是 FORM 的 'method' 的值。
Chrome Firefox 沒有將 FORM 對(duì)象加入到內(nèi)聯(lián)事件處理函數(shù)的作用域鏈中,判斷是否加入 FORM 對(duì)象是看該函數(shù)綁定的目標(biāo)對(duì)象的 'form' 屬性是否存在。從上文中的 WebKit 的源碼中可以看到 Chrome 正是使用了 'this.form' 來判斷,只有目標(biāo)元素是一個(gè) FORM 的子孫級(jí)元素并且該目標(biāo)元素是一個(gè)表單元素時(shí),'form' 屬性才會(huì)存在。本例中的 SPAN 元素并不是表單元素,因此變量 'method' 最終得到的是 'document.method' 的值。
如果將以上代碼中的 SPAN 元素更換為 INPUT 元素或其他表單元素,則在所有瀏覽器中的表現(xiàn)將一致。
3. 由于內(nèi)聯(lián)事件處理函數(shù)的這種特殊的作用域鏈而產(chǎn)生問題的實(shí)例
3.1. 在元素的內(nèi)聯(lián)事件處理函數(shù)中訪問的變量意外的與該該函數(shù)作用域鏈中非全局對(duì)象的其他對(duì)象的屬性重名時(shí)出現(xiàn)的問題
當(dāng)一個(gè)內(nèi)聯(lián)事件處理函數(shù)中訪問的變量意外的與該函數(shù)作用域鏈中非全局對(duì)象(window)的其他對(duì)象的屬性重名,將導(dǎo)致該變量的實(shí)際值不是預(yù)期值。
假設(shè)有以下代碼:
<button onclick="onsearch()"> click here </button>
<script>
function onsearch(){
alert("Click!");
}
</script>
作者本意為點(diǎn)擊按鈕即彈出“Click!”信息,但 WebKit 引擎瀏覽器的 HTMLElement 對(duì)象都有一個(gè)名為 onsearch 的事件監(jiān)聽器,這將導(dǎo)致上述代碼在 Chrome Safari 中不能按照預(yù)期執(zhí)行。本例中由于該監(jiān)聽器未定義(為 null),因此將報(bào) “Uncaught TypeError: object is not a function” 的錯(cuò)誤。
附:在上述代碼中,追加以下代碼確認(rèn) 'onsearch' 的位置:
<script>
var o=document.getElementsByTagName("button")[0];
if("onsearch" in o)alert("當(dāng)前對(duì)象有 onsearch 屬性。");
if(o.hasOwnProperty("onsearch"))alert("onsearch 屬性是當(dāng)前對(duì)象私有。");
</script>
3.2. 在表單內(nèi)的子孫級(jí)非表單元素的內(nèi)聯(lián)事件處理函數(shù)中試圖調(diào)用表單的屬性或方法時(shí)出現(xiàn)的問題
假設(shè)有以下代碼:
<form action="xxx" method="get">
...
<a href="#" onclick="submit();">click</a>
</form>
作者本意為點(diǎn)擊 A 元素后調(diào)用 FORM 的 'submit' 方法,但 Chrome Firefox 并未將 FORM 對(duì)象加入到該內(nèi)聯(lián)事件處理函數(shù)的作用域鏈中,因此以上代碼在 Chrome Firefox 中并不能正常運(yùn)行。
解決方案
1. 盡量不要使用內(nèi)聯(lián)事件處理函數(shù),使用 DOM 標(biāo)準(zhǔn)的事件注冊(cè)方式為該元素注冊(cè)事件處理函數(shù),如:
<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. 必須使用內(nèi)聯(lián)事件處理函數(shù)時(shí),要保證該函數(shù)內(nèi)試圖訪問的變量是位于全局作用域內(nèi)的,而不會(huì)因該函數(shù)獨(dú)特的作用域鏈而引用到非預(yù)期的對(duì)象。最簡(jiǎn)單的辦法是使用前綴,如 'my_onsearch'。
更多信息請(qǐng)查看IT技術(shù)專欄