JS 萬字總結 重量級乾貨!!!

2.6萬字JS乾貨分享,帶你領略前端魅力!

前言

本篇文章屬於知識總結型,概括出許多比較零散的知識點,都是乾貨噢~css

若是你是小白那麼這篇文章正好適合你,若是你是老手那麼不妨鞏固一下看看還有哪些邊角料沒補!html

建議:適合有js基礎的小夥伴觀看,篇幅較長,建議先收藏再慢慢瀏覽前端

整整花了一週時間總結了一些比較重點也有些比較偏的知識,但願各位小夥伴慢慢品嚐,若是有不對的地方或者是須要優化的地方望請告知,儘可能給你們呈現最有價值的文章。我的水平有限,還請各位大佬指點迷津。但願各位看了這篇文章能有本身的想法,在前端道路上還很漫長,與我一同探索吧!html5

目錄

1、 變量類型node

2、 深拷貝與淺拷貝jquery

3、 原型與原型鏈ios

4、 繼承與實現nginx

5、 實現class與extendses6

6、 做用域、執行上下文與閉包web

7、 this

8、 apply、call、bind實現

9、 同步與異步

10、 AMD、CMD、CommonJS與ES6模塊化

11、 script標籤之async與defer

12、 改變數組自己的api

十3、 window之location、navigator

十4、 ajax與fetch

十5、 WebSocket

十6、 短輪詢、長輪詢與WebSocket

十7、 長鏈接與短鏈接

十8、 存儲

十9、 跨域

二10、 setTimeout與setInterval

二11、 requestAnimationFrame

二12、 事件

二十3、 總結

二十4、 其餘文章

1、變量類型

==與===

對於==的判斷

  • 並非那麼嚴謹的判斷左右兩端是否相等
  • 它會優先對比數據的類型是否一致
  • 不一致則進行隱式轉換,一致則判斷值的大小,得出結果
  • 繼續判斷兩個類型是否爲null與undefined,若是是則返回true
  • 接着判斷是否爲string與number,若是是把string轉換爲number再對比大小
  • 判斷其中一方是否爲boolean,若是是就轉爲number再進一步判斷
  • 判斷一方是否爲object,另外一方爲string、number、symbol,若是是則把object轉爲原始類型再判斷
比較狀況
  1. 數組 == 值,(值類型指的是原始類型)會先轉成數值再比較,與字符串比較會先轉成字符串再比較
  2. 引用 == 值,會把引用類型轉成原始類型再比較
  3. 值 == 值,直接比較類型再比較值的大小
  4. 字符串 == 數字,則把字符串轉爲數值再比較
  5. 其餘類型 == boolean,則把boolean轉成數值再進一步比較
  6. undefined == null,也會發生隱式轉換,且2者能夠相互轉換,即2者相等,與自身也相等
  7. 對象 == 非對象,若是非對象爲string或number,則返回ToPrimitive(對象) == 非對象,的結果;ToPrimitive方法的參數若是是原始類型則直接返回;若是是對象,則調用valueOf方法,若是是原始值再進行原始類型轉換和大小對比;若是不是原始值則調用toString,且結果爲原始值則進行原始類型比較,若是不是原始值則拋出錯誤
// 如下結果都爲true
console.log([5]==5,['5']==5)
console.log({name:'5'}=='[object Object]')
console.log('5'==5,true==1,false==0)
console.log(undefined==null)
console.log([5,6]=='5,6',['5','6']=='5,6')
複製代碼

大白話:優先比較類型,同類型,比大小,非原始,調ToPrimitive,爲對象調valueOf,還非原始調toString,最後還非原始則報錯,若是爲原始則進行類型對比,若是不一樣類型再轉換,以後對比大小。

所謂==比較就是要轉換成同類型比較,若是沒法轉成同類型就報錯

優先比類型,再比null與undefined,再比string和number,再比boolean與any,再比object與string、number、symbol;以上若是轉爲原始類型比較,則進行類型轉換,直到類型相同再比較值的大小。這就是==的隱式轉換對比,比較繞,給個圖就清晰了!

以下爲判斷步驟

==與===

思考?如何判斷此表達式(注意==!與!==) []==![]

  • 基於運算符的優先級此式會先運算![]的結果
  • !優先於==,且[]爲真值(轉成boolean,結果爲true的就爲真值,包括{};轉成false的就爲假值),![]結果爲false,因此當前表達式轉化爲 []==false
  • 經過以前總結的轉換關係,任何類型與boolean類型比較,因此[]==false 轉化爲 []==0 比較
  • 此時變爲object與0比較,調用object的轉換成原始類型的方法valueOf其結果仍是valueOf
  • 再調用toString結果爲'',再進行string轉成number,則[]轉成數字類型0
  • 表達式進一步轉換成0==0,結果爲true。

雖然過程複雜,記住判斷的思路便可,非對象之間,先類型轉換再比大小,對象比較則調用獲取原始值方法再進一步比較。

以下爲toString與valueOf轉換

ToPrimitive轉換

對於===的判斷

  • ===屬於嚴格判斷,直接判斷二者類型是否相同,不一樣則返回false
  • 若是相同再比較大小,不會進行任何隱式轉換
  • 對於引用類型來講,比較的都是引用內存地址,因此===這種方式的比較,除非二者存儲的內存地址相同才相等,反之false
const a=[]
const b=a
a===b //true
---------------
const a=[]
const b=[]
a===b //false
複製代碼

7大原始類型與Object類型

  1. Boolean
  2. Null
  3. Undefined
  4. Number
  5. BigInt
  6. String
  7. Symbol
  8. Object

類型判斷

原始類型判斷
  • 原始類型string、number、undefined、boolean、symbol、bigint都能經過typeof(返回字符串形式)直接判斷類型,還有對象類型function也可判斷
  • 除了null沒法經過typeof(爲object)直接判斷類型(歷史遺留),包括對象類型,typeof把null看成對象類型處理,因此typeof沒法判斷對象類型,typeof也能判斷function
非原始類型判斷(以及null)

判斷數組

  • 使用Array.isArray()判斷數組
  • 使用[] instanceof Array判斷是否在Array的原型鏈上,便可判斷是否爲數組
  • [].constructor === Array經過其構造函數判斷是否爲數組
  • 也可以使用Object.prototype.toString.call([])判斷值是否爲'[object Array]'來判斷數組

判斷對象

  • Object.prototype.toString.call({})結果爲'[object Object]'則爲對象
  • {} instanceof Object判斷是否在Object的原型鏈上,便可判斷是否爲對象
  • {}.constructor === Object經過其構造函數判斷是否爲對象

判斷函數

  • 使用func typeof function判斷func是否爲函數
  • 使用func instanceof Function判斷func是否爲函數
  • 經過func.constructor === Function判斷是否爲函數
  • 也可以使用Object.prototype.toString.call(func)判斷值是否爲'[object Function]'來判斷func

判斷null

  • 最簡單的是經過null===null來判斷是否爲null
  • (!a && typeof (a) != 'undefined' && a != 0)判斷a是否爲null
  • Object.prototype.__proto__===a判斷a是否爲原始對象原型的原型即null

判斷是否爲NaN

  • isNaN(any)直接調用此方法判斷是否爲非數值

一些其餘判斷

  • Object.is(a,b)判斷a與b是否徹底相等,與===基本相同,不一樣點在於Object.is判斷+0不等於-0NaN等於自身
  • 一些其餘對象類型能夠基於原型鏈判斷和構造函數判斷
  • prototypeObj.isPrototypeOf(object)判斷object的原型是否爲prototypeObj,不一樣於instanceof,此方法直接判斷原型,而非instanceof 判斷的是右邊的原型鏈

一個簡單的類型驗證函數

function isWho(x) {
  // null
  if (x === null) return 'null'
  const primitive = ['number', 'string', 'undefined',
    'symbol', 'bigint', 'boolean', 'function'
  ]
  let type = typeof x
  //原始類型以及函數
  if (primitive.includes(type)) return type
  //對象類型
  if (Array.isArray(x)) return 'array'
  if (Object.prototype.toString.call(x) === '[object Object]') return 'object'
  if (x.hasOwnProperty('constructor')) return x.constructor.name
  const proto = Object.getPrototypeOf(x)
  if (proto) return proto.constructor.name
  // 沒法判斷
  return "can't get this type"
}
複製代碼

2、深拷貝與淺拷貝

在項目中有許多地方須要數據克隆,特別是引用類型對象,咱們沒法使用普通的賦值方式克隆,雖然咱們通常使用第三方庫如lodash來實現深拷貝,可是咱們也須要知道一些其中的原理

淺拷貝

  • Object.assign({},obj)淺拷貝object
  • obj1={...obj2}經過spread展開運算符淺拷貝obj2
  • Object.fromEntries(Object.entries(obj))經過生成迭代器再經過迭代器生成對象
  • Object.create({},Object.getOwnPropertyDescriptors(obj))淺拷貝obj
  • Object.defineProperties({},Object.getOwnPropertyDescriptors(obj))淺拷貝obj

簡單實現淺拷貝

// a原拷貝對象,b新對象
for (const key in a) {
  b[key] = a[key]
}
------------------------------------------
for (const key of Object.keys(a)) {
  b[key] = a[key]
}
複製代碼

淺拷貝只拷貝一層屬性對於引用類型沒法拷貝

深拷貝

  • JSON.parse(JSON.stringify(obj))經過JSON的2次轉換深拷貝obj,不過沒法拷貝undefinedsymbol屬性,沒法拷貝循環引用對象
  • 本身實現深拷貝

簡單深拷貝

//簡單版深拷貝,只能拷貝基本原始類型和普通對象與數組,沒法拷貝循環引用
function simpleDeepClone(a) {
  const b=Array.isArray(a) ? [] : {}
  for (const key of Object.keys(a)) {
    const type = typeof a[key]
    if (type !== 'object' || a[key] === null) {
      b[key] = a[key]
    } else {
      b[key] = simpleDeepClone(a[key])
    }
  }
  return b
}
//精簡版深拷貝只能拷貝基本原始類型和普通對象與數組,能夠拷貝循環引用
function deepClone(a, weakMap = new WeakMap()) {
  if (typeof a !== 'object' || a === null) return a
  if (s = weakMap.get(a)) return s
  const b = Array.isArray(a) ? [] : {}
  weakMap.set(a, b)
  for (const key of Object.keys(a)) b[key] = clone(a[key], weakMap)
  return b
}
//js原生深拷貝,沒法拷貝Symbol、null、循環引用
function JSdeepClone(data) {
  if (!data || !(data instanceof Object) || (typeof data == "function")) {
    return data || undefined;
  }
  const constructor = data.constructor;
  const result = new constructor();
  for (const key in data) {
    if (data.hasOwnProperty(key)) {
      result[key] = deepClone(data[key]);
    }
  }
  return result;
}
複製代碼

比較完善的深拷貝

//深拷貝具體版,非徹底,但大部分均可以
function deepClonePlus(a, weakMap = new WeakMap()) {
  const type = typeof a
  if (a === null || type !== 'object') return a
  if (s = weakMap.get(a)) return s
  const allKeys = Reflect.ownKeys(a)
  const newObj = Array.isArray(a) ? [] : {}
  weakMap.set(a, newObj)
  for (const key of allKeys) {
    const value = a[key]
    const T = typeof value
    if (value === null || T !== 'object') {
      newObj[key] = value
      continue
    }
    const objT = Object.prototype.toString.call(value)
    if (objT === '[object Object]' || objT === '[object Array]') {
      newObj[key] = deepClonePlus(value, weakMap)
      continue
    }
    if (objT === '[object Set]' || objT === '[object Map]') {
      if (objT === '[object Set]') {
        newObj[key] = new Set()
        value.forEach(v => newObj[key].add(deepClonePlus(v, weakMap)))
      } else {
        newObj[key] = new Map()
        value.forEach((v, i) => newObj[key].set(i, deepClonePlus(v, weakMap)))
      }
      continue
    }
    if (objT === '[object Symbol]') {
      newObj[key] = Object(Symbol.prototype.valueOf.call(value))
      continue
    }
    newObj[key] = new a[key].constructor(value)
  }
  return newObj
}
複製代碼

