一些很有用但不常见的 JavaScript APIs

Nicholas Zakas 发布了一个幻灯片,讲的是一些知名度低的 JavaScript API,我把它们都整理出来,并适当加了一些个人见解。

这篇文章里介绍的 API 大部分都是被普遍支持的(大部分 IE6~7 也能用),但是像是被我当成速查手册的 javascriptkit.com 也没完全包含进去,这就有点怪异了,我是认为像这类入门网站,是该好好完善兼容性高的基础 API,这样新人入行时才不至于漏掉一些实用知识。

下面的例子如无特殊说明,都是基于这个 HTML 结构:

<nav id="nav">
  <!-- comment -->
  <a>Home</a>
  <a>Blog</a>
</nav>
<script>
  var nav = document.getElementById('nav')
</script>

元素遍历

Element.children[]:这个 API 比起 Element.childNodes[] 的优点就是只包含 1 === nodeType 亦即 ELEMENT_NODE 的子节点,对于元素遍历来说方便很多,和 jQuery.children() 是一样效果的。注:IE8- 会包含 8 === nodeTypeCOMMENT_NODE,需要手动判断。

nav.childNodes[0] // '#text'
nav.children[0]   // <a>

Element.firstElementChildElement.lastElementChildElement.nextElementSiblingElement.previousElementSibling:和 children[] 一样,都会跳过非 ELEMENT_NODE 的子节点。注:IE8- 不支持。

nav.firstChild                              // '#text'
nav.lastChild                               // '#text'
nav.firstElementChild                       // <a>Home</a>
nav.lastElementChild                        // <a>Blog</a>
nav.firstElementChild.nextSibling           // '#text'
nav.lastElementChild.previousSibling        // '#text'
nav.firstElementChild.nextElementSibling    // <a>Blog>
nav.lastElementChild.previousElementSibling // <a>Home</a>

Element.contains():判断一个元素是否包含另一个元素。相比之下,jQuery.contains() 实现得很别扭,只能对比原生 DOM 元素,自己的 jQuery 对象无法对比。

document.body.contains(nav)         // true
jQuery.contains(document.body, nav) // true
jQuery.contains($('body'), nav)     // false

元素修改

Element.insertAdjacentHTML(location, html_string):把 html_string 插入到指定的地方,html_string 必须是合法的 HTML 片段,location 有四个值,分别是 beforebeginbeforeendafterbeginafterend,假设往 nav 插入 HTML,对应的位置如下:

<!-- beforebegin --><nav><!-- afterbegin -->
  <a>Home</a>
  <a>Blog</a>
<!-- beforeend --></nav><!-- afterend -->

需要注意的是 beforebeginafterend 是只有元素拥有父元素且在 DOM 树中时才有效,用在特定元素上也有可能出现非预期结果:

// 用在 body 上会产生两个 <head> 和 <body>(仅在 Chrome 上测试过)
document.body.insertAdjacentHTML('beforebegin', '<p>hello')
document.body.insertAdjacentHTML('afterend', '<p>hello')

// <p> 会消失
var d = document.createElement('div')
d.insertAdjacentHTML('afterend', '<p>hello')
nav.appendChild(d)

// <p> 会出现在 <nav> 里,和 <div> 同级
var d2 = document.createElement('div')
nav.appendChild(d2)
d2.insertAdjacentHTML('afterend', '<p>hello')

性能insertAdjacentHTMLinnerHTML 相近,选择用谁要看需求,如果插入的位置依赖于某个元素,insertAdjacentHTML 通常会便利一些。

Element.outerHTMLinnerHTML 的变种,也就不必过多说明了。应用起来和上两者一样,某些情况下会很便利。

document.implementation.createHTMLDocument(): 创建一个新的 DOCUMENT_NODE,也就是 document。虽然似乎没什么用,不过可以做的东西有很多,比如:

var new_doc = document.implementation.createHTMLDocument()
new_doc.body.innerHTML = html_string
// 删除特定标签
[].forEach.call(new_doc.body.querySelectorAll('script,link,object'), function(el){
  el.parentNode.removeChild(el)
})
// 将处理过、干净安全的 HTML 代码添加到真正的 DOM
document.body.innerHTML = new_doc.body.innerHTML

需要注意的是,创建出来文档里的元素用 getComputedStyle() 不一定能得到样式值的,所以要用于计算的话,请谨慎(仅在 Chrome 测试过):

var new_doc = document.implementation.createHTMLDocument()
  , p = new_doc.createElement('p')
p.innerHTML = 'hello world'
p.style.cssText = 'color: red;'
new_doc.body.appendChild(p)
console.log(p.style.color)                                 // 'red'
console.log(getComputedStyle(new_doc.querySelector('p')))  // all empty

文本选择

Element.select():选择文本框内的文本。

