前端開發基礎-JavaScript

發表於2016-03-22

這是很久很久之前想寫的東西,拖了五六個月,沒有動筆,現今補齊,內容有些多,對初學者有用,錯誤之處,望指出。

理解作用域

理解作用域鏈是Js程式設計中一個必須要具備的,作用域決定了變數和函式有權力訪問哪些資料。在Web瀏覽器中,全域性執行環境是window物件,這也意味著所有的全域性變數或者方法都是window物件的屬性或方法。當一個函式在被呼叫的時候都會建立自己的執行環境,而這個函式中所寫的程式碼就開始進入這個函式的執行環境,於是由變數物件構建起了一個作用域鏈。

在這個例子中全域性環境中包含了兩個物件(全域性環境的變數物件不算),window.wow和window.message,而這個message函式中又包含了兩個物件,它自己的變數物件(其中定義了arguments物件)和全域性環境的變數物件。當這個函式開始執行時,message自己的變數物件中定義了_wow,而它的全域性環境的變數物件有wow,假設在message中alert一下wow,實際上是message中包含的全域性環境的變數物件.wow,於是可以訪問。

如果執行message函式alert一下wow,它的作用域是這樣開始搜尋的,先搜尋message自己的變數物件中是否存在wow,如果有就訪問並且立馬停止搜尋,如果沒有則繼續往上訪問它,有wow,則訪問並且立馬停止搜尋,以此類推一直搜尋到全域性環境上的變數物件,如果這裡都沒,恭喜你,這裡要拋錯了。

在這個例子中包含有三個執行環境,全域性環境,message的環境,a的環境。從這裡可以看出message自身包含兩個物件,自己的變數物件和全域性環境中的變數物件,而函式a則包含了三個,自身的變數物件,message的變數物件和全域性變數物件。

當開始執行這個函式時,在函式a中可以訪問到變數g,那是因為函式a包含了message的變數物件,於是在自身沒有開始搜尋上一級的變數物件時發現了,於是可以訪問。那麼訪問c的原理也是如此,當自身和上一級的message的變數物件都沒有,但是全域性變數物件中存在,於是訪問成功。

瞭解這個作用域,對於Js程式設計是至關重要的,不然可能會出現,明明想要的預期結果是123,但是變成了456,為什麼?那就是因為一級一級的搜尋,可能會存在覆蓋,或者搜尋到別的地方就立即停止搜尋了。

理解引用型別

引用型別雖然看起來和類很相似,但是它們卻是不同的概念,引用型別的值,也就是物件是引用型別的一個例項。在Js中引用型別主要有Object,Array,Date,正則,Function等。

Object和Function在後面詳細複述。

Array

在Js中陣列可以儲存任意的資料,而且它的大小是可以動態調整的類似於OC中的NSMutableArray。建立陣列可以使用建構函式的方式也可以使用字面量的形式,另外可以使用concat從一個陣列中複製一個副本出來。陣列本身提供了很多方法讓開發者使用來運算元組。

  • length 陣列的長度
  • toString 可以返回一個以,拼接的字串,相當於是呼叫了下join(‘,’)
  • join 可以用一個分割符來拼接成一個字串
  • push 新增一個資料到陣列的末端
  • pop 刪除陣列中的最後一項,有返回值
  • shift 刪除陣列的第一項,有返回值
  • unshift 新增一個資料到陣列的首端
  • reverse 倒序
  • sort 可以傳入一個排序的函式
  • slice 可以基於當前陣列返回一個新的陣列,接收兩個引數,返回項的起始位置和結束位置
  • splice 可以傳入N個引數,第一個參數列示要刪除,插入或則替換的位置,第二個參數列示要刪除的項數,第三個到第N個表示要插入或則替換的資料

Date

時間物件也是使用非常多的玩意,它是使用GMT時間來描述,而且時間物件是可以直接比對大小的。

常用的方法

  • getTime 獲取時間物件的毫秒數
  • setTime 設定時間物件的毫秒數,會改變日期
  • getFullYear 獲取時間物件的年(2015)
  • getMonth 獲取時間物件的月(需要加1)
  • getDay 獲取日期的星期幾(0-6)星期天到星期六
  • getDate 獲取日期的天數
  • getHours 獲取當前日期的小時
  • getMinutes 獲取當前日期的分鐘數
  • getSeconds 獲取當然日期的秒數

上面看起來都是獲取,當然也有設定,只是相應的get置換成set即可。

正規表示式

在Js里正規表示式是用RegExp型別來支援的,關於正則可以看看之前寫的一篇文章,用python來描述的如何讀懂正則。

Js也支援三種模式,gim,表示全域性,不區分大小寫,多行。

一般來說很少有人這麼使用var xxx = new RegExp(),而是用字面量的方式,比如var xx = /[bc]/gi;像用的比較多的方法有exec用於捕獲包含第一個匹配項的陣列,沒有則返回null。test,用於判斷,如果匹配返回true,不匹配返回false。

處理字串

在Js中還有一種叫做包裝型別的玩意,正因為此所以處理一些基本資料型別,比如字串時,有很多方法可以使用。

  • concat 可以將一個或者多個字串拼接起來,返回一個新的字串
  • slice 接收兩個引數,起始位置和結束位置,返回一個新的字串
  • substr和substring和slice一樣,唯一的不同是substr第二個引數是返回字串的個數
  • indexOf 從頭開始查詢字串,存在會返回它所在的位置,沒有返回-1
  • lastIndexOf 從最後開始查詢字串
  • toUpperCase 轉大寫
  • toLowerCase 轉小寫
  • match 正規表示式使用跟exec一樣
  • search 正規表示式使用,查詢到返回一個位置,沒有返回-1
  • replace 替換,第一個引數可以是正規表示式也可以是字串,第二個引數是要替換的字串
  • localeCompare比較字串,如果字串相等返回0,如果字串的字母排在引數字串之前,返回負數,如果是之後,返回正數。

