前端高級面試題@JS篇

目錄

JS

Es6

Node

性能優化

網絡 / 瀏覽器

算法

說說js中的詞法做用域css

js中只有詞法做用域,也就是說在定義時而不是執行時肯定做用域。例如:html

var value = 1;
 
function foo() {
    console.log(value);
}
 
function bar() {
    var value = 2;
    foo();
}
 
bar();<br>//1
複製代碼

注意: with和eval能夠修改詞法做用域node

什麼是閉包react

《深刻淺出nodejs》中對閉包的定義:web

在js中,實現外部做用域訪問內部做用域中變量的方法叫作「閉包」。算法

說說js的垃圾回收(GC)json

v8的垃圾回收策略主要基於分代式垃圾回收機制。將內存分爲新生代和老生代,分別採用不一樣的算法。設計模式

新生代採用Scavenge算法

Scavenge爲新生代採用的算法,是一種採用複製的方式實現的垃圾回收算法。它將內存分爲from和to兩個空間。每次gc,會將from空間的存活對象複製到to空間。而後兩個空間角色對換(又稱反轉)。
該算法是犧牲空間換時間,因此適合新生代,由於它的對象生存週期較短。api

老生代採用Mark-Sweep 和 Mark-Compact

老生代中對象存活時間較長,不適合Scavenge算法。
Mark-Sweep是標記清除的意思。Scavenge是隻複製存活對象,而Mark-Sweep是隻清除死亡對象。該算法分爲兩個步驟:數組

  1. 遍歷堆中全部對象並標記活着的對象
  2. 清除沒有標記的對象

Mark-Sweep存在一個問題,清除死亡對象後會形成內存空間不連續,若是這時候再分配一個大對象,全部的空間碎片都沒法完成這次分配,就會形成提早觸發gc。這時候v8會使用Mark-Compact算法。
Mark-Copact是標記整理的意思。它會在標記完成以後將活着的對象往一端移動,移動完成後直接清理掉邊界外的內存。由於存在整理過程,因此它的速度慢於Mark-Sweep,node中主要採用Mark-Sweep。

Incremental Marking

爲了不出現Javascript應用邏輯與垃圾回收器看到的狀況不一致,垃圾回收時應用邏輯會停下來。這種行爲被成爲全停頓(stop-the-world)。這對老生代影響較大。
Incremental Marking稱爲增量標記,也就是拆分爲許多小的「步進」,每次作完一「步進」,就讓Javascript執行一下子,垃圾回收與應用邏輯交替執行。
採用Incremental Marking後,gc的最大停頓時間較少到原來的 1 / 6 左右。

v8的內存限制

  • 64位系統最大約爲1.4G
  • 32位系統最大約爲0.7G

node中查看內存使用量

