web前端知識點(JavaScript篇)

那肉香腸發表於2020-09-19

call,apply,bind

call,apply,bind這三者的區別,及內部實現原理, 點這裡

promise

promise函式的內部實現原理, 點這裡

閉包

閉包就是能夠讀取其他函式內部變數的函式。形式上,就是一個函式返回一個內部函式到函式外,內部函式引用外部函式的區域性變數。本質上,閉包是將函式內部和函式外部連線起來的橋樑。

原型鏈

JavaScript中每一個物件都有一個__proto__和constructor屬性,每一個函式都有一個prototype屬性,因函式也是物件,所以函式也擁有__proto__和constructor屬性。

__proto__指向的是它們的原型物件,也可以理解為父物件。如果訪問本身一個不存在的屬性,那麼沒有獲取之後會去它的原型物件去獲取,而原型物件本身也是一個普通物件,如果在它的原型物件中同樣沒有獲取到,那麼就會往原型物件的原型物件去獲取,直到頂層物件null(原型鏈終點,一個沒有任何屬性的物件),返回undefined。這就形成了一條原型鏈。

prototype屬性是函式獨有的,是從一個函式指向一個物件,稱之為函式的原型物件。原型物件內包含特定型別所有例項共享的屬性和方法,作用為被該函式例項化出來的物件找到共用的屬性和方法。

constructor是從一個物件指向一個函式,稱之為該物件的建構函式。每個物件都有對應的建構函式,因為物件的建立前提是需要有constructor。

JS 非同步併發控制

js 非同步併發控制一個很重要的場景就是大檔案的分片上傳,加上 http1.1 能夠同時傳送6個請求,將大檔案分成多個片段進行併發請求,能夠減少上傳時間,並且能夠進行斷點續傳與暫停上傳等功能。

以下是非同步併發控制大致邏輯:

function sendRequest(arr, max = 6, callback) {
  let i = 0 // 陣列下標
  let fetchArr = [] // 正在執行的請求
  let toFetch = () => {    // 如果非同步任務都已開始執行,剩最後一組,則結束併發控制
    if (i === arr.length) {      return Promise.resolve()
    }    // 執行非同步任務
    let it = fetch(arr[i++])    // 新增非同步事件的完成處理
    it.then(() => {
      fetchArr.splice(fetchArr.indexOf(it), 1)
    })
    fetchArr.push(it)
    let p = Promise.resolve()    // 如果併發數達到最大數,則等其中一個非同步任務完成再新增
    if (fetchArr.length >= max) {
      p = Promise.race(fetchArr)
    }    // 執行遞迴
    return p.then(() => toFetch())
  }
  toFetch().then(() => 
    // 最後一組全部執行完再執行回撥函式
    Promise.all(fetchArr).then(() => {
      callback()
    })
  )
}

節流與防抖

節流:

節流是在規定的時間內只執行一次,稀釋函式執行頻率。比如規定時間2s內執行了一次函式,那麼在這2s內再次觸發將不會執行。

function throttle(time, fn) {
  let isRun = false
  return function () {    if (isRun) return
    isRun = true
    let arg = [...arguments]
    setTimeout(() => {
      fn.apply(null, arg)
      isRun = false
    }, time * 1000)
  }
}

防抖:

防抖是在等待的時間內不斷觸發函式,但函式真正執行的將是最後觸發的那次。比如規定時間為2s,如果第二次與第一次的觸發的時間間隔小於2s,那麼第一次將會被清除,留第二次觸發的函式繼續等待,如果2s內沒有第三次觸發,將執行第二次觸發的函式,如果2s內又觸發了第三次,那麼第二次觸發的函式也將被清除,留第三次觸發的函式繼續等待。

function debounce(time, fn) {
  let timer = null
  return function () {
    let arg = [...arguments]    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      fn.apply(null, arg)
      clearTimeout(timer)
      timer = null
    }, time * 1000)
  }
}

斐波那契數列、快排、氣泡排序

斐波那契數列:1、1、2、3、5、8、13、21、……

// 遞迴function fibonacci(num) {  if (num === 1 || num === 2) {    return 1
  }  return fibonacci(num - 2) + fibonacci(num - 1)
}// 迴圈function fibonacci1(n) {  var n1 = 1, n2 = 1, sum;  for (let i = 2; i < n; i++) {
    sum = n1 + n2
    n1 = n2
    n2 = sum
  }  return sum
}