Element.setSelectionRange(position, length):选择指定范围内的文本,position0 开始算,小于 0 的也算 0length 如果小于或等于 position 则不会选择任何字符。

Element.selectionStartElement.selectionEnd:标识文本选择的起始位置。

document.activeElement:选择文档内获得焦点的元素。

XMLHttpRequest

XMLHttpRequest 很多人都会选择使用已经封装好的库吧,毕竟有兼容的问题,不过我喜欢从简,一般会挑个轻量的,之前也写了一个 jQuery 风格的 ajax 函数。如果你也想自己写,可以试试以下几个能加强 XHR 的玩意。

new FormData():实际就是模拟一个 <form> 提交时封装的数据包,不过是用类似 key: value 方式来处理,同时也能直接接受 <input> 的值,相当便利。

xhr.upload.onprogress:通常请求时都是用动态 GIF 来表示进行中,不过我曾经遇过一个客户,想要显示进度,又不希望用 Flash,当时因为难度问题,说服了他放弃,不过现在倒是有了这东西可以用。注:IE9- 不支持。

xhr.timeoutxhr.ontimeout:控制超时。注:IE9- 不支持。

xhr.responseType:目前支持四种,textdocumentblobarraybuffer,配合 xhr.response 使用。原来是只有 responseTextresponseXML 的,不过由于是字符串,下载后的处理比较麻烦,通过这些扩展类型,会方便很多,比如 documentblob 就能直接处理了:

var xhr = new XMLHttpRequest()
  , data = new FormData() // or FormData(document.form[index])
data.append('key', 'value')
data.append('key2', fileInput.file[0]) // 获取 <input> 元素的值

xhr.open('get', url, true)
xhr.timeout = 5000
xhr.ontimeout = function(event){
  console.log(arguments)
}
xhr.responseType = 'document'
xhr.upload.onprogress = function(event){
  console.log(arguments)
}
xhr.onload = function(event){
  var doc = event.currentTarget.response
  // do something like
  doc.querySelector('body')
}
xhr.send()

CSS 相关的

Element.matchesSelector()jQuery.is() 的原生实现。注:IE8- 不支持,其他的都要前缀,如 webkitMatchesSelector

Element.getBoundingClientRect():获取指定元素的矩形区域信息,简单地说,就是这个元素在文档里的座标、长高分别是多少,比起 getComputedStyle() 更方便,因为值是纯数字。注:IE7- 会给每个座标加 2,就像是被一个 padding: 2px 的容器包裹。

Element.document.elementFromPoint(x, y):获取指定座标的元素,如有多个,取 z-index 最大的。用在游戏或互动界面上应该不错,比如说球是不是进入了球门的座标里,或者结合 window.innerWidthwindow.innerHeight 来判断一个元素是否进入了可视范围,不过我更喜欢用 getBoundingClientRect()

下面是「加载更多」的不完整实现:

var footer = document.querySelector('footer')
window.addEventListener('scroll', function(){
  var rect = footer.getBoundingClientRect()
  if (rect.top <= window.innerHeight) {
    // do something...
  }
}, false)

window.matchMedia():判断 window 符不符合 CSS Media Query 的条件,比如 window.matchMedia("(max-width: 320px)")。主要用途是为移动设备启用不同的 JS 效果

JavaScript 原生的 API 是越来越强大、好用,很多时候都可以不需要库的加持,不过如果仍然要苦逼地支持 IE7-(其实我想说 IE8-……)的话,库还是最好的选择,毕竟解决了很多兼容性的问题。

BTW,最近看《松本行弘的程序世界》,Matz 对动态类型语言的一个观点觉得很实用:「无论如何都想检查(参数类型)的时候,也不要检查对象是否属于某个类,而是要检查对象是否有某个方法」,也就是这样:

var respond_to = function (o, f) {
  return !!(o != null && o[f]);
}
// 假设需转换标题为大写字母,但是传入参数不一定是 string
function title (o) {
  if (respond_to(o, 'toUpperCase')) {
    return o.toUpperCase()
  }
  // 传统做法
  if (typeof o === 'string') {
    return o.toUpperCase()
  }
}

这种做法原本是针对支持继承和多态的语言,比如父类和子类就有可能拥有同名的方法,如果单纯判断是不是某个类,就不能应对所有情况,而直接判断是否有这个方法的话,就可以适应各种情况,在 JavaScript 里也可以实现类似的效果:

var text_post = { title: "Hello World", toUpperCase: function(){
      return this.title.toUpperCase()
    }
  }
  , music_post = { name: "Hey Jade", toUpperCase: function(){
      return this.name.toUpperCase()
    }
  }
console.log(title(text_post)) // 'HELLO WORLD'
console.log(title(music_post)) // 'HEY JADE'

参考资料: