天天看點

【直接收藏】前端JavaScript面試100問(中)

作者:陸榮濤

31、http 的了解 ?

HTTP 協定是超文本傳輸協定,是用戶端浏覽器或其他程式“請求”與 Web 伺服器響應之間的應用層通信協定。

HTTPS主要是由HTTP+SSL建構的可進行加密傳輸、身份認證的一種安全通信通道。

32、http 和 https 的差別 ?

  • 1、https協定需要到ca申請證書,一般免費證書較少,因而需要一定費用。
  • 2、http是超文本傳輸協定,資訊是明文傳輸,https則是具有安全性的ssl加密傳輸協定。
  • 3、http和https使用的是完全不同的連接配接方式,用的端口也不一樣,前者是80,後者是443。
  • 4、http的連接配接很簡單,是無狀态的;HTTPS協定是由SSL+HTTP協定建構的可進行加密傳輸、身份認證的網絡協定,比http協定安全。

33、git 的常用指令有哪些 ?

git branch 分支檢視

git branch branch_1 增加分支

git checkout branch 分支切換

git merge branch_1 合并分支(合并前要切換目前分支至master)

git branch -d branch_1 删除分支

git remote 檢視目前倉庫管理的遠端倉庫資訊

git remote show origin 檢視指定的遠端倉庫的詳細資訊

git push --set-upstream origin branch_1 第一次将本地分支推到遠端倉庫

git push <遠端主機名> <本地分支名>:<遠端分支名> 将本地分支推到遠端分支

git pull <遠端主機名> <遠端分支>:<本地分支> 将遠端分支拉到本地分支

git branch -d branch_0 删除本地合并後分支

git brench -D branch_0 删除本地未合并分支

it push origin --delete branch_0 删除遠端分支

git restore [filename] 進行清除工作區的改變

git tag 檢視标簽

git tag v1.0.0 打标簽

git push origin v1.0.0 将tag同步到遠端伺服器

34、平時是使用 git 指令還是圖形化工具 ?

repository:git庫相關操作,基本意思就是字面意思。

  • 1)資料總管中浏覽該Git庫工作空間檔案,省去查找路徑不斷點選滑鼠的操作。
  • 2)啟動Git bash工具(指令行工具)。
  • 3)檢視目前分支檔案狀态,不包括未送出的資訊。
  • 4)檢視某個分支的檔案(彈出框中可選擇需要檢視的版本、分支或标簽),跟上一條差不多,用的比較少,可能是沒有這方面的額需求。
  • 5)可視化目前分支曆史、可視化所有分支曆史:彈出分支操作曆史,也就是gitk工具,放到gitk工具中介紹。
  • edit:用于操作commit時操作資訊輸入,隻能操作文字輸入部分,你沒有看錯。常用的快捷鍵大家都知道,何必要單獨做成基本沒啥用的。本來以為對變更的檔案進行批量操作、本來以為可以對未版本跟蹤的檔案批量删除、本來、、、,都說了是本來。
  • Branch:建立分支(需要選擇其實版本,可以根據版本号、其他分支或标簽來選擇)、檢出分支(覺得切換分支更合适)、重命名分支、删除分支、目前分支Reset操作(會丢棄所有未送出的變更,包括工作區和索引區,當然了,有彈出框提示危險操作)。

35、Promsie.all() 使用過嗎, 它是怎麼使用的 ?

promise.all()用于一個異步操作需要在幾個異步操作完成後再進行時使用。

promise.all()接受一個promise對象組成的數組參數,傳回promise對象。

當數組中所有promise都完成了,就執行目前promise對象的then方法,如果數組中有一個promise執行失敗了,就執行目前promise對象的catch方法。

36、什麼是三次握手和四次揮手 ?

三次握手是網絡用戶端跟網絡伺服器之間建立連接配接,并進行通信的過程。相當于用戶端和伺服器之間你來我往的3個步驟。

  • 第一次握手是建立連接配接,用戶端發送連接配接請求封包,并傳送規定的資料包;
  • 第二次握手是伺服器端表示接收到連接配接請求封包,并回傳規定的資料包;
  • 第三次握手是用戶端接收到伺服器回傳的資料包後,給伺服器端再次發送資料包。這樣就完成了用戶端跟伺服器的連接配接和資料傳送。

四次揮手表示目前這次連接配接請求已經結束,要斷開這次連接配接。

  • 第一次揮手是用戶端對伺服器發起斷開請求,
  • 第二次握手是伺服器表示收到這次斷開請求,
  • 第三次握手是伺服器表示已經斷開連接配接
  • 第四次握手是用戶端斷開連接配接。

37、for in 和 for of 循環的差別 ?