➜  ~ node
> process.memoryUsage()   //node進程內存使用
{ rss: 27054080,   // 進程常駐內存
  heapTotal: 7684096,  // 已申請到的堆內存
  heapUsed: 4850344,    // 當前使用的堆內存
  external: 9978  // 堆外內存(不是經過v8分配的內存)
  
> os.totalmem()  //系統總內存
17179869184

> os.freemem()  //系統閒置內存
3239858176

複製代碼

說說你瞭解的設計模式

發佈訂閱模式

在js中事件模型就至關於傳統的發佈訂閱模式,具體實現參考實現一個node中的EventEmiter

策略模式

定義: 定義一系列算法,把它們一個個封裝起來,而且使它們能夠相互替換。

策略模式實現表單校驗
const strategies = {
    isNoEmpty: function(value, errorMsg){
        if(value.trim() === ''){
            return errorMsg
        }
    },
    maxLength: function(value, errorMsg, len) {
        if(value.trim() > len) {
            return errorMsg
        }
    }
}

class Validator {
  constructor() {
    this.catch = [];
  }
  add(value, rule, errorMsg, ...others) {
    this.catch.push(function() {
      return strategies[rule].apply(this, [value, errorMsg, ...others]);
    });
  }
  start() {
    for (let i = 0, validatorFunc; (validatorFunc = this.catch[i++]); ) {
      let msg = validatorFunc();
      if (msg) {
        return msg;
      }
    }
  }
}

//使用
const validatorFunc = function() {
    const validator = new Validator();
    validator.add(username, 'isNoEmpty', '用戶名不能爲空');
    validator.add(password, 'isNoEmpty', '密碼不能爲空');
    const USERNAME_LEN = PASSWORD_LEN = 10;
    validator.add(username, 'maxLength', `用戶名不能超過${USERNAME_LEN}個字`, USERNAME_LEN);
    validator.add(password, 'isNoEmpty', `密碼不能爲空${PASSWORD_LEN}個字`, PASSWORD_LEN);
    let msg = validator.start();
    if(msg) {
        return msg;
    }
}

複製代碼

命令模式

應用場景: 有時候咱們要向某些對象發送請求,但不知道請求的接收者是誰,也不知道請求的操做是什麼,此時但願以一種鬆耦合的方式來設計軟件,使得請求的發送者和接收者可以消除彼此的耦合關係。

命令模式實現動畫
class MoveCommand {
  constructor(reciever, pos) {
    this.reciever = reciever;
    this.pos = pos;
    this.oldPos = null;
  }
  excute() {
    this.reciever.start("left", this.pos, 1000);
    this.reciever.getPos();
  }
  undo() {
    this.reciever.start("left", this.oldPos, 1000);
  }
}


複製代碼

ES6 模塊與 CommonJS 模塊的差別

  1. CommonJS輸出的是值的拷貝,ES6模塊輸出的是值的引用。
    也就是說CommonJS引用後改變模塊內變量的值,其餘引用模塊不會改變,而ES6模塊會改變。

  2. CommonJS是運行時加載,ES6模塊是編譯時輸出接口。
    之因此Webpack的Tree Shaking是基於ES6的,就是由於ES6在編譯的時候就能肯定依賴。由於使用babel-preset-2015這個預設默認是會把ES6模塊編譯爲CommonJS的,因此想使用Tree Shaking還須要手動修改這個預設。

module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [['babel-preset-es2015', {modules: false}]],
          }
        }
      }
    ]
}
複製代碼

async函數實現原理

async函數是基於generator實現,因此涉及到generator相關知識。在沒有async函數以前,一般使用co庫來執行generator,因此經過co咱們也能模擬async的實現。

function Asyncfn() {
  return co(function*() {
    //.....
  });
}
function co(gen) {
  return new Promise((resolve, reject) => {
    const fn = gen();
    function next(data) {
      let { value, done } = fn.next(data);
      if (done) return resolve(value);
      Promise.resolve(value).then(res => {
        next(res);
      }, reject);
    }
    next();
  });
}
複製代碼

說說瀏覽器和node中的事件循環(EventLoop)

瀏覽器

如圖:瀏覽器中相對簡單,共有兩個事件隊列,當主線程空閒時會清空Microtask queue(微任務隊列)依次執行Task Queue(宏任務隊列)中的回調函數,每執行完一個以後再清空Microtask queue。

「當前執行棧」 -> 「micro-task」 -> 「task queue中取一個回調」 -> 「micro-task」 -> ... (不斷消費task queue) -> 「micro-task」

nodejs

node中機制和瀏覽器有一些差別。node中的task queue是分爲幾個階段,清空micro-task是在一個階段結束以後(瀏覽器中是每個任務結束以後),各個階段以下:

   ┌───────────────────────┐
