JavaScript簡明教程之瀏覽器,前端程序猿必看

因爲JavaScript的出現就是爲了能在瀏覽器中運行,因此,瀏覽器天然是JavaScript開發者必需要關注的。【寫的比較多,不免有疏漏的地方,但願朋友們指正】css

目前主流的瀏覽器分這麼幾種:html

IE 6~11:國內用得最多的IE瀏覽器,從來對W3C標準支持差。從IE10開始支持ES6標準;java

Chrome:Google出品的基於Webkit內核瀏覽器,內置了很是強悍的JavaScript引擎——V8。因爲Chrome一經安裝就時刻保持自升級,因此不用管它的版本,最新版早就支持ES6了;python

Sarafi:Apple的Mac系統自帶的基於Webkit內核的瀏覽器,從OS X 10.7 Lion自帶的6.1版本開始支持ES6,目前最新的OS X 10.10 Yosemite自帶的Sarafi版本是8.x,早已支持ES6;jquery

Firefox:Mozilla本身研製的Gecko內核和JavaScript引擎OdinMonkey。早期的Firefox按版本發佈,後來終於聰明地學習Chrome的作法進行自升級,時刻保持最新;ajax

移動設備上目前iOS和Android兩大陣營分別主要使用Apple的Safari和Google的Chrome,因爲二者都是Webkit核心,結果HTML5首先在手機上全面普及(桌面絕對是Microsoft拖了後腿),對JavaScript的標準支持也很好,最新版本均支持ES6。json

其餘瀏覽器如Opera等因爲市場份額過小就被自動忽略了。api

另外還要注意識別各類國產瀏覽器,如某某安全瀏覽器,某某旋風瀏覽器,它們只是作了一個殼,其核心調用的是IE,也有號稱同時支持IE和Webkit的「雙核」瀏覽器。跨域

不一樣的瀏覽器對JavaScript支持的差別主要是,有些API的接口不同,好比AJAX,File接口。對於ES6標準,不一樣的瀏覽器對各個特性支持也不同。promise

在編寫JavaScript的時候,就要充分考慮到瀏覽器的差別,儘可能讓同一份JavaScript代碼能運行在不一樣的瀏覽器中。

瀏覽器對象
JavaScript能夠獲取瀏覽器提供的不少對象,並進行操做。

window
window對象不但充當全局做用域,並且表示瀏覽器窗口。

window對象有innerWidth和innerHeight屬性,能夠獲取瀏覽器窗口的內部寬度和高度。內部寬高是指除去菜單欄、工具欄、邊框等佔位元素後,用於顯示網頁的淨寬高。

兼容性:IE<=8不支持。

'use strict';
// 能夠調整瀏覽器窗口大小試試:
alert('window inner size: ' + window.innerWidth + ' x ' + window.innerHeight);
對應的,還有一個outerWidth和outerHeight屬性,能夠獲取瀏覽器窗口的整個寬高。

navigator
navigator對象表示瀏覽器的信息,最經常使用的屬性包括:

navigator.appName:瀏覽器名稱;
navigator.appVersion:瀏覽器版本;
navigator.language:瀏覽器設置的語言;
navigator.platform:操做系統類型;
navigator.userAgent:瀏覽器設定的User-Agent字符串。
'use strict';
alert('appName = ' + navigator.appName + '\n' +
      'appVersion = ' + navigator.appVersion + '\n' +
      'language = ' + navigator.language + '\n' +
      'platform = ' + navigator.platform + '\n' +
      'userAgent = ' + navigator.userAgent);

請注意,navigator的信息能夠很容易地被用戶修改,因此JavaScript讀取的值不必定是正確的。不少初學者爲了針對不一樣瀏覽器編寫不一樣的代碼,喜歡用if判斷瀏覽器版本,例如:

var width;
if (getIEVersion(navigator.userAgent) < 9) { 
  width = document.body.clientWidth;
} else { 
  width = window.innerWidth;
}

但這樣既可能判斷不許確,也很難維護代碼。正確的方法是充分利用JavaScript對不存在屬性返回undefined的特性,直接用短路運算符||計算:

var width = window.innerWidth || document.body.clientWidth;
screen
screen對象表示屏幕的信息,經常使用的屬性有:

screen.width:屏幕寬度,以像素爲單位;
screen.height:屏幕高度,以像素爲單位;
screen.colorDepth:返回顏色位數,如八、1六、24。
'use strict';
alert('Screen size = ' + screen.width + ' x ' + screen.height);
location
location對象表示當前頁面的URL信息。例如,一個完整的URL:

http://www.example.com:8080/p...
能夠用location.href獲取。要得到URL各個部分的值,能夠這麼寫:

location.protocol; // 'http'
location.host; // 'www.example.com'
location.port; // '8080'
location.pathname; // '/path/index.html'
location.search; // '?a=1&b=2'
location.hash; // 'TOP'
要加載一個新頁面,能夠調用location.assign()。若是要從新加載當前頁面,調用location.reload()方法很是方便。

if (confirm('從新加載當前頁' + location.href + '?')) {
    location.reload();
} else {
    location.assign('/discuss'); // 設置一個新的URL地址
}

document
document對象表示當前頁面。因爲HTML在瀏覽器中以DOM形式表示爲樹形結構,document對象就是整個DOM樹的根節點。

document的title屬性是從HTML文檔中的<title>xxx</title>讀取的,可是能夠動態改變:

'use strict';
document.title = '努力學習JavaScript!';
請觀察瀏覽器窗口標題的變化。

要查找DOM樹的某個節點,須要從document對象開始查找。最經常使用的查找是根據ID和Tag Name。

咱們先準備HTML數據:

<dl id="drink-menu" style="border:solid 1px #ccc;padding:6px;"> 
  <dt>摩卡</dt> 
  <dd>熱摩卡咖啡</dd> 
  <dt>酸奶</dt> 
  <dd>北京老酸奶</dd> 
  <dt>果汁</dt> 
  <dd>鮮榨蘋果汁</dd>
</dl>

用document對象提供的getElementById()和getElementsByTagName()能夠按ID得到一個DOM節點和按Tag名稱得到一組DOM節點:

'use strict';
var menu = document.getElementById('drink-menu');
var drinks = document.getElementsByTagName('dt');
var i, s, menu, drinks;

menu = document.getElementById('drink-menu');
menu.tagName; // 'DL'

drinks = document.getElementsByTagName('dt');
s = '提供的飲料有:';
for (i=0; i<drinks.length; i++) {
    s = s + drinks[i].innerHTML + ',';
}
alert(s);

document對象還有一個cookie屬性,能夠獲取當前頁面的Cookie。

Cookie是由服務器發送的key-value標示符。由於HTTP協議是無狀態的,可是服務器要區分究竟是哪一個用戶發過來的請求,就能夠用Cookie來區分。當一個用戶成功登陸後,服務器發送一個Cookie給瀏覽器,例如user=ABC123XYZ(加密的字符串)...,此後,瀏覽器訪問該網站時,會在請求頭附上這個Cookie,服務器根據Cookie便可區分出用戶。

Cookie還能夠存儲網站的一些設置,例如,頁面顯示的語言等等。

JavaScript能夠經過document.cookie讀取到當前頁面的Cookie:

document.cookie; // 'v=123; remember=true; prefer=zh'
因爲JavaScript能讀取到頁面的Cookie,而用戶的登陸信息一般也存在Cookie中,這就形成了巨大的安全隱患,這是由於在HTML頁面中引入第三方的JavaScript代碼是容許的:

<!-- 當前頁面在wwwexample.com -->
<html> 
  <head> 
    <script src="http://www.foo.com/jquery.js"></script>   
  </head> 
    ...
</html>

若是引入的第三方的JavaScript中存在惡意代碼,則www.foo.com網站將直接獲取到www.example.com網站的用戶登陸信息。

爲了解決這個問題,服務器在設置Cookie時可使用httpOnly,設定了httpOnly的Cookie將不能被JavaScript讀取。這個行爲由瀏覽器實現,主流瀏覽器均支持httpOnly選項,IE從IE6 SP1開始支持。

爲了確保安全,服務器端在設置Cookie時,應該始終堅持使用httpOnly。

history
history對象保存了瀏覽器的歷史記錄,JavaScript能夠調用history對象的back()或forward (),至關於用戶點擊了瀏覽器的「後退」或「前進」按鈕。

這個對象屬於歷史遺留對象,對於現代Web頁面來講,因爲大量使用AJAX和頁面交互,簡單粗暴地調用history.back()可能會讓用戶感到很是憤怒。

新手開始設計Web頁面時喜歡在登陸頁登陸成功時調用history.back(),試圖回到登陸前的頁面。這是一種錯誤的方法。

任何狀況,你都不該該使用history這個對象了。

操做DOM
因爲HTML文檔被瀏覽器解析後就是一棵DOM樹,要改變HTML的結構,就須要經過JavaScript來操做DOM。

始終記住DOM是一個樹形結構。操做一個DOM節點實際上就是這麼幾個操做:

更新:更新該DOM節點的內容,至關於更新了該DOM節點表示的HTML的內容;

遍歷:遍歷該DOM節點下的子節點,以便進行進一步操做;

添加:在該DOM節點下新增一個子節點,至關於動態增長了一個HTML節點;

刪除:將該節點從HTML中刪除,至關於刪掉了該DOM節點的內容以及它包含的全部子節點。

在操做一個DOM節點前,咱們須要經過各類方式先拿到這個DOM節點。最經常使用的方法是document.getElementById()和document.getElementsByTagName(),以及CSS選擇器document.getElementsByClassName()。

因爲ID在HTML文檔中是惟一的,因此document.getElementById()能夠直接定位惟一的一個DOM節點。document.getElementsByTagName()和document.getElementsByClassName()老是返回一組DOM節點。要精確地選擇DOM,能夠先定位父節點,再從父節點開始選擇,以縮小範圍。

// 返回ID爲'test'的節點:
var test = document.getElementById('test');

// 先定位ID爲'test-table'的節點,再返回其內部全部tr節點:
var trs = document.getElementById('test-table').getElementsByTagName('tr');

// 先定位ID爲'test-div'的節點,再返回其內部全部class包含red的節點:
var reds = document.getElementById('test-div').getElementsByClassName('red');

// 獲取節點test下的全部直屬子節點:
var cs = test.children;

// 獲取節點test下第一個、最後一個子節點:
var first = test.firstElementChild;var last = test.lastElementChild;

第二種方法是使用querySelector()和querySelectorAll()
,須要瞭解selector語法,而後使用條件來獲取節點,更加方便:

// 經過querySelector獲取ID爲q1的節點:
var q1 = document.querySelector('#q1');

// 經過querySelectorAll獲取q1節點內的符合條件的全部節點:
var ps = q1.querySelectorAll('div.highlighted > p');

注意:低版本的IE<8不支持querySelector和querySelectorAll。IE8僅有限支持。

嚴格地講,咱們這裏的DOM節點是指Element,可是DOM節點其實是Node,在HTML中,Node包括Element、Comment、CDATA_SECTION等不少種,以及根節點Document類型,可是,絕大多數時候咱們只關心Element,也就是實際控制頁面結構的Node,其餘類型的Node忽略便可。根節點Document已經自動綁定爲全局變量document。

更新DOM
拿到一個DOM節點後,咱們能夠對它進行更新。

能夠直接修改節點的文本,方法有兩種:

