一份前端面試複習計劃

腹黑的可樂發表於2022-12-14

手寫題:Promise 原理

class MyPromise {
  constructor(fn) {
    this.callbacks = [];
    this.state = "PENDING";
    this.value = null;

    fn(this._resolve.bind(this), this._reject.bind(this));
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) =>
      this._handle({
        onFulfilled: onFulfilled || null,
        onRejected: onRejected || null,
        resolve,
        reject,
      })
    );
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }

  _handle(callback) {
    if (this.state === "PENDING") {
      this.callbacks.push(callback);

      return;
    }

    let cb =
      this.state === "FULFILLED" ? callback.onFulfilled : callback.onRejected;
    if (!cb) {
      cb = this.state === "FULFILLED" ? callback.resolve : callback.reject;
      cb(this.value);

      return;
    }

    let ret;

    try {
      ret = cb(this.value);
      cb = this.state === "FULFILLED" ? callback.resolve : callback.reject;
    } catch (error) {
      ret = error;
      cb = callback.reject;
    } finally {
      cb(ret);
    }
  }

  _resolve(value) {
    if (value && (typeof value === "object" || typeof value === "function")) {
      let then = value.then;

      if (typeof then === "function") {
        then.call(value, this._resolve.bind(this), this._reject.bind(this));

        return;
      }
    }

    this.state === "FULFILLED";
    this.value = value;
    this.callbacks.forEach((fn) => this._handle(fn));
  }

  _reject(error) {
    this.state === "REJECTED";
    this.value = error;
    this.callbacks.forEach((fn) => this._handle(fn));
  }
}

const p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error("fail")), 3000);
});

const p2 = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(p1), 1000);
});

p2.then((result) => console.log(result)).catch((error) => console.log(error));

原型鏈

原型鏈實際上在上面原型的問題中就有涉及到,在原型的繼承中,我們繼承來多個原型,這裡再提一下實現完美
繼承的方案,透過藉助寄生組合繼承,PersonB.prototype = Object.create(PersonA.prototype)
這是當我們例項化PersonB得到例項化物件,訪問例項化物件的屬性時會觸發get方法,它會先在自身屬性上查
找,如果沒有這個屬性,就會去__proto__中查詢,一層層向上直到查詢到頂層物件Object,這個查詢的過程
就是原型鏈來。

基於 Localstorage 設計一個 1M 的快取系統,需要實現快取淘汰機制

設計思路如下:

  • 儲存的每個物件需要新增兩個屬性:分別是過期時間和儲存時間。
  • 利用一個屬性儲存系統中目前所佔空間大小,每次儲存都增加該屬性。當該屬性值大於 1M 時,需要按照時間排序系統中的資料,刪除一定量的資料保證能夠儲存下目前需要儲存的資料。
  • 每次取資料時,需要判斷該快取資料是否過期,如果過期就刪除。

以下是程式碼實現,實現了思路,但是可能會存在 Bug,但是這種設計題一般是給出設計思路和部分程式碼,不會需要寫出一個無問題的程式碼

