JS權威指南讀書筆記(四)

第十三章 Web瀏覽器中的JavaScript

在HTML里嵌入JavaScript

在HTML文檔里嵌入客戶端JavaScript代碼有4種方法:

  1. 內聯,放置在<script></script>標簽對之間。
  2. 放置在由<script>標簽的src屬性指定的外部文件中。
  3. 放置在HTML事件處理程序中,該事件處理程序由onclick這樣的HTML屬性值指定。
  4. 放在一個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.imagesdocumemnt.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";//對整個文檔開啟編輯

所有現代瀏覽器都支持contenteditabledesignMode屬性。但是它們是不太兼容的。所有瀏覽器都允許插入與刪除文本并用鼠標和鍵盤移動光標。
瀏覽器定義了很多文本編輯命令,大部分沒有快捷鍵。可以使用Document對象的execCommand()方法。需要注意的是,這是Document的方法,而不是設置了contenteditable屬性的元素的方法。如果文檔中有多個可編輯的元素,命令將自動應用到選區或插入光標所在的那個元素上。具體的參數可參考這里

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 個人博客:https://yeaseonzhang.github.io 花了半個多月的時間,終于又把“JS紅寶書”...
    Yeaseon閱讀 11,582評論 9 52
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,923評論 18 139
  • 一、JS前言 (1)認識JS 也許你已經了解HTML標記(也稱為結構),知道了CSS樣式(也稱為表示),會使用HT...
    凜0_0閱讀 2,798評論 0 8
  • 我是個偵探小說迷,最喜歡的作者是阿加莎,偶像是福爾摩斯。 沒事就喜歡研究各種奇奇怪怪的謀殺事件,覺得以自己的聰明才...
    酸菜面閱讀 7,484評論 3 4
  • 小瓜今天回家鄉。 往年她都是年三十才回家,今年提前了兩天。 她想回去幫他老媽整理屋子,不知道她老媽會不會有機會給她...
    果123閱讀 167評論 0 0