┌─>│        timers         │<————— 執行 setTimeout()、setInterval() 的回調
│  └──────────┬────────────┘
|             |<-- 執行全部 Next Tick Queue 以及 MicroTask Queue 的回調
│  ┌──────────┴────────────┐
│  │     pending callbacks │<————— 執行由上一個 Tick 延遲下來的 I/O 回調(待完善,可忽略)
│  └──────────┬────────────┘
|             |<-- 執行全部 Next Tick Queue 以及 MicroTask Queue 的回調
│  ┌──────────┴────────────┐
│  │     idle, prepare     │<————— 內部調用(可忽略)
│  └──────────┬────────────┘     
|             |<-- 執行全部 Next Tick Queue 以及 MicroTask Queue 的回調
|             |                   ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:- (執行幾乎全部的回調,除了 close callbacks 以及 timers 調度的回調和 setImmediate() 調度的回調,在恰當的時機將會阻塞在此階段)
│  │         poll          │<─────┤  connections, │ 
│  └──────────┬────────────┘      │   data, etc.  │ 
│             |                   |               | 
|             |                   └───────────────┘
|             |<-- 執行全部 Next Tick Queue 以及 MicroTask Queue 的回調
|  ┌──────────┴────────────┐      
│  │        check          │<————— setImmediate() 的回調將會在這個階段執行
│  └──────────┬────────────┘
|             |<-- 執行全部 Next Tick Queue 以及 MicroTask Queue 的回調
│  ┌──────────┴────────────┐
└──┤    close callbacks    │<————— socket.on('close', ...)
   └───────────────────────┘

這裏咱們主要關注其中的3個階段:timer、poll和check,其中poll隊列相對複雜:

輪詢 階段有兩個重要的功能:
一、計算應該阻塞和輪詢 I/O 的時間。
二、而後,處理 輪詢 隊列裏的事件。

當事件循環進入 輪詢 階段且 沒有計劃計時器時 ,將發生如下兩種狀況之一:
一、若是輪詢隊列不是空的,事件循環將循環訪問其回調隊列並同步執行它們,直到隊列已用盡,或者達到了與系統相關的硬限制。
二、若是輪詢隊列是空的,還有兩件事發生:
a、若是腳本已按 setImmediate() 排定,則事件循環將結束 輪詢 階段,並繼續 check階段以執行這些計劃腳本。
b、若是腳本 還沒有 按 setImmediate()排定,則事件循環將等待回調添加到隊列中,而後當即執行。

一旦輪詢隊列爲空,事件循環將檢查已達到時間閾值的計時器。若是一個或多個計時器已準備就緒,則事件循環將繞回計時器階段以執行這些計時器的回調。

細節請參考The Node.js Event Loop, Timers, and process.nextTick()
中文:Node.js 事件循環,定時器和 process.nextTick()

經過程序理解瀏覽器和node中的差別

setTimeout(() => {
  console.log("timer1");
  Promise.resolve().then(function() {
    console.log("promise1");
  });
}, 0);

setTimeout(() => {
  console.log("timer2");
  Promise.resolve().then(function() {
    console.log("promise2");
  });
}, 0);
複製代碼

在瀏覽器中的順序是:timer1 -> promise1 -> timer2 -> pormise2
node中順序是: timer1 -> timer2 -> promise1 -> promise2
這道題目很好的說明了node中的micro-task是在一個階段的任務執行完以後才清空的。

實現一個node中的EventEmiter

簡單實現:

class EventsEmiter {
  constructor() {
    this.events = {};
  }
  on(type, fn) {
    const events = this.events;
    if (!events[type]) {
      events[type] = [fn];
    } else {
      events[type].push(fn);
    }
  }
  emit(type, ...res) {
    const events = this.events;
    if (events[type]) {
      events[type].forEach(fn => fn.apply(this, res));
    }
  }
  remove(type, fn) {
    const events = this.events;
    if (events[type]) {
      events[type] = events[type].filer(lisener => lisener !== fn);
    }
  }
}

複製代碼

實現一個node中util模塊的promisify方法

let fs = require("fs");
let read = fs.readFile;

function promisify(fn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      fn(...args, (err, data) => {
        if (err) {
          reject(err);
        }
        resolve(data);
      });
    });
  };
}

// 回調用法
// read("./test.json", (err, data) => {
//   if (err) {
//     console.error("err", err);
//   }
//   console.log("data", data.toString());
// });

// promise用法
let readPromise = promisify(read);