class Store {
  constructor() {
    let store = localStorage.getItem('cache')
    if (!store) {
      store = {
        maxSize: 1024 * 1024,
        size: 0
      }
      this.store = store
    } else {
      this.store = JSON.parse(store)
    }
  }
  set(key, value, expire) {
    this.store[key] = {
      date: Date.now(),
      expire,
      value
    }
    let size = this.sizeOf(JSON.stringify(this.store[key]))
    if (this.store.maxSize < size + this.store.size) {
      console.log('超了-----------');
      var keys = Object.keys(this.store);
      // 時間排序
      keys = keys.sort((a, b) => {
        let item1 = this.store[a], item2 = this.store[b];
        return item2.date - item1.date;
      });
      while (size + this.store.size > this.store.maxSize) {
        let index = keys[keys.length - 1]
        this.store.size -= this.sizeOf(JSON.stringify(this.store[index]))
        delete this.store[index]
      }
    }
    this.store.size += size

    localStorage.setItem('cache', JSON.stringify(this.store))
  }
  get(key) {
    let d = this.store[key]
    if (!d) {
      console.log('找不到該屬性');
      return
    }
    if (d.expire > Date.now) {
      console.log('過期刪除');
      delete this.store[key]
      localStorage.setItem('cache', JSON.stringify(this.store))
    } else {
      return d.value
    }
  }
  sizeOf(str, charset) {
    var total = 0,
      charCode,
      i,
      len;
    charset = charset ? charset.toLowerCase() : '';
    if (charset === 'utf-16' || charset === 'utf16') {
      for (i = 0, len = str.length; i < len; i++) {
        charCode = str.charCodeAt(i);
        if (charCode <= 0xffff) {
          total += 2;
        } else {
          total += 4;
        }
      }
    } else {
      for (i = 0, len = str.length; i < len; i++) {
        charCode = str.charCodeAt(i);
        if (charCode <= 0x007f) {
          total += 1;
        } else if (charCode <= 0x07ff) {
          total += 2;
        } else if (charCode <= 0xffff) {
          total += 3;
        } else {
          total += 4;
        }
      }
    }
    return total;
  }
}

事件委託的使用場景

場景:給頁面的所有的a標籤新增click事件,程式碼如下:

document.addEventListener("click", function(e) {
    if (e.target.nodeName == "A")
        console.log("a");
}, false);

但是這些a標籤可能包含一些像span、img等元素,如果點選到了這些a標籤中的元素,就不會觸發click事件,因為事件繫結上在a標籤元素上,而觸發這些內部的元素時,e.target指向的是觸發click事件的元素(span、img等其他元素)。

這種情況下就可以使用事件委託來處理,將事件繫結在a標籤的內部元素上,當點選它的時候,就會逐級向上查詢,知道找到a標籤為止,程式碼如下:

document.addEventListener("click", function(e) {
    var node = e.target;
    while (node.parentNode.nodeName != "BODY") {
        if (node.nodeName == "A") {
            console.log("a");
            break;
        }
        node = node.parentNode;
    }
}, false);

程式碼輸出結果

function Dog() {
  this.name = 'puppy'
}
Dog.prototype.bark = () => {
  console.log('woof!woof!')
}
const dog = new Dog()
console.log(Dog.prototype.constructor === Dog && dog.constructor === Dog && dog instanceof Dog)

輸出結果:true

解析: 因為constructor是prototype上的屬性,所以dog.constructor實際上就是指向Dog.prototype.constructor;constructor屬性指向建構函式。instanceof而實際檢測的是型別是否在例項的原型鏈上。

constructor是prototype上的屬性,這一點很容易被忽略掉。constructor和instanceof 的作用是不同的,感性地來說,constructor的限制比較嚴格,它只能嚴格對比物件的建構函式是不是指定的值;而instanceof比較鬆散,只要檢測的型別在原型鏈上,就會返回true。

程式碼輸出結果

const promise = Promise.resolve().then(() => {
  return promise;
})
promise.catch(console.err)

輸出結果如下:

Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>

這裡其實是一個坑,.then.catch 返回的值不能是 promise 本身,否則會造成死迴圈。

參考 前端進階面試題詳細解答

陣列去重

使用 indexOf/includes 實現

function unique(arr) {
    var res = [];
    for(var i = 0; i < arr.length; i++) {
        if(res.indexOf(arr[i]) === -1) res.push(arr[i]);
        // if(!res.includes(arr[i])) res.push(arr[i]);
    }
    return res;
}

使用 filter(forEach) + indexOf/includes 實現

// filter
function unique(arr) {
    var res = arr.filter((value, index) => {
        // 只存第一個出現的元素
        return arr.indexOf(value) === index;
    });
    return res;
}
// forEach
function unique(arr) {
    var res = [];
    arr.forEach((value) => {
        if(!res.includes(value)) res.push(value);
    });
    return res;
}

非 API 版本(原生)實現