`for in` 用于周遊對象的鍵(`key`),`for in`會周遊所有自身的和原型鍊上的可枚舉屬性。如果是數組,for in會将數組的索引(index)當做對象的key來周遊,其他的object也是一樣的。 `for of`是`es6`引入的文法,用于周遊 所有疊代器iterator,其中包括`HTMLCollection`,`NodeList`,`Array`,`Map`,`Set`,`String`,`TypedArray`,`arguments`等對象的值(`item`)。

38、async/await 怎麼抛出錯誤異常 ?

如果可能出錯的代碼比較少的時候可以使用try/catch結構來了處理,如果可能出錯的代碼比較多的時候,可以利用async函數傳回一個promise對象的原理來處理,給async修飾的函數調用後傳回的promise對象,調用catch方法來處理異常。

39、 函數式程式設計和指令式程式設計的差別 ?

  • 指令式程式設計(過程式程式設計) :

專注于”如何去做”,這樣不管”做什麼”,都會按照你的指令去做。解決某一問題的具體算法實作。

  • 函數式程式設計:把運算過程盡量寫成一系列嵌套的函數調用。

函數式程式設計強調沒有”副作用”,意味着函數要保持獨立,所有功能就是傳回一個新的值,沒有其他行為,尤其是不得修改外部變量的值。

所謂”副作用”,指的是函數内部與外部互動(最典型的情況,就是修改全局變量的值),産生運算以外的其他結果。

40、http 常見的響應狀态碼 ?

100——客戶必須繼續送出請求

101——客戶要求伺服器根據請求轉換HTTP協定版本

200——交易成功

201——提示知道新檔案的URL

202——接受和處理、但處理未完成

203——傳回資訊不确定或不完整

204——請求收到,但傳回資訊為空

205——伺服器完成了請求,使用者代理必須複位目前已經浏覽過的檔案

206——伺服器已經完成了部分使用者的GET請求

300——請求的資源可在多處得到

301——删除請求資料

302——在其他位址發現了請求資料

303——建議客戶通路其他URL或通路方式

304——用戶端已經執行了GET,但檔案未變化

305——請求的資源必須從伺服器指定的位址得到

306——前一版本HTTP中使用的代碼,現行版本中不再使用

307——申明請求的資源臨時性删除

400——錯誤請求,如文法錯誤

401——請求授權失敗

402——保留有效ChargeTo頭響應

403——請求不允許

404——沒有發現檔案、查詢或URl

405——使用者在Request-Line字段定義的方法不允許

406——根據使用者發送的Accept拖,請求資源不可通路

407——類似401,使用者必須首先在代理伺服器上得到授權

408——用戶端沒有在使用者指定的餓時間内完成請求

409——對目前資源狀态,請求不能完成

410——伺服器上不再有此資源且無進一步的參考位址

411——伺服器拒絕使用者定義的Content-Length屬性請求

412——一個或多個請求頭字段在目前請求中錯誤

413——請求的資源大于伺服器允許的大小

414——請求的資源URL長于伺服器允許的長度

415——請求資源不支援請求項目格式

416——請求中包含Range請求頭字段,在目前請求資源範圍内沒有range訓示值,請求也不包含If-Range請求頭字段

417——伺服器不滿足請求Expect頭字段指定的期望值,如果是代理伺服器,可能是下一級伺服器不能滿足請求

500——伺服器産生内部錯誤

501——伺服器不支援請求的函數

502——伺服器暫時不可用,有時是為了防止發生系統過載

503——伺服器過載或暫停維修

504——關口過載,伺服器使用另一個關口或服務來響應使用者,等待時間設定值較長

505——伺服器不支援或拒絕支請求頭中指定的HTTP版本

41、 什麼是事件流以及事件流的傳播機制 ?

事件觸發後,從開始找目标元素,然後執行目标元素的事件,再到離開目标元素的整個過程稱之為事件流。

W3C标準浏覽器事件流的傳播分為3個階段:捕獲階段、目标階段、冒泡階段

  • 捕獲階段指找目标元素的過程,這個找的過程,是從最大的document對象到html,再到body,。。。直到目标元素。
  • 找到目标元素後,調用執行他綁定事件時對應的處理函數,這個過程被稱之為目标階段。
  • 當目标元素的事件執行結束後,再從目标元素,到他的父元素。。。body、html再到document的過程,是冒泡階段。