快速排序:

function quickSortFn(_arr) {
  let arr = [..._arr]  if (arr.length <= 1) {    return arr
  }
  let left = []
  let right = []
  let item = arr.pop()  for (let i = 0, len = arr.length; i < len; i++) {
    let val = arr[i]    if (val >= item) {
      right.push(val)
    } else {
      left.push(val)
    }
  }  return [...quickSortFn(left), item, ...quickSortFn(right)]
}

氣泡排序:

function bubbleSort(_arr) {
  let arr = [..._arr]
  let len = arr.length  for (let i = 0; i < len - 1; i++) {    for (let k = i + 1; k < len; k++) {      if (arr[i] > arr[k]) {
        [arr[i], arr[k]] = [arr[k], arr[i]]
      }
    }
  }  return arr
}

多維陣列轉一維陣列

// 第一種let a = [1,2,[3,4],[5,[6,[7,8]],9]]
a.join(',').split(',')// 第二種function unid1(arr) {  for (let item of arr) {    if (Object.prototype.toString.call(item).slice(8, -1) === 'Array') {
      unid1(item);
    } else {
      result.push(item);
    }
  }  return result;
}

js流程控制

function LazyMan(name) {  this.task = []
  let self = this
  let fn = (name => {    return () => {
      console.log(name)
      self.next()
    }
  })(name)
  self.task.push(fn)
  setTimeout(() => {
    console.log(222)
    self.next()
  })
}
LazyMan.prototype = {
  constructor: LazyMan,
  next() {
    let fn = this.task.shift()
    fn && fn()
  },
  eat(val) {
    let self = this
    self.task.push((val => {      return () => {
        console.log(val)
        self.next()
      }
    })(val))    return this
  },
  sleep(num) {
    let self = this
    self.task.push((num => {      return () => {
        setTimeout(() => {
          console.log(num)
          self.next()
        }, +num * 1000)
      }
    })(num))    return this
  }
}function lazyMan(name) {  return new LazyMan(name)
}
lazyMan('zz').eat('lunch').sleep('3').eat('dinner')

物件深複製與淺複製

深複製與淺複製的區別本質是被複製出來的值的記憶體地址是否有改變,記憶體地址沒變就是淺複製,有變就是深複製。這裡涉及到了JavaScript的引用資料型別,引用資料型別的複製,複製的不是物件本身,而是一個指向該物件的指標,當這個物件本身的值改變,那麼所有引用這個物件的變數都會改變。

淺複製:

Object.assign()

深複製:

JSON.parse(JSON.stringify(object)):

這個能夠複製除Function、RegExp與undefined等型別之外的值,如果遇到這種型別,將會被自動忽略。

迴圈遞迴複製:

function getType(val) {  return Object.prototype.toString.call(val).slice(8, -1)
}function deepClone(obj) {  if (obj && typeof obj === 'object') {
    let returnObj = getType(obj) === 'Array' ? [] : {}
    let item = ''    for (let key in obj) {
      item = obj[key]      if (key === "__proto__") {        continue;
      }      if (getType(item) === 'Array' || getType(item) === 'Object') {
        returnObj[key] = deepClone(item)
      } else {
        returnObj[key] = item
      }
    }    return returnObj
  }
}

非同步與事件輪詢機制

JavaScript語言的核心特點就是單執行緒,單執行緒的原因主要是對DOM的操作,多執行緒操作DOM會引起衝突。為了利用多核CPU的計算能力,HTML5提出了web worker標準,允許JavaScript建立多執行緒,且建立執行緒完全受主執行緒控制,且不得操作DOM。

js的非同步是透過回撥函式實現的,即任務佇列。雖然js是單執行緒的,但瀏覽器的多執行緒的,則js的執行遇到非同步任務都會呼叫瀏覽器的多執行緒去執行,當非同步任務有了結果,則會將非同步任務的回撥函式放入非同步任務佇列。

任務佇列分為兩種:宏任務佇列與微任務佇列。

當js從上往下執行時,如遇到非同步任務,瀏覽器則用其他執行緒去執行,當非同步任務有了結果,則將回撥函式放到任務佇列中,當主執行棧執行完後,會去查詢微任務佇列,如果有則執行,微任務佇列執行完後,則將宏任務佇列放入主執行棧重新開始下一輪迴圈。

