第十三章 Web浏览器中的JavaScript
在HTML里嵌入JavaScript
在HTML文档里嵌入客户端JavaScript代码有4种方法:
- 内联,放置在
<script></script>
标签对之间。 - 放置在由
<script>
标签的src属性指定的外部文件中。 - 放置在HTML事件处理程序中,该事件处理程序由
onclick
这样的HTML属性值指定。 - 放在一个URL里,这个URL使用特殊的"javascript:"协议。
现在大部分项目使用的是第二种方法,也就是通过src的方式引入脚本,使用该方法有以下优点:
- 可以把大量的JS代码从HTML文件删除,有助于保持内容和行为的分离。
- 如果多个Web页面共用相同的JS代码,用src属性就可以很方便的引入脚本。而且不需要重复写脚本。
- 如果一个脚本由多个页面共享,那么只需要下载一次。其他的页面可以在浏览器缓存中读取。
- 可以使用其他web服务器提供的脚本代码。很常用的做法就是引入第三方库的时候,一般使用百度或者谷歌提供的cdn地址。
URL中的JavaScript
javascript:URL
这个字符串是会被JS解释器运行的JS代码。它被当做单独的一行代码对待,这意味着语句之间必须用分号隔开,而//注释必须换成/* */.
javascript:URL
能识别的“资源”是转换成字符串的执行代码的返回值。如果代码返回undefined,那么这个资源就是没有内容的。
javascript:URL
可以用在可以使用常规URL的任意地方:比如<a>
的href属性,<form>
的action属性,甚至window.open()
方法的参数。
如果要确保javascript:URL
不会覆盖当前文档,可以用void操作符强制函数调用或给表达式赋予undefined值。比如:
<a href="javascript:void;"></a>
第14章 Window对象
浏览器定位和导航
Window对象的location属性引用的是Location对象,表示该窗口中当前显示的文档的URL,Document对象的location属性引用的也是Location对象,所以两者是恒等的(在浏览器内):
window.location === document.location
// 返回true
Document对象也有一个URL属性,是文档首次载入后保存该文档的URL的静态字符串。如果定位到文档中的片段标识符,Location对象会做相应的更新,而document.URL属性却不会改变。
以下是google首页https://www.google.com.hk/?hl=zh-CN&gws_rd=ssl的window.location的具体属性。
DOMStringList {length: 0}
assign:ƒ ()
hash:""
host:"www.google.com.hk"
hostname:"www.google.com.hk"
href:"https://www.google.com.hk/?hl=zh-CN&gws_rd=ssl"
origin:"https://www.google.com.hk"
pathname:"/"
port:""
protocol:"https:"
reload:ƒ reload()
replace:ƒ ()
search:"?hl=zh-CN&gws_rd=ssl"
toString:ƒ toString()
valueOf:ƒ valueOf()
Symbol(Symbol.toPrimitive):undefined
下面说几个比较重要的属性:
Location对象的href属性是一个字符串,后者包含URL的完整文本。Location对象的toString()方法返回href属性的值,因此在会隐式调用toString()的情况下,可以使用location代替location.href。所以以下代码是等价的:
window.location.href = 'https://www.google.com'
window.location = 'https://www.google.com'
search属性返回的是问号之后的URL(包括问号?),一般是用来查询的字符串,最常用的用法就是使用search在不同页面之间传递不敏感的信息,比如id之类的。标准的search参数是形如?key=value&key=value
,在笔者开发的项目中,导出文件就是使用search参数通过get请求向后台传递参数,有一个问题就是在实际的生产环境(edas环境)中,传递中文参数会报错,所以前端还需要将value使用encodeURI()
进行编码。
window.location.replace()
在载入新文档之前会从浏览历史中把当前文档删除。如果检测到用户的浏览器不支持某些新特性,那么就可以使用该方法来载入polyfill版本。
if (!ifSupport) {
window.location.replace('your page');
}
如果replace()的参数是一个相对URL,那么就会相对于当前页面所在的目录来解析。
执行window.location.reload()
会刷新当前页面。
浏览器和屏幕信息
Navigator对象
Window对象的navigator属性引用的是包含浏览器厂商和版本信息的Navigator对象。下面是MacBook Air的主要window.navigator
属性:
appCodeName:"Mozilla"
appName:"Netscape"
appVersion:"5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36"
cookieEnabled:true
language:"zh-CN"
languages:(4) ["zh-CN", "zh", "en", "fr"]
maxTouchPoints:0
onLine:true
platform:"MacIntel"
product:"Gecko"
productSub:"20030107"
usb:USB {onconnect: null, ondisconnect: null}
userAgent:"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36"
vendor:"Google Inc."
appName:
Web浏览器的全称。在旧版本IE中,就是"Microsoft Internet Explorer"。为了兼容现存的浏览器嗅探代码,其他浏览器(包括新版的IE)通常也取名为"Netscape"。
appVersion:
此属性通常以数字开始,并跟着包含浏览器厂商和版本信息的详细字符串。前面的数字通常是4.0
或者5.0
,表示是第4代或第5代兼容的浏览器。但是该字符串没有标准的格式,无法来判断浏览器类型。通常使用的是userAgent
属性。
userAgent:
浏览器在它的USER-AGENT HTTP头部发送的字符串。这个属性通常包含appVersion中的所有信息,并且常常可能包含其他细节,虽然没有标准格式,但是由于包含绝大部分信息,因此使用该属性判断浏览器类型。
下面是一个判断浏览器类型的通用方法:
function getExplore() {
var sys = {},
ua = navigator.userAgent.toLowerCase(),
s;
(s = ua.match(/rv:([\d.]+)\) like gecko/)) ? sys.ie = s[1]:
(s = ua.match(/msie ([\d\.]+)/)) ? sys.ie = s[1] :
(s = ua.match(/edge\/([\d\.]+)/)) ? sys.edge = s[1] :
(s = ua.match(/firefox\/([\d\.]+)/)) ? sys.firefox = s[1] :
(s = ua.match(/(?:opera|opr).([\d\.]+)/)) ? sys.opera = s[1] :
(s = ua.match(/chrome\/([\d\.]+)/)) ? sys.chrome = s[1] :
(s = ua.match(/version\/([\d\.]+).*safari/)) ? sys.safari = s[1] : 0;
// 根据关系进行判断
if (sys.ie) return ('IE: ' + sys.ie)
if (sys.edge) return ('EDGE: ' + sys.edge)
if (sys.firefox) return ('Firefox: ' + sys.firefox)
if (sys.chrome) return ('Chrome: ' + sys.chrome)
if (sys.opera) return ('Opera: ' + sys.opera)
if (sys.safari) return ('Safari: ' + sys.safari)
return 'Unknown'
}
onLine:
表示浏览器当前是否连接到网络。可以根据这个布尔值状态来进行一些操作。
Screen对象
Window对象的screen属性引用的是Screen对象。以下是chrome 62的window.screen
属性的具体属性:
availHeight:797
availLeft:0
availTop:23
availWidth:1440
colorDepth:24
height:900
pixelDepth:24
width:1440
属性width和height指定的是以像素为单位的窗口大小。属性availWidth和availHeight指定的实际可用的显示大小。属性colorDepth指定的是显示的BPP(bits-per-pixel)值,典型的值由16、24和32。
可以用Screen对象来确定Web应用是否运行在一个小屏幕的设备上。
打开和关闭窗口
window.open()用来打开一个新的浏览器窗口。该方法指定的URL到新的或已存在的窗口中,并返回代表那个窗口的Window对象。open()有4个可选的参数。
第一个参数是要在新窗口显示的URL。如果省略这个参数,那么空页面的URL为about:blank
。
第二个参数是新打开窗口的名字。如果指定一个已经存在的窗口的名字,那么会直接使用(跳到)已存在的窗口。否则,会打开新的窗口,并且将指定的名字赋值给新窗口。如果省略此参数,那么新窗口的name为''。(chrome 62)查看新窗口的name可以通过window.name
来获取到,该属性是可读可写的,脚本可以随意设置。
第三个参数是一个以逗号分隔的列表,包含大小和各种属性,用来表明新窗口如何打开。比如,要打开允许改变大小的浏览器窗口,并且包含状态栏、工具栏和地址栏,可以这样:
window.open("https://www.google.com.hk","someName","width=400,height=350,status=yes,resizable=yes");
然后就会打开一个指定宽高,但是没有状态栏、工具栏,可以缩放窗口。在chrome 62测试后,不论status值为true或false,新窗口都没有工具栏,不论resizable值为何,都可以改变窗口大小。
第三个参数是非标准的,HTML5规范也主张浏览器忽略。
第四个参数只有在第二个参数命名的是一个存在的窗口时才有用。该参数是一个布尔值,声明了由第一个参数指定的URL是替换掉当前URL(true),还是在新窗口新建一个URL(false),默认值为false。
第15章 脚本化文档
节点列表和HTML集合
getElementsByName()
和getElementsByTagName
都返回NodeList对象,而类似document.images
和documemnt.forms
的属性为HTMLCollection对象。
NodeList和HTMLCollection对象都是只读的类数组对象。具有leng属性,也具有索引(只读)。所以可以使用for循环进行迭代。
但是他们不是真正的数组,所以不能直接在两个集合上面调用Array的方法,可以通过以下方式来调用:
var tag = document.getElementsByTagName('*')
var content = Array.prototype.map.call(tag,function(e) {
return e.innerHTML;
});
//下面的方法是使用了ES6的数组方法from(),该方法将类数组转化为真正的数组。
let array = Array.from(tag);
NodeList和HTMLCollection对象不是历史文档状态的静态快照,而是实时的。如果在一个不存在div元素的文档中插入一个新的<div>
元素,那么getElementsByTagName('div')
的length
属性会由0变为1.
通过CSS选择器选取元素
与CSS3选择器的标准化一起的另一个称做“选择器API”的W3C标准定义了获取匹配一个给定选择器的元素的JS方法。该API的关键的Document方法querySelectorAll()
。接受包含一个CSS选择器的字符串参数,返回一个表示文档中匹配选择器的所有元素的NodeList对象。
需要注意的是,querySelectorAll()
返回的NodeList不是实时的。只包含调用时刻选择器所匹配的元素,不更新后续变化。如果没有匹配的元素,将返回一个空的NodeList对象([])。如果选择器字符串非法,将抛出异常。
类似的还有querySelector()
方法。但是与querySelectorAll()
不同的是,querySelector()
只返回第一个匹配的元素,如果没有匹配的元素那么就返回null。
在CSS中,伪元素匹配文本节点的一部分而不是实际元素。如果和querySelector()
或querySelectorAll()
一起使用它们是不匹配的。而且很多浏览器会拒绝返回":link"等伪类的匹配结果,因为会泄露用户的浏览历史记录。jQuery的$()
与querySelectorAll()
就是等效的。
文档结构和遍历
Document对象、Element对象和Text对象都是Node对象。Node定义了以下重要的属性:
parentNode
该节点的父节点。如果一个节点没有父节点,那么将会返回null。
childNodes
只读的类数组对象,它是该节点的子节点的实时表示。
firstChild、lastChild
该节点的子节点中的第一个和最后一个,如果没有则返回null。
nextSibling、previousSibling
该节点的兄弟节点的前一个和下一个。如果没有则返回null。
nodeType
该节点的类型。具体值见下表(参考于MDN)
常量 | 值 | 描述 |
---|---|---|
Node.ELEMENT_NODE | 1 | 一个元素节点,例如<p>和<div> 。 |
Node.TEXT_NODE | 3 | Element或者Attr中实际的文字 |
Node.PROCESSING_INSTRUCTION_NODE | 7 | 一个用于XML文档的 ProcessingInstruction ,例如<?xml-stylesheet ... ?>声明。 |
Node.COMMENT_NODE | 8 | 一个 Comment 节点。 |
Node.DOCUMENT_NODE | 9 | 一个 Document 节点。 |
Node.DOCUMENT_TYPE_NODE | 10 | 描述文档类型的 DocumentType 节点。例如 <!DOCTYPE html> 就是用于 HTML5 的。 |
Node.DOCUMENT_FRAGMENT_NODE | 11 | 一个 DocumentFragment 节点 |
为什么值不是连续的,因为1-12中没有的值已经被废弃了,无需了解。
nodeValue
Text节点或Comment节点的文本内容。
nodeName
元素的标签名,以大写形式表示。
属性
表示HTML文档元素的HTMLElement
对象定义了读/写属性,它们映射了元素的HTML属性。HTMLElement
定义了通用的HTTP属性(比如id)的属性。特定的Element子类型为其元素定义了特定的属性。例如:
var image = document.getElementById("id");
var imgUrl = image.src;//获取图片的URL
var id = image.id;//获取节点的id属性
HTML属性名不区分大小写,但是JS属性名则大小写敏感。如果属性名包含不止一个单词,那么应该采用驼峰命名来书写js属性名。
有些HTML属性名在JS中是保留字。一般规则是为JS属性名加上前缀html
。比如,HTML的for属性(<label>
元素)在JS中变为htmlFor
属性。HTML的属性class
变为className
。
数据集属性
在HTML5文档中,任意以"data-"为前缀的小写的属性名字都是合法的。HTML5还在Element对象上定义了dataset属性。比如:
var node = document.getElementById('id');
node.dataset.x = 'x';
//node.dataset.x保存的就是data-x属性的值。
node.dataset.nameTest = 'test';
// node.dataset.nameTest驼峰式命名保存的是data-name-test属性的值。
dataset属性是实时、双向接口。
元素的内容
读取Element的innerHTML属性作为字符串标记返回那个元素的内容。通常设置innerHTML效率非常高,甚至在指定的值需要解析时效率也不错。但是,对innerHTML属性用"+="操作符重复追加一小段文本效率低下,因为既要序列化又要解析。
HTML5还标准化了outerHTML属性。outerHTML会返回包含被查询元素的开头和结尾标签所组成的字符串。当设置元素的outerHTML时,元素本身被新的内容替换。只有Element节点定义了outerHTML属性,Document节点没有。
创建、插入和删除节点
创建节点
使用Document
对象的createElement()
方法,给方法传递元素的标签名,对HTML来说名字不区分大小写。
Text节点用类似的方法创建:
var h = document.createElement("H1");
var t = document.createTextNode("Hello World");
h.appendChild(t);
上例就是使用createTextNode()
方法在h1
标签里创建了Text文本。
另一种创建新文档节点的方法是复制已存在的节点。每个节点有一个cloneNode()
方法来返回该节点的副本。给方法传递参数false
或者不传参数,那么该方法只进行浅复制(只复制当前节点);给方法传递参数true
能够递归地复制所有的后代节点。举个例子:
//在谷歌的搜索结果页面进行浅复制某元素
document.getElementById('fbar').cloneNode();
//返回结果如下:
<div id="fbar" class="_Zvd" style="left:0;right:0"></div>
插入节点
一旦创建了一个新的节点,就可以使用Node的方法appendChild()
或insertBefore()
将它插入到文档中。
appendChild()
是在需要插入的Element节点上调用,该方法的参数是需要插入的元素。
insertBefore()
接受两个参数。第一个参数就是待插入的节点,第二个参数是已存在的节点,第一参数将插入到第二参数的前面。该方法应该是在新节点的父节点上调用,第二参数必须是该父节点的子节点。如果传递null
作为第二参数,那么行为就跟appendChild()
类似。举个例子:
// Create a new, plain <span> element
var sp1 = document.createElement("span");
// Get a reference to the element, before we want to insert the element
var sp2 = document.getElementById("childElement");
// Get a reference to the parent element
var parentDiv = sp2.parentNode;
// Insert the new element into the DOM before sp2
parentDiv.insertBefore(sp1, sp2);
需要注意的是,如果调用上述两个方法将已存在文档中的一个节点再次插入,那么该节点将自动从当前的位置删除并在新的位置重新插入。也就是说,没有必要显示的进行删除节点。
删除和替换节点
removeChild()
方法从文档树中删除一个节点。同样的,该方法是在参数的父节点上调用。参数就是需要删除的子节点。所以可以这么删除一个节点:
var n = document.getElementById('childrenId');
n.parentNode.removeChild(n);
replaceChild()
方法删除一个节点并用一个新的节点取代。同样的,该方法需要在参数的父节点上调用。该方法具有两个参数,第一个参数是新节点,第二个参数是需要代替的节点。比如:
// 用一个文本字符串来替换节点n
n.parentNode.replaceChild(document.createTextNode("Hello World"),n);
使用DocumentFragment
使用DocumentFragment
是一种特殊的Node,作为其他节点的一个临时的容器。
var frag = document.createDocumentFragment();
DocumentFragment
是独立的,而不是任何其他文档的一部分。它的parentNode
总是null
。但是可以有任意多的子节点。
DocumentFragment
是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有的子元素所代替。
因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(reflow)(对元素位置和几何上的计算)。因此,使用文档片段document fragments 通常会起到优化性能的作用。举个例子:
// assuming it exists (ul element)
let ul = document.getElementsByTagName("ul")[0],
docfrag = document.createDocumentFragment();
const browserList = [
"Internet Explorer",
"Mozilla Firefox",
"Safari",
"Chrome",
"Opera"
];
browserList.forEach((e) => {
let li = document.createElement("li");
// 插入纯文本
li.textContent = e;
// 将遍历的li插入文档片段中
docfrag.appendChild(li);
});
//遍历完后,将文档片段插入到ul中,文档片段被子元素代替
ul.appendChild(docfrag);
可编辑的内容
有两种方法来启用编辑功能。其一,设置任何标签的HTML contenteditable
属性;其二,设置对应元素的JavaScript contenteditable
属性。浏览器可能为表单字段和contenteditable
元素支持自动拼写检查。在支持该功能的浏览器中,检查可能默认开启或者关闭。为元素添加spellcheck
属性显示开启拼写检查,spellcheck=false
显示关闭。
var node = document.getElementById("editor");
node.contentEditable = "true";//通过JS开启编辑
将Document对象的designMode
属性设置为字符串"on"使得整个文档可编辑。designMode
属性没有对应的HTML属性。举个例子:
document.designMode = "on";//对整个文档开启编辑
所有现代浏览器都支持contenteditable
和designMode
属性。但是它们是不太兼容的。所有浏览器都允许插入与删除文本并用鼠标和键盘移动光标。
浏览器定义了很多文本编辑命令,大部分没有快捷键。可以使用Document对象的execCommand()
方法。需要注意的是,这是Document的方法,而不是设置了contenteditable
属性的元素的方法。如果文档中有多个可编辑的元素,命令将自动应用到选区或插入光标所在的那个元素上。具体的参数可参考这里。