42、子產品化文法 ? commonJS AMD CMD ES6 Module

  • commonJS是nodejs自帶的一種子產品化文法,将一個檔案看做是一個子產品,可以将檔案中導出的時候,被另一個檔案導入使用。導出使用:module.exports導出。導入使用:require函數導入。
  • AMD是社群開發的子產品化文法,需要依賴require.js實作,分為定義子產品,導出資料和導入子產品,使用資料。AMD文法的導入是依賴前置的,也就是說,需要用到的檔案需要在第一次打開頁面全部加載完成,造成的後果就是首屏加載很慢,後續操作會很流暢。
  • CMD是玉伯開發的子產品化文法,需要依賴sea.js實作,也分為子產品定義導出,和子產品導入使用資料。CMD文法可以依賴前置,也可以按需導入,緩解了AMD文法的依賴前置。
  • ES6的子產品化文法,類似于commonJS的文法,分為資料導出和資料導入,導入導出更加靈活。

43、 什麼是懶加載和預加載 ?

  • 懶加載:懶加載也叫延遲加載,延遲加載網絡資源或符合某些條件時才加載資源。常見的就是圖檔延時加載。懶加載的意義:懶加載的主要目的是作為伺服器前端的優化,減少請求數或延遲請求數。懶惰實作方式:
    • 1.第一種是純粹的延遲加載,使用setTimeOut或setInterval進行加載延遲.
    • 2.第二種是條件加載,符合某些條件,或觸發了某些事件才開始異步下載下傳。
    • 3.第三種是可視區加載,即僅加載使用者可以看到的區域,這個主要由監控滾動條來實作,一般會在距使用者看到某圖檔前一定距離便開始加載,這樣能保證使用者拉下時正好能看到圖檔。
  • 預加載:提前加載圖檔,當使用者需要檢視時可直接從本地緩存中渲染。

兩者的行為是相反的,一個是提前加載,一個是遲緩甚至不加載。懶加載對伺服器前端有一定的緩解壓力作用,預加載則會增加伺服器前端壓力。預加載應用如廣告彈窗等。

44、token 一般存放在哪裡 ? 為什麼不存放在 cookie 内 ?

token一般放在本地存儲中。token的存在本身隻關心請求的安全性,而不關心token本身的安全,因為token是伺服器端生成的,可以了解為一種加密技術。但如果存在cookie内的話,浏覽器的請求預設會自動在請求頭中攜帶cookie,是以容易受到csrf攻擊。

45、 less 和 sass 的差別 ?

  • 編譯環境不一樣,sass是伺服器端處理的,可以用Ruby、node-sass來編譯;less需要引入less.js來處理輸出,也可以使用工具在伺服器端處理成css,也有線上編譯的。
  • 變量定義符不一樣,less用的是@,而sass用$。
  • sass支援分支語句,less不支援

44、浏覽器的同源政策機制 ?

同源政策,又稱SOP,全稱Same Origin Policy,是浏覽器最基本的安全功能。站在浏覽器的較短看網頁,如果網絡上的接口可以不受限制、無需授權随意被人調用,那将是一個非常嚴重的混亂場景。浏覽器為了安全有序,内部實作了同源政策。

同源政策,指的是浏覽器限制目前網頁隻能通路同源的接口資源。

所謂同源,指目前頁面和請求的接口,兩方必須是同協定、且同域名、且同端口。隻要有一個不相同,則會受到浏覽器限制,不允許請求。

但當一個項目變的很大的時候,将所有内容放在一個網站或一個伺服器中會讓網站變的臃腫且性能低下,是以,在一些場景中,我們需要跨過同源政策,請求到不同源的接口資源,這種場景叫跨域。

跨域大緻有3種方案:

  • jsonp
  • 這種方式是利用浏覽器不限制某些标簽發送跨域請求,例如link、img、iframe、script。通常請求請求回來的資源要在js中進行處理,是以jsonp跨域是利用script标簽進行發送,且這種請求方式隻能是get請求。
  • cors
  • 這種方式是讓接口資源方面進行授權,授權允許通路。在接口資源處添加響應頭即可通過浏覽器的同源政策,響應頭具體的鍵值對如下:
  • {Access-Control-Allow-Origin: '*'}
  • proxy
  • 這種方式屬于找外援的一種方式,浏覽器隻能限制目前正在打開的web頁面發送請求,但無法限制伺服器端請求接口資源。是以我們可以将請求發送到自己伺服器,然後自己伺服器去請求目标接口資源,最後自己伺服器将接口資源傳回給目前頁面,類似于找外援代替自己請求目标接口資源。
  • 這種方式通常要對伺服器進行代理配置,需要對apache伺服器、nginx伺服器、nodejs伺服器進行配置。

45、 浏覽器的緩存有哪些 ? 什麼時候使用強制緩存 ? 什麼時候使用協商緩存 ?

當我們通路同一個頁面時,請求資源、資料都是需要一定的耗時,如果可以将一些資源緩存下來,那麼從第二次通路開始,就可以減少加載時間,提高使用者體驗,也能減輕伺服器的壓力。

浏覽器緩存分為強緩存和協商緩存,當存在緩存時,用戶端第一次向伺服器請求資料時,用戶端會緩存到記憶體或者硬碟當中,當第二次擷取相同的資源,強緩存和協商緩存的應對方式有所不同。

強緩存:當用戶端第二次向伺服器請求相同的資源時,不會向伺服器發送請求,而是直接從記憶體/硬碟中間讀取。緩存由伺服器的響應頭裡 cache-control 和 expires 兩個字段決定

協商緩存:當用戶端第二次向伺服器請求相同的資源時,先向伺服器發送請求"詢問"該請求的檔案緩存在ben'd與伺服器相比是否更改,如果更改,則更新檔案,如果沒有就從記憶體/硬碟中讀取。協商緩存由 last-modified 和 etag兩個字段決定

46、 數組方法 forEach 和 map 的差別 ?

forEach和map都是循環周遊數組中的每一項。forEach() 和 map() 裡面每一次執行匿名函數都支援3個參數:數組中的目前項item,目前項的索引index,原始數組input。匿名函數中的this都是指Window。隻能周遊數組。

他們的差別是:forEach沒有傳回值,但map中要有傳回值,傳回處理後的所有新元素組成的數組。

47、 什麼是函數作用域 ? 什麼是作用域鍊 ?

作用域就是在代碼執行過程中,形成一個獨立的空間,讓空間内的變量不會洩露在空間外,也讓獨立空間内的變量函數在獨立空間内運作,而不會影響到外部的環境。

作用域分為全局作用域和局部作用域,也就是本來有一個巨大的空間,空間内定義的函數内部,就形成了一個獨立的小空間,全局作用域是最大的作用域。

但是當獨立空間内的資料不能滿足需求時,是可以從外部擷取資料的,也就是說這樣的獨立空間之間是可以有層級關系的,外部的空間不可以從内部的空間擷取資料,但内部的空間可以。當子級空間在父級空間中擷取資料的時,父級空間沒有的話,父級空間也會到他的父級空間中查找資料,這樣形成的鍊式結構叫作用域鍊。

當将一個變量當做值使用時,會先在目前作用域中查找這個變量的定義和資料,如果沒有定義的話,就會去父級作用域中查找,如果父級作用域中有的話就使用這個值,如果父級作用域中也沒有的話,就通過父級作用域查找他的父級作用域,直到找到最大的作用域-全局,如果全局也沒有就報錯。

當将一個變量當做資料容器存儲,也就是給變量指派的時候,也要先在自己作用域中查找變量的定義,如果沒有就在上一級作用域中查找,直到全局,如果全局作用域中也沒有這個變量的定義,就在全局定義這個變量并指派。

48、 ES6 中 Set 和 Map 的原理 ?

Set 是無重複值的有序清單。根據 `Object.is()`方法來判斷其中的值不相等,以保證無重複。Set 會自動移除重複的值,是以你可以使用它來過濾數組中的重複值并傳回結果。Set并不是數組的子類型,是以你無法随機通路其中的值。但你可以使用`has()` 方法來判斷某個值是否存在于 Set 中,或通過 `size` 屬性來檢視其中有多少個值。Set 類型還擁有`forEach()`方法,用于處理每個值

Map 是有序的鍵值對,其中的鍵允許是任何類型。與 Set 相似,通過調用 `Object.is()`方法來判斷重複的鍵,這意味着能将數值 5 與字元串 "5" 作為兩個相對獨立的鍵。使用`set()` 方法能将任何類型的值關聯到某個鍵上,并且該值此後能用 `get()` 方法提取出來。Map 也擁有一個 `size` 屬性與一個 `forEach()` 方法,讓項目通路更容易。

49、 0.1 + 0.2 為什麼不等于 0.3, 在項目中遇到要怎麼處理 ?

計算機内部存儲資料使用2進制存儲,兩個數字進行的數學運算,首先是将這兩個數字以2進制形式,存儲在計算機内部,然後在計算機内部使用兩個2進制數字進行計算,最後将計算結果的2進制數字轉為10進制展示出來。

由于10進制的小數在轉2進制的時候,規則是小數部分乘以2,判斷是否得到一個整數,如果得到整數,轉換完成;如果沒有得到整數,則繼續乘以2判斷。是以,0.1和0.2在轉換2進制的時候,其實是一個無限死循環,也就是一直乘以2沒有得到整數的時候,但計算機内部對于無限死循環的資料,會根據一個标準保留52位。也就是說,計算機内部在存儲0.1和0.2的時候,本來就不精準,兩個不精準的小數在計算後,距離精準的結果是有一定誤差的。

項目中碰到這種情況,有3種處理方法:

  • 将小數乘以10的倍數,轉為整數,然後計算,計算完成後,再縮小10的倍數,例如:
var result = ((0.1 * 10) + (0.2 * 10)) / 10
    // result === 0.3           
  • 使用數字的toFixed方法,強制保留小數點後多少位,例
var result = (0.1 + 0.2).toFixed(2)
   // result === 0.30           
  • 自定義數字運算方法,當需要進行數學運算的時候,不直接進行,調用自定義的方法進行,例:(加法封裝)
function add(...args){
         var num = args.find(item => {
             if(item != 0 && !item){
                throw new Error("數學運算要使用數字")
             }
         })
         var arr = args.map(item => {
             var index = (item+'').indexOf('.')
             if(index >= 0){
                 return (item+'').split('.')[1].length
             }
         })
         arr = arr.filter(item => item)
         if(arr.length){
             var max = Math.max(...arr)
             var data = args.map(item => item * Math.pow(10, max))
             var data.reduce((a, b) => a + b) / Math.pow(10, max)
         }else{
             var data = args
             return data.reduce((a, b) => a + b)
         }
     }
     // 調用使用:
     var num1 = add(0.1, 0.2)
     console.log(num1); // 0.3

     var num2 = add(1, 2)
     console.log(num2); // 3

     var num3 = add(1, 2.1)
     console.log(num3); // 3.1           

50、 什麼是子產品化思想 ?

就是JS中将不同功能的代碼封裝在不同的檔案中, 互相引用時不會發生命名沖突的一種思想, 大多數情況下, 一個檔案就是一個子產品

子產品化的實作,有多種方案:

  • CommonJS:
  • CommonJS是nodejs中使用的子產品化規範在 nodejs 應用中每個檔案就是一個子產品,擁有自己的作用域,檔案中的變量、函數都是私有的,與其他檔案相隔離。子產品導出:module.exports=資料,子產品導入:require('子產品檔案路徑')
  • ES6的子產品化:
  • 子產品功能主要由兩個指令構成:export和import。export指令用于規定子產品的對外接口,import指令用于輸入其他子產品提供的功能。
  • 一個子產品就是一個獨立的檔案。該檔案内部的所有變量,外部無法擷取。如果你希望外部能夠讀取子產品内部的某個變量,就必須使用export關鍵字輸出該變量。下面是一個 JS 檔案,裡面使用export指令輸出變量。
  • AMD (Asynchronous Module Definition):
  • 特點: 提倡依賴前置,在定義子產品的時候就要聲明其依賴的子產品:導入子產品require([module],callback);定義子產品:define('子產品名稱', 函數)。
  • CMD (Common Module Definition):
  • CMD規範是國内SeaJS的推廣過程中産生的。提倡就近依賴(按需加載),在用到某個子產品的時候再去require。定義子產品:define(function (require, exports, module) {}),使用子產品:seajs.use()

51、 說說怎麼用js 寫無縫輪播圖

将所有需要輪播的内容動态複制一份,放在原本的容器中,加定時器讓整個容器中的内容滾動輪播,當内容輪播到left值為-原本的内容寬度時,快速将内容切換到left值為0的狀态。

52、 JS 如何實作多線程 ?

我們都知道JS是一種單線程語言,即使是一些異步的事件也是在JS的主線程上運作的(具體是怎麼運作的,可以看我另一篇部落格JS代碼運作機制)。像setTimeout、ajax的異步請求,或者是dom元素的一些事件,都是在JS主線程執行的,這些操作并沒有在浏覽器中開辟新的線程去執行,而是當這些異步操作被操作時或者是被觸發時才進入事件隊列,然後在JS主線程中開始運作。

首先說一下浏覽器的線程,浏覽器中主要的線程包括,UI渲染線程,JS主線程,GUI事件觸發線程,http請求線程。

JS作為腳本語言,它的主要用途是與使用者互動,以及操作DOM。這決定了它隻能是單線程,否則會帶來很複雜的同步問題。(這裡這些問題我們不做研究)

但是單線程的語言,有一個很緻命的确定。如果說一個腳本語言在執行時,其中某一塊的功能在執行時耗費了大量的時間,那麼就會造成阻塞。這樣的項目,使用者體驗是非常差的,是以這種現象在項目的開發過程中是不允許存在的。

其實JS為我們提供了一個Worker的類,它的作用就是為了解決這種阻塞的現象。當我們使用這個類的時候,它就會向浏覽器申請一個新的線程。這個線程就用來單獨執行一個js檔案。 var worker = new Worker(js檔案路徑);那麼這個語句就會申請一個線程用來執行這個js檔案。這樣也就實作了js的多線程。

53、 閉包的使用場景 ?

一個函數被當作值傳回時,也就相當于傳回了一個通道,這個通道可以通路這個函數詞法作用域中的變量,即函數所需要的資料結構儲存了下來,資料結構中的值在外層函數執行時建立,外層函數執行完畢時理因銷毀,但由于内部函數作為值傳回出去,這些值得以儲存下來。而且無法直接通路,必須通過傳回的函數。這也就是私有性。

本來執行過程和詞法作用域是封閉的,這種傳回的函數就好比是一個蟲洞,開了挂。

閉包的形成很簡單,在執行過程完畢後,傳回函數,或者将函數得以保留下來,即形成閉包。

  • 防抖:
function debounce(fn, interval) {
       let timer = null; // 定時器
       return function() {
           // 清除上一次的定時器
           clearTimeout(timer);
           // 拿到目前的函數作用域
           let _this = this;
           // 拿到目前函數的參數數組
           let args = Array.prototype.slice.call(arguments, 0);
           // 開啟倒計時定時器
           timer = setTimeout(function() {
               // 通過apply傳遞目前函數this,以及參數
               fn.apply(_this, args);
               // 預設300ms執行
           }, interval || 300)
       }
   }           
  • 節流:
function throttle(fn, interval) {
      let timer = null; // 定時器
      let firstTime = true; // 判斷是否是第一次執行
      // 利用閉包
      return function() {
          // 拿到函數的參數數組
          let args = Array.prototype.slice.call(arguments, 0);
          // 拿到目前的函數作用域
          let _this = this;
          // 如果是第一次執行的話,需要立即執行該函數
          if(firstTime) {
              // 通過apply,綁定目前函數的作用域以及傳遞參數
              fn.apply(_this, args);
              // 修改辨別為null,釋放記憶體
              firstTime = null;
          }
          // 如果目前有正在等待執行的函數則直接傳回
          if(timer) return;
          // 開啟一個倒計時定時器
          timer = setTimeout(function() {
              // 通過apply,綁定目前函數的作用域以及傳遞參數
              fn.apply(_this, args);
              // 清除之前的定時器
              timer = null;
              // 預設300ms執行一次
          }, interval || 300)
      }
  }           
  • 疊代器:
var arr =['aa','bb','cc'];
   function incre(arr){
       var i=0;
       return function(){
           //這個函數每次被執行都傳回數組arr中 i下标對應的元素
            return arr[i++] || '數組值已經周遊完';
       }
   }
   var next = incre(arr);
   console.log(next());//aa
   console.log(next());//bb
   console.log(next());//cc
   console.log(next());//數組值已經周遊完           
  • 緩存:
var fn=(function(){
           var cache={};//緩存對象
           var calc=function(arr){//計算函數
               var sum=0;
               //求和
               for(var i=0;i<arr.length;i++){
                   sum+=arr[i];
               }
               return sum;
           }

           return function(){
               var args = Array.prototype.slice.call(arguments,0);//arguments轉換成數組
               var key=args.join(",");//将args用逗号連接配接成字元串
               var result , tSum = cache[key];
               if(tSum){//如果緩存有
                   console.log('從緩存中取:',cache)//列印友善檢視
                   result = tSum;
               }else{
                   //重新計算,并存入緩存同時指派給result
                   result = cache[key]=calc(args);
                   console.log('存入緩存:',cache)//列印友善檢視
               }
               return result;
           }
        })();
       fn(1,2,3,4,5);
       fn(1,2,3,4,5);
       fn(1,2,3,4,5,6);
       fn(1,2,3,4,5,8);
       fn(1,2,3,4,5,6);           
  • getter和setter:
function fn(){
          var name='hello'
          setName=function(n){
              name = n;
          }
          getName=function(){
              return name;
          }

          //将setName,getName作為對象的屬性傳回
          return {
              setName:setName,
              getName:getName
          }
      }
      var fn1 = fn();//傳回對象,屬性setName和getName是兩個函數
      console.log(fn1.getName());//getter
          fn1.setName('world');//setter修改閉包裡面的name
      console.log(fn1.getName());//getter           
  • 柯裡化:
function curryingCheck(reg) {
      return function(txt) {
          return reg.test(txt)
      }
  }

  var hasNumber = curryingCheck(/\d+/g)
  var hasLetter = curryingCheck(/[a-z]+/g)

  hasNumber('test1')      // true
  hasNumber('testtest')   // false
  hasLetter('21212')      // false           
  • 循環中綁定事件或執行異步代碼
var p1 = "ss";
   var p2 = "jj";
   function testSetTime(para1,para2){
       return (function(){
           console.log(para1 + "-" + para2);
       })
   }
   var test = testSetTime(p1, p2);
   setTimeout(test, 1000);
   setTimeout(function(){
       console.log(p1 + "-" + p2)
   },1000)           
  • 單例模式:
var Singleton = (function () {
      var instance;

      function createInstance() {
          return new Object("I am the instance");
      }

      return {
          getInstance: function () {
              if (!instance) {
                  instance = createInstance();
              }
              return instance;
          }
      };
  })();           

54、 常見的相容問題有哪些 ?

  • 擷取标簽節點:
  • document.getElementsByClassName('類名')在低版本ie中不相容。解決方法是使用其他方式擷取:
document.getElementById('id名')
   document.getElementsByTagName('标簽名')
   document.getElementsByName('name屬性值')
   document.querySelector('css選擇器')
   document.querySelectorAll('css選擇器')           

擷取卷去的高度

// 當有文檔聲明的時候
document.documentElement.scrollTop
document.documentElement.srollLeft
// 沒有文檔聲明的時候
document.body.scrollTop
document.body.scrollLeft           

解決辦法使用相容寫法:

// 擷取
var t = document.documentElement.scrollTop || document.body.scrollTop
var l = document.documentElement.srollLeft || document.body.scrollLeft
// 設定
document.documentElement.scrollTop = document.body.scrollTop = 數值
document.documentElement.srollLeft = document.body.scrollLeft = 數值           

擷取樣式

// W3C标準浏覽器
    window.getComputedStyle(元素)
    // 低版本IE中
    元素.currentStyle           

使用函數封裝的方式相容:

function getStyle(ele,attr){
        if(window.getComputedStyle){
           return getComputedStyle(ele)[attr]
        }else{
            return ele.currentStyle[attr]
        }
    }           

事件偵聽器

// W3C浏覽器
  ele.addEventListener(事件類型,函數)
  // 低版本Ie
  ele.attachEvent('on事件類型',函數)           

使用函數封裝的方式解決:

function bindEvent(ele,type,handler){
      if(ele.addEventListener){
          ele.addEventListener(type,handler)
      }else if(ele.attachEvent){
          ele.attachEvent('on'+type,handler)
      }else{
          ele['on'+type] = handler
      }
  }           

事件解綁

// W3C浏覽器
  ele.removeEventListener(事件類型,函數)
  // 低版本Ie
  ele.detachEvent('on事件類型',函數)           

使用函數封裝的方式解決:

function unBind(ele,type,handler){
        if(ele.removeEventListener){
            ele.removeEventListener(type,handler)
        }else if(ele.detachEvent){
            ele.detachEvent('on'+type,handler)
        }else{
            ele['on'+type] = null
        }
    }           

事件對象的擷取

// W3C浏覽器
   元素.on事件類型 = function(e){}
   元素.addEventListener(事件類型,fn)
   function fn(e){

   }
   // 在低版本IE中
   元素.on事件類型 = function(){ window.event }
   元素.addEventListener(事件類型,fn)
   function fn(){
       window.event
   }           

使用短路運算符解決:

元素.on事件類型 = function(e){
        var e = e || window.event
    }
    元素.addEventListener(事件類型,fn)
    function fn(e){
        var e = e || window.event
    }           

阻止預設行為

// W3C浏覽器
   元素.on事件類型 = function(e){
       e.preventDefault()
   }
   // 在低版本IE中
   元素.on事件類型 = function(){ window.event.returnValue = false }           

通過封裝函數解決

元素.on事件類型 = function(e){
         var e = e || window.event
         e.preventDefault?e.preventDefault():e.returnValue=false
     }           

阻止事件冒泡

// W3C浏覽器
     元素.on事件類型 = function(e){
         e.stopPropagation()
     }
     // 在低版本IE中
     元素.on事件類型 = function(){ window.event.cancelBubble = true }           

通過函數封裝解決:

元素.on事件類型 = function(e){
     var e = e || window.event
     e.stopPropagation?e.stopPropagation():e.cancelBubble=true
 }           

擷取精準的目标元素

// W3C浏覽器
    元素.on事件類型 = function(e){
        e.target
    }
    // 在低版本IE中
    元素.on事件類型 = function(){ window.event.srcElement }           

通過短路運算符解決:

元素.on事件類型 = function(e){
        var e = e || window.event
        var target = e.target || e.srcElement;
    }           

擷取鍵盤碼

// W3C浏覽器
     元素.on事件類型 = function(e){
         e.keyCode
     }
     // 在低版本火狐中
     元素.on事件類型 = function(e){
       e.which
     }           

通過短路運算符解決:

元素.on事件類型 = function(e){
    var e = e || window.event
    var keycode = e.keyCode || e.which;
}           

55、 在 JS 中如何阻止事件冒泡 ?

使用事件對象阻止事件冒泡,以前的w3c浏覽器中,使用事件對象的方法阻止:

事件對象.stopPropagation()

在ie低版本浏覽器中,使用事件對象的屬性阻止:

事件對象.cancelBubble = true