function unique(arr) {
    var res = [];
    for(var i = 0; i < arr.length; i++) {
        var flag = false;
        for(var j = 0; j < res.length; j++) {
            if(arr[i] === res[j]) {
                flag = true;
                break;
            }
        }
        if(flag === false) res.push(arr[i]);
    }
    return res;
}

ES6 使用 Set + 擴充套件運算子(...)/Array.from() 實現

function unique(arr) {
    // return [...new Set(arr)];
    return Array.from(new Set(arr));
}

瞭解 this 嘛,bind,call,apply 具體指什麼

它們都是函式的方法

call: Array.prototype.call(this, args1, args2]) apply: Array.prototype.apply(this, [args1, args2]) :ES6 之前用來展開陣列呼叫, foo.appy(null, []),ES6 之後使用 ... 運算子

  • New 繫結 > 顯示繫結 > 隱式繫結 > 預設繫結
  • 如果需要使用 bind 的柯里化和 apply 的陣列解構,繫結到 null,儘可能使用 Object.create(null) 建立一個 DMZ 物件

四條規則:

  • 預設繫結,沒有其他修飾(bind、apply、call),在非嚴格模式下定義指向全域性物件,在嚴格模式下定義指向 undefined
function foo() {
     console.log(this.a); 
}

var a = 2;
foo();
  • 隱式繫結:呼叫位置是否有上下文物件,或者是否被某個物件擁有或者包含,那麼隱式繫結規則會把函式呼叫中的 this 繫結到這個上下文物件。而且,物件屬性鏈只有上一層或者說最後一層在呼叫位置中起作用
function foo() {
  console.log(this.a);
}

var obj = {
  a: 2,
  foo: foo,
}

obj.foo(); // 2
  • 顯示繫結:透過在函式上執行 call 和 apply ,來顯示的繫結 this
function foo() {
  console.log(this.a);
}

var obj = {
  a: 2
};

foo.call(obj);

顯示繫結之硬繫結

function foo(something) {
  console.log(this.a, something);

  return this.a + something;
}

function bind(fn, obj) {
  return function() {
    return fn.apply(obj, arguments);
  };
}

var obj = {
  a: 2
}

var bar = bind(foo, obj);

New 繫結,new 呼叫函式會建立一個全新的物件,並將這個物件繫結到函式呼叫的 this。

  • New 繫結時,如果是 new 一個硬繫結函式,那麼會用 new 新建的物件替換這個硬繫結 this,
function foo(a) {
  this.a = a;
}

var bar = new foo(2);
console.log(bar.a)

Promise.allSettled

描述:等到所有promise都返回結果,就返回一個promise例項。

實現

Promise.allSettled = function(promises) {
    return new Promise((resolve, reject) => {
        if(Array.isArray(promises)) {
            if(promises.length === 0) return resolve(promises);
            let result = [];
            let count = 0;
            promises.forEach((item, index) => {
                Promise.resolve(item).then(
                    value => {
                        count++;
                        result[index] = {
                            status: 'fulfilled',
                            value: value
                        };
                        if(count === promises.length) resolve(result);
                    }, 
                    reason => {
                        count++;
                        result[index] = {
                            status: 'rejected'.
                            reason: reason
                        };
                        if(count === promises.length) resolve(result);
                    }
                );
            });
        }
        else return reject(new TypeError("Argument is not iterable"));
    });
}

分片思想解決大資料量渲染問題

題目描述:渲染百萬條結構簡單的大資料時 怎麼使用分片思想最佳化渲染

實現程式碼如下:

let ul = document.getElementById("container");
// 插入十萬條資料
let total = 100000;
// 一次插入 20 條
let once = 20;
//總頁數
let page = total / once;
//每條記錄的索引
let index = 0;
//迴圈載入資料
function loop(curTotal, curIndex) {
  if (curTotal <= 0) {
    return false;
  }
  //每頁多少條
  let pageCount = Math.min(curTotal, once);
  window.requestAnimationFrame(function () {
    for (let i = 0; i < pageCount; i++) {
      let li = document.createElement("li");
      li.innerText = curIndex + i + " : " + ~~(Math.random() * total);
      ul.appendChild(li);
    }
    loop(curTotal - pageCount, curIndex + pageCount);
  });
}
loop(total, index);
擴充套件思考:對於大資料量的簡單 dom 結構渲染可以用分片思想解決 如果是複雜的 dom 結構渲染如何處理?