一種是修改innerHTML屬性,這個方式很是強大,不但能夠修改一個DOM節點的文本內容,還能夠直接經過HTML片斷修改DOM節點內部的子樹:

// 獲取<p id="p-id">...</p>
var p = document.getElementById('p-id');

// 設置文本爲abc:
p.innerHTML = 'ABC';   // <p id="p-id">ABC</p>

// 設置HTML:
p.innerHTML = 'ABC <span style="color:red">RED</span> XYZ';
// <p>...</p>的內部結構已修改

用innerHTML時要注意,是否須要寫入HTML。若是寫入的字符串是經過網絡拿到了,要注意對字符編碼來避免XSS攻擊。

第二種是修改innerText或textContent屬性,這樣能夠自動對字符串進行HTML編碼,保證沒法設置任何HTML標籤:

// 獲取<p id="p-id">...</p>
var p = document.getElementById('p-id');

// 設置文本:
p.innerText = '<script>alert("Hi")</script>';

// HTML被自動編碼,沒法設置一個<script>節點:
// <p id="p-id">&lt;script&gt;alert("Hi")&lt;/script&gt;</p>

二者的區別在於讀取屬性時,innerText不返回隱藏元素的文本,而textContent返回全部文本。另外注意IE<9不支持textContent。

修改CSS也是常常須要的操做。DOM節點的style屬性對應全部的CSS,能夠直接獲取或設置。由於CSS容許font-size這樣的名稱,但它並不是JavaScript有效的屬性名,因此須要在JavaScript中改寫爲駝峯式命名fontSize:

// 獲取<p id="p-id">...</p>
var p = document.getElementById('p-id');

// 設置CSS:
p.style.color = '#ff0000';
p.style.fontSize = '20px';
p.style.paddingTop = '2em';

插入DOM
當咱們得到了某個DOM節點,想在這個DOM節點內插入新的DOM,應該如何作?

若是這個DOM節點是空的,例如,<div></div>,那麼,直接使用innerHTML = '<span>child</span>'就能夠修改DOM節點的內容,至關於「插入」了新的DOM節點。

若是這個DOM節點不是空的,那就不能這麼作,由於innerHTML會直接替換掉原來的全部子節點。

有兩個辦法能夠插入新的節點。一個是使用appendChild,把一個子節點添加到父節點的最後一個子節點。例如:

<!-- HTML結構 -->
<p id="js">JavaScript</p>
<div id="list"> 
  <p id="java">Java</p>
  <p id="python">Python</p> 
  <p id="scheme">Scheme</p>
</div>
把<p id="js">JavaScript</p>添加到<div id="list">的最後一項:

var js = document.getElementById('js'), 
     list = document.getElementById('list');
list.appendChild(js);

如今,HTML結構變成了這樣:

<!-- HTML結構 -->
<div id="list"> 
  <p id="java">Java</p> 
  <p id="python">Python</p> 
  <p id="scheme">Scheme</p> 
  <p id="js">JavaScript</p>
</div>

由於咱們插入的js節點已經存在於當前的文檔樹,所以這個節點首先會從原先的位置刪除,再插入到新的位置。

更多的時候咱們會從零建立一個新的節點,而後插入到指定位置:

var list = document.getElementById('list'), 
      haskell = document.createElement('p');
haskell.id = 'haskell';
haskell.innerText = 'Haskell';
list.appendChild(haskell);
這樣咱們就動態添加了一個新的節點:

<!-- HTML結構 -->
<div id="list"> 
  <p id="java">Java</p> 
  <p id="python">Python</p> 
  <p id="scheme">Scheme</p> 
  <p id="haskell">Haskell</p>
</div>

動態建立一個節點而後添加到DOM樹中,能夠實現不少功能。舉個例子,下面的代碼動態建立了一個<style>節點,而後把它添加到<head>節點的末尾,這樣就動態地給文檔添加了新的CSS定義:

var d = document.createElement('style');
d.setAttribute('type', 'text/css');
d.innerHTML = 'p { color: red }';
document.getElementsByTagName('head')[0].appendChild(d);

能夠在Chrome的控制檯執行上述代碼,觀察頁面樣式的變化。

insertBefore
若是咱們要把子節點插入到指定的位置怎麼辦?可使用parentElement.insertBefore(newElement, referenceElement);,子節點會插入到referenceElement以前。

仍是以上面的HTML爲例,假定咱們要把Haskell插入到Python以前:

<!-- HTML結構 -->
<div id="list"> 
  <p id="java">Java</p> 
  <p id="python">Python</p> 
  <p id="scheme">Scheme</p>
</div>

能夠這麼寫:

var list = document.getElementById('list'), 
      ref = document.getElementById('python'), 
      haskell = document.createElement('p');
haskell.id = 'haskell';
haskell.innerText = 'Haskell';
list.insertBefore(haskell, ref);

新的HTML結構以下:

<!-- HTML結構 -->
<div id="list"> 
  <p id="java">Java</p> 
  <p id="haskell">Haskell</p> 
  <p id="python">Python</p> 
  <p id="scheme">Scheme</p>
</div>

可見,使用insertBefore重點是要拿到一個「參考子節點」的引用。不少時候,須要循環一個父節點的全部子節點,能夠經過迭代children屬性實現:

var i, c, list = document.getElementById('list');
for (i = 0; i < list.children.length; i++) { 
  c = list.children[i]; // 拿到第i個子節點
}

刪除DOM
刪除一個DOM節點就比插入要容易得多。要刪除一個節點,首先要得到該節點自己以及它的父節點,而後,調用父節點的removeChild
把本身刪掉:

// 拿到待刪除節點:
var self = document.getElementById('to-be-removed');

// 拿到父節點:
var parent = self.parentElement;

// 刪除:
var removed = parent.removeChild(self);
removed === self; // true

注意到刪除後的節點雖然不在文檔樹中了,但其實它還在內存中,能夠隨時再次被添加到別的位置。

當你遍歷一個父節點的子節點並進行刪除操做時,要注意,children
屬性是一個只讀屬性,而且它在子節點變化時會實時更新。

例如,對於以下HTML結構:

<div id="parent"> 
  <p>First</p> 
  <p>Second</p>
</div>

當咱們用以下代碼刪除子節點時:

var parent = document.getElementById('parent');
parent.removeChild(parent.children[0]);
parent.removeChild(parent.children[1]); // <-- 瀏覽器報錯

瀏覽器報錯:parent.children[1]不是一個有效的節點。緣由就在於,當<p>First</p>節點被刪除後,parent.children的節點數量已經從2變爲了1,索引[1]已經不存在了。

所以,刪除多個節點時,要注意children屬性時刻都在變化。

操做表單
用JavaScript操做表單和操做DOM是相似的,由於表單自己也是DOM樹。

不過表單的輸入框、下拉框等能夠接收用戶輸入,因此用JavaScript來操做表單,能夠得到用戶輸入的內容,或者對一個輸入框設置新的內容。

HTML表單的輸入控件主要有如下幾種:

文本框,對應的<input type="text">,用於輸入文本;

口令框,對應的<input type="password">,用於輸入口令;

單選框,對應的<input type="radio">,用於選擇一項;

複選框,對應的<input type="checkbox">,用於選擇多項;

下拉框,對應的<select>,用於選擇一項;

隱藏文本,對應的<input type="hidden">,用戶不可見,但表單提交時會把隱藏文本發送到服務器。

獲取值
若是咱們得到了一個<input>節點的引用,就能夠直接調用value
得到對應的用戶輸入值:

// <input type="text" id="email">
var input = document.getElementById('email');
input.value; // '用戶輸入的值'

這種方式能夠應用於text、password、hidden以及select。可是,對於單選框和複選框,value屬性返回的永遠是HTML預設的值,而咱們須要得到的實際是用戶是否「勾上了」選項,因此應該用checked判斷:

// <label><input type="radio" name="weekday" id="monday" value="1"> Monday</label>
// <label><input type="radio" name="weekday" id="tuesday" value="2"> Tuesday</label>
var mon = document.getElementById('monday');
var tue = document.getElementById('tuesday');
mon.value; // '1'
tue.value; // '2'
mon.checked; // true或者false
tue.checked; // true或者false

設置值
設置值和獲取值相似,對於text、password、hidden以及select,直接設置value就能夠:

// <input type="text" id="email">
var input = document.getElementById('email');
input.value = 'test@example.com'; // 文本框的內容已更新

對於單選框和複選框,設置checked爲true或false便可。

HTML5控件
HTML5新增了大量標準控件,經常使用的包括date、datetime、datetime-local、color等,它們都使用<input>標籤:

<input type="date" value="2015-07-01">

<input type="datetime-local" value="2015-07-01T02:03:04">

<input type="color" value="#ff0000">

不支持HTML5的瀏覽器沒法識別新的控件,會把它們當作type="text"來顯示。支持HTML5的瀏覽器將得到格式化的字符串。例如,type="date"類型的input的value將保證是一個有效的YYYY-MM-DD格式的日期,或者空字符串。

提交表單
最後,JavaScript能夠以兩種方式來處理表單的提交(AJAX方式在後面章節介紹)。

方式一是經過<form>元素的submit()方法提交一個表單,例如,響應一個<button>的click事件,在JavaScript代碼中提交表單:

<!-- HTML -->
<form id="test-form">
  <input type="text" name="test"> 
  <button type="button" onclick="doSubmitForm()">Submit</button>
</form>

<script>
  function doSubmitForm() { 
    var form = document.getElementById('test-form'); 
    // 能夠在此修改form的input... 
    // 提交form: form.submit();
  }
</script>

這種方式的缺點是擾亂了瀏覽器對form的正常提交。瀏覽器默認點擊<button type="submit">時提交表單,或者用戶在最後一個輸入框按回車鍵。所以,第二種方式是響應<form>自己的onsubmit
事件,在提交form時做修改:

<!-- HTML -->
<form id="test-form" onsubmit="return checkForm()"> 
  <input type="text" name="test"> 
    <button type="submit">Submit</button>
</form>
<script>
  function checkForm() { 
    var form = document.getElementById('test-form'); 
      // 能夠在此修改form的input... 
      // 繼續下一步: return true;
  }
</script>

注意要return true來告訴瀏覽器繼續提交,若是return false
,瀏覽器將不會繼續提交form,這種狀況一般對應用戶輸入有誤,提示用戶錯誤信息後終止提交form。在檢查和修改<input>時,要充分利用<input type="hidden">來傳遞數據。

例如,不少登陸表單但願用戶輸入用戶名和口令,可是,安全考慮,提交表單時不傳輸明文口令,而是口令的MD5。普通JavaScript開發人員會直接修改<input>:

<!-- HTML -->
<form id="login-form" method="post" onsubmit="return checkForm()"> 
  <input type="text" id="username" name="username">   
  <input type="password" id="password" name="password"> 
  <button type="submit">Submit</button>
</form>

<script>
  function checkForm() { 
    var pwd = document.getElementById('password'); 
    // 把用戶輸入的明文變爲MD5: 
    pwd.value = toMD5(pwd.value); 
    // 繼續下一步: 
    return true;
  }
</script>
這個作法看上去沒啥問題,但用戶輸入了口令提交時,口令框的顯示會忽然從幾個*變成32個*(由於MD5有32個字符)。

要想不改變用戶的輸入,能夠利用<input type="hidden">實現:

