第十八章 JavaScript与XML
一、浏览器对XML DOM的支持
1、 DOM2级核心
在支持DOM2级的浏览器中可以使用以下语法来创建一个空白的 XML文档:
var xmldom = document.implementation.createDocument(namespaceUri, root, doctype);
要检测浏览器是否支持 DOM2级 XML,可以使用下面这行代码:
var hasXmlDom = document.implementation.hasFeature("XML", "2.0");
在实际开发中,很少需要从头开始创建一个 XML 文档,更常见的情况往往是将某个XML文档解析为DOM结构,或者反之。由于 DOM2级规范没有提供这种功能,因此就出现了一些事实标准。
2、DOMParser类型
为了将XML解析为 DOM文档,Firefox引入了DOMParser 类型;后来,IE9、Safari、Chrome和 Opera 也支持了这个类型。在解析 XML之前,首先必须创建一个 DOMParser 的实例,然后再调用 parseFromString()方法。
DOMParser只能解析格式良好的XML,因而不能把 HTML解析为HTML文档。在发生解析错误时,仍然会从 parseFromString()中返回一个Document 对象,但这个对象的文档元素是<parsererror>,而文档元素的内容是对解析错误的描述。
下面是一个例子:
1 var parser = new DOMParser(); 2 var xmldom = parser.parseFromString("<root><child/></root>", "text/xml"); 3 alert(xmldom.documentElement.tagName); //"root" 4 alert(xmldom.documentElement.firstChild.tagName); //"child" 5 var anotherChild = xmldom.createElement("child"); 6 xmldom.documentElement.appendChild(anotherChild); 7 var children = xmldom.getElementsByTagName("child"); 8 alert(children.length); //2 9 10 var parser = new DOMParser(),xmldom,errors; 11 try { 12 //解析出错 13 xmldom = parser.parseFromString("<root>", "text/xml"); 14 errors = xmldom.getElementsByTagName("parsererror"); 15 if (errors.length > 0){ 16 throw new Error("Parsing error!"); 17 } 18 } catch (ex) { 19 alert("Parsing error!"); 20 } 21
3、XMLSerializer类型
在引入DOMParser的同时,Firefox还引入了 XMLSerializer 类型,提供了相反的功能:将DOM文档序列化为 XML字符串。后来,IE9+、Opera、Chrome和 Safari都支持了XMLSerializer。
要序列化DOM文档,首先必须创建XMLSerializer的实例,然后将文档传入其serializetoString()方法,如下面的例子所示:
var serializer = new XMLSerializer(); var xml = serializer.serializetoString(xmldom); alert(xml)
4、IE8及之前版本中的XML
a、在IE中,与其他浏览器不同,IE通过ActiveX对象实现对XML的支持。要创建一个XML文档的实例,要使用ActiveXObject构造函数并为其传入一个表示XML文档版本的字符串。微软推荐使用 MSXML2.DOMDocument.6.0 或 MSXML2.DOMDocument.3.0。
如下:
//尝试创建每个版本的实例并观察是否有错误发生,可以确定哪个版本可用 function createDocument(){ if (typeof arguments.callee.activeXString != "string"){ var versions = ["MSXML2.DOMDocument.6.0", "MSXML2.DOMDocument.3.0","MSXML2.DOMDocument"], i, len; for (i=0,len=versions.length; i < len; i++){ try { new ActiveXObject(versions[i]); arguments.callee.activeXString = versions[i]; break; } catch (ex){ //跳过 } } } return new ActiveXObject(arguments.callee.activeXString); }
b、要解析 XML 字符串,首先必须创建一个DOM文档,然后调用loadXML()方法。新创建的 XML文档完全是一个空文档,因而不能对其执行任何操作。为 loadXML()方法传入的XML字符串经解析之后会被填充到 DOM 文档中,如下。
1 var xmldom = createDocument(); 2 xmldom.loadXML("<root><child/></root>"); 3 alert(xmldom.documentElement.tagName); //"root" 4 alert(xmldom.documentElement.firstChild.tagName); //"child" 5 var anotherChild = xmldom.createElement("child"); 6 xmldom.documentElement.appendChild(anotherChild); 7 var children = xmldom.getElementsByTagName("child"); 8 alert(children.length); //2
c、 序列化XML
IE将序列化 XML 的能力内置在了 DOM 文档中。每个 DOM 节点都有一个 xml 属性,其中保存着表示该节点的 XML 字符串。
例如:alert(xmldom.xml);
d、加载XML
加载文档的方式也可以分为同步和异步两种。要指定加载文档的方式,可以设置async属性,true表示异步,false表示同步(默认值为true)。在确定了加载XML文档的方式后,调用 load()可以启动下载过程。
同步加载:
1 var xmldom = createDocument(); 2 xmldom.async = false; 3 xmldom.load("example.xml"); 4 if (xmldom.parseError != 0){ 5 //处理错误 6 } else { 7 alert(xmldom.documentElement.tagName); //"root" 8 alert(xmldom.documentElement.firstChild.tagName); //"child" 9 var anotherChild = xmldom.createElement("child"); 10 xmldom.documentElement.appendChild(anotherChild); 11 var children = xmldom.getElementsByTagName("child"); 12 alert(children.length); //2 13 alert(xmldom.xml); 14 }
异步加载:虽然同步方式比较方便,但如果下载时间太长,会导致程序反应很慢。因此,在加载XML文档时,通常都使用异步方式。 在异步加载XML文件的情况下,需要为XML DOM文档的onreadystatechange 事件指定处理程序,有4个就绪状态。
在实际开发中,要关注的只有一个就绪状态:4。这个状态表示 XML文件已经全部加载完毕,而且已经全部解析为DOM文档。通过 XML文档的readyState属性可以取得其就绪状态。以异步方式加载XML文件的典型模式如下。
1 var xmldom = createDocument(); 2 xmldom.async = true; 3 xmldom.onreadystatechange = function(){ 4 if (xmldom.readyState == 4){ 5 if (xmldom.parseError != 0){ //错误处理 6 alert("An error occurred:\nError Code: " 7 + xmldom.parseError.errorCode + "\n" 8 + "Line: " + xmldom.parseError.line + "\n" 9 + "Line Pos: " + xmldom.parseError.linepos + "\n" 10 + "Reason: " + xmldom.parseError.reason); 11 } else { //处理文档 12 alert(xmldom.documentElement.tagName); //"root" 13 alert(xmldom.documentElement.firstChild.tagName); //"child" 14 var anotherChild = xmldom.createElement("child"); 15 xmldom.documentElement.appendChild(anotherChild); 16 var children = xmldom.getElementsByTagName("child"); 17 alert(children.length); //2 18 alert(xmldom.xml); 19 } 20 } 21 }; 22 xmldom.load("example.xml");
5、跨浏览器处理XML
综合上面的代码,就可以得到跨浏览器处理XML的方案。
下面这个函数可以在所有四种主要浏览器中使用:
//解析 XML function parseXml(xml){ var xmldom = null; if (typeof DOMParser != "undefined"){ xmldom = (new DOMParser()).parseFromString(xml, "text/xml"); var errors = xmldom.getElementsByTagName("parsererror"); if (errors.length){ throw new Error("XML parsing error:" + errors[0].textContent); } } else if (typeof ActiveXObject != "undefined"){ xmldom = createDocument(); xmldom.loadXML(xml); if (xmldom.parseError != 0){ throw new Error("XML parsing error: " + xmldom.parseError.reason); } } else { throw new Error("No XML parser available."); } return xmldom; } //序列化 XML function serializeXml(xmldom){ if (typeof XMLSerializer != "undefined"){ return (new XMLSerializer()).serializetoString(xmldom); } else if (typeof xmldom.xml != "undefined"){ return xmldom.xml; } else { throw new Error("Could not serialize XML DOM."); } }
二、浏览器对 XPath的支持
XPath是设计用来在DOM文档中查找节点的一种手段,因而对XML处理也很重要。XPath是在DOM3级XPath模块中首次跻身推荐标准行列的。很多浏览器都实现了这个推荐标准,但IE则以自己的方式实现了XPath。
1、 DOM3级XPath
要确定某浏览器是否支持DOM3级XPath,可以使用以下JavaScript代码:
var supportsXPath = document.implementation.hasFeature("XPath", "3.0");
在DOM3级XPath规范定义的类型中,最重要的两个类型是XPathEvaluator和XPathResult。XPathEvaluator用于在特定的上下文中对XPath表达式求值。
a、单节点结果: 指定常量 XPathResult.FirsT_ORDERED_NODE_TYPE 会返回第一个匹配的节点,可以通过结果的 singleNodeValue属性来访问该节点。例如:
1 var result = xmldom.evaluate("employee/name", xmldom.documentElement, null, 2 XPathResult.FirsT_ORDERED_NODE_TYPE, null); 3 if (result !== null) { 4 alert(result.singleNodeValue.tagName); 5 }
b、简单类型结果:
//如果有节点匹配"employee/name",则 booleanValue 属性的值就是 true var result = xmldom.evaluate("employee/name", xmldom.documentElement, null,XPathResult.BOOLEAN_TYPE, null); alert(result.booleanValue);
//计算与给定模式匹配的所有节点数量的 count() var result = xmldom.evaluate("count(employee/name)", xmldom.documentElement,null, XPathResult.NUMBER_TYPE, null); alert(result.numberValue); //对于字符串类型,evaluate()方法会查找与 XPath 表达式匹配的第一个节点,然后返回其第一个子节点的值 var result = xmldom.evaluate("employee/name", xmldom.documentElement, null,XPathResult.STRING_TYPE, null); alert(result.stringValue);
c、默认类型结果: 所有 XPath表达式都会自动映射到特定的结果类型。像前面那样设置特定的结果类型,可以限制表达式的输出。而使用XPathResult.ANY_TYPE 常量可以自动确定返回结果的类型。一般来说,自动选择的结果类型可能是布尔值、数值、字符串值或一个次序不一致的节点迭代器。要确定返回的是什么结果类型,可以检测结果的resultType 属性。
对于利用了命名空间的XML文档,XPathEvaluator 必须知道命名空间信息,然后才能正确地进行求值。处理命名空间的方法也不止一种。以下面的XM代码为例。
1 <?xml version="1.0" ?> 2 <wrox:books xmlns:wrox="http://www.wrox.com/"> 3 <wrox:book> 4 <wrox:title>Professional JavaScript for Web Developers</wrox:title> 5 <wrox:author>Nicholas C. Zakas</wrox:author> 6 </wrox:book> 7 <wrox:book> 8 <wrox:title>Professional Ajax</wrox:title> 9 <wrox:author>Nicholas C. Zakas</wrox:author> 10 <wrox:author>Jeremy McPeak</wrox:author> 11 <wrox:author>Joe Fawcett</wrox:author> 12 </wrox:book> 13 </wrox:books>
在这个XML文档中,所有元素定义都来自 http://www.wrox.com/命名空间,以前缀wrox标识。 如果要对这个文档使用XPath,就需要定义要使用的命名空间;否则求值将会失败。
处理命名空间的第一种方法是通过 createNSResolver()来创建 XPathNSResolver 对象:
1 //通过 createNSResolver()来创建 XPathNSResolver 对象。 2 var nsresolver = xmldom.createNSResolver(xmldom.documentElement); 3 var result = xmldom.evaluate("wrox:book/wrox:author", 4 xmldom.documentElement, nsresolver, 5 XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); 6 alert(result.snapshotLength);
处理命名空间的第二种方法就是定义一个函数,让它接收一个命名空间前缀,返回关联的 URI:
//定义一个函数,让它接收一个命名空间前缀,返回关联的URI var nsresolver = function(prefix){ switch(prefix){ case "wrox": return "http://www.wrox.com/"; //其他前缀 } }; var result = xmldom.evaluate("count(wrox:book/wrox:author)", xmldom.documentElement, nsresolver, XPathResult.NUMBER_TYPE, null); alert(result.numberValue);
2、IE中的XPath
IE对XPath的支持是内置在基于ActiveX的 XML DOM文档对象中的,没有使用DOMParser返回的DOM对象。因此,为了在 IE9及之前的版本中使用XPath,必须使用基于ActiveX的实现。这个接口在每个节点上额外定义了两个的方法:selectSingleNode()和 selectNodes();
selectSingleNode()方法接受一个XPath模式,在找到匹配节点时返回第一个匹配的节点,如果没有找到匹配的节点就返回null。
selectNodes()方法也接收一个 XPath模式作为参数,但它返回与模式匹配的所有节点的 NodeList(如果没有匹配的节点,则返回一个包含零项的NodeList)。
var element = xmldom.documentElement.selectSingleNode("employee/name"); if (element !== null){ alert(element.xml); } var elements = xmldom.documentElement.selectNodes("employee/name"); alert(elements.length);
IE对命名空间的支持:要在 IE 中处理包含命名空间的 XPath 表达式,你必须知道自己使用的命名空间,并按照如下格式创建一个字符串:"xmlns:prefix1='uri1' xmlns:prefix2='uri2' xmlns:prefix3='uri3'"
1 xmldom.setProperty("SelectionNamespaces", "xmlns:wrox=’http://www.wrox.com/’"); 2 var result = xmldom.documentElement.selectNodes("wrox:book/wrox:author"); 3 alert(result.length);
3、跨浏览器使用XPath
因为IE对XPath功能的支持有限,因此跨浏览器 XPath只能保证达到 IE支持的功能。也就是要在其他使用 DOM3 级 XPath 对象的浏览器中,重新创建 selectSingleNode()和 selectNodes()方法。
selectSingleNode(),它接收三个参数:上下文节点、 XPath表达式和可选的命名空间对象
1 function selectSingleNode(context, expression, namespaces){ 2 var doc = (context.nodeType != 9 ? context.ownerDocument : context); 3 if (typeof doc.evaluate != "undefined"){ 4 var nsresolver = null; 5 if (namespaces instanceof Object){ 6 nsresolver = function(prefix){ 7 return namespaces[prefix]; 8 }; 9 } 10 var result = doc.evaluate(expression, context, nsresolver, 11 XPathResult.FirsT_ORDERED_NODE_TYPE, null); 12 return (result !== null ? result.singleNodeValue : null); 13 } else if (typeof context.selectSingleNode != "undefined"){ 14 //创建命名空间字符串 15 if (namespaces instanceof Object){ 16 var ns = ""; 17 for (var prefix in namespaces){ 18 if (namespaces.hasOwnProperty(prefix)){ 19 ns += "xmlns:" + prefix + "='" + namespaces[prefix] + "' "; 20 } 21 } 22 doc.setProperty("SelectionNamespaces", ns); 23 } 24 return context.selectSingleNode(expression); 25 } else { 26 throw new Error("No XPath engine found."); 27 } 28 } 29 30 var result = selectSingleNode(xmldom.documentElement, "wrox:book/wrox:author",{ wrox: "http://www.wrox.com/" }); 31 alert(serializeXml(result));
selectNodes()函数,接收与selectSingleNode()相同的三个参数,而且大部分逻辑都相似。
1 function selectNodes(context, expression, namespaces){ 2 var doc = (context.nodeType != 9 ? context.ownerDocument : context); 3 if (typeof doc.evaluate != "undefined"){ 4 var nsresolver = null; 5 if (namespaces instanceof Object){ 6 nsresolver = function(prefix){ 7 return namespaces[prefix]; 8 }; 9 } 10 var result = doc.evaluate(expression, context, nsresolver, 11 XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); 12 var nodes = new Array(); 13 if (result !== null){ 14 for (var i=0, len=result.snapshotLength; i < len; i++){ 15 nodes.push(result.snapshotItem(i)); 16 } 17 } 18 return nodes; 19 } else if (typeof context.selectNodes != "undefined"){ 20 //创建命名空间字符串 21 if (namespaces instanceof Object){ 22 var ns = ""; 23 for (var prefix in namespaces){ 24 if (namespaces.hasOwnProperty(prefix)){ 25 ns += "xmlns:" + prefix + "='" + namespaces[prefix] + "' "; 26 } 27 } 28 doc.setProperty("SelectionNamespaces", ns); 29 } 30 var result = context.selectNodes(expression); 31 var nodes = new Array(); 32 for (var i=0,len=result.length; i < len; i++){ 33 nodes.push(result[i]); 34 } 35 return nodes; 36 } else { 37 throw new Error("No XPath engine found."); 38 } 39 } 40 41 var result = selectNodes(xmldom.documentElement, "wrox:book/wrox:author",{ wrox: "http://www.wrox.com/" }); 42 alert(result.length);
三、浏览器对 XSLT的支持
XSLT是与XML相关的一种技术,它利用XPath将文档从一种表现形式转换成另一种表现形式,XML和XPath不同,XSLT没有正式的 API,只能依靠浏览器开发商以自己的方式来实现它。
1、IE中的XSLT
a、简单的XSLT转换
使用XSLT样式表转换XML文档的最简单方式,就是将它们分别加到一个DOM文档中,然后再使用transformNode()方法
b、复杂的XSLT转换
虽然 transformNode()方法提供了基本的 XSLT转换能力,但还有使用这种语言的更复杂的方式。 为此,必须要使用XSL模板和 XSL处理器。
2、XSLTProcessor类型
Mozilla通过在Firefox中创建新的类型,实现了JavaScript对XSLT的支持。开发人员可以通过XSLTProcessor类型使用XSLT转换 XML文档,其方式与在IE中使用 XSL处理器类似。
3、跨浏览器使用XSLT
IE对XSLT转换的支持与XSLTProcessor的区别实在太大,因此要想重新实现二者所有这方面的功能并不现实。因此,跨浏览器兼容性最好的 XSLT转换技术,只能是返回结果字符串,下面这个函数可以在 IE、Firefox、Chrome、Safari和 Opera中使用。
1 function transform(context, xslt){ 2 if (typeof XSLTProcessor != "undefined"){ 3 var processor = new XSLTProcessor(); 4 processor.importStylesheet(xslt); 5 var result = processor.transformTodocument(context); 6 return (new XMLSerializer()).serializetoString(result); 7 } else if (typeof context.transformNode != "undefined") { 8 return context.transformNode(xslt); 9 } else { 10 throw new Error("No XSLT processor available."); 11 } 12 }
第十九章 E4X
E4X是以ECMA-357标准的形式发布的对 ECMAScript的一个扩展。E4X的目的是为操作XML数据提供与标准ECMAScript更相近的语法。
E4X具有下列特征:
1、E4X只用一个类型来表示 XML中的各种节点。
2、XML 对象中封装了对所有节点都有用的数据和行为。为表现多个节点的集合,这个规范定义了 XMLList 类型 。
3、Namespace 和 QName,分别表现命名空间和限定名。
4、为便于查询 XML结构,E4X还修改了标准了的 ECMAScript语法,如:使用两个点(..)表示要匹配所有后代元素,使用@字符表示应该返回一或多个特性。星号字符(*)是一个通配符,可以匹配任意类型的节点。 所有这些查询都可以通过一组执行相同操作的方法来实现。
到2011年底,Firefox还是唯一一个支持E4X的浏览器,很多浏览器基本没有实现,现在开发中也用得很少,所以这一章只简单了解。
第二十章 JSON
JSON是一个轻量级的数据格式,可以简化表示复杂数据结构的工作量。JSON使用JavaScript语法的子集表示对象、数组、字符串、数值、布尔值和null。即使 XML也能表示同样复杂的数据结果,但JSON没有那么烦琐,而且在JavaScript中使用更便利。
一、语法
1、简单值
最简单的JSON 数据形式就是简单值。如数值 5,字符串 "Hello World!"。JavaScript字符串与JSON字符串的最大区别在于,JSON 字符串必须使用双引号。
2、对象
JSON中对象的属性必须加双引号,这在JSON中是必需的。属性的值可以是简单值,也可以是复杂类型值,因此可以像下面这样在对象中嵌入对象:
{ "name": "Nicholas", "age": 29, "school": { "name": "Merrimack College", "location": "north Andover, MA" } }
3、数组
JavaScript中的数组字面量:
var values = [25, "hi", true];
在JSON中,可以采用同样的语法表示同一个数组:(注意没有变量和分号)
如: [25, "hi", true]
二、解析与序列化
1、JSON对象有两个方法:stringify()和 parse()。在简单的情况下,这两个方法分别用于把JavaScript对象序列化为JSON字符串和把 JSON字符串解析为原生JavaScript值。
1 var book = { 2 title: "Professional JavaScript", 3 authors: [ 4 "Nicholas C. Zakas" 5 ], 6 edition: 3, 7 year: 2011 8 }; 9 var jsonText = JSON.stringify(book);
默认情况下,JSON.stringify()输出的JSON字符串不包含任何空格字符或缩进,所以输出如下:
{"title":"Professional JavaScript","authors":["Nicholas C. Zakas"],"edition":3, "year":2011}
将JSON字符串直接传递给 JSON.parse()就可以得到相应的 JavaScript值。例如,使用下列代码 就可以创建与book 类似的对象:
var bookcopy = JSON.parse(jsonText);
2、序列化选项
JSON.stringify()除了要序列化的JavaScript对象外,还可以接收另外两个参数,第一个参数是个过滤器,可以是一个数组,也可以是一个函数;第二个参数是一个选项,表示是否在 JSON字符串中保留缩进。
a、如果过滤器参数是数组,那么JSON.stringify()的结果中将只包含数组中列出的属性,如下:
var book = { "title": "Professional JavaScript", "authors": [ "Nicholas C. Zakas" ], edition: 3, year: 2011 }; //{"title":"Professional JavaScript","edition":3} var jsonText = JSON.stringify(book, ["title", "edition"]);
b、传入的函数接收两个参数,属性(键)名和属性值。根据属性(键)名可以知道应该如何处理要序列化的对象中的属性。如果函数返回了undefined,那么相应的属性会被忽略。
1 var book = { 2 "title": "Professional JavaScript", 3 "authors": [ 4 "Nicholas C. Zakas" 5 ], 6 edition: 3, 7 year: 2011 8 }; 9 var jsonText = JSON.stringify(book, function(key, value){ 10 switch(key){ 11 case "authors": 12 return value.join(",") 13 case "year": 14 return 5000; 15 case "edition": 16 return undefined; 17 default: 18 return value; 19 } 20 });
序列化后的 JSON字符串如下所示:
{"title":"Professional JavaScript","authors":"Nicholas C. Zakas","year":5000}
3、字符串缩进
JSON.stringify()方法的第三个参数用于控制结果中的缩进和空白符。如果这个参数是一个数值,那它表示的是每个级别缩进的空格数。
4、toJSON()方法
有时候,JSON.stringify()还是不能满足对某些对象进行自定义序列化的需求。在这些情况下,可以给对象定义toJSON()方法,返回其自身的JSON数据格式。
1 var book = { 2 "title": "Professional JavaScript", 3 "authors": [ 4 "Nicholas C. Zakas" 5 ], 6 edition: 3, 7 year: 2011, 8 toJSON: function(){ 9 return this.title; 10 } 11 }; 12 var jsonText = JSON.stringify(book);
以上代码在 book 对象上定义了一个 toJSON()方法,该方法返回图书的书名。
toJSON()可以作为函数过滤器的补充,假设把一个对象传入JSON.stringify(),序列化该对象的顺序如下:(1)、如果存在toJSON()方法而且能通过它取得有效的值,则调用该方法。否则,返回对象本身。 (2)、如果提供了第二个参数,应用这个函数过滤器。传入函数过滤器的值是第(1)步返回的值。 (3)、对第(2)步返回的每个值进行相应的序列化。 (4)、如果提供了第三个参数,执行相应的格式化。
5、 解析选项
JSON.parse()方法也可以接收另一个参数,该参数是一个函数,将在每个键值对儿上调用,这个函数被称为还原函数。如果还原函数返回undefined,则表示要从结果中删除相应的键;如果返回其他值,则将该值插入到结果中。
1 var book = { 2 "title": "Professional JavaScript", 3 "authors": [ 4 "Nicholas C. Zakas" 5 ], 6 edition: 3, 7 year: 2011, 8 releaseDate: new Date(2011, 11, 1) 9 }; 10 var jsonText = JSON.stringify(book); 11 var bookcopy = JSON.parse(jsonText, function(key, value){ 12 if (key == "releaseDate"){ 13 return new Date(value); 14 } else { 15 return value; 16 } 17 }); 18 alert(bookcopy.releaseDate.getFullYear());
以上代码book对象有一个releaseDate属性,该属性保存着一个Date对象。这个对象在经过序列化之后变成了有效的JSON字符串,然后经过解析又在bookcopy中还原为一个Date对象。还原函数在遇到"releaseDate"键时,会基于相应的值创建一个新的Date对象。结果就是bookcopy.releaseDate属性中会保存一个 Date 对象。正因为如此,才能基于这个对象调用getFullYear()方法,如果不这样bookcopy.releaseDate属性中只是一个字符串,不是Date对象。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。