前端一面高頻面試題(附答案)

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

說一說正向代理和反向代理

正向代理

我們常說的代理也就是指正向代理,正向代理的過程,它隱藏了真實的請求客戶端,服務端不知道真實的客戶端是誰,客戶端請求的服務都被代理伺服器代替來請求。

反向代理

這種代理模式下,它隱藏了真實的服務端,當我們向一個網站發起請求的時候,背後可能有成千上萬臺伺服器為我們服務,具體是哪一臺,我們不清楚,我們只需要知道反向代理伺服器是誰就行,而且反向代理伺服器會幫我們把請求轉發到真實的伺服器那裡去,一般而言反向代理伺服器一般用來實現負載平衡。

負載平衡的兩種實現方式?

  • 一種是使用反向代理的方式,使用者的請求都傳送到反向代理服務上,然後由反向代理伺服器來轉發請求到真實的伺服器上,以此來實現叢集的負載平衡。
  • 另一種是 DNS 的方式,DNS 可以用於在冗餘的伺服器上實現負載平衡。因為現在一般的大型網站使用多臺伺服器提供服務,因此一個域名可能會對應多個伺服器地址。當使用者向網站域名請求的時候,DNS 伺服器返回這個域名所對應的伺服器 IP 地址的集合,但在每個回答中,會迴圈這些 IP 地址的順序,使用者一般會選擇排在前面的地址傳送請求。以此將使用者的請求均衡的分配到各個不同的伺服器上,這樣來實現負載均衡。這種方式有一個缺點就是,由於 DNS 伺服器中存在快取,所以有可能一個伺服器出現故障後,域名解析仍然返回的是那個 IP 地址,就會造成訪問的問題。

如何最佳化動畫?

對於如何最佳化動畫,我們知道,一般情況下,動畫需要頻繁的操作DOM,就就會導致頁面的效能問題,我們可以將動畫的position屬性設定為absolute或者fixed,將動畫脫離文件流,這樣他的迴流就不會影響到頁面了。

函式柯里化

柯里化(currying) 指的是將一個多引數的函式拆分成一系列函式,每個拆分後的函式都只接受一個引數。

對於已經柯里化後的函式來說,當接收的引數數量與原函式的形引數量相同時,執行原函式; 當接收的引數數量小於原函式的形引數量時,返回一個函式用於接收剩餘的引數,直至接收的引數數量與形引數量一致,執行原函式。

陣列能夠呼叫的函式有那些?

  • push
  • pop
  • splice
  • slice
  • shift
  • unshift
  • sort
  • find
  • findIndex
  • map/filter/reduce 等函數語言程式設計方法
  • 還有一些原型鏈上的方法:toString/valudOf

行內元素有哪些?塊級元素有哪些? 空(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>

用過 TypeScript 嗎?它的作用是什麼?

為 JS 新增型別支援,以及提供最新版的 ES 語法的支援,是的利於團隊協作和排錯,開發大型專案

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

介紹下 promise 的特性、優缺點,內部是如何實現的,動手實現 Promise

1)Promise基本特性

  • 1、Promise有三種狀態:pending(進行中)、fulfilled(已成功)、rejected(已失敗)
  • 2、Promise物件接受一個回撥函式作為引數, 該回撥函式接受兩個引數,分別是成功時的回撥resolve和失敗時的回撥reject;另外resolve的引數除了正常值以外, 還可能是一個Promise物件的例項;reject的引數通常是一個Error物件的例項。
  • 3、then方法返回一個新的Promise例項,並接收兩個引數onResolved(fulfilled狀態的回撥);onRejected(rejected狀態的回撥,該引數可選)
  • 4、catch方法返回一個新的Promise例項
  • 5、finally方法不管Promise狀態如何都會執行,該方法的回撥函式不接受任何引數
  • 6、Promise.all()方法將多個多個Promise例項,包裝成一個新的Promise例項,該方法接受一個由Promise物件組成的陣列作為引數(Promise.all()方法的引數可以不是陣列,但必須具有Iterator介面,且返回的每個成員都是Promise例項),注意引數中只要有一個例項觸發catch方法,都會觸發Promise.all()方法返回的新的例項的catch方法,如果引數中的某個例項本身呼叫了catch方法,將不會觸發Promise.all()方法返回的新例項的catch方法
  • 7、Promise.race()方法的引數與Promise.all方法一樣,引數中的例項只要有一個率先改變狀態就會將該例項的狀態傳給Promise.race()方法,並將返回值作為Promise.race()方法產生的Promise例項的返回值
  • 8、Promise.resolve()將現有物件轉為Promise物件,如果該方法的引數為一個Promise物件,Promise.resolve()將不做任何處理;如果引數thenable物件(即具有then方法),Promise.resolve()將該物件轉為Promise物件並立即執行then方法;如果引數是一個原始值,或者是一個不具有then方法的物件,則Promise.resolve方法返回一個新的Promise物件,狀態為fulfilled,其引數將會作為then方法中onResolved回撥函式的引數,如果Promise.resolve方法不帶引數,會直接返回一個fulfilled狀態的 Promise 物件。需要注意的是,立即resolve()的 Promise 物件,是在本輪“事件迴圈”(event loop)的結束時執行,而不是在下一輪“事件迴圈”的開始時。
  • 9、Promise.reject()同樣返回一個新的Promise物件,狀態為rejected,無論傳入任何引數都將作為reject()的引數

2)Promise優點

  • ①統一非同步 API

    • Promise 的一個重要優點是它將逐漸被用作瀏覽器的非同步 API ,統一現在各種各樣的 API ,以及不相容的模式和手法。
  • ②Promise 與事件對比

    • 和事件相比較, Promise 更適合處理一次性的結果。在結果計算出來之前或之後註冊回撥函式都是可以的,都可以拿到正確的值。 Promise 的這個優點很自然。但是,不能使用 Promise 處理多次觸發的事件。鏈式處理是 Promise 的又一優點,但是事件卻不能這樣鏈式處理。
  • ③Promise 與回撥對比

    • 解決了回撥地獄的問題,將非同步操作以同步操作的流程表達出來。
  • ④Promise 帶來的額外好處是包含了更好的錯誤處理方式(包含了異常處理),並且寫起來很輕鬆(因為可以重用一些同步的工具,比如 Array.prototype.map() )。

3)Promise缺點

  • 1、無法取消Promise,一旦新建它就會立即執行,無法中途取消。
  • 2、如果不設定回撥函式,Promise內部丟擲的錯誤,不會反應到外部。
  • 3、當處於Pending狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。
  • 4、Promise 真正執行回撥的時候,定義 Promise 那部分實際上已經走完了,所以 Promise 的報錯堆疊上下文不太友好。

4)簡單程式碼實現
最簡單的Promise實現有7個主要屬性, state(狀態), value(成功返回值), reason(錯誤資訊), resolve方法, reject方法, then方法

class Promise{
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    let resolve = value => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
      }
    };
    let reject = reason => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
      }
    };
    try {
      // 立即執行函式
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }
  then(onFulfilled, onRejected) {
    if (this.state === 'fulfilled') {
      let x = onFulfilled(this.value);
    };
    if (this.state === 'rejected') {
      let x = onRejected(this.reason);
    };
  }
}

5)面試夠用版

function myPromise(constructor){ let self=this;
  self.status="pending" //定義狀態改變前的初始狀態 
  self.value=undefined;//定義狀態為resolved的時候的狀態 
  self.reason=undefined;//定義狀態為rejected的時候的狀態 
  function resolve(value){
    //兩個==="pending",保證了了狀態的改變是不不可逆的 
    if(self.status==="pending"){
      self.value=value;
      self.status="resolved"; 
    }
  }
  function reject(reason){
     //兩個==="pending",保證了了狀態的改變是不不可逆的
     if(self.status==="pending"){
        self.reason=reason;
        self.status="rejected"; 
      }
  }
  //捕獲構造異常 
  try{
      constructor(resolve,reject);
  }catch(e){
    reject(e);
    } 
}
myPromise.prototype.then=function(onFullfilled,onRejected){ 
  let self=this;
  switch(self.status){
    case "resolved": onFullfilled(self.value); break;
    case "rejected": onRejected(self.reason); break;
    default: 
  }
}

// 測試
var p=new myPromise(function(resolve,reject){resolve(1)}); 
p.then(function(x){console.log(x)})
//輸出1

6)大廠專供版

const PENDING = "pending"; 
const FULFILLED = "fulfilled"; 
const REJECTED = "rejected";
const resolvePromise = (promise, x, resolve, reject) => {
  if (x === promise) {
    // If promise and x refer to the same object, reject promise with a TypeError as the reason.
    reject(new TypeError('迴圈引用'))
  }
  // if x is an object or function,
  if (x !== null && typeof x === 'object' || typeof x === 'function') {
    // If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.
    let called
    try { // If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
      let then = x.then // Let then be x.then
      // If then is a function, call it with x as this
      if (typeof then === 'function') {
        // If/when resolvePromise is called with a value y, run [[Resolve]](promise, y)
        // If/when rejectPromise is called with a reason r, reject promise with r.
        then.call(x, y => {
          if (called) return
          called = true
          resolvePromise(promise, y, resolve, reject)
        }, r => {
          if (called) return
          called = true
          reject(r)
        })
      } else {
        // If then is not a function, fulfill promise with x.
        resolve(x)
      }
    } catch (e) {
      if (called) return
      called = true
      reject(e)
    }
  } else {
    // If x is not an object or function, fulfill promise with x
    resolve(x)
  }
}
function Promise(excutor) {
  let that = this; // 快取當前promise例項例物件
  that.status = PENDING; // 初始狀態
  that.value = undefined; // fulfilled狀態時 返回的資訊
  that.reason = undefined; // rejected狀態時 拒絕的原因 
  that.onFulfilledCallbacks = []; // 儲存fulfilled狀態對應的onFulfilled函式
  that.onRejectedCallbacks = []; // 儲存rejected狀態對應的onRejected函式
  function resolve(value) { // value成功態時接收的終值
    if(value instanceof Promise) {
      return value.then(resolve, reject);
    }
    // 實踐中要確保 onFulfilled 和 onRejected ⽅方法非同步執⾏行行,且應該在 then ⽅方法被調⽤用的那⼀一輪事件迴圈之後的新執⾏行行棧中執⾏行行。
    setTimeout(() => {
      // 調⽤用resolve 回撥對應onFulfilled函式
      if (that.status === PENDING) {
        // 只能由pending狀態 => fulfilled狀態 (避免調⽤用多次resolve reject)
        that.status = FULFILLED;
        that.value = value;
        that.onFulfilledCallbacks.forEach(cb => cb(that.value));
      }
    });
  }
  function reject(reason) { // reason失敗態時接收的拒因
    setTimeout(() => {
      // 調⽤用reject 回撥對應onRejected函式
      if (that.status === PENDING) {
        // 只能由pending狀態 => rejected狀態 (避免調⽤用多次resolve reject)
        that.status = REJECTED;
        that.reason = reason;
        that.onRejectedCallbacks.forEach(cb => cb(that.reason));
      }
    });
  }

  // 捕獲在excutor執⾏行行器器中丟擲的異常
  // new Promise((resolve, reject) => {
  //     throw new Error('error in excutor')
  // })
  try {
    excutor(resolve, reject);
  } catch (e) {
    reject(e);
  }
}
Promise.prototype.then = function(onFulfilled, onRejected) {
  const that = this;
  let newPromise;
  // 處理理引數預設值 保證引數後續能夠繼續執⾏行行
  onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;
  onRejected = typeof onRejected === "function" ? onRejected : reason => {
    throw reason;
  };
  if (that.status === FULFILLED) { // 成功態
    return newPromise = new Promise((resolve, reject) => {
      setTimeout(() => {
        try{
          let x = onFulfilled(that.value);
          resolvePromise(newPromise, x, resolve, reject); //新的promise resolve 上⼀一個onFulfilled的返回值
        } catch(e) {
          reject(e); // 捕獲前⾯面onFulfilled中丟擲的異常then(onFulfilled, onRejected);
        }
      });
    })
  }
  if (that.status === REJECTED) { // 失敗態
    return newPromise = new Promise((resolve, reject) => {
      setTimeout(() => {
        try {
          let x = onRejected(that.reason);
          resolvePromise(newPromise, x, resolve, reject);
        } catch(e) {
          reject(e);
        }
      });
    });
  }
  if (that.status === PENDING) { // 等待態
// 當非同步調⽤用resolve/rejected時 將onFulfilled/onRejected收集暫存到集合中
    return newPromise = new Promise((resolve, reject) => {
      that.onFulfilledCallbacks.push((value) => {
        try {
          let x = onFulfilled(value);
          resolvePromise(newPromise, x, resolve, reject);
        } catch(e) {
          reject(e);
        }
      });
      that.onRejectedCallbacks.push((reason) => {
        try {
          let x = onRejected(reason);
          resolvePromise(newPromise, x, resolve, reject);
        } catch(e) {
          reject(e);
        }
      });
    });
  }
};

什麼是原型什麼是原型鏈?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>

</body>
<script>
    function Person () {    }    var person  = new Person();    person.name = 'Kevin';    console.log(person.name) // Kevin

    // prototype
    function Person () {    }    Person.prototype.name = 'Kevin';    var person1 = new Person();    var person2 = new Person();    console.log(person1.name)// Kevin
    console.log(person2.name)// Kevin

    // __proto__
    function Person () {    }    var person = new Person();    console.log(person.__proto__ === Person.prototype) // true

    //constructor
    function Person() {    }    console.log(Person === Person.prototype.constructor) // true

    //綜上所述
    function Person () {    }    var person = new Person()    console.log(person.__proto__ == Person.prototype) // true
    console.log(Person.prototype.constructor == Person) // true
    //順便學習一下ES5得方法,可以獲得物件得原型
    console.log(Object.getPrototypeOf(person) === Person.prototype) // true

    //例項與原型
    function Person () {    }    Person.prototype.name = 'Kevin';    var person = new Person();    person.name = 'Daisy';    console.log(person.name) // Daisy
    delete person.name;    console.log(person.name) // Kevin

    //原型得原型
    var obj = new Object();    obj.name = 'Kevin',    console.log(obj.name) //Kevin

     //原型鏈
     console.log(Object.prototype.__proto__ === null) //true
     // null 表示"沒用物件" 即該處不應該有值

     // 補充
     function Person() {     }     var person = new Person()     console.log(person.constructor === Person) // true
     //當獲取person.constructor時,其實person中並沒有constructor屬性,當不能讀取到constructor屬性時,會從person的原型
     //也就是Person.prototype中讀取時,正好原型中有該屬性,所以
     person.constructor === Person.prototype.constructor

     //__proto__
     //其次是__proto__,絕大部分瀏覽器都支援這個非標準的方法訪問原型,然而它並不存在於Person.prototype中,實際上,它
     // 是來自與Object.prototype,與其說是一個屬性,不如說是一個getter/setter,當使用obj.__proto__時,可以理解成返回了
     // Object.getPrototypeOf(obj)
     總結:          1、當一個物件查詢屬性和方法時會從自身查詢,如果查詢不到則會透過__proto__指向被例項化的建構函式的prototype     2、隱式原型也是一個物件,是指向我們建構函式的原型     3、除了最頂層的Object物件沒有__proto_,其他所有的物件都有__proto__,這是隱式原型     4、隱式原型__proto__的作用是讓物件透過它來一直往上查詢屬性或方法,直到找到最頂層的Object的__proto__屬性,它的值是null,這個查詢的過程就是原型鏈



</script>
</html>

Vue的父子元件生命週期鉤子函式執行順序?

<!-- 載入渲染過程 -->
    <!-- 父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created ->
    子beforeMount -> 子mounted -> 父mounted -->
    <!-- 子元件更新過程 -->
    <!-- 父beforeUpdate -> 子beforeUpdate -> 子updaed -> 父updated -->
    <!-- 父元件跟新過程 -->
    <!-- 父beforeUpdate -> 父updated -->
    <!-- 銷燬過程 -->
    <!-- 父beforeDestroy -> 子beforeDestroy -> 子destroyed ->父destroyed -->

懶載入的特點

  • 減少無用資源的載入:使用懶載入明顯減少了伺服器的壓力和流量,同時也減小了瀏覽器的負擔。
  • 提升使用者體驗: 如果同時載入較多圖片,可能需要等待的時間較長,這樣影響了使用者體驗,而使用懶載入就能大大的提高使用者體驗。
  • 防止載入過多圖片而影響其他資原始檔的載入 :會影響網站應用的正常使用。

陣列去重

實現程式碼如下:

function uniqueArr(arr) {
  return [...new Set(arr)];
}

事件迴圈機制 (Event Loop)

事件迴圈機制從整體上告訴了我們 JavaScript 程式碼的執行順序 Event Loop即事件迴圈,是指瀏覽器或Node的一種解決javaScript單執行緒執行時不會阻塞的一種機制,也就是我們經常使用非同步的原理。

先執行 Script 指令碼,然後清空微任務佇列,然後開始下一輪事件迴圈,繼續先執行宏任務,再清空微任務佇列,如此往復。

  • 宏任務:Script/setTimeout/setInterval/setImmediate/ I/O / UI Rendering
  • 微任務:process.nextTick()/Promise

上訴的 setTimeout 和 setInterval 等都是任務源,真正進入任務佇列的是他們分發的任務。

優先順序

  • setTimeout = setInterval 一個佇列
  • setTimeout > setImmediate
  • process.nextTick > Promise
for (const macroTask of macroTaskQueue) {  
  handleMacroTask();    
  for (const microTask of microTaskQueue) {    
      handleMicroTask(microTask);  
  }
}

程式碼輸出結果

// a
function Foo () {
 getName = function () {
   console.log(1);
 }
 return this;
}
// b
Foo.getName = function () {
 console.log(2);
}
// c
Foo.prototype.getName = function () {
 console.log(3);
}
// d
var getName = function () {
 console.log(4);
}
// e
function getName () {
 console.log(5);
}

Foo.getName();           // 2
getName();               // 4
Foo().getName();         // 1
getName();               // 1 
new Foo.getName();       // 2
new Foo().getName();     // 3
new new Foo().getName(); // 3

輸出結果:2 4 1 1 2 3 3

解析:

  1. Foo.getName(), Foo為一個函式物件,物件都可以有屬性,b 處定義Foo的getName屬性為函式,輸出2;
  2. getName(), 這裡看d、e處,d為函式表示式,e為函式宣告,兩者區別在於變數提升,函式宣告的 5 會被後邊函式表示式的 4 覆蓋;
  3. Foo().getName(), 這裡要看a處,在Foo內部將全域性的getName重新賦值為 console.log(1) 的函式,執行Foo()返回 this,這個this指向window,Foo().getName() 即為window.getName(),輸出 1;
  4. getName(), 上面3中,全域性的getName已經被重新賦值,所以這裡依然輸出 1;
  5. new Foo.getName(), 這裡等價於 new (Foo.getName()),先執行 Foo.getName(),輸出 2,然後new一個例項;
  6. new Foo().getName(), 這 裡等價於 (new Foo()).getName(), 先new一個Foo的例項,再執行這個例項的getName方法,但是這個例項本身沒有這個方法,所以去原型鏈__protot__上邊找,例項.protot === Foo.prototype,所以輸出 3;
  7. new new Foo().getName(), 這裡等價於new (new Foo().getName()),如上述6,先輸出 3,然後new 一個 new Foo().getName() 的例項。

什麼是閉包,閉包的作用是什麼

當一個內部函式被呼叫,就會形成閉包,閉包就是能夠讀取其他函式內部變數的函式。
閉包作用:
區域性變數無法共享和長久的儲存,而全域性變數可能造成變數汙染,所以我們希望有一種機制既可以長久的儲存變數又不會造成全域性汙染。

寄生組合繼承

題目描述:實現一個你認為不錯的 js 繼承方式

實現程式碼如下:

function Parent(name) {
  this.name = name;
  this.say = () => {
    console.log(111);
  };
}
Parent.prototype.play = () => {
  console.log(222);
};
function Children(name) {
  Parent.call(this);
  this.name = name;
}
Children.prototype = Object.create(Parent.prototype);
Children.prototype.constructor = Children;
// let child = new Children("111");
// // console.log(child.name);
// // child.say();
// // child.play();

實現函式原型方法

call

使用一個指定的 this 值和一個或多個引數來呼叫一個函式。

實現要點:

  • this 可能傳入 null;
  • 傳入不固定個數的引數;
  • 函式可能有返回值;
Function.prototype.call2 = function (context) {
    var context = context || window;
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }

    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}

apply

apply 和 call 一樣,唯一的區別就是 call 是傳入不固定個數的引數,而 apply 是傳入一個陣列。

實現要點:

  • this 可能傳入 null;
  • 傳入一個陣列;
  • 函式可能有返回值;
Function.prototype.apply2 = function (context, arr) {
    var context = context || window;
    context.fn = this;

    var result;
    if (!arr) {
        result = context.fn();
    } else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}

bind

bind 方法會建立一個新的函式,在 bind() 被呼叫時,這個新函式的 this 被指定為 bind() 的第一個引數,而其餘引數將作為新函式的引數,供呼叫時使用。

實現要點:

  • bind() 除了 this 外,還可傳入多個引數;
  • bing 建立的新函式可能傳入多個引數;
  • 新函式可能被當做建構函式呼叫;
  • 函式可能有返回值;
Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

實現 new 關鍵字

new 運算子用來建立使用者自定義的物件型別的例項或者具有建構函式的內建物件的例項。

實現要點:

  • new 會產生一個新物件;
  • 新物件需要能夠訪問到建構函式的屬性,所以需要重新指定它的原型;
  • 建構函式可能會顯示返回;
function objectFactory() {
    var obj = new Object()
    Constructor = [].shift.call(arguments);
    obj.__proto__ = Constructor.prototype;
    var ret = Constructor.apply(obj, arguments);

    // ret || obj 這裡這麼寫考慮了建構函式顯示返回 null 的情況
    return typeof ret === 'object' ? ret || obj : obj;
};

使用:

function person(name, age) {
    this.name = name
    this.age = age
}
let p = objectFactory(person, '布蘭', 12)
console.log(p)  // { name: '布蘭', age: 12 }

實現 instanceof 關鍵字

instanceof 就是判斷建構函式的 prototype 屬性是否出現在例項的原型鏈上。

function instanceOf(left, right) {
    let proto = left.__proto__
    while (true) {
        if (proto === null) return false
        if (proto === right.prototype) {
            return true
        }
        proto = proto.__proto__
    }
}

上面的 left.proto 這種寫法可以換成 Object.getPrototypeOf(left)。

實現 Object.create

Object.create()方法建立一個新物件,使用現有的物件來提供新建立的物件的__proto__。

Object.create2 = function(proto, propertyObject = undefined) {
    if (typeof proto !== 'object' && typeof proto !== 'function') {
        throw new TypeError('Object prototype may only be an Object or null.')
    if (propertyObject == null) {
        new TypeError('Cannot convert undefined or null to object')
    }
    function F() {}
    F.prototype = proto
    const obj = new F()
    if (propertyObject != undefined) {
        Object.defineProperties(obj, propertyObject)
    }
    if (proto === null) {
        // 建立一個沒有原型物件的物件,Object.create(null)
        obj.__proto__ = null
    }
    return obj
}

實現 Object.assign

Object.assign2 = function(target, ...source) {
    if (target == null) {
        throw new TypeError('Cannot convert undefined or null to object')
    }
    let ret = Object(target) 
    source.forEach(function(obj) {
        if (obj != null) {
            for (let key in obj) {
                if (obj.hasOwnProperty(key)) {
                    ret[key] = obj[key]
                }
            }
        }
    })
    return ret
}

實現 JSON.stringify

JSON.stringify([, replacer [, space]) 方法是將一個 JavaScript 值(物件或者陣列)轉換為一個 JSON 字串。此處模擬實現,不考慮可選的第二個引數 replacer 和第三個引數 space

  1. 基本資料型別:

    • undefined 轉換之後仍是 undefined(型別也是 undefined)
    • boolean 值轉換之後是字串 "false"/"true"
    • number 型別(除了 NaN 和 Infinity)轉換之後是字串型別的數值
    • symbol 轉換之後是 undefined
    • null 轉換之後是字串 "null"
    • string 轉換之後仍是string
    • NaN 和 Infinity 轉換之後是字串 "null"
  2. 函式型別:轉換之後是 undefined
  3. 如果是物件型別(非函式)

    • 如果是一個陣列:如果屬性值中出現了 undefined、任意的函式以及 symbol,轉換成字串 "null" ;
    • 如果是 RegExp 物件:返回 {} (型別是 string);
    • 如果是 Date 物件,返回 Date 的 toJSON 字串值;
    • 如果是普通物件;

      • 如果有 toJSON() 方法,那麼序列化 toJSON() 的返回值。
      • 如果屬性值中出現了 undefined、任意的函式以及 symbol 值,忽略。
      • 所有以 symbol 為屬性鍵的屬性都會被完全忽略掉。
  4. 對包含迴圈引用的物件(物件之間相互引用,形成無限迴圈)執行此方法,會丟擲錯誤。
function jsonStringify(data) {
    let dataType = typeof data;

    if (dataType !== 'object') {
        let result = data;
        //data 可能是 string/number/null/undefined/boolean
        if (Number.isNaN(data) || data === Infinity) {
            //NaN 和 Infinity 序列化返回 "null"
            result = "null";
        } else if (dataType === 'function' || dataType === 'undefined' || dataType === 'symbol') {
            //function 、undefined 、symbol 序列化返回 undefined
            return undefined;
        } else if (dataType === 'string') {
            result = '"' + data + '"';
        }
        //boolean 返回 String()
        return String(result);
    } else if (dataType === 'object') {
        if (data === null) {
            return "null"
        } else if (data.toJSON && typeof data.toJSON === 'function') {
            return jsonStringify(data.toJSON());
        } else if (data instanceof Array) {
            let result = [];
            //如果是陣列
            //toJSON 方法可以存在於原型鏈中
            data.forEach((item, index) => {
                if (typeof item === 'undefined' || typeof item === 'function' || typeof item === 'symbol') {
                    result[index] = "null";
                } else {
                    result[index] = jsonStringify(item);
                }
            });
            result = "[" + result + "]";
            return result.replace(/'/g, '"');

        } else {
            //普通物件
            /**             * 迴圈引用拋錯(暫未檢測,迴圈引用時,堆疊溢位)             * symbol key 忽略             * undefined、函式、symbol 為屬性值,被忽略             */
            let result = [];
            Object.keys(data).forEach((item, index) => {
                if (typeof item !== 'symbol') {
                    //key 如果是symbol物件,忽略
                    if (data[item] !== undefined && typeof data[item] !== 'function'
                        && typeof data[item] !== 'symbol') {
                        //鍵值如果是 undefined、函式、symbol 為屬性值,忽略
                        result.push('"' + item + '"' + ":" + jsonStringify(data[item]));
                    }
                }
            });
            return ("{" + result + "}").replace(/'/g, '"');
        }
    }
}

實現 JSON.parse

介紹 2 種方法實現:

  • eval 實現;
  • new Function 實現;

eval 實現

第一種方式最簡單,也最直觀,就是直接呼叫 eval,程式碼如下:

var json = '{"a":"1", "b":2}';
var obj = eval("(" + json + ")");  // obj 就是 json 反序列化之後得到的物件

但是直接呼叫 eval 會存在安全問題,如果資料中可能不是 json 資料,而是可執行的 JavaScript 程式碼,那很可能會造成 XSS 攻擊。因此,在呼叫 eval 之前,需要對資料進行校驗。

var rx_one = /^[\],:{}\s]*$/;
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
var rx_four = /(?:^|:|,)(?:\s*\[)+/g;

if (
    rx_one.test(
        json.replace(rx_two, "@")
            .replace(rx_three, "]")
            .replace(rx_four, "")
    )
) {
    var obj = eval("(" +json + ")");
}

new Function 實現

Function 與 eval 有相同的字串引數特性。

var json = '{"name":"小姐姐", "age":20}';
var obj = (new Function('return ' + json))();

實現 Promise

實現 Promise 需要完全讀懂 Promise A+ 規範,不過從總體的實現上看,有如下幾個點需要考慮到:

  • then 需要支援鏈式呼叫,所以得返回一個新的 Promise;
  • 處理非同步問題,所以得先用 onResolvedCallbacks 和 onRejectedCallbacks 分別把成功和失敗的回撥存起來;
  • 為了讓鏈式呼叫正常進行下去,需要判斷 onFulfilled 和 onRejected 的型別;
  • onFulfilled 和 onRejected 需要被非同步呼叫,這裡用 setTimeout 模擬非同步;
  • 處理 Promise 的 resolve;
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class Promise {
    constructor(executor) {
        this.status = PENDING;
        this.value = undefined;
        this.reason = undefined;
        this.onResolvedCallbacks = [];
        this.onRejectedCallbacks = [];

        let resolve = (value) = > {
            if (this.status === PENDING) {
                this.status = FULFILLED;
                this.value = value;
                this.onResolvedCallbacks.forEach((fn) = > fn());
            }
        };

        let reject = (reason) = > {
            if (this.status === PENDING) {
                this.status = REJECTED;
                this.reason = reason;
                this.onRejectedCallbacks.forEach((fn) = > fn());
            }
        };

        try {
            executor(resolve, reject);
        } catch (error) {
            reject(error);
        }
    }

    then(onFulfilled, onRejected) {
        // 解決 onFufilled,onRejected 沒有傳值的問題
        onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) = > v;
        // 因為錯誤的值要讓後面訪問到,所以這裡也要丟擲錯誤,不然會在之後 then 的 resolve 中捕獲
        onRejected = typeof onRejected === "function" ? onRejected : (err) = > {
            throw err;
        };
        // 每次呼叫 then 都返回一個新的 promise
        let promise2 = new Promise((resolve, reject) = > {
            if (this.status === FULFILLED) {
                //Promise/A+ 2.2.4 --- setTimeout
                setTimeout(() = > {
                    try {
                        let x = onFulfilled(this.value);
                        // x可能是一個proimise
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                }, 0);
            }

            if (this.status === REJECTED) {
                //Promise/A+ 2.2.3
                setTimeout(() = > {
                    try {
                        let x = onRejected(this.reason);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                }, 0);
            }

            if (this.status === PENDING) {
                this.onResolvedCallbacks.push(() = > {
                    setTimeout(() = > {
                        try {
                            let x = onFulfilled(this.value);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    }, 0);
                });

                this.onRejectedCallbacks.push(() = > {
                    setTimeout(() = > {
                        try {
                            let x = onRejected(this.reason);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    }, 0);
                });
            }
        });

        return promise2;
    }
}
const resolvePromise = (promise2, x, resolve, reject) = > {
    // 自己等待自己完成是錯誤的實現,用一個型別錯誤,結束掉 promise  Promise/A+ 2.3.1
    if (promise2 === x) {
        return reject(
            new TypeError("Chaining cycle detected for promise #<Promise>"));
    }
    // Promise/A+ 2.3.3.3.3 只能呼叫一次
    let called;
    // 後續的條件要嚴格判斷 保證程式碼能和別的庫一起使用
    if ((typeof x === "object" && x != null) || typeof x === "function") {
        try {
            // 為了判斷 resolve 過的就不用再 reject 了(比如 reject 和 resolve 同時呼叫的時候)  Promise/A+ 2.3.3.1
            let then = x.then;
            if (typeof then === "function") {
            // 不要寫成 x.then,直接 then.call 就可以了 因為 x.then 會再次取值,Object.defineProperty  Promise/A+ 2.3.3.3
                then.call(
                    x, (y) = > {
                        // 根據 promise 的狀態決定是成功還是失敗
                        if (called) return;
                        called = true;
                        // 遞迴解析的過程(因為可能 promise 中還有 promise) Promise/A+ 2.3.3.3.1
                        resolvePromise(promise2, y, resolve, reject);
                    }, (r) = > {
                        // 只要失敗就失敗 Promise/A+ 2.3.3.3.2
                        if (called) return;
                        called = true;
                        reject(r);
                    });
            } else {
                // 如果 x.then 是個普通值就直接返回 resolve 作為結果  Promise/A+ 2.3.3.4
                resolve(x);
            }
        } catch (e) {
            // Promise/A+ 2.3.3.2
            if (called) return;
            called = true;
            reject(e);
        }
    } else {
        // 如果 x 是個普通值就直接返回 resolve 作為結果  Promise/A+ 2.3.4
        resolve(x);
    }
};

Promise 寫完之後可以透過 promises-aplus-tests 這個包對我們寫的程式碼進行測試,看是否符合 A+ 規範。不過測試前還得加一段程式碼:

// promise.js
// 這裡是上面寫的 Promise 全部程式碼
Promise.defer = Promise.deferred = function () {
    let dfd = {}
    dfd.promise = new Promise((resolve,reject)=>{
        dfd.resolve = resolve;
        dfd.reject = reject;
    });
    return dfd;
}
module.exports = Promise;

全域性安裝:

npm i promises-aplus-tests -g

終端下執行驗證命令:

promises-aplus-tests promise.js

上面寫的程式碼可以順利透過全部 872 個測試用例。

Promise.resolve

Promsie.resolve(value) 可以將任何值轉成值為 value 狀態是 fulfilled 的 Promise,但如果傳入的值本身是 Promise 則會原樣返回它。

Promise.resolve = function(value) {
    // 如果是 Promsie,則直接輸出它
    if(value instanceof Promise){
        return value
    }
    return new Promise(resolve => resolve(value))
}

Promise.reject

和 Promise.resolve() 類似,Promise.reject() 會例項化一個 rejected 狀態的 Promise。但與 Promise.resolve() 不同的是,如果給 Promise.reject() 傳遞一個 Promise 物件,則這個物件會成為新 Promise 的值。

Promise.reject = function(reason) {
    return new Promise((resolve, reject) => reject(reason))
}

Promise.all

Promise.all 的規則是這樣的:

  • 傳入的所有 Promsie 都是 fulfilled,則返回由他們的值組成的,狀態為 fulfilled 的新 Promise;
  • 只要有一個 Promise 是 rejected,則返回 rejected 狀態的新 Promsie,且它的值是第一個 rejected 的 Promise 的值;
  • 只要有一個 Promise 是 pending,則返回一個 pending 狀態的新 Promise;
Promise.all = function(promiseArr) {
    let index = 0, result = []
    return new Promise((resolve, reject) => {
        promiseArr.forEach((p, i) => {
            Promise.resolve(p).then(val => {
                index++
                result[i] = val
                if (index === promiseArr.length) {
                    resolve(result)
                }
            }, err => {
                reject(err)
            })
        })
    })
}

Promise.race

Promise.race 會返回一個由所有可迭代例項中第一個 fulfilled 或 rejected 的例項包裝後的新例項。

Promise.race = function(promiseArr) {
    return new Promise((resolve, reject) => {
        promiseArr.forEach(p => {
            Promise.resolve(p).then(val => {
                resolve(val)
            }, err => {
                rejecte(err)
            })
        })
    })
}

Promise.allSettled

Promise.allSettled 的規則是這樣:

  • 所有 Promise 的狀態都變化了,那麼新返回一個狀態是 fulfilled 的 Promise,且它的值是一個陣列,陣列的每項由所有 Promise 的值和狀態組成的物件;
  • 如果有一個是 pending 的 Promise,則返回一個狀態是 pending 的新例項;
Promise.allSettled = function(promiseArr) {
    let result = []

    return new Promise((resolve, reject) => {
        promiseArr.forEach((p, i) => {
            Promise.resolve(p).then(val => {
                result.push({
                    status: 'fulfilled',
                    value: val
                })
                if (result.length === promiseArr.length) {
                    resolve(result) 
                }
            }, err => {
                result.push({
                    status: 'rejected',
                    reason: err
                })
                if (result.length === promiseArr.length) {
                    resolve(result) 
                }
            })
        })  
    })   
}

Promise.any

Promise.any 的規則是這樣:

  • 空陣列或者所有 Promise 都是 rejected,則返回狀態是 rejected 的新 Promsie,且值為 AggregateError 的錯誤;
  • 只要有一個是 fulfilled 狀態的,則返回第一個是 fulfilled 的新例項;
  • 其他情況都會返回一個 pending 的新例項;
Promise.any = function(promiseArr) {
    let index = 0
    return new Promise((resolve, reject) => {
        if (promiseArr.length === 0) return 
        promiseArr.forEach((p, i) => {
            Promise.resolve(p).then(val => {
                resolve(val)

            }, err => {
                index++
                if (index === promiseArr.length) {
                  reject(new AggregateError('All promises were rejected'))
                }
            })
        })
    })
}

localStorage sessionStorage cookies 有什麼區別?

localStorage:以鍵值對的方式儲存 儲存時間沒有限制 永久生效 除非自己刪除記錄
sessionStorage:當頁面關閉後被清理與其他相比不能同源視窗共享 是會話級別的儲存方式
cookies 資料不能超過4k 同時因為每次http請求都會攜帶cookie 所有cookie只適合儲存很小的資料 如會話標識

迴流與重繪的概念及觸發條件

(1)迴流

當渲染樹中部分或者全部元素的尺寸、結構或者屬性發生變化時,瀏覽器會重新渲染部分或者全部文件的過程就稱為迴流

下面這些操作會導致迴流:

  • 頁面的首次渲染
  • 瀏覽器的視窗大小發生變化
  • 元素的內容發生變化
  • 元素的尺寸或者位置發生變化
  • 元素的字型大小發生變化
  • 啟用CSS偽類
  • 查詢某些屬性或者呼叫某些方法
  • 新增或者刪除可見的DOM元素

在觸發迴流(重排)的時候,由於瀏覽器渲染頁面是基於流式佈局的,所以當觸發迴流時,會導致周圍的DOM元素重新排列,它的影響範圍有兩種:

  • 全域性範圍:從根節點開始,對整個渲染樹進行重新佈局
  • 區域性範圍:對渲染樹的某部分或者一個渲染物件進行重新佈局

(2)重繪

當頁面中某些元素的樣式發生變化,但是不會影響其在文件流中的位置時,瀏覽器就會對元素進行重新繪製,這個過程就是重繪

下面這些操作會導致迴流:

  • color、background 相關屬性:background-color、background-image 等
  • outline 相關屬性:outline-color、outline-width 、text-decoration
  • border-radius、visibility、box-shadow

注意: 當觸發迴流時,一定會觸發重繪,但是重繪不一定會引發迴流。

Number() 的儲存空間是多大?如果後臺傳送了一個超過最大自己的數字怎麼辦

Math.pow(2, 53) ,53 為有效數字,會發生截斷,等於 JS 能支援的最大數字。

說一下你對盒模型的理解?

CSS3中的盒模型有以下兩種:標準盒模型、IE盒模型
盒模型都是由四個部分組成的,分別是margin、border、padding和content
標準盒模型和IE盒模型的區別在於設定width和height時, 所對應的範圍不同
1、標準盒模型的width和height屬性的範圍只包含了content
2、IE盒模型的width和height屬性的範圍包含了border、padding和content
可以透過修改元素的box-sizing屬性來改變元素的盒模型;
1、box-sizing:content-box表示標準盒模型(預設值)
2、box-sizing:border-box表示IE盒模型(怪異盒模型)

相關文章