<!-- HTML -->
<form id="login-form" method="post" onsubmit="return checkForm()"> 
  <input type="text" id="username" name="username"> 
  <input type="password" id="input-password"> 
  <input type="hidden" id="md5-password" name="password"> 
  <button type="submit">Submit</button>
</form>

<script>
  function checkForm() { 
    var input_pwd = document.getElementById('input-password'); 
    var md5_pwd = document.getElementById('md5-password'); 
    // 把用戶輸入的明文變爲MD5: 
    md5_pwd.value = toMD5(input_pwd.value); 
    // 繼續下一步: return true;
  }
</script>

注意到id爲md5-password的<input>標記了name="password",而用戶輸入的id爲input-password的<input>沒有name屬性。沒有name屬性的<input>
的數據不會被提交。

操做文件
在HTML表單中,能夠上傳文件的惟一控件就是<input type="file">。

注意:當一個表單包含<input type="file">時,表單的enctype必須指定爲multipart/form-data,method
必須指定爲post,瀏覽器才能正確編碼並以multipart/form-data格式發送表單的數據。

出於安全考慮,瀏覽器只容許用戶點擊<input type="file">來選擇本地文件,用JavaScript對<input type="file">的value賦值是沒有任何效果的。當用戶選擇了上傳某個文件後,JavaScript也沒法得到該文件的真實路徑:

一般,上傳的文件都由後臺服務器處理,JavaScript能夠在提交表單時對文件擴展名作檢查,以便防止用戶上傳無效格式的文件:

var f = document.getElementById('test-file-upload');
var filename = f.value; // 'C:\fakepath\test.png'
if (!filename || !(filename.endsWith('.jpg') || filename.endsWith('.png') || filename.endsWith('.gif'))) {   
  alert('Can only upload image file.'); 
  return false;
}
File API

因爲JavaScript對用戶上傳的文件操做很是有限,尤爲是沒法讀取文件內容,使得不少須要操做文件的網頁不得不用Flash這樣的第三方插件來實現。

隨着HTML5的普及,新增的File API容許JavaScript讀取文件內容,得到更多的文件信息。

HTML5的File API提供了File和FileReader兩個主要對象,能夠得到文件信息並讀取文件。

下面的例子演示瞭如何讀取用戶選取的圖片文件,並在一個<div>中預覽圖像:

var fileInput = document.getElementById('test-image-file'), 
      info = document.getElementById('test-file-info'), 
      preview = document.getElementById('test-image-preview');
// 監聽change事件:
fileInput.addEventListener('change', function () { 
  // 清除背景圖片: 
  preview.style.backgroundImage = ''; 
  // 檢查文件是否選擇: 
  if (!fileInput.value) { 
    info.innerHTML = '沒有選擇文件'; 
    return; 
  } 
  // 獲取File引用: 
  var file = fileInput.files[0]; 
  // 獲取File信息: 
  info.innerHTML = '文件: ' + file.name + '<br>' + 
                              '大小: ' + file.size + '<br>' + 
                              '修改: ' + file.lastModifiedDate; 
  if (file.type !== 'image/jpeg' && 
      file.type !== 'image/png' && 
      file.type !== 'image/gif') { 
        alert('不是有效的圖片文件!'); 
        return; 
  } 
  // 讀取文件:
  var reader = new FileReader(); 
  reader.onload = function(e) { 
    var data = e.target.result; 
    // 'data:image/jpeg;base64,/9j/4AAQSk...(base64編碼)...'       
    preview.style.backgroundImage = 'url(' + data + ')'; 
  }; 
  // 以DataURL的形式讀取文件: 
  reader.readAsDataURL(file);
});

上面的代碼演示瞭如何經過HTML5的File API讀取文件內容。以DataURL的形式讀取到的文件是一個字符串,相似於data:image/jpeg;base64,/9j/4AAQSk...(base64編碼)...,經常使用於設置圖像。若是須要服務器端處理,把字符串base64,後面的字符發送給服務器並用Base64解碼就能夠獲得原始文件的二進制內容。

回調
上面的代碼還演示了JavaScript的一個重要的特性就是單線程執行模式。在JavaScript中,瀏覽器的JavaScript執行引擎在執行JavaScript代碼時,老是以單線程模式執行,也就是說,任什麼時候候,JavaScript代碼都不可能同時有多於1個線程在執行。

你可能會問,單線程模式執行的JavaScript,如何處理多任務?
在JavaScript中,執行多任務實際上都是異步調用,好比上面的代碼:

reader.readAsDataURL(file);
就會發起一個異步操做來讀取文件內容。由於是異步操做,因此咱們在JavaScript代碼中就不知道何時操做結束,所以須要先設置一個回調函數:

reader.onload = function(e) {
// 當文件讀取完成後,自動調用此函數:
};
當文件讀取完成後,JavaScript引擎將自動調用咱們設置的回調函數。執行回調函數時,文件已經讀取完畢,因此咱們能夠在回調函數內部安全地得到文件內容。

AJAX
AJAX不是JavaScript的規範,它只是一個哥們「發明」的縮寫:Asynchronous JavaScript and XML,意思就是用JavaScript執行異步網絡請求。

若是仔細觀察一個Form的提交,你就會發現,一旦用戶點擊「Submit」按鈕,表單開始提交,瀏覽器就會刷新頁面,而後在新頁面裏告訴你操做是成功了仍是失敗了。若是不幸因爲網絡太慢或者其餘緣由,就會獲得一個404頁面。

這就是Web的運做原理:一次HTTP請求對應一個頁面。

若是要讓用戶留在當前頁面中,同時發出新的HTTP請求,就必須用JavaScript發送這個新請求,接收到數據後,再用JavaScript更新頁面,這樣一來,用戶就感受本身仍然停留在當前頁面,可是數據卻能夠不斷地更新。

最先大規模使用AJAX的就是Gmail,Gmail的頁面在首次加載後,剩下的全部數據都依賴於AJAX來更新。

