整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          前端經典面試題(60道前端面試題包含JS、CSS、React、瀏覽器等)

          答題

          1、什么是防抖和節流?有什么區別?如何實現?

          參考答案
          

          防抖

          觸發高頻事件后n秒內函數只會執行一次,如果n秒內高頻事件再次被觸發,則重新計算時間

          • 思路:

          每次觸發事件時都取消之前的延時調用方法

          function debounce(fn) {
                let timeout = null; // 創建一個標記用來存放定時器的返回值
                return function () {
                  clearTimeout(timeout); // 每當用戶輸入的時候把前一個 setTimeout clear 掉
                  timeout = setTimeout(() => { // 然后又創建一個新的 setTimeout, 這樣就能保證輸入字符后的 interval 間隔內如果還有字符輸入的話,就不會執行 fn 函數
                    fn.apply(this, arguments);
                  }, 500);
                };
              }
              function sayHi() {
                console.log('防抖成功');
              }
          
              var inp = document.getElementById('inp');
              inp.addEventListener('input', debounce(sayHi)); // 防抖
          

          節流

          高頻事件觸發,但在n秒內只會執行一次,所以節流會稀釋函數的執行頻率

          • 思路:

          每次觸發事件時都判斷當前是否有等待執行的延時函數

          function throttle(fn) {
                let canRun = true; // 通過閉包保存一個標記
                return function () {
                  if (!canRun) return; // 在函數開頭判斷標記是否為true,不為true則return
                  canRun = false; // 立即設置為false
                  setTimeout(() => { // 將外部傳入的函數的執行放在setTimeout中
                    fn.apply(this, arguments);
                    // 最后在setTimeout執行完畢后再把標記設置為true(關鍵)表示可以執行下一次循環了。當定時器沒有執行的時候標記永遠是false,在開頭被return掉
                    canRun = true;
                  }, 500);
                };
              }
              function sayHi(e) {
                console.log(e.target.innerWidth, e.target.innerHeight);
              }
              window.addEventListener('resize', throttle(sayHi));
          

          2、 get請求傳參長度的誤區、get和post請求在緩存方面的區別

          誤區:我們經常說get請求參數的大小存在限制,而post請求的參數大小是無限制的。

          參考答案

          實際上HTTP 協議從未規定 GET/POST 的請求長度限制是多少。對get請求參數的限制是來源與瀏覽器或web服務器,瀏覽器或web服務器限制了url的長度。為了明確這個概念,我們必須再次強調下面幾點:

          • HTTP 協議 未規定 GET 和POST的長度限制
          • GET的最大長度顯示是因為 瀏覽器和 web服務器限制了 URI的長度
          • 不同的瀏覽器和WEB服務器,限制的最大長度不一樣
          • 要支持IE,則最大長度為2083byte,若只支持Chrome,則最大長度 8182byte

          補充補充一個get和post在緩存方面的區別:

          • get請求類似于查找的過程,用戶獲取數據,可以不用每次都與數據庫連接,所以可以使用緩存。
          • post不同,post做的一般是修改和刪除的工作,所以必須與數據庫交互,所以不能使用緩存。因此get請求適合于請求緩存。

          3、模塊化發展歷程

          可從IIFE、AMD、CMD、CommonJS、UMD、webpack(require.ensure)、ES Module、<script type="module"> 這幾個角度考慮。

          參考答案
          

          模塊化主要是用來抽離公共代碼,隔離作用域,避免變量沖突等。

          IIFE:使用自執行函數來編寫模塊化,特點:在一個單獨的函數作用域中執行代碼,避免變量沖突

          (function(){
            return {
              data:[]
            }
          })()
          

          AMD:使用requireJS 來編寫模塊化,特點:依賴必須提前聲明好

          define('./index.js',function(code){
              // code 就是index.js 返回的內容
          })
          

          CMD:使用seaJS 來編寫模塊化,特點:支持動態引入依賴文件

          define(function(require, exports, module) {  
            var indexCode = require('./index.js');
          })
          

          CommonJS:nodejs 中自帶的模塊化。

          var fs = require('fs');
          

          UMD:兼容AMD,CommonJS 模塊化語法。

          webpack(require.ensure):webpack 2.x 版本中的代碼分割。

          ES Modules:ES6 引入的模塊化,支持import 來引入另一個 js 。

          import a from 'a';
          

          4、npm 模塊安裝機制,為什么輸入 npm install 就可以自動安裝對應的模塊?

          參考答案
          

          1. npm 模塊安裝機制:

          • 發出npm install命令
          • 查詢node_modules目錄之中是否已經存在指定模塊
            • npm 向 registry 查詢模塊壓縮包的網址
            • 下載壓縮包,存放在根目錄下的.npm目錄里
            • 解壓壓縮包到當前項目的node_modules目錄
            • 若存在,不再重新安裝
            • 若不存在

          2. npm 實現原理

          輸入 npm install 命令并敲下回車后,會經歷如下幾個階段(以 npm 5.5.1 為例):

          1. 執行工程自身 preinstall
            當前 npm 工程如果定義了 preinstall 鉤子此時會被執行。
          2. 確定首層依賴模塊
            首先需要做的是確定工程中的首層依賴,也就是 dependencies 和 devDependencies 屬性中直接指定的模塊(假設此時沒有添加 npm install 參數)。
            工程本身是整棵依賴樹的根節點,每個首層依賴模塊都是根節點下面的一棵子樹,npm 會開啟多進程從每個首層依賴模塊開始逐步尋找更深層級的節點。
          3. 獲取模塊
            獲取模塊是一個遞歸的過程,分為以下幾步:
          • 獲取模塊信息。在下載一個模塊之前,首先要確定其版本,這是因為 package.json 中往往是 semantic version(semver,語義化版本)。此時如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有該模塊信息直接拿即可,如果沒有則從倉庫獲取。如 packaeg.json 中某個包的版本是 ^1.1.0,npm 就會去倉庫中獲取符合 1.x.x 形式的最新版本。
          • 獲取模塊實體。上一步會獲取到模塊的壓縮包地址(resolved 字段),npm 會用此地址檢查本地緩存,緩存中有就直接拿,如果沒有則從倉庫下載。
          • 查找該模塊依賴,如果有依賴則回到第1步,如果沒有則停止。
        1. 模塊扁平化(dedupe)
          上一步獲取到的是一棵完整的依賴樹,其中可能包含大量重復模塊。比如 A 模塊依賴于 loadsh,B 模塊同樣依賴于 lodash。在 npm3 以前會嚴格按照依賴樹的結構進行安裝,因此會造成模塊冗余。
          從 npm3 開始默認加入了一個 dedupe 的過程。它會遍歷所有節點,逐個將模塊放在根節點下面,也就是 node-modules 的第一層。當發現有
          重復模塊時,則將其丟棄。
          這里需要對
          重復模塊進行一個定義,它指的是模塊名相同semver 兼容。每個 semver 都對應一段版本允許范圍,如果兩個模塊的版本允許范圍存在交集,那么就可以得到一個兼容版本,而不必版本號完全一致,這可以使更多冗余模塊在 dedupe 過程中被去掉。
          比如 node-modules 下 foo 模塊依賴 lodash@^1.0.0,bar 模塊依賴 lodash@^1.1.0,則
          ^1.1.0 為兼容版本。
          而當 foo 依賴 lodash@^2.0.0,bar 依賴 lodash@^1.1.0,則依據 semver 的規則,二者不存在兼容版本。會將一個版本放在 node_modules 中,另一個仍保留在依賴樹里。
          舉個例子,假設一個依賴樹原本是這樣:
          node_modules -- foo ---- lodash@version1
          -- bar ---- lodash@version2
          假設 version1 和 version2 是兼容版本,則經過 dedupe 會成為下面的形式:
          node_modules -- foo
          -- bar
          -- lodash(保留的版本為兼容版本)
          假設 version1 和 version2 為非兼容版本,則后面的版本保留在依賴樹中:
          node_modules -- foo -- lodash@version1
          -- bar ---- lodash@version2
        2. 安裝模塊
          這一步將會更新工程中的 node_modules,并執行模塊中的生命周期函數(按照 preinstall、install、postinstall 的順序)。
        3. 執行工程自身生命周期
          當前 npm 工程如果定義了鉤子此時會被執行(按照 install、postinstall、prepublish、prepare 的順序)。
          最后一步是生成或更新版本描述文件,npm install 過程完成。
        4. 5、ES5的繼承和ES6的繼承有什么區別?

          參考答案
          

          ES5的繼承時通過prototype或構造函數機制來實現。ES5的繼承實質上是先創建子類的實例對象,然后再將父類的方法添加到this上(Parent.apply(this))。

          ES6的繼承機制完全不同,實質上是先創建父類的實例對象this(所以必須先調用父類的super()方法),然后再用子類的構造函數修改this

          具體的:ES6通過class關鍵字定義類,里面有構造方法,類之間通過extends關鍵字實現繼承。子類必須在constructor方法中調用super方法,否則新建實例報錯。因為子類沒有自己的this對象,而是繼承了父類的this對象,然后對其進行加工。如果不調用super方法,子類得不到this對象。

          ps:super關鍵字指代父類的實例,即父類的this對象。在子類構造函數中,調用super后,才可使用this關鍵字,否則報錯。

          6、setTimeout、Promise、Async/Await 的區別

          參考答案

          7、定時器的執行順序或機制?

          參考答案
          

          **因為js是單線程的,瀏覽器遇到setTimeout或者setInterval會先執行完當前的代碼塊,在此之前會把定時器推入瀏覽器的待執行事件隊列里面,等到瀏覽器執行完當前代碼之后會看一下事件隊列里面有沒有任務,有的話才執行定時器的代碼。**所以即使把定時器的時間設置為0還是會先執行當前的一些代碼。

          function test(){
              var aa = 0;
              var testSet = setInterval(function(){
                  aa++;
                  console.log(123);
                  if(aa<10){
                      clearInterval(testSet);
                  }
              },20);
            var testSet1 = setTimeout(function(){
              console.log(321)
            },1000);
            for(var i=0;i<10;i++){
              console.log('test');
            }
          }
          test()
          

          輸出結果:

          test //10次
          undefined
          123
          321
          

          8、['1','2','3'].map(parseInt) 輸出什么,為什么?

          參考答案
          

          輸出:[1, NaN, NaN]

          • 首先讓我們回顧一下,map函數的第一個參數callback:

          var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg])這個callback一共可以接收三個參數,其中第一個參數代表當前被處理的元素,而第二個參數代表該元素的索引。

          • 而parseInt則是用來解析字符串的,使字符串成為指定基數的整數。parseInt(string, radix)接收兩個參數,第一個表示被處理的值(字符串),第二個表示為解析時的基數。
          • 了解這兩個函數后,我們可以模擬一下運行情況
          1. parseInt('1', 0) //radix為0時,且string參數不以“0x”和“0”開頭時,按照10為基數處理。這個時候返回1
          2. parseInt('2', 1) //基數為1(1進制)表示的數中,最大值小于2,所以無法解析,返回NaN
          3. parseInt('3', 2) //基數為2(2進制)表示的數中,最大值小于3,所以無法解析,返回NaN
          • map函數返回的是一個數組,所以最后結果為[1, NaN, NaN]

          9、Doctype作用? 嚴格模式與混雜模式如何區分?它們有何意義?

          參考答案
          

          Doctype聲明于文檔最前面,告訴瀏覽器以何種方式來渲染頁面,這里有兩種模式,嚴格模式和混雜模式。

          • 嚴格模式的排版和 JS 運作模式是 以該瀏覽器支持的最高標準運行。
          • 混雜模式,向后兼容,模擬老式瀏覽器,防止瀏覽器無法兼容頁面。

          10、fetch發送2次請求的原因

          參考答案
          

          fetch發送post請求的時候,總是發送2次,第一次狀態碼是204,第二次才成功?

          原因很簡單,因為你用fetch的post請求的時候,導致fetch 第一次發送了一個Options請求,詢問服務器是否支持修改的請求頭,如果服務器支持,則在第二次中發送真正的請求。


          • location.href-- 返回或設置當前文檔的URL
          • location.search -- 返回URL中的查詢字符串部分。例如 http://www.dreamdu.com/dreamd... 返回包括(?)后面的內容?id=5&name=dreamdu
          • location.hash -- 返回URL#后面的內容,如果沒有#,返回空 location.host -- 返回URL中的域名部分,例如http://www.dreamdu.com
          • location.hostname -- 返回URL中的主域名部分,例如http://dreamdu.com
          • location.pathname -- 返回URL的域名后的部分。例如 http://www.dreamdu.com/xhtml/ 返回/xhtml/
          • location.port -- 返回URL中的端口部分。例如 http://www.dreamdu.com:8080/xhtml/ 返回8080
          • location.protocol -- 返回URL中的協議部分。例如 http://www.dreamdu.com:8080/xhtml/ 返回(//)前面的內容http:
          • location.assign -- 設置當前文檔的URL
          • location.replace() -- 設置當前文檔的URL,并且在history對象的地址列表中移除這個URL location.replace(url);
          • location.reload() -- 重載當前頁面

          history對象

          • history.go() -- 前進或后退指定的頁面數
          • history.go(num); history.back() -- 后退一頁
          • history.forward() -- 前進一頁

          Navigator對象

          • navigator.userAgent -- 返回用戶代理頭的字符串表示(就是包括瀏覽器版本信息等的字符串)
          • navigator.cookieEnabled -- 返回瀏覽器是否支持(啟用)cookie

          6、Cookie、sessionStorage、localStorage的區別

          參考答案
          

          共同點:都是保存在瀏覽器端,并且是同源的

          • Cookie:cookie數據始終在同源的http請求中攜帶(即使不需要),即cookie在瀏覽器和服務器間來回傳遞。而sessionStorage和localStorage不會自動把數據發給服務器,僅在本地保存。cookie數據還有路徑(path)的概念,可以限制cookie只屬于某個路徑下,存儲的大小很小只有4K左右。(key:可以在瀏覽器和服務器端來回傳遞,存儲容量小,只有大約4K左右)
          • sessionStorage:僅在當前瀏覽器窗口關閉前有效,自然也就不可能持久保持,localStorage:始終有效,窗口或瀏覽器關閉也一直保存,因此用作持久數據;cookie只在設置的cookie過期時間之前一直有效,即使窗口或瀏覽器關閉。(key:本身就是一個回話過程,關閉瀏覽器后消失,session為一個回話,當頁面不同即使是同一頁面打開兩次,也被視為同一次回話)
          • localStorage:localStorage 在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的。(key:同源窗口都會共享,并且不會失效,不管窗口或者瀏覽器關閉與否都會始終生效)

          補充說明一下cookie的作用:

          • 保存用戶登錄狀態。例如將用戶id存儲于一個cookie內,這樣當用戶下次訪問該頁面時就不需要重新登錄了,現在很多論壇和社區都提供這樣的功能。cookie還可以設置過期時間,當超過時間期限后,cookie就會自動消失。因此,系統往往可以提示用戶保持登錄狀態的時間:常見選項有一個月、三個 月、一年等。
          • 跟蹤用戶行為。例如一個天氣預報網站,能夠根據用戶選擇的地區顯示當地的天氣情況。如果每次都需要選擇所在地是煩瑣的,當利用了 cookie后就會顯得很人性化了,系統能夠記住上一次訪問的地區,當下次再打開該頁面時,它就會自動顯示上次用戶所在地區的天氣情況。因為一切都是在后 臺完成,所以這樣的頁面就像為某個用戶所定制的一樣,使用起來非常方便
          • 定制頁面。如果網站提供了換膚或更換布局的功能,那么可以使用cookie來記錄用戶的選項,例如:背景色、分辨率等。當用戶下次訪問時,仍然可以保存上一次訪問的界面風格。

          7、Cookie如何防范XSS攻擊

          參考答案
          

          XSS(跨站腳本攻擊)是指攻擊者在返回的HTML中嵌入javascript腳本,為了減輕這些攻擊,需要在HTTP頭部配上,set-cookie:

          • httponly-這個屬性可以防止XSS,它會禁止javascript腳本來訪問cookie。
          • secure - 這個屬性告訴瀏覽器僅在請求為https的時候發送cookie。

          結果應該是這樣的:Set-Cookie=.....

          8、瀏覽器和 Node 事件循環的區別?

          參考答案
          

          其中一個主要的區別在于瀏覽器的event loop 和nodejs的event loop 在處理異步事件的順序是不同的,nodejs中有micro event;其中Promise屬于micro event 該異步事件的處理順序就和瀏覽器不同.nodejs V11.0以上 這兩者之間的順序就相同了.

          function test () {
             console.log('start')
              setTimeout(() => {
                  console.log('children2')
                  Promise.resolve().then(() => {console.log('children2-1')})
              }, 0)
              setTimeout(() => {
                  console.log('children3')
                  Promise.resolve().then(() => {console.log('children3-1')})
              }, 0)
              Promise.resolve().then(() => {console.log('children1')})
              console.log('end') 
          }
          
          test()
          
          // 以上代碼在node11以下版本的執行結果(先執行所有的宏任務,再執行微任務)
          // start
          // end
          // children1
          // children2
          // children3
          // children2-1
          // children3-1
          
          // 以上代碼在node11及瀏覽器的執行結果(順序執行宏任務和微任務)
          // start
          // end
          // children1
          // children2
          // children2-1
          // children3
          // children3-1
          

          9、簡述HTTPS中間人攻擊

          參考答案
          

          https協議由 http + ssl 協議構成,具體的鏈接過程可參考SSL或TLS握手的概述

          中間人攻擊過程如下:

          1. 服務器向客戶端發送公鑰。
          2. 攻擊者截獲公鑰,保留在自己手上。
          3. 然后攻擊者自己生成一個【偽造的】公鑰,發給客戶端。
          4. 客戶端收到偽造的公鑰后,生成加密hash值發給服務器。
          5. 攻擊者獲得加密hash值,用自己的私鑰解密獲得真秘鑰。
          6. 同時生成假的加密hash值,發給服務器。
          7. 服務器用私鑰解密獲得假秘鑰。
          8. 服務器用加秘鑰加密傳輸信息

          防范方法:

          1. 服務端在發送瀏覽器的公鑰中加入CA證書,瀏覽器可以驗證CA證書的有效性

          10、說幾條web前端優化策略

          參考答案
          

          (1). 減少HTTP請求數

          這條策略基本上所有前端人都知道,而且也是最重要最有效的。都說要減少HTTP請求,那請求多了到底會怎么樣呢?首先,每個請求都是有成本的,既包 含時間成本也包含資源成本。一個完整的請求都需要經過DNS尋址、與服務器建立連接、發送數據、等待服務器響應、接收數據這樣一個“漫長”而復雜的過程。時間成本就是用戶需要看到或者“感受”到這個資源是必須要等待這個過程結束的,資源上由于每個請求都需要攜帶數據,因此每個請求都需要占用帶寬。

          另外,由于瀏覽器進行并發請求的請求數是有上限的,因此請求數多了以后,瀏覽器需要分批進行請求,因此會增加用戶的等待時間,會給 用戶造成站點速度慢這樣一個印象,即使可能用戶能看到的第一屏的資源都已經請求完了,但是瀏覽器的進度條會一直存在。減少HTTP請求數的主要途徑包括:

          (2). 從設計實現層面簡化頁面

          如果你的頁面像百度首頁一樣簡單,那么接下來的規則基本上都用不著了。保持頁面簡潔、減少資源的使用時最直接的。如果不是這樣,你的頁面需要華麗的皮膚,則繼續閱讀下面的內容。

          (3). 合理設置HTTP緩存

          緩存的力量是強大的,恰當的緩存設置可以大大的減少HTTP請求。以有啊首頁為例,當瀏覽器沒有緩存的時候訪問一共會發出78個請求,共600多K 數據(如圖1.1),而當第二次訪問即瀏覽器已緩存之后訪問則僅有10個請求,共20多K數據(如圖1.2)。(這里需要說明的是,如果直接F5刷新頁面 的話效果是不一樣的,這種情況下請求數還是一樣,不過被緩存資源的請求服務器是304響應,只有Header沒有Body,可以節省帶寬)

          怎樣才算合理設置?原則很簡單,能緩存越多越好,能緩存越久越好。例如,很少變化的圖片資源可以直接通過HTTP Header中的Expires設置一個很長的過期頭;變化不頻繁而又可能會變的資源可以使用Last-Modifed來做請求驗證。盡可能的讓資源能夠 在緩存中待得更久。

          (4). 資源合并與壓縮

          如果可以的話,盡可能的將外部的腳本、樣式進行合并,多個合為一個。另外,CSS、Javascript、Image都可以用相應的工具進行壓縮,壓縮后往往能省下不少空間。

          (5). CSS Sprites

          合并CSS圖片,減少請求數的又一個好辦法。

          (6). Inline Images

          使用data: URL scheme的方式將圖片嵌入到頁面或CSS中,如果不考慮資源管理上的問題的話,不失為一個好辦法。如果是嵌入頁面的話換來的是增大了頁面的體積,而且無法利用瀏覽器緩存。使用在CSS中的圖片則更為理想一些。

          (7). Lazy Load Images

          這條策略實際上并不一定能減少HTTP請求數,但是卻能在某些條件下或者頁面剛加載時減少HTTP請求數。對于圖片而言,在頁面剛加載的時候可以只 加載第一屏,當用戶繼續往后滾屏的時候才加載后續的圖片。這樣一來,假如用戶只對第一屏的內容感興趣時,那剩余的圖片請求就都節省了。有啊首頁曾經的做法 是在加載的時候把第一屏之后的圖片地址緩存在Textarea標簽中,待用戶往下滾屏的時候才“惰性”加載。

          11、你了解的瀏覽器的重繪和回流導致的性能問題

          參考答案
          

          重繪(Repaint)和回流(Reflow)

          重繪和回流是渲染步驟中的一小節,但是這兩個步驟對于性能影響很大。

          • 重繪是當節點需要更改外觀而不會影響布局的,比如改變 color就叫稱為重繪
          • 回流是布局或者幾何屬性需要改變就稱為回流。

          回流必定會發生重繪,重繪不一定會引發回流。回流所需的成本比重繪高的多,改變深層次的節點很可能導致父節點的一系列回流。

          所以以下幾個動作可能會導致性能問題:

          • 改變 window 大小
          • 改變字體
          • 添加或刪除樣式
          • 文字改變
          • 定位或者浮動
          • 盒模型

          很多人不知道的是,重繪和回流其實和 Event loop 有關。

          1. 當 Event loop 執行完 Microtasks 后,會判斷 document 是否需要更新。因為瀏覽器是 60Hz 的刷新率,每 16ms 才會更新一次。
          2. 然后判斷是否有 resize或者 scroll,有的話會去觸發事件,所以 resizescroll事件也是至少 16ms 才會觸發一次,并且自帶節流功能。
          3. 判斷是否觸發了 media query
          4. 更新動畫并且發送事件
          5. 判斷是否有全屏操作事件
          6. 執行 requestAnimationFrame回調
          7. 執行 IntersectionObserver回調,該方法用于判斷元素是否可見,可以用于懶加載上,但是兼容性不好
          8. 更新界面
          9. 以上就是一幀中可能會做的事情。如果在一幀中有空閑時間,就會去執行 requestIdleCallback回調。

          減少重繪和回流

          • 使用 translate 替代 top
            <div class="test"></div>
            <style>
            .test {
            position: absolute;
            top: 10px;
            width: 100px;
            height: 100px;
            background: red;
            }
            </style>
            <script>
            setTimeout(() => {
            // 引起回流
            document.querySelector('.test').style.top = '100px'
            }, 1000)
            </script>
          • 使用 visibility替換 display: none,因為前者只會引起重繪,后者會引發回流(改變了布局)
            把 DOM 離線后修改,比如:先把 DOM 給
            display:none(有一次 Reflow),然后你修改100次,然后再把它顯示出來
            不要把 DOM 結點的屬性值放在一個循環里當成循環里的變量
            for(let i = 0; i < 1000; i++) {
            // 獲取 offsetTop 會導致回流,因為需要去獲取正確的值
            console.log(document.querySelector('.test').style.offsetTop)
            }
          • 不要使用 table 布局,可能很小的一個小改動會造成整個 table 的重新布局
          • 動畫實現的速度的選擇,動畫速度越快,回流次數越多,也可以選擇使用 requestAnimationFrame
          • CSS 選擇符從右往左匹配查找,避免 DOM 深度過深
          • 將頻繁運行的動畫變為圖層,圖層能夠阻止該節點回流影響別的元素。比如對于 video標簽,瀏覽器會自動將該節點變為圖層。

          react、Vue

          1、寫 React / Vue 項目時為什么要在列表組件中寫 key,其作用是什么?

          參考答案
          

          vue和react都是采用diff算法來對比新舊虛擬節點,從而更新節點。在vue的diff函數中(建議先了解一下diff算法過程)。在交叉對比中,當新節點跟舊節點頭尾交叉對比沒有結果時,會根據新節點的key去對比舊節點數組中的key,從而找到相應舊節點(這里對應的是一個key => index 的map映射)。如果沒找到就認為是一個新增節點。而如果沒有key,那么就會采用遍歷查找的方式去找到對應的舊節點。一種一個map映射,另一種是遍歷查找。相比而言。map映射的速度更快。vue部分源碼如下:

          // vue項目  src/core/vdom/patch.js  -488行
          // 以下是為了閱讀性進行格式化后的代碼
          
          // oldCh 是一個舊虛擬節點數組
          if (isUndef(oldKeyToIdx)) {
            oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
          }
          if(isDef(newStartVnode.key)) {
            // map 方式獲取
            idxInOld = oldKeyToIdx[newStartVnode.key]
          } else {
            // 遍歷方式獲取
            idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
          }
          

          創建map函數

          function createKeyToOldIdx (children, beginIdx, endIdx) {
            let i, key
            const map = {}
            for (i = beginIdx; i <= endIdx; ++i) {
              key = children[i].key
              if (isDef(key)) map[key] = i
            }
            return map
          }
          

          遍歷尋找

          // sameVnode 是對比新舊節點是否相同的函數
           function findIdxInOld (node, oldCh, start, end) {
              for (let i = start; i < end; i++) {
                const c = oldCh[i]
                
                if (isDef(c) && sameVnode(node, c)) return i
              }
            }
          

          2、React 中 setState 什么時候是同步的,什么時候是異步的?

          參考答案
          

          在React中,如果是由React引發的事件處理(比如通過onClick引發的事件處理),調用setState不會同步更新this.state,除此之外的setState調用會同步執行this.state。所謂“除此之外”,指的是繞過React通過addEventListener直接添加的事件處理函數,還有通過setTimeout/setInterval產生的異步調用。

          **原因:**在React的setState函數實現中,會根據一個變量isBatchingUpdates判斷是直接更新this.state還是放到隊列中回頭再說,而isBatchingUpdates默認是false,也就表示setState會同步更新this.state,但是,有一個函數batchedUpdates,這個函數會把isBatchingUpdates修改為true,而當React在調用事件處理函數之前就會調用這個batchedUpdates,造成的后果,就是由React控制的事件處理過程setState不會同步更新this.state

          3、下面輸出什么

          class Example extends React.Component {
            constructor() {
              super();
              this.state = {
                val: 0
              };
            }
            
            componentDidMount() {
              this.setState({val: this.state.val + 1});
              console.log(this.state.val);    // 第 1 次 log
          
              this.setState({val: this.state.val + 1});
              console.log(this.state.val);    // 第 2 次 log
          
              setTimeout(() => {
                this.setState({val: this.state.val + 1});
                console.log(this.state.val);  // 第 3 次 log
          
                this.setState({val: this.state.val + 1});
                console.log(this.state.val);  // 第 4 次 log
              }, 0);
            }
          
            render() {
              return null;
            }
          };
          1、第一次和第二次都是在 react 自身生命周期內,觸發時 isBatchingUpdates 為 true,所以并不會直接執行更新 state,而是加入了 dirtyComponents,所以打印時獲取的都是更新前的狀態 0。
          
          2、兩次 setState 時,獲取到 this.state.val 都是 0,所以執行時都是將 0 設置成 1,在 react 內部會被合并掉,只執行一次。設置完成后 state.val 值為 1。
          
          3、setTimeout 中的代碼,觸發時 isBatchingUpdates 為 false,所以能夠直接進行更新,所以連著輸出 2,3。
          
          輸出: 0 0 2 3
          

          4、為什么虛擬dom會提高性能?

          參考答案
          

          虛擬dom相當于在js和真實dom中間加了一個緩存,利用dom diff算法避免了沒有必要的dom操作,從而提高性能。

          具體實現步驟如下:

          用 JavaScript 對象結構表示 DOM 樹的結構;然后用這個樹構建一個真正的 DOM 樹,插到文檔當中

          當狀態變更的時候,重新構造一棵新的對象樹。然后用新的樹和舊的樹進行比較,記錄兩棵樹差異

          把2所記錄的差異應用到步驟1所構建的真正的DOM樹上,視圖就更新了。

          css

          1、分析比較 opacity: 0、visibility: hidden、display: none 優劣和適用場景

          參考答案
          

          結構:display:none: 會讓元素完全從渲染樹中消失,渲染的時候不占據任何空間, 不能點擊, visibility: hidden:不會讓元素從渲染樹消失,渲染元素繼續占據空間,只是內容不可見,不能點擊 opacity: 0: 不會讓元素從渲染樹消失,渲染元素繼續占據空間,只是內容不可見,可以點擊

          繼承:display: none:是非繼承屬性,子孫節點消失由于元素從渲染樹消失造成,通過修改子孫節點屬性無法顯示。visibility: hidden:是繼承屬性,子孫節點消失由于繼承了hidden,通過設置visibility: visible;可以讓子孫節點顯式。

          性能:displaynone : 修改元素會造成文檔回流,讀屏器不會讀取display: none元素內容,性能消耗較大 visibility:hidden: 修改元素只會造成本元素的重繪,性能消耗較少讀屏器讀取visibility: hidden元素內容 opacity: 0 :修改元素會造成重繪,性能消耗較少

          聯系:它們都能讓元素不可見

          2、清除浮動的方式有哪些?比較好的是哪一種?

          參考答案
          

          常用的一般為三種.clearfix, clear:both,overflow:hidden;

          比較好是 .clearfix,偽元素萬金油版本,后兩者有局限性.

          .clearfix:after {
            visibility: hidden;
            display: block;
            font-size: 0;
            content: " ";
            clear: both;
            height: 0;
          }
          
          <!--
          為毛沒有 zoom ,_height 這些,IE6,7這類需要 csshack 不再我們考慮之內了
          .clearfix 還有另外一種寫法,
          -->
          
          .clearfix:before, .clearfix:after {
              content:"";
              display:table;
          }
          .clearfix:after{
              clear:both;
              overflow:hidden;
          }
          .clearfix{
              zoom:1;
          }
          
          <!--
          用display:table 是為了避免外邊距margin重疊導致的margin塌陷,
          內部元素默認會成為 table-cell 單元格的形式
          -->
          

          clear:both:若是用在同一個容器內相鄰元素上,那是賊好的,有時候在容器外就有些問題了, 比如相鄰容器的包裹層元素塌陷

          overflow:hidden:這種若是用在同個容器內,可以形成 BFC避免浮動造成的元素塌陷

          4、css sprite 是什么,有什么優缺點

          參考答案
          

          概念:將多個小圖片拼接到一個圖片中。通過 background-position 和元素尺寸調節需要顯示的背景圖案。

          優點:

          1. 減少 HTTP 請求數,極大地提高頁面加載速度
          2. 增加圖片信息重復度,提高壓縮比,減少圖片大小
          3. 更換風格方便,只需在一張或幾張圖片上修改顏色或樣式即可實現

          缺點:

          1. 圖片合并麻煩
          2. 維護麻煩,修改一個圖片可能需要重新布局整個圖片,樣式

          5、link與@import的區別

          參考答案
          
          1. link是 HTML 方式, @import是 CSS 方式
          2. link最大限度支持并行下載,@import過多嵌套導致串行下載,出現FOUC
          3. link可以通過rel="alternate stylesheet"指定候選樣式
          4. 瀏覽器對link支持早于@import,可以使用@import對老瀏覽器隱藏樣式
          5. @import必須在樣式規則之前,可以在 css 文件中引用其他文件
          6. 總體來說:link 優于@import

          6、display: block;和display: inline;的區別

          參考答案
          

          block元素特點:

          1.處于常規流中時,如果width沒有設置,會自動填充滿父容器 2.可以應用margin/padding 3.在沒有設置高度的情況下會擴展高度以包含常規流中的子元素 4.處于常規流中時布局時在前后元素位置之間(獨占一個水平空間) 5.忽略vertical-align

          inline元素特點

          1.水平方向上根據direction依次布局

          2.不會在元素前后進行換行

          3.受white-space控制

          4.margin/padding在豎直方向上無效,水平方向上有效

          5.width/height屬性對非替換行內元素無效,寬度由元素內容決定

          6.非替換行內元素的行框高由line-height確定,替換行內元素的行框高由height,margin,padding,border決定 7.浮動或絕對定位時會轉換為block8.vertical-align屬性生效

          7、容器包含若干浮動元素時如何清理浮動

          參考答案
          
          1. 容器元素閉合標簽前添加額外元素并設置clear: both
          2. 父元素觸發塊級格式化上下文(見塊級可視化上下文部分)
          3. 設置容器元素偽元素進行清理推薦的清理浮動方法
          /**
          * 在標準瀏覽器下使用
          * 1 content內容為空格用于修復opera下文檔中出現
          *   contenteditable屬性時在清理浮動元素上下的空白
          * 2 使用display使用table而不是block:可以防止容器和
          *   子元素top-margin折疊,這樣能使清理效果與BFC,IE6/7
          *   zoom: 1;一致
          **/
          
          .clearfix:before,
          .clearfix:after {
              content: " "; /* 1 */
              display: table; /* 2 */
          }
          
          .clearfix:after {
              clear: both;
          }
          
          /**
          * IE 6/7下使用
          * 通過觸發hasLayout實現包含浮動
          **/
          .clearfix {
              *zoom: 1;
          }
          

          8、PNG,GIF,JPG 的區別及如何選

          參考答案
          

          GIF:

          1. 8 位像素,256 色
          2. 無損壓縮
          3. 支持簡單動畫
          4. 支持 boolean 透明
          5. 適合簡單動畫

          JPEG

          1. 顏色限于 256
          2. 有損壓縮
          3. 可控制壓縮質量
          4. 不支持透明
          5. 適合照片

          PNG

          1. 有 PNG8 和 truecolor PNG
          2. PNG8 類似 GIF 顏色上限為 256,文件小,支持 alpha 透明度,無動畫
          3. 適合圖標、背景、按鈕

          9、display,float,position 的關系

          參考答案
          
          1. 如果display為 none,那么 position 和 float 都不起作用,這種情況下元素不產生框
          2. 否則,如果 position 值為 absolute 或者 fixed,框就是絕對定位的,float 的計算值為 none,display 根據下面的表格進行調整。
          3. 否則,如果 float 不是 none,框是浮動的,display 根據下表進行調整
          4. 否則,如果元素是根元素,display 根據下表進行調整
          5. 其他情況下 display 的值為指定值 總結起來:絕對定位、浮動、根元素都需要調整display

          10、如何水平居中一個元素

          參考答案
          
          • 如果需要居中的元素為常規流中 inline 元素,為父元素設置text-align: center;即可實現
          • 如果需要居中的元素為常規流中 block 元素,1)為元素設置寬度,2)設置左右 margin 為 auto。3)IE6 下需在父元素上設置text-align: center;,再給子元素恢復需要的值
          • <body>
            <div class="content">
            aaaaaa aaaaaa a a a a a a a a
            </div>
            </body>

            <style>
            body {
            background:
            #DDD;
            text-align: center; /* 3 */
            }
            .content {
            width: 500px; /* 1 */
            text-align: left; /* 3 */
            margin: 0 auto; /* 2 */

            background: purple;
            }
            </style>
          • 如果需要居中的元素為浮動元素,1)為元素設置寬度,2)position: relative;,3)浮動方向偏移量(left 或者 right)設置為 50%,4)浮動方向上的 margin 設置為元素寬度一半乘以-1
            <body>
            <div class="content">
            aaaaaa aaaaaa a a a a a a a a
            </div>
            </body>

            <style>
            body {
            background:
            #DDD;
            }
            .content {
            width: 500px; /* 1 */
            float: left;

            position: relative; /* 2 */
            left: 50%; /* 3 */
            margin-left: -250px; /* 4 */

            background-color: purple;
            }
            </style>
          • 如果需要居中的元素為絕對定位元素,1)為元素設置寬度,2)偏移量設置為 50%,3)偏移方向外邊距設置為元素寬度一半乘以-1
            <body>
            <div class="content">
            aaaaaa aaaaaa a a a a a a a a
            </div>
            </body>

            <style>
            body {
            background:
            #DDD;
            position: relative;
            }
            .content {
            width: 800px;

            position: absolute;
            left: 50%;
            margin-left: -400px;

            background-color: purple;
            }
            </style>
          • 如果需要居中的元素為絕對定位元素,1)為元素設置寬度,2)設置左右偏移量都為 0,3)設置左右外邊距都為 auto
            <body>
            <div class="content">
            aaaaaa aaaaaa a a a a a a a a
            </div>
            </body>

            <style>
            body {
            background:
            #DDD;
            position: relative;
            }
            .content {
            width: 800px;

            position: absolute;
            margin: 0 auto;
            left: 0;
            right: 0;

            background-color: purple;
            }
            </style>

          JavaScript

          1、JS有幾種數據類型,其中基本數據類型有哪些?

          參考答案
          

          七種數據類型

          • Boolean
          • Null
          • Undefined
          • Number
          • String
          • Symbol (ECMAScript 6 新定義)
          • Object

          (ES6之前)其中5種為基本類型:string,number,boolean,null,undefined,

          ES6出來的Symbol也是原始數據類型 ,表示獨一無二的值

          Object為引用類型(范圍挺大),也包括數組、函數,

          2、Promise 構造函數是同步執行還是異步執行,那么 then 方法呢?

          參考答案
          const promise = new Promise((resolve, reject) => {
            console.log(1)
            resolve()
            console.log(2)
          })
          
          promise.then(() => {
            console.log(3)
          })
          
          console.log(4)
          

          輸出結果是:

          1
          2
          4
          3
          promise構造函數是同步執行的,then方法是異步執行的
          Promise new的時候會立即執行里面的代碼 then是微任務 會在本次任務執行完的時候執行 setTimeout是宏任務 會在下次任務執行的時候執行
          

          3、JS的四種設計模式

          參考答案
          

          工廠模式

          簡單的工廠模式可以理解為解決多個相似的問題;

          function CreatePerson(name,age,sex) {
              var obj = new Object();
              obj.name = name;
              obj.age = age;
              obj.sex = sex;
              obj.sayName = function(){
                  return this.name;
              }
              return obj;
          }
          var p1 = new CreatePerson("longen",'28','男');
          var p2 = new CreatePerson("tugenhua",'27','女');
          console.log(p1.name); // longen
          console.log(p1.age);  // 28
          console.log(p1.sex);  // 男
          console.log(p1.sayName()); // longen
          
          console.log(p2.name);  // tugenhua
          console.log(p2.age);   // 27
          console.log(p2.sex);   // 女
          console.log(p2.sayName()); // tugenhua  
          

          單例模式

          只能被實例化(構造函數給實例添加屬性與方法)一次

          // 單體模式
          var Singleton = function(name){
              this.name = name;
          };
          Singleton.prototype.getName = function(){
              return this.name;
          }
          // 獲取實例對象
          var getInstance = (function() {
              var instance = null;
              return function(name) {
                  if(!instance) {//相當于一個一次性閥門,只能實例化一次
                      instance = new Singleton(name);
                  }
                  return instance;
              }
          })();
          // 測試單體模式的實例,所以a===b
          var a = getInstance("aa");
          var b = getInstance("bb");  
          

          沙箱模式

          將一些函數放到自執行函數里面,但要用閉包暴露接口,用變量接收暴露的接口,再調用里面的值,否則無法使用里面的值

          let sandboxModel=(function(){
              function sayName(){};
              function sayAge(){};
              return{
                  sayName:sayName,
                  sayAge:sayAge
              }
          })()
          

          發布者訂閱模式

          就例如如我們關注了某一個公眾號,然后他對應的有新的消息就會給你推送,

          //發布者與訂閱模式
              var shoeObj = {}; // 定義發布者
              shoeObj.list = []; // 緩存列表 存放訂閱者回調函數
          
              // 增加訂閱者
              shoeObj.listen = function(fn) {
                  shoeObj.list.push(fn); // 訂閱消息添加到緩存列表
              }
          
              // 發布消息
              shoeObj.trigger = function() {
                      for (var i = 0, fn; fn = this.list[i++];) {
                          fn.apply(this, arguments);//第一個參數只是改變fn的this,
                      }
                  }
               // 小紅訂閱如下消息
              shoeObj.listen(function(color, size) {
                  console.log("顏色是:" + color);
                  console.log("尺碼是:" + size);
              });
          
              // 小花訂閱如下消息
              shoeObj.listen(function(color, size) {
                  console.log("再次打印顏色是:" + color);
                  console.log("再次打印尺碼是:" + size);
              });
              shoeObj.trigger("紅色", 40);
              shoeObj.trigger("黑色", 42);  
          

          代碼實現邏輯是用數組存貯訂閱者, 發布者回調函數里面通知的方式是遍歷訂閱者數組,并將發布者內容傳入訂閱者數組

          4、列舉出集中創建實例的方法

          參考答案
          

          1.字面量

          let obj={'name':'張三'}
          

          2.Object構造函數創建

          let Obj=new Object()
          Obj.name='張三'
          

          3.使用工廠模式創建對象

          function createPerson(name){
           var o = new Object();
           o.name = name;
           };
           return o; 
          }
          var person1 = createPerson('張三');
          

          4.使用構造函數創建對象

          function Person(name){
           this.name = name;
          }
          var person1 = new Person('張三');
          

          5、簡述一下前端事件流

          參考答案
          

          HTML中與javascript交互是通過事件驅動來實現的,例如鼠標點擊事件onclick、頁面的滾動事件onscroll等等,可以向文檔或者文檔中的元素添加事件偵聽器來預訂事件。想要知道這些事件是在什么時候進行調用的,就需要了解一下“事件流”的概念。

          什么是事件流:事件流描述的是從頁面中接收事件的順序,DOM2級事件流包括下面幾個階段。

          • 事件捕獲階段
          • 處于目標階段
          • 事件冒泡階段

          addEventListeneraddEventListener是DOM2 級事件新增的指定事件處理程序的操作,這個方法接收3個參數:要處理的事件名、作為事件處理程序的函數和一個布爾值。最后這個布爾值參數如果是true,表示在捕獲階段調用事件處理程序;如果是false,表示在冒泡階段調用事件處理程序。

          IE只支持事件冒泡

          6、Function._proto_(getPrototypeOf)是什么?

          參考答案
          

          獲取一個對象的原型,在chrome中可以通過__proto__的形式,或者在ES6中可以通過Object.getPrototypeOf的形式。

          那么Function.proto是什么么?也就是說Function由什么對象繼承而來,我們來做如下判別。

          Function.__proto__==Object.prototype //false
          Function.__proto__==Function.prototype//true
          

          我們發現Function的原型也是Function。

          我們用圖可以來明確這個關系:



          7、簡述一下原型 / 構造函數 / 實例

          參考答案
          
          • 原型(prototype): 一個簡單的對象,用于實現對象的 屬性繼承。可以簡單的理解成對象的爹。在 Firefox 和 Chrome 中,每個JavaScript對象中都包含一個__proto__(非標準)的屬性指向它爹(該對象的原型),可obj.__proto__進行訪問。
          • 構造函數: 可以通過new新建一個對象的函數。
          • 實例: 通過構造函數和new創建出來的對象,便是實例。實例通過__proto__指向原型,通過constructor指向構造函數

          這里來舉個栗子,以Object為例,我們常用的Object便是一個構造函數,因此我們可以通過它構建實例。

          // 實例
          const instance = new Object()
          

          則此時, 實例為instance, 構造函數為Object,我們知道,構造函數擁有一個prototype的屬性指向原型,因此原型為:

          // 原型
          const prototype = Object.prototype
          

          這里我們可以來看出三者的關系:

          實例.__proto__ === 原型
          
          原型.constructor === 構造函數
          
          構造函數.prototype === 原型
          
          // 這條線其實是是基于原型進行獲取的,可以理解成一條基于原型的映射線
          // 例如: 
          // const o = new Object()
          // o.constructor === Object   --> true
          // o.__proto__ = null;
          // o.constructor === Object   --> false
          實例.constructor === 構造函數
          

          8、簡述一下JS繼承,并舉例

          參考答案
          

          在 JS 中,繼承通常指的便是 原型鏈繼承,也就是通過指定原型,并可以通過原型鏈繼承原型上的屬性或者方法。

          • 最優化: 圣杯模式
            var inherit = (function(c,p){
            var F = function(){};
            return function(c,p){
            F.prototype = p.prototype;
            c.prototype = new F();
            c.uber = p.prototype;
            c.prototype.constructor = c;
            }
            })();
          • 使用 ES6 的語法糖 class / extends

          9、函數柯里化

          參考答案
          

          在函數式編程中,函數是一等公民。那么函數柯里化是怎樣的呢?

          函數柯里化指的是將能夠接收多個參數的函數轉化為接收單一參數的函數,并且返回接收余下參數且返回結果的新函數的技術。

          函數柯里化的主要作用和特點就是參數復用、提前返回和延遲執行。

          在一個函數中,首先填充幾個參數,然后再返回一個新的函數的技術,稱為函數的柯里化。通常可用于在不侵入函數的前提下,為函數 預置通用參數,供多次重復調用。

          const add = function add(x) {
              return function (y) {
                  return x + y
              }
          }
          
          const add1 = add(1)
          
          add1(2) === 3
          add1(20) === 21
          

          10、說說bind、call、apply 區別?

          參考答案
          

          callapply 都是為了解決改變 this 的指向。作用都是相同的,只是傳參的方式不同。

          除了第一個參數外,call 可以接收一個參數列表,apply 只接受一個參數數組。

          let a = {
              value: 1
          }
          function getValue(name, age) {
              console.log(name)
              console.log(age)
              console.log(this.value)
          }
          getValue.call(a, 'yck', '24')
          getValue.apply(a, ['yck', '24'])
          

          bind和其他兩個方法作用也是一致的,只是該方法會返回一個函數。并且我們可以通過 bind實現柯里化。

          (下面是對這三個方法的擴展介紹)
          

          如何實現一個 bind 函數

          對于實現以下幾個函數,可以從幾個方面思考

          • 不傳入第一個參數,那么默認為 window
          • 改變了 this 指向,讓新的對象可以執行該函數。那么思路是否可以變成給新的對象添加一個函數,然后在執行完以后刪除?
          Function.prototype.myBind = function (context) {
            if (typeof this !== 'function') {
              throw new TypeError('Error')
            }
            var _this = this
            var args = [...arguments].slice(1)
            // 返回一個函數
            return function F() {
              // 因為返回了一個函數,我們可以 new F(),所以需要判斷
              if (this instanceof F) {
                return new _this(...args, ...arguments)
              }
              return _this.apply(context, args.concat(...arguments))
            }
          }
          

          如何實現一個call函數

          Function.prototype.myCall = function (context) {
            var context = context || window
            // 給 context 添加一個屬性
            // getValue.call(a, 'yck', '24') => a.fn = getValue
            context.fn = this
            // 將 context 后面的參數取出來
            var args = [...arguments].slice(1)
            // getValue.call(a, 'yck', '24') => a.fn('yck', '24')
            var result = context.fn(...args)
            // 刪除 fn
            delete context.fn
            return result
          }
          

          如何實現一個apply函數

          Function.prototype.myApply = function (context) {
            var context = context || window
            context.fn = this
          
            var result
            // 需要判斷是否存儲第二個參數
            // 如果存在,就將第二個參數展開
            if (arguments[1]) {
              result = context.fn(...arguments[1])
            } else {
              result = context.fn()
            }
          
            delete context.fn
            return result
          }
          

          11、箭頭函數的特點

          參考答案
          function a() {
              return () => {
                  return () => {
                      console.log(this)
                  }
              }
          }
          console.log(a()()())
          

          箭頭函數其實是沒有 this的,這個函數中的 this只取決于他外面的第一個不是箭頭函數的函數的 this。在這個例子中,因為調用 a符合前面代碼中的第一個情況,所以 thiswindow。并且 this一旦綁定了上下文,就不會被任何代碼改變。

          程序閱讀題

          1、下面程序輸出的結果是什么?

          function sayHi() {
            console.log(name);
            console.log(age);
            var name = "Lydia";
            let age = 21;
          }
          
          sayHi();
          
          • A: Lydiaundefined
          • B: LydiaReferenceError
          • C: ReferenceError21
          • D: undefinedReferenceError
          參考答案
          

          在函數中,我們首先使用var關鍵字聲明了name變量。這意味著變量在創建階段會被提升(JavaScript會在創建變量創建階段為其分配內存空間),默認值為undefined,直到我們實際執行到使用該變量的行。我們還沒有為name變量賦值,所以它仍然保持undefined的值。

          使用let關鍵字(和const)聲明的變量也會存在變量提升,但與var不同,初始化沒有被提升。在我們聲明(初始化)它們之前,它們是不可訪問的。這被稱為“暫時死區”。當我們在聲明變量之前嘗試訪問變量時,JavaScript會拋出一個ReferenceError

          關于let的是否存在變量提升,我們何以用下面的例子來驗證:

          let name = 'ConardLi'
          {
            console.log(name) // Uncaught ReferenceError: name is not defined
            let name = 'code秘密花園'
          }
          

          let變量如果不存在變量提升,console.log(name)就會輸出ConardLi,結果卻拋出了ReferenceError,那么這很好的說明了,let也存在變量提升,但是它存在一個“暫時死區”,在變量未初始化或賦值前不允許訪問。

          變量的賦值可以分為三個階段:

          • 創建變量,在內存中開辟空間
          • 初始化變量,將變量初始化為undefined
          • 真正賦值

          關于letvarfunction

          • let的「創建」過程被提升了,但是初始化沒有提升。
          • var的「創建」和「初始化」都被提升了。
          • function的「創建」「初始化」和「賦值」都被提升了。

          2、下面代碼輸出什么

          var a = 10;
          (function () {
              console.log(a)
              a = 5
              console.log(window.a)
              var a = 20;
              console.log(a)
          })()
          

          依次輸出:undefined -> 10 -> 20

          在立即執行函數中,var a = 20; 語句定義了一個局部變量 a,由于js的變量聲明提升機制,局部變量a的聲明會被提升至立即執行函數的函數體最上方,且由于這樣的提升并不包括賦值,因此第一條打印語句會打印undefined,最后一條語句會打印20。
          
          由于變量聲明提升,a = 5; 這條語句執行時,局部的變量a已經聲明,因此它產生的效果是對局部的變量a賦值,此時window.a 依舊是最開始賦值的10,
          

          3、下面的輸出結果是什么?

          class Chameleon {
            static colorChange(newColor) {
              this.newColor = newColor;
            }
          
            constructor({ newColor = "green" } = {}) {
              this.newColor = newColor;
            }
          }
          
          const freddie = new Chameleon({ newColor: "purple" });
          freddie.colorChange("orange");
          
          • A: orange
          • B: purple
          • C: green
          • D: TypeError

          答案: D

          colorChange方法是靜態的。靜態方法僅在創建它們的構造函數中存在,并且不能傳遞給任何子級。由于freddie是一個子級對象,函數不會傳遞,所以在freddie實例上不存在freddie方法:拋出TypeError

          4、下面代碼中什么時候會輸出1?

          var a = ?;
          if(a == 1 && a == 2 && a == 3){
               conso.log(1);
          }
          參考答案
          

          因為==會進行隱式類型轉換 所以我們重寫toString方法就可以了

          var a = {
            i: 1,
            toString() {
              return a.i++;
            }
          }
          
          if( a == 1 && a == 2 && a == 3 ) {
            console.log(1);
          }
          

          5、下面的輸出結果是什么?

          var obj = {
              '2': 3,
              '3': 4,
              'length': 2,
              'splice': Array.prototype.splice,
              'push': Array.prototype.push
          }
          obj.push(1)
          obj.push(2)
          console.log(obj)
          參考答案
          

          1.使用第一次push,obj對象的push方法設置 obj[2]=1;obj.length+=12.使用第二次push,obj對象的push方法設置 obj[3]=2;obj.length+=13.使用console.log輸出的時候,因為obj具有 length 屬性和 splice 方法,故將其作為數組進行打印 4.打印時因為數組未設置下標為 0 1 處的值,故打印為empty,主動 obj[0] 獲取為 undefined

          6、下面代碼輸出的結果是什么?

          var a = {n: 1};
          var b = a;
          a.x = a = {n: 2};
          
          console.log(a.x)     
          console.log(b.x)
          參考答案
          

          undefined {n:2}

          首先,a和b同時引用了{n:2}對象,接著執行到a.x = a = {n:2}語句,盡管賦值是從右到左的沒錯,但是.的優先級比=要高,所以這里首先執行a.x,相當于為a(或者b)所指向的{n:1}對象新增了一個屬性x,即此時對象將變為{n:1;x:undefined}。之后按正常情況,從右到左進行賦值,此時執行a ={n:2}的時候,a的引用改變,指向了新對象{n:2},而b依然指向的是舊對象。之后執行a.x = {n:2}的時候,并不會重新解析一遍a,而是沿用最初解析a.x時候的a,也即舊對象,故此時舊對象的x的值為{n:2},舊對象為 {n:1;x:{n:2}},它被b引用著。后面輸出a.x的時候,又要解析a了,此時的a是指向新對象的a,而這個新對象是沒有x屬性的,故訪問時輸出undefined;而訪問b.x的時候,將輸出舊對象的x的值,即{n:2}。

          7、下面代碼的輸出是什么?

          function checkAge(data) {
            if (data === { age: 18 }) {
              console.log("You are an adult!");
            } else if (data == { age: 18 }) {
              console.log("You are still an adult.");
            } else {
              console.log(`Hmm.. You don't have an age I guess`);
            }
          }
          
          checkAge({ age: 18 });
          參考答案
          

          Hmm.. You don't have an age I guess

          在比較相等性,原始類型通過它們的值進行比較,而對象通過它們的引用進行比較。JavaScript檢查對象是否具有對內存中相同位置的引用。

          我們作為參數傳遞的對象和我們用于檢查相等性的對象在內存中位于不同位置,所以它們的引用是不同的。

          這就是為什么{ age: 18 } === { age: 18 }{ age: 18 } == { age: 18 }返回 false的原因。

          8、下面代碼的輸出是什么?

          const obj = { 1: "a", 2: "b", 3: "c" };
          const set = new Set([1, 2, 3, 4, 5]);
          
          obj.hasOwnProperty("1");
          obj.hasOwnProperty(1);
          set.has("1");
          set.has(1);
          參考答案
          

          true` `true` `false` `true

          所有對象鍵(不包括Symbols)都會被存儲為字符串,即使你沒有給定字符串類型的鍵。這就是為什么obj.hasOwnProperty('1')也返回true

          上面的說法不適用于Set。在我們的Set中沒有“1”set.has('1')返回false。它有數字類型1set.has(1)返回true

          9、下面代碼的輸出是什么?

          // example 1
          var a={}, b='123', c=123;  
          a[b]='b';
          a[c]='c';  
          console.log(a[b]);
          
          ---------------------
          // example 2
          var a={}, b=Symbol('123'), c=Symbol('123');  
          a[b]='b';
          a[c]='c';  
          console.log(a[b]);
          
          ---------------------
          // example 3
          var a={}, b={key:'123'}, c={key:'456'};  
          a[b]='b';
          a[c]='c';  
          console.log(a[b]);
          參考答案
          

          這題考察的是對象的鍵名的轉換。

          • 對象的鍵名只能是字符串和 Symbol 類型。
          • 其他類型的鍵名會被轉換成字符串類型。
          • 對象轉字符串默認會調用 toString 方法。
          // example 1
          var a={}, b='123', c=123;
          a[b]='b';
          // c 的鍵名會被轉換成字符串'123',這里會把 b 覆蓋掉。
          a[c]='c';  
          // 輸出 c
          console.log(a[b]);
          
          
          // example 2
          var a={}, b=Symbol('123'), c=Symbol('123');  
          // b 是 Symbol 類型,不需要轉換。
          a[b]='b';
          // c 是 Symbol 類型,不需要轉換。任何一個 Symbol 類型的值都是不相等的,所以不會覆蓋掉 b。
          a[c]='c';
          // 輸出 b
          console.log(a[b]);
          
          
          // example 3
          var a={}, b={key:'123'}, c={key:'456'};  
          // b 不是字符串也不是 Symbol 類型,需要轉換成字符串。
          // 對象類型會調用 toString 方法轉換成字符串 [object Object]。
          a[b]='b';
          // c 不是字符串也不是 Symbol 類型,需要轉換成字符串。
          // 對象類型會調用 toString 方法轉換成字符串 [object Object]。這里會把 b 覆蓋掉。
          a[c]='c';  
          // 輸出 c
          console.log(a[b]);
          

          10、下面代碼的輸出是什么?

          (() => {
            let x, y;
            try {
              throw new Error();
            } catch (x) {
              (x = 1), (y = 2);
              console.log(x);
            }
            console.log(x);
            console.log(y);
          })();
          參考答案
          

          1` `undefined` `2

          catch塊接收參數x。當我們傳遞參數時,這與變量的x不同。這個變量x是屬于catch作用域的。

          之后,我們將這個塊級作用域的變量設置為1,并設置變量y的值。現在,我們打印塊級作用域的變量x,它等于1

          catch塊之外,x仍然是undefined,而y2。當我們想在catch塊之外的console.log(x)時,它返回undefined,而y返回2

          11、下面代碼的輸出結果是什么?

          function Foo() {
              Foo.a = function() {
                  console.log(1)
              }
              this.a = function() {
                  console.log(2)
              }
          }
          Foo.prototype.a = function() {
              console.log(3)
          }
          Foo.a = function() {
              console.log(4)
          }
          Foo.a();
          let obj = new Foo();
          obj.a();
          Foo.a();
          參考答案
          

          輸出順序是 4 2 1

          function Foo() {
              Foo.a = function() {
                  console.log(1)
              }
              this.a = function() {
                  console.log(2)
              }
          }
          // 以上只是 Foo 的構建方法,沒有產生實例,此刻也沒有執行
          
          Foo.prototype.a = function() {
              console.log(3)
          }
          // 現在在 Foo 上掛載了原型方法 a ,方法輸出值為 3
          
          Foo.a = function() {
              console.log(4)
          }
          // 現在在 Foo 上掛載了直接方法 a ,輸出值為 4
          
          Foo.a();
          // 立刻執行了 Foo 上的 a 方法,也就是剛剛定義的,所以
          // # 輸出 4
          
          let obj = new Foo();
          /* 這里調用了 Foo 的構建方法。Foo 的構建方法主要做了兩件事:
          1. 將全局的 Foo 上的直接方法 a 替換為一個輸出 1 的方法。
          2. 在新對象上掛載直接方法 a ,輸出值為 2。
          */
          
          obj.a();
          // 因為有直接方法 a ,不需要去訪問原型鏈,所以使用的是構建方法里所定義的 this.a,
          // # 輸出 2
          
          Foo.a();
          // 構建方法里已經替換了全局 Foo 上的 a 方法,所以
          // # 輸出 1

          作者:靜觀流葉

          原文地址:https://mp.weixin.qq.com/s/PTSaytcf3xgOp6C9l3Pvjw

          全前端開發面試題目:包含算法+網絡+css面試+js+h5面試題目,尾部有最全BAT前端面試經典77題和答案,想要的就快來領走吧~(領取方式見文末)

          一、前端算法面試

          1、基本排序的方式

          冒泡、快排、桶排、堆排序、歸并排序、插入排序。

          基本也就這些吧。最重要的還是快排和插入排序和堆排序。自己多寫幾次,有遞歸形式的再好好看一下使用循環形式達到結果的形式,基本也就沒什么問題了。

          2、二分搜索等。

          3、二叉樹遍歷相關。

          例如前序遍歷中序遍歷后序遍歷等,深度優先搜索和廣度優先最好掌握。還有二叉樹的反轉。

          算法面試總結

          最重要的還是快排和插入排序和堆排序,有遞歸形式的再好好看。如果你算法不好就可以直接告訴面試官。不需要過多作假,面試官其實也很聰明的,如果你作弊很快就能發現的。

          二、前端計算機網絡面試

          1.最重要的就是http協議。畢竟前端大多都和http協議搞事情。http協議大多包含了以下幾種問題吧

          • http報文解構都包含什么(大致就是那個三部分,請求實體、請求頭、請求行這些)
          • http協議常用的狀態碼
          • http頭部關于緩存的字段
          • http和https的區別
          • http1.0 1.1 2 大致的區別

          除此之外暫時想不到其他的了。緩存字段和200、404、304之類非常常用的狀態碼一定要記住。

          2.tcp的三次握手和四次揮手。除此之外再就沒什么了。

          3.udp和tcp的比較等等

          網絡面試總結

          計算機網絡部分知識點個人感覺不是特別多,基本面試官也只是簡單問一下,以上這些你能說明白就沒什么大問題。

          三、 前端html和css面試

          1.盒模型概念

          2.position定位屬性和相關的關系,例如relative占用文檔流,而absolute和fixed卻不占用,還有relative和absolute結合使用會有變化,一定要搞懂。

          3.display相關屬性,常用的block、inline、等等。都要弄明白。

          4.IFC和BFC問題。

          5.雙飛翼布局。

          6.垂直居中、水平居中的多種方式和方法,這倆個都有很多的方法,盡量每個都記下來4個方法以上。

          7.圣杯布局

          8.rem、em的使用。

          9.less等預處理語言。

          10.媒體查詢。

          11.vh和vw,當然這個考的實在太少。

          12.h5的語義化部分。這個其實在面試上是比較重要的一個問題,

          13.h5 的web worker 還有websocket。這些不需要你真正使用過,但是要明白他們在什么場景下使用。

          14.h5的語義化標簽都有什么,大致看一下,記住一部分。還有新加的例如video這些東西。

          15.css3的都很重要,包括transform、transition、animate這些都好好看一下。

          16.css選擇器的優先級,也就是class id 元素選擇器這些的優先級計算。

          17.css樣式表引用的優先級部分。

          18.清除浮動。

          19.canves 某些特別的公司部門可能需要。

          20.css實現響應式的方式。

          四、前端js面試

          1.js的作用域作用域鏈。

          2.js的原型原型鏈,來一張巨經典的圖片。能看懂或者你給面試官講明白就ok了。還有instanceOf那些判斷。

          3.js的dom元素操作插入刪除等等。

          4.js的垃圾回收機制。(問的比較少)。

          5.js中String或者Array或者Math內部常用的方法。

          6.ajax請求詳細怎么實現,和其中的響應狀態。

          7.js的基礎類型和引用類型

          8.事件委托、事件冒泡、事件捕獲。

          9.addEventlistener和正常的onclick=()=> 的區別

          10.js中new和object.creat區別

          11.除了js中DOM的基礎方法還有BOM中的方法。比如通過js直接取出相應的url的端口號,或者是協議名,或者url整體。

          12.瀏覽器輸入url之后最后網頁渲染出來經過了什么

          13.js跨域實現。我大概回答的有webpack和nginx等代理跨域。CORS和jsonp等。

          14.瀏覽器的同源策略

          15.瀏覽器的回流和重繪,就是reflow那些東西。

          16.call、bind、apply區別。

          17.js實現繼承怎么實現。原理是? 這個其實和上面的原型鏈原型是一起的。百度一下,多記住幾種方式。

          18.請求get和post的區別

          19.js方法參數argument

          20.深拷貝和淺拷貝,還可以引入序列化。還要明白序列化的缺點。

          以下都是es6

          21.promise,最好自己聲明一個構造函數嘗試去實現,這樣面試官會覺得你學的很認真。如果實在自己寫不出來上網查一下promise的實現原理或者源碼應該就有。

          22.閉包

          23.let和const,最好全面一點解釋,包括可以說一下變量提升引入一下var,和這些在什么場景下去使用。

          24.generator

          25.es6加上symbol的基礎數據類型

          26.set類型可以實現數組去重等。

          27.箭頭函數 重點,很多時候都會問 包括里面的簡便寫法和內部的this指針指向等等

          28.解構賦值

          29.es6新增加的正則符號。

          30.或者直接問你es6有什么新加的東西,說得越多越好

          五:其他前端面試問題

          1.跨域。

          2.網絡安全,大多就是csrf和xss看明白原理和解決方式就可以了。

          3.數據結構的棧、隊列、鏈表、等等這些基礎的數據結構要明白是什么。

          4.設計模式

          5.數據庫相關操作語句(這個問得比較少,但是基礎的數據庫語句還是要學的)

          6.linex基礎指令,一般很少問。就算問也是問你知道什么指令

          7.bootstrap實現柵格的原理

          8.webpack相關配置。例如跨域之類的

          以上就前端開發面試80題,以下是最新總結出來最全BAT前端開發面試題目,包含:BAT面試經典JS前端77題和答案,想要的趕緊來領吧!。

          BAT經典前端開發面試77題和答案

          答案領取方法:

          歡迎私信口令 "我要77" 就可以快速領到題目和答案鏈接!

          上就是畢業季了,很多同學都準備實習面試找工作了,今天來分享我在面試的畢業生的時候,會經常考察的16道JavaScript原理題,僅提供相應的核心原理和思路,具體細節大家可以收藏自己敲一遍研究理解。

          一定要收藏自己練一遍!理解了才是自己的!



          1. 實現一個call函數

          // 思路:將要改變this指向的方法掛到目標this上執行并返回
          Function.prototype.mycall = function (context) {
            if (typeof this !== 'function') {
              throw new TypeError('not funciton')
            }
            context = context || window
            context.fn = this
            let arg = [...arguments].slice(1)
            let result = context.fn(...arg)
            delete context.fn
            return result
          } 
          

          2. 實現一個apply函數

          // 思路:將要改變this指向的方法掛到目標this上執行并返回
          Function.prototype.myapply = function (context) {
            if (typeof this !== 'function') {
              throw new TypeError('not funciton')
            }
            context = context || window
            context.fn = this
            let result
            if (arguments[1]) {
              result = context.fn(...arguments[1])
            } else {
              result = context.fn()
            }
            delete context.fn
            return result
          }

          3. 實現一個bind函數

          // 思路:類似call,但返回的是函數
          Function.prototype.mybind = function (context) {
            if (typeof this !== 'function') {
              throw new TypeError('Error')
            }
            let _this = this
            let arg = [...arguments].slice(1)
            return function F() {
              // 處理函數使用new的情況
              if (this instanceof F) {
                return new _this(...arg, ...arguments)
              } else {
                return _this.apply(context, arg.concat(...arguments))
              }
            }
          }

          4. new本質

          function myNew (fun) {
            return function () {
              // 創建一個新對象且將其隱式原型指向構造函數原型
              let obj = {
                __proto__ : fun.prototype
              }
              // 執行構造函數
              fun.call(obj, ...arguments)
              // 返回該對象
              return obj
            }
          }
          function person(name, age) {
            this.name = name
            this.age = age
          }
          let obj = myNew(person)('chen', 18) 
          // {name: "chen", age: 18}

          5. Object.create的基本實現原理

          // 思路:將傳入的對象作為原型
          function create(obj) {
            function F() {}
            F.prototype = obj
            return new F()
          }

          6. instanceof的原理

          // 思路:右邊變量的原型存在于左邊變量的原型鏈上
          function instanceOf(left, right) {
            let leftValue = left.__proto__
            let rightValue = right.prototype
            while (true) {
              if (leftValue === null) {
                return false
              }
              if (leftValue === rightValue) {
                return true
              }
              leftValue = leftValue.__proto__
            }
          }

          7. 實現一個基本的Promise

          // 未添加異步處理等其他邊界情況
          // ①自動執行函數,②三個狀態,③then
          class Promise {
            constructor (fn) {
              // 三個狀態
              this.state = 'pending'
              this.value = undefined
              this.reason = undefined
              let resolve = value => {
                if (this.state === 'pending') {
                  this.state = 'fulfilled'
                  this.value = value
                }
              }
              let reject = value => {
                if (this.state === 'pending') {
                  this.state = 'rejected'
                  this.reason = value
                }
              }
              // 自動執行函數
              try {
                fn(resolve, reject)
              } catch (e) {
                reject(e)
              }
            }
            // then
            then(onFulfilled, onRejected) {
              switch (this.state) {
                case 'fulfilled':
                  onFulfilled()
                  break
                case 'rejected':
                  onRejected()
                  break
                default:
              }
            }
          }

          8. 實現淺拷貝

          // 1. ...實現
          let copy1 = {...{x:1}}
          // 2. Object.assign實現
          let copy2 = Object.assign({}, {x:1})

          9. 使用setTimeout模擬setInterval

          // 可避免setInterval因執行時間導致的間隔執行時間不一致
          setTimeout (function () {
            // do something
            setTimeout (arguments.callee, 500)
          }, 500)

          10. js實現一個繼承方法

          // 借用構造函數繼承實例屬性
          function Child () {
            Parent.call(this)
          }
          // 寄生繼承原型屬性
          (function () {
            let Super = function () {}
            Super.prototype = Parent.prototype
            Child.prototype = new Super()
          })()
          



          11. 實現一個基本的深拷貝

          // 1. JOSN.stringify()/JSON.parse()
          let obj = {a: 1, b: {x: 3}}
          JSON.parse(JSON.stringify(obj))
          // 2. 遞歸拷貝
          function deepClone(obj) {
           let copy = obj instanceof Array ? [] : {}
           for (let i in obj) {
            if (obj.hasOwnProperty(i)) {
            copy[i] = typeof obj[i] === 'object'?deepClone(obj[i]):obj[i]
              }
            }
            return copy
          }

          12. 實現一個基本的Event Bus

          // 組件通信,一個觸發與監聽的過程
          class EventEmitter {
            constructor () {
              // 存儲事件
              this.events = this.events || new Map()
            }
            // 監聽事件
            addListener (type, fn) {
              if (!this.events.get(type)) {
                this.events.set(type, fn)
              }
            }
            // 觸發事件
            emit (type) {
              let handle = this.events.get(type)
              handle.apply(this, [...arguments].slice(1))
            }
          }
          // 測試
          let emitter = new EventEmitter()
          // 監聽事件
          emitter.addListener('ages', age => {
            console.log(age)
          })
          // 觸發事件
          emitter.emit('ages', 18)  // 18

          13. 實現一個雙向數據綁定

          let obj = {}
          let input = document.getElementById('input')
          let span = document.getElementById('span')
          // 數據劫持
          Object.defineProperty(obj, 'text', {
            configurable: true,
            enumerable: true,
            get() {
              console.log('獲取數據了')
            },
            set(newVal) {
              console.log('數據更新了')
              input.value = newVal
              span.innerHTML = newVal
            }
          })
          // 輸入監聽
          input.addEventListener('keyup', function(e) {
            obj.text = e.target.value
          })

          14. 實現一個簡單路由

          // hash路由
          class Route{
            constructor(){
              // 路由存儲對象
              this.routes = {}
              // 當前hash
              this.currentHash = ''
              // 綁定this,避免監聽時this指向改變
              this.freshRoute = this.freshRoute.bind(this)
              // 監聽
              window.addEventListener('load', this.freshRoute, false)
              window.addEventListener('hashchange', this.freshRoute, false)
            }
            // 存儲
            storeRoute (path, cb) {
              this.routes[path] = cb || function () {}
            }
            // 更新
            freshRoute () {
              this.currentHash = location.hash.slice(1) || '/'
              this.routes[this.currentHash]()
            }   
          }

          15. 實現一個節流函數

          // 思路:在規定時間內只觸發一次
          function throttle (fn, delay) {
            // 利用閉包保存時間
            let prev = Date.now()
            return function () {
              let context = this
              let arg = arguments
              let now = Date.now()
              if (now - prev >= delay) {
                fn.apply(context, arg)
                prev = Date.now()
              }
            }
          }
          function fn () {
            console.log('節流')
          }
          addEventListener('scroll', throttle(fn, 1000)) 

          16. 實現一個防抖函數


          主站蜘蛛池模板: 国产精品一区视频| 亚洲AV色香蕉一区二区| 亚洲韩国精品无码一区二区三区| 日韩精品人妻一区二区三区四区| 国产精品免费大片一区二区| 在线免费观看一区二区三区| 无码人妻一区二区三区兔费| 一区二区三区免费视频观看| 国产精品视频一区二区三区经 | 国产自产V一区二区三区C| 东京热人妻无码一区二区av| 国产成人一区二区动漫精品| 国产主播福利精品一区二区| 亚洲一区中文字幕在线观看| 97久久精品午夜一区二区| 国产一区二区三区亚洲综合| 97人妻无码一区二区精品免费| 精品一区二区三区无码免费视频| 国产亚洲3p无码一区二区| 日本内射精品一区二区视频| 一区二区三区免费视频网站| 久久久久人妻一区二区三区| 久久精品国产免费一区| 国产色情一区二区三区在线播放| 国产一区二区三区内射高清| 国精产品一区一区三区MBA下载| 免费播放一区二区三区| 无码AV天堂一区二区三区| 欧亚精品一区三区免费| 久久久久人妻精品一区三寸| 国产一区二区草草影院| 久久久人妻精品无码一区| 久久精品无码一区二区三区日韩| 亚洲一区二区女搞男| 精品熟人妻一区二区三区四区不卡| 无码精品不卡一区二区三区| 无码人妻精品一区二区三区99不卡 | 99久久精品国产高清一区二区 | 亚洲不卡av不卡一区二区| 亚洲AV成人一区二区三区观看| 精品欧洲av无码一区二区|