readPromise("./test.json").then(res => {
  console.log("data", res.toString());
});

複製代碼

如何實現一個自定義流

根據所建立的流類型,新的流類必須實現一個或多個特定的方法,以下圖所示:

用例 需實現的方法
只讀流 Readable _read
只寫流 Writable _write, _writev, _final
可讀可寫流 Duplex _read, _write, _writev, _final
對寫入的數據進行操做,而後讀取結果 Transform _transform, _flush, _final

以雙工流爲例:

const { Duplex } = require('stream');

class Myduplex extends  Duplex {
  constructor(arr, opt) {
    super(opt);
    this.arr = arr
    this.index = 0
  }
  //實現可讀流部分
  _read(size) {
    this.index++
    if(this.index === 3) {
        this.push(null) 
    } else {
        this.push(this.index.toString())
    }
  }
  //實現可寫流
  _write(chunk, encoding, callback) {
    this.arr.push(chunk.toString())
    callback()
  }
}
複製代碼

更多內容能夠參考個人另外一篇文章:說說node中可讀流和可寫流nodejs官網

性能優化之dns-prefetch、prefetch、preload、defer、async

dns-prefetch

域名轉化爲ip是一個比較耗時的過程,dns-prefetch能讓瀏覽器空閒的時候幫你作這件事。尤爲大型網站會使用多域名,這時候更加須要dns預取。

//來自百度首頁
<link rel="dns-prefetch" href="//m.baidu.com">
複製代碼

prefetch

prefetch通常用來預加載可能使用的資源,通常是對用戶行爲的一種判斷,瀏覽器會在空閒的時候加載prefetch的資源。

<link rel="prefetch" href="http://www.example.com/">
複製代碼

preload

和prefetch不一樣,prefecth一般是加載接下來可能用到的頁面資源,而preload是加載當前頁面要用的腳本、樣式、字體、圖片等資源。因此preload不是空閒時加載,它的優先級更強,而且會佔用http請求數量。

<link rel='preload' href='style.css' as="style" onload="console.log('style loaded')"
複製代碼

as值包括

  • "script"
  • "style"
  • "image"
  • "media"
  • "document" onload方法是資源加載完成的回調函數

defer和async

//defer
<script defer src="script.js"></script>
//async
<script async src="script.js"></script>
複製代碼

defer和async都是異步(並行)加載資源,不一樣點是async是加載完當即執行,而defer是加載完不執行,等到全部元素解析完再執行,也就是DOMContentLoaded事件觸發以前。
由於async加載的資源是加載完執行,因此它比不能保證順序,而defer會按順序執行腳本。

說說react性能優化

shouldComponentUpdate

舉例:下面是antd-design-mobile的Modal組件中對的內部蒙層組件的處理

import * as React from "react";

export interface lazyRenderProps {
  style: {};
  visible?: boolean;
  className?: string;
}

export default class LazyRender extends React.Component<lazyRenderProps, any> {
  shouldComponentUpdate(nextProps: lazyRenderProps) {
    return !!nextProps.visible;
  }
  render() {
    const props: any = { ...this.props };
    delete props.visible;
    return <div {...props} />;
  }
}

複製代碼

immutable

像上面這種只比較了一個visible屬性,而且它是string類型,若是是一個object類型那麼就不能直接比較了,這時候使用immutable庫更好一些。
immutable優點:

  • 性能更好
  • 更加安全 immutable劣勢:
  • 庫比較大(壓縮後大約16k)
  • api和js不兼容

解決方案:seamless-immutable seamless-immutable這個庫沒有完整實現Persistent Data Structure,而是使用了Object.defineProperty擴展了JS的Object和Array對象,因此保持了相同的Api,同時庫的代碼量更少,壓縮後大約2k

基於key的優化

文檔中已經強調,key須要保證在當前的做用域中惟一,不要使用當前循環的index(尤爲在長列表中)。
參考 reactjs.org/docs/reconc…

說說瀏覽器渲染流程

