JS中的作用域(scope)

本文深入探讨了JavaScript中循环添加事件时闭包的影响,并提供了多种理解和解决此问题的方法,包括闭包的原理、作用域、传参方式等。通过实例分析,展示了如何避免内存泄漏和确保每个事件处理器都能正确获取对应的循环变量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、android:

// 注入js函数监听
    private void addCloseClickListner() { 
        wv_Show_Document.loadUrl(
                "javascript:" +
                        "(function()" +
                        "{" +
                        "   var objs = document.getElementsByClassName(\"icon-3\"); " +
                        "   for(var i=0;i<objs.length;i++)  " +
                        "   {" +
                        "       var imgstr = objs[i].getElementsByTagName(\"a\")[0].href;" +
                        "       objs[i].onclick= (function myclick(str)" +
                        "                        {" +
                        "                              return function(){ " +
                        "                                   window.imagelistner.openImage(str);" +
                        "                                   return false;" +
                        "                              };" +
                        "                        })(imgstr);" +
                        "  }" +
                        "}" +
                        ")()"
        );
    }

2、测试:

<!doctype html>
<html>
<head>
<script Language="JavaScript">
window.onload = function(){

    var objs = document.getElementsByClassName("icon-3"); 
    for(var i=0;i<objs.length;i++)  {
         //alert(objs[i].getElementsByTagName("a").href);
         var imgstr = objs[i].getElementsByTagName("a")[0].href;
         //console.log('--------------'+imgs[i]);
         objs[i].onclick= (function myclick(str){
                return function(){ //inner function 
                    console.log(str);
                    return false;
                };
         })(imgstr);
    }

}



function getElementsByClassName2(fatherId,tagName,className){
    node = fatherId&&document.getElementById(fatherId) || document;
    tagName = tagName || "*";
    className = className.split(" ");
    var classNameLength = className.length;
    for(var i=0,j=classNameLength;i<j;i++){
        //创建匹配类名的正则
        className[i]= new RegExp("(^|\\s)" + className[i].replace(/\-/g, "\\-") + "(\\s|$)"); 
    }
    var elements = node.getElementsByTagName(tagName);
    var result = [];
    for(var i=0,j=elements.length,k=0;i<j;i++){//缓存length属性
        var element = elements[i];
            while(className[k++].test(element.className)){//优化循环
            if(k === classNameLength){
                //result[result.length] = element;
                result[result.length] = element.getElementsByTagName("a")[0];
                break;
            }   
        }
        k = 0;
    } 

    return result;
}

function TEST(){
     console.log('TEST');
}    
</script>
<body onload="TEST">
<ul class="list-txt">
<li class="icon-3">
<a href="http://static.kocla.com/kocla/2015-08-04/8a20ae9c468ac4400146a85f5d17233e/document/8cfc0af88519489c835dcc297550ca65.pdf">产品使用说明页-音乐宝宝v1.0</a>
</li>
<li class="icon-3">
<a   href="http://static.kocla.com/kocla/2015-08-04/8a20ae9c468ac4400146a85f5d17233e/document/37e181cd880146a09f89000831350450.jpg">电子元器件学习入门知识</a>
</li>
<li class="icon-3">
<a href="www.sina.com.cn">新浪网</a>
</li>
</ul>

</body>
</head>
</html>

依次点击获得<a>的href:
这里写图片描述

3、其他
参考:JavaScript 循环添加事件时闭包的影响有哪些解法?
JavaScript 循环添加事件时闭包的影响有哪些解法?修改
网上搜到的关于该问题的一个方案是借一层函数避免问题
http://blog.youkuaiyun.com/victorn/article/details/3899261
不过到底还是很难理解.. 还有其他的方法去理解和解决吗?
更新: 我草草套了一层函数还好也避开了
修改
举报 1 条评论 分享 • 邀请回答
按投票排序
按时间排序
9 个回答

赞同
35
反对,不会显示你的姓名
杨志,很遗憾很少有pure jser或pure js questio…
[已重置]、等风来、jude tony 等人赞同
很高兴有一个纯JS的问题。
1,@杨咖啡 说的JS传参是传值不传址,其实不是这样的。JS中传参有两种方式:by value and by sharing.
像C,C++,Java,他们传参方式是by value 和 by reference。前者就是传值,后者是传址。而JS也是这样的,前者是传值,后者是传址。
By value是对于原始数据类型,例如int,char之类的;而By sharing 和By reference是对于高级数据结构,如Object,struct之类。我们可以想象到一个Object或是struct 不能仅仅通过传值进行传参。
一个简单的例子说明by reference和 by sharing的不同。

var bar;
var foo = bar;
bar = {'key' : 'value'};
console.log(foo , bar );

By sharing 中foo 是undefined , bar 是{‘key’ : ‘value’}; 而By reference 则应该两者都是{‘key’ : ‘value’}。

  1. 其实LZ要理解这个问题,要明白JS中的作用域(scope)。
    每个函数在创建完成时,他有3个重要的内置属性(property)也同时被创建。
    {
    AO //记录function内的变量,参数等信息
    this // 就是在调用this.xx的时候的this
    scope // 指向外层函数AO的一个链(在实现的时候,可能通过数组来实现).
    }
    JS中,大家经常讲的Scope其实是这样:SCOPE=AO+scope.
    回到闭包的问题上:
    如果我们这样写这个程序:
for(var i =0; i<link.length; i++){ //window scope
link[i].onclick = function(){ alert(i); }; // inner function 
}

可以得到inner function的SCOPE是这样的:

{
AO
this // 等于link[i]
scope // 指向window的记录,包括我们需要的变量i
}
这个for循环会立即执行完毕,那么当onclick触发时,inner function查找变量 i 时,会在AO+scope中找,AO中没有,scope中的变量i已经成为了link.length.

利用大家所说的闭包写这个程序:

//here is the window scope
for(var i =0; i<link.length; i++){ 

link[i].onclick = (function(i){ // outer function 
return function(){ //inner function 
alert(i);
};
})(i);
}

分析inner function的SCOPE:
{
AO // no important infomation
this // we don’t care it.
scope //outer function and window scope
}
outer function的SCOPE
{
AO // 包含参数i
this // don’t care it .
scope // window scope.
}

这时,如果inner function被触发,他会从自己的AO以及scope(outer function的AO 和 window scope)中找寻变量i. 可以看到outer function的AO中已经包含了i,而且对于这个for循环,会有对应有N个(function(){})() 被创建执行。所以每个inner function都有一个特定的包含了变量 i 的outer function。

这样就可以顺利输出0,1,2,3。。。。。。。。。

结论: 我们可以看到,闭包其实就是因为Scope产生的,所以,广义上来讲,所有函数都是闭包。

另外,这里面也包含了,this, function expression 和function declaration的区别,这里就不一一讲了。

  1. 另外一种方法:
    利用 dom onclick事件的bubble特性,也就是@xiiiiiin所讲的弄个代理。

在link dom节点的父节点上定义onclick事件监听。参数为e(其他的名字也可以,但要有参数)。 这样我们通过e.target就可以知道是那个子节点被click了,也可以做相应的处理。
这是一个比较好的方法。(闭包有时会产生内存泄漏)。

大概就说这么多吧,还要上班呢。希望对LZ有用。如果哪里错了,也请多多批评指正。
发布于 2012-01-13 18 条评论 • 作者保留权利

赞同
6
反对,不会显示你的姓名
松鼠奥利奥,← 我厂招聘 Web 前后端开发
孟达、fankyC、杨兴洲 等人赞同
我觉得最好的方式就是通过包装一层函数来解决。

将原来的
alink.onclick = function(){alert(i)};
改成:
(function(i) { alink.onclick = function(){alert(i)}; })(i);

我觉得这是最好的方法了,js 中只有 function 才会划分作用域(和 python 有点像),if/else、for 循环都不会划分作用域,所以原来的方式六次循环引用的都是同一个变量 i,由于闭包绑定到 function 中去。
现在包装了一层之后,i 被传递到内层的匿名函数 local 作用域中去,所以六次循环都会建立独立的 i (因为是六个不同的作用域)。
发布于 2012-01-12 5 条评论 • 作者保留权利

赞同
2
反对,不会显示你的姓名
依云,摆脱过去,梦想渐近。四处展望,行业发达…
死跑龙套的、松鼠奥利奥 赞同
不看那文章你的问题还真难理解。
你把参数进去嘛:

alink.onclick = (function(i){
return function(){
alert(i);
};
})(i);

另外我不喜欢 onxxx 属性。

PS: 知乎用富文本编辑器弄得我贴代码都麻烦,另外,答案的后部分第 N 次消失看不到了。

编辑于 2012-01-12 3 条评论 • 作者保留权利

赞同
3
反对,不会显示你的姓名
杨奇超,你快长肉啊
梁新宇、泛泛而谈、陈框框 赞同
这跟JS函数的传参方式和事件的赋值方式有关。
1、JS函数传参是传值不传址的。
2、onclick的值应该给一个函数声明,事件触发时只会传一个event参数给声明的函数。

如果在循环中使用alink[i].onclick = function() { alert(i); };
i 不是这个匿名函数的参数,是传址进去的,当onclick事件触发的时候循环已经结束了,i 已经是最后一个值了。
如果声明 function(i) { alert(i); }
那这个 i 就指代了 event,这时候事件触发的时候只会弹出触发的事件名。

