元素的内联事件处理函数的特殊作用域在各浏览器中存在差异

标准参考


无。

问题描述

在一个元素的属性中绑定事件,实际上就创建了一个内联事件处理函数(如<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'。

(0)

相关推荐

  • 元素的内联事件处理函数的特殊作用域在各浏览器中存在差异

    标准参考 无. 问题描述 在一个元素的属性中绑定事件,实际上就创建了一个内联事件处理函数(如<h1 onclick="alert(this);"...>...</h1>),内联事件处理函数有其特殊的作用域链,并且各浏览器的实现细节也有差异. 造成的影响 如果在元素的内联事件处理函数中使用的变量或调用的方法不当,将导致脚本运行出错. 受影响的浏览器 所有浏览器 问题分析 1. 内联事件处理函数的作用域链 与其他函数不同,内联事件处理函数的作用域链从头部开始依次是:

  • 深入探讨:宏、内联函数与普通函数的区别

    内联函数的执行过程与带参数宏定义很相似,但参数的处理不同.带参数的宏定义并不对参数进行运算,而是直接替换:内联函数首先是函数,这就意味着函数的很多性质都适用于内联函数,即内联函数先把参数表达式进行运算求值,然后把表达式的值传递给形式参数.    内联函数与带参数宏定义的另一个区别是,内联函数的参数类型和返回值类型在声明中都有明确的指定:而带参数宏定义的参数没有类型的概念,只有在宏展开以后,才由编译器检查语法,这就存在很多的安全隐患.    使用内联函数时,应注意以下问题:    1)内联函数的定

  • C++编程中队内联函数的理解和使用

    函数调用过程 c++经过编译生成可执行程序文件exe,存放在外存储器中.程序启动,系统从外存储器中将可执行文件装载到内存中,从入口地址(main函数起始处)开始执行.程序执行中遇到了对其他函数的调用,就暂停当前函数的执行,并保存下一条指令的地址作为从被调函数返回后继续执行的入口点,保存现场.然后转到被调函数的入口地址执行被调函数.遇到return语句或者被调函数结束后,恢复先前保存的现场,从先前保存的返回地址处继续执行主调函数的其余部分. 内联函数 函数调用需要进行现场保护,以便在函数调用之后继

  • 块元素block element和内联元素inline element

    内联级元素(inline-level element)中试图插入块级(block-level element)元素.这样做是不允许的.唯一的能在内联元素中插入块级元素的例外是object标签.那么什么是内联级元素(inline-level element)和块级(block-level element)元素呢?. 块元素(block element)一般是其他元素的容器元素,块元素一般都从新行开始,它可以容纳内联元素和其他块元素,常见块元素是段落标签''P"."form"这个

  • C语言中的内联函数(inline)与宏定义(#define)详细解析

    先简明扼要,说下关键:1.内联函数在可读性方面与函数是相同的,而在编译时是将函数直接嵌入调用程序的主体,省去了调用/返回指令,这样在运行时速度更快. 2.内联函数可以调试,而宏定义是不可以调试的.内联函数与宏本质上是两个不同的概念如果程序编写者对于既要求快速,又要求可读的情况下,则应该将函数冠以inline.下面详细介绍一下探讨一下内联函数与宏定义. 一.内联函数是什么?内联函数是代码被插入到调用者代码处的函数.如同 #define 宏(但并不等同,原因见下文),内联函数通过避免被调用的开销来提

  • c++中的内联函数inline用法实例

    问题描述:类中成员函数缺省默认是内联的,如果在类定义时就在类内给出函数定义,那当然最好.如果在类中未给出成员函数定义,而又想内联该函数的话,那在类外要加上 inline,否则就认为不是内联的.内联函数的inline要加在函数前面,不可以加在声明前面. class A { public:void Foo(int x, int y) { } // 自动地成为内联函数 } //正确写法: // 头文件 class A { public: void Foo(int x, int y); } // 定义文

  • Go编译原理之函数内联

    目录 前言 函数内联概述 函数内联底层实现 visitBottomUp caninl inlcalls 前言 在前一篇文章中分享了编译器优化的变量捕获部分,本文分享编译器优化的另一个内容—函数内联.函数内联是指将将较小的函数内容,直接放入到调用者函数中,从而减少函数调用的开销 函数内联概述 我们知道每一个高级编程语言的函数调用,成本都是在与需要为它分配栈内存来存储参数.返回值.局部变量等等,Go的函数调用的成本在于参数与返回值栈复制.较小的栈寄存器开销以及函数序言部分的检查栈扩容(Go语言中的栈

  • Go语言编程通过dwarf获取内联函数

    目录 dwarf组成 如何将 addr 转换为行号 内联函数 如何展开内联函数 使用 parca 展开内联函数 parca 输出有以下问题 dwarf组成 dwarf 由 The Debugging Information Entry . type Entry struct { Offset Offset Tag Tag // 描述其类型 Children bool Field []Field // 包含的字段 } 不同的 entry 有不同的类型: tag compile unit, 在 go

  • Vue绑定内联样式问题

    使用 v-bind:style 可以给元素绑定内联样式,方法与:class类似,也有对象语法和数组语法,看起来很直接在元素上写CSS: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>示例</title> </head> <body> <div id="app"> <div :sty

  • C++实操之内联成员函数介绍

    目录 前言 什么是内联函数: 如何使一个函数成为内联: 为什么使用内联: 优点 : 缺点 : 关键点 : 总结 前言 在C语言中,我们使用了宏函数,这是编译器用来减少执行时间的一种优化技术.那么问题来了,在C++中,有什么更好的方法来解决这个问题呢?我们引入了内联函数,这是编译器用来减少执行时间的一种优化技术.我们将讨论内联函数的 "what, why, when & how". 什么是内联函数: 内联函数是C++的一个增强功能,可以减少程序的执行时间.函数可以通过指示编译器,

随机推荐