刷题 | 牛客 - 前端面试手撕题 - 中等 - 1-2/20 知识点&解答

知识点总结:

DOM0级标准事件(onclick)------》dom.onclick。 如 document.querySelector('ul').onclick

事件委托,event.target 指当前点击的这个元素


FED1 事件委托

描述

请补全JavaScript代码,要求如下:

  1. 给"ul"标签添加点击事件

  2. 当点击某"li"标签时,该标签内容拼接"."符号。如:某"li"标签被点击时,该标签内容为".."

注意:

  1. 必须使用DOM0级标准事件(onclick)

解答

html 复制代码
<!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">
        // 填写JavaScript
        
        document.querySelector('ul').onclick = event => {
            // 法一:事件委托 + tagName
            if(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 += '.'; }

✨ 重点解释:

  1. nodeName 是什么?

    • DOM 节点的属性 ,返回该节点的名字

    • 对于++元素节点nodeType === 1),返回的就是标签名++。

    • 在 HTML 文档中,返回的大写字符串(例如 "LI", "UL")。

    • 在 XML / XHTML 中,++nodeName 会区分大小写++ (比如你写 <Li> 就真的是 "Li")。

  2. 为什么转小写?

    • 保证兼容性,不受 HTML / XHTML / XML 的大小写差异影响。

    • 写成++nodeName.toLowerCase() === 'li'++就能稳定匹配。

  3. tagName 的区别:

    • tagName 只对 元素节点 有意义。

    • nodeName所有 节点都有意义:

      • 元素节点:返回标签名 (DIV, LI)

      • 文本节点:返回 #text

      • 注释节点:返回 #comment

    • 所以 nodeName 更通用。

  4. 这里的作用:

    • 确认点击目标确实是 <li> 元素,而不是 <ul> 或其它节点。

    • toLowerCase() 保证跨浏览器 / 文档类型下都能稳定匹配。


✅ 总结:

  • 法一 :常规做法,简单直接,但大小写固定要写 "LI"

  • 法二:简洁,但有潜在 bug(点空白会误加)。

  • 法三:最稳妥的写法 ------

    • nodeName 判断,适用于更多情况。

    • 转小写避免大小写不一致问题。

    • 推荐在写通用组件 / 库的时候用这种方式。


知识点:事件委托(event) & DOM0级标准事件(onclick)

在 HTML 中,tagName 默认返回 大写

关键点: event.target

  • ++event.target 表示 实际被点击的那个元素++

👉 在 DOM 里,点击"空白"其实也是点到某个元素。比如:

  • 点击

  • → event.target 是这个
  • 点击

      的 padding / margin 区域 → event.target 是
        本身


  • 事件委托(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))

javascript 复制代码
return [...new Set(array)];
或者
return Array.from(new Set(array));

new Set(array) 返回类数组对象,Array.from 将类数组对象转为真正的数组。


法二:传统写法(进阶)(如 新数组 + includes)

javascript 复制代码
const arr = [];
array.forEach(item => {
    if(!arr.includes(item)){
        arr.push(item);
    }
})

forEach 是元素值循环,即 item是元素值 ---》forEach 第一个参数是 元素值,第二个参数是 索引。------》forEach((item, index) => ...)

for是索引循环

时间复杂度是 O(n²),对大数组性能不太好


法三:算法思路(排序+相邻比较)

javascript 复制代码
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)

但是会改变原数组顺序,适合只关注结果、不关心顺序的情况。


法四:哈希表(性能更优)

javascript 复制代码
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 去重

javascript 复制代码
const arr = [...new Map(
    array.Map(item => [item.id, item])
).values()];

保留第一次出现的对象,同时去除重复id的对象。

在实际项目中常见,如接口返回的用户列表去重。


总结 :最简洁 用 Set ------》思路 用 序 ------》性能 用 哈希表或Map ------》 实际开发常见 对象数组按 id 去重


所有的代码,以及其他方法(上述是几个重要的方法):

html 复制代码
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博客


相关推荐
再吃一根胡萝卜4 小时前
08 · 前端:Vue3 + SSE 流式与执行链路可视化
面试
再吃一根胡萝卜4 小时前
03 · 后端:FastAPI 与分层架构
面试
mldong6 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排7 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60617 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角8 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!8 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰9 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
Interview Aid1129 小时前
Walmart Global Tech SDE 三轮面经|基础、并发、压力面
面试·职场和发展
mayaairi10 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js