XPath中的string()函数是类型转换体系里最基础也最容易踩坑的一个。它的作用是把任意类型的值转换成字符串,而当参数是节点集时,转换行为尤其值得注意:并不是拼接所有节点的内容,而是只取第一个节点按照特定规则取字符串值。理解这套规则,对写XSLT模板、做XML数据提取、以及在浏览器里用document.evaluate执行查询都非常有帮助。

string()函数的基本语法与参数行为
string()函数的签名非常简单:string(object?)。它接受一个可选参数,把这个参数转换为字符串并返回。如果不传参数,则默认以当前上下文节点(即英文句点.所指代的节点)作为参数,等价于string(.)。
参数可以是四种类型之一:节点集(node-set)、布尔值、数字或者字符串本身。字符串传入自然原样返回,重点在于其他三种。数字的转换规则有几个细节要注意:整数会转换成不带小数点的形式,比如string(42)返回42;非整数值则保留小数部分,string(3.14)返回3.14;而特殊的非数字值NaN会转换成字符串NaN,正负无穷分别转换成Infinity和-Infinity,正零和负零都转换成0。布尔值的转换则很直白:string(true())返回true,string(false())返回false。
真正复杂的是节点集参数,这也是本文的核心。很多人以为string()会把节点集里所有节点的内容拼起来,实际上规范明确规定:只取节点集中文档顺序的第一个节点,然后计算这个节点的字符串值。如果节点集为空,返回空字符串。
各类节点的字符串值是怎么来的
所谓节点的字符串值(string-value),在XPath数据模型里是每个节点固有的属性,不同节点类型的计算方式不同,理解这一点是掌握string()的关键。
对于元素节点,它的字符串值是该元素所有文本后代节点的字符数据按文档顺序拼接后的结果,注意这不包含属性和注释、处理指令的内容。比如对于<book><title>XML</title>入门</book>这个片段,string(/book)的结果是XML入门,两个文本节点的内容被连在一起。
文本节点的字符串值就是它自身的字符数据;属性节点的字符串值是属性的规范化值;注释节点返回注释的正文;处理指令节点返回处理指令名字之后的部分。根节点(文档节点)的规则和元素节点一样,取所有后代文本节点拼接。命名空间节点则返回绑定到该前缀的命名空间URI。下面用一个例子演示:
<catalog>
<product id="P001" category="tool">
<!-- 高性价比工具 -->
<name>XPath</name>
<name>XQuery</name>
</product>
</catalog>针对这个文档执行几条查询:string(/catalog/product/@id)返回P001,因为属性节点的字符串值就是属性值;string(//name)返回XPath而不是XPathXQuery,因为节点集里文档顺序第一个节点是第一个name元素;string(/catalog/product/comment())返回 高性价比工具 ,注释正文原样保留。
string()与text()的区别及隐式转换陷阱
初学者经常混淆string()和text()。两者性质完全不同:text()是一个节点测试,用于选取当前节点的直接子文本节点,返回的是节点集;string()是函数,返回的是字符串。在需要布尔判断或者字符串比较的场合,混淆二者会直接导致逻辑错误。
一个经典陷阱出现在谓词里。假设想筛选name元素的直接文本等于XPath的产品,写//product[name/text()='XPath']是常见的做法,它比较的是第一个文本子节点。但如果写成//product[string(name)='XPath'],比较的是name元素的完整字符串值,当name包含混合内容时结果可能不同。再比如想判断元素是否有非空内容,//product[string-length(name)>0]是对的,而//product[name/text()]判断的是是否存在文本子节点,两者在元素只包含空白文本时表现不一致。
另一个隐蔽问题是隐式转换。当节点集出现在需要字符串的上下文中(比如=比较的右侧、string-length的参数里),XPath会自动做与string()等价的转换,只取第一个节点。这可能让"判断节点集是否包含某个值"的语义与预期不符:实际上=对节点集的比较是存在性语义,只要集合中任意一个节点的字符串值等于右侧值就为真,这与string()的取首个节点规则是两回事,混在一起理解很容易出错。
在XSLT和浏览器环境中的实践
在XSLT中,string()的使用频率很高,比如输出某个字段的值时明确做一次转换,可以让模板意图更清晰。下面是一个简单的模板片段,它对节点集调用string()并演示了只取首个节点的行为:
<xsl:template match="product">
<div class="product">
<!-- string()只取第一个name元素 -->
<span><xsl:value-of select="string(name)"/></span>
<!-- 拼接所有name需要用for-each遍历 -->
<xsl:for-each select="name">
<span><xsl:value-of select="."/></span>
</xsl:for-each>
</div>
</xsl:template>在浏览器里,document.evaluate配合XPathResult的STRING_TYPE也可以直接拿到表达式的字符串结果,其内部同样遵循string()的转换规则。示例代码如下:
var doc = new DOMParser().parseFromString(xmlSource, "application/xml"); var result = doc.evaluate( "string(//name)", // 表达式本身调用了string() doc, null, XPathResult.STRING_TYPE, // 结果类型为字符串 null ); console.log(result.stringValue); // 输出第一个name的字符串值
需要注意的是,如果表达式改成//name而结果类型仍选STRING_TYPE,引擎会对结果节点集做隐式string()转换,效果与显式写string()一致。但如果节点集为空,stringValue就是空字符串,这点在判空逻辑里要提前考虑。掌握这些规则后,无论是做数据抽取还是写转换模板,都能准确预判string()的输出,避免把"只取第一个节点"误当成"拼接全部内容"这类隐蔽错误。