跟着 MDN 学 HTML day_52:(深入 XPathExpression 接口)

在前一篇文章中,我们详细学习了 XPathEvaluator 接口,了解了如何创建 XPath 表达式求值器并执行查询。今天我们将继续深入,探讨 XPathExpression 接口。这个接口代表一个已经编译好的 XPath 表达式,可以被重复用于在文档或特定节点上进行求值。

XPathExpression 的核心价值在于预编译。当我们需要在应用中多次执行同一个 XPath 查询时,使用预编译的表达式对象可以避免每次重新解析 XPath 字符串,从而提升性能。同时,表达式中出现的所有命名空间前缀也会被预先解析,进一步提高了效率。

一、XPathExpression 概述

XPathExpression 是一个基线广泛可用的接口,它代表一个经过编译的 XPath 表达式。编译后的表达式可以在文档或特定节点上求值,以从 DOM 树中获取所需的信息。

这个接口特别适用于表达式会被重复使用的场景。由于表达式只编译一次,所有内部的前缀和路径解析工作都会在编译阶段完成,后续每次求值时就可以直接使用,大幅减少了重复计算的开销。

示例:XPathExpression 的创建和基本使用

html 复制代码
<div>XPath example</div>
<div>Number of <div>s: <output></output></div>
javascript 复制代码
const xpath = "//div";
const evaluator = new XPathEvaluator();

// 通过 createExpression 创建编译后的表达式对象
const expression = evaluator.createExpression(xpath);

// 在文档上执行编译后的表达式
const result = expression.evaluate(
  document,
  XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
);

// 显示匹配的 div 数量
document.querySelector("output").textContent = result.snapshotLength;

在这个例子中,我们首先创建了一个 XPathEvaluator 实例,然后通过 createExpression() 方法将字符串 "//div" 编译为一个 XPathExpression 对象。之后,调用该对象的 evaluate() 方法即可在文档上执行查询。

二、XPathExpression.evaluate() 方法

定义

evaluate() 是 XPathExpression 接口唯一的实例方法。它用于在给定的节点或文档上执行编译好的 XPath 表达式,并返回一个 XPathResult 对象。

语法

javascript 复制代码
evaluate(contextNode)
evaluate(contextNode, type)
evaluate(contextNode, type, result)

参数说明:

contextNode:作为求值上下文的节点,表达式将相对于此节点进行求值

type:可选,指定返回结果的类型,必须是 XPathResult 的常量之一

result:可选,允许指定一个可复用的结果对象。如果传入 null 或实现不支持复用,将返回新的结果对象

示例:在不同上下文中使用 evaluate

html 复制代码
<div class="container">
  <p class="item">第一项</p>
  <p class="item">第二项</p>
</div>
<div class="container">
  <p class="item">第三项</p>
</div>
<div>总计:<output id="total"></output></div>
<div>第一个容器:<output id="first"></output></div>
javascript 复制代码
const evaluator = new XPathEvaluator();

// 编译表达式:查找所有 class 为 item 的 p 元素
const expression = evaluator.createExpression("//p[@class='item']");

// 在整个文档上求值
const totalResult = expression.evaluate(
  document,
  XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
);
document.getElementById("total").textContent = totalResult.snapshotLength;
// 输出:3

// 在第一个容器上求值
const firstContainer = document.querySelector(".container");
const firstResult = expression.evaluate(
  firstContainer,
  XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
);
document.getElementById("first").textContent = firstResult.snapshotLength;
// 输出:2

这个例子清晰地展示了编译后的表达式可以在不同的上下文节点上执行,每次都能得到相对于该上下文的正确结果。这种灵活性使得 XPathExpression 在处理复杂 DOM 结构时非常实用。

三、evaluate 方法的返回值

evaluate() 方法始终返回一个 XPathResult 对象,该对象的具体结构取决于 type 参数的设置。了解不同的结果类型,能帮助我们更准确地获取所需数据。

常用结果类型回顾

XPathResult.NUMBER_TYPE:返回数值,通过 numberValue 属性获取

XPathResult.STRING_TYPE:返回字符串,通过 stringValue 属性获取

