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
注意的一點:微任務隊列中的微任務回調函數是放入當前微任務隊列中,而不是下輪循環隊列。
瀏覽器垃圾回收機制
- 標記清除
垃圾收集器給內存中的所有變量都加上標記,然后去掉環境中的變量以及被環境中的變量引用的變量的標記。在此之后再被加上的標記的變量即為需要回收的變量,因為環境中的變量已經無法訪問到這些變量。
- 引用計數
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
function $new (fn) { var _obj = {} var _args = Array.prototype.slice.call(arguments, 1) _obj.__proto__ = fn.prototype var result = fn.call(_obj, ..._args) if (typeof result === 'object' || typeof result === 'function') { return result } return _obj } var fn = function (name, age) { this.name = name; this.age = age; } $new(fn, '李四', 20)
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做布局的一個原因。
- 不要在布局信息改變的時候做查詢(會導致渲染隊列強制刷新)
- 獲取能引起回流的元素屬性值,應進行緩存
未完待續......