函式

說起來Js的核心是什麼?那就是函式了。對於函式主要是理解它的幾個概念。

  • 它可以當值來傳遞,沒有重栽。
  • 宣告的時候,比如function a(){} var a = function(){} 執行時會有區別
  • 函式內部的引數arguments包含了傳入的所有引數
  • this,表示在這個函式內的作用域,以及prototype

理解匿名函式和閉包

匿名函式又叫拉姆達函式,主要是在把函式當值傳遞的時候用,或者是把函式當返回值,比如:

其實第二種方式跟閉包的意義一樣了,所謂的閉包書面的解釋是可以訪問另一個函式作用域內變數的函式,稍微改寫一下可能會更明顯。

從這裡可以看出來return的函式可以訪問到name,而外部卻不行,這個返回值的函式就可以理解為閉包。理解閉包還可以看一個經典的求值的例子。

從這個例子上來看,我們想得到的結果是10次迴圈a[i]儲存著一個閉包,然後alert出從0到10,但是結果很出人意料,全部是10,為什麼?哪裡理解的不對呢?a[i]明明是內部函式,然後讓它訪問另外一個函式作用域內的變數i。

個人覺得可以這樣去分析問題,在客戶端執行Js時有一個全域性執行環境,指向的是window物件。而所謂的物件也就是引用型別,實際上在後臺執行環境中,它就是一個指標。

回到Js當程式碼在執行的時候,會建立變數物件並且構建一個作用域鏈,而這個物件儲存著當前函式可以訪問的物件。

上述的i和a[0]裡的i是同一個i,那麼結果就是10。

進一步處理

接著按上面的節奏來分析

什麼是傳參?按值傳遞,相當於是在那個立即執行的函式中建立了一個新的地址和空間,雖然值是一樣的,但是每一個k又是不同的,所以得到的結果正好滿足了我們的預期。

本來正常情況下save_i執行完畢後就要銷燬,但是內部的閉包被包含在這個作用域內了,所以save_i沒法銷燬,從這裡可以看的出來閉包會帶來記憶體的問題,因為用完之後沒法銷燬,如果不注意的話。

那麼用完之後只能設定為null來解除引用,等著自動銷燬把記憶體回收。

Object

JavaScript的所有物件都衍生於Object物件,所有物件都繼承了Object.prototype上的方法和屬性,雖然它們可能會被覆蓋,熟悉它對於程式設計能起到很大的作用,也能比較深刻的瞭解JavaScript這門語言。

Object

建立一個物件可以使用new,也可以使用快速建立的方式:

_object物件中就可以使用Object.prototype中所有的方法和屬性,雖然看起來它是空的。說到這裡在程式設計中常常有一個非常有用的需求,如何判斷一個物件是空物件。

這是zepto中的判斷一個物件是否是空物件,常常使用:

也順便看了下jQuery原理是一模一樣的:

使用in操作符來實現,它不會遍歷到父原型鏈。

constructor返回一個指向建立了該物件的函式引用,這個東西主要是可以用來識別(類)到底是指向哪裡的。

defineProperty直接在一個物件上定義一個新屬性,非常適合用於動態構建,傳入三個引數[動態新增物件的目標物件,需要定義或被修改的屬性名,需要定義的物件],在第三個引數中可以有些屬性來表示是否繼承(proto),要不要定義get,set方法,enumerable是否可列舉。

defineProperties跟上述defineProperty一樣,但是它可以新增多個。

getOwnPropertyNames返回一個由指定物件的所有屬性組成的陣列

keys返回一個陣列包括物件所有的屬性(可列舉)

keys是經常會用到的一個屬性,它只能包可列舉的,如果想獲取一個物件的所有屬性包括不列舉的,那麼使用getOwnPropertyNames。

hasOwnProperty用於判斷某個物件是否包含有自身的屬性,這個方法常常用於檢測物件中的屬性是否存在,它只檢測自身,對於繼承過來的都是false,這一點是非常重要的理解。

isPrototypeOf 用於檢測一個物件是否在另一個物件的原型鏈上,比如有兩個物件是互相互動的,常常會使用它來進行檢測。

propertyIsEnumerable這個方法也比較重要,返回一個布林值,檢測一個物件的自身屬性是否可以列舉

可列舉的理解,也就是物件的屬性可列舉,它的屬性值不可以修改,但是在Js中它有自己的定義,引擎內部看不見的該屬性的[[Enumerable]]特性為true,那麼就是可列舉的。基本上把一個普通物件可以看做是一個列舉型別,比如var color = {‘red’:1},red是可以修改的,但是red是可列舉的,但是如果是繼承過來的屬性,propertyIsEnumerable是返回false的,它還有一個特點,就是自身。

如果要定義不可列舉的屬性,那就要使用defineProperty方法了,目前不能用物件直接量或者建構函式定義出來。

深拷貝與淺拷貝

關於拷貝的問題,主要分為深拷貝和淺拷貝,但是如果從空間分配上來說JavaScript的拷貝不應該算是深拷貝,比如:

今天突然想到了這麼一個問題,在C語言中,所謂的拷貝,就是分兩種情況,一種是把指標地址拷貝給另外一個變數,雖然也開闢的了一個記憶體空間,在棧上也存在著一個地址,我對這個變數進行修改,同一個指標是會改變其值的,這種拷貝叫淺拷貝。另外一種情況,直接開闢一個新空間,把需要複製的值都複製在這個新的空間中,這種拷貝叫中深拷貝。

如果看到上述的一段Js程式碼,很多人說它是淺拷貝,假設傳入一個a物件,拷貝完成之後返回一個d,當我修改返回物件的值時並不能同時修改a物件,於是,在這裡我有一個很大的疑問,在Js中到底什麼是淺拷貝,什麼是深拷貝的問題?

這一點上感覺Js真的很奇葩,如果在開發iOS中,不可變物件copy一下,依然是不可變,所以是淺拷貝,拷貝了指標變數中儲存的地址值。如果是可變物件copy一下,到不可變,空間變化了,包括不可變mutableCopy到不可變,空間依然變化了,所以是深拷貝。但是JavaScript中對於這一點要考慮一種情況,值型別,和引用型別,這個基礎知識,我相信大家都非常清楚。數字,字串等都是值型別,object,array等都是引用型別。

從這個例子中可以看的出來,它們使用的都是=符號,而陣列a發生了變化,numb數字卻沒有發生變化。那麼從這裡,可以有一個總結,所謂了深拷貝,淺拷貝的問題,應該針對的是有多個巢狀發生的情況。不然假設是這樣的情況,還能叫淺拷貝麼?

明顯物件o中的de屬性修改並沒有影響到原始物件,一個物件中的屬性是一個字串,如果從記憶體空間的角度上來說,這裡明顯是開闢了新的空間,還能說是淺拷貝麼?那麼針對另外一種情況。

如果一個物件中的第一層屬性,不是值型別,只單層迴圈,這樣來看的話確實是一個淺拷貝,因為在Js中引用型別用=賦值,實際上是引用,這樣說的通。所以,深拷貝,還需要做一些處理,把object,array等引用型別識別出來,深層遞迴到最後一層,一個一個的拷貝。

思路是如此,這個例子只考慮了兩種情況,物件和陣列,為了驗證這樣的思路,最後的結果與預期是一樣的。

物件導向

物件導向的語言有一個非常明顯的標誌:類,通過類來建立任意多個具有相同屬性和方法的物件,可惜的是Js裡沒有這樣的概念。

但是Js有一個特性:一切皆是物件。

聰明的開發者通過這些特性進行摸索,於是迂迴發明了一些程式設計,以便更好的組織程式碼結構。

工廠模式

主要是用來解決有多個相同屬性和方法的物件的問題,可以用函式來封裝特定的介面來實現

建構函式模式

我們知道像原生的建構函式,比如Object,Array等,它們是在執行時自動出現在執行環境中的。因此,為了模仿它,這裡也可以通過一個普通的函式,並且new出一個物件,這樣就成為了自定義的建構函式,也可以為他們新增自定義的屬性和方法。

但是這樣的建構函式有一個缺陷,就是每個方法都會在每個例項上建立一次,因為每次建立都需要分配記憶體空間,但是有時候這樣的特性還是有用的,主要是要控制它們,在不使用的時候釋放記憶體。

像apple,dell是通過Computer例項化出來的不同的物件,但是它們的constructor都是指向Computer的。這裡也可以使用instanceof來對(物件)進行檢測。

在書寫上建構函式跟其他函式是沒有什麼區別的,主要的區別還是在使用上,建構函式需要使用new操作符。

其實這樣的書寫,已經跟類沒有什麼區別了,表面上來看,而建構函式我個人更傾向於一個類的某個靜態方法。

原型模式

說到原型模式就不得不提一提關於指標的問題,因為每一個函式都有一個prototype屬性,而這個屬性是一個指標,指向一個物件。

C語言描述指標,這個在iOS開發中非常重要

比如我先定義一個int型別的指標變數和一個普通的int型別資料,然後給指標變數賦值。

*是一個特殊符號用於標明它是一個指標變數。

&是地址符

分析這個就要說到棧記憶體和堆記憶體了,比如*p在棧記憶體中分配了一個地址假設是ff22x0,它還沒有空間。而pp存在一個地址ff23x0,並且分配了一個空間儲存著123,這個地址是指向這個空間的。

p = &pp 這樣的賦值操作,也就是把ff23x0取出來,並且給p分配一個空間把ff23x0儲存進去,並且ff22x0指向這個空間。

*p = 999 從這裡就可以看出來p操作的是地址,而這個地址不就是ff23x0麼,於是pp成了999。

所謂的指標也就是儲存著地址的變數。

回到原型上,如果每一個函式中的 prototype屬性都是一個指標,實際上它只是一個地址引用著一個空間,而這個空間正是我們寫的xxx.prototype.xxx = function(){}這樣的程式碼在執行時分配的空間。那麼可見,使用原型的好處是空間只分配一次,大家都是共享的,因為它是指標。

先看一個例子

在解釋這個原型鏈之前,還要明白Js的一個特性,就是如果自身不存在,它會沿著原型往上查詢。它的原理稍微有些繞,Computer自身的prototype是指向它自身的原型物件的,而每一個函式又有一個constructor指向它自身,prototype.constructor又指向它自身。於是Computer的兩個例項apple,dell內部有一個proto屬性是指向Computer.prototype的,最後的結果是它們可以使用showMessage方法。

