一道前端面试题,用纯js返回点击元素在父级元素内的位置

类似jquery 的 .index();
就一级,类似

<ul id="myul">
<li>click</li>
<li>click</li>
<li>click</li>
<li>click</li>
<li>click</li>
</ul>

var nodeList = document.getElementsByTagName('li'); for(var i = 0;i<nodeList.length;i++){ //nodeList[i].index = i nodeList[i].addEventListener("click", function(e){ console.log(nodeList.indexOf(nodeList[i])) }, false); }

我写的貌似有了闭包的问题,更正我的代码也好,我乱掉了。。。。
请问要如何实现呢?

阅读 12.3k
7 个回答

比起“闭包”问题,我更倾向于它是一个作用域的问题。
不少前端都错用了“闭包”这个词,本文将尽量避免使用这个词。

看看你的代码:

 var nodeList = document.getElementsByTagName('li');
 for (var i = 0; i < nodeList.length; i++) {
    nodeList[i].addEventListener("click", function(e) {
        console.log(nodeList.indexOf(nodeList[i]))
    }, false);
 }

ECMAScript规定,独立作用域只能通过“函数(function)”代码类型的执行上下文创建。
其他方式,for循环,{}代码块,定义函数,全部都不会产生新的作用域。

上述代码中有没有新的作用域?有的,你用了一些形如function(e){}的匿名函数字面量,在执行时,每一个都会创建一个新的作用域。
你的这些匿名函数字面量不会立即执行,它被设定为,在点击时才执行,这里它仅仅是被定义,被当做参数传入addEventListener函数而已。

在你的代码中,i和nodeList处于你的代码的顶级作用域之中,这个i在内存中只有一份。很显然,你的问题出在“为何让五个回调函数公用一个变量i”。
——当你点击的时候,for循环早就已经执行完毕,调用会沿着作用域链,逐级往上找,直到找到那个已经用完到达5的i。

所以我们要解决这个问题,必须缓存i。

解法

1

以下方案在于,在于使用立即执行函数,通过函数的实参缓存i:

var nodeList = document.getElementsByTagName('li');
for (var i = 0; i < nodeList.length; i++) {
    nodeList[i].onclick = (function(j){
        return function(e) {
            alert(j)
        };
    })(i);
}

2

var nodeList = document.getElementsByTagName('li');
for (var i = 0; i < nodeList.length; i++) {
    (function(j){
        nodeList[j].addEventListener("click", function(e) {
            alert(j)
        }, false);
    })(i) ;
}

3

以下方案其实是使用函数调用缓存实参的变种,它将index缓存在forEach循环的函数调用中:

// 兼容性提示:注意ES5的forEach方法的兼容性
 var nodeList = document.getElementsByTagName("li") ,
     arr = Array(nodeList.length+1).join("*").split("") ;
 arr.forEach(function(val,i){
     nodeList[i].addEventListener("click", function() {
         alert(i) ;
     }, false);
 })

之前有个答案用到了数组,其实不需要再额外用立即执行函数。

下面是forEach的实现方式(并不完全,没有包含对边界状态的控制)。
我们可以看到,forEach循环在遍历过程中调用了fn.call(),通过函数调用,构建了新的函数上下文,缓存了函数当时的实参k。

Array.prototype.forEach = function(fn, context) {
    for (var k = 0, length = this.length; k < length; k++) {
        fn.call(context, this[k], k, this);
    }
};

4

以下方案在于,把index值缓存在DOM属性中:

var nodeList = document.getElementsByTagName("li");
for (var i = 0; i < nodeList.length; i++) {
   nodeList[i].setAttribute("data-i",i) ;
   nodeList[i].addEventListener("click", function(e) {
       alert(this.getAttribute("data-i"))
   }, false);
}

5

// 兼容性提示:注意HTML5 data api的兼容性
var nodeList = document.getElementsByTagName("li");
for (var i = 0; i < nodeList.length; i++) {
   nodeList[i].dataset["i"] = i ;
   nodeList[i].addEventListener("click", function(e) {
       alert(this.dataset["i"])
   }, false);
}

6

以下方案在于,把index值缓存在HTMLLIElement对象中(也包括Fakefish的答案),将变量查找转为属性查找:

var nodeList = document.getElementsByTagName('li');
for (var i = 0; i < nodeList.length; i++) {
   nodeList[i].index = i ;
   nodeList[i].addEventListener("click", function() {
       alert(this.index)
   }, false);
}

7

以下方案在于利用ES5中的indexOf方法,只是需要预先将DOM元素转为Array:

// 兼容性提示:注意ES5的indexOf方法的兼容性
// 兼容性提示:DOM对象和JS在低版本IE是无法互相转化的
var nodeList = document.getElementsByTagName('li') ,
    arrNodes = Array.prototype.slice.call(nodeList) ,
    onclickfunc = function(evt){
        alert( arrNodes.indexOf(evt.target) )
    } ;

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

8

// 兼容性提示:注意ES5的indexOf方法的兼容性
// 兼容性提示:DOM对象和JS在低版本IE是无法互相转化的
var nodeList = document.getElementsByTagName('li') ,
    arrNodes = Array.prototype.slice.call(nodeList) ,
    nodeUls = document.getElementsByTagName('ul') ;

nodeUls[0].addEventListener("click",function(evt){
    alert(arrNodes.indexOf(evt.target))
},false);

LZ把几种方案混用了哦(包括在博客中),其实是没有必要的。


本想扔几个链接详细解释词法作用域的,结果发现大部分原文都消失了。前端就是这样,好文章混杂在一大堆烂解释中,还动不动就消失……所以说做好知识储备是非常重要的。

还好汤姆大叔的博客还在^^,这个系列真是百推不厌。好好读一读其中的第十一篇到第十六篇吧:)

javascript不熟悉,代码未测试,但是我觉得应该是这个思路。
取得元素的父元素,再获取父元素所有的子元素,循环遍历来确认具体位置。

el.parentNode.children.indexOf(el)

如果是获取当前节点的所有相邻节点,可以这样搞:

Array.prototype.filter.call(el.parentNode.children, function(child){
  return child !== el;
});

这里有一个蛮有意思的东西你可以看看

You might not need JQuery

        [].slice.call(document.getElementById("myul").children).forEach(function(el, index){
            (function(index){
                el.addEventListener("click", function(){
                    alert(index);
                });
            }(index));
        });

我猜 你的面试官 是想看到这样的

楼主是作用域的问题,我猜这道题是网易研究院的


var ulNode=document.getElementById("myul");
var liNodes=ulNode.childNodes||ulNode.children;
for(var i=0;i<liNodes.length;i++){//不同浏览器中childNodes的返回值不同,兼容浏览器
  if(liNodes[i].nodeType!=1){
     ulNodes.removeChild(liNodes[i]);
  }
};
for(var i=0;i<liNodes.length;i++){
  liNodes[i].index=i;
};
ulNode.addEventListener('click',function(e){
    if(e.target.nodeName.toUpperCase()=="LI")
    alert(e.target.index);
  },false);

突然看到这个问题 虽然已经有好多人答了还是发了一波
var liList = document.getElementsByTagName('li');
var list = Array.prototype.slice.call(liList,0);

    
    list.forEach(function(item,index){
        item.onclick = function(){
            console.log(index);
        }
    });
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