JS手撕:DOM操作 & 浏览器API高频场景详解

0 阅读4分钟

在前端开发中,我们经常会遇到一些重复且基础的需求——比如解析URL参数、给大量元素绑定点击事件、实现图片懒加载等。这些功能看似简单,但写得不够严谨就容易出现bug(比如中文参数乱码、事件绑定冗余、滚动加载卡顿)。

今天就整理了7个前端高频实用JS功能,用“通俗话+专业解析”的方式,把每个功能的原理、代码细节和使用场景讲透,还附上了可直接复制使用的优化版代码,新手也能快速套用。

一、URL参数解析:把URL里的参数“拆”成可直接用的对象

通俗解读

我们经常会看到这样的URL:https://xxx.com/list?page=1&size=10&keyword=前端,里面的page、size、keyword就是参数。这个功能就是把这些参数“拆出来”,变成一个对象(比如{page:1, size:10, keyword:"前端"}),不用我们手动去切割字符串,省心又不易错。

专业解析

核心利用浏览器原生的URL对象,它能自动解析URL的协议(http/https)、主机(xxx.com)、路径(/list)和查询参数(?后面的内容);再配合URLSearchParams处理查询参数,同时解决中文乱码(用decodeURIComponent解码)、空值、重复key、数字类型转换等常见问题。

完整代码(可直接复制)

function parseParam(url) {
  // 创建URL对象,自动解析协议、域名、路径、参数(原生API,无需手动切割)
  const urlObj = new URL(url);
  // 获取查询参数部分(即?后面的内容,不含?)
  const queryParams = new URLSearchParams(urlObj.search);
  const paramsObj = {};

  // 遍历所有查询参数,处理各种边界情况
  for (let [key, value] of queryParams.entries()) {
    // 空值处理:如果参数值是空字符串或null,统一赋值为true(比如?flag&name=xxx,flag对应true)
    if (value === '' || value == null) {
      paramsObj[key] = true;
    } else {
      // 解码参数:处理中文、特殊字符(比如%E5%89%8D%E7%AB%AF解码为“前端”)
      let val = decodeURIComponent(value);
      // 纯数字字符串转为数字类型(比如"123"转为123,避免后续使用时还要手动转换)
      val = /^\d+$/.test(val) ? parseFloat(val) : val;

      // 重复key处理:如果参数有多个相同key(比如?tag=js&tag=html),转为数组形式
      if (paramsObj.hasOwnProperty(key)) {
        paramsObj[key] = [].concat(paramsObj[key], val);
      } else {
        paramsObj[key] = val;
      }
    }
  }

  return paramsObj;
}

// 示例使用
const url = "https://api.example.com/data?id=123&name=张三&page=2&tag=js&tag=html&flag";
const params = parseParam(url);
console.log(params);
// 输出:{id: 123, name: "张三", page: 2, tag: ["js", "html"], flag: true}

关键注意点

  • URL对象是浏览器原生API,无需引入第三方库,兼容现代浏览器(IE不支持,需兼容IE可额外处理);

  • 中文参数必须用decodeURIComponent解码,因为URL中中文会被自动编码为%开头的字符;

  • 重复key(如?tag=js&tag=html)处理为数组,避免后面的参数覆盖前面的。

二、事件委托:一次绑定,搞定所有子元素的事件

通俗解读

如果页面有100个按钮,给每个按钮都绑定点击事件,会占用很多内存,而且新增按钮还得重新绑定。事件委托就是“找一个父容器”,只给父容器绑定一次事件,不管里面有多少个子元素(哪怕是后来新增的),点击子元素时都会触发父容器的事件,再通过判断点击的是哪个子元素,执行对应逻辑。

专业解析

利用DOM事件的“冒泡机制”(子元素的事件会向上传递给父元素),将事件绑定在父容器上,通过event.target获取真正被点击的子元素,再通过matches()方法匹配目标元素选择器,实现“一次绑定,多元素复用”,优化性能并简化代码。

完整代码(可直接复制)

/**
 * 事件委托(代理)
 * @param {string} eventType 事件类型 click/input 等(比如"click"、"input")
 * @param {string|Element} elDelegate 委托父元素(选择器字符串或DOM对象)
 * @param {string} selector 真正要触发的目标元素选择器(比如"#btn"、".item")
 * @param {Function} fn 触发的回调函数(this指向目标元素)
 */