當然它們還有一個搜尋原則,比如在呼叫showMessage的時候,引擎先問apple自身有showMessage嗎?“沒有”,繼續搜尋,apple的原型有嗎,“有”,呼叫。所以從這裡可以看出,this.showMessage是會覆蓋prototype.showMessage的。

另外還可以使用isPrototypeOf來檢測一個物件是否在另一個物件的原型鏈上,上述的程式碼返回的是true。

使用hasOwnProperty來檢測到底是物件屬性還是原型屬性,使用this建立的屬性是一個物件屬性。

從上面可以看出來原型鏈的好處,但是它也不是萬能的,正因為指標的存在,對於某些引用型別來說這個就非常不好了,我需要保持原物件屬性值是每一個物件特有的,而不是共享的,於是把之前的建構函式與原型組合起來,也就解決了這樣的問題。

這樣的結果是在物件中都會建立一份屬於自己的屬性,而方法則是共享的。

動態原型模式

有時候遇到某些問題需要動態新增原型,但是例項中是不能新增的,所以繞來一下,在初始化建構函式中新增。

只要初始化了一次,以後就不用修改了。

寄生建構函式模式

這種模式的原理就是在一個函式中封裝需要建立物件的程式碼,然後返回它。

看起來它跟工廠模式還是很像的,

穩妥模式

這種模式主要是在解決需要安全的環境中使用,一般來說一個類如果不提供getter,setter方法,是不允許直接訪問和修改的。

這樣的方式可以保證屬性或者說是資料的安全性,不允許直接隨便修改,如果不提供setter方法的話,壓根就不允許。

繼承

談到物件導向,那麼就不能不談談繼承的問題了,而在Js中主要是將原型作為實現繼承的主要思路。

使用這樣的方式,實際上是從Computer的例項中先借它的prototype中所有的方法,但是這裡會存在幾個問題。

  • 如果Computer中需要傳入引數,比如name,借的時候我根本不知道要傳入什麼引數。
  • 在Apple中如果要繼續給原型新增方法,那麼就不能使用字面量的形式了,它會覆蓋掉
  • 如果要重寫父類中的方法必須要在借prototype之後
  • 那麼如何確定原型和例項的關係?貌似用instanceof和isPrototypeOf都會返回true

解決問題一如何傳入引數

我們知道Js中有兩個方法可以改變函式的上下文,apply和call,實際上類就是函式,這裡既借屬性也借prototype,不就可以解決這樣的問題了麼。

在執行時先借prototype,然後再借子類的this,但是這個也有個問題,那就是會呼叫兩次父類。

繼承的技巧

還有一種繼承是生成一個臨時物件,然後臨時物件借需要繼承的父類的prototype。

使用這樣的方式有個很大的缺陷,那就是不要借屬性之類的資料,因為它們是共享的,這是一個淺拷貝,還是因為指標的原因。不過要是繼承方法,這種方式很方便。

還有一種方式跟上述類似,主要是封裝了一層函式,用來返回物件。

寄生組合繼承

這樣的方式主要解決的問題是呼叫兩次父類的問題,避免額外的借來的屬性或方法。想想看第一次Computer.call(this),借來了this上的屬性或方法,第二次Apple.prototype = new Computer(),又借來了this上的屬性或方法,這裡的初衷是想借原型,沒辦法這個是例項,所以該借的不該借的都借來了。那麼要避免這樣的問題,就要解決繼承屬性的繼承屬性,繼承原型的繼承原型,也不亂借。

第一步把supers的原型賦值給F,第二步建立F的例項,第三步把_f例項的constructor屬性修改成子類,第四步把_f例項賦值給子類的prototype。

這樣的話就是不該借的也不會繼承了

理解記憶體管理

一般來說記憶體管理主要有這麼幾種方式,引用計數和標記,而JavaScript採用的就是標記管理的方式。Js的記憶體管理是自動的,但是並不是說執行完後立馬銷燬,而是有時間週期性,相隔一段時間執行一下垃圾回收,把沒有引用的記憶體全部銷燬。

OC中採用的是引用計數來手動管理記憶體,這樣的方式比較好,可以讓開發者自己來管理。當然也有不好的地方,如果遺忘了釋放,很可能引起應用的崩潰。

總體來看在IE中因為COM元件的原因,可能會發生迴圈引用的問題,這個問題在引用計數的記憶體管理都會遇見。所謂的迴圈引用是指在物件A中包含了一個指向B的指標,然後再物件B中包含一個指向A的指標,於是悲劇了。

 

大家都引用,於是,可想而知。要避免這種問題,一定要在不使用的時候my.element = null,把它斷開。

那麼,其他瀏覽器呢?還是標記清理的機制,比如一個函式的變數,在進入環境時標記上“進入環境”,執行完之後標記上“離開環境”,然後等待系統來釋放。

IE有一個手動釋放的方法,window.CollectGarbage,呼叫它就立馬釋放已經標記離開環境的變數,不過很多文章都不建議這樣做。

那麼一般都這樣做,引用型別的釋放

讓my脫離執行環境,標記上已經離開環境,然後等待系統執行垃圾回收,釋放記憶體。

XMLHttpRequest

註明: IE8已上,支援現代XMLHttpRequest

客戶端Js與伺服器進行網路互動必備的一個玩意,它不支援跨域,若要跨域還需要進行一些額外的處理。