瀏覽器的主進程:Browser進程

  1. 負責下載資源
  2. 建立銷燬renderer進程
  3. 負責將renderer進程生成的位圖渲染到頁面上
  4. 與用戶交互

瀏覽器內核:renderer進程

js引擎線程

由一個主線程和多個web worder線程組成,web worker線程不能操做dom

GUI線程

用於解析html生成DOM樹,解析css生成CSSOM,佈局layout、繪製paint。迴流和重繪依賴該線程

事件線程

當事件觸發時,該線程將事件的回調函數放入callback queue(任務隊列)中,等待js引擎線程處理

定時觸發線程

setTimeout和setInterval由該線程來記時,記時結束,將回調函數放入任務隊列

http請求線程

每有一個http請求就開一個該線程,每當檢測到狀態變動就會產生一個狀態變動事件,若是這個事件由對應的回掉函數,將這個函數放入任務隊列

任務隊列輪詢線程

用於輪詢監放任務隊列

流程

  1. 獲取html文件
  2. 從上到下解析html
  3. 並行請求資源(css資源不會阻塞html解析,可是會阻塞頁面渲染。js資源會組織html解析)
  4. 生成DOM tree 和 style rules
  5. 構建render tree
  6. 執行佈局過程(layout、也叫回流),肯定元素在屏幕上的具體座標
  7. 繪製到屏幕上(paint)

事件

DOMContentLoaded

當初始的HTML文檔被徹底加載和解析完成(script腳本執行完,所屬的script腳本以前的樣式表加載解析完成)以後,DOMContentLoaded事件被觸發

onload

全部資源加載完成觸發window的onload事件

參考流程圖:www.processon.com/view/5a6861…

說說http2.0

http2.0是對SPDY協議的一個升級版。和http1.0相比主要有如下特性:

  • 二進制分幀
  • 首部壓縮
  • 多路複用
  • 請求優先級
  • 服務端推送(server push)

詳細可參考: HTTP----HTTP2.0新特性

實現一個reduce方法

注意邊界條件:一、數組長度爲0,而且reduce沒有傳入初始參數時,拋出錯誤。二、reduce有返回值。

Array.prototype.myReduce = function(fn, initial) {
  if (this.length === 0 && !initial) {
    throw new Error("no initial and array is empty");
  }
  let start = 1;
  let pre = this[0];
  if (initial) {
    start = 0;
    pre = initial;
  }
  for (let i = start; i < this.length; i++) {
    let current = this[i];
    pre = fn.call(this, pre, current, i);
  }
  return pre;
};
複製代碼

實現一個promise.all方法,要求保留錯誤而且併發數爲3

標準的all方法是遇到錯誤會當即將promise置爲失敗態,並觸發error回調。保留錯誤的定義爲:promise遇到錯誤保存在返回的結果中。

function promiseall(promises) {
  return new Promise(resolve => {
    let result = [];
    let flag = 0;
    let taskQueue = promises.slice(0, 3); //任務隊列,初始爲最大併發數3
    let others = promises.slice(3); //排隊的任務

    taskQueue.forEach((promise, i) => {
      singleTaskRun(promise, i);
    });

    let i = 3; //新的任務從索引3開始
    function next() {
      if (others.length === 0) {
        return;
      }
      const newTask = others.shift();
      singleTaskRun(newTask, i++);
    }

    function singleTaskRun(promise, i) {
      promise
        .then(res => {
          check();
          result[i] = res;
          next();
        })
        .catch(err => {
          check();
          result[i] = err;
          next();
        });
    }
    function check() {
      flag++;
      if (flag === promises.length) {
        resolve(result);
      }
    }
  });
}
複製代碼

測試代碼:

let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("1");
  }, 1000);
});
let p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("2");
  }, 1500);
});
let p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("3");
  }, 2000);
});
let p4 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("4");
  }, 2500);
});
let p_e = new Promise((resolve, reject) => {
  // throw new Error("出錯");
  reject("錯誤");
});
let p5 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("5");
  }, 5000);
});

