除 ES 基礎之外,Web 前端經常會用到一些跟瀏覽器相關的 API,接下來我們一起梳理一下。
</br>
## 知識點梳理
* BOM 操作
* DOM 操作
* 事件綁定
* Ajax
* 存儲
</br>
## BOM
BOM(瀏覽器對象模型)是瀏覽器本身的一些信息的設置和獲取,例如獲取瀏覽器的寬度、高度,設置讓瀏覽器跳轉到哪個地址。
* `navigator`
* `screen`
* `location`
* `history`
這些對象就是一堆非常簡單粗暴的 API,沒任何技術含量,講起來一點意思都沒有,大家去 MDN 或者 w3school 這種網站一查就都明白了。面試的時候,面試官基本不會出太多這方面的題目,因為只要基礎知識過關了,這些 API 即便你記不住,上網一查也都知道了。下面列舉一下常用功能的代碼示例
獲取瀏覽器特性(即俗稱的`UA`)然后識別客戶端,例如判斷是不是 Chrome 瀏覽器
```
var ua = navigator.userAgent
var isChrome = ua.indexOf('Chrome')
console.log(isChrome)
```
獲取屏幕的寬度和高度
```
console.log(screen.width)
console.log(screen.height)
```
獲取網址、協議、path、參數、hash 等
```
// 例如當前網址是 https://juejin.im/timeline/frontend?a=10&b=10#some
console.log(location.href) // https://juejin.im/timeline/frontend?a=10&b=10#some
console.log(location.protocol) // https:
console.log(location.pathname) // /timeline/frontend
console.log(location.search) // ?a=10&b=10
console.log(location.hash) // #some
```
另外,還有調用瀏覽器的前進、后退功能等
```
history.back()
history.forward()
```
</br>
## DOM
> 題目:DOM 和 HTML 區別和聯系
### 什么是 DOM
講 DOM 先從 HTML 講起,講 HTML 先從 XML 講起。XML 是一種可擴展的標記語言,所謂可擴展就是它可以描述任何結構化的數據,它是一棵樹!
```
<?xml version="1.0" encoding="UTF-8"?>
<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
<other>
<a></a>
<b></b>
</other>
</note>
```
HTML 是一個有既定標簽標準的 XML 格式,標簽的名字、層級關系和屬性,都被標準化(否則瀏覽器無法解析)。同樣,它也是一棵樹。
```
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div>
<p>this is p</p>
</div>
</body>
</html>
```
我們開發完的 HTML 代碼會保存到一個文檔中(一般以`.html`或者`.htm`結尾),文檔放在服務器上,瀏覽器請求服務器,這個文檔被返回。因此,最終瀏覽器拿到的是一個文檔而已,文檔的內容就是 HTML 格式的代碼。
但是瀏覽器要把這個文檔中的 HTML 按照標準渲染成一個頁面,此時瀏覽器就需要將這堆代碼處理成自己能理解的東西,也得處理成 JS 能理解的東西,因為還得允許 JS 修改頁面內容呢。
基于以上需求,瀏覽器就需要把 HTML 轉變成 DOM,HTML 是一棵樹,DOM 也是一棵樹。對 DOM 的理解,可以暫時先拋開瀏覽器的內部因素,先從 JS 著手,即可以認為 DOM 就是 JS 能識別的 HTML 結構,一個普通的 JS 對象或者數組。