這時候就需要使用虛擬列表了 大家自行百度哈 虛擬列表和虛擬表格在日常專案使用還是很頻繁的

非同步任務排程器

描述:實現一個帶併發限制的非同步排程器 Scheduler,保證同時執行的任務最多有 limit 個。

實現

class Scheduler {
    queue = [];  // 用佇列儲存正在執行的任務
    runCount = 0;  // 計數正在執行的任務個數
    constructor(limit) {
        this.maxCount = limit;  // 允許併發的最大個數
    }
    add(time, data){
        const promiseCreator = () => {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    console.log(data);
                    resolve();
                }, time);
            });
        }
        this.queue.push(promiseCreator);
        // 每次新增的時候都會嘗試去執行任務
        this.request();
    }
    request() {
        // 佇列中還有任務才會被執行
        if(this.queue.length && this.runCount < this.maxCount) {
            this.runCount++;
            // 執行先加入佇列的函式
            this.queue.shift()().then(() => {
                this.runCount--;
                // 嘗試進行下一次任務
                this.request();
            });
        }
    }
}

// 測試
const scheduler = new Scheduler(2);
const addTask = (time, data) => {
    scheduler.add(time, data);
}

addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
// 輸出結果 2 3 1 4

什麼是同源策略

跨域問題其實就是瀏覽器的同源策略造成的。

同源策略限制了從同一個源載入的文件或指令碼如何與另一個源的資源進行互動。這是瀏覽器的一個用於隔離潛在惡意檔案的重要的安全機制。同源指的是:協議埠號域名必須一致。

同源策略:protocol(協議)、domain(域名)、port(埠)三者必須一致。

同源政策主要限制了三個方面:

  • 當前域下的 js 指令碼不能夠訪問其他域下的 cookie、localStorage 和 indexDB。
  • 當前域下的 js 指令碼不能夠操作訪問操作其他域下的 DOM。
  • 當前域下 ajax 無法傳送跨域請求。

同源政策的目的主要是為了保證使用者的資訊保安,它只是對 js 指令碼的一種限制,並不是對瀏覽器的限制,對於一般的 img、或者script 指令碼請求都不會有跨域的限制,這是因為這些操作都不會透過響應結果來進行可能出現安全問題的操作。

什麼是作用域鏈?

首先要了解作用域鏈,當訪問一個變數時,編譯器在執行這段程式碼時,會首先從當前的作用域中查詢是否有這個識別符號,如果沒有找到,就會去父作用域查詢,如果父作用域還沒找到繼續向上查詢,直到全域性作用域為止,,而作用域鏈,就是有當前作用域與上層作用域的一系列變數物件組成,它保證了當前執行的作用域對符合訪問許可權的變數和函式的有序訪問。

vue-router

vue-router是vuex.js官方的路由管理器,它和vue.js的核心深度整合,讓構建但頁面應用變得易如反掌

<router-link> 元件支援使用者在具有路由功能的應用中 (點選) 導航。 透過 to 屬性指定目標地址

<router-view> 元件是一個 functional 元件,渲染路徑匹配到的檢視元件。

<keep-alive> 元件是一個用來快取元件

router.beforeEach

router.afterEach

to: Route: 即將要進入的目標 路由物件

from: Route: 當前導航正要離開的路由

next: Function: 一定要呼叫該方法來 resolve 這個鉤子。執行效果依賴 next 方法的呼叫引數。

介紹了路由守衛及用法,在專案中路由守衛起到的作用等等

程式碼輸出結果

async function async1 () {
  console.log('async1 start');
  await new Promise(resolve => {
    console.log('promise1')
  })
  console.log('async1 success');
  return 'async1 end'
}
console.log('srcipt start')
async1().then(res => console.log(res))
console.log('srcipt end')