刨析深拷貝(我的思路)

  • 本人使用遞歸算法來實習深拷貝,因爲使用遞歸,會讓代碼看起來更加易懂,在不觸及調用棧溢出的狀況下,推薦使用遞歸
  • 深拷貝,其實考驗的就是如何把引用類型給拷貝過來,還有Symbol類型比較特殊,如何實現一個比較完整的深拷貝就要涉及不一樣類型的拷貝方式
  1. 首先考慮簡單的原始類型,因爲原始類型在內存中保存的是值能夠直接經過值的賦值操做,先判斷傳入參數是否爲原始類型,包括null這裏歸爲原始類型來判斷,不必進入對象環節,函數直接賦值不影響使用
  2. 通過原始類型的篩選,剩下對象類型,取出全部對象的鍵,經過Reflect.OwnKeys(obj)取出對象自身全部的鍵,包括Symbol的鍵也能取出
  3. 因爲對象有2種體現形式,數組普通對象,對於這2者要單獨判斷,先生成一個拷貝容器即newObj
  4. 接下來就能夠開始遍歷 步驟2 中獲取到對象全部的鍵(僅自身包含的鍵),經過for..of 遍歷,取出當前要拷貝的對象a,對應於當前遍歷鍵的值,即a[key]
  5. 對a[key]值的類型進行判斷,此值類型的可能性包括全部的類型,因此又回到步驟1中先判斷原始類型數據;若是是原始類型能夠直接賦值跳過這一輪,進行下一輪遍歷
  6. 通過上一步的篩選,此時剩下的只是對象類型,因爲對象類型沒法經過typeof直接區分,因此能夠借用原始對象原型方法 Object.prototype.toString.call(obj) 來進行對象具體類型的判斷
  7. toString判斷的結果會以'[object xxx]',xxx爲對應對象類型形式體現,基於這種轉換能夠清晰判斷對象的具體類型,以後再對各類類型進行相應的深拷貝便可
  8. 以上並未使用遞歸,因爲上述的拷貝,還未涉及多層次的嵌套關係並不須要使用遞歸
  9. 接下來將要判斷嵌套類型數據,(此順序可變,不過出現頻率高的儘可能放在前頭)首先判斷普通對象和數組,若是是,則直接扔給遞歸處理,因爲處理數組和普通對象的邏輯已經在這以前處理好了,如今只需重複上面的步驟,因此直接遞歸調用就好,遞歸到最後一層,應該是原始類型的數據,不會進入無限調用
  10. 接下來是判斷2種特殊類型Set和Map,因爲這2種類型的拷貝方式不一樣,進一步經過if分支對其判斷,遍歷裏邊所存放的值,Set使用add方法向新的拷貝容器添加與拷貝對象相同的值,此處值的拷貝也應該使用深拷貝,即直接把值丟給遞歸函數,它就會返回一個拷貝好的值。Map相似,調用set方法設置鍵和值,不過正好Map的鍵能夠存放各類類型
  11. 到了拷貝Symbol環節,這個類型相對特殊一點,Symbol的值是惟一的,因此要獲取原Symbol所對應的Symbol值,則必須經過借用Symbol的原型方法來指明要獲取Symbol所對應Symbol的原始值,基於原始值建立一個包裝器對象,則這個對象的值與原來相同
  12. 篩選到這裏,剩餘的對象,基本上就是一些內置對象或者是不須要遞歸遍歷屬性的對象,那麼就能夠基於這些對象原型的構造函數來實例化相應的對象
  13. 最後遍歷完全部的屬性就能夠返回這個拷貝後的新容器對象,做爲拷貝對象的替代
  14. 基於循環引用對象的解析,因爲循環引用對象會形成循環遞歸致使調用棧溢出,因此要考慮到一個對象不能被屢次拷貝。基於這個條件能夠使用Map對象來保存一個拷貝對應的表,由於Map的鍵的特殊效果能夠保存對象,所以正好適用於對拷貝對象的記錄,且值則是對應的新拷貝容器,當下次遞歸進來的時候先在拷貝表裏查詢這個鍵是否存在,若是存在說明已經拷貝過,則直接返回以前拷貝的結果,反之繼續
  15. 因爲Map存放的鍵屬於強引用類型,且深拷貝的數據量也不小,若是這些拷貝後的拷貝表不及時釋放可能會形成垃圾堆積影響性能,所以須要使用到weakMap方法代替Map,weakMap存放的鍵爲弱引用類型,且鍵必須爲對象類型,正好以前的newObj就是對象類型能夠存放,使用弱引用的好處,能夠優化垃圾回收,weakMap存放的是拷貝表,此拷貝表在拷貝完成以後就沒有做用了,以前存放的拷貝對象,通過深拷貝給新拷貝容器,則這些舊對象在銷燬以後,對應於拷貝表裏的對象也應該隨之清除,不該該還保留,這就是使用弱引用來保存表的緣由。

以上就是本人在實現過程當中的思路,可能講的比較囉嗦,可是我仍是但願使用通俗的話讓各位明白,表達能力有限,望諒解。

接下來讓咱們看看WeakMap的好處

let obj = {
  name: {
    age: [{
      who: 'me'
    }]
  }
}
let wm = new WeakMap()
deepClonePlus(obj, wm)
obj=null
console.dir(wm) // No properties 即爲空
複製代碼

從上面能夠看出若是原拷貝對象被清空那麼WeakMap保存的拷貝表也將被清空,總的來講方便一點,總比麻煩一點好

看看這種狀況

const obj = {
  name: {
    age: [{
      who: 'me'
    }]
  }
}
let wm = new WeakMap()
console.time('start')
for (let i = 0; i < 1000000; i++) {
  deepClonePlus(obj, wm) // wm爲手動傳入的weakmap
  // 此處爲了與下面對比,這裏故意重置weakmap存儲的拷貝值
  wm = new WeakMap() 
}
console.timeEnd('start') // 耗時2645ms
------------------------------------------------
let wm = new WeakMap()
let m
console.time('start')
for (let i = 0; i < 1000000; i++) {
  deepClonePlus(obj, wm)
  // 這次爲對照組,也執行建立WeakMap可是不重置以前拷貝的wm
  m = new WeakMap()
}
console.timeEnd('start') // 耗時73ms
複製代碼

從以上對比能夠看出若是是屢次拷貝同一對象,最好使用WeakMap來存儲拷貝表,那麼以後的每次拷貝只需從拷貝表中取出值便可,因爲是淺拷貝因此時間較短(注意:不過這種直接從WeakMap中取出的值屬於淺拷貝,使用同一個wm對象拷貝出來的都是淺拷貝,若是每一個都須要深拷貝那麼只能每次從新建立WeakMap)

3、原型與原型鏈

原型

  • 只有對象類型纔有原型概念
  • 普通對象(即便用對象字面量或者Object構造器建立的對象)的原型爲__proto__屬性,此屬性實際上是個訪問器屬性,並非真實存在的屬性,或者能夠使用es6的Reflect.getPrototypeOf(obj)Object.getPrototypeOf(obj)方法獲取對象的原型,其關係Reflect.getPrototypeOf({}) === Object.getPrototypeOf({}) === {}.__proto__
  • 普通函數有2個屬性,一個是是__proto__(與普通對象相似),還有一個是函數專有的prototype屬性,由於函數有雙重身份,便可以是實例也能夠是構造器,因此關係比較特殊
  • 不是全部的對象都會有原型,好比對象原型Object.prototype的原型Object.prototype.__proto__就指向null,字典對象的原型也爲null(把對象的__proto__設置爲null,或者使用Object.create(null)建立一個沒有原型的字典對象,可是這個對象仍是屬於對象類型),因此原始對象原型(Object.prototype)就是最原始的原型,其餘對象類型都要繼承自它。
  • 箭頭函數雖然屬於函數,由Function產生,可是沒有prototype屬性沒有構造器特性,因此也就沒有所謂的constructor,就不能做爲構造器使用

原型鏈

這裏會詳細介紹原型、原型鏈、實例、構造器的關係 先看最原始的關係

原型鏈

由如上關係能夠驗證console.log(Function.prototype.__proto__.constructor.__proto__.constructor === Function) //true

  • 全部函數都是由Function函數構造器實例化而來
  • 全部實例的原型都指向構造它的構造器的prototype
  • 每一個構造器自身特有的方法就是靜態方法,原型上的方法可供全部繼承它或間接繼承它的實例使用
  • 構造器也是函數,也是被Function實例化出來的,因此構造器的__proto__就是Function,可是構造器的prototype屬性指向的原型,是此構造器實例化出來的實例所指向的原型;簡單說構造器的prototype就是做爲它的實例的原型

看看函數的原型鏈

函數原型鏈

  • 在js中函數有多重身份,函數能夠做爲類就是構造器使用,定義靜態方法,做爲普通函數調用,
  • 只有由原始函數構造器(Function)實例化的函數才擁有直接使用函數原型(Function.prototype)上面的內置方法,建立函數只能經過原始函數構造器生成,
  • 普通函數做爲構造器使用(new)時至關於類(class)使用,類的prototype就是實例的原型,咱們能夠給原型添加屬性,給類添加屬性時就至關於給構造器添加靜態屬性
  • 普通函數在建立實例的時候,會生成一個實例的原型,此原型指向Object.prototype即原始對象原型,也就是繼承對象原型,這麼一來實例也繼承了對象的原型,則實例也屬於對象類型

4、繼承與實現

繼承

  • 所謂繼承通常說的是原型繼承,一個原型上面定義的方法通常都是基於其實例的用途來定義的,也就是說,原型的方法應該是實例常常用到的通用方法,而構造器方法通常是特定狀況下可能會用到的方法,可按需調用,原型方法只能供其實例來使用
  • 繼承可讓原型鏈豐富,根據需求定製不一樣的原型鏈,不會存在內存浪費的狀況,原型只會保留一份,用到的時候調用就行,還能節省空間

原型繼承

  • 能夠看出原型通常是一些共有的特性,實例是特有的特性,繼承的越多越具體,原型鏈的最頂端是最抽象的,越底端越具體,這樣一來咱們能夠根據需求在恰當位置繼承來實現個性化的定製屬性,統一而又有多樣化

繼承的實現

  • 經過es6的extends關鍵字來繼承原型
  • 手動實現原型繼承
//貌似這個方法就能實現原型繼承,不過因爲使用__proto__訪問器是不推薦的
function foo(v) {
  this.v = v
}
function boo(v) {
  this.vv = v
}
boo.prototype.__proto__ = foo.prototype
const b = new boo(3)
------------------------------------------------
//採用這種方式能夠實現原型的繼承,也是直接修改原型
function foo(v) {
  this.v = v
}
function boo(v) {
  this.vv = v
}
boo.prototype = Object.create(foo.prototype, {
  constructor: {
    value: boo,
    enumerable: false,
    writable: true,
    configurable: true
  }
})
const b = new boo(3)
-----------------------------------------------
//藉助一個空構造器來實現原型繼承
function foo(v) {
  this.v = v
}
function boo(v) {
  this.vv = v
}
function o() {}
o.prototype = foo.prototype
boo.prototype = new o()
boo.prototype.constructor = boo
const b = new boo(3)
------------------------------------------------
//class的extends會將子類的__proto__設置爲父類
若是實現相似extends的繼承還需加上
boo.__proto__ = foo
複製代碼

實現構造器原型的繼承,無非就是父構造器原型賦值給子構造器原型的原型,還有須要保證子構造器原型不能含有父構造器的屬性

5、實現class與extends

實現class

  • es6加入的class實際上是爲了開發者方便建立類,與其餘語言在寫法上儘可能一致,可是js原生並無類這個東西,爲了實現類的效果,能夠經過js的構造器來實現,class使用new關鍵字生成實例,構造器也是經過new來實例化,那麼能夠推斷class本質也是個構造器
  • 手動實現class
const Class = (function () {
  function Constructor(name) {
    this.name = name
  }
  //添加原型方法
  Constructor.prototype.getName = function name(name) {
    console.log('原型方法getName:' + this.name);
  }
  //添加原型屬性
  Constructor.prototype.age = '原型屬性age'
  //添加靜態方法
  Constructor.log = function log() {
    console.log('我是構造器的靜態方法log');
  }
  //添加靜態屬性
  Constructor.isWho = '構造器靜態屬性isWho'
  return Constructor
})()
const i = new Class('我是實例')
複製代碼

實現class語法糖,只需封裝一層函數。

  • 返回的Constructor就是實例的構造器,其prototype是個空白的對象這是因爲Function形成的
  • new後面調用的函數必須是一個構造器函數,用於構造實例,此構造器的this指向實例
  • 構造器內部須要實現依照傳入的參數設置實例的屬性
  • 定義Class時須要實現原型屬性和靜態屬性的掛載

以上只實現class的定義,接下來要實現可以兼容繼承的寫法

實現extends

  • 繼承須要知足原型的繼承
  • 還須要知足可調用父類構造器
//父類
const Parent = (function () {
  function Constructor(age) {
    this.age = age
  }
  Constructor.prototype.getName = function () {
    console.log(this.name);
  }
  return Constructor
})()
//子類
const Class = (function (_Parent = null) {
  if (_Parent) {
    Constructor.prototype = Object.create(_Parent.prototype, {
      constructor: {
        value: Constructor,
        enumerable: false,
        writable: true,
        configurable: true
      }
    })
    Constructor.__proto__ = _Parent
  }
  function Constructor(name, age) {
    _Parent ? _Parent.call(this, age) : this
    this.name = name
  }
  Constructor.prototype.getAge = function () {
    console.log(this.age);
  }
  return Constructor
})(Parent)
複製代碼
  • 實現原型繼承,能夠使用以前的繼承寫法,注意class形式的繼承,會把父類設爲子類的__proto__
  • 在構造函數內判斷是否有父類,若是有就要調用父類的構造函數,把當前的this傳入,這樣才能生成父類構造器中定義的屬性,這纔算是真正的繼承。繼承不單繼承原型還能實現繼承父類構造器中定義的屬性
  • 對於原型方法和靜態方法也是相似定義,注意定義的方法若是用到this須要使用function關鍵字定義函數,不可以使用匿名函數,不然this沒法指向調用對象自己

6、做用域、執行上下文與閉包

做用域與做用域鏈

做用域

  • 全部未定義的變量直接賦值會自動聲明爲全局做用域的變量(隱式全局變量能夠用delete刪除,var定義的則不行)
