web前端知識點(JavaScript篇)

call,apply,bind

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

promise

promise函數的內部實現原理,點這裏

閉包

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

原型鏈

JavaScript中每一個對象都有一個__proto__和constructor屬性,每一個函數都有一個prototype屬性,因函數也是對象,所以函數也擁有__proto__和constructor屬性。

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

prototype屬性是函數獨有的,是從一個函數指向一個對象,稱之爲函數的原型對象。原型對象內包含特定類型所有實例共享的屬性和方法,作用爲被該函數實例化出來的對象找到共用的屬性和方法。

constructor是從一個對象指向一個函數,稱之爲該對象的構造函數。每個對象都有對應的構造函數,因爲對象的建立前提是需要有constructor。

節流與防抖

節流:

節流是在規定的時間內只執行一次,稀釋函數執行頻率。比如規定時間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,如果是引用類型,返回這個引用對象

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:新的數據類型,表示獨一無二的值,最大的用法是用來定義對象的唯一屬性名。

 

 

未完待續......

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章