輸出結果如下:

script start
async1 start
promise1
script end

這裡需要注意的是在async1await後面的Promise是沒有返回值的,也就是它的狀態始終是pending狀態,所以在await之後的內容是不會執行的,包括async1後面的 .then

行內元素有哪些?塊級元素有哪些? 空(void)元素有那些?

  • 行內元素有:a b span img input select strong
  • 塊級元素有:div ul ol li dl dt dd h1 h2 h3 h4 h5 h6 p

空元素,即沒有內容的HTML元素。空元素是在開始標籤中關閉的,也就是空元素沒有閉合標籤:

  • 常見的有:<br><hr><img><input><link><meta>
  • 鮮見的有:<area><base><col><colgroup><command><embed><keygen><param><source><track><wbr>

二分查詢--時間複雜度 log2(n)

題目描述:如何確定一個數在一個有序陣列中的位置

實現程式碼如下:

function search(arr, target, start, end) {
  let targetIndex = -1;

  let mid = Math.floor((start + end) / 2);

  if (arr[mid] === target) {
    targetIndex = mid;
    return targetIndex;
  }

  if (start >= end) {
    return targetIndex;
  }

  if (arr[mid] < target) {
    return search(arr, target, mid + 1, end);
  } else {
    return search(arr, target, start, mid - 1);
  }
}
// const dataArr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
// const position = search(dataArr, 6, 0, dataArr.length - 1);
// if (position !== -1) {
//   console.log(`目標元素在陣列中的位置:${position}`);
// } else {
//   console.log("目標元素不在陣列中");
// }

程式碼輸出結果

var x = 3;
var y = 4;
var obj = {
    x: 1,
    y: 6,
    getX: function() {
        var x = 5;
        return function() {
            return this.x;
        }();
    },
    getY: function() {
        var y = 7;
        return this.y;
    }
}
console.log(obj.getX()) // 3
console.log(obj.getY()) // 6

輸出結果:3 6

解析:

  1. 我們知道,匿名函式的this是指向全域性物件的,所以this指向window,會列印出3;
  2. getY是由obj呼叫的,所以其this指向的是obj物件,會列印出6。

節流

節流(throttle):觸發高頻事件,且 N 秒內只執行一次。這就好比公交車,10 分鐘一趟,10 分鐘內有多少人在公交站等我不管,10 分鐘一到我就要發車走人!類似qq飛車的復位按鈕。

核心思想:使用時間戳或標誌來實現,立即執行一次,然後每 N 秒執行一次。如果N秒內觸發則直接返回。

應用:節流常應用於滑鼠不斷點選觸發、監聽滾動事件。

實現:

// 版本一:標誌實現
function throttle(fn, wait){
    let flag = true;  // 設定一個標誌
    return function(...args){
        if(!flag) return;
        flag = false;
        setTimeout(() => {
            fn.call(this, ...args);
            flag = true;
        }, wait);
    }
}

// 版本二:時間戳實現
function throttle(fn, wait) {
    let pre = 0;
    return function(...args) {
        let now = new Date();
        if(now - pre < wait) return;
        pre = now;
        fn.call(this, ...args);
    }
}

HTML5有哪些更新

1. 語義化標籤

  • header:定義文件的頁首(頭部);
  • nav:定義導航連結的部分;
  • footer:定義文件或節的頁尾(底部);
  • article:定義文章內容;
  • section:定義文件中的節(section、區段);
  • aside:定義其所處內容之外的內容(側邊);

2. 媒體標籤

(1) audio:音訊

<audio src='' controls autoplay loop='true'></audio>

屬性:

  • controls 控制皮膚
  • autoplay 自動播放
  • loop=‘true’ 迴圈播放

(2)video影片

<video src='' poster='imgs/aa.jpg' controls></video>

屬性:

  • poster:指定影片還沒有完全下載完畢,或者使用者還沒有點選播放前顯示的封面。預設顯示當前影片檔案的第一針畫面,當然透過poster也可以自己指定。
  • controls 控制皮膚
  • width
  • height