a=1 // 隱式全局變量 嚴格模式報錯
var b=2 // 顯式全局變量
console.log(a,b) //1 2
delete a  // 嚴格模式報錯
delete b  // 嚴格模式報錯
console.log(b,a) // 2   a is not defined 
複製代碼
  • window對象的全部屬性擁有全局做用域
  • 內層做用域能夠訪問外層做用域,反之不行
  • var聲明的變量,在除了函數做用域以外,在其餘塊語句中不會建立獨立做用域
  • let和const聲明的變量存在塊語句做用域,且不會變量提高
  • 同做用域下不能重複使用let、const聲明同名變量,var能夠,後者覆蓋前者
  • for循環的條件語句的做用域與其循環體的做用域不一樣,條件語句塊屬於循環體的父級做用域
// 如下語句使用let聲明不報錯,說明爲不一樣做用域
for (let i = 0; i < 5; i++) {
  let i = 5
}
--------------------------------------------
// 此語句報錯,說明循環體爲條件語句塊的子做用域
// for循環執行順序爲:條件語句塊1->條件語句塊2->循環體->條件語句塊3->條件語句塊2 依次類推
for (let i = 0; i < 5; i=x) { // x is not defined
  let x = 5
}
複製代碼

做用域鏈

  • 做用域鏈也就是所謂的變量查找的範圍
  • 在當前做用域引用變量時,若是沒有此變量,則會一路往父級做用域查找此變量,直到全局做用域,若是都沒有,在非嚴格狀況下會自動聲明,因此是undefined,在嚴格條件下則會報錯
  • 變量的查找路徑依據的是在建立這個做用域的地方向上查找,並不是是在執行時的做用域,以下 b變量的值爲2。能夠看出當執行到須要b變量時,當前做用域下並無b,因此要到定義這個b變量的靜態做用域中尋找,即建立時候的做用域鏈上查找b的值
b = 1
function a() {
  // 定義b,找到
  const b = 2
  function s() {
    // 使用到b,當前做用域並無,向上找
    console.log(b);
  }
  return s
}
const s = a()
var b = 3
s() // 2
複製代碼
  • 做用域在腳本解析階段就已經規定好了,因此與執行階段無關,且沒法改變

執行上下文

  • 執行上下文在運行時肯定,隨時可能改變
  • 調用棧中存放多個執行上下文,按照後進先出的規則進行建立和銷燬,最底部的執行上下文,也就是棧低的執行上下文爲全局上下文,最先被壓入棧中,其上下文中的this指向window,嚴格模式下爲undefined
  • 建立執行上下文時,會綁定當前this,肯定詞法環境,存儲當前環境下函數聲明內容,變量let與const綁定但未關聯任何值,確認變量環境時,綁定var的初始值爲undefined
  • 在var聲明以前,調用var聲明的變量時值爲undefined,由於建立了執行上下文,var聲明的變量已經綁定初始undefined,而在let和const聲明以前調用其聲明的變量時,因爲只綁定在了執行上下文中,但並未初始任何值,因此在聲明以前調用則會拋出引用錯誤(即TDZ暫時性死區),這也就是函數聲明與var聲明在執行上下文中的提高

這裏瞭解一下函數、變量提高

console.dir(foo) // foo(){}
function foo() {}
var foo = 5
/*
console.dir(foo) // undefined
var foo = 5
*/
------------------------------
var foo = 5
function foo() {}
console.dir(foo) // 5
複製代碼

從以上代碼結果能夠得出結論:

  • 上面代碼塊可以體現,在解析階段會將函數與變量提高,且函數的優先級比var聲明的變量高,由於打印的是函數聲明,若是var聲明的優先級高,那麼應該是undefined
  • 從下面的代碼塊中能夠看出foo在代碼執行的時候被賦值爲5,而函數聲明在解析階段已經結束,在執行階段沒有效果
  • 還有一點 我的認爲在解析階段,函數聲明與變量聲明提高以後在代碼塊中的位置順序沒什麼關係

閉包

  • 所謂閉包就是函數與其詞法環境(建立當前做用時的任何局部變量)的引用。閉包能夠使內部函數訪問到外部函數的做用域,當函數被建立時即生成閉包
function fn1() {
  var name = 'hi';
  function fn2() {
    console.log(name);
  }
  return fn2
}
fn1()() // hi
複製代碼
  • 當你從函數內部返回一個內部函數時,返回的函數將會保留當前閉包,即當前詞法環境
  • 閉包只會保留環境中任何變量的最後一個值,這是由於閉包所保存的是整個變量的對象
  • 閉包的做用域鏈包含着它本身的做用域,以及包含它父級函數的做用域和全局做用域
  • 當返回一個閉包時,保留此閉包下的全部被外部引用的對象
  • 閉包之間是獨立的,在閉包環境下能夠建立多個不一樣的閉包環境暴露給外部,從而實現不一樣的效果
function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2));  // 7
console.log(add10(2)); // 12
複製代碼
  • 暴露閉包的方式不止返回內部函數一種,還能夠使用回調函數產生閉包環境,或者把內部函數賦值給其餘外部對象使用
  • 閉包在沒有被外部使用的狀況下,隨執行結束銷燬,如何產生閉包而且保留閉包環境的關鍵就在於不讓其環境被垃圾回收系統自動清除,那麼就要使內部環境中的引用被外部保留,這樣才能保留閉包
  • 閉包雖然方便咱們操做和保留內部環境,可是閉包在處理速度和內存消耗方面對腳本性能具備負面影響,除非在特定的狀況下使用

這裏看個有趣的東西

function foo(){
  let a={name:'me'}
  let b={who:'isMe'}
  let wm=new WeakMap()
  function bar(){
    console.log(a)  // a被閉包保留
    wm.set(b,1) // 弱引用b對象
    return wm //wm被閉包保留
  }
  return bar
}
const wm=foo()()
console.dir(wm) // No properties 即爲空
-------------------------------------------
function foo(){
  let a={name:'me'}
  let wm=new WeakMap()
  function bar(){
    console.log(a)
    wm.set(a,1)
    return wm
  }
  return bar
}
const wm=foo()()
console.dir(wm) // 保存了對象a與其值1
複製代碼
  • 從上塊代碼中能夠看出,bar被return到外部環境,因此其內部造成閉包,bar中使用到的變量(a,wm)都會被保留下來,可是最後打印wm的時候爲空?這是由於外部並無引用到b對象,只是經過wm弱引用保存b的值,從wm爲空能夠看出,閉包內部的b被清除,因此wm也自動清除b的弱引用,能夠論證以前所說,閉包只保留外部用到的變量
  • 從下塊代碼能直接看出a就是閉包中的a,bar在外部執行時須要用到a與wm因此保留了下來
  • 有人可能會不解,爲何上塊代碼中的b也被wm.set(b,1)引用,可是最終就沒有呢,那是由於WeakMap中保留的是b的弱引用,能夠理解爲,wm中的b是依賴原函數中的b而存在,當wm被return時,閉包中的b,沒有被任何外部所依賴,而是別人依賴它。能夠這麼理解 b牽着別人走,由於b沒有被外面人牽着走,因此b這個鏈子就被斷開,也影響到b牽的人一塊丟了

7、this

先看一張圖

函數this

  • this的綁定在建立執行上下文時肯定
  • 大多數狀況函數調用的方式決定this的值,this在執行時沒法賦值
  • this的值爲當前執行的環境對象,非嚴格下老是指向一個對象,嚴格下能夠是任意值
  • 全局環境下this始終指向window,嚴格模式下函數的調用沒有明確調用對象的狀況下,函數內部this指向undefined,非嚴格下指向window
  • 箭頭函數的this永遠指向建立當前詞法環境時的this
  • 做爲構造函數時,函數中的this指向實例對象
  • this的綁定只受最靠近調用它的成員的引用
  • 執行上下文在被執行的時候纔會建立,建立執行上下文時纔會綁定this,因此this的指向永遠是在執行時肯定
function foo(){
  console.dir(this) // window ,嚴格下undefined
}
foo()
-----------------------------------------------
function foo(){
  console.dir(this) //非嚴格Number對象,嚴格模式 5
}
foo.call(5)
複製代碼

嚴格與非嚴格模式下的this指向是不一樣的,非嚴格老是指向一個對象,嚴格模式能夠爲任意值

執行前

this演示1

執行後

this演示2

以上2圖能夠使用chrome開發工具來進行查看程序執行時的相關數據,能夠看到嚴格模式下簡單調用的函數內部的this指向undefined

普通函數中的this

直接調用

在沒有明確調用者狀況下函數內部this指向window,嚴格模式下都爲undefined,除非綁定函數的this指向,纔會改變this

// 直接調用函數
function foo() {
  console.dir(this) //window,嚴格下 undefined
  function boo(){
    console.dir(this) //window,嚴格下 undefined
  }
  boo()
}
----------------------------------------------
// 取出對象中的函數,再進行調用
const obj = {
  foo: function foo() {
    console.dir(this) //window,嚴格下 undefined
    function boo() {
      console.dir(this) //window,嚴格下 undefined
    }
    return boo
  }
}
const foo = obj.foo
foo()()
----------------------------------------------
// 直接經過對象調用函數,再調用返回的函數,能夠看出this的指向隨調用對象改變
const obj = {
  foo: function foo() {
    console.dir(this) //obj,嚴格下 obj
    function boo() {
      console.dir(this) //window,嚴格下 undefined
    }
    return boo
  }
}
const foo = obj.foo()
foo()
----------------------------------------------
// 基於回調函數也是如此
function foo(func) {
  console.dir(this) // window ,嚴格下 undefined
  func()
}
foo(function () {
  console.dir(this) // window ,嚴格下 undefined
})
複製代碼

基於調用者以及不一樣調用方式

函數調用也就是在函數名後面加個(),表示調用,若是函數名前沒有加任何東西,那麼默認爲簡單調用,在嚴格與非嚴格環境下,簡單調用的函數內部this指向undefined與window,可是全局環境下的this永遠爲window

基於對象

當函數做爲對象的方法調用時,不受函數定義方式或者位置影響

// 函數this指向調用者對象
const obj = {
  foo: function () {
    console.dir(this) // obj1,嚴格下 obj1
    function boo() {
      console.dir(this) // window,嚴格下 undefined
    }
    boo()
    return boo
  }
}
const obj1 = {}
obj1.boo = obj.foo
obj1.boo()
----------------------------------------------
// 不一樣調用對象時,this指向調用者
const obj = {
  foo: function () {
    console.dir(this) // obj,嚴格下 obj
    function boo() {
      console.dir(this)
    }
    boo() // window,嚴格下 undefined
    return boo
  }
}
const obj1 = {}
obj1.boo = obj.foo()
obj1.boo() // obj1,嚴格下 obj1
----------------------------------------------
// this指向最近的調用者
const obj = {
  name: 'obj',
  obj1: {
    name: 'obj1',
    foo: function () {
      console.dir(this.name) // obj1
    }
  }
}
obj.obj1.foo()
複製代碼

基於new關鍵字

// 基於new關鍵字調用的函數內部this指向實例
function foo() {
  console.dir(this) // foo實例
  console.log(this instanceof foo) //true
  console.log(foo.prototype.isPrototypeOf(this)) //true
  that = this
}
var that
const f = new foo()
console.log(that === f) // true
----------------------------------------------
// 嵌套函數內部this與調用函數所在環境的this無關
function foo() {
  console.dir(this) // foo實例
  function boo() {
    console.dir(this) //window,嚴格下undefined
  }
  boo()
}
const f = new foo()
複製代碼

基於定時器與微任務

微任務中的簡單調用的函數this指向window嚴格下指向undefined,而定時器中的回調函數無論在嚴格仍是非嚴格環境下this永遠指向window,說明一點,調用window對象的方法時this指向window也就是全局對象,換句話說,簡單調用的函數若是屬於window自己自帶的方法那麼這個方法的this指向window

// 異步任務中簡單調用的函數都是進入隊列,最後由全局環境調用
const id = setInterval(function () {
  console.dir(this) // window ,嚴格下 window
  setTimeout(() => {
    console.dir(this) // window ,嚴格下 window
    clearInterval(id)
  });
})
----------------------------------------------
new Promise(function (resolve, reject) {
  console.dir(this) // window ,嚴格下 undefined
  resolve()
}).then(function (res) {
  console.dir(this) // window ,嚴格下 undefined
});
----------------------------------------------
(async function foo() {
  function boo() {
    console.dir(this) // window ,嚴格下 undefined
  }
  await boo()
  console.dir(this) // window ,嚴格下 undefined
})()
----------------------------------------------
// 定時器的回調最終都會被做爲簡單函數被執行,定時器屬於window對象的方法
function foo(){
  setTimeout(function (){
    console.log(this) //window ,嚴格下window
  })
}
foo.call(5)
----------------------------------------------
// 函數內部的this就是指向調用者,而且能夠看出簡單調用的回調函數中的this也指向window
const obj = {
  foo(callback) {
    callback()
    console.log(this.foo === obj.foo) // true
    console.log(this === obj) // true
  }
}
obj.foo(function () {
  console.log(this) //window ,嚴格下undefined
})
----------------------------------------------
// 經過arguments調用的回調函數中的this指向調用者,注意嚴格與非嚴格下的arguments對象有所不一樣
const obj = {
  foo(callback) {
    arguments[0]()
    console.log(this.foo === obj.foo) // true
    console.log(this === obj) // true
  }
}
obj.foo(function () {
  console.log(this) //arguments對象 ,嚴格下 arguments對象
})
複製代碼

箭頭函數中的this

es6引入的箭頭函數,是不具備this綁定,不過在其函數體中能夠使用this,而這個this指向的是箭頭函數當前所處的詞法環境中的this對象,能夠理解爲,this在箭頭函數中是透明的,箭頭函數包不住this,因此函數內部與外部的this爲同一值

  • 判斷箭頭函數的this指向,咱們能夠把箭頭函數當作透明,其上下文中的this就是它的this
// 能夠看出箭頭函數中的this就是其所在環境的this,箭頭函數沒法固定this,由其環境決定
const foo = () => {
  console.dir(this) //window ,嚴格下仍是window
}
foo()
----------------------------------------------
// 可見對象中的this指向window,箭頭函數中的this指向對象中的this。因爲只有建立執行上下文才會綁定this指向,而除了全局上下文,只有函數做用域纔會建立上下文環境從而綁定this,建立對象不會綁定this,因此仍是全局this
const obj={
  this:this,
  foo:()=>{
    console.dir(this) //window ,嚴格下 window
  }
}
console.dir(obj.this) //window ,嚴格下 window
obj.foo()
---------------------------------------------
// 對象方法內部嵌套箭頭函數,則此箭頭函數的this屬於外部非箭頭函數this。當調用obj.foo時foo函數建立的執行上下文中的this綁定對象obj,而箭頭函數並不會綁定this,因此其this屬於foo下的this,即對象obj
const obj = {
  foo: function () {
    return () => {
      console.dir(this) //obj ,嚴格下 obj
    }
  }
}
obj.foo()()
複製代碼

如何改變函數的this指向

最簡單的方法經過apply、call、bind來給函數綁定this

  • apply方法中第一個參數爲被調用的函數中的this指向,傳入你想要綁定的this值便可,第二個參數爲被調用函數的參數集合,一般是個數組
  • call與apply方法基本一致,區別在於傳入參數形式不一樣,call傳入的參數爲可變參數列表,參數按逐個傳入
  • bind方法與以上不一樣的是不會直接調用函數,只是先綁定函數的this,到要使用的時候調用便可,此方法返回一個綁定this與參數以後的新函數,其傳入參數形式同call
  • 經過變量保留指定this來達到固定this
// 經過變量保留父級this,進行對_this變量修改也就達到修改原this的效果
const obj = {
  name: 'obj',
  foo: function () {
    let _this = this
    function boo() {
      _this.name = 'OBJ'
      console.dir(obj.name) // OBJ
    }
    return boo
  }
}
obj.foo()()
複製代碼

8、apply、call、bind實現

這3者的實現其實差很少,bind實現可能會有點不同,都要實現this的改變

手動實現apply

  • 思路就是想辦法使函數被傳入的thisArg調用,那麼函數的this就指向調用者
Function.prototype.Apply = function (thisArg, args = Symbol.for('args')) {
  console.dir(this)            //this爲這個方法的調用者=>foo函數
  const fn = Symbol('fn')      //生成一個不重複的鍵
  thisArg[fn] = this || window //把foo函數做爲傳入this的一個方法
  args === Symbol.for('args') 
  ? thisArg[fn]()
  : thisArg[fn](...args)       //調用這方法,傳參
  delete thisArg[fn]           //使用完刪除
}
var name = 'foo'
var age = 5
function foo(age,height) {
  console.log(this.name) // obj
  console.log(age)       // 3
  console.log(height)    // null
}
const obj = {
  name: 'obj',
  age: 3
}
foo.Apply(obj,[obj.age,null])
複製代碼

手動實現call

基本思路同apply,就是傳參形式改變一下,這裏經過arguments獲取參數列表

Function.prototype.Call = function (thisArg) {
  console.dir(this)            //this爲這個方法的調用者=>foo函數
  const fn = Symbol('fn')      //生成一個不重複的鍵
  thisArg[fn] = this || window //把foo函數做爲傳入this的一個方法
  const args = Array.from(arguments).slice(1)
  args.length ? thisArg[fn](...args) : thisArg[fn]()  //調用這方法,傳參
  delete thisArg[fn]           //使用完刪除
}
複製代碼

手動實現bind

bind函數要可以返回嚴格綁定this與參數後的函數,調用這個返回的函數時有可能還會傳入參數,那麼須要拼接參數

Function.prototype.Bind = function (thisArg) {
  const fn = Symbol('fn')       //生成一個不重複的鍵
  thisArg[fn] = this || window  //把foo函數做爲傳入this的一個方法
  const f = thisArg[fn]         // 負責一份函數
  delete thisArg[fn]            //刪除原來對象上的函數,可是保留了this指向
  const args = Array.from(arguments).slice(1)
  return function () {
    const arg = args.concat(...arguments)
    f(...arg)
  }
}
var name = 'foo'
var age = 5
var height = 4
function foo(age, height) {
  console.log(this.name)       // obj
  console.log(age)             // 3
  console.log(height)          // 2
}
const obj = {
  name: 'obj',
  age: 3
}
foo.Bind(obj, obj.age)(2)
複製代碼

9、同步與異步

同步

  • 基於js的單線程同時只能處理一件事情,而同步便是在主線程上排隊執行的任務,只有當前任務執行完成,纔會進入下一個任務。同步執行的函數會在預期獲得結果,也就是能夠清楚何時能獲得返回值
  • 全部同步代碼只會進入調用棧,同步代碼會阻塞主線程的執行,並且會優先與其餘非同步代碼執行

異步

  • 異步是指當前執行的代碼會進入異步線程處理以後纔會再由主線程處理回調
  • 異步的結果不是立刻可以獲得,而是會在未來的某個時間點獲取到
  • 一般異步代碼所要通過的步驟比同步代碼多,因爲異步代碼不是直接放在調用棧中執行,而是要派發(可能不須要)給其餘線程處理,等處理完成後的回調放在某個地方存儲(好比任務隊列),等到同步隊列執行完成以後纔會取回異步回調代碼進行執行

異步、單線程與EventLoop

先看一張圖,有個大致架構

事件循環

  • js主線程處理當前正在執行的代碼,它會執行當前調用棧棧頂的執行上下文,從堆空間(通常是存儲對象)和棧空間(通常存儲非對象值以及對象引用)取數據,進而處理當前調用棧所用到的數據
  • 全部的同步代碼會按照代碼順序壓入調用棧中等待主線程執行,若是代碼中遇到了異步代碼,則會根據異步類型拋給異步線程執行
  • 異步類型,主要分爲微任務與宏任務
  • 任務隊列其實本質就是一塊內存空間,裏面的任務是依據FIFO先進先出的規則來執行,全部異步代碼執行完畢的回調都是加入到異步任務隊列中等待主線程的調用
  • 異步能夠提升cpu的利用率
微任務
  • 微任務隊列與宏任務隊列的區別就在於,主線程對於其中的任務調度的區別,主進程會優先執行微任務隊列中的所有任務,當微任務中的所有任務執行完畢纔會進而轉到宏任務執行
  • 微任務能夠由這些方法關鍵字調用產生Promise、async、await、MutaionObserver、process.nextTick(Node.js環境)
  • 若是調用微任務方法時,方法內部包含其餘線程干預處理時,會拋給指定線程執行,而主線程繼續執行下面的代碼,等到其餘線程處理完成以後,若是有回調函數則會把回調加入到指定異步類型(這裏爲微任務隊列)的隊列中排隊等待主線程執行
  • 微任務與宏任務的主要區別在於,主線程優先執行所有微任務,待執行完成以後纔會挨個執行宏任務
宏任務
  • 通常的宏任務隊列存放的是WebApis的回調,WebApis中包含許多線程,GUI渲染線程(與js主線程互斥不能同時執行)、事件觸發線程、定時器線程、異步網絡請求線程
  • 宏任務存放由異步WebApis產生的回調函數,但優先級低於微任務
js單線程
  • js單線程設計之初就是爲了簡化代碼,解決DOM衝突,若是js爲多線程語言,那麼有可能產生多個線程同時操做DOM的狀況,那麼將會致使js操做同個DOM引發衝突,介於多線程的鎖機制來解決衝突,但又使得js的代碼複雜度提升
  • 基於js單線程的設計,進而引出異步執行的方式,使得js具備相似多線程程的效果,但無論異步仍是同步,js永遠都只有一個線程在執行
EventLoop
  • 事件循環機制是針對於主線程的調度方式
  • 能夠理解爲主線程在尋找任務執行的過程就是事件循環,其尋找方式就是調用機制
  • 先了解一下瀏覽器是如何執行js代碼的
    • 一般瀏覽器在最開始運行js代碼的入口就是html中的script標籤所涵蓋的代碼
    • 當GUI渲染線程解析到script標籤,則會把標籤所涵蓋的js代碼加入到宏任務隊列中
    • 首先js引擎(如V8引擎)先取第一個宏任務,即script的代碼塊,而後主線程在調用棧中解析js代碼
    • 等全部代碼解析完成以後開始運行js代碼
    • 若是遇到同步代碼直接執行
    • 遇到異步代碼,若是是宏任務類型即異步WebApis處理的異步代碼,那麼將會通知WebApis在對應的線程中處理異步任務,此時js主線程繼續執行下面的代碼,在其餘線程處理完畢以後若是有回調函數,則異步線程會將回調函數加入到宏任務隊列尾部,
    • 若是是微任務類型的異步代碼,也同宏任務處理,只不過是把回調函數加入到微任務隊列中,其執行的優先級高於宏任務隊列
    • 當同步代碼所有執行完成,主線程將會一直檢測任務隊列,若是有異步微任務則執行徹底部的微任務
    • 進一步執行瀏覽器渲染進程繪製頁面,以後就是開始下一輪的事件循環,就又回到取宏任務執行
    • 這裏注意,全部的微任務都是由宏任務中執行的代碼產生,一開始只有宏任務隊列有任務

如下展現的是事件循環大體流程

事件循環

如下爲主線程判斷邏輯

事件判斷

前端異步的場景

  • 前端異步主要用於代碼可能會發生等待,並且等待過程不能阻塞主線程運行的狀況
  • 一般WebApis接口都是異步調用的,因爲須要其餘線程的處理,就須要等待其返回結果,那麼js主線程就不必一直等待,這樣就須要使用異步來進行處理
  • 好比定時器任務setTimeout、setInterval、ajax請求、圖片動態加載、DOM事件觸發這些都屬於瀏覽器執行的異步任務;如js中的Promise、async、await屬於js語言自身的異步操做這些均可以實現異步
  • 當須要動態加載圖片的時候就須要用到異步;當須要執行的js的同步代碼須要長時間佔用的主線程時能夠使用異步方式拆分爲多個步驟執行,這樣能夠避免瀏覽器頁面長時間無響應或者卡頓
  • 當須要執行很長一段時間才能獲得結果的代碼時也能夠使用html5中的Web worker在瀏覽器渲染進程下新開一個線程用來專門執行此代碼,經過postMessage來返回運行結果這樣也不會佔用js主線程,可是這個線程沒法操做DOM和BOM

WebWorker多線程

  • 基於js單線程的侷限性,若是執行一個很耗時間的函數,那麼主線程將會被長時間佔用,所以致使事件循環暫停,使得瀏覽器沒法及時渲染和響應,那麼將會形成頁面崩潰,用戶體驗降低,因此html5支持了webworker
  • webwork簡單理解就是可讓特定的js代碼在其餘線程中執行,等執行結束後返回結果給主線程接收便可
  • 好比在js中須要實現一個識別圖片的算法,並且此算法須要很長的計算時間,若是讓js主線程來執行將會致使上述發生的事情,那麼正好能夠使用webwork技術來實現。
  • 建立一個webworker文件,其中寫入算法代碼,在最後調用postMessage(result)方法返回結果給主線程,js主代碼中經過w=new Worker(文件路徑)來建立一個渲染進程的webworker子線程實例,經過w.onmessage=function(e){console.log(e.data)}給其添加一個事件監聽器,當webworker中傳遞消息給js主線程時會在此回調函數中執行,經過調用w.terminate()終止webworker線程
  • webworker線程與js主線程最大的區別就在於webworker線程沒法操做window與document對象
// test.html(主線程)
const w= new Worker('postMessage.js')
w.onmessage=function(e){
  console.log(e.data);
}
w.postMessage('b') // b is cat
w.terminate() // 手動關閉子線程
----------------------------------------------
// postMessage.js(worker線程)
this.addEventListener('message', (e) => {
  switch (e.data) {
    case 'a': this.postMessage(e.data+' is tom')
      break;
    case 'b': this.postMessage(e.data + ' is cat')
      break;
    default:  this.postMessage(e.data + " i don't know")
    this.close() // 自身關閉
      break;
  }
})
複製代碼

10、AMD、CMD、CommonJS與ES6模塊化

模塊化的引入主要是用於解決命名衝突、代碼複用、代碼可讀性、依賴管理等