不同的js非同步API的回撥函式放入不同的任務佇列。

宏任務(macrotask)佇列API:

  • setTimeout
  • setInterval
  • setImmediate(node,IE10+)
  • requestAnimationFrame(瀏覽器)

微任務(microtask)佇列API:

  • process.nextTick(node)
  • MutationObserver(瀏覽器)
  • Promise.then catch finally

注意的一點:微任務佇列中的微任務回撥函式是放入當前微任務佇列中,而不是下輪迴圈佇列。

瀏覽器垃圾回收機制

  • 標記清除
大部分瀏覽器以此方式進行垃圾回收,當變數進入執行環境(函式中宣告變數,執行時)的時候,垃圾回收器將其標記為“進入環境”,當變數離開環境的時候(函式執行結束)將其標記為“離開環境”,在離開環境之後還有的變數則是需要被刪除的變數。標記方式不定,可以是某個特殊位的反轉或維護一個列表等。
垃圾收集器給記憶體中的所有變數都加上標記,然後去掉環境中的變數以及被環境中的變數引用的變數的標記。在此之後再被加上的標記的變數即為需要回收的變數,因為環境中的變數已經無法訪問到這些變數。
  • 引用計數
另一種不太常見的垃圾回收策略是引用計數。引用計數的含義是跟蹤記錄每個值被引用的次數。當宣告瞭一個變數並將一個引用型別賦值給該變數時,則這個值的引用次數就是1。相反,如果包含對這個值引用的變數又取得了另外一個值,則這個值的引用次數就減1。當這個引用次數變成0時,則說明沒有辦法再訪問這個值了,因而就可以將其所佔的記憶體空間給收回來。這樣,垃圾收集器下次再執行時,它就會釋放那些引用次數為0的值所佔的記憶體。

js執行上下文和執行棧

該點的解釋則是表明JavaScript程式內部的執行機制。

執行上下文,簡而言之,就是當前JavaScript程式碼被解析和執行時所在環境的抽象概念,JavaScript任何程式碼都是在執行上下文中執行。

三種型別:

  • 全域性執行上下文:不在任何函式內的程式碼都處於全域性執行上下文,一個程式只能有一個全域性執行上下文。做了兩件事:1、建立了一個全域性物件,瀏覽器則是window;2、將this指向這個全域性物件。
  • 函式執行上下文:每個函式都有自己的執行上下文。呼叫函式時,都會為這個函式建立一個新的執行上下文,也只在函式被呼叫時才會被建立。一個程式內的函式執行上下文沒有數量限制,每當一個函式執行上下文被建立,則會執行一系列操作。
  • eval函式執行上下文:不常用,略。

生命週期:

  • 建立:建立變數物件,建立作用域鏈,確定this指向(this的賦值是在執行的時候確定的)。
  • 執行:變數賦值,程式碼執行。
  • 回收:執行完成,執行上下文出棧,等待回收。

管理執行上下文:

所有的執行上下文采用的是棧結構來管理,遵循先進後出。全域性JavaScript程式碼在瀏覽器執行時,實現建立一個全域性執行上下文,壓入執行棧的底端,每建立一個函式執行上下文,則把它壓入執行棧的頂端,等待函式執行完,該函式的執行上下文出棧等待回收。

JavaScript解析引擎總是訪問執行棧的頂端,當瀏覽器關閉,則全域性執行上下文出棧。

url輸入到頁面顯示之間的過程

  • 使用者輸入的url作DNS解析,獲取IP地址
  • 建立TCP連線
  • 傳送HTTP請求,獲取html檔案
  • 解析HTML檔案,構建DOM樹及CSSOM規則樹,然後合併渲染樹,繪製介面。
  • 傳送HTTP獲取HTML檔案內其他資源。

new運算子中的執行過程

  • 建立一個新物件 newObject
  • 將新物件 newObject 的 __proto__ 指向原函式 fn 的 prototype
  • 執行原函式 result = fn.call(newObject)
  • 如果 result 為引用型別則返回 result,不是則返回新物件 newObject

async/await的實現原理

async/await的作用為阻塞非同步執行任務,等待非同步任務執行完返回,再執行下面任務,非同步任務返回的是一個Promise物件。

實現原理為generator + yield + promise:generator自動執行且返回一個promise物件。

let test = function () {  // ret 為一個Promise物件,因為ES6語法規定 async 函式的返回值必須是一個 promise 物件
  let ret = _asyncToGenerator(function* () {    for (let i = 0; i < 10; i++) {
      let result = yield sleep(1000);
      console.log(result);
    }
  });  return ret;
}();// generator 自執行器function _asyncToGenerator(genFn) {  return new Promise((resolve, reject) => {
    let gen = genFn();    function step(key, arg) {
      let info = {};      try {
        info = gen[key](arg);
      } catch (error) {
        reject(error);        return;
      }      if (info.done) {
        resolve(info.value);
      } else {        return Promise.resolve(info.value).then((v) => {          return step('next', v);
        }, (error) => {          return step('throw', error);
        });
      }
    }
    step('next');
  });
}

跨域問題的產生及解決方案與原理

跨域是指一個域下的文件或指令碼試圖去請求另一個域下的資源,這裡跨域是廣義的。

而狹義的跨域是指:當瀏覽器與伺服器通訊的兩個地址的協議、域名、埠,這三者任意一個不同,都會導致跨域問題的產生,這是基於瀏覽器的同源策略限制。

限制的行為:

  • cookie,localstorage和IndexDB無法讀取
  • DOM無法獲取
  • Ajax請求不能傳送

解決方案:

  • jsonp跨域通訊:只能用於get請求,基於瀏覽器允許HTML標籤載入不同域名下的靜態資源,透過script動態載入一個帶參網址實現跨域通訊實現跨域。
  • postMessage跨域:postMessage是HTML5 XMLHttpRequest Level 2中的API,且是為數不多可以跨域操作的window屬性之一。
  • nginx代理:伺服器端呼叫HTTP介面只是使用HTTP協議,不會執行JS指令碼,不需要同源策略,也就不存在跨越問題。
  • 跨域資源共享(CORS):只服務端設定Access-Control-Allow-Origin即可,前端無須設定,若要帶cookie請求:前後端都需要設定。
  • nodejs中介軟體代理跨域:node中介軟體實現跨域代理,原理大致與nginx相同,都是透過啟一個代理伺服器,實現資料的轉發,也可以透過設定cookieDomainRewrite引數修改響應頭中cookie中域名,實現當前域的cookie寫入。
  • WebSocket協議跨域:WebSocket protocol是HTML5一種新的協議。它實現了瀏覽器與伺服器全雙工通訊,同時允許跨域通訊,是server push技術的一種很好的實現。
  • document.domain + iframe跨域:此方案僅限主域相同,子域不同的跨域應用場景。實現原理:兩個頁面都透過js強制設定document.domain為基礎主域,就實現了同域
  • location.hash + iframe跨域:a欲與b跨域相互通訊,透過中間頁c來實現。 三個頁面,不同域之間利用iframe的location.hash傳值,相同域之間直接js訪問來通訊。
  • window.name + iframe跨域:window.name屬性的獨特之處:name值在不同的頁面(甚至不同域名)載入後依舊存在,並且可以支援非常長的 name 值(2MB)。

正向代理與反向代理的區別:

正向代理與反向代理並沒有形式上的區別,只是一個認知的問題。比如a請求b有跨域問題,正向代理與反向代理都可以透過中介c來實現,a -> c -> b -> c -> a這樣完成了一次跨域通訊,如果a請求c,知道c會去請求b再返回,則是一個正向代理,如果a不知道請求c,c最終去請求了b,那這就是一個反向代理。最終目的地址以IP為準。

es6新特性

  • 字串擴充套件:includes、startsWith、endsWith等新API及模板字串。
  • 物件擴充套件:keys、values、entries、assian等。
  • 陣列擴充套件:find、findIndex、includes等。
  • 新的變數宣告:let、const。
  • 解構表示式:陣列解構與物件解構。
  • 函式最佳化:函式引數預設值、箭頭函式、物件的函式屬性簡寫。
  • 陣列最佳化:map與reduce等API的增加。
  • Promise:非同步微任務API的增加。
  • 新資料結構:set、map。
  • 模組化:export、import。
  • 二進位制與八進位制字面量:數字前面新增0o/0O和0b/0B可將其轉化為二進位制和八進位制。
  • 類class:原型鏈的語法糖表現形式。
  • for...of/for...in:新的遍歷方式。
  • async/await:同步非同步任務。
  • Symbol:新的資料型別,表示獨一無二的值,最大的用法是用來定義物件的唯一屬性名。