function on(eventType, elDelegate, selector, fn) {
  // 1. 处理委托父元素:如果传入的是选择器字符串,自动转为DOM对象
  if (!(elDelegate instanceof Element) && typeof elDelegate === 'string') {
    elDelegate = document.querySelector(elDelegate);
  }

  // 安全判断:如果没找到父元素,直接退出,避免报错
  if (!elDelegate) return null;

  // 2. 给父元素绑定事件,利用事件冒泡机制
  elDelegate.addEventListener(eventType, (e) => {
    let el = e.target; // 真正被点击/触发的元素(子元素)

    // 3. 向上查找匹配selector的元素(防止点击的是子元素的子节点)
    while (el && !el.matches(selector)) {
      if (el === elDelegate) { // 查到委托父元素还没匹配到,说明不是目标元素,停止查找
        el = null;
        break;
      }
      el = el.parentNode; // 向上查找父级节点
    }

    // 4. 如果找到目标元素,执行回调,this指向目标元素
    el && fn.call(el, e, el);
  });

  return elDelegate;
}

// HTML示例
/*

*/

// 示例使用1:单个目标元素
on('click', '#box', '#btn', function(e, el){
  console.log('点击成功!');
  console.log(this); // this 指向 #btn(目标元素)
  console.log(el); // el 也是目标元素,和this一致
});

// 示例使用2:多个目标元素(.item)
on('click', '#box', '.item', function(e, el){
  console.log('点击了item按钮:', el.innerText);
});

关键注意点

  • 委托父元素必须是目标元素的祖先节点(比如按钮的父div、body、document);

  • 不要阻止事件冒泡(e.stopPropagation()),否则事件无法传递到父元素,委托失效;

  • 新增的子元素(比如通过JS动态添加的按钮),无需重新绑定事件,会自动触发委托的事件。

三、滚动加载:滚动到底部自动加载更多内容

通俗解读

我们刷朋友圈、逛电商列表时,往下滚动页面,到底部后会自动加载更多内容,这就是滚动加载。核心就是“判断页面是否滚动到底部”,如果到了,就执行加载数据的逻辑。

专业解析

通过监听window的scroll事件,获取三个关键高度:可视区域高度(屏幕能看到的页面高度)、滚动条已滚动距离、页面总高度(包括看不见的部分)。核心判断公式:可视区域高度 + 已滚动距离 ≥ 页面总高度,满足该条件即表示滚动到底部。

完整代码(可直接复制)

// 监听滚动事件
window.addEventListener('scroll', function() {
    // 1. 可视区域高度(屏幕能看到的高度,不同设备可能不同)
    const clientHeight = document.documentElement.clientHeight;
    // 2. 滚动条卷上去的高度(已滚动的距离,兼容不同浏览器)
    const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
    // 3. 整个页面总高度(包括看不见的部分,即页面完整高度)
    const scrollHeight = document.documentElement.scrollHeight;

    // 核心判断:可视高度 + 已滚动距离 ≥ 总高度 → 滚动到底部(加10是为了提前加载,优化体验)
    if (clientHeight + scrollTop >= scrollHeight - 10) {
        console.log("滚动到底部啦!");
        // 这里写加载更多逻辑(比如请求接口、渲染列表)
        // loadMoreData(); // 假设这是加载更多数据的函数
    }
}, false);

// 优化建议:滚动事件会频繁触发,可结合节流函数(参考后面的图片懒加载),避免性能消耗
// 比如:window.addEventListener('scroll', throttle(handleScroll, 300), false);

关键注意点

  • scroll事件会频繁触发(滚动过程中每秒触发几十次),建议结合节流函数(后面会讲),减少函数执行次数,优化性能;

  • 滚动到底部的判断可加一个小偏移量(比如-10),让加载提前触发,避免用户看到底部空白再加载;

  • 加载数据时,建议添加“加载中”状态,防止用户多次触发加载。

四、图片懒加载:减少页面加载时间,提升体验

通俗解读

页面有很多图片时,如果一打开就加载所有图片,会导致页面加载变慢、卡顿。图片懒加载就是“只加载屏幕能看到的图片”,用户往下滚动页面,图片进入视野后再加载,既节省带宽,又提升页面加载速度。

专业解析

核心思路:先给图片设置自定义属性(比如data-src)存储真实图片地址,src属性设为占位图(或空);监听scroll事件,判断图片是否进入可视区域,若进入,则将data-src的值赋给src,实现图片加载。同时用节流函数限制scroll事件触发频率,避免性能消耗。

完整代码(可直接复制)

// 节流函数:限制函数在指定时间内只能执行一次(优化scroll事件频繁触发)
function throttle(fn, delay) {
  let timer = null;
  return function (...args) {
    if (!timer) {
      fn.apply(this, args); // 执行函数
      timer = setTimeout(() => {
        timer = null; // 延迟后重置timer,允许下次执行
      }, delay);
    }
  };
}

