刷题 | 牛客 - 前端面试手撕题 - 中等 - 1-2/20 知识点解答
知识点总结:
DOM0级标准事件(onclick)——》dom.onclick。 如 document.querySelector('ul').onclick
事件委托,event.target 指当前点击的这个元素
FED1 事件委托
描述
请补全JavaScript代码,要求如下:
1. 给"ul"标签添加点击事件
2. 当点击某"li"标签时,该标签内容拼接"."符号。如:某"li"标签被点击时,该标签内容为".."
注意:
1. 必须使用DOM0级标准事件(onclick)
解答
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><style>/* 填写样式 */ul {list-style: none;padding: 0;}li{cursor: pointer;font-size: 20px;padding: 5px;}li: hover{background-color: #f0f0f0;}</style>
</head><body><ul><li>.</li><li>.</li><li>.</li></ul><!-- 填写标签 --><script type="text/javascript">// 填写JavaScriptdocument.querySelector('ul').onclick = event => {// 法一:事件委托 + tagNameif(event.target.tagName === 'LI'){event.target.innerHTML += '.';} // 法二:事件委托+innerText,潜在问题:点击到非li的ul区域内,会在空白处添加.event.target.innerText += '.'// 法三:事件委托 + nodeName,将其转小写event = event || window. Event;if(event.target.nodeName.toLowerCase() === 'li'){event.target.innerText += '.'}}// 拓展:选中所有的 li,遍历每个li, 给它绑定点击事件// 结果:对本事件无效。原因:样例输入也是 DOM0级,每个事件只能绑定一次,后面会覆盖前面的。/* const lis = document.querySelectorAll('li');lis.forEach(li => {li.onclick = function(){this.innerHTML += '.';}}) */</script>
</body></html>
分析:
法一:事件委托 + tagName
if (event.target.tagName === 'LI') { event.target.innerHTML += '.'; }
-
tagName
返回元素的标签名(HTML 下总是大写,比如"UL"
,"LI"
)。 -
所以必须写成
"LI"
才能匹配。 -
优点:直观、常见。
-
缺点:大小写敏感,不同文档类型(HTML vs XHTML)可能行为不同。
法二:事件委托 + innerText
event.target.innerText += '.';
-
不做判断,直接加点。
-
风险:如果点到的是
<ul>
空白区,event.target
变成<ul>
,那就会在整个<ul>
的文本后面加点,逻辑错误。 -
优点:代码简洁。
-
缺点:不安全,容易误操作。
法三:事件委托 + nodeName.toLowerCase()
event = event || window.event; // 兼容性写法
if (event.target.nodeName.toLowerCase() === 'li') { event.target.innerText += '.'; }
✨ 重点解释:
-
nodeName
是什么?-
DOM 节点的属性,返回该节点的名字。
-
对于 元素节点(
nodeType === 1
),返回的就是标签名。 -
在 HTML 文档中,返回的大写字符串(例如
"LI"
,"UL"
)。 -
在 XML / XHTML 中,
nodeName
会区分大小写(比如你写<Li>
就真的是"Li"
)。
-
-
为什么转小写?
-
保证兼容性,不受 HTML / XHTML / XML 的大小写差异影响。
-
写成
nodeName.toLowerCase() === 'li'
就能稳定匹配。
-
-
和
tagName
的区别:-
tagName
只对 元素节点有意义。 -
nodeName
对 所有节点都有意义:-
元素节点:返回标签名 (
DIV
,LI
) -
文本节点:返回
#text
-
注释节点:返回
#comment
-
-
所以
nodeName
更通用。
-
-
这里的作用:
-
确认点击目标确实是
<li>
元素,而不是<ul>
或其它节点。 -
toLowerCase()
保证跨浏览器 / 文档类型下都能稳定匹配。
-
✅ 总结:
法一:常规做法,简单直接,但大小写固定要写
"LI"
。法二:简洁,但有潜在 bug(点空白会误加)。
法三:最稳妥的写法 ——
用
nodeName
判断,适用于更多情况。转小写避免大小写不一致问题。
推荐在写通用组件 / 库的时候用这种方式。
知识点:事件委托(event) & DOM0级标准事件(onclick)
在 HTML 中,tagName
默认返回 大写
关键点: event.target
-
event.target
表示 实际被点击的那个元素
👉 在 DOM 里,点击“空白”其实也是点到某个元素。比如:
点击
<li>
→event.target
是这个<li>
点击
<ul>
的 padding / margin 区域 →event.target
是<ul>
本身
事件委托(event.target)
只绑定一次事件在
ul
上,节省内存,适合元素很多、动态添加的场景。逐个绑定(①const lis = document.querySelectorAll('li'); ②lis.forEach(li => { li.onclick = function(){} }))
每个
<li>
单独有事件,代码直观,但如果<li>
很多,性能稍差。
FED2 数组去重
描述
请补全JavaScript代码,要求去除数组参数中的重复数字项并返回该数组。
注意:
1. 数组参数仅包含数字
示例1
输入:
_deleteRepeat([-1,1,2,2])
输出:
[-1,1,2]
法一:ES6 的 Set (最简洁,O(n))
return [...new Set(array)];
或者
return Array.from(new Set(array));
new Set(array) 返回类数组对象,Array.from 将类数组对象转为真正的数组。
法二:传统写法(进阶)(如 新数组 + includes)
const arr = [];
array.forEach(item => {if(!arr.includes(item)){arr.push(item);}
})
forEach 是元素值循环,即 item是元素值 —》forEach 第一个参数是 元素值,第二个参数是 索引。——》forEach((item, index) => ...)
for是索引循环
时间复杂度是 O(n²),对大数组性能不太好
法三:算法思路(排序+相邻比较)
array.sort((a, b)=> a - b);
const arr = array.filter((item, i) => i === 0 || item !== array[i - 1])l
先用.sort 进行排序,再用 filter 进行过滤,保留第一个元素 以及 之后的 元素与前一个元素不同的,即不重复的;否则,过滤掉。
时间复杂度是 O(n log n)。
但是会改变原数组顺序,适合只关注结果、不关心顺序的情况。
法四:哈希表(性能更优)
const seen = {};
const arr = array.filter(item => {if(!seen[item]){seen[item] = true;return true;}return false;
});
使用 普通对象 当作哈希表,时间复杂度是O(n),性能更好。
缺点:对象键会被转为字符串,比如数字 1 和字符串 "1" 会冲突。——》解决:使用 Map
法五: ★ 对象数组去重(★ 实战场景:Map按 id 去重)
数组里存的是对象:[{id:1,name:'A'},{id:1,name:'A'},{id:2,name:'B'}] 。
使用 Map 按 id 去重
const arr = [...new Map(array.Map(item => [item.id, item])
).values()];
保留第一次出现的对象,同时去除重复id的对象。
在实际项目中常见,如接口返回的用户列表去重。
总结:最简洁 用 Set——》思路 用 排序 ——》性能 用 哈希表或Map ——》 实际开发常见 对象数组按 id 去重。
所有的代码,以及其他方法(上述是几个重要的方法):
const arr = [...new Map(array.Map(item => [item.id, item])
).values()];<!DOCTYPE html>
<html><head><meta charset=utf-8></head><body><script type="text/javascript">const _deleteRepeat = array => {// 补全代码/* 数组去重 */// 法一:arr.includes + push(不重复者 添加到新数组中)[以下4种 即可]let arr = []let len = array.length;/*for(let i=0;i < len;i++){if(!arr.includes(array[i])){arr.push(array[i]);}}return arr; */// 注意点:forEach 中的参数 item 是对应的值/* array.forEach(item => {if(!arr.includes(item)){arr.push(item);}})return arr;*/// map 中属性名不可重复/* const map = new Map();array.forEach(item => {if(!map.has(item)){map.set(item, true);arr.push(item);}})return arr; */// 对象属性名不可重复/* const obj = {}array.forEach(item => {if(!obj[item]){obj[item] = true;arr.push(item);}})return arr;*/// 法二:Set + Array.from 方法(es6方法):new Set(array) 返回类数组,可以用 Array.from 转为真正的数组// return Array.from(new Set(array));return [...new Set(array)];// 法三:数组的 reduce 累加器(累加数,当前的值)/* const newArr = array.reduce((pre, cur) => {if(pre.includes(cur) === false){pre.push(cur);}return pre;}, []);return newArr;*/}</script></body>
</html>
知识点:数组去重总结,见 手撕代码 | 知识点总结 - 数组去重-CSDN博客