AMD異步模塊定義

  • AMD全稱Asynchronous Module Definition異步模塊定義
  • AMD並不是原生js支持,是RequireJS模塊化開發當中推廣的產物,AMD依賴於RequireJS函數庫,打包生成對應效果的js代碼
  • RequireJS主要用於解決多個js文件之間的依賴關係、瀏覽器加載大量js代碼致使無響應、異步加載模塊
  • RequireJS經過define(id?,dependencies?,factory)定義模塊,id可選,爲定義模塊的標識,默認爲模塊文件名不包括後綴,dependencies可選,是當前模塊依賴的模塊路徑數組,factory爲工廠方法,初始化模塊的函數或者對象,若是爲函數將會只執行一次,若是是對象將做爲模塊的輸出
  • 經過require(dependencies,factory)導入模塊,其中dependencies爲須要導入的模塊路徑數組,factory爲當模塊導入以後的回調函數,此函數的參數列表爲對應導入的模塊
  • 經過require.config(配置對象)配置各模塊路徑和引用名
require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "jquery.min",  //實際路徑爲js/lib/jquery.min.js
    "underscore": "underscore.min",
  }
})
複製代碼

CMD通用模塊定義

  • CMD全稱Common Module Definition通用模塊定義
  • 同AMD,CMD也有一個函數庫SeaJS與RequireJS相似的功能
  • CMD推崇一個文件一個模塊,推崇依賴就近,定義模塊define(id?,deps?,factory),id同AMD,deps通常不在其中寫依賴,而是在factory中在須要使用的時候引入模塊,factory函數接收3各參數,參數一require方法,用來內部引入模塊的時候調用,參數二exports是一個對象,用來向外部提供模塊接口,參數三module也是一個對象上面存儲了與當前模塊相關聯的一些屬性和方法
  • 經過seajs.use(deps,func)加載模塊,deps爲引入到模塊路徑數組,func爲加載完成後的回調函數

AMD、CMD的主要區別在於

AMD推崇依賴前置,在定義模塊的時候就要聲明其依賴的模塊 CMD推崇就近依賴,只有在用到某個模塊的時候再去require

CommonJS

  • CommonJS模塊規範,一般用於Nodejs中的模塊化
  • 擁有4個環境變量modul、exports、require、global
  • 經過module.exports(不推薦exports)導出模塊對象,經過require(模塊路徑)加載模塊
  • 當一個模塊同時存在exports和module.exports時後者覆蓋前者
  • 規範中__dirname表明當前模塊文件所在的文件夾路徑,__filename表明當前模塊文件夾路徑+文件名
  • CommonJS經過同步的方式加載模塊,其輸出的模塊是一個拷貝對象,因此修改原的模塊不會對被引入的模塊內部產生影響,且模塊在代碼運行的時候加載

ES6模塊化

  • es6引入的export與import用於解決js自身不具有模塊功能的缺陷
  • 經過export或者export default導出模塊接口,經過import xxx from '路徑',導入模塊
  • 對於export導出的接口能夠使用import {接口} from '路徑',經過解構的方式按需導入
  • 對於export default默認導出的,能夠使用import xxx from '路徑',來導入默認導出的接口,xxx能夠是自定義名稱,且一個模塊只能有一個默認導出,能夠有多個export
  • 還能夠經過別名的方式設置導出和導入的接口名,如export {a as foo},把foo做爲a的別名導出,import foo as b from 路徑,把b看成foo的別名導入
  • es6模塊是在代碼編譯時輸出接口即編譯時加載,es6是經過命令來指定導出和加載,且導出的是模塊中的只讀引用,若是原始模塊中的值被改變了,那麼加載的值也會隨之改變,因此是動態引用

11、script標籤之async與defer

使用async屬性

  • 若是script標籤設置了這個值,則說明引入的js須要異步加載和執行,注意此屬性只適用於外部引入的js
  • 在有async的狀況下腳本異步加載和執行,而且不會阻塞頁面加載,可是也並不會保證其加載的順序,若是多個async優先執行,則先加載好的js文件,因此使用此方式加載的js文件最好不要包含其餘依賴

使用defer屬性

  • 若是使用此屬性,也將會使js異步加載執行,且會在文檔被解析完成後執行,這樣就不會阻塞頁面加載,可是它將會按照原來的執行順序執行,對於有依賴關係的也可以使用
  • html4.0中定義了defer,html5.0中定義了async

不一樣狀況

  • 若是隻有async,那麼腳本在下載完成後異步執行。
  • 若是隻有defer,那麼腳本會在頁面解析完畢以後執行。
  • 若是都沒有,那麼腳本會在頁面中立刻解執行,中止文檔解析阻塞頁面加載
  • 若是都有那麼同async,固然此狀況通常用於html的版本兼容下,若是沒有async則defer生效
  • 不過仍是推薦直接把script標籤放在body底部

12、改變數組自己的api

  1. pop() 尾部彈出一個元素
  2. push() 尾部插入一個元素
  3. shift() 頭部彈出一個元素
  4. unshift() 頭部插入一個元素
  5. sort([func]) 對數組進行排序,func有2各參數,其返回值小於0,那麼參數1被排列到參數2以前,反之參數2排在參數1以前
  6. reverse() 原位反轉數組中的元素
  7. splice(pos,deleteCount,...item) 返回修改後的數組,從pos開始刪除deleteCount個元素,並在當前位置插入items
  8. copyWithin(pos[, start[, end]]) 複製從start到end(不包括end)的元素,到pos開始的索引,返回改變後的數組,淺拷貝
  9. arr.fill(value[, start[, end]]) 從start到end默認到數組最後一個位置,不包括end,填充val,返回填充後的數組

其餘數組api不改變原數組

十3、window之location、navigator

location對象

  • location爲全局對象window的一個屬性,且window.location===document.location,其中的屬性都是可讀寫的,可是隻有修改hrefhash纔有意義,href會從新定位到一個URL,hash會跳到當前頁面中的anchor名字的標記(若是有),並且頁面不會被從新加載
// 這行代碼將會使當前頁面重定向到http://www.baidu.com
window.location.href = 'http://www.baidu.com'
----------------------------------------------
// 若是使用hash而且配合input輸入框,那麼當頁面刷新以後,鼠標將會自動聚焦到對應id的input輸入框,
<input type="text" id="target">
<script>
  window.location.hash = '#target'
</script>
複製代碼

先看下其擁有的屬性

location屬性

這裏補充一個origin屬性,返回URL協議+服務器名稱+端口號 (location.origin == location.protocol + '//' + location.host)

  • 能夠經過上述屬性來獲取URL中的指定部分,或者修改href於hash達到從新定位與跳轉
  • 添加hash改變監聽器,來控制hash改變時執行的代碼
window.addEventListener("hashchange", funcRef);
// 或者
window.onhashchange = funcRef;
複製代碼

location方法

location方法

  • assign(url),經過調用window.location.assign方法來打開指定url的新頁面window.location.assign('http://www.baidu.com')在當前頁面打開百度,可回退
  • replace(url),在當前頁面打開指定url,不可回退
  • reload([Boolean]),調用此方法將會從新加載當前頁面,若是參數爲false或者不填,則會以最優的方式從新加載頁面,可能從緩存中取資源,若是參數爲true則會從服務器從新請求加載資源

navigator對象

  • window.navigator對象包含有關瀏覽器的信息,能夠用它來查詢一些關於運行當前腳本的應用程序的相關信息
document.write("瀏覽器的代碼名:" + navigator.appCodeName + "<br>");
document.write("瀏覽器的名稱:" + navigator.appName + "<br>");
document.write("當前瀏覽器的語言:" + navigator.browserLanguage + "<br>");
document.write("瀏覽器的平臺和版本信息:" + navigator.appVersion + "<br>");
document.write("瀏覽器中是否啓用 cookie :" + navigator.cookieEnabled + "<br>");
document.write("運行瀏覽器的操做系統平臺 :" + navigator.platform + "<br>");
複製代碼
  • navigator.appCodeName 只讀,任何瀏覽器中,老是返回 'Gecko'。該屬性僅僅是爲了保持兼容性。
  • navigator.appName 只讀,返回瀏覽器的官方名稱。不要期望該屬性返回正確的值。
  • navigator.appVersion 只讀,返回一個字符串,表示瀏覽器的版本。不要期望該屬性返回正確的值。
  • navigator.platform 只讀,返回一個字符串,表示瀏覽器的所在系統平臺。
  • navigator.product 只讀,返回當前瀏覽器的產品名稱(如,"Gecko")。
  • navigator.userAgent 只讀,返回當前瀏覽器的用戶代理字符串(user agent string)

以下在不一樣瀏覽器打印的信息

/*
chrome:
    Mozilla/5.0
    (Macintosh; Intel Mac OS X 10_12_6)
    AppleWebKit/537.36 (KHTML, like Gecko)
    Chrome/61.0.3163.91 Safari/537.36
safari:
    Mozilla/5.0
    (Macintosh; Intel Mac OS X 10_12_6)
    AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0
    Safari/604.1.38
ios11劉海X:
    Mozilla/5.0
    (iPhone; CPU iPhone OS 11_0 like Mac OS X)
    AppleWebKit/604.1.38 (KHTML, like Gecko)
    Version/11.0 Mobile/15A372 Safari/604.1
ipad:
    Mozilla/5.0
    (iPad; CPU OS 9_1 like Mac OS X)
    AppleWebKit/601.1.46 (KHTML, like Gecko)
    Version/9.0 Mobile/13B143 Safari/601.1
galxy sansum:
    Mozilla/5.0
    (Linux; Android 5.0; SM-G900P Build/LRX21T)
    AppleWebKit/537.36 (KHTML, like Gecko)
    Chrome/61.0.3163.91 Mobile Safari/537.36
安裝uc瀏覽器:
    Mozilla/5.0
    (Linux; U; Android 6.0.1; zh-CN; Mi Note 2 Build/MXB48T)
    AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0
    Chrome/40.0.2214.89 UCBrowser/11.4.9.941 Mobile Safari/537.36
winphone:
    Mozilla/5.0
    (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E)
    AppleWebKit/537.36 (KHTML, like Gecko) 
    Chrome/61.0.3163.91 Mobile Safari/537.36
hybrid方法的可能:
    Mozilla/5.0
    (iPhone; CPU iPhone OS 11_0 like Mac OS X)
    AppleWebKit/604.1.38 (KHTML, like Gecko)
    Mobile/15A372 weibo/80011134
*/
複製代碼

十4、ajax與fetch

ajax

  • ajax全稱Asynchronous JavaScript And XML也就是異步js與xml,它可讓頁面在不刷新的狀況下發起請求獲取數據
  • 使用window.XMLHttpRequest構造器實例化一個網絡請求對象const XHR = new XMLHttpRequest()
  • XHR.open(method, url, [ async, [ user, [ password]]])此方法用來發送一個請求,method爲請求方法,url爲請求地址,async爲boolean值默認爲true即便用異步請求,user和password在請求須要用戶和密碼的時候使用
  • XHR.send(body)參數爲發生請求主體內容,其格式能夠爲FormData、ArrayBuffer、Document、序列化字符串,在收到響應後,響應的數據會自動填充XHR對象的屬性
  • 當須要設置請求頭時能夠調用XHR.setRequestHeader(header,value)設置請求頭的類型與值,當以post方式發起請求就用設置XHR.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')此請求頭,值可更改
  • 經過監聽實例的onreadystatechange屬性方法,當readyState的值改變的時候會觸發onreadystatechange對應的回調函數XHR.onreadystatechange = function () { }
  • 請求狀態readyState有5個值,對應5個請求狀態,只讀
    • 0 表示 請求還未初始化,還沒有調用 open() 方法。
    • 1 表示 已創建服務器連接,open() 方法已經被調用。
    • 2 表示 請求已接受,send() 方法已經被調用,而且頭部和狀態已經可得到。
    • 3 表示 正在處理請求,下載中; responseText 屬性已經包含部分數據。
    • 4 表示 完成,下載操做已完成。
  • 還有status屬性,它是此次請求中的響應數字狀態碼,即爲咱們平時看到的1xx、2xx、3xx、4xx、5xx表示這次請求的狀態結果,在還未發起請求和出錯時都爲0,只讀
  • XHR.responseText屬性爲這次響應的數據,爲字符串,多是JSON格式須要JSON.parse解析
  • XHR.responseXML屬性爲xml形式的數據,能夠經過XHR.responseType = 'document'XHR.overrideMimeType('text/xml')來解析爲XML
  • XHR.withCredentials屬性設置爲boolean值,經過此屬性來設置是否使用cookies、authorization等憑證字段
  • XHR.timeout經過此屬性來設置請求超時時間
  • XHR.ontimeout經過此屬性來設置請求超時的回調函數,函數的參數爲事件對象
  • XHR.abort()此方法用來終止網絡請求
  • XHR.getAllResponseHeaders()此方法用來獲取全部的響應頭
  • XHR.getResponseHeader(name)此方法用來獲取指定的響應頭
  • 還有6個關於進度的事件
    • loadstart 在收到響應的第一個字節觸發
    • progress 在接收期間不斷觸發
    • error 發生錯誤
    • abort 調用abort方法而終止
    • load 接收到完整數據,可代替readystatechange與readyState判斷
    • loadend 在通訊完成或abort error load事件後觸發
  • 經過XHR.addEventListener(eventname,callback)方法添加對應的事件監聽,其回調函數接收一個事件對象參數
  • progress事件對象有3個屬性用於查看當前進度相關信息,lengthComputable爲boolean值,表示進度是否可用,position表示已經接收的字節數,totalSize表示總須要傳輸的內容長度即Content-Length字節數,一般在分片傳輸內容的時候用到

簡單的發起一次請求