// 图片懒加载核心函数
function lazyload() {
  const imgs = document.getElementsByTagName('img'); // 获取所有图片元素(注意加s,避免报错)
  const viewHeight = document.documentElement.clientHeight; // 可视区域高度
  // 滚动距离(兼容不同浏览器)
  const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

  // 遍历所有图片,判断是否进入可视区域
  for (let i = 0; i < imgs.length; i++) {
    const img = imgs[i];
    const offsetTop = img.offsetTop; // 图片到页面顶部的距离

    // 判断:图片顶部距离 ≤ 可视区域高度 + 滚动距离 → 图片进入可视区域
    if (offsetTop < viewHeight + scrollTop) {
      // 优化:已经加载过的图片跳过,避免重复赋值(防止scroll事件重复触发导致的冗余操作)
      if (img.src === img.dataset.src) continue;

      // 开始加载图片:将data-src(真实地址)赋给src
      img.src = img.dataset.src;
    }
  }
}

// 监听scroll事件,用节流函数限制触发频率(300ms执行一次)
window.addEventListener('scroll', throttle(lazyload, 300));

// 页面刚打开时,执行一次懒加载(加载可视区域内的图片)
window.addEventListener('load', lazyload);

// HTML示例
/*
<!-- 占位图用1x1透明图,或loading图片,data-src存储真实图片地址 -->

*

关键注意点

  • 图片必须设置src属性(可设为占位图),否则会出现图片占位空白;

  • 节流函数的延迟时间(300ms)可根据需求调整,延迟太短达不到优化效果,太长会影响体验;

  • 页面加载完成后(window.load)必须执行一次lazyload,避免可视区域内的图片无法加载。

五、统计HTML页面标签:快速了解页面结构

通俗解读

有时候我们需要知道一个页面用了多少种标签、每种标签用了多少次(比如做页面优化、排查冗余标签),这个功能就能自动统计,不用手动一个个数,还能按使用次数排序。

专业解析

利用document.getElementsByTagName('*')获取页面所有元素,转为数组后提取每个元素的标签名;用Set统计标签种类(Set自动去重),用reduce统计每种标签的数量,最后用sort排序,返回标签种类数和各标签数量(从多到少)。

完整代码(可直接复制)

function countTagsOnPage() {
  // 1. 获取页面所有元素(*表示匹配所有标签)
  const allTags = document.getElementsByTagName('*');
  // 2. 转为数组,提取每个元素的标签名,并转为大写(统一格式,避免div和DIV重复统计)
  const tagNames = [...allTags].map(el => el.tagName.toUpperCase());
  
  // 3. 统计标签种类(Set自动去重,size就是种类数)
  const totalTagTypes = new Set(tagNames).size;

  // 4. 统计每种标签的数量(用reduce累加)
  const tagCount = tagNames.reduce((acc, tag) => {
    acc[tag] = (acc[tag] || 0) + 1; // 有则加1,无则初始化为1
    return acc;
  }, {});

  // 5. 按标签数量从多到少排序(将对象转为数组,再排序)
  const sorted = Object.entries(tagCount).sort((a, b) => b[1] - a[1]);

  // 返回统计结果:标签种类数、排序后的标签数量
  return {
    totalTagTypes,
    tagCounts: sorted
  };
}

// 使用并打印结果
const res = countTagsOnPage();
console.log('页面标签种类:', res.totalTagTypes);
console.log('各标签数量(从多到少):', res.tagCounts);
// 示例输出:
// 页面标签种类:8
// 各标签数量(从多到少):[["DIV", 12], ["SPAN", 8], ["IMG", 5], ["BUTTON", 3], ...]

关键注意点

  • tagName返回的是大写标签名(比如div返回DIV),统一转为大写可避免大小写重复统计;

  • document.getElementsByTagName('*')会获取所有元素,包括head、body、script等隐藏元素,若需统计可见元素,可添加筛选条件;

  • 排序后的结果是二维数组,每个元素的第一个值是标签名,第二个值是数量。

六、点击打印HTML标签名:快速定位元素标签

通俗解读

开发时,我们经常需要知道点击的元素是什么标签(比如排查样式问题、调试事件绑定),这个功能就是“点击页面任意元素,自动打印该元素的标签名”,不用手动去开发者工具里查看。

专业解析

利用事件委托(前面讲过的知识点),在document上绑定一次click事件,通过event.target获取被点击的具体元素,再用tagName获取该元素的标签名,最后用console.log打印(也可改为弹窗显示)。

完整代码(可直接复制)

// 利用事件委托,在document上绑定一次click事件,处理所有元素的点击
document.addEventListener('click', function(event) {
    // event.target 指向被点击的具体元素(最底层子元素)
    const clickedElement = event.target;
    
    // 获取标签名(tagName返回大写形式,如'DIV'、'SPAN'、'IMG')
    const tagName = clickedElement.tagName;
    
    // 打印标签名(默认控制台打印,可改为弹窗)
    console.log(`点击的元素标签名:${tagName}`);
    // 如需弹窗显示,可取消下面这行的注释
    // alert(`点击的元素标签名:${tagName}`);
});

// 示例:点击页面上的div、span、按钮,控制台会分别打印 DIV、SPAN、BUTTON

关键注意点

  • 点击的是元素的子节点(比如span里的文本),event.target会指向文本节点的父元素(span),不影响标签名获取;

  • 可根据需求修改打印方式(控制台打印/弹窗),弹窗适合非开发环境快速查看;

  • 若只想打印特定元素的标签名,可添加筛选条件(比如只打印按钮标签:if(tagName === 'BUTTON') { ... })。

七、模拟JSONP:解决跨域请求问题

通俗解读

前端请求接口时,经常会遇到“跨域”报错(比如前端域名是a.com,接口域名是b.com),JSONP是一种简单的跨域解决方案。核心就是“通过创建script标签,加载接口地址,利用script标签不受跨域限制的特性,获取后端返回的数据”。

注意:结合你提供的报错信息 link hit security strategy(链接触发安全策略),若使用JSONP时出现该报错,大概率是后端接口的安全策略限制了该请求(比如不允许JSONP请求、域名白名单限制),需联系后端调整安全策略。

专业解析

JSONP的核心原理:script标签的src属性不受同源策略限制,可加载任意域名的资源。前端生成唯一回调函数名,拼接在接口URL中;后端接收请求后,返回“回调函数名(数据)”的格式;前端通过全局回调函数,接收并处理后端返回的数据,最后删除临时创建的script标签和全局函数,避免冗余。

完整代码(可直接复制)

function JSONP(url, _params = {}) {
  // 1. 生成唯一回调函数名(防止多个JSONP请求冲突,默认用jsonp_+时间戳)
  const callbackName = _params.callback || "jsonp_" + Date.now();
  
  // 2. 处理请求参数(排除callback,因为要单独拼接)
  const params = [];
  for (let key in _params) {
    if (key !== "callback") {
      // 编码参数值,处理中文/特殊字符
      params.push(`${key}=${encodeURIComponent(_params[key])}`);
    }
  }
  // 3. 拼接callback参数(JSONP核心:后端会根据该参数返回对应的回调函数调用)
  params.push(`callback=${callbackName}`);

  // 4. 创建script标签,用于加载接口(script不受跨域限制)
  const script = document.createElement("script");
  // 拼接接口URL和参数(url?key1=value1&key2=value2&callback=xxx)
  script.src = `${url}?${params.join("&")}`;

  // 5. 返回Promise,方便用then/catch处理结果
  return new Promise((resolve, reject) => {
    
    // 6. 挂载全局回调函数(必须在script加载前定义,否则后端返回时函数还不存在)
    window[callbackName] = (result) => {
      try {
        resolve(result); // 成功:将后端返回的数据传入resolve
      } catch (err) {
        reject(err); // 失败:捕获异常并传入reject
      } finally {
        // 7. 清理工作:删除script标签和全局回调函数,避免内存泄漏
        document.body.removeChild(script);
        delete window[callbackName];
      }
    };

    // 8. 处理脚本加载失败(比如网络错误、接口不存在)
    script.onerror = () => {
      reject(new Error("JSONP 请求失败"));
      // 失败也需要清理
      document.body.removeChild(script);
      delete window[callbackName];
    };

    // 9. 把script插入页面(最后执行,确保回调函数已定义)
    document.body.appendChild(script);
  });
}

// 示例使用(结合你提供的URL)
JSONP("https://api.example.com/data", {
  id: 123,
  callback: "getData" // 可选:指定后端约定的回调名,不指定则自动生成
}).then(res => {
  console.log("拿到数据:", res);
}).catch(err => {
  console.log("出错:", err);
  // 若出现 "link hit security strategy" 报错,需检查后端安全策略
});

// 后端返回格式(必须是回调函数调用的形式)
// getData({ "name": "张三", "id": 123 })
// 前端会通过window.getData接收该数据,并传入then的回调函数

关键注意点

  • JSONP只支持GET请求,不支持POST请求(因为script标签的src只能发起GET请求);

  • 回调函数名必须唯一,避免多个JSONP请求冲突(代码中用时间戳保证唯一性);

  • 若出现 link hit security strategy 报错,不是前端代码问题,而是后端接口的安全策略限制了该JSONP请求,需联系后端调整(比如添加前端域名到白名单、允许JSONP请求);

  • 请求完成后必须清理script标签和全局函数,避免内存泄漏。

总结

以上7个JS功能,覆盖了前端开发中URL处理、事件绑定、性能优化、跨域请求等高频场景,代码均经过优化,可直接复制到项目中使用。

重点提醒:使用JSONP时若遇到 link hit security strategy 报错,需排查后端安全策略,而非前端代码;另外,事件绑定和滚动相关功能,建议结合节流函数优化性能,避免频繁触发函数导致页面卡顿。