詳情點這裡。

 優雅降級與漸進增強

優雅降級:一開始就針對低版本瀏覽器進行構建頁面,完成基本的功能,然後再針對高階瀏覽器進行效果、互動、追加功能達到更好的體驗。

漸進增強:一開始就構建站點的完整功能,然後針對瀏覽器測試和修復。比如一開始使用 CSS3 的特性構建了一個應用,然後逐步針對各大瀏覽器進行 hack 使其可以在低版本瀏覽器上正常瀏覽。

優雅降級和漸進增強都關注於同一網站在不同裝置裡不同瀏覽器下的表現程度。關鍵的區別則在於它們各自關注於何處,以及這種關注如何影響工作的流程。

優雅降級觀點認為應該針對那些最高階、最完善的瀏覽器來設計網站。而將那些被認為“過時”或有功能缺失的瀏覽器下的測試工作安排在開發週期的最後階段,並把測試物件限定為主流瀏覽器(如 IE、Mozilla 等)的前一個版本。在這種設計範例下,舊版的瀏覽器被認為僅能提供“簡陋卻無妨 (poor, but passable)” 的瀏覽體驗。你可以做一些小的調整來適應某個特定的瀏覽器。但由於它們並非我們所關注的焦點,因此除了修復較大的錯誤之外,其它的差異將被直接忽略。

漸進增強觀點則認為應關注於內容本身。請注意其中的差別:我甚至連“瀏覽器”三個字都沒提。內容是我們建立網站的誘因。有的網站展示它,有的則收集它,有的尋求,有的操作,還有的網站甚至會包含以上的種種,但相同點是它們全都涉及到內容。這使得漸進增強成為一種更為合理的設計範例。這也是它立即被 Yahoo! 所採納並用以構建其“ 分級式瀏覽器支援 (Graded Browser Support)”策略的原因所在。

重排(迴流)與重繪

這兩者之間的關係:重繪不一定重排,而重排一定重繪。

重排:當渲染樹的一部分必須更新並且節點的尺寸發生了變化,瀏覽器會使渲染樹中受到影響的部分失效,並重新構造渲染樹。

重繪:在一個元素的外觀被改變所觸發的瀏覽器行為,瀏覽器會根據元素的新屬性重新繪製,使元素呈現新的外觀。

引起的原因:

重排:

  • 頁面第一次渲染 在頁面發生首次渲染的時候,所有元件都要進行首次佈局,這是開銷最大的一次迴流。
  • 瀏覽器視窗尺寸改變
  • 元素位置和尺寸發生改變的時候
  • 新增和刪除可見元素
  • 內容發生改變(文字數量或圖片大小等等)
  • 元素字型大小變化。
  • 啟用CSS偽類(例如::hover)。
  • 設定style屬性
  • 查詢某些屬性或呼叫某些方法。比如說:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight

除此之外,當我們呼叫getComputedStyle方法,或者IE裡的currentStyle時,也會觸發迴流,原理是一樣的,都為求一個“即時性”和“準確性”。

重繪:

  • 當render tree中的一些元素需要更新屬性,而這些屬性只是影響元素的外觀,風格,而不會影響佈局的,比如visibility、outline、背景色等屬性的改變。

最佳化:

  • 不要一條一條地修改 DOM 的樣式。可以先定義好 css 的 class,然後修改 DOM 的 className。
  • 不要把 DOM 結點的屬性值放在一個迴圈裡當成迴圈裡的變數。
  • 為動畫的 HTML 元件使用 fixed 或 absoult 的 position,那麼修改他們的 CSS 是不會 reflow 的。
  • 千萬不要使用 table 佈局。因為可能很小的一個小改動會造成整個 table 的重新佈局。 table及其內部元素除外,它可能需要多次計算才能確定好其在渲染樹中節點的屬性,通常要花3倍於同等元素的時間。這也是為什麼我們要避免使用table做佈局的一個原因。
  • 不要在佈局資訊改變的時候做查詢(會導致渲染佇列強制重新整理) 
  • 獲取能引起迴流的元素屬性值,應進行快取


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69959503/viewspace-2722523/,如需轉載,請註明出處,否則將追究法律責任。

相關文章