// 最簡單的發起一個請求
const XHR = new XMLHttpRequest()
XHR.open('get','http://127.0.0.1:3000/test?key=value')
XHR.send()
XHR.addEventListener('load',(e)=>{
  // 服務端返回的是查詢參數
  console.log(XHR.response) // {"key":"value"}
})
複製代碼

基於XMLHttpRequest封裝一個請求方法

// 發送的數據
const data = {
  name: 'tom'
}
// 請求配置
const config = {
  type: "post",
  url: "http://127.0.0.1:3000/test",
  data: data,
  dataType: 'application/json',
  success: function (res) {
    console.log(res);
  },
  error: function (e) {
    console.log(e);
  }
}
// 請求構造器
function Ajax(conf) {
  this.type = conf.type || 'get'
  this.url = conf.url || ''
  this.data = conf.data || {}
  this.dataType = conf.dataType || ''
  this.success = conf.success || null
  this.error = conf.error || null
}
// send方法
Ajax.prototype.send = function () {
  if (this.url === '') return
  const XHR = new XMLHttpRequest()
  XHR.addEventListener('load', () => {
    if (XHR.status >= 200 && XHR.status < 300 || XHR.status == 304) {
      typeof this.success === 'function' && this.success(XHR.response)
    }
  })
  XHR.addEventListener('error', (e) => {
    typeof this.error === 'function' && this.error(e)
  })
  if (this.type.toLowerCase() === 'get') {
    XHR.open('get', this.url)
    XHR.send(null)
  } else {
    XHR.open(this.type, this.url)
    XHR.setRequestHeader('Content-Type', this.dataType || 'application/x-www-form-urlencoded')
    let data = this.data
    if (this.dataType === 'application/json') {
      data = JSON.stringify(this.data)
    }
    XHR.send(data)
  }
}
// 發送請求
const ajax = new Ajax(config).send()
複製代碼

因爲網絡請求模塊封裝較繁瑣,這裏就簡單封裝了一下,僅供參考(。^▽^)

fetch

  • fetch API提供了js接口,用於替代XMLHttpRequest方式的網絡請求,fetch()全局方法使用起來比XHR更加方便
  • fetch方法接受2個參數,參數1爲請求url或 Request 對象,參數2爲可選配置對象
// fetch方法返回一個Promise對象,可用then方法接收結果,用catch方法捕獲異常,同Promise使用
// 配置對象具體配置
const config = {
  method: 'GET',      // 請求方法
  headers: {          // 頭信息
    'user-agent': 'Mozilla/4.0 MDN Example',
    'content-type': 'application/json'
  },
  body: JSON.stringify({  // 請求的 body 信息,Blob, FormData 等
    data: 1
  }),
  mode: 'cors',             // 請求的模式,cors、 no-cors 或 same-origin
  credentials: 'include',   // omit、same-origin 或 include。爲了在當前域名內自動發送 cookie, 必須提供這個選項
  cache: 'no-cache',        // default 、 no-store 、 reload 、 no-cache 、 force-cache 或者 only-if-cached
  redirect: 'follow',       // 可用的 redirect 模式: follow (自動重定向), error (若是產生重定向將自動終止而且拋出一個錯誤), 或者 manual (手動處理重定向).
  referrer: 'no-referrer',  // no-referrer、client或一個 URL。默認是 client。
  referrerPolicy: 'no-referrer', // 指定 referer HTTP頭
  integrity: 'sha256-BpfBw7ivV8q2jLiT13fxDYAe2tJllusRSZ273h2nFSE=', // 包括請求的  subresource integrity 值
}
// 發起請求
fetch('http://biadu.com' [, config])
複製代碼
  • then的回調函數接受一個Response對象參數,其對象擁有9個屬性,8個方法

  • 9個屬性

    • type 只讀 包含Response的類型 (例如, basic, cors)
    • url 只讀 包含Response的URL
    • useFinalURL 包含了一個布爾值來標示這是不是該Response的最終URL
    • status 只讀 包含Response的狀態碼
    • ok 只讀 包含了一個布爾值來標示該Response成功(狀態碼200-299)
    • redirected 只讀 表示該Response是否來自一個重定向,若是是的話,它的URL列表將會有多個
    • statusText 只讀 包含了與該Response狀態碼一致的狀態信息
    • headers 只讀 包含此Response所關聯的Headers 對象
    • bodyUsed Body 只讀 包含了一個布爾值來標示該Response是否讀取過Body
  • 8個方法

    • clone 建立一個Response對象的克隆
    • error 返回一個綁定了網絡錯誤的新的Response對象
    • redirect(url, status) 用另外一個URL建立一個新的 response
    • arrayBuffer 接受一個 Response 流, 並等待其讀取完成. 並 resolve 一個 ArrayBuffer 對象
    • blob blob()方法使用一個 Response 流,並將其讀取完成
    • formData 將 Response 對象中的所承載的數據流讀取並封裝成爲一個對象
    • json 使用一個 Response 流,並將其讀取完成。解析結果是將文本體解析爲 JSON
    • text 提供了一個可供讀取的"返回流", 它返回一個包含USVString對象,編碼爲UTF-8

十5、WebSocket

  • WebSocket是一種在單個TCP鏈接上進行全雙工通訊的協議,即鏈接雙方能夠同時實時收發數據,它能夠在用戶的瀏覽器和服務器之間打開雙工、雙向通信會話。
  • WebSocket API提供全局方法WebSocket(url[, protocols])建立實例,參數1 對方絕對url其url以ws://或者wss://(加密)開頭,參數2 protocols是單協議或者包含協議的字符串數組
// 必須傳入絕對URL,能夠是任何網站
const s = new WebSocket('ws://www.baidu.com') 
s.readyState    // 0 創建鏈接 1 已經創建 2 正在關閉 3 鏈接已關閉或者沒有連接成功
s.send('hello') // 發送的數據必須是純文本
s.onopen = function () {}
s.onerror = function () {}
s.onmessage = function (event) {
  // 當接收到消息時
  console.log(event.data) // 數據是純字符
}
s.close()   // 關閉鏈接
s.onclose = function (event) {
  /*
    * event.wasClean 是否明確的關閉 
    * event.code 服務器返回的數值狀態碼
    * event.reason 字符串,服務器返回的消息
    */
}
複製代碼
  • 10個屬性
    • binaryType 返回websocket鏈接所傳輸二進制數據的類型(blob, arraybuffer)
    • bufferedAmount 只讀 返回已經被send()方法放入隊列中但尚未被髮送到網絡中的數據的字節數。一旦隊列中的全部數據被髮送至網絡,則該屬性值將被重置爲0。可是,若在發送過程當中鏈接被關閉,則屬性值不會重置爲0。
    • extensions 只讀 返回服務器選擇的擴展名。這當前只是空字符串或鏈接協商的擴展列表
    • onclose 用於指定鏈接失敗後的回調函數
    • onmessage 用於指定當從服務器接受到信息時的回調函數
    • onopen 用於指定鏈接成功後的回調函數
    • protocol 只讀 服務器選擇的下屬協議
    • readyState 只讀 當前的連接狀態,共4個
      • 0 創建鏈接
      • 1 已經鏈接
      • 2 正在關閉
      • 3 鏈接已經關閉或者沒有鏈接成功
    • url 只讀 WebSocket 的絕對路徑
  • 2個方法
    • close(code, reason) 數字狀態碼 可選 默認 1005和一個可選的類可讀的字符串,它解釋了鏈接關閉的緣由。
    • send(data) 向服務器發送數據(ArrayBuffer,Blob等)

十6、短輪詢、長輪詢與WebSocket

短輪詢

  • http 短輪詢是server收到請求無論是否有數據到達都直接響應http請求,服務端響應完成,就會關閉這個TCP鏈接;若是瀏覽器收到的數據爲空,則隔一段時間,瀏覽器又會發送相同的http請求到server以獲取數據響應
  • 缺點:消息交互的實時性較低(server端到瀏覽器端的數據反饋效率低)

簡單演示

const xhr = new XMLHttpRequest()
// 每秒發送一次短輪詢
const id = setInterval(() => {
  xhr.open('GET', 'http://127.0.0.1:3000/test?key=value')
  xhr.addEventListener('load', (e) => {
    if (xhr.status == 200) {
      // 處理數據
      console.log(xhr.response)
      // 若是不須要能夠關閉
      clearInterval(id)
    }
  })
  xhr.send()
}, 1000)
複製代碼

長輪詢

  • http 長輪詢是server收到請求後若是有數據,馬上響應請求;若是沒有數據就會停留一段時間,這段時間內,若是server請求的數據到達(如查詢數據庫或數據的邏輯處理完成),就會馬上響應;若是這段時間事後,尚未數據到達,則以空數據的形式響應http請求;若瀏覽器收到的數據爲空,會再次發送一樣的http請求到server
  • 缺點:server 沒有數據到達時,http鏈接會停留一段時間,這會形成服務器資源浪費

簡單演示

function ajax() {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', 'http://127.0.0.1:3000/test?key=value');
  xhr.addEventListener('load', (e) => {
    if (xhr.status == 200) {
      // 處理數據
      console.log(xhr.response)
      // 若是不須要能夠關閉
      if (xhr.response != '') return
      ajax()
    }
  })
  xhr.send();
}
複製代碼

相同點

  • 當server的數據不可達時,基於http長輪詢和短輪詢的http請求,都會停留一段時間
  • 都是用於實時從服務器獲取數據更新

不一樣點

  • http長輪詢是在服務器端的停留,而http短輪詢是在瀏覽器端的停留
  • 短輪詢隔一段時間向服務器發起請求,無論服務器數據有沒有變化都直接返回結果,長輪詢則在服務器數據有發生變化的時候才返回結果,若是在必定時間沒有變化那麼將會超時自動關閉鏈接

輪詢對比

Web Socket

  • 爲了解決http無狀態,被動性,以及輪詢問題,html5新推出了websocket協議,瀏覽器和服務器只需完成一次握手,二者便可創建持久性鏈接,並進行雙向通訊
  • 基於http進行握手,發生加密數據,保持鏈接不斷開
  • 優勢:
    • 較少的控制開銷,在進行客戶端與服務器的數據交換時,用於協議控制的數據包頭較小
    • 更強的實時性,全雙工通訊,沒必要侷限於一方發起的請求,服務器與客戶端能夠隨時發送數據,延遲更少
    • 有狀態的鏈接,websocket在通訊以前須要雙方創建鏈接,才能進行通訊,而http協議在每次請求都要攜帶狀態信息
    • 基於二進制數據傳輸,websocket定義了二進制幀,能夠處理二進制內容,相比於文本傳輸,提升了效率
    • 支持自定義子協議,能夠自行擴展協議,如部分瀏覽器支持壓縮等
    • 更好的壓縮效果,Websocket在適當的擴展支持下,能夠沿用以前內容的上下文,在傳遞相似的數據時,能夠顯著地提升壓縮率

十7、長鏈接與短鏈接

短鏈接

  • HTTP/1.0中默認使用短鏈接,也就是說,客戶端和服務器每進行一次HTTP操做,就創建一次鏈接,任務結束就中斷鏈接
  • 當客戶端瀏覽器訪問的某個HTML或其餘類型的Web頁中包含有其餘的Web資源(如JavaScript文件、圖像文件、CSS文件等),每遇到這樣一個Web資源,瀏覽器就會從新創建一個HTTP會話
  • 短鏈接的操做步驟是:創建鏈接——數據傳輸——關閉鏈接...創建鏈接——數據傳輸——關閉鏈接
  • 像WEB網站的http服務通常都用短鏈接,併發量大,但每一個用戶無需頻繁操做狀況下需用短鏈接

長鏈接

  • 從HTTP/1.1起,默認使用長鏈接,用以保持鏈接特性。使用長鏈接的HTTP協議,會在響應頭加入這行代碼Connection:keep-alive
  • 在使用長鏈接的狀況下,當一個網頁打開完成後,客戶端和服務器之間用於傳輸HTTP數據的TCP鏈接不會關閉,客戶端再次訪問這個服務器時,會繼續使用這一條已經創建的鏈接
  • keep-alive不會永久保持鏈接,它有一個保持時間,能夠在不一樣的服務器軟件(如Apache)中設定這個時間。實現長鏈接須要客戶端和服務端都支持長鏈接
  • 長鏈接的操做步驟是:創建鏈接——數據傳輸...(保持鏈接)...數據傳輸——關閉鏈接
  • 長鏈接多用於操做頻繁,點對點的通信,並且鏈接數不能太多狀況

長短輪詢和長短鏈接區別

  • HTTP協議的長鏈接和短鏈接,實質上是TCP協議的長鏈接和短鏈接
  • 長短鏈接經過雙方請求響應頭是否設置Connection:keep-alive來決定使用,而是否輪詢,是根據服務端的處理方式來決定的,與客戶端沒有關係
  • 實現方式不一樣,長短鏈接經過協議來實現,而長短輪詢經過服務器編程手動實現

十8、存儲

Cookie

  • cookie是由服務器發送給客戶端用於存儲少許信息,以鍵值對形式存儲{key:value}