(3)source標籤
因為瀏覽器對影片格式支援程度不一樣,為了能夠相容不同的瀏覽器,可以透過source來指定影片源。

<video>
     <source src='aa.flv' type='video/flv'></source>
     <source src='aa.mp4' type='video/mp4'></source>
</video>

3. 表單

表單型別:

  • email :能夠驗證當前輸入的郵箱地址是否合法
  • url : 驗證URL
  • number : 只能輸入數字,其他輸入不了,而且自帶上下增大減小箭頭,max屬性可以設定為最大值,min可以設定為最小值,value為預設值。
  • search : 輸入框後面會給提供一個小叉,可以刪除輸入的內容,更加人性化。
  • range : 可以提供給一個範圍,其中可以設定max和min以及value,其中value屬性可以設定為預設值
  • color : 提供了一個顏色拾取器
  • time : 時分秒
  • data : 日期選擇年月日
  • datatime : 時間和日期(目前只有Safari支援)
  • datatime-local :日期時間控制元件
  • week :周控制元件
  • month:月控制元件

表單屬性:

  • placeholder :提示資訊
  • autofocus :自動獲取焦點
  • autocomplete=“on” 或者 autocomplete=“off” 使用這個屬性需要有兩個前提:

    • 表單必須提交過
    • 必須有name屬性。
  • required:要求輸入框不能為空,必須有值才能夠提交。
  • pattern=" " 裡面寫入想要的正則模式,例如手機號patte="^(+86)?\d{10}$"
  • multiple:可以選擇多個檔案或者多個郵箱
  • form=" form表單的ID"

表單事件:

  • oninput 每當input裡的輸入框內容發生變化都會觸發此事件。
  • oninvalid 當驗證不透過時觸發此事件。

4. 進度條、度量器

  • progress標籤:用來表示任務的進度(IE、Safari不支援),max用來表示任務的進度,value表示已完成多少
  • meter屬性:用來顯示剩餘容量或剩餘庫存(IE、Safari不支援)

    • high/low:規定被視作高/低的範圍
    • max/min:規定最大/小值
    • value:規定當前度量值

設定規則:min < low < high < max

5.DOM查詢操作

  • document.querySelector()
  • document.querySelectorAll()

它們選擇的物件可以是標籤,可以是類(需要加點),可以是ID(需要加#)

6. Web儲存

HTML5 提供了兩種在客戶端儲存資料的新方法:

  • localStorage - 沒有時間限制的資料儲存
  • sessionStorage - 針對一個 session 的資料儲存

7. 其他

  • 拖放:拖放是一種常見的特性,即抓取物件以後拖到另一個位置。設定元素可拖放:
<img draggable="true" />
  • 畫布(canvas ): canvas 元素使用 JavaScript 在網頁上繪製影像。畫布是一個矩形區域,可以控制其每一畫素。canvas 擁有多種繪製路徑、矩形、圓形、字元以及新增影像的方法。
<canvas id="myCanvas" width="200" height="100"></canvas>
  • SVG:SVG 指可伸縮向量圖形,用於定義用於網路的基於向量的圖形,使用 XML 格式定義圖形,影像在放大或改變尺寸的情況下其圖形質量不會有損失,它是全球資訊網聯盟的標準
  • 地理定位:Geolocation(地理定位)用於定位使用者的位置。‘

總結: (1)新增語義化標籤:nav、header、footer、aside、section、article
(2)音訊、影片標籤:audio、video
(3)資料儲存:localStorage、sessionStorage
(4)canvas(畫布)、Geolocation(地理定位)、websocket(通訊協議)
(5)input標籤新增屬性:placeholder、autocomplete、autofocus、required
(6)history API:go、forward、back、pushstate

移除的元素有:

  • 純表現的元素:basefont,big,center,font, s,strike,tt,u;
  • 對可用性產生負面影響的元素:frame,frameset,noframes;

相關文章