首页  >  问答  >  正文

javascript - 一道前端面试题,用纯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); }

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

巴扎黑巴扎黑2749 天前421

全部回复(7)我来回复

  • PHPz

    PHPz2017-04-10 13:14:08

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

    看看你的代码:

     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把几种方案混用了哦(包括在博客中),其实是没有必要的。


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

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

    回复
    0
  • 大家讲道理

    大家讲道理2017-04-10 13:14:08

    这样?
    http://jsfiddle.net/fakefish/5QvkA/1/

    回复
    0
  • 阿神

    阿神2017-04-10 13:14:08

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

    el.parentNode.children.indexOf(el)
    

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

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

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

    You might not need JQuery

    回复
    0
  • 天蓬老师

    天蓬老师2017-04-10 13:14:08

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

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

    回复
    0
  • PHP中文网

    PHP中文网2017-04-10 13:14:08

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

    回复
    0
  • 大家讲道理

    大家讲道理2017-04-10 13:14:08

    
    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);
    
    

    回复
    0
  • 大家讲道理

    大家讲道理2017-04-10 13:14:08

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

        
        list.forEach(function(item,index){
            item.onclick = function(){
                console.log(index);
            }
        });

    回复
    0
  • 取消回复