一道经典JS面试题
见过这道题吗?
<!DOCTYPE html> <html> <head> <meta name="description" content="iMagic"> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <title>JS Bin</title> </head> <body> <h2>点击以下三个li,输出分别是什么?</h2> <ul> <li>Item 0</li> <li>Item 1</li> <li>Item 2</li> </ul> <script> var lis = document.getElementsByTagName('li'); for (var i = 0; i < lis.length; i++) { lis[i].onclick = function (event) { console.log('你点击了 ' + i); } } </script> </body> </html>
第一个问题,点击三个li,console输出结果是什么?(答错扣30分)
# 聪明的你这时点击了三个li
"你点击了 3"
"你点击了 3"
"你点击了 3"
(⊙⊙!)
第二个问题,为什么输出总是“3”呢?(答错扣30分,可以考虑提前结束面试)
我们祭出console.log大法:
var lis = document.getElementsByTagName('li'); for (var i = 0; i < lis.length; i++) { console.log('添加第' + i + '个onclick listener'); lis[i].onclick = function (event) { console.log( '只有点击时才进入onclick这个function, 这时i已经变成了' + i); console.log('你点击了 ' + i); } console.log('i这时变成了' + i); } console.log('i在for循环结束后变成了' + i);
"添加第0个onclick listener"
"i这时变成了0"
"添加第1个onclick listener"
"i这时变成了1"
"添加第2个onclick listener"
"i这时变成了2"
"i在for循环结束后变成了3"
# 聪明的你这时点击了一个li
"只有点击时才进入onclick这个function, 这时i已经变成了3"
"你点击了 3"
第三个问题,如何修复这段代码,确保在点击Item 0, 1, 2的时候分别输出0, 1, 2?(答不出来扣40分,结束面试,每给出一种解法加30分)
第一种解法:利用closure对每一个 onclick event handler 建立一个其专属的 function,并在建立每个function的时候保存住 i 的值:
var lis = document.getElementsByTagName('li'); for (var i = 0; i < lis.length; i++) { lis[i].onclick = (function (invokedi) { console.log('现在建立对于Item' + invokedi + '的event handler'); return function uniqEventHandlerFori(event){ console.log( 'invokedi是在建立event handler时传入的,它的值是' + invokedi); console.log('你点击了 ' + invokedi); } }(i)) }
结果:
"现在建立对于Item0的event handler"
"现在建立对于Item1的event handler"
"现在建立对于Item2的event handler"
# 聪明的你这时点击了三个li
"invokedi是在建立event handler时传入的,它的值是0"
"你点击了 0"
"invokedi是在建立event handler时传入的,它的值是1"
"你点击了 1"
"invokedi是在建立event handler时传入的,它的值是2"
"你点击了 2"
第二种解法:利用 Function.prototype.bind(thisArg, params...) 对每一个 onclick event handler 建立一个其专属的 function,并在建立每个function的时候保存住 i 的值:
var lis = document.getElementsByTagName('li'); for (var i = 0; i < lis.length; i++) { lis[i].onclick = function handlerToBind (event){ console.log( 'function handlerToBind(){...}.bind(i) --> ' + ' 建立了一个新的function,并且在这个函数里面,' + 'this === 运行bind(i)时i的值 === ' + this); console.log('你点击了 ' + this); }.bind(i); }
结果:# 聪明的你这时点击了三个li
"function handlerToBind(){...}.bind(i) --> 建立了一个新的function,并且在这个函数里面,this === 运行bind(i)时i的值 === 0"
"你点击了 0"
"function handlerToBind(){...}.bind(i) --> 建立了一个新的function,并且在这个函数里面,this === 运行bind(i)时i的值 === 1"
"你点击了 1"
"function handlerToBind(){...}.bind(i) --> 建立了一个新的function,并且在这个函数里面,this === 运行bind(i)时i的值 === 2"
"你点击了
2"
第三种解法
:event.target
拿到实际被点击的 li,然后利用 Array.prototype.indexOf(item) 去查找它的index。
var lis = document.getElementsByTagName('li'); console.log( '通过document.getElementsByTagName返回的lis' + '并没有返回真正的Array, 只是一个Array-like object. ' + '我们需要把它构建成一个真正的Array才能在待会用indexOf方法. ' ); var lisArray = Array.prototype.slice.call(lis); console.log('现在你可以愉快的使用lisArray.indexOf(item)了'); for (var i = 0; i < lis.length; i++) { lis[i].onclick = function handlerToBind (event){ console.log( 'event.target 就是被点击的 DOM Element: ' + event.target.innerHTML + '. 它存在于lis和lisArray中'); console.log('你点击了 ' + lisArray.indexOf(event.target)); }; }
结果:
"通过document.getElementsByTagName返回的lis并没有返回真正的Array, 只是一个Array-like object. 我们需要把它构建成一个真正的Array才能在待会用indexOf方法. "
"现在你可以愉快的使用lisArray.indexOf(item)了"
# 聪明的你这时点击了三个li
"event.target 就是被点击的 DOM Element: Item 0. 它存在于lis和lisArray中"
"你点击了 0"
"event.target 就是被点击的 DOM Element: Item 1. 它存在于lis和lisArray中"
"你点击了 1"
"event.target 就是被点击的 DOM Element: Item 2. 它存在于lis和lisArray中"
"你点击了 2"
(重点)第四种解法
:使用ES6的 let,把 i 限定在block level里面 (单独给出此解法加10分,如果同时给出第一或第二种解法,此解法加30分)
var lis = document.getElementsByTagName('li'); for (let i = 0; i < lis.length; i++) { lis[i].onclick = function (event) { console.log('你点击了 ' + i); } }
结果:
# 聪明的你这时点击了三个li
"你点击了 0"
"你点击了 1"
"你点击了 2"
参考:https://zhuanlan.zhihu.com/p/24024766