現在的w3c浏覽器也支援ie低版本浏覽器中的寫法,是以以前在阻止事件冒泡的時候,需要考慮相容寫法,現在就不需要了,直接用ie低版本浏覽器中的寫法即可。

56、兩個數組 var A = [1, 5, 6]; var B = [2, 6, 7],實作一個方法,找出僅存在于A 或者 僅 存在于B中的所有數字。

function getDiff(arr, brr){
    // 僅存在于arr中的内容
      var onlyArr = arr.filter(item => !brr.some(v => item === v))
      // 僅存在于brr中的内容
      var onlyBrr = brr.filter(v => !arr.some(item => v === item))
      // 需要哪個就傳回哪個,或者一起傳回
      return {
          "僅存在于arr中的内容": onlyArr,
          "僅存在于brr中的内容": onlyBrr
      }
  }           

57、 你了解構造函數嗎 ? class 是什麼 ? 兩者有什麼差別 ?

在es5中構造函數其實就是在定義一個類,可以執行個體化對象,es6中class其實是構造函數的文法糖。但還是有點差別的:

  • 在class内部和class的方法内部,預設使用嚴格模式
  • class類不存在預解析,也就是不能先調用class生成執行個體,再定義class類,但是構造函數可以。
  • class中定義的方法預設不能被枚舉,也就是不能被周遊。
  • class必須使用new執行,但是構造函數沒有new也可以執行。
  • class中的所有方法都沒有原型,也就不能被new
  • class中繼承可以繼承靜态方法,但是構造函數的繼承不能。

58、是否存在a的值(a==0 && a==1)為true 的情況 ?

var value = -1
   Object.defineProperty(window,'a',{
       get(){
           return value+=1;
       }
   })
   if(a===0&&a===1){ // true
       console.log('success')
   }           

59、for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 1000); } 要求:輸出0,1,2,3,4

首先這個面試題考察的是對于js中異步代碼以及作用域的了解:

js中常見的異步代碼包括定時器和ajax。js執行代碼的流程是碰到同步代碼就執行,碰到異步就交給浏覽器的webAPI處理,當webAPI中的異步該執行時,webAPI會将需要執行的回調函數放在任務隊列中,等候執行,是以,js中所有的異步代碼總會在所有同步代碼執行結束後,再執行任務隊列中的代碼。

在這個問題中,循環是同步代碼,定時器是異步代碼,是以整個循環都執行結束以後才會執行定時器代碼。

for循環中使用var定義的變量是全局變量,定時器回調函數中輸出變量的時候,根據作用域規則,先在目前作用域中變量i的定義表達式,如果沒有找到,就去上一級作用域中找,此時,在局部作用域中沒有找到,去上級作用域中,也就是全局找到了,全局中的i,因為循環已經執行結束了,是以i的值是5。

最終,會輸出5個5。

其次考察的是對于類似問題的解決方式,間接性判斷你是否有過類似情況的開發:

這個問題的解決思路就是讓回調函數中輸出i的時候,不要去全局中找i,因為全局的i在循環執行結束後已經變成5了,根據這個思路,有2種解決辦法:

  • 在異步代碼外面嵌套一層函數作用域
for(var i = 0;i < 5; i++){
    (function(i) {
        setTimeout(function() {
            console.log(i)
        }, 1000)
    })(i)
}           

原理是自調用函數會産生作用域,循環5次就會産生5個作用域,每個作用域代碼在執行的時候都有形參i傳遞。是以每個作用域中的i都是不同的,分别是:0 1 2 3 4。當作用域中的異步代碼執行的時候,自己作用域中沒有i變量的定義,然後上級作用域就是自調用函數的作用域,找到了單獨的i。最終可以輸出:0 1 2 3 4

  1. 将循環代碼中的var換成es6的let
for(let i = 0;i < 5; i++){
         setTimeout(function() {
             console.log(i)
         }, 1000)
     }           

es6的let自帶塊級作用域,原理跟第一種解決思路是一樣的,轉成es5後,代碼是一樣的。

60、實作一個 add 方法 使計算結果能夠滿足如下預期: - add(1)(2)(3)() = 6 - add(1,2,3)(4)() = 10

function add (...args) {
    if(args.length === 3){
        return -(args[0] * args[1] * 2 + args[2] * 2)
    }else{
        return -args[args.length-1]
    }

}

function currying (fn) {
  let args = []
  return function _c (...newArgs) {
    if (newArgs.length) {
      args = [
        ...args,
        ...newArgs
      ]
      return _c
    } else {
      return fn.apply(this, args)
    }
  }
}
let addCurry = currying(add)

var a = addCurry(1)(2)(3)()
console.log(-a); // 10

var b = addCurry(1,2,3)(4)()
console.log(6 - b); // 10           

繼續閱讀