let all = promiseall([p_e, p1, p3, p2, p4, p5]);
all.then(
  data => {
    console.log("data", data);    // [ '錯誤', '1', '3', '2', '4', '5' ]
  }
);
複製代碼

不用遞歸函數求一個二叉樹的高度

先看一下遞歸的實現(二叉樹的深度優先遍歷):

function getBinaryTreeHeigth(node) {
  let maxDeep = 0;
  function next(n, deep) {
    deep++;
    if (n.l) {
      let newDeep = next(n.l, deep);
      if (newDeep > maxDeep) {
        maxDeep = newDeep;
      }
    }
    if (n.r) {
      let newDeep = next(n.r, deep);
      if (newDeep > maxDeep) {
        maxDeep = newDeep;
      }
    }
    return deep;
  }
  next(node, 0);
  return maxDeep;
}

function Node(v, l, r) {
  this.v = v;
  this.l = l;
  this.r = r;
}
複製代碼

非遞歸的實現(二叉樹的廣度優先遍歷):

function getBinaryTreeHeigth(node) {
  if (!node) {
    return 0;
  }
  const queue = [node];
  let deep = 0;
  while (queue.length) {
    deep++;
    for (let i = 0; i < queue.length; i++) {
      const cur = queue.pop();
      if (cur.l) {
        queue.unshift(cur.l);
      }
      if (cur.r) {
        queue.unshift(cur.r);
      }
    }
  }
  return deep;
}

function Node(v, l, r) {
  this.v = v;
  this.l = l;
  this.r = r;
}
複製代碼

js中求兩個大數相加

給定兩個以字符串形式表示的非負整數 num1 和 num2,返回它們的和,仍用字符串表示。

輸入:num1 = '1234', num2 = '987'
輸出:'2221'

function bigIntAdd(str1, str2) {
  let result = [];
  let ary1 = str1.split("");
  let ary2 = str2.split("");
  let flag = false; //是否進位
  while (ary1.length || ary2.length) {
    let result_c = sigle_pos_add(ary1.pop(), ary2.pop());
    if (flag) {
      result_c = result_c + 1;
    }
    result.unshift(result_c % 10);

    if (result_c >= 10) {
      flag = true;
    } else {
      flag = false;
    }
  }
  if(flag) {
    result.unshift('1');
  }
  return result.join("");
}

function sigle_pos_add(str1_c, str2_c) {
  let l = (r = 0);
  if (str1_c) {
    l = Number(str1_c);
  }
  if (str2_c) {
    r = Number(str2_c);
  }
  return l + r;
}
複製代碼

測試代碼:

const str1 = "1234";
const str2 = "987654321";
const str3 = "4566786445677555";
const str4 = "987";

console.log(bigIntAdd(str1, str4))  //'2221'
console.log(bigIntAdd(str2, str3))  //'4566787433331876'
複製代碼

實現一個數組隨機打亂算法

function disOrder(ary) {
  for (let i = 0; i < ary.length; i++) {
    let randomIndex = Math.floor(Math.random() * ary.length);
    swap(ary, i, randomIndex);
  }
}

function swap(ary, a, b) {
  let temp = ary[a];
  ary[a] = ary[b];
  ary[b] = temp;
}

let ary = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
disOrder(ary);

console.log(ary);

複製代碼

給數字增長「逗號」分隔

輸入: '"123456789.012"' 輸出:123,456,789.012

正則解法:

function parseNumber(num) {
  if (!num) return "";
  return num.replace(/(\d)(?=(\d{3})+(\.|$))/g, "$1,");
}

複製代碼

非正則:

function formatNumber(num) {
  if (!num) return "";
  let [int, float] = num.split(".");
  let intArr = int.split("");
  let result = [];
  let i = 0;
  while (intArr.length) {
    if (i !== 0 && i % 3 === 0) {
      result.unshift(intArr.pop() + ",");
    } else {
      result.unshift(intArr.pop());
    }
    i++;
  }

  return result.join("") + "." + (float ? float : "");
}
複製代碼
相關文章
相關標籤/搜索