cookie原理

  • 客戶端請求服務器時,若是服務器須要記錄該用戶狀態,就使用response向客戶端瀏覽器頒發一個Cookie。而客戶端瀏覽器會把Cookie保存起來。當瀏覽器再請求 服務器時,瀏覽器把請求的網址連同該Cookie一同提交給服務器。服務器經過檢查該Cookie來獲取用戶狀態
  • cookie是不可跨域,可是隻在域名不一樣的狀況下不支持跨域,忽略協議與端口,https://localhost:80/http://localhost:8080/的Cookie是共享的,能夠經過domain設置域,path設置域下的共享路徑
  • cookie屬性
    • name 表示設置的cookie名也就是key,不能重複,不可更改
    • value 表示設置cookie的值
    • domain 表示cookie綁定的域名,默認綁定當前域,多級域名不可交換cookie,若是設置以點開頭的域名,則全部子域名能夠訪問,如設置.baidu.com,則a.baidu.com可訪問其上級域名的cookie
    • path 表示cookie所能使用的路徑,默認'/'路徑,只要知足當前匹配路徑以及子路徑均可以共享cookie
    • maxAge 表示cookie失效時間,單位秒,正數爲失效時間,負數表示當前cookie在瀏覽器關閉時失效,0表示刪除cookie
    • secure 表示cookie是否使用安全協議傳輸如HTTPS、SSL,默認不使用,只在HTTPS等安全協議下有效,這個屬性並不能對客戶端的cookie進行加密,不能保證絕對的安全性
    • version 當前cookie使用的版本號,0 表示遵循Netscape的Cookie規範(多數),1表示遵循W3C的RFC2109規範(較嚴格),默認爲0
    • same-site 規定瀏覽器不能在跨域請求中攜帶 Cookie,減小CSRF攻擊
    • HttpOnly 若是這個屬性設置爲true,就不能經過js腳原本獲取cookie的值,用來限制非HTTP協議程序接口對客戶端Cookie進行訪問,能夠有效防止XSS攻擊(跨站腳本攻擊,代碼注入攻擊)
  • 前端經過document.cookie對cookie進行讀寫操做
  • 建立cookie就是後端的事情了

Session

  • session 表示服務器與客戶端的一次會話過程,session對象存儲特定用戶的屬性及配置信息
  • 當用戶在應用程序的 Web 頁之間跳轉時,存儲在session 對象中的變量將不會丟失,而是在整個用戶會話中一直存在下去。當客戶端關閉會話,或者 session 超時失效時會話結束

session

  • 用戶第一次請求服務器的時候,服務器根據用戶提交的相關信息,建立建立對應的 session ,請求返回時將此 session 的惟一標識信息 sessionID 返回給瀏覽器,瀏覽器接收到服務器返回的 sessionID 信息後,會將此信息存入到 Cookie 中,同時 Cookie 記錄此 sessionID 屬於哪一個域名
  • 當用戶第二次訪問服務器的時候,請求會自動判斷此域名下是否存在 Cookie 信息,若是存在自動將 Cookie 信息也發送給服務端,服務端會從 Cookie 中獲取 sessionID,再根據 sessionID 查找對應的 session 信息,若是沒有找到說明用戶沒有登陸或者登陸失效,若是找到 session 證實用戶已經登陸可執行後面操做
  • session 的運行依賴 session id,而 session id 是存在 Cookie中的

cookie與session的區別

  • cookie數據存放在客戶的瀏覽器上,session數據放在服務器上
  • cookie不是很安全,別人能夠分析存放在本地的cookie並進行cookie欺騙,考慮到安全應當使用session。用戶驗證這種場合通常會用 session
  • session保存在服務器,客戶端不知道其中的信息;反之,cookie保存在客戶端,服務器可以知道其中的信息
  • session會在必定時間內保存在服務器上,當訪問增多,會比較佔用你服務器的性能,考慮到減輕服務器性能方面,應當使用cookie
  • session中保存的是對象,cookie中保存的是字符串
  • session不能區分路徑,同一個用戶在訪問一個網站期間,全部的session在任何一個地方均可以訪問到,而cookie中若是設置了路徑參數,那麼同一個網站中不一樣路徑下的cookie互相是訪問不到的
  • session: 是在服務端保存的一個數據結構,用來跟蹤用戶的狀態,這個數據能夠保存在集羣、數據庫、文件中
  • cookie: 是客戶端保存用戶信息的一種機制,用來記錄用戶的一些信息,也是實現session的一種方式

本地存儲localStorage與sessionStorage

localStorage

  • localStorage瀏覽器api,用於存儲本地數據,可持久化,永不過時,除非主動刪除

基本使用