在使用xhr物件時,要呼叫的第一個方法是open(),它接受三個引數[傳送請求的型別,請求的URL,描述是否同步還是非同步的布林值]false同步,true非同步。

關於Ajax同步非同步的個人理解:

  • 同步,是用資料塊的方式來傳輸的,在Js執行的表現上,當執行到這個Ajax請求時會等待它與伺服器互動成功之後才能執行下面一行的程式碼,也就是阻塞。
  • 非同步,是用位元組來傳輸的,它不等待是否成功,會執行之後的程式碼

結束時需要呼叫xhr.send(),如果沒有傳送資料的主體,必須要null,做為傳送引數。另外在接收到響應之前還可以呼叫abort()來取消非同步請求(不建議呼叫它)

HTTP狀態驗證

當收到響應後會自動填充xhr物件,它有幾個比較重要的狀態,我們必須要了解清楚與處理。

  • responseText:作為響應主體返回的文字
  • responseXML:如果響應內容的型別是”text/xml”或者”application/xml”,這個屬性中儲存的就是XML的DOM文件
  • status:響應的HTTP狀態
  • statusText:HTTP狀態的說明
  • readyState:用於描述請求傳送到完成的過程

正常情況下需要檢測status === 200 readyState === 4 這就表示responseText或者responseXML中已經填充了全部的資料可以提供給客戶端使用了。

readyState狀態

xhr物件其他方法或事件

每一個請求和響應都會帶有相應的HTTP頭資訊,其中對開發者是很有用的,而xhr物件提供了一個setRequestHeader方法來設定頭資訊,它必須在呼叫open方法之後並且在send方法之前。

既然有設定,那麼必須得有獲取,xhr物件也提供了兩個方法分別來獲取,getResponseHeader傳入一個頭部欄位名來獲取,getAllResponseHeaders來獲取全部的頭資訊。

而接收資料則需要處理onreadystatechange事件,每次重新整理狀態時,系統都會重新呼叫此事件。

跨域

客戶端Js出於安全的考慮,不允許跨域呼叫其他頁面的物件,正是因為這樣才給Ajax帶來了很多不方便的地方。跨域最簡單的理解就是因為Js同源策略的存在,比如a.com域名下的Js不能訪問b.com下的Js物件。

  • 協議埠沒法跨,客戶端
  • 在跨域上,域僅僅是通過首部來識別,window.location.protocol +window.location.host

利用document.domain和iframe來設定

對於主域相同而子域名不同的情況,可以通過document.domain來處理,比如www.163.com/index.html和wow.163.com/wower.html,在這兩個檔案中分別加入document.domain = “163.com”,然後在index.html頁面中建立一個iframe引入wower.html,獲取iframe的contentDocument,這樣這兩個js就可以互動了。

index.html

wower.html

使用這樣的方式來實現的跨域是有限制的

  • 主域名必須是同一個
  • 安全性引發的問題,比如第一個頁面出現了安全問題,在後面的頁面也會出現
  • iframe引用過多的話,每一個iframe都必須設定document.domain,比較瑣碎

偶爾可以使用一下

利用window.name

稍微有些繞,但是資料量比較大,也比較安全

  • wow.163.com/app.html 應用所在的頁面
  • wow.163.com/empty.html 中間代理頁面,搞個空的即可,但是必須在主域名下
  • www.qq.com/data.html 需要互動的資料頁面

在data.html頁面中

app.html頁面中建立一個隱藏的iframe,它的scr指向data.html,在onload事件中,把當前iframe的contentWindow.loaction修改成empty.html,當再次onload時就可以通過contentWindow.name來獲取到123了。

偶爾使用

利用iframe和location.hash

利用這種方式,說實話(不建議),比較繞,而且資料量小,直接暴露在URL上。它的原理主要是這樣的,假設wow.163.com/index.html頁面,wow.163.com/empty.html(空的,什麼內容都沒有),需要交換資料的頁面在www.qq.com/a.html上。

在wow.163.com/index.html#(#號就是我們要傳遞的資料),建立一個隱藏的iframe,hash值可以當引數傳遞給www.qq.com/a.html#(),在www.qq.com/a.html中可以獲取到hash值,根據它進行處理,然後在www.qq.com/a.html頁面中建立一個隱藏iframe,把處理的結果當hash值進行傳遞,給wow.163.com/empty.html#()這樣,在同一個域名下,wow.163.com/empty.html中的js可以通過parent.parent.location.hash = self.location.hash來改變hash值,這樣就達到了跨域的目的。

不建議使用,坑爹的思路

JSONP

這種方式是目前開發時最常用的一種方式,利用動態建立script標籤來實現跨域的目的,雖然瀏覽器有顯示Js物件的訪問,但是它沒有限制Js檔案的載入,任何域名下的Js檔案都可以載入。

對客戶端而言,檔案的載入其實就是傳送一次GET請求,在服務端實現時,也就是處理這次的GET請求,並且響應,引數可以通過?來帶走,俗稱一波流。

在客戶端上對於script檔案載入是否已經完畢的判斷,IE是判斷script標籤的readystatechange屬性,而其他瀏覽器是onload事件。

突然感覺做移動端不考慮IE的相容,果然是槓槓的,建議使用

HTML5 postMessage

主要是利用window.postMessage來傳送訊息,監聽window.message來獲取訊息,判斷origin可以判斷訊息來源,data獲取訊息內容,soucre來引用傳送方的window物件引用。