用JavaScript寫一個完整的AJAX代碼並不複雜,可是須要注意:AJAX請求是異步執行的,也就是說,要經過回調函數得到響應。

在現代瀏覽器上寫AJAX主要依靠XMLHttpRequest對象:

'use strict';
function success(text) {
    var textarea = document.getElementById('test-response-text');
    textarea.value = text;
}

function fail(code) {
    var textarea = document.getElementById('test-response-text');
    textarea.value = 'Error code: ' + code;
}

var request = new XMLHttpRequest(); // 新建XMLHttpRequest對象

request.onreadystatechange = function () { // 狀態發生變化時,函數被回調
    if (request.readyState === 4) { // 成功完成
        // 判斷響應結果:
        if (request.status === 200) {
            // 成功,經過responseText拿到響應的文本:
            return success(request.responseText);
        } else {
            // 失敗,根據響應碼判斷失敗緣由:
            return fail(request.status);
        }
    } else {
        // HTTP請求還在繼續...
    }
}

// 發送請求:
request.open('GET', '/api/categories');
request.send();

alert('請求已發送,請等待響應...');
對於低版本的IE,須要換一個ActiveXObject對象:

'use strict';
function success(text) {
    var textarea = document.getElementById('test-ie-response-text');
    textarea.value = text;
}

function fail(code) {
    var textarea = document.getElementById('test-ie-response-text');
    textarea.value = 'Error code: ' + code;
}

// 新建Microsoft.XMLHTTP對象
var request = new ActiveXObject('Microsoft.XMLHTTP'); 

request.onreadystatechange = function () { // 狀態發生變化時,函數被回調
    if (request.readyState === 4) { // 成功完成
        // 判斷響應結果:
        if (request.status === 200) {
            // 成功,經過responseText拿到響應的文本:
            return success(request.responseText);
        } else {
            // 失敗,根據響應碼判斷失敗緣由:
            return fail(request.status);
        }
    } else {
        // HTTP請求還在繼續...
    }
}

// 發送請求:
request.open('GET', '/api/categories');
request.send();

alert('請求已發送,請等待響應...');
若是你想把標準寫法和IE寫法混在一塊兒,能夠這麼寫:

var request;
if (window.XMLHttpRequest) { 
  request = new XMLHttpRequest();
} else { 
  request = new ActiveXObject('Microsoft.XMLHTTP');
}

注意,不要根據瀏覽器的navigator.userAgent來檢測瀏覽器是否支持某個JavaScript特性,一是由於這個字符串自己能夠僞造,二是經過IE版本判斷JavaScript特性將很是複雜。

當建立了XMLHttpRequest對象後,要先設置onreadystatechange的回調函數。在回調函數中,一般咱們只需經過readyState === 4判斷請求是否完成,若是已完成,再根據status === 200判斷是不是一個成功的響應。

XMLHttpRequest對象的open()方法有3個參數,第一個參數指定是GET仍是POST,第二個參數指定URL地址,第三個參數指定是否使用異步,默認是true,因此不用寫。

注意,千萬不要把第三個參數指定爲false,不然瀏覽器將中止響應,直到AJAX請求完成。若是這個請求耗時10秒,那麼10秒內你會發現瀏覽器處於「假死」狀態。

最後調用send()方法才真正發送請求。GET請求不須要參數,POST請求須要把body部分以字符串或者FormData對象傳進去。

安全限制
上面代碼的URL使用的是相對路徑。若是你把它改成'http://www.sina.com.cn/',再運行,確定報錯。在Chrome的控制檯裏,還能夠看到錯誤信息。

這是由於瀏覽器的同源策略致使的。默認狀況下,JavaScript在發送AJAX請求時,URL的域名必須和當前頁面徹底一致。徹底一致的意思是,域名要相同(www.example.com和example.com不一樣),協議要相同(http和https不一樣),端口號要相同(默認是:80
端口,它和:8080就不一樣)。有的瀏覽器口子鬆一點,容許端口不一樣,大多數瀏覽器都會嚴格遵照這個限制。

那是否是用JavaScript沒法請求外域(就是其餘網站)的URL了呢?方法仍是有的,大概有這麼幾種:

一是經過Flash插件發送HTTP請求,這種方式能夠繞過瀏覽器的安全限制,但必須安裝Flash,而且跟Flash交互。不過Flash用起來麻煩,並且如今用得也愈來愈少了。

二是經過在同源域名下架設一個代理服務器來轉發,JavaScript負責把請求發送到代理服務器:'/proxy?url=http://www.sina.com.cn'

代理服務器再把結果返回,這樣就遵照了瀏覽器的同源策略。這種方式麻煩之處在於須要服務器端額外作開發。

第三種方式稱爲JSONP,它有個限制,只能用GET請求,而且要求返回JavaScript。這種方式跨域其實是利用了瀏覽器容許跨域引用JavaScript資源:

<html>
  <head> 
  <script src="http://example.com/abc.js"></script> 
    ...
  </head>
  <body>
    ...
  </body>
</html>

JSONP一般以函數調用的形式返回,例如,返回JavaScript內容以下:

foo('data');
這樣一來,咱們若是在頁面中先準備好foo()函數,而後給頁面動態加一個<script>節點,至關於動態讀取外域的JavaScript資源,最後就等着接收回調了。

以163的股票查詢URL爲例,對於URL:http://api.money.126.net/data...,你將獲得以下返回:

refreshPrice({"0000001":{"code": "0000001", ... });
所以咱們須要首先在頁面中準備好回調函數:

function refreshPrice(data) { 
  var p = document.getElementById('test-jsonp'); 
  p.innerHTML = '當前價格:' + data['0000001'].name +': ' + 
  data['0000001'].price + ';' + data['1399001'].name + ': ' + 
  data['1399001'].price;
}
最後用getPrice()函數觸發:

function getPrice() { 
  var js = document.createElement('script'), 
       head = document.getElementsByTagName('head')[0]; 
  js.src = 'http://api.money.126.net/data/feed/0000001,1399001?callback=refreshPrice'; 
  head.appendChild(js);
}

就完成了跨域加載數據。

CORS
若是瀏覽器支持HTML5,那麼就能夠一勞永逸地使用新的跨域策略:CORS了。

CORS全稱Cross-Origin Resource Sharing,是HTML5規範定義的如何跨域訪問資源。

瞭解CORS前,咱們先搞明白概念:

Origin表示本域,也就是瀏覽器當前頁面的域。當JavaScript向外域(如sina.com)發起請求後,瀏覽器收到響應後,首先檢查Access-Control-Allow-Origin是否包含本域,若是是,則這次跨域請求成功,若是不是,則請求失敗,JavaScript將沒法獲取到響應的任何數據。

用一個圖來表示就是:

js-cors
假設本域是my.com,外域是sina.com,只要響應頭Access-Control-Allow-Origin爲http://my.com,或者是*,本次請求就能夠成功。

可見,跨域可否成功,取決於對方服務器是否願意給你設置一個正確的Access-Control-Allow-Origin,決定權始終在對方手中。

上面這種跨域請求,稱之爲「簡單請求」。簡單請求包括GET、HEAD和POST(POST的Content-Type類型僅限application/x-www-form-urlencoded、multipart/form-data和text/plain),而且不能出現任何自定義頭(例如,X-Custom: 12345),一般能知足90%的需求。

不管你是否須要用JavaScript經過CORS跨域請求資源,你都要了解CORS的原理。最新的瀏覽器全面支持HTML5。在引用外域資源時,除了JavaScript和CSS外,都要驗證CORS。例如,當你引用了某個第三方CDN上的字體文件時:

/* CSS */
@font-face { 
  font-family: 'FontAwesome'; 
  src: url('http://cdn.com/fonts/fontawesome.ttf')  
  format('truetype');
}

若是該CDN服務商未正確設置Access-Control-Allow-Origin,那麼瀏覽器沒法加載字體資源。

對於PUT、DELETE以及其餘類型如application/json的POST請求,在發送AJAX請求以前,瀏覽器會先發送一個OPTIONS請求(稱爲preflighted請求)到這個URL上,詢問目標服務器是否接受:

OPTIONS /path/to/resource HTTP/1.1
Host: bar.com
Origin: http://my.com
Access-Control-Request-Method: POST
服務器必須響應並明確指出容許的Method:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://my.com
Access-Control-Allow-Methods: POST, GET, PUT, OPTIONS
Access-Control-Max-Age: 86400
瀏覽器確認服務器響應的Access-Control-Allow-Methods
頭確實包含將要發送的AJAX請求的Method,纔會繼續發送AJAX,不然,拋出一個錯誤。

因爲以POST、PUT方式傳送JSON格式的數據在REST中很常見,因此要跨域正確處理POST和PUT請求,服務器端必須正確響應OPTIONS請求。

須要深刻了解CORS的童鞋請移步W3C文檔。

Promise
在JavaScript的世界中,全部代碼都是單線程執行的。

因爲這個「缺陷」,致使JavaScript的全部網絡操做,瀏覽器事件,都必須是異步執行。異步執行能夠用回調函數實現:

function callback() { 
  console.log('Done');
}
console.log('before setTimeout()');
setTimeout(callback, 1000); // 1秒鐘後調用callback函數
console.log('after setTimeout()');

觀察上述代碼執行,在Chrome的控制檯輸出能夠看到:

before setTimeout()
after setTimeout()
(等待1秒後)
Done
可見,異步操做會在未來的某個時間點觸發一個函數調用。

AJAX就是典型的異步操做。以上一節的代碼爲例:

request.onreadystatechange = function () { 
  if (request.readyState === 4) { 
    if (request.status === 200) { 
      return success(request.responseText); 
    } else { 
      return fail(request.status); 
    } 
  }
}

把回調函數success(request.responseText)和fail(request.status)寫到一個AJAX操做裏很正常,可是很差看,並且不利於代碼複用。

有沒有更好的寫法?好比寫成這樣:

var ajax = ajaxGet('http://...');
ajax.ifSuccess(success) 
.ifFail(fail);

這種鏈式寫法的好處在於,先統一執行AJAX邏輯,不關心如何處理結果,而後,根據結果是成功仍是失敗,在未來的某個時候調用success函數或fail函數。

古人云:「君子一言既出;駟馬難追」,這種「承諾未來會執行」的對象在JavaScript中稱爲Promise對象。

Promise有各類開源實現,在ES6中被統一規範,由瀏覽器直接支持。

咱們先看一個最簡單的Promise例子:生成一個0-2之間的隨機數,若是小於1,則等待一段時間後返回成功,不然返回失敗:

function test(resolve, reject) { 
  var timeOut = Math.random() * 2; 
  log('set timeout to: ' + timeOut + ' seconds.'); 
  setTimeout(function () { 
    if (timeOut < 1) { 
      log('call resolve()...'); 
      resolve('200 OK'); 
    } else { 
      log('call reject()...'); 
      reject('timeout in ' + timeOut + ' seconds.'); 
    } 
  }, timeOut * 1000);
}

這個test()函數有兩個參數,這兩個參數都是函數,若是執行成功,咱們將調用resolve('200 OK'),若是執行失敗,咱們將調用reject('timeout in ' + timeOut + ' seconds.')。能夠看出,test()函數只關心自身的邏輯,並不關心具體的resolve和reject將如何處理結果。

有了執行函數,咱們就能夠用一個Promise對象來執行它,並在未來某個時刻得到成功或失敗的結果:

var p1 = new Promise(test);
var p2 = p1.then(function (result) { 
  console.log('成功:' + result);
});
var p3 = p2.catch(function (reason) { 
  console.log('失敗:' + reason);
});
變量p1是一個Promise對象,它負責執行test函數。因爲test
函數在內部是異步執行的,當test函數執行成功時,咱們告訴Promise對象:

// 若是成功,執行這個函數:
p1.then(function (result) { 
  console.log('成功:' + result);
});

當test函數執行失敗時,咱們告訴Promise對象:

p2.catch(function (reason) { 
  console.log('失敗:' + reason);
});
Promise對象能夠串聯起來,因此上述代碼能夠簡化爲:

new Promise(test).then(function (result) { 
  console.log('成功:' + result);
}).catch(function (reason) { 
  console.log('失敗:' + reason);
});

實際測試一下,看看Promise是如何異步執行的:

'use strict';
// 清除log:
var logging = document.getElementById('test-promise-log');
while (logging.children.length > 1) { 
  logging.removeChild(logging.children[logging.children.length - 1]);
}

// 輸出log到頁面:
function log(s) { 
  var p = document.createElement('p'); 
  p.innerHTML = s; 
  logging.appendChild(p);
}

new Promise(function (resolve, reject) {
    log('start new Promise...');
    var timeOut = Math.random() * 2;
    log('set timeout to: ' + timeOut + ' seconds.');
    setTimeout(function () {
        if (timeOut < 1) {
            log('call resolve()...');
            resolve('200 OK');
        }
        else {
            log('call reject()...');
            reject('timeout in ' + timeOut + ' seconds.');
        }
    }, timeOut * 1000);
}).then(function (r) {
    log('Done: ' + r);
}).catch(function (reason) {
    log('Failed: ' + reason);
});

可見Promise最大的好處是在異步執行的流程中,把執行代碼和處理結果的代碼清晰地分離了:

promise
Promise還能夠作更多的事情,好比,有若干個異步任務,須要先作任務1,若是成功後再作任務2,任何任務失敗則再也不繼續並執行錯誤處理函數。

要串行執行這樣的異步任務,不用Promise須要寫一層一層的嵌套代碼。有了Promise,咱們只須要簡單地寫:

job1.then(job2).then(job3).catch(handleError);
其中,job一、job2和job3都是Promise對象。

下面的例子演示瞭如何串行執行一系列須要異步計算得到結果的任務:

'

use strict';

var logging = document.getElementById('test-promise2-log');
while (logging.children.length > 1) {
    logging.removeChild(logging.children[logging.children.length - 1]);
}

function log(s) {
    var p = document.createElement('p');
    p.innerHTML = s;
    logging.appendChild(p);
}

// 0.5秒後返回input*input的計算結果:
function multiply(input) {
    return new Promise(function (resolve, reject) {
        log('calculating ' + input + ' x ' + input + '...');
        setTimeout(resolve, 500, input * input);
    });
}

// 0.5秒後返回input+input的計算結果:
function add(input) {
    return new Promise(function (resolve, reject) {
        log('calculating ' + input + ' + ' + input + '...');
        setTimeout(resolve, 500, input + input);
    });
}

var p = new Promise(function (resolve, reject) {
    log('start new Promise...');
    resolve(123);
});

p.then(multiply)
 .then(add)
 .then(multiply)
 .then(add)
 .then(function (result) {
    log('Got value: ' + result);
});
setTimeout能夠當作一個模擬網絡等異步執行的函數。如今,咱們把上一節的AJAX異步執行函數轉換爲Promise對象,看看用Promise如何簡化異步處理:

'use strict';

// ajax函數將返回Promise對象:
function ajax(method, url, data) {
    var request = new XMLHttpRequest();
    return new Promise(function (resolve, reject) {
        request.onreadystatechange = function () {
            if (request.readyState === 4) {
                if (request.status === 200) {
                    resolve(request.responseText);
                } else {
                    reject(request.status);
                }
            }
        };
        request.open(method, url);
        request.send(data);
    });
}

var log = document.getElementById('test-promise-ajax-result');
var p = ajax('GET', '/api/categories');
p.then(function (text) { // 若是AJAX成功,得到響應內容
    log.innerText = text;
}).catch(function (status) { // 若是AJAX失敗,得到響應代碼
    log.innerText = 'ERROR: ' + status;
});

除了串行執行若干異步任務外,Promise還能夠並行執行異步任務。

試想一個頁面聊天系統,咱們須要從兩個不一樣的URL分別得到用戶的我的信息和好友列表,這兩個任務是能夠並行執行的,用
Promise.all()實現以下:

var p1 = new Promise(function (resolve, reject) { 
  setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) { 
setTimeout(resolve, 600, 'P2');
});
// 同時執行p1和p2,並在它們都完成後執行then:
Promise.all([p1, p2]).then(function (results) { 
  console.log(results); // 得到一個Array: ['P1', 'P2']
});

有些時候,多個異步任務是爲了容錯。好比,同時向兩個URL讀取用戶的我的信息,只須要得到先返回的結果便可。這種狀況下,用Promise.race()實現:

var p1 = new Promise(function (resolve, reject) { 
  setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) { 
  setTimeout(resolve, 600, 'P2');
});
Promise.race([p1, p2]).then(function (result) { 
  console.log(result); // 'P1'
});

因爲p1執行較快,Promise的then()將得到結果'P1'。p2仍在繼續執行,但執行結果將被丟棄。

若是咱們組合使用Promise,就能夠把不少異步任務以並行和串行的方式組合起來執行。
更多資源上:http://www.quzhuanpan.com,本人創建了個Q羣,歡迎你們加入討論js,css,python爬蟲等技術(QQ羣:512245829)

相關文章
相關標籤/搜索