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
注意的一點:微任務佇列中的微任務回撥函式是放入當前微任務佇列中,而不是下輪迴圈佇列。
瀏覽器垃圾回收機制
- 標記清除
垃圾收集器給記憶體中的所有變數都加上標記,然後去掉環境中的變數以及被環境中的變數引用的變數的標記。在此之後再被加上的標記的變數即為需要回收的變數,因為環境中的變數已經無法訪問到這些變數。
- 引用計數
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:新的資料型別,表示獨一無二的值,最大的用法是用來定義物件的唯一屬性名。
未完待續......