localStorage.setItem("b", "isaac");  //設置b爲"isaac"
localStorage.getItem("b");           //獲取b的值,爲"isaac"
localStorage.key(0);                 //獲取第0個數據項的鍵名,此處即爲「b」
localStorage.removeItem("b");        //清除c的值
localStorage.clear();                //清除當前域名下的全部localStorage數據
複製代碼
  • localStorage只要在相同的協議、相同的主機名、相同的端口下,就能讀取/修改到同一份localStorage數據,通常用於跨頁面共享數據
  • 可經過window.addEventListener("storage", function(e){}設置localStorage事件監聽,當存儲區域的內容發生改變時,將會調用回調

sessionStorage

  • sessionStorage用於本地存儲一個會話(session)中的數據,這些數據只有在同一個會話中的頁面才能訪問而且當會話結束後數據也隨之銷燬。所以sessionStorage不是一種持久化的本地存儲,僅僅是會話級別的存儲
sessionStorage.setItem(name, num);    //存儲數據
sessionStorage.setItem('value2', 119);
sessionStorage.valueOf();             //獲取所有數據
sessionStorage.getItem(name);         //獲取指定鍵名數據
sessionStorage.sessionData;           //sessionStorage是js對象,也能夠使用key的方式來獲取值
sessionStorage.removeItem(name);      //刪除指定鍵名數據
sessionStorage.clear();
複製代碼
  • 使用方式與localStorage相似
  • 僅在當前網頁會話下有效,關閉頁面或瀏覽器後就會被清除
  • 主要用於存儲當前頁面獨有的數據,不與瀏覽器其餘頁面共享

區別

  • 數據存儲方面
    • cookie數據始終在同源的http請求中攜帶(即便不須要),即cookie在瀏覽器和服務器間來回傳遞。cookie數據還有路徑(path)的概念,能夠限制cookie只屬於某個路徑下
    • sessionStorage和localStorage不會自動把數據發送給服務器,僅在本地保存。
  • 存儲數據大小
    • 存儲大小限制也不一樣,cookie數據不能超過4K,同時由於每次http請求都會攜帶cookie、因此cookie只適合保存很小的數據,如會話標識。
    • sessionStorage和localStorage雖然也有存儲大小的限制,但比cookie大得多,能夠達到5M或更大
  • 數據存儲有效期
    • sessionStorage:僅在當前瀏覽器窗口關閉以前有效; localStorage:始終有效,窗口或瀏覽器關閉也一直保存,本地存儲,所以用做持久數據;
    • cookie:只在設置的cookie過時時間以前有效,即便窗口關閉或瀏覽器關閉
  • 做用域不一樣
    • sessionStorage不在不一樣的瀏覽器窗口中共享,即便是同一個頁面;
    • localStorage在全部同源窗口中都是共享的;也就是說只要瀏覽器不關閉,數據仍然存在
    • cookie: 也是在全部同源窗口中都是共享的.也就是說只要瀏覽器不關閉,數據仍然存在

storage

十9、跨域

jsonp

  • jsonp是一種跨域通訊手段,經過script標籤的src屬性實現跨域,因爲瀏覽器同源策略,並不會截斷script的跨域響應
  • 經過將前端方法名做爲參數傳遞到服務器端,而後由服務器端注入參數以後再返回,實現服務器端向客戶端通訊
  • 因爲使用script標籤的src屬性,所以只支持get方法

來實現一下吧

// 前端準備
// 定義回調函數
function fn(arg) {
  // arg爲服務端傳來的數據
  console.log(`客戶端獲取的數據:${arg}`)
}
// 建立script標籤
const s = document.createElement('script')
// 給script標籤的src屬性賦值,值爲請求url,查詢參數callback,需與後端對應
// fn爲前端回調函數名
s.src = `http://127.0.0.1:3000/test?callback=fn`
// 向html添加此標籤,添加完成以後瀏覽器自動請求script的src對應的網址
document.getElementsByTagName('head')[0].appendChild(s);
// 等待瀏覽器收到響應以後,將會自動執行響應內容的代碼
----------------------------------------------
// 後端準備
// nestjs(ts)處理
@Controller('test') //api
export class TestController {
  @Get() //get方式請求
  //取url中的查詢參數,即?以後的鍵值對,鍵與值對應query對象參數的鍵與值
  callback(@Query() query) {  
    // 返回的數據
    const data = '我是服務端返回的數據';
    // 取查詢參數,這裏的callback要與前端?以後的鍵名一致,fn即fn函數名
    const fn = query.callback;
    // 返回結果,格式:函數名(服務器的數據),注意這裏須要序列化成字符串,若是參數自己是字符串那麼要加引號,前端並不知道data是字符串
    return `${fn}('${data}')`;
  }
}

// express(js)處理,同上
router.get('/test', async (req, res) => {
  const data = '我是服務器返回的數據'
  // req.query爲查詢參數列表
  const fn = req.query.callback
  // 返回數據
  res.send(`${fn}('${data}')`)
})
複製代碼

響應內容

jsonp響應

CORS

  • 跨域資源共享cors,它使用額外的 HTTP 頭來告訴瀏覽器,讓運行在一個 origin (domain) 上的Web應用被准許訪問來自不一樣源服務器上的指定的資源
  • 須要服務端與客戶端同時支持cors跨域方式才能進行跨域請求,服務端經過設置Access-Control-Allow-Origin:*便可開啓cors容許跨域請求,使用通配符*表示容許全部不一樣域的源訪問資源,也可單獨設置指定容許的源域名
  • 使用cors跨域時,將會在發起請求時出現2種狀況:
  • 簡單請求,需知足如下條件
    • 使用get、head、post方式發起的請求
    • Content-Type 的值僅限於下列三者之一:
      • text/plain
      • multipart/form-data
      • application/x-www-form-urlencoded
    • 不知足這些條件即爲預檢請求
  • 預檢請求
    • 需預檢的請求要求必須首先使用OPTIONS方法發起一個預檢請求到服務器,以獲知服務器是否容許該實際請求
    • 預檢請求的使用,能夠避免跨域請求對服務器的用戶數據產生未預期的影響
    • 當知足如下條件之一,將會發送預檢請求
      • 使用了下面任一 HTTP 方法:
      • PUT
      • DELETE
      • CONNECT
      • OPTIONS
      • TRACE
      • PATCH
    • 人爲設置了對 CORS 安全的首部字段集合以外的其餘首部字段。該集合爲:
      • Accept
      • Accept-Language
      • Content-Language
      • Content-Type (須要注意額外的限制)
      • DPR
      • Downlink
      • Save-Data
      • Viewport-Width
      • Width
    • Content-Type 的值不屬於下列之一:
      • application/x-www-form-urlencoded
      • multipart/form-data
      • text/plain
    • 知足以上條件之一將會發起預檢請求,總共會發起2次請求,第一次爲OPTIONS方式的請求,用來肯定服務器是否支持跨域,若是支持,再發起第二次實際請求,不然不發送第二次請求

postMessage

  • postMessage可用於不一樣頁面之間的跨域傳遞數據
  • postMessage(data,origin[, source])data爲發送的數據只能發送字符串信息,origin發送目標源,指定哪些窗口能接收到消息事件,若是origin設置爲*則表示無限制,source爲發送消息窗口的window對象引用,
<!-- test.html -->
<iframe src="http://127.0.0.1:5501/postMessage.html"
name="postIframe" onload="messageLoad()"></iframe>
<script>
// 定義加載以後執行的函數,給postMessage.html發送數據
function messageLoad() {
  const url = 'http://127.0.0.1:5501/postMessage.html'
  window.postIframe.postMessage('給postMessage的數據', url)
}
// 用於監聽postMessage.html的回饋,執行回調
window.addEventListener('message', (event) => {
  console.log(event.data);
})
</script>
----------------------------------------------
<!-- postMessage.html -->
<script>
  // 監聽test.html發來的數據,延遲1秒返回數據
  window.addEventListener('message', (event) => {
    setTimeout(() => {
      event.source.postMessage('給test的數據', event.origin)
    },1000)
  })
</script>
複製代碼
  • event對象的幾個重要屬性
    • data 指的是從其餘窗口發送過來的消息對象
    • type 指的是發送消息的類型
    • source 指的是發送消息的窗口對象
    • origin 指的是發送消息的窗口的源

window.name

  • 因爲window.name屬於全局屬性,在html中的iframe加載新頁面(能夠是跨域),經過iframe設置的src指向的源中更改name的值,同時主頁面中的name也隨之更改,可是須要給iframe中的window設置爲about:blank或者同源頁面便可
  • iframe使用以後應該刪除,name的值只能爲string類型,且數據量最大支持2MB
<!-- test.html -->
// 封裝應該用於獲取數據的函數
function foo(url, func) {
  let isFirst = true
  const ifr = document.createElement('iframe')
  loadFunc = () => {
    if (isFirst) {
      // 設置爲同源
      ifr.contentWindow.location = 'about:blank'
      isFirst = false
    } else {
      func(ifr.contentWindow.name)
      ifr.contentWindow.close()
      document.body.removeChild(ifr)
    }
  }
  ifr.src = url
  ifr.style.display = 'none'
  document.body.appendChild(ifr)
  // 加載以後的回調
  ifr.onload = loadFunc
}
foo(`http://127.0.0.1:5501/name.html`, (data) => {
  console.log(data) //
})
----------------------------------------------
<!-- name.html -->
const obj = { name: "iframe" }
// 修改name的值,必須爲string類型
window.name = JSON.stringify(obj);
複製代碼

document.domain

  • document.domain的值對應當前頁面的域名
  • 經過對domain設置當前域名來實現跨域,不過僅限於域名不一樣,可是又要屬於同一個基礎域名下,如http://a.baidu.comhttp://b.baidu.com這2個子域名之間才能使用domain跨域,通常用於子域名之間的跨域訪問
  • domain只能賦值爲當前域名或者其基礎域名,即上級域名
<!-- test.html -->
<script>
document.domain = 'baidu.com';
const ifr = document.createElement('iframe');
ifr.src = 'a.baidu.com/test.html';
ifr.style.display = 'none';
document.body.appendChild(ifr);
ifr.onload = function(){
  var doc = ifr.contentDocument || ifr.contentWindow.document;
  // 此處便可操做domain.html的document
  ifr.onload = null;
};
</script>
----------------------------------------------
<!-- domain.html -->
<script>
  // domain.html下設置爲與test.html中的domain一致
  document.domain = 'baidu.com';
</script>
複製代碼
  • 主要就是經過設置爲同源域名(只能爲其基礎域名),經過iframe操做另外一個頁面的內容

nginx反向代理

  • nginx反向代理,代理從客戶端來的請求,轉發到其代理源
  • 經過配置nginx的配置文件實現代理到不一樣源
// nginx.conf配置
server {
  listen 80;  // 監聽端口
  server_name  www.baidu.com; // 匹配來源
  location / {  //匹配路徑
    // 反向代理到http://127.0.0.1:3000
    proxy_pass http://127.0.0.1:3000;
    // 默認入口文件
    index  index.html index.htm index.jsp;
}
複製代碼
  • nginx反向代理還能實現負載均衡

二10、setTimeout與setInterval

setTimeout

  • setTimeout屬於webApi的一部分,能夠實現延時調用,屬於異步宏任務,一次性使用
  • setTimeout(func|code, [delay], [arg1], [arg2], ...) 參數1爲想要執行的函數或代碼字符串,參數2爲延遲執行時間,單位毫秒默認0,參數3及以後的參數爲參數1爲函數時傳入的參數,調用以後會返回一個定時器id
  • 此方法只執行一次,能夠使用clearTimeout(id)清除定時器來取消回調
  • 看一下setTimeout的延遲執行機制

setTimeout

  • 以上使用嵌套setTimeout來實現循環調用,能夠從中看出setTimeout計時是從上一個setTimeout回調執行以後開始的,看看代碼效果

setTimeout代碼演示

  • 上圖計算的是2次調用回調之間的間隔,不包括回調執行時間,能夠看出在開啓定時器以後到執行回調的時間確實是參數2所設置的值,延遲時間與回調函數執行時間無關;
  • 簡單來說setTimeout的延遲時間不包括自身回調所佔用的時間

也就是說setTimeout是在上一次回調執行以後纔開啓的定時

setInterval

  • setInterval一樣也是webApi的一部分,主要用來定時循環執行代碼
  • 不一樣於setTimeout,此定時器的延遲執行機制有所不一樣
  • setInterval(func|code, [delay], [arg1], [arg2], ...),參數列表同setTimeout,參數2爲每次循環時間

setInterval

  • 從上圖能夠先得出結論,setInterval的延遲執行時間包含自身回調執行所佔用的時間,看看代碼效果

setInterval代碼演示

  • 上圖計算的是2次調用回調之間的間隔,不包括回調執行時間,能夠看出setInterval在2次執行之間的延遲受到了回調的影響,再驗證一下

setInterval代碼演示1

  • 這次我把回調執行時間也算在計時以內,如今看來setInterval的定時時間確實包含了自身回調所佔用的時間

因爲這2個api都屬於異步宏任務,在執行的時候都會進入任務隊列,若是隊列前的任務執行時間較長,那麼也會影響到定時器的執行時機

在瀏覽器中alert、confirm、prompt都會阻塞js主線程執行,直到彈窗消失,可是定時器還會繼續執行;定時器並不能達到0延遲,最小延遲限制在4ms

二11、requestAnimationFrame

  • 在requestAnimationFrame還未出來以前,大多數使用定時器完成js動畫,可是因爲定時器不許確,並且每次更新動畫的時候不能保證與瀏覽器渲染同步,這樣將會致使畫面的不流暢
  • 因爲目前主流屏幕的固定刷新頻率通常爲60HZ即一秒60幀,每次刷新間隔爲1000/60ms,爲了使瀏覽器獲得最好的渲染效果,瀏覽器每次渲染應該與屏幕刷新率保持一致,那麼對於js動畫而言,最好的更新時機應該與瀏覽器儘可能保持一致
  • 當每次瀏覽器將要重繪以前,把要執行更新的動畫更新完成,那麼當瀏覽器渲染的時候將會保持最新的動畫,這就是requestAnimationFrame所作的事情
  • requestAnimationFrame(callback) 的參數就是每次渲染前須要執行的動畫更新函數,當瀏覽器將要重繪畫面時就會執行這個回調函數,這個回調函數接受一個參數,即從當前頁面加載以後到如今所通過的毫秒數
  • 此api將會與瀏覽器渲染同步,即瀏覽器渲染幾回這個api將會執行幾回,那麼就達到了不掉幀的效果,畫面效果就更加流程
  • requestAnimationFrame執行時機在事件循環機制中處於微任務隊列以後,瀏覽器渲染以前,瀏覽器渲染以後就會進入下一次的事件循環(宏任務開始,瀏覽器渲染結束)
  • 若是使用定時器進行js動畫操做,那麼首先將會致使動畫更新與瀏覽器每次重繪時機不匹配,形成卡頓,其次過於頻繁的更新動畫還會致使沒必要要的性能開銷,且並不是可以達到更好的效果
  • 簡單說使用requestAnimationFrame更新的動畫與瀏覽器保持同步,不會掉幀,除非瀏覽器掉幀或者,js主線程阻塞致使瀏覽器沒法正常渲染,使用定時器更新動畫,若是頻率高了會影響性能,且達不到更好的效果,若是頻率低了將會有不連貫的感受

requestAnimationFrame

  • 從上圖能夠看出確實是每幀執行一次,不過要注意,調用一次requestAnimationFrame只會執行一次,若是須要持續執行須要在回調函數內繼續調用

二12、事件

DOM0事件

  • DOM0事件並不是w3c標準,在DOM標準造成以前的事件模型就是咱們所說的0級DOM
  • 添加DOM0事件,都是把一個函數賦值給文檔元素,在事件監聽函數被調用時,將會作爲產生事件的元素方法調用,因此this指向目標元素,簡單說就是直接把回調函數做爲文檔元素的一個方法調用
  • 刪除DOM0事件只需把事件賦值爲null便可
document.getElementById("btn").onclick = function () {}
----------------------------------------------
<input type="button" onclick="alert('hi!');">
複製代碼
  • 若是回調方法返回一個false則會阻止瀏覽器事件的默認行爲
  • DOM0事件在事件捕獲階段,沒法接收事件,即沒沒法觸發事件捕獲,可是可以正常觸發冒泡
  • 因爲DOM0事件的回調屬於文檔元素的方法,致使沒法添加多個同名事件,不過看來兼容性最好

DOM2事件

  • 因爲w3c推出的1級DOM標準中並無定義事件相關的內容,因此沒有所謂的1級DOM事件模型
  • 在2級DOM中除了定義了一些DOM相關的操做以外還定義了一個事件模型 ,這個標準下的事件模型就是咱們所說的2級DOM事件模型
  • 2級DOM定義了事件傳播,在事件傳播過程當中將會經歷3個階段:
    1. capturing階段,即事件捕獲階段,在某個DOM上觸發事件時,事件會先從Document對象 沿着dom數向下傳遞直到觸發節點,此過程就是事件捕獲階段,在此過程當中能夠捕獲傳播的事件
    2. 目標元素的事件處理階段,此階段事件到達觸發目標,調用回調處理事件
    3. bubbling階段,即事件冒泡階段,在目標元素處理完成以後,此事件還會向上冒泡,回傳到Document,此階段與捕獲階段相反
  • 以上就是事件在觸發以後的傳播過程,能夠配合下圖理解

事件傳遞

  • DOM2 註冊事件,能夠經過addEventListener(eventName,callback,isCapturing)方法爲元素設置事件監聽器,參數1爲註冊事件名不帶on開頭的string類型,參數2爲觸發事件的回調函數,接受一個事件對象參數,參數3爲是否在捕獲階段觸發,默認爲false
  • 經過removeEventListener(eventName,callback,isCapturing)方法移除指定事件名、回調、是否捕獲的事件,匿名回調沒法刪除
  • 可給一個元素添加多個相同的事件,經過不一樣的回調實現不一樣效果
  • DOM2中的回調函數中的this指向,由瀏覽器決定,w3c標準中並未規定其指向,通常狀況this指向window
  • 回調函數event對象參數
  • 屬性
    • type 發生事件的類型
    • target 發生事件的階段,爲觸發事件的對象,能夠與currentTarget不一樣
    • currentTarget 正在處理事件的節點,即註冊此回調函數的元素
    • clientX,clientY鼠標相對瀏覽器的x座標與y座標
    • screenX,screenY鼠標相對於顯示器左上角x,y座標
  • 方法
    • stopPropagation() 阻止當前事件的進一步傳播
    • preventDefault() 阻止瀏覽器執行與世界相關的默認動做,與DOM0返回false相同
  • 觸發時機
    • document 往 target節點傳播,捕獲前進,遇到註冊的捕獲事件當即觸發執行
    • 到達target節點,觸發事件(對於target節點上,是先捕獲仍是先冒泡則捕獲事件和冒泡事件的註冊順序,先註冊先執行)
    • target節點 往 document 方向傳播,冒泡前進,遇到註冊的冒泡事件當即觸發

事件代理

  • 事件代理又或是事件委託,經過事件冒泡機制,使用單一父節點來操做多個子節點的響應,簡單講就是把全部子節點的事件去除,只給父節點註冊事件,那麼就能夠經過事件冒泡機制來處理子節點的響應
  • 基於事件委託能夠減小事件註冊,節省內存,簡化dom節點於事件的更新
<ul id="f">
  <li>a</li>
  <li>b</li>
  <li>c</li>
</ul>
<script>
  const ul = document.querySelector('#f')
  // 點擊li時觸發事件委託
  ul.addEventListener('click',function foo(event){
    // 處理元素爲父元素
    console.dir(event.currentTarget)  // ul#f
    // 觸發元素爲子元素,event.target爲具體觸發對象
    console.dir(event.target)         // li
  })
//--------------------------------------------
  // 經過點擊添加子元素
  ul.addEventListener('click',function foo(event){
    const child = document.createElement('li')
    child.innerText = '我是新增的子元素'
    event.currentTarget.appendChild(child)
  })
//--------------------------------------------
  // 經過點擊刪除子元素
  ul.addEventListener('click',function foo(event){
    event.currentTarget.removeChild(event.target)
  })
</script>
----------------------------------------------
<!-- 若是點擊span 想知道是哪一個li下面的元素 -->
<ul id="f">
  <li>a</li>
  <li>
    <span>b</span>
  </li>
  <li>
    <span>c</span>
  </li>
</ul>
<script>
  const ul = document.querySelector('#f')
  ul.addEventListener('click', function foo(event) {
    let target = event.target
    // 一級級向上尋找直到找到知足條件的元素
    while (target.nodeName.toLowerCase() !== 'li') {
      target.target.parentNode
    }
    console.dir(target) // li
    console.dir(target.parentNode === event.currentTarget) //true
  })
</script>
複製代碼
  • 以上就是幾個簡單的事件代理的例子,事件代理可以在咱們平時開發中減小不少沒必要要的代碼,優化事件系統,可是在使用的過程也要注意相應的問題
  • 事件代理基於冒泡機制,若是代理層級過多,且在冒泡階段若是被某層阻止冒泡那麼父級將不會收到事件
  • 理論上委託會致使瀏覽器頻繁調用處理函數,雖然極可能不須要處理,因此建議就近委託
  • 若是事件代理了許多狀況那麼要作好完善邏輯分析,避免一些誤判的狀況

總結

以上總結可能沒有什麼順序,可是每章節都是針對性的講解,零散的知識點較多,但願看完這篇文章能擴展你的知識面,也許某方面講的不是很詳細,若是感興趣能夠找些針對性的文章進行深刻了解。

部份內容並不是原創,仍是要感謝前輩的總結,若是本文影響到您的利益,那麼還請事先告知,在寫本文時的初衷就是想給更多學習前端的小夥伴拓展知識,夯實基礎,共同進步,也爲了之後方便複習使用

總結不易,如需轉載請註明出處,感謝!

求點贊

若是本文對你有所幫助,就請點個贊支持一下吧,讓更多人看到,你的支持就是我堅持寫做下去的動力,若是喜歡個人文章,那麼還請關注後續的文章吧~ ψ(`∇´)ψ

其餘文章

js系列

淺談ES6新特性

css系列

css之flex佈局、經常使用水平垂直居中

速記系列

速記之數組api

速記之字符串api

速記之對象api