### 獲取 DOM 節點
最常用的 DOM API 就是獲取節點,其中常用的獲取方法如下面代碼示例:
```
// 通過 id 獲取
var div1 = document.getElementById('div1') // 元素
// 通過 tagname 獲取
var divList = document.getElementsByTagName('div') // 集合
console.log(divList.length)
console.log(divList[0])
// 通過 class 獲取
var containerList = document.getElementsByClassName('container') // 集合
// 通過 CSS 選擇器獲取
var pList = document.querySelectorAll('p') // 集合
```
> 題目:property 和 attribute 的區別是什么?
### property
DOM 節點就是一個 JS 對象,它符合之前講述的對象的特征 —— 可擴展屬性,因為 DOM 節點本質上也是一個 JS 對象。因此,如下代碼所示,`p`可以有`style`屬性,有`className` `nodeName` `nodeType`屬性。注意,**這些都是 JS 范疇的屬性,符合 JS 語法標準的**。
```
var pList = document.querySelectorAll('p')
var p = pList[0]
console.log(p.style.width) // 獲取樣式
p.style.width = '100px' // 修改樣式
console.log(p.className) // 獲取 class
p.className = 'p1' // 修改 class
// 獲取 nodeName 和 nodeType
console.log(p.nodeName)
console.log(p.nodeType)
```
### attribute
property 的獲取和修改,是直接改變 JS 對象,而 attribute 是直接改變 HTML 的屬性,兩種有很大的區別。attribute 就是對 HTML 屬性的 get 和 set,和 DOM 節點的 JS 范疇的 property 沒有關系。
```
var pList = document.querySelectorAll('p')
var p = pList[0]
p.getAttribute('data-name')
p.setAttribute('data-name', 'juejin')
p.getAttribute('style')
p.setAttribute('style', 'font-size:30px;')
```
而且,get 和 set attribute 時,還會觸發 DOM 的查詢或者重繪、重排,頻繁操作會影響頁面性能。
> 題目:DOM 操作的基本 API 有哪些?
### DOM 樹操作
新增節點
```
var div1 = document.getElementById('div1')
// 添加新節點
var p1 = document.createElement('p')
p1.innerHTML = 'this is p1'
div1.appendChild(p1) // 添加新創建的元素
// 移動已有節點。注意,這里是“移動”,并不是拷貝
var p2 = document.getElementById('p2')
div1.appendChild(p2)
```
獲取父元素
```
var div1 = document.getElementById('div1')
var parent = div1.parentElement
```
獲取子元素
```
var div1 = document.getElementById('div1')
var child = div1.childNodes
```
刪除節點
```
var div1 = document.getElementById('div1')
var child = div1.childNodes
div1.removeChild(child[0])
```
還有其他操作的API,例如獲取前一個節點、獲取后一個節點等,但是面試過程中經常考到的就是上面幾個。
</br>
## 事件
### 事件綁定
普通的事件綁定寫法如下:
```
var btn = document.getElementById('btn1')
btn.addEventListener('click', function (event) {
// event.preventDefault() // 阻止默認行為
// event.stopPropagation() // 阻止冒泡
console.log('clicked')
})
```
為了編寫簡單的事件綁定,可以編寫通用的事件綁定函數。這里雖然比較簡單,但是會隨著后文的講解,來繼續完善和豐富這個函數。
```
// 通用的事件綁定函數
function bindEvent(elem, type, fn) {
elem.addEventListener(type, fn)
}
var a = document.getElementById('link1')
// 寫起來更加簡單了
bindEvent(a, 'click', function(e) {
e.preventDefault() // 阻止默認行為
alert('clicked')
})
```
最后,**如果面試被問到 IE 低版本兼容性問題,我勸你果斷放棄這份工作機會**。現在互聯網流量都在 App 上, IE 占比越來越少,再去為 IE 浪費青春不值得,要盡量去做 App 相關的工作。
> 題目:什么是事件冒泡?
### 事件冒泡
```
<body>
<div id="div1">
<p id="p1">激活</p>
<p id="p2">取消</p>
<p id="p3">取消</p>
<p id="p4">取消</p>
</div>
<div id="div2">
<p id="p5">取消</p>
<p id="p6">取消</p>
</div>
</body>
```
對于以上 HTML 代碼結構,要求點擊`p1`時候進入激活狀態,點擊其他任何`<p>`都取消激活狀態,如何實現?代碼如下,注意看注釋:
```
var body = document.body
bindEvent(body, 'click', function (e) {
// 所有 p 的點擊都會冒泡到 body 上,因為 DOM 結構中 body 是 p 的上級節點,事件會沿著 DOM 樹向上冒泡
alert('取消')
})
var p1 = document.getElementById('p1')
bindEvent(p1, 'click', function (e) {
e.stopPropagation() // 阻止冒泡
alert('激活')
})
```
如果我們在`p1` `div1` `body`中都綁定了事件,它是會根據 DOM 的結構來冒泡,從下到上挨個執行的。但是我們使用`e.stopPropagation()`就可以阻止冒泡
> 題目:如何使用事件代理?有何好處?
### 事件代理
我們設定一種場景,如下代碼,一個`<div>`中包含了若干個`<a>`,而且還能繼續增加。那如何快捷方便地為所有`<a>`綁定事件呢?
```
<div id="div1">
<a href="#">a1</a>
<a href="#">a2</a>
<a href="#">a3</a>
<a href="#">a4</a>
</div>
<button>點擊增加一個 a 標簽</button>
```
這里就會用到事件代理。我們要監聽`<a>`的事件,但要把具體的事件綁定到`<div>`上,然后看事件的觸發點是不是`<a>`。
```
var div1 = document.getElementById('div1')
div1.addEventListener('click', function (e) {
// e.target 可以監聽到觸發點擊事件的元素是哪一個
var target = e.target
if (e.nodeName === 'A') {
// 點擊的是 <a> 元素
alert(target.innerHTML)
}
})
```
我們現在完善一下之前寫的通用事件綁定函數,加上事件代理。
```
function bindEvent(elem, type, selector, fn) {
// 這樣處理,可接收兩種調用方式 bindEvent(div1, 'click', 'a', function () {...}) 和 bindEvent(div1, 'click', function () {...}) 這兩種
if (fn == null) {
fn = selector
selector = null
}
// 綁定事件
elem.addEventListener(type, function (e) {
var target
if (selector) {
// 有 selector 說明需要做事件代理
// 獲取觸發時間的元素,即 e.target
target = e.target
// 看是否符合 selector 這個條件
if (target.matches(selector)) {
fn.call(target, e)
}
} else {
// 無 selector ,說明不需要事件代理
fn(e)
}
})
}
```
然后這樣使用,簡單很多。
```
// 使用代理,bindEvent 多一個 'a' 參數
var div1 = document.getElementById('div1')
bindEvent(div1, 'click', 'a', function (e) {
console.log(this.innerHTML)
})
// 不使用代理
var a = document.getElementById('a1')
bindEvent(div1, 'click', function (e) {
console.log(a.innerHTML)
})
```
最后,使用代理的優點如下:
* 使代碼簡潔
* 減少瀏覽器的內存占用
</br>
## Ajax
### XMLHttpRequest
> 題目:手寫 XMLHttpRequest 不借助任何庫
這是很多奇葩的、個性的面試官經常用的手段。這種考查方式存在很多爭議,但是你不能完全說它是錯誤的,畢竟也是考查對最基礎知識的掌握情況。
```
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
// 這里的函數異步執行,可參考之前 JS 基礎中的異步模塊
if (xhr.readyState == 4) {
if (xhr.status == 200) {
alert(xhr.responseText)
}
}
}
xhr.open("GET", "/api", false)
xhr.send(null)
```
當然,使用 jQuery、Zepto 或 Fetch 等庫來寫就更加簡單了,這里不再贅述。
### 狀態碼說明
上述代碼中,有兩處狀態碼需要說明。`xhr.readyState`是瀏覽器判斷請求過程中各個階段的,`xhr.status`是 HTTP 協議中規定的不同結果的返回狀態說明。
`xhr.readyState`的狀態碼說明:
* 0 -代理被創建,但尚未調用 `open()` 方法。
* 1 -`open()` 方法已經被調用。
* 2 -`send()` 方法已經被調用,并且頭部和狀態已經可獲得。
* 3 -下載中, `responseText` 屬性已經包含部分數據。
* 4 -下載操作已完成
> 題目:HTTP 協議中,response 的狀態碼,常見的有哪些?
`xhr.status`即 HTTP 狀態碼,有 `2xx` `3xx` `4xx` `5xx` 這幾種,比較常用的有以下幾種:
* `200` 正常
* `3xx`
* `301` 永久重定向。如`http://xxx.com`這個 GET 請求(最后沒有`/`),就會被`301`到`http://xxx.com/`(最后是`/`)
* `302` 臨時重定向。臨時的,不是永久的
* `304` 資源找到但是不符合請求條件,不會返回任何主體。如發送 GET 請求時,head 中有`If-Modified-Since: xxx`(要求返回更新時間是`xxx`時間之后的資源),如果此時服務器 端資源未更新,則會返回`304`,即不符合要求
* `404` 找不到資源
* `5xx` 服務器端出錯了
看完要明白,為何上述代碼中要同時滿足`xhr.readyState == 4`和`xhr.status == 200`。
### Fetch API
目前已經有一個獲取 HTTP 請求更加方便的 API:`Fetch`,通過`Fetch`提供的`fetch()`這個全局函數方法可以很簡單地發起異步請求,并且支持`Promise`的回調。但是 Fetch API 是比較新的 API,具體使用的時候還需要查查 [caniuse](https://caniuse.com/),看下其瀏覽器兼容情況。
看一個簡單的例子:
```
fetch('some/api/data.json', {
method:'POST', //請求類型 GET、POST
headers:{}, // 請求的頭信息,形式為 Headers 對象或 ByteString
body:{}, //請求發送的數據 blob、BufferSource、FormData、URLSearchParams(get 或head 方法中不能包含 body)
mode:'', //請求的模式,是否跨域等,如 cors、 no-cors 或 same-origin
credentials:'', //cookie 的跨域策略,如 omit、same-origin 或 include
cache:'', //請求的 cache 模式: default、no-store、reload、no-cache、 force-cache 或 only-if-cached
}).then(function(response) { ... });
```
`Fetch` 支持`headers`定義,通過`headers`自定義可以方便地實現多種請求方法( PUT、GET、POST 等)、請求頭(包括跨域)和`cache`策略等;除此之外還支持 response(返回數據)多種類型,比如支持二進制文件、字符串和`formData`等。
### 跨域
> 題目:如何實現跨域?
瀏覽器中有 **同源策略** ,即一個域下的頁面中,無法通過 Ajax 獲取到其他域的接口。例如有一個接口`http://m.juejin.com/course/ajaxcourserecom?cid=459`,你自己的一個頁面`http://www.yourname.com/page1.html`中的 Ajax 無法獲取這個接口。這正是命中了“同源策略”。如果瀏覽器哪些地方忽略了同源策略,那就是瀏覽器的安全漏洞,需要緊急修復。
url 哪些地方不同算作跨域?
* 協議
* 域名
* 端口
但是 HTML 中幾個標簽能逃避過同源策略——`<script src="xxx">`、`<img src="xxxx"/>`、`<link href="xxxx">`,這三個標簽的`src/href`可以加載其他域的資源,不受同源策略限制。
因此,這使得這三個標簽可以做一些特殊的事情。
* `<img>`可以做打點統計,因為統計方并不一定是同域的,在講解 JS 基礎知識異步的時候有過代碼示例。除了能跨域之外,`<img>`幾乎沒有瀏覽器兼容問題,它是一個非常古老的標簽。
* `<script>`和`<link>`可以使用 CDN,CDN 基本都是其他域的鏈接。
* 另外`<script>`還可以實現 JSONP,能獲取其他域接口的信息,接下來馬上講解。
但是請注意,所有的跨域請求方式,最終都需要信息提供方來做出相應的支持和改動,也就是要經過信息提供方的同意才行,否則接收方是無法得到它們的信息的,瀏覽器是不允許的。
### 解決跨域 - JSONP
首先,有一個概念你要明白,例如訪問`http://coding.m.juejin.com/classindex.html`的時候,服務器端就一定有一個`classindex.html`文件嗎?—— 不一定,服務器可以拿到這個請求,動態生成一個文件,然后返回。 同理,`<script src="http://coding.m.juejin.com/api.js">`也不一定加載一個服務器端的靜態文件,服務器也可以動態生成文件并返回。OK,接下來正式開始。
例如我們的網站和掘金網,肯定不是一個域。我們需要掘金網提供一個接口,供我們來獲取。首先,我們在自己的頁面這樣定義
```
<script>
window.callback = function (data) {
// 這是我們跨域得到信息
console.log(data)
}
</script>
```
然后掘金網給我提供了一個`http://coding.m.juejin.com/api.js`,內容如下(之前說過,服務器可動態生成內容)
```
callback({x:100, y:200})
```
最后我們在頁面中加入`<script src="http://coding.m.juejin.com/api.js"></script>`,那么這個js加載之后,就會執行內容,我們就得到內容了。
### 解決跨域 - 服務器端設置 http header
這是需要在服務器端設置的,作為前端工程師我們不用詳細掌握,但是要知道有這么個解決方案。而且,現在推崇的跨域解決方案是這一種,比 JSONP 簡單許多。
```
response.setHeader("Access-Control-Allow-Origin", "http://m.juejin.com/"); // 第二個參數填寫允許跨域的域名稱,不建議直接寫 "*"
response.setHeader("Access-Control-Allow-Headers", "X-Requested-With");
response.setHeader("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
// 接收跨域的cookie
response.setHeader("Access-Control-Allow-Credentials", "true");
```
</br>
## 存儲
> 題目:cookie 和 localStorage 有何區別?
### cookie
cookie 本身不是用來做服務器端存儲的(計算機領域有很多這種“狗拿耗子”的例子,例如 CSS 中的 float),它是設計用來在服務器和客戶端進行信息傳遞的,因此我們的每個 HTTP 請求都帶著 cookie。但是 cookie 也具備瀏覽器端存儲的能力(例如記住用戶名和密碼),因此就被開發者用上了。
使用起來也非常簡單,`document.cookie = ....`即可。
但是 cookie 有它致命的缺點:
* 存儲量太小,只有 4KB
* 所有 HTTP 請求都帶著,會影響獲取資源的效率
* API 簡單,需要封裝才能用
### localStorage 和 sessionStorage
后來,HTML5 標準就帶來了`sessionStorage`和`localStorage`,先拿`localStorage`來說,它是專門為了瀏覽器端緩存而設計的。其優點有:
* 存儲量增大到 5MB
* 不會帶到 HTTP 請求中
* API 適用于數據存儲 `localStorage.setItem(key, value)` `localStorage.getItem(key)`
`sessionStorage`的區別就在于它是根據 session 過去時間而實現,而`localStorage`會永久有效,應用場景不同。例如,一些需要及時失效的重要信息放在`sessionStorage`中,一些不重要但是不經常設置的信息,放在`localStorage`中。
另外告訴大家一個小技巧,針對`localStorage.setItem`,使用時盡量加入到`try-catch`中,某些瀏覽器是禁用這個 API 的,要注意。
</br>
## 小結
本小節總結了 W3C 標準中 Web-API 部分,面試中常考的知識點,這些也是日常開發中最常用的 API 和知識。