第十三章 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.
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
屬性的元素的方法。如果文檔中有多個可編輯的元素,命令將自動應用到選區或插入光標所在的那個元素上。具體的參數可參考這里。