而使用alink[i].onclick = (function(_i) { return function() { alert(_i); } })(i);
这里是执行外层的匿名函数,返回内层的这个匿名函数传给onclick。
这里注意外层函数是立即执行的,带一个参数,是我们传给它的,而不是事件触发器。
内层函数是不带参数的,事件执行时触发器会传给它一个event值。
对循环中的每一个 i 都会生成一个匿名函数,i 作为生成的匿名函数的参数,是传值的。
相当于循环中当 i = 2 的时候,生成了这样一个函数:function() { alert(2); }; 赋值给了alink[2].onclick,即 alink[2].onclick = function() { alert(2); };
这才是我们想要的。

PS:闭包只是个手法,而不是解决问题的核心所在。
这种手法跟下面的方法是等价的,而下面并没有用闭包。

var al = function(param) {
return function() {
alert(param);
}
}

循环中alink[i].onlick = al(i);
编辑于 2012-01-12 添加评论 • 作者保留权利

赞同
1
反对,不会显示你的姓名
彬仔,码农
whiletrue 赞同
呃……这例子,更偏向于变量作用域的问题吧
发布于 2012-01-13 1 条评论 • 作者保留权利

赞同
0
反对,不会显示你的姓名
匿名用户
在这里我觉得排名第一的回答太过于晦涩,一般人根本看不懂。
在这里我提出一个比较有意思的猜想,
//————————————————————————————————————————
//贴出代码

http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>无标题文档</title>
<style>
li{height:20px;background:#222;width:200px;border-bottom:2px solid #fff;}
</style>
</head>

<body>
<ul id='ul1'>
<li></li>
<Li></Li>
<li></li>
<Li></Li>
</ul>
<script>
var oUl1=document.getElementById('ul1');
//alert(oUl1);
var aLi=document.getElementsByTagName('li');
var length=aLi.length;
for(var i=0;i<length;i++)
{

aLi[i].onclick=(function (index){
var y=i;
return function (){

// alert(index);
alert(y);
}; 
}
)(i);
}


//提出猜想,引用次数加一变成2不再有跨作用域问题

//猜测正确
</script>
</body>
</html>

//————————————————————————————————————
如你所见,index缓冲值就是大多数人所采用的方法,这是一种非常靠谱的做法,但是很少有人能说的清为什么,而是用所谓的专业来让你困惑。

//————————————————————————————————————
其实这个也是偶然间看到垃圾回收机制,引用次数所提出的猜想,
正常情况下,如果采用var y=i,然后继续在return fn里引用的话,确实只能引用到最后一个值,因为大多数情况下,0,1,2…..这些序列号只会被一个变量保存,大家也不会想到用另一个变量再次保存,
但是现在我们用y再次引用,让它的次数变成了2,这样所谓的跨作用域就不存在了,我们可以引用到每个值。
//————————————————————————————
当然了这只是个猜想,只是在这里得到了验证,至于能否继续,还要继续考察。
发布于 2014-10-28 1 条评论 • 作者保留权利

赞同
0
反对,不会显示你的姓名
xiiiiiin,WEB DEVELOPER…
做个代理嘛。
循环添加每个节点的attribute,然后在外层elem绑一个事件。
能不用闭包就不用嘛
发布于 2012-01-12 3 条评论 • 作者保留权利
包义德,编程,文学
重新思考了一下这个问题,它的关键在于弄清JavaScript变量作用域和作用域链,前面的回答似乎都没有解释清楚。

《JavaScript: The Good Parts》中提到了这个问题,看这个例子:

// BAD EXAMPLE
var addActions = function (nodes) {
for (var i = 0; i < nodes.length; i++) {
nodes[i].onclick = function (e) {
alert(i);
};
}
};
// END BAD EXAMPLE

解释是这样的:
函数的本意是给每个事件处理器不同的 i 。但它未能达到目的,因为事件处理器函数绑定了 i 本身,而不是函数在构造时的变量 i 的值。

之所以是这样的,看过《JavaScript: The Definitive Guide》就会明白:
所有的JavaScript函数都是闭包:它们都是对象,都关联到作用域链。每次调用JavaScript函数的时候,都会为之创建一个新的对象来保存局部变量,并把这个对象添加至作用域链中(简单理解:作用域链就是对象列表)。
这个糟糕的例子中function (e) { alert(i); }都会在同一个函数调用中定义,因此它们共享变量 i 。也就是说它们的作用域链里面的 i 都是同一个 i 。即关联到闭包的作用域链都是“活动的”。

《JavaScript: The Good Parts》当然也给出了解决方案:

var addActions = function (nodes) {
var helper = function (i) {
return function (e) {
alert(i);
};
};

for (var i = 0; i < nodes.length; i++) {
nodes[i].onclick = helper(i);
}
};

解释: 在循环之外创建一个辅助函数,而不是在循环中创建函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值