www.b.com/b.html傳送訊息給www.a.com/a.html

www.a.com/a.html獲取訊息

iframe的傳送方式

話不多說,移動端這種跨域方式也很常用(建議推薦使用)

HTML5 跨域頭 XMLHttpRequest2

如果是自己產品,又是做移動端可以使用,比上述任何方式都要方便,需要服務端支援響應時也要設定跨域頭。

如果伺服器響應此頭,瀏覽器會檢查此頭,它的值表示請求內容所允許的域名,也就是如果是*號,表示所有域都可以訪問,如果這裡是a.com,表示除了同源外,只允許來自a.com域的訪問。

如果需要讀取cookie則需要設定它

設定允許跨域的請求型別

相容性問題,某些版本的瀏覽器需要在open之後,設定xhr.withCredentials = true;話不多說,建議推薦使用

瀏覽器物件模型

BOM提供了很多物件,它的核心是window,表示它是瀏覽器的一個例項,在ECMAScript中又是Global物件。它提供了很多訪問瀏覽器的功能,這些功能與網頁無關,所以缺少事實標準的BOM既有意思又有些坑。複習它,主要是複習幾個比較有用的物件,其他可以瞭解一二。

location

算起來它是我用的最多的一個物件

它提供了當前視窗載入的頁面有關的資訊,也對URL進行了片段分解,既是window的屬性,也是document的屬性。

  • hash 返回URL的雜湊(#號後面跟著的零個或多個值)
  • host 返回伺服器名稱和埠號
  • hostname 返回不帶埠號的伺服器名稱
  • href 返回當前載入頁面的完整URL
  • pathname 返回URL中的目錄或檔名
  • port 返回URL中指定的埠號
  • protocol 返回頁面使用的協議
  • search 返回URL中的查詢字串,它以問好(?)開頭

上述的屬性基本上都可以直接使用,search除外,它返回的是一個完整的查詢字串,沒有辦法訪問其中的每個查詢字串引數,還需要額外的進行處理。

一般來說根據它的特點,?開頭&拼接,key=value的形式來展現,最好是key和value都要decodeURIComponent一下。

在location中除了上述的屬性外,還有一些比較有用的方法和技巧,主要是用來控制頁面跳轉的問題。

  • assign方法接收一個引數,表示立即開啟一個新的頁面並在歷史紀錄中生成一條記錄,它的效果等同於window.location.href = ”或者location.href = ”
  • 修改location物件的屬性比如href,hash,search等也可以來改變URL
  • replace方法接收一個引數,既跳轉到新的URL上,並且不會在歷史紀錄中增加一條新的紀錄
  • reload表示重新載入當前頁面

處理框架,設定時間,open,視窗位置,視窗大小

open現在估計沒人會用了

如果頁面中包含框架,則每個框架都有自己的window物件,可以使用frames來獲取,比如frames[0]或者frames[‘name’]。這裡還要了解的是top,parent,對於這些只要理解的層級關係,每一個指向都是會非常清楚的。

在做某些動畫效果的時候,主要是針對PC端,可能會使用到視窗位置,視窗大小的屬性來進行計算,比如innerWidth,innerHeight,outerWidth,outerHeight,獲取到這些尺寸,一般會與當前div的高寬進行減法來獲取精準的位置。

setTimeout和setInterval是進行時間排程的函式,我們知道Js是單執行緒的,但是可以使用這個在特定的時間範圍內執行程式碼,前面一個setTimeout是在指定的時間內執行(只執行一次),後面的setInterval則是以指定的時間重複執行(N次)

navigator

用這個一般是在統計使用者瀏覽器版本,作業系統等場景下才用的上,偶爾有幾個會比較實用。

  • cookieEnabled 判斷cookie是否開啟
  • userAgent 瀏覽器使用者代理字串
  • plugins陣列 主要是用來檢測瀏覽器安裝的外掛

###screen

在Js中有幾個物件在程式設計裡真用不上,這個就是其中之一。它主要是用來表明客戶端的能力,比如顯示器的資訊,畫素,高,寬等。

history

history物件儲存著使用者上網的歷史紀錄,但是這個也是非常不常用。主要是用go方法,back方法,forward方法。

說實話,後面三個navigator,screen,history基本上很廢材,HTML5中的history物件pushState非常有用外。

文件物件模型

DOM是針對HTML和XML文件的一個API,主要是使用JavaScript來進行程式設計操作HTML和XML文件。其他語言如果實現了DOM標準,理論上也是可以使用這個API的,這裡僅僅討論JavaScript的應用。

理解層級結構與關係

在瀏覽器中比如HTML頁面是由很多有層次結構的標籤組成的,而為這些標籤提供查詢,新增,刪除等等方法主要就是DOM在提供支援。

(頁面又稱為文件)文件中所有的節點之間都存在這樣或那樣的關係,比如下面一個經典的HTML:

一個標籤又可以稱為一個元素,head和body那就是兄弟關係,它們都來自一個父系html,又可以說html的子元素是head和body,可能這樣描述還不太明顯,這樣就用原生Js操作DOM來的方式來看看層級結構。

 

先通過getElementsByTagName獲取html根元素的節點,每一個元素都有一個childNodes集合和一個parentNode分別代表子節點集合和父節點,如果不存在,則都是null,如果是集合不存在,則是一個[]。

html的childNodes //[head,body] html的parentNode // document
每一個元素也都有一個firstChild和lastChild來分別代表第一個子元素和最後一個子元素

每一個元素也都有一個nextSibling和previousSibling分別代表前面一個元素和後面一個元素,以當前自己為參照物。

從這樣可以看出來,它就像族譜一樣對元素的關係進行了定義,通過理解這些層級關係,利用DOM提供的API可以很順利的進行操作。

操作DOM

常見的獲取方式

document.getElementById (通過ID來獲取到節點)
document.getElementsByTagName (通過節點標籤來獲取)
document.querySelector
document.querySelectorAll
後面兩個屬於HTML5提供的新API,在移動端會用的比較多,前者是獲取單個,後者獲取集合。

常見新增,刪除

appendChild
insterBefore
replaceChild
removeChild
appendChild主要是向childNodes集合的末尾新增一條元素,insterBefore可以用來插入特定位置,兩個引數,要插入的節點和作為參照的節點,更新成功後插入的節點會在參照節點之前,也就是參照節點的previousSibling。replaceChild和insterBefore有些類似,兩個引數,要插入的節點和參照節點,更新成功後,要插入的節點會替換參照節點,removeChild就比較好理解了,刪除一個節點,這四個方法都有返回值。

常見元素屬性

一般來說,如果var doc = document.getElementById(‘doc’);doc.id = ‘xx’;這樣的方式也是可以更新或者獲取到元素的屬性的,不過不推薦這麼使用,要獲取元素的屬性,DOM API也提供了三個方法來使用。

getAttribute
setAttribute
removeAttribute
getAttribute可以獲取元素的屬性,setAttribute可以對元素的屬性進行設定,如果屬性名不存在,則建立該屬性。removeAttribute則是完全刪除此屬性。

還有一個屬性attributes,主要是獲取元素屬性集合,這個不是很常用,主要是在遍歷元素屬性時會使用到,它是一個集合。

常見建立元素或文字

一般情況下建立元素都會使用字串的形式,innerHTML進去。不過,某些情況下,會用到createElement來建立一個元素,如果用到它,那麼建立的文字也必須使用createTextNode了。

對於文字節點,註釋節點等開發真的很少用,可以當一個子類大概瞭解即可。

關於模式的討論,主要可以用document.compatMode來判斷,如果是CSS1Compat就是標準模式,移動端不會出現這樣的情況,IE上可能有別的模式,模式主要是影響到CSS佈局上,Js影響非常少。

在移動端上滾動是一個比較要處理的問題,一般來說會使用scrollIntoView,scrollIntoViewIfNeeded,scrollByLines,scrollByPages,這四個方法safari chrome都有實現,意味著在iOS和安卓平臺都是良好的。
scrollByPages 將元素的內容滾動到指定的頁面高度,具體的高度是由元素的高度來決定的。
scrollByLines 將元素的內容滾動到知道的行數高度,引數可正可負。
scrollIntoViewIfNeeded,當元素在視窗(viewport)不可見,會滾動容器元素或者瀏覽器視窗讓其可見。如果是可見的,這個方法不起任何作用。如果引數為true,可能是垂直居中的可見。
scrollIntoView 滾動容器元素或者瀏覽器視窗,讓元素可見。
一些小技巧

每一個元素都存在一個contains方法,用來檢測傳入的節點是不是當前節點的子節點,火狐對於的方法名叫compareDocumentPosition。

如果要獲取一個文字節點可以使用innerText(純文字)來獲取字串,如果要獲取所有的包括標籤的字串可以使用innerHTML。它們還有一種outer系列對應的方法,主要的區別是前者(outerText)會替換節點,後者(outerHTML)會修改呼叫它的元素,一般基本沒人使用。它們可以獲取,也可以通過賦值來設定新的節點。

DOM2和DOM3

對於這兩級在DOM中基本上IE沒啥支援,或者說支援的非常少,像style物件,CSS的一些物件外。

這裡最大的變化是增加了對XML名稱空間的支援,元素樣式的訪問,節點的遍歷以及range。當然目前來看,節點的遍歷,range,XML名稱空間在開發中使用的非常少,可以當資料來閱讀,瞭解有這麼回事,用到的時候再查詢。而元素樣式的訪問,這個在開發中普遍使用的較多,因為在沒法使用css3動畫的瀏覽器中,可以通過改變樣式來到達動畫的目的。

對於iframe的訪問這裡增加了一個contentDocument物件來進行引用,還有節點的比較,isSameNode和isEqualNode,這兩個的區別在於,前者是否引用的同一個節點物件,後者是指兩個節點是否是相同的型別。不過,它們使用的也不多,瞭解就好。

元素的大小

這個部分需要理解,因為關乎到元素在瀏覽器上的位置顯示,跟動畫有關係,四個屬性。

  • offsetWidth 元素在水平方向佔用的空間大小
  • offsetHeight 元素在垂直方向佔用的空間大小
  • offsetLeft 元素的左外邊框到內邊框的距離
  • offsetTop 元素的上外邊框到內邊框的距離

滾動大小

這個在視察滾動或者處理滾動條的時候用的上,也是四個屬性

  • scrollHeight 在沒有滾動的情況下,元素的總高度
  • scrollWidth 在沒有滾動的情況下,元素的總寬度
  • scrollLeft 被隱藏在內容區域左側的畫素度
  • scrollTop 被隱藏在內容區域上側的畫素度

下面這些IE全部不支援,range支援一種叫做文字範圍的東西

元素遍歷

關於遍歷其實有兩個方法可用createNodeIterator和createTreeWalker,不過這些在開發中幾乎不會使用到,誰沒事去遍歷節點完呢。

關於range

這個也是非常少會使用到,除非是做那種編輯器應用或者線上編輯器等等,不過使用它可以更精準的控制的DOM,主要是使用createRange方法。

事件

IE瀏覽器的事件不是重點

事件是JavaScript與HTML進行互動的一個紐帶,理解事件可以更好的處理Web應用程式,現在的瀏覽器中主要支援兩種事件流:

  • 事件冒泡
  • 事件捕獲
  • DOM事件流

事件冒泡則是指事件開始時由具體的元素接收,然後逐級向上傳播。比如:

給p標籤監聽一個事件,它的流向是p,div,body,html,document,其實細心看來這種流的走向會存在一個問題,給div也監聽一個事件,當使用者點選P的時候是會觸發兩次的,好在event物件中有可以阻止事件冒泡的方法。

事件捕獲則是指事件由最上級接收,逐級向下傳播到具體的元素上,瞭解了冒泡之後這個就非常好理解了,正是一個相反的步驟。

而DOM事件流又正好是冒泡與捕獲的結合體,它分為三個階段:事件捕獲,目標事件,事件冒泡,如果在紙上畫出來,它的走向就是一個圓形。

對於事件處理程式,寫在HTML標籤中的,另外一種是直接寫一個function的,比如doc.onclick = function(){},一般來說這些瀏覽器支援,但是基本上不會使用了。因為前者是跟HTML耦合的,不利程式碼維護,而且雖然HTML載入了但是Js檔案還未載入,使用者點選後,是直接報錯的。後者雖然也可以刪除,比如doc.onclick = null,對於對程式碼有強迫症的同學,基本上不會使用到它。

那麼,我們該怎麼給一個元素新增上事件處理程式呢?

DOM2級事件處理程式

  • addEventLister
  • removeEventLister

所有的DOM節點都具備這兩個方法,它接收三個引數:

  • 要處理的事件名稱,比如click(這裡跟上述兩個以及IE註冊事件都不同,不需要on)
  • 需要事件進行處理的函式
  • 一個布林值,表示(true,在捕獲階段呼叫事件處理函式)(false,在冒泡階段呼叫事件處理函式)

一般情況下第三個引數都填false

IE瀏覽器對應的兩個方法,attachEvent,detachEvent,它們只有冒泡,事件名要加上on。

事件物件

在註冊完事件處理程式後,事件的一個比較重要的物件必須要理解,event事件物件

一般來說,這個物件中包含著所有與當前元素所監聽的事件有關的資訊,比如元素監聽的事件型別,元素本身等等。

比較重要的屬性和方法(只讀)

  • currentTarget 真正監聽事件的那個元素
  • target 事件的目標元素
  • type 事件的型別
  • perventDefault() 取消事件的預設行為
  • stopPropagation() 取消事件的捕獲或者冒泡
  • bubbles 事件是否冒泡
  • eventPhase 事件處理程式的三個階段,1捕獲2處於目標3冒泡

比較重要的屬性和方法(讀寫)

  • clientX 滑鼠在視窗中的水平位置
  • clientY 滑鼠在視窗中的垂直位置

事件型別

PC端主要是針對滑鼠,移動端則是觸控,手勢相關的處理

如果在PC端上發生一次click事件,實際上它是發生了三次事件,mousedown當滑鼠按下的時候,mouseup當使用者放開的時候,click兩個加起來就發生了一次click事件。相對於移動,PC上的滑鼠事件非常的豐富,例如mouseover當滑鼠首次移入一個元素邊界時觸發,mouseout當滑鼠移出元素時觸發,這個移出,到子元素上也會觸發這個事件,mousemove當滑鼠在元素內移動時重複觸發。

總體來說對於文件載入,表單控制元件,視窗大小改變等事件,比如獲取焦點,在失去或者獲取焦點是值改變等移動上都是一樣的,focus(獲得焦點)blur(失去焦點)。

在做一些視差滾動的效果時scroll事件是非常好用,移動上在css中提供了一個類似的屬性。

唯一的區別是移動端上沒有鍵盤事件。

移動事件

  • touchstart 當手指觸控到螢幕時觸發
  • touchmove 當手指在螢幕上連續滑動時觸發
  • touchend 當手指從螢幕上移開時觸發
  • touchcancel 當系統停止跟蹤觸控時觸發(這個事件沒有確定的觸發時間)

它們都是冒泡的,也可以取消

三個跟蹤觸控事件的屬性

  • touches 當前跟蹤觸控操作的touch陣列,在touchend事件中為空
  • targetTouchs 特定事件目標的touch陣列
  • ChangedTouches 上次觸控時發生了什麼改變的touch陣列

移動event事件物件

PC上存在的,在移動上也存在,描述上有差異,比如

  • target 觸控的DOM節點目標
  • pageX 觸控目標在頁面中的X座標
  • pageY 觸控目標在頁面中的Y座標

一些手勢

  • gesturestart 當一個手指按在螢幕上另外一個手指又觸控螢幕時觸發
  • gesturechange 依賴前者當其中的一個手指發生改變時觸發
  • gestureend 當任何一個手指離開時觸發

移動手勢乾貨三部曲

結語

現在的前端開發瞭解JS還是僅僅不夠的,你需要多方面擴充套件。

訪問Front-End-Develop-Guide專案,資料已準備齊全。

相關文章