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


Posted in Javascript onJanuary 12, 2011

标准参考

无。

问题描述

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

Javascript 相关文章推荐
动态修改DOM 里面的 id 属性的弊端分析
Sep 03 Javascript
扩展JavaScript功能的正确方法(译文)
Apr 12 Javascript
chrome浏览器不支持onmouseleave事件的解决技巧
May 31 Javascript
ExtJS4给Combobox设置列表中的默认值示例
May 02 Javascript
理运用命名空间让js不产生冲突避免全局变量的泛滥
Jun 15 Javascript
JS获取图片高度宽度的方法分享
Apr 17 Javascript
jQuery实现控制文字内容溢出用省略号(…)表示的方法
Feb 26 Javascript
JavaScript知识点总结(十一)之js中的Object类详解
May 31 Javascript
BootStrap轮播HTML代码(推荐)
Dec 10 Javascript
jQuery实现简单的下拉菜单导航功能示例
Dec 07 jQuery
微信小程序实现多行文字超出部分省略号显示功能
Oct 23 Javascript
ES6如何用一句代码实现函数的柯里化
Jan 18 Javascript
克隆javascript对象的三个方法小结
Jan 12 #Javascript
JavaScript中两种链式调用实现代码
Jan 12 #Javascript
Chrome中JSON.parse的特殊实现
Jan 12 #Javascript
js中将字符串转换成json的三种方式
Jan 12 #Javascript
Javascript在IE下设置innerHTML时出现未知的运行时错误的解决方法
Jan 12 #Javascript
基于JQuery实现相同内容合并单元格的代码
Jan 12 #Javascript
jquery EasyUI的formatter格式化函数代码
Jan 12 #Javascript
You might like
php扩展ZF――Validate扩展
2008/01/10 PHP
php excel reader读取excel内容存入数据库实现代码
2012/12/06 PHP
php字符比较函数similar_text、strnatcmp与strcasecmp用法分析
2014/11/18 PHP
PHP之预定义接口详解
2015/07/29 PHP
php使用flock阻塞写入文件和非阻塞写入文件的实例讲解
2017/07/10 PHP
Avengerls vs KG BO3 第一场2.18
2021/03/10 DOTA
取得父标签
2006/11/14 Javascript
Javascript 面向对象 继承
2010/05/13 Javascript
jQuery Validation插件remote验证方式的Bug解决
2010/07/01 Javascript
IE6背景图片不缓存问题解决方案及图片使用策略多个方法小结
2012/05/14 Javascript
jQuery之DOM对象和jQuery对象的转换与区别分析
2015/01/08 Javascript
javascript和jquery实现用户登录验证
2016/05/04 Javascript
JS实现课堂随机点名和顺序点名
2017/03/09 Javascript
JS实现的倒计时恢复按钮点击功能【可用于协议阅读倒计时】
2018/04/19 Javascript
vue组件name的作用小结
2018/05/23 Javascript
js 实现在2d平面上画8的方法
2018/10/10 Javascript
原生JS实现贪吃蛇小游戏
2020/03/09 Javascript
uni-app微信小程序登录授权的实现
2020/05/22 Javascript
Vue Elenent实现表格相同数据列合并
2020/11/30 Vue.js
python读取excel指定列数据并写入到新的excel方法
2018/07/10 Python
python 将list转成字符串,中间用符号分隔的方法
2018/10/23 Python
python中bs4.BeautifulSoup的基本用法
2019/07/27 Python
日本最新流行服饰网购:Nissen
2016/07/24 全球购物
英国最大的宠物食品和宠物用品网上零售商: Zooplus
2016/08/01 全球购物
乌克兰排名第一的在线旅游超市:Farvater.Travel
2020/01/02 全球购物
Hibernate持久层技术
2013/12/16 面试题
校园安全广播稿
2014/02/08 职场文书
学校庆元旦歌咏比赛主持词
2014/03/18 职场文书
中学生英语演讲稿
2014/04/26 职场文书
社会实践先进工作者事迹材料
2014/05/06 职场文书
组织鉴定材料
2014/06/02 职场文书
2015年百日安全活动总结
2015/03/26 职场文书
2016年基层党组织创先争优承诺书
2016/03/25 职场文书
mysql批量新增和存储的方法实例
2021/04/07 MySQL
go语言基础 seek光标位置os包的使用
2021/05/09 Golang
OpenCV-Python实现轮廓拟合
2021/06/08 Python