XPathResult.BOOLEAN_TYPE:返回布尔值,通过 booleanValue 属性获取

XPathResult.ORDERED_NODE_SNAPSHOT_TYPE:返回节点快照列表

XPathResult.FIRST_ORDERED_NODE_TYPE:返回第一个匹配节点

XPathResult.ANY_TYPE:由表达式自然决定类型

示例:使用不同类型获取结果

html 复制代码
<div class="product" data-price="50">键盘</div>
<div class="product" data-price="120">鼠标</div>
<div class="product" data-price="80">耳机</div>
<div>
  <p>商品数量:<output id="count"></output></p>
  <p>总价格:<output id="sum"></output></p>
  <p>第一个商品:<output id="first-product"></output></p>
  <p>有超过100元的商品:<output id="has-expensive"></output></p>
</div>
javascript 复制代码
const evaluator = new XPathEvaluator();

// 编译多个表达式用于不同目的
const countExpression = evaluator.createExpression("count(//div[@class='product'])");
const sumExpression = evaluator.createExpression("sum(//div[@class='product']/@data-price)");
const firstExpression = evaluator.createExpression("//div[@class='product'][1]");
const boolExpression = evaluator.createExpression("boolean(//div[@data-price > 100])");

// 获取商品数量
const countResult = countExpression.evaluate(
  document,
  XPathResult.NUMBER_TYPE
);
document.getElementById("count").textContent = countResult.numberValue;
// 输出:3

// 获取总价格
const sumResult = sumExpression.evaluate(
  document,
  XPathResult.NUMBER_TYPE
);
document.getElementById("sum").textContent = sumResult.numberValue;
// 输出:250

// 获取第一个商品的文本
const firstResult = firstExpression.evaluate(
  document,
  XPathResult.STRING_TYPE
);
document.getElementById("first-product").textContent = firstResult.stringValue;
// 输出:键盘

// 判断是否存在超过100元的商品
const boolResult = boolExpression.evaluate(
  document,
  XPathResult.BOOLEAN_TYPE
);
document.getElementById("has-expensive").textContent = boolResult.booleanValue ? "是" : "否";
// 输出:是

通过预编译不同的表达式并指定合适的结果类型,我们可以高效地获取各种形式的结果。这种预编译加多类型返回的模式,让 XPath 查询在实际应用中非常灵活。

四、异常处理

在调用 evaluate() 方法时,如果传入的参数不符合要求,可能会抛出多种类型的 DOMException 异常。了解这些异常有助于我们编写更健壮的代码。

常见异常类型

INVALID_EXPRESSION_ERR:表达式不合法

TYPE_ERR:结果无法转换为指定的类型

NAMESPACE_ERR:表达式中的命名空间前缀无法解析

WRONG_DOCUMENT_ERR:提供的上下文节点来自不被支持的文档

NOT_SUPPORTED_ERR:上下文节点类型不被允许或请求类型不被允许

示例:捕获 evaluate 方法的异常

javascript 复制代码
const evaluator = new XPathEvaluator();
const expression = evaluator.createExpression("//div");

// 正常执行
try {
  const result = expression.evaluate(
    document,
    XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
  );
  console.log("执行成功,找到节点数:", result.snapshotLength);
} catch (e) {
  console.error("执行失败:", e.name, e.message);
}

// 传入非法类型参数
try {
  const result = expression.evaluate(
    document,
    999 // 非法的类型值
  );
} catch (e) {
  console.error("类型错误:", e.name);
  // 输出:类型错误:NotSupportedError 或 TypeError
}

在实际开发中,特别是当 XPath 表达式来自用户输入或外部配置时,做好异常捕获非常重要,可以防止程序因为无效的表达式而崩溃。

五、结果对象复用

evaluate() 方法的第三个参数允许我们传入一个已有的 XPathResult 对象。如果浏览器实现支持,这个方法可以复用该对象来存储结果,从而减少垃圾回收的压力,在需要频繁执行查询的场景下能够提升性能。

示例:尝试复用结果对象

html 复制代码
<div class="item">A</div>
<div class="item">B</div>
<div class="item">C</div>
<div class="item">D</div>
<div>当前数量:<output id="result-display"></output></div>
javascript 复制代码
const evaluator = new XPathEvaluator();
const expression = evaluator.createExpression("//div[@class='item']");

// 创建一个可复用的结果对象
let reusableResult = null;

// 第一次求值
reusableResult = expression.evaluate(
  document,
  XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
  reusableResult
);
console.log("第一次结果对象:", reusableResult);

// 第二次求值,尝试复用同一个结果对象
reusableResult = expression.evaluate(
  document,
  XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
  reusableResult
);
console.log("第二次结果对象:", reusableResult);

// 显示结果
document.getElementById("result-display").textContent = reusableResult.snapshotLength;
// 输出:4

需要注意的是,并非所有浏览器实现都会真正复用传入的结果对象。如果浏览器不支持复用,方法会创建一个新的结果对象并返回。因此,在编写代码时应始终使用返回值来获取结果,而不是假设传入的对象已经被修改。

六、XPathExpression 与动态数据

在实际应用中,我们经常需要在数据发生变化后重新查询 DOM。预编译的 XPathExpression 对象特别适合这种场景,因为表达式本身不会改变,只需要在新的上下文中重新调用 evaluate() 即可。

示例:动态添加元素后重新求值

html 复制代码
<button id="add-btn">添加项目</button>
<button id="count-btn">统计项目数</button>
<ul id="list">
  <li class="task">初始任务</li>
</ul>
<div>任务总数:<output id="task-count"></output></div>
javascript 复制代码
const evaluator = new XPathEvaluator();
const expression = evaluator.createExpression("//li[@class='task']");

// 更新任务统计
function updateTaskCount() {
  const result = expression.evaluate(
    document,
    XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
  );
  document.getElementById("task-count").textContent = result.snapshotLength;
}

// 添加新任务
document.getElementById("add-btn").addEventListener("click", () => {
  const newTask = document.createElement("li");
  newTask.className = "task";
  newTask.textContent = `任务 ${Date.now()}`;
  document.getElementById("list").appendChild(newTask);
});

// 点击按钮时统计任务数
document.getElementById("count-btn").addEventListener("click", updateTaskCount);

// 初始统计
updateTaskCount();

在这个示例中,我们只在开始时编译了一次 XPath 表达式。之后每次点击统计按钮时,都使用同一个表达式对象重新求值,获取最新的任务列表。这种模式在需要频繁查询 DOM 的应用中非常高效。

七、XPathExpression 的创建与生命周期

XPathExpression 对象通过 XPathEvaluator.createExpression() 方法创建。一旦创建,该对象就可以在整个应用的生命周期中被反复使用,直到不再需要时由垃圾回收机制自动清理。

示例:封装可复用的查询工具

javascript 复制代码
// 创建一个 XPath 查询工具类
class XPathQuery {
  constructor(xpath) {
    this.evaluator = new XPathEvaluator();
    this.expression = this.evaluator.createExpression(xpath);
  }

  // 执行查询并返回节点列表
  queryNodes(contextNode = document) {
    const result = this.expression.evaluate(
      contextNode,
      XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
    );
    const nodes = [];
    for (let i = 0; i < result.snapshotLength; i++) {
      nodes.push(result.snapshotItem(i));
    }
    return nodes;
  }

  // 执行查询并返回第一个匹配节点
  queryFirstNode(contextNode = document) {
    const result = this.expression.evaluate(
      contextNode,
      XPathResult.FIRST_ORDERED_NODE_TYPE
    );
    return result.singleNodeValue;
  }
}

// 使用工具类
const divQuery = new XPathQuery("//div");
const allDivs = divQuery.queryNodes();
console.log("所有 div 数量:", allDivs.length);

const firstDiv = divQuery.queryFirstNode();
console.log("第一个 div:", firstDiv);

这种封装方式将编译后的表达式保存在实例中,对外提供简洁的查询接口。在大型应用中,这种方式能够提升代码的可维护性和执行效率。

八、与 XPathEvaluator.evaluate 的对比

在上一篇文章中,我们学习了直接使用 XPathEvaluator.evaluate() 方法执行查询。而 XPathExpression 则提供了一种面向对象的替代方案。两者在功能上是等价的,但在使用方式上有不同的侧重点。

示例:两种方式的对比

html 复制代码
<div class="box">A</div>
<div class="box">B</div>
<div>方式一:<output id="method1"></output></div>
<div>方式二:<output id="method2"></output></div>
javascript 复制代码
const xpath = "//div[@class='box']";

// 方式一:直接使用 XPathEvaluator.evaluate()
const evaluator = new XPathEvaluator();
const result1 = evaluator.evaluate(
  xpath,
  document,
  null,
  XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
);
document.getElementById("method1").textContent = result1.snapshotLength;

// 方式二:使用 XPathExpression
const expression = evaluator.createExpression(xpath);
const result2 = expression.evaluate(
  document,
  XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
);
document.getElementById("method2").textContent = result2.snapshotLength;

// 两种方式结果相同
console.log(result1.snapshotLength === result2.snapshotLength); // true

方式一适合一次性查询,代码更简洁。方式二适合需要多次执行的查询,因为它将表达式的解析和求值分离,可以重复使用编译结果。在性能敏感的场景中,特别是循环或高频操作中,应优先使用 XPathExpression。

九、浏览器兼容性

XPathExpression 接口属于基线广泛可用的特性,在现代主流浏览器中都得到了良好支持,包括 Chrome、Firefox、Safari 和 Edge 的最新版本。

在开发中使用时,如果目标环境包含较旧的浏览器,建议先进行功能检测,或使用 try-catch 包裹相关代码以确保程序的健壮性。对于绝大多数现代 Web 应用来说,可以放心地直接使用这个接口。

十、总结

今天的学习内容聚焦于 XPathExpression 接口,我们从它的定义和创建方式开始,深入学习了 evaluate() 方法的语法、参数、返回值以及异常处理。通过丰富的示例代码,我们看到了预编译表达式在不同上下文中的应用,以及结果对象复用的可能性。

XPathExpression 的核心价值在于将 XPath 查询的编译和求值两个阶段分离,为需要重复查询的场景提供了性能优化。结合 XPathEvaluator 和 XPathResult,这三个接口共同构成了浏览器中完整的 XPath 处理能力。

在实际开发中,当 CSS 选择器无法满足复杂的 DOM 查询需求时,XPathExpression 为我们提供了一个强大而高效的替代方案。掌握这个接口,能够让我们在处理复杂文档结构时更加游刃有余。


想要解锁更多HTML 核心标签实战、前端零基础入门干货、开发避坑全指南吗?
持续关注,后续将更新CSS 布局实战、JavaScript 交互基础、全站导航开发等硬核内容,带你从新手快速进阶,轻松搞定前端开发!

相关推荐
SLD_Allen6 小时前
Go语言runtime全景
开发语言·javascript·golang
Elastic 中国社区官方博客6 小时前
OpenTelemetry Java 扩展:无需分叉 agent 即可自定义追踪
java·大数据·运维·开发语言·数据库·人工智能·elasticsearch
美狐美颜sdk6 小时前
直播APP开发如何实现美颜功能?视频美颜SDK接入流程与技术方案解析
大数据·人工智能·音视频·美颜sdk·美颜api
计算机魔术师6 小时前
NVIDIA 季度营收指引达 1080 亿美元,首次突破单季千亿大关
前端
不灭的黄金瞳1236 小时前
C语言手写顺序表
c语言·开发语言·数据结构
阿无,6 小时前
Java多线程面试题之AQS
java·开发语言
数据狐(Datafox)6 小时前
京东商品列表API技术解析与落地应用(含标准 JSON 示例)
java·大数据·前端·人工智能·python·数据分析·json
风萧何6 小时前
架构艺术,是权衡的艺术。
前端·javascript·架构
偷偷挖矿的牛马矿工6 小时前
execa NodeJs新一代child_process的强力替代品
javascript
豆沙沙包?6 小时前
C++~~~set容器、map容器(p57-P69)
开发语言·c++