2萬字 | 前端基礎拾遺90問

你們好,我是練習時長一年半的前端練習生,喜歡唱、跳、rap、敲代碼。本文是筆者一年多來對前端基礎知識的總結和思考,這些題目對本身是總結,對你們也是一點微薄的資料,但願能給你們帶來一些幫助和啓發。成文過程當中獲得了許多大佬的幫助,在此感謝愷哥的小冊、神三元同窗的前端每日一問以及許多素未謀面的朋友們,讓我等萌新也有機會在前人的財富中拾人牙慧,班門弄斧Thanks♪(・ω・)ノcss

本文將從如下十一個維度爲讀者總結前端基礎知識html

JS基礎

1. 如何在ES5環境下實現let

這個問題實質上是在回答letvar有什麼區別,對於這個問題,咱們能夠直接查看babel轉換先後的結果,看一下在循環中經過let定義的變量是如何解決變量提高的問題前端

babel在let定義的變量前加了道下劃線,避免在塊級做用域外訪問到該變量,除了對變量名的轉換,咱們也能夠經過自執行函數來模擬塊級做用域

(function(){
  for(var i = 0; i < 5; i ++){
    console.log(i)  // 0 1 2 3 4
  }
})();

console.log(i)      // Uncaught ReferenceError: i is not defined
複製代碼

不過這個問題並無結束,咱們回到varlet/const的區別上:react

  • var聲明的變量會掛到window上,而letconst不會
  • var聲明的變量存在變量提高,而letconst不會
  • letconst聲明造成塊做用域,只能在塊做用域裏訪問,不能跨塊訪問,也不能跨函數訪問
  • 同一做用域下letconst不能聲明同名變量,而var能夠
  • 暫時性死區,letconst聲明的變量不能在聲明前被使用

babel的轉化,其實只實現了第二、三、5點git


2. 如何在ES5環境下實現const

實現const的關鍵在於Object.defineProperty()這個API,這個API用於在一個對象上增長或修改屬性。經過配置屬性描述符,能夠精確地控制屬性行爲。Object.defineProperty() 接收三個參數:github

Object.defineProperty(obj, prop, desc)web

參數 說明
obj 要在其上定義屬性的對象
prop 要定義或修改的屬性的名稱
descriptor 將被定義或修改的屬性描述符

屬性描述符 說明 默認值
value 該屬性對應的值。能夠是任何有效的 JavaScript 值(數值,對象,函數等)。默認爲 undefined undefined
get 一個給屬性提供 getter 的方法,若是沒有 getter 則爲 undefined undefined
set 一個給屬性提供 setter 的方法,若是沒有 setter 則爲 undefined。當屬性值修改時,觸發執行該方法 undefined
writable 當且僅當該屬性的writable爲true時,value才能被賦值運算符改變。默認爲 false false
enumerable enumerable定義了對象的屬性是否能夠在 for...in 循環和 Object.keys() 中被枚舉 false
Configurable configurable特性表示對象的屬性是否能夠被刪除,以及除value和writable特性外的其餘特性是否能夠被修改 false

對於const不可修改的特性,咱們經過設置writable屬性來實現算法

function _const(key, value) {    
    const desc = {        
        value,        
        writable: false    
    }    
    Object.defineProperty(window, key, desc)
}
    
_const('obj', {a: 1})   //定義obj
obj.b = 2               //能夠正常給obj的屬性賦值
obj = {}                //沒法賦值新對象
複製代碼

參考資料:如何在 ES5 環境下實現一個const ?數據庫

3. 手寫call()

call() 方法使用一個指定的 this 值和單獨給出的一個或多個參數來調用一個函數
語法:function.call(thisArg, arg1, arg2, ...)編程

call()的原理比較簡單,因爲函數的this指向它的直接調用者,咱們變動調用者即完成this指向的變動:

//變動函數調用者示例
function foo() {
    console.log(this.name)
}

// 測試
const obj = {
    name: '寫代碼像蔡徐抻'
}
obj.foo = foo   // 變動foo的調用者
obj.foo()       // '寫代碼像蔡徐抻'
複製代碼

基於以上原理, 咱們兩句代碼就能實現call()

Function.prototype.myCall = function(thisArg, ...args) {
    thisArg.fn = this              // this指向調用call的對象,即咱們要改變this指向的函數
    return thisArg.fn(...args)     // 執行函數並return其執行結果
}
複製代碼

可是咱們有一些細節須要處理:

Function.prototype.myCall = function(thisArg, ...args) {
    const fn = Symbol('fn')        // 聲明一個獨有的Symbol屬性, 防止fn覆蓋已有屬性
    thisArg = thisArg || window    // 若沒有傳入this, 默認綁定window對象
    thisArg[fn] = this              // this指向調用call的對象,即咱們要改變this指向的函數
    const result = thisArg[fn](...args)  // 執行當前函數
    delete thisArg[fn]              // 刪除咱們聲明的fn屬性
    return result                  // 返回函數執行結果
}

//測試
foo.myCall(obj)     // 輸出'寫代碼像蔡徐抻'
複製代碼

4. 手寫apply()

apply() 方法調用一個具備給定this值的函數,以及做爲一個數組(或相似數組對象)提供的參數。
語法:func.apply(thisArg, [argsArray])

apply()call()相似,區別在於call()接收參數列表,而apply()接收一個參數數組,因此咱們在call()的實現上簡單改一下入參形式便可

Function.prototype.myApply = function(thisArg, args) {
    const fn = Symbol('fn')        // 聲明一個獨有的Symbol屬性, 防止fn覆蓋已有屬性
    thisArg = thisArg || window    // 若沒有傳入this, 默認綁定window對象
    thisArg[fn] = this              // this指向調用call的對象,即咱們要改變this指向的函數
    const result = thisArg[fn](...args)  // 執行當前函數(此處說明一下:雖然apply()接收的是一個數組,但在調用原函數時,依然要展開參數數組。能夠對照原生apply(),原函數接收到展開的參數數組)
    delete thisArg[fn]              // 刪除咱們聲明的fn屬性
    return result                  // 返回函數執行結果
}

//測試
foo.myApply(obj, [])     // 輸出'寫代碼像蔡徐抻'
複製代碼

5. 手寫bind()

bind() 方法建立一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定爲 bind() 的第一個參數,而其他參數將做爲新函數的參數,供調用時使用。
語法: function.bind(thisArg, arg1, arg2, ...)

從用法上看,彷佛給call/apply包一層function就實現了bind():

Function.prototype.myBind = function(thisArg, ...args) {
    return () => {
        this.apply(thisArg, args)
    }
}
複製代碼

但咱們忽略了三點:

  1. bind()除了this還接收其餘參數,bind()返回的函數也接收參數,這兩部分的參數都要傳給返回的函數
  2. new會改變this指向:若是bind綁定後的函數被new了,那麼this指向會發生改變,指向當前函數的實例
  3. 沒有保留原函數在原型鏈上的屬性和方法
Function.prototype.myBind = function (thisArg, ...args) {
    var self = this
    // new優先級
    var fbound = function () {
        self.apply(this instanceof self ? this : thisArg, args.concat(Array.prototype.slice.call(arguments)))
    }
    // 繼承原型上的屬性和方法
    fbound.prototype = Object.create(self.prototype);

    return fbound;
}

//測試
const obj = { name: '寫代碼像蔡徐抻' }
function foo() {
    console.log(this.name)
    console.log(arguments)
}

foo.myBind(obj, 'a', 'b', 'c')()    //輸出寫代碼像蔡徐抻 ['a', 'b', 'c']
複製代碼

6. 手寫一個防抖函數

防抖和節流的概念都比較簡單,因此咱們就不在「防抖節流是什麼」這個問題上浪費過多篇幅了,簡單點一下:

防抖,即短期內大量觸發同一事件,只會執行一次函數,實現原理爲設置一個定時器,約定在xx毫秒後再觸發事件處理,每次觸發事件都會從新設置計時器,直到xx毫秒內無第二次操做,防抖經常使用於搜索框/滾動條的監聽事件處理,若是不作防抖,每輸入一個字/滾動屏幕,都會觸發事件處理,形成性能浪費。

function debounce(func, wait) {
    let timeout = null
    return function() {
        let context = this
        let args = arguments
        if (timeout) clearTimeout(timeout)
        timeout = setTimeout(() => {
            func.apply(context, args)
        }, wait)
    }
}
複製代碼

7. 手寫一個節流函數

防抖是延遲執行,而節流是間隔執行,函數節流即每隔一段時間就執行一次,實現原理爲設置一個定時器,約定xx毫秒後執行事件,若是時間到了,那麼執行函數並重置定時器,和防抖的區別在於,防抖每次觸發事件都重置定時器,而節流在定時器到時間後再清空定時器

function throttle(func, wait) {
    let timeout = null
    return function() {
        let context = this
        let args = arguments
        if (!timeout) {
            timeout = setTimeout(() => {
                timeout = null
                func.apply(context, args)
            }, wait)
        }

    }
}
複製代碼

實現方式2:使用兩個時間戳prev舊時間戳now新時間戳,每次觸發事件都判斷兩者的時間差,若是到達規定時間,執行函數並重置舊時間戳

function throttle(func, wait) {
    var prev = 0;
    return function() {
        let now = Date.now();
        let context = this;
        let args = arguments;
        if (now - prev > wait) {
            func.apply(context, args);
            prev = now;
        }
    }
}
複製代碼

8. 數組扁平化

對於[1, [1,2], [1,2,3]]這樣多層嵌套的數組,咱們如何將其扁平化爲[1, 1, 2, 1, 2, 3]這樣的一維數組呢:

1.ES6的flat()

const arr = [1, [1,2], [1,2,3]]
arr.flat(Infinity)  // [1, 1, 2, 1, 2, 3]
複製代碼

2.序列化後正則

const arr = [1, [1,2], [1,2,3]]
const str = `[${JSON.stringify(arr).replace(/(\[|\])/g, '')}]`
JSON.parse(str)   // [1, 1, 2, 1, 2, 3]
複製代碼

3.遞歸
對於樹狀結構的數據,最直接的處理方式就是遞歸

const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
  let result = []
  for (const item of arr) {
    item instanceof Array ? result = result.concat(flat(item)) : result.push(item)
  }
  return result
}

flat(arr) // [1, 1, 2, 1, 2, 3]
複製代碼

4.reduce()遞歸

const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
  return arr.reduce((prev, cur) => {
    return prev.concat(cur instanceof Array ? flat(cur) : cur)
  }, [])
}

flat(arr)  // [1, 1, 2, 1, 2, 3]
複製代碼

5.迭代+展開運算符

// 每次while都會合並一層的元素,這裏第一次合併結果爲[1, 1, 2, 1, 2, 3, [4,4,4]]
// 而後arr.some斷定數組中是否存在數組,由於存在[4,4,4],繼續進入第二次循環進行合併
let arr = [1, [1,2], [1,2,3,[4,4,4]]]
while (arr.some(Array.isArray)) {
  arr = [].concat(...arr);
}

console.log(arr)  // [1, 1, 2, 1, 2, 3, 4, 4, 4]
複製代碼

9. 手寫一個Promise

實現一個符合規範的Promise篇幅比較長,建議閱讀筆者上一篇文章:異步編程二三事 | Promise/async/Generator實現原理解析 | 9k字


JS面向對象

在JS中一切皆對象,但JS並非一種真正的面向對象(OOP)的語言,由於它缺乏類(class)的概念。雖然ES6引入了classextends,使咱們可以輕易地實現類和繼承。但JS並不存在真實的類,JS的類是經過函數以及原型鏈機制模擬的,本小節的就來探究如何在ES5環境下利用函數和原型鏈實現JS面向對象的特性

在開始以前,咱們先回顧一下原型鏈的知識,後續new繼承等實現都是基於原型鏈機制。不少介紹原型鏈的資料都能寫上洋洋灑灑幾千字,但我以爲讀者們不須要把原型鏈想太複雜,容易把本身繞進去,其實在我看來,原型鏈的核心只須要記住三點:

  1. 每一個對象都有__proto__屬性,該屬性指向其原型對象,在調用實例的方法和屬性時,若是在實例對象上找不到,就會往原型對象上找
  2. 構造函數的prototype屬性也指向實例的原型對象
  3. 原型對象的constructor屬性指向構造函數

1. 模擬實現new

首先咱們要知道new作了什麼

  1. 建立一個新對象,並繼承其構造函數的prototype,這一步是爲了繼承構造函數原型上的屬性和方法
  2. 執行構造函數,方法內的this被指定爲該新實例,這一步是爲了執行構造函數內的賦值操做
  3. 返回新實例(規範規定,若是構造方法返回了一個對象,那麼返回該對象,不然返回第一步建立的新對象)
// new是關鍵字,這裏咱們用函數來模擬,new Foo(args) <=> myNew(Foo, args)
function myNew(foo, ...args) {
  // 建立新對象,並繼承構造方法的prototype屬性, 這一步是爲了把obj掛原型鏈上, 至關於obj.__proto__ = Foo.prototype
  let obj = Object.create(foo.prototype)  
  
  // 執行構造方法, 併爲其綁定新this, 這一步是爲了讓構造方法能進行this.name = name之類的操做, args是構造方法的入參, 由於這裏用myNew模擬, 因此入參從myNew傳入
  let result = foo.apply(obj, args)

  // 若是構造方法已經return了一個對象,那麼就返回該對象,不然返回myNew建立的新對象(通常狀況下,構造方法不會返回新實例,但使用者能夠選擇返回新實例來覆蓋new建立的對象)
  return Object.prototype.toString.call(result) === '[object Object]' ? result : obj
}

// 測試:
function Foo(name) {
  this.name = name
}
const newObj = myNew(Foo, 'zhangsan')
console.log(newObj)                 // Foo {name: "zhangsan"}
console.log(newObj instanceof Foo)  // true
複製代碼

2. ES5如何實現繼承

說到繼承,最容易想到的是ES6的extends,固然若是隻回答這個確定不合格,咱們要從函數和原型鏈的角度上實現繼承,下面咱們一步步地、遞進地實現一個合格的繼承

一. 原型鏈繼承

原型鏈繼承的原理很簡單,直接讓子類的原型對象指向父類實例,當子類實例找不到對應的屬性和方法時,就會往它的原型對象,也就是父類實例上找,從而實現對父類的屬性和方法的繼承

// 父類
function Parent() {
    this.name = '寫代碼像蔡徐抻'
}
// 父類的原型方法
Parent.prototype.getName = function() {
    return this.name
}
// 子類
function Child() {}

// 讓子類的原型對象指向父類實例, 這樣一來在Child實例中找不到的屬性和方法就會到原型對象(父類實例)上尋找
Child.prototype = new Parent()
Child.prototype.constructor = Child // 根據原型鏈的規則,順便綁定一下constructor, 這一步不影響繼承, 只是在用到constructor時會須要

// 而後Child實例就能訪問到父類及其原型上的name屬性和getName()方法
const child = new Child()
child.name          // '寫代碼像蔡徐抻'
child.getName()     // '寫代碼像蔡徐抻'
複製代碼

原型繼承的缺點:

  1. 因爲全部Child實例原型都指向同一個Parent實例, 所以對某個Child實例的父類引用類型變量修改會影響全部的Child實例
  2. 在建立子類實例時沒法向父類構造傳參, 即沒有實現super()的功能
// 示例:
function Parent() {
    this.name = ['寫代碼像蔡徐抻'] 
}
Parent.prototype.getName = function() {
    return this.name
}
function Child() {}

Child.prototype = new Parent()
Child.prototype.constructor = Child 

// 測試
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name)          // ['foo']
console.log(child2.name)          // ['foo'] (預期是['寫代碼像蔡徐抻'], 對child1.name的修改引發了全部child實例的變化)
複製代碼

二. 構造函數繼承

構造函數繼承,即在子類的構造函數中執行父類的構造函數,併爲其綁定子類的this,讓父類的構造函數把成員屬性和方法都掛到子類的this上去,這樣既能避免實例之間共享一個原型實例,又能向父類構造方法傳參

function Parent(name) {
    this.name = [name]
}
Parent.prototype.getName = function() {
    return this.name
}
function Child() {
    Parent.call(this, 'zhangsan')   // 執行父類構造方法並綁定子類的this, 使得父類中的屬性可以賦到子類的this上
}

//測試
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name)          // ['foo']
console.log(child2.name)          // ['zhangsan']
child2.getName()                  // 報錯,找不到getName(), 構造函數繼承的方式繼承不到父類原型上的屬性和方法
複製代碼

構造函數繼承的缺點:

  1. 繼承不到父類原型上的屬性和方法

三. 組合式繼承

既然原型鏈繼承和構造函數繼承各有互補的優缺點, 那麼咱們爲何不組合起來使用呢, 因此就有了綜合兩者的組合式繼承

function Parent(name) {
    this.name = [name]
}
Parent.prototype.getName = function() {
    return this.name
}
function Child() {
    // 構造函數繼承
    Parent.call(this, 'zhangsan') 
}
//原型鏈繼承
Child.prototype = new Parent()
Child.prototype.constructor = Child

//測試
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name)          // ['foo']
console.log(child2.name)          // ['zhangsan']
child2.getName()                  // ['zhangsan']
複製代碼

組合式繼承的缺點:

  1. 每次建立子類實例都執行了兩次構造函數(Parent.call()new Parent()),雖然這並不影響對父類的繼承,但子類建立實例時,原型中會存在兩份相同的屬性和方法,這並不優雅

四. 寄生式組合繼承

爲了解決構造函數被執行兩次的問題, 咱們將指向父類實例改成指向父類原型, 減去一次構造函數的執行

function Parent(name) {
    this.name = [name]
}
Parent.prototype.getName = function() {
    return this.name
}
function Child() {
    // 構造函數繼承
    Parent.call(this, 'zhangsan') 
}
//原型鏈繼承
// Child.prototype = new Parent()
Child.prototype = Parent.prototype  //將`指向父類實例`改成`指向父類原型`
Child.prototype.constructor = Child

//測試
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name)          // ['foo']
console.log(child2.name)          // ['zhangsan']
child2.getName()                  // ['zhangsan']
複製代碼

但這種方式存在一個問題,因爲子類原型和父類原型指向同一個對象,咱們對子類原型的操做會影響到父類原型,例如給Child.prototype增長一個getName()方法,那麼會致使Parent.prototype也增長或被覆蓋一個getName()方法,爲了解決這個問題,咱們給Parent.prototype作一個淺拷貝

function Parent(name) {
    this.name = [name]
}
Parent.prototype.getName = function() {
    return this.name
}
function Child() {
    // 構造函數繼承
    Parent.call(this, 'zhangsan') 
}
//原型鏈繼承
// Child.prototype = new Parent()
Child.prototype = Object.create(Parent.prototype)  //將`指向父類實例`改成`指向父類原型`
Child.prototype.constructor = Child

//測試
const child = new Child()
const parent = new Parent()
child.getName()                  // ['zhangsan']
parent.getName()                 // 報錯, 找不到getName()
複製代碼

到這裏咱們就完成了ES5環境下的繼承的實現,這種繼承方式稱爲寄生組合式繼承,是目前最成熟的繼承方式,babel對ES6繼承的轉化也是使用了寄生組合式繼承

咱們回顧一下實現過程:
一開始最容易想到的是原型鏈繼承,經過把子類實例的原型指向父類實例來繼承父類的屬性和方法,但原型鏈繼承的缺陷在於對子類實例繼承的引用類型的修改會影響到全部的實例對象以及沒法向父類的構造方法傳參
所以咱們引入了構造函數繼承, 經過在子類構造函數中調用父類構造函數並傳入子類this來獲取父類的屬性和方法,但構造函數繼承也存在缺陷,構造函數繼承不能繼承到父類原型鏈上的屬性和方法
因此咱們綜合了兩種繼承的優勢,提出了組合式繼承,但組合式繼承也引入了新的問題,它每次建立子類實例都執行了兩次父類構造方法,咱們經過將子類原型指向父類實例改成子類原型指向父類原型的淺拷貝來解決這一問題,也就是最終實現 —— 寄生組合式繼承


V8引擎機制

1. V8如何執行一段JS代碼

  1. 預解析:檢查語法錯誤但不生成AST
  2. 生成AST:通過詞法/語法分析,生成抽象語法樹
  3. 生成字節碼:基線編譯器(Ignition)將AST轉換成字節碼
  4. 生成機器碼:優化編譯器(Turbofan)將字節碼轉換成優化過的機器碼,此外在逐行執行字節碼的過程當中,若是一段代碼常常被執行,那麼V8會將這段代碼直接轉換成機器碼保存起來,下一次執行就沒必要通過字節碼,優化了執行速度

上面幾點只是V8執行機制的極簡總結,建議閱讀參考資料:

1.V8 是怎麼跑起來的 —— V8 的 JavaScript 執行管道
2.JavaScript 引擎 V8 執行流程概述

2. 介紹一下引用計數和標記清除

  • 引用計數:給一個變量賦值引用類型,則該對象的引用次數+1,若是這個變量變成了其餘值,那麼該對象的引用次數-1,垃圾回收器會回收引用次數爲0的對象。可是當對象循環引用時,會致使引用次數永遠沒法歸零,形成內存沒法釋放。
  • 標記清除:垃圾收集器先給內存中全部對象加上標記,而後從根節點開始遍歷,去掉被引用的對象和運行環境中對象的標記,剩下的被標記的對象就是沒法訪問的等待回收的對象。

3. V8如何進行垃圾回收

JS引擎中對變量的存儲主要有兩種位置,棧內存和堆內存,棧內存存儲基本類型數據以及引用類型數據的內存地址,堆內存儲存引用類型的數據

棧內存的回收:

棧內存調用棧上下文切換後就被回收,比較簡單

堆內存的回收:

V8的堆內存分爲新生代內存和老生代內存,新生代內存是臨時分配的內存,存在時間短,老生代內存存在時間長

  • 新生代內存回收機制:
    • 新生代內存容量小,64位系統下僅有32M。新生代內存分爲From、To兩部分,進行垃圾回收時,先掃描From,將非存活對象回收,將存活對象順序複製到To中,以後調換From/To,等待下一次回收
  • 老生代內存回收機制
    • 晉升:若是新生代的變量通過屢次回收依然存在,那麼就會被放入老生代內存中
    • 標記清除:老生代內存會先遍歷全部對象並打上標記,而後對正在使用或被強引用的對象取消標記,回收被標記的對象
    • 整理內存碎片:把對象挪到內存的一端

參考資料:聊聊V8引擎的垃圾回收

4. JS相較於C++等語言爲何慢,V8作了哪些優化

  1. JS的問題:
    • 動態類型:致使每次存取屬性/尋求方法時候,都須要先檢查類型;此外動態類型也很難在編譯階段進行優化
    • 屬性存取:C++/Java等語言中方法、屬性是存儲在數組中的,僅需數組位移就能夠獲取,而JS存儲在對象中,每次獲取都要進行哈希查詢
  2. V8的優化:
    • 優化JIT(即時編譯):相較於C++/Java這類編譯型語言,JS一邊解釋一邊執行,效率低。V8對這個過程進行了優化:若是一段代碼被執行屢次,那麼V8會把這段代碼轉化爲機器碼緩存下來,下次運行時直接使用機器碼。
    • 隱藏類:對於C++這類語言來講,僅需幾個指令就能經過偏移量獲取變量信息,而JS須要進行字符串匹配,效率低,V8借用了類和偏移位置的思想,將對象劃分紅不一樣的組,即隱藏類
    • 內嵌緩存:即緩存對象查詢的結果。常規查詢過程是:獲取隱藏類地址 -> 根據屬性名查找偏移值 -> 計算該屬性地址,內嵌緩存就是對這一過程結果的緩存
    • 垃圾回收管理:上文已介紹

參考資料:爲何V8引擎這麼快?


瀏覽器渲染機制

1. 瀏覽器的渲染過程是怎樣的

大致流程以下:

  1. HTML和CSS通過各自解析,生成DOM樹和CSSOM樹
  2. 合併成爲渲染樹
  3. 根據渲染樹進行佈局
  4. 最後調用GPU進行繪製,顯示在屏幕上

2. 如何根據瀏覽器渲染機制加快首屏速度

  1. 優化文件大小:HTML和CSS的加載和解析都會阻塞渲染樹的生成,從而影響首屏展現速度,所以咱們能夠經過優化文件大小、減小CSS文件層級的方法來加快首屏速度
  2. 避免資源下載阻塞文檔解析:瀏覽器解析到<script>標籤時,會阻塞文檔解析,直到腳本執行完成,所以咱們一般把<script>標籤放在底部,或者加上defer、async來進行異步下載

3. 什麼是迴流(重排),什麼狀況下會觸發迴流

  • 當元素的尺寸或者位置發生了變化,就須要從新計算渲染樹,這就是迴流
  • DOM元素的幾何屬性(width/height/padding/margin/border)發生變化時會觸發迴流
  • DOM元素移動或增長會觸發迴流
  • 讀寫offset/scroll/client等屬性時會觸發迴流
  • 調用window.getComputedStyle會觸發迴流

4. 什麼是重繪,什麼狀況下會觸發重繪

  • DOM樣式發生了變化,但沒有影響DOM的幾何屬性時,會觸發重繪,而不會觸發迴流。重繪因爲DOM位置信息不須要更新,省去了佈局過程,於是性能上優於迴流

5. 什麼是GPU加速,如何使用GPU加速,GPU加速的缺點

  • 優勢:使用transform、opacity、filters等屬性時,會直接在GPU中完成處理,這些屬性的變化不會引發迴流重繪
  • 缺點:GPU渲染字體會致使字體模糊,過多的GPU處理會致使內存問題

6. 如何減小回流

  • 使用class替代style,減小style的使用
  • 使用resize、scroll時進行防抖和節流處理,這二者會直接致使迴流
  • 使用visibility替換display: none,由於前者只會引發重繪,後者會引起迴流
  • 批量修改元素時,能夠先讓元素脫離文檔流,等修改完畢後,再放入文檔流
  • 避免觸發同步佈局事件,咱們在獲取offsetWidth這類屬性的值時,能夠使用變量將查詢結果存起來,避免屢次查詢,每次對offset/scroll/client等屬性進行查詢時都會觸發迴流
  • 對於複雜動畫效果,使用絕對定位讓其脫離文檔流,複雜的動畫效果會頻繁地觸發迴流重繪,咱們能夠將動畫元素設置絕對定位從而脫離文檔流避免反覆迴流重繪。

參考資料:必須明白的瀏覽器渲染機制


瀏覽器緩存策略

1. 介紹一下瀏覽器緩存位置和優先級

  1. Service Worker
  2. Memory Cache(內存緩存)
  3. Disk Cache(硬盤緩存)
  4. Push Cache(推送緩存)
  5. 以上緩存都沒命中就會進行網絡請求

2. 說說不一樣緩存間的差異

  1. Service Worker

和Web Worker相似,是獨立的線程,咱們能夠在這個線程中緩存文件,在主線程須要的時候讀取這裏的文件,Service Worker使咱們能夠自由選擇緩存哪些文件以及文件的匹配、讀取規則,而且緩存是持續性的

  1. Memory Cache

即內存緩存,內存緩存不是持續性的,緩存會隨着進程釋放而釋放

  1. Disk Cache

即硬盤緩存,相較於內存緩存,硬盤緩存的持續性和容量更優,它會根據HTTP header的字段判斷哪些資源須要緩存

  1. Push Cache

即推送緩存,是HTTP/2的內容,目前應用較少

3. 介紹一下瀏覽器緩存策略

強緩存(不要向服務器詢問的緩存)

設置Expires

  • 即過時時間,例如「Expires: Thu, 26 Dec 2019 10:30:42 GMT」表示緩存會在這個時間後失效,這個過時日期是絕對日期,若是修改了本地日期,或者本地日期與服務器日期不一致,那麼將致使緩存過時時間錯誤。

設置Cache-Control

  • HTTP/1.1新增字段,Cache-Control能夠經過max-age字段來設置過時時間,例如「Cache-Control:max-age=3600」除此以外Cache-Control還能設置private/no-cache等多種字段

協商緩存(須要向服務器詢問緩存是否已通過期)

Last-Modified

  • 即最後修改時間,瀏覽器第一次請求資源時,服務器會在響應頭上加上Last-Modified ,當瀏覽器再次請求該資源時,瀏覽器會在請求頭中帶上If-Modified-Since 字段,字段的值就是以前服務器返回的最後修改時間,服務器對比這兩個時間,若相同則返回304,不然返回新資源,並更新Last-Modified

ETag

  • HTTP/1.1新增字段,表示文件惟一標識,只要文件內容改動,ETag就會從新計算。緩存流程和 Last-Modified 同樣:服務器發送 ETag 字段 -> 瀏覽器再次請求時發送 If-None-Match -> 若是ETag值不匹配,說明文件已經改變,返回新資源並更新ETag,若匹配則返回304

二者對比

  • ETag 比 Last-Modified 更準確:若是咱們打開文件但並無修改,Last-Modified 也會改變,而且 Last-Modified 的單位時間爲一秒,若是一秒內修改完了文件,那麼仍是會命中緩存
  • 若是什麼緩存策略都沒有設置,那麼瀏覽器會取響應頭中的 Date 減去 Last-Modified 值的 10% 做爲緩存時間

參考資料:瀏覽器緩存機制剖析


網絡相關

1. 講講網絡OSI七層模型,TCP/IP和HTTP分別位於哪一層

alt

模型 概述 單位
物理層 網絡鏈接介質,如網線、光纜,數據在其中以比特爲單位傳輸 bit
數據鏈路層 數據鏈路層將比特封裝成數據幀並傳遞
網絡層 定義IP地址,定義路由功能,創建主機到主機的通訊 數據包
傳輸層 負責將數據進行可靠或者不可靠傳遞,創建端口到端口的通訊 數據段
會話層 控制應用程序之間會話能力,區分不一樣的進程
表示層 數據格式標識,基本壓縮加密功能
應用層 各類應用軟件

2. 常見HTTP狀態碼有哪些

2xx 開頭(請求成功)

200 OK:客戶端發送給服務器的請求被正常處理並返回


3xx 開頭(重定向)

301 Moved Permanently:永久重定向,請求的網頁已永久移動到新位置。 服務器返回此響應時,會自動將請求者轉到新位置

302 Moved Permanently:臨時重定向,請求的網頁已臨時移動到新位置。服務器目前從不一樣位置的網頁響應請求,但請求者應繼續使用原有位置來進行之後的請求

304 Not Modified:未修改,自從上次請求後,請求的網頁未修改過。服務器返回此響應時,不會返回網頁內容


4xx 開頭(客戶端錯誤)

400 Bad Request:錯誤請求,服務器不理解請求的語法,常見於客戶端傳參錯誤

401 Unauthorized:未受權,表示發送的請求須要有經過 HTTP 認證的認證信息,常見於客戶端未登陸

403 Forbidden:禁止,服務器拒絕請求,常見於客戶端權限不足

404 Not Found:未找到,服務器找不到對應資源


5xx 開頭(服務端錯誤)

500 Inter Server Error:服務器內部錯誤,服務器遇到錯誤,沒法完成請求

501 Not Implemented:還沒有實施,服務器不具有完成請求的功能

502 Bad Gateway:做爲網關或者代理工做的服務器嘗試執行請求時,從上游服務器接收到無效的響應。

503 service unavailable:服務不可用,服務器目前沒法使用(處於超載或停機維護狀態)。一般是暫時狀態。


3. GET請求和POST請求有何區別

標準答案:

  • GET請求參數放在URL上,POST請求參數放在請求體裏
  • GET請求參數長度有限制,POST請求參數長度能夠很是大
  • POST請求相較於GET請求安全一點點,由於GET請求的參數在URL上,且有歷史記錄
  • GET請求能緩存,POST不能

更進一步:

其實HTTP協議並無要求GET/POST請求參數必須放在URL上或請求體裏,也沒有規定GET請求的長度,目前對URL的長度限制,是各家瀏覽器設置的限制。GET和POST的根本區別在於:GET請求是冪等性的,而POST請求不是

冪等性,指的是對某一資源進行一次或屢次請求都具備相同的反作用。例如搜索就是一個冪等的操做,而刪除、新增則不是一個冪等操做。

因爲GET請求是冪等的,在網絡很差的環境中,GET請求可能會重複嘗試,形成重複操做數據的風險,所以,GET請求用於無反作用的操做(如搜索),新增/刪除等操做適合用POST

參考資料:HTTP|GET 和 POST 區別?網上多數答案都是錯的


4. HTTP的請求報文由哪幾部分組成

一個HTTP請求報文由請求行(request line)、請求頭(header)、空行和請求數據4個部分組成

響應報文和請求報文結構相似,再也不贅述

5. HTTP常見請求/響應頭及其含義

通用頭(請求頭和響應頭都有的首部)

字段 做用
Cache-Control 控制緩存 public:表示響應能夠被任何對象緩存(包括客戶端/代理服務器)
private(默認值):響應只能被單個客戶緩存,不能被代理服務器緩存
no-cache:緩存要通過服務器驗證,在瀏覽器使用緩存前,會對比ETag,若沒變則返回304,使用緩存
no-store:禁止任何緩存
Connection 是否須要持久鏈接(HTTP 1.1默認持久鏈接) keep-alive / close
Transfer-Encoding 報文主體的傳輸編碼格式 chunked(分塊) / identity(未壓縮和修改) / gzip(LZ77壓縮) / compress(LZW壓縮,棄用) / deflate(zlib結構壓縮)

請求頭

字段 做用 語法
Accept 告知(服務器)客戶端能夠處理的內容類型 text/html、image/*、*/*
If-Modified-Since Last-Modified的值發送給服務器,詢問資源是否已通過期(被修改),過時則返回新資源,不然返回304 示例:If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
If-Unmodified-Since Last-Modified的值發送給服務器,詢問文件是否被修改,若沒有則返回200,不然返回412預處理錯誤,可用於斷點續傳。通俗點說If-Unmodified-Since是文件沒有修改時下載,If-Modified-Since是文件修改時下載 示例:If-Unmodified-Since: Wed, 21 Oct 2015 07:28:00 GMT
If-None-Match ETag的值發送給服務器,詢問資源是否已通過期(被修改),過時則返回新資源,不然返回304 示例:If-None-Match: "bfc13a6472992d82d"
If-Match ETag的值發送給服務器,詢問文件是否被修改,若沒有則返回200,不然返回412預處理錯誤,可用於斷點續傳 示例:If-Match: "bfc129c88ca92d82d"
Range 告知服務器返回文件的哪一部分, 用於斷點續傳 示例:Range: bytes=200-1000, 2000-6576, 19000-
Host 指明瞭服務器的域名(對於虛擬主機來講),以及(可選的)服務器監聽的TCP端口號 示例:Host:www.baidu.com
User-Agent 告訴HTTP服務器, 客戶端使用的操做系統和瀏覽器的名稱和版本 User-Agent: Mozilla/<version> (<system-information>) <platform> (<platform-details>) <extensions>

響應頭

字段 做用 語法
Location 須要將頁面從新定向至的地址。通常在響應碼爲3xx的響應中才會有意義 Location: <url>
ETag 資源的特定版本的標識符,若是內容沒有改變,Web服務器不須要發送完整的響應 ETag: "<etag_value>"
Server 處理請求的源頭服務器所用到的軟件相關信息 Server: <product>

實體頭(針對請求報文和響應報文的實體部分使用首部)

字段 做用 語法
Allow 資源可支持http請求的方法 Allow: <http-methods>,示例:Allow: GET, POST, HEAD
Last-Modified 資源最後的修改時間,用做一個驗證器來判斷接收到的或者存儲的資源是否彼此一致,精度不如ETag 示例:Last-Modified: Wed, 21 Oct 2020 07:28:00 GMT
Expires 響應過時時間 Expires: <http-date>,示例:Expires: Wed, 21 Oct 2020 07:28:00 GMT

HTTP首部固然不止這麼幾個,但爲了不寫太多你們記不住(主要是別的我也沒去看),這裏只介紹了一些經常使用的,詳細的能夠看MDN的文檔


6. HTTP/1.0和HTTP/1.1有什麼區別

  • 長鏈接: HTTP/1.1支持長鏈接和請求的流水線,在一個TCP鏈接上能夠傳送多個HTTP請求,避免了由於屢次創建TCP鏈接的時間消耗和延時
  • 緩存處理: HTTP/1.1引入Entity tag,If-Unmodified-Since, If-Match, If-None-Match等新的請求頭來控制緩存,詳見瀏覽器緩存小節
  • 帶寬優化及網絡鏈接的使用: HTTP1.1則在請求頭引入了range頭域,支持斷點續傳功能
  • Host頭處理: 在HTTP/1.0中認爲每臺服務器都有惟一的IP地址,但隨着虛擬主機技術的發展,多個主機共享一個IP地址愈發廣泛,HTTP1.1的請求消息和響應消息都應支持Host頭域,且請求消息中若是沒有Host頭域會400錯誤

7. 介紹一下HTTP/2.0新特性

  • 多路複用: 即多個請求都經過一個TCP鏈接併發地完成
  • 服務端推送: 服務端可以主動把資源推送給客戶端
  • 新的二進制格式: HTTP/2採用二進制格式傳輸數據,相比於HTTP/1.1的文本格式,二進制格式具備更好的解析性和拓展性
  • header壓縮: HTTP/2壓縮消息頭,減小了傳輸數據的大小

8. 說說HTTP/2.0多路複用基本原理以及解決的問題

HTTP/2解決的問題,就是HTTP/1.1存在的問題:

  • TCP慢啓動: TCP鏈接創建後,會經歷一個先慢後快的發送過程,就像汽車啓動通常,若是咱們的網頁文件(HTML/JS/CSS/icon)都通過一次慢啓動,對性能是不小的損耗。另外慢啓動是TCP爲了減小網絡擁塞的一種策略,咱們是沒有辦法改變的。
  • 多條TCP鏈接競爭帶寬: 若是同時創建多條TCP鏈接,當帶寬不足時就會競爭帶寬,影響關鍵資源的下載。
  • HTTP/1.1隊頭阻塞: 儘管HTTP/1.1長連接能夠經過一個TCP鏈接傳輸多個請求,但同一時刻只能處理一個請求,當前請求未結束前,其餘請求只能處於阻塞狀態。

爲了解決以上幾個問題,HTTP/2一個域名只使用一個TCP⻓鏈接來傳輸數據,並且請求直接是並行的、非阻塞的,這就是多路複用

實現原理: HTTP/2引入了一個二進制分幀層,客戶端和服務端進行傳輸時,數據會先通過二進制分幀層處理,轉化爲一個個帶有請求ID的幀,這些幀在傳輸完成後根據ID組合成對應的數據。


9. 說說HTTP/3.0

儘管HTTP/2解決了不少1.1的問題,但HTTP/2仍然存在一些缺陷,這些缺陷並非來自於HTTP/2協議自己,而是來源於底層的TCP協議,咱們知道TCP連接是可靠的鏈接,若是出現了丟包,那麼整個鏈接都要等待重傳,HTTP/1.1能夠同時使用6個TCP鏈接,一個阻塞另外五個還能工做,但HTTP/2只有一個TCP鏈接,阻塞的問題便被放大了。

因爲TCP協議已經被普遍使用,咱們很難直接修改TCP協議,基於此,HTTP/3選擇了一個折衷的方法——UDP協議,HTTP/2在UDP的基礎上實現多路複用、0-RTT、TLS加密、流量控制、丟包重傳等功能。


參考資料:http發展史(http0.九、http1.0、http1.一、http二、http3)梳理筆記 (推薦閱讀)


10. HTTP和HTTPS有何區別

  • HTTPS使用443端口,而HTTP使用80
  • HTTPS須要申請證書
  • HTTP是超文本傳輸協議,是明文傳輸;HTTPS是通過SSL加密的協議,傳輸更安全
  • HTTPS比HTTP慢,由於HTTPS除了TCP握手的三個包,還要加上SSL握手的九個包

11. HTTPS是如何進行加密的

咱們經過分析幾種加密方式,層層遞進,理解HTTPS的加密方式以及爲何使用這種加密方式:

對稱加密

客戶端和服務器公用一個密匙用來對消息加解密,這種方式稱爲對稱加密。客戶端和服務器約定好一個加密的密匙。客戶端在發消息前用該密匙對消息加密,發送給服務器後,服務器再用該密匙進行解密拿到消息。

這種方式必定程度上保證了數據的安全性,但密鑰一旦泄露(密鑰在傳輸過程當中被截獲),傳輸內容就會暴露,所以咱們要尋找一種安全傳遞密鑰的方法。

非對稱加密

採用非對稱加密時,客戶端和服務端均擁有一個公鑰和私鑰,公鑰加密的內容只有對應的私鑰能解密。私鑰本身留着,公鑰發給對方。這樣在發送消息前,先用對方的公鑰對消息進行加密,收到後再用本身的私鑰進行解密。這樣攻擊者只拿到傳輸過程當中的公鑰也沒法破解傳輸的內容

儘管非對稱加密解決了因爲密鑰被獲取而致使傳輸內容泄露的問題,但中間人仍然能夠用 篡改公鑰的方式來獲取或篡改傳輸內容,並且非對稱加密的性能比對稱加密的性能差了很多

第三方認證

上面這種方法的弱點在於,客戶端不知道公鑰是由服務端返回,仍是中間人返回的,所以咱們再引入一個第三方認證的環節:即第三方使用私鑰加密咱們本身的公鑰,瀏覽器已經內置一些權威第三方認證機構的公鑰,瀏覽器會使用第三方的公鑰來解開第三方私鑰加密過的咱們本身的公鑰,從而獲取公鑰,若是能成功解密,就說明獲取到的本身的公鑰是正確的

但第三方認證也未能徹底解決問題,第三方認證是面向全部人的,中間人也能申請證書,若是中間人使用本身的證書掉包原證書,客戶端仍是沒法確認公鑰的真僞

數字簽名

爲了讓客戶端可以驗證公鑰的來源,咱們給公鑰加上一個數字簽名,這個數字簽名是由企業、網站等各類信息和公鑰通過單向hash而來,一旦構成數字簽名的信息發生變化,hash值就會改變,這就構成了公鑰來源的惟一標識。

具體來講,服務端本地生成一對密鑰,而後拿着公鑰以及企業、網站等各類信息到CA(第三方認證中心)去申請數字證書,CA會經過一種單向hash算法(好比MD5),生成一串摘要,這串摘要就是這堆信息的惟一標識,而後CA還會使用本身的私鑰對摘要進行加密,連同咱們本身服務器的公鑰一同發送給我咱們。

瀏覽器拿到數字簽名後,會使用瀏覽器本地內置的CA公鑰解開數字證書並驗證,從而拿到正確的公鑰。因爲非對稱加密性能低下,拿到公鑰之後,客戶端會隨機生成一個對稱密鑰,使用這個公鑰加密併發送給服務端,服務端用本身的私鑰解開對稱密鑰,此後的加密鏈接就經過這個對稱密鑰進行對稱加密。

綜上所述,HTTPS在驗證階段使用非對稱加密+第三方認證+數字簽名獲取正確的公鑰,獲取到正確的公鑰後以對稱加密的方式通訊

參考資料:看圖學HTTPS


前端安全

什麼是CSRF攻擊

CSRF即Cross-site request forgery(跨站請求僞造),是一種挾制用戶在當前已登陸的Web應用程序上執行非本意的操做的攻擊方法。

假如黑客在本身的站點上放置了其餘網站的外鏈,例如"www.weibo.com/api,默認狀況下,瀏覽器會帶着weibo.com的cookie訪問這個網址,若是用戶已登陸過該網站且網站沒有對CSRF攻擊進行防護,那麼服務器就會認爲是用戶本人在調用此接口並執行相關操做,導致帳號被劫持。

如何防護CSRF攻擊

  • 驗證Token:瀏覽器請求服務器時,服務器返回一個token,每一個請求都須要同時帶上token和cookie纔會被認爲是合法請求
  • 驗證Referer:經過驗證請求頭的Referer來驗證來源站點,但請求頭很容易僞造
  • 設置SameSite:設置cookie的SameSite,可讓cookie不隨跨域請求發出,但瀏覽器兼容不一

什麼是XSS攻擊

XSS即Cross Site Scripting(跨站腳本),指的是經過利用網頁開發時留下的漏洞,注入惡意指令代碼到網頁,使用戶加載並執行攻擊者惡意製造的網頁程序。常見的例如在評論區植入JS代碼,用戶進入評論頁時代碼被執行,形成頁面被植入廣告、帳號信息被竊取

XSS攻擊有哪些類型

  • 存儲型:即攻擊被存儲在服務端,常見的是在評論區插入攻擊腳本,若是腳本被儲存到服務端,那麼全部看見對應評論的用戶都會受到攻擊。
  • 反射型:攻擊者將腳本混在URL裏,服務端接收到URL將惡意代碼當作參數取出並拼接在HTML裏返回,瀏覽器解析此HTML後即執行惡意代碼
  • DOM型:將攻擊腳本寫在URL中,誘導用戶點擊該URL,若是URL被解析,那麼攻擊腳本就會被運行。和前二者的差異主要在於DOM型攻擊不通過服務端

如何防護XSS攻擊

  • 輸入檢查:對輸入內容中的<script><iframe>等標籤進行轉義或者過濾
  • 設置httpOnly:不少XSS攻擊目標都是竊取用戶cookie僞造身份認證,設置此屬性可防止JS獲取cookie
  • 開啓CSP,即開啓白名單,可阻止白名單之外的資源加載和運行


排序算法

1. 手寫冒泡排序

冒泡排序應該是不少人第一個接觸的排序,比較簡單,不展開講解了

function bubbleSort(arr){
  for(let i = 0; i < arr.length; i++) {
    for(let j = 0; j < arr.length - i - 1; j++) {
      if(arr[j] > arr[j+1]) {
        let temp = arr[j]
        arr[j] = arr[j+1]
        arr[j+1] = temp
      }
    }
  }
  return arr
}
複製代碼

2. 如何優化一個冒泡排序

冒泡排序總會執行(N-1)+(N-2)+(N-3)+..+2+1趟,但若是運行到當中某一趟時排序已經完成,或者輸入的是一個有序數組,那麼後邊的比較就都是多餘的,爲了不這種狀況,咱們增長一個flag,判斷排序是否在中途就已經完成(也就是判斷有無發生元素交換)

function bubbleSort(arr){
  for(let i = 0; i < arr.length; i++) {
  let flag = true
    for(let j = 0; j < arr.length - i - 1; j++) {
      if(arr[j] > arr[j+1]) {
        flag = false
        let temp = arr[j]
        arr[j] = arr[j+1]
        arr[j+1] = temp
      }
    }
    // 這個flag的含義是:若是`某次循環`中沒有交換過元素,那麼意味着排序已經完成
    if(flag)break;
  }
  return arr
}
複製代碼

3. 手寫快速排序

快排基本步驟:

  1. 選取基準元素
  2. 比基準元素小的元素放到左邊,大的放右邊
  3. 在左右子數組中重複步驟一二,直到數組只剩下一個元素
  4. 向上逐級合併數組
function quickSort(arr) {
    if(arr.length <= 1) return arr          //遞歸終止條件
    const pivot = arr.length / 2 | 0        //基準點
    const pivotValue = arr.splice(pivot, 1)[0]
    const leftArr = []
    const rightArr = []
    arr.forEach(val => {
        val > pivotValue ? rightArr.push(val) : leftArr.push(val)
    })
    return [ ...quickSort(leftArr), pivotValue, ...quickSort(rightArr)]
}
複製代碼

4. 如何優化一個快速排序

原地排序

上邊這個快排只是讓讀者找找感受,咱們不能這樣寫快排,若是每次都開兩個數組,會消耗不少內存空間,數據量大時可能形成內存溢出,咱們要避免開新的內存空間,即原地完成排序

咱們能夠用元素交換來取代開新數組,在每一次分區的時候直接在原數組上交換元素,將小於基準數的元素挪到數組開頭,以[5,1,4,2,3]爲例:

咱們定義一個pos指針, 標識等待置換的元素的位置, 而後逐一遍歷數組元素, 遇到比基準數小的就和arr[pos]交換位置, 而後pos++

代碼實現:

function quickSort(arr, left, right) {          //這個left和right表明分區後「新數組」的區間下標,由於這裏沒有新開數組,因此須要left/right來確認新數組的位置
    if (left < right) {
        let pos = left - 1                      //pos即「被置換的位置」,第一趟爲-1
        for(let i = left; i <= right; i++) {    //循環遍歷數組,置換元素
            let pivot = arr[right]              //選取數組最後一位做爲基準數,
            if(arr[i] <= pivot) {               //若小於等於基準數,pos++,並置換元素, 這裏使用小於等於而不是小於, 實際上是爲了不由於重複數據而進入死循環
                pos++
                let temp = arr[pos]
                arr[pos] = arr[i]
                arr[i] = temp
            }
        }
        //一趟排序完成後,pos位置即基準數的位置,以pos的位置分割數組
        quickSort(arr, left, pos - 1)        
        quickSort(arr, pos + 1, right)
    }
    return arr      //數組只包含1或0個元素時(即left>=right),遞歸終止
}

//使用
var arr = [5,1,4,2,3]
var start = 0;
var end = arr.length - 1;
quickSort(arr, start, end)
複製代碼

這個交換的過程仍是須要一些時間理解消化的,詳細分析能夠看這篇:js算法-快速排序(Quicksort)

三路快排

上邊這個快排還談不上優化,應當說是快排的糾正寫法,其實有兩個問題咱們還能優化一下:

  1. 有序數組的狀況:若是輸入的數組是有序的,而取基準點時也順序取,就可能致使基準點一側的子數組一直爲空, 使時間複雜度退化到O(n2)
  2. 大量重複數據的狀況:例如輸入的數據是[1,2,2,2,2,3], 不管基準點取一、2仍是3, 都會致使基準點兩側數組大小不平衡, 影響快排效率

對於第一個問題, 咱們能夠經過在取基準點的時候隨機化來解決,對於第二個問題,咱們能夠使用三路快排的方式來優化,比方說對於上面的[1,2,2,2,2,3],咱們基準點取2,在分區的時候,將數組元素分爲小於2|等於2|大於2三個區域,其中等於基準點的部分再也不進入下一次排序, 這樣就大大提升了快排效率

5. 手寫歸併排序

歸併排序和快排的思路相似,都是遞歸分治,區別在於快排邊分區邊排序,而歸併在分區完成後纔會排序

function mergeSort(arr) {
    if(arr.length <= 1) return arr		//數組元素被劃分到剩1個時,遞歸終止
    const midIndex = arr.length/2 | 0
    const leftArr = arr.slice(0, midIndex)
    const rightArr = arr.slice(midIndex, arr.length)
    return merge(mergeSort(leftArr), mergeSort(rightArr))	//先劃分,後合併
}

//合併
function merge(leftArr, rightArr) {
    const result = []
    while(leftArr.length && rightArr.length) {
    	leftArr[0] <= rightArr[0] ? result.push(leftArr.shift()) : result.push(rightArr.shift())
    }
    while(leftArr.length) result.push(leftArr.shift())
    while(rightArr.length) result.push(rightArr.shift())
    return result
}
複製代碼

6. 手寫堆排序

堆是一棵特殊的樹, 只要知足這棵樹是徹底二叉樹堆中每個節點的值都大於或小於其左右孩子節點這兩個條件, 那麼就是一個堆, 根據堆中每個節點的值都大於或小於其左右孩子節點, 又分爲大根堆和小根堆

堆排序的流程:

  1. 初始化大(小)根堆,此時根節點爲最大(小)值,將根節點與最後一個節點(數組最後一個元素)交換
  2. 除開最後一個節點,從新調整大(小)根堆,使根節點爲最大(小)值
  3. 重複步驟二,直到堆中元素剩一個,排序完成

[1,5,4,2,3]爲例構築大根堆:

代碼實現:

// 堆排序
const heapSort = array => {
        // 咱們用數組來儲存這個大根堆,數組就是堆自己
	// 初始化大頂堆,從第一個非葉子結點開始
	for (let i = Math.floor(array.length / 2 - 1); i >= 0; i--) {
		heapify(array, i, array.length);
	}
	// 排序,每一次 for 循環找出一個當前最大值,數組長度減一
	for (let i = Math.floor(array.length - 1); i > 0; i--) {
		// 根節點與最後一個節點交換
		swap(array, 0, i);
		// 從根節點開始調整,而且最後一個結點已經爲當前最大值,不須要再參與比較,因此第三個參數爲 i,即比較到最後一個結點前一個便可
		heapify(array, 0, i);
	}
	return array;
};

// 交換兩個節點
const swap = (array, i, j) => {
	let temp = array[i];
	array[i] = array[j];
	array[j] = temp;
};

// 將 i 結點如下的堆整理爲大頂堆,注意這一步實現的基礎其實是:
// 假設結點 i 如下的子堆已是一個大頂堆,heapify 函數實現的
// 功能是其實是:找到 結點 i 在包括結點 i 的堆中的正確位置。
// 後面將寫一個 for 循環,從第一個非葉子結點開始,對每個非葉子結點
// 都執行 heapify 操做,因此就知足告終點 i 如下的子堆已是一大頂堆
const heapify = (array, i, length) => {
	let temp = array[i]; // 當前父節點
	// j < length 的目的是對結點 i 如下的結點所有作順序調整
	for (let j = 2 * i + 1; j < length; j = 2 * j + 1) {
		temp = array[i]; // 將 array[i] 取出,整個過程至關於找到 array[i] 應處於的位置
		if (j + 1 < length && array[j] < array[j + 1]) {
			j++; // 找到兩個孩子中較大的一個,再與父節點比較
		}
		if (temp < array[j]) {
			swap(array, i, j); // 若是父節點小於子節點:交換;不然跳出
			i = j; // 交換後,temp 的下標變爲 j
		} else {
			break;
		}
	}
}
複製代碼

參考資料: JS實現堆排序

7. 歸併、快排、堆排有何區別

排序 時間複雜度(最好狀況) 時間複雜度(最壞狀況) 空間複雜度 穩定性
快速排序 O(nlogn) O(n^2) O(logn)~O(n) 不穩定
歸併排序 O(nlogn) O(nlogn) O(n) 穩定
堆排序 O(nlogn) O(nlogn) O(1) 不穩定

其實從表格中咱們能夠看到,就時間複雜度而言,快排並無很大優點,然而爲何快排會成爲最經常使用的排序手段,這是由於時間複雜度只能說明隨着數據量的增長,算法時間代價增加的趨勢,並不直接表明實際執行時間,實際運行時間還包括了不少常數參數的差異,此外在面對不一樣類型數據(好比有序數據、大量重複數據)時,表現也不一樣,綜合來講,快排的時間效率是最高的

在實際運用中, 並不僅使用一種排序手段, 例如V8的Array.sort()就採起了當 n<=10 時, 採用插入排序, 當 n>10 時,採用三路快排的排序策略


設計模式

設計模式有許多種,這裏挑出幾個經常使用的:

設計模式 描述 例子
單例模式 一個類只能構造出惟一實例 Redux/Vuex的store
工廠模式 對建立對象邏輯的封裝 jQuery的$(selector)
觀察者模式 當一個對象被修改時,會自動通知它的依賴對象 Redux的subscribe、Vue的雙向綁定
裝飾器模式 對類的包裝,動態地拓展類的功能 React高階組件、ES7 裝飾器
適配器模式 兼容新舊接口,對類的包裝 封裝舊API
代理模式 控制對象的訪問 事件代理、ES6的Proxy

1. 介紹一下單一職責原則和開放封閉原則

  • 單一職責原則:一個類只負責一個功能領域中的相應職責,或者能夠定義爲:就一個類而言,應該只有一個引發它變化的緣由。

  • 開放封閉原則:核心的思想是軟件實體(類、模塊、函數等)是可擴展的、但不可修改的。也就是說,對擴展是開放的,而對修改是封閉的。


2. 單例模式

單例模式即一個類只能構造出惟一實例,單例模式的意義在於共享、惟一Redux/Vuex中的store、JQ的$或者業務場景中的購物車、登陸框都是單例模式的應用

class SingletonLogin {
  constructor(name,password){
    this.name = name
    this.password = password
  }
  static getInstance(name,password){
    //判斷對象是否已經被建立,若建立則返回舊對象
    if(!this.instance)this.instance = new SingletonLogin(name,password)
    return this.instance
  }
}
 
let obj1 = SingletonLogin.getInstance('CXK','123')
let obj2 = SingletonLogin.getInstance('CXK','321')
 
console.log(obj1===obj2)    // true
console.log(obj1)           // {name:CXK,password:123}
console.log(obj2)           // 輸出的依然是{name:CXK,password:123}
複製代碼

3. 工廠模式

工廠模式即對建立對象邏輯的封裝,或者能夠簡單理解爲對new的封裝,這種封裝就像建立對象的工廠,故名工廠模式。工廠模式常見於大型項目,好比JQ的$對象,咱們建立選擇器對象時之因此沒有new selector就是由於$()已是一個工廠方法,其餘例子例如React.createElement()Vue.component()都是工廠模式的實現。工廠模式有多種:簡單工廠模式工廠方法模式抽象工廠模式,這裏只以簡單工廠模式爲例:

class User {
  constructor(name, auth) {
    this.name = name
    this.auth = auth
  }
}

class UserFactory {
  static createUser(name, auth) {
    //工廠內部封裝了建立對象的邏輯:
    //權限爲admin時,auth=1, 權限爲user時, auth爲2
    //使用者在外部建立對象時,不須要知道各個權限對應哪一個字段, 不須要知道賦權的邏輯,只須要知道建立了一個管理員和用戶
    if(auth === 'admin')  new User(name, 1)
    if(auth === 'user')  new User(name, 2)
  }
}

const admin = UserFactory.createUser('cxk', 'admin');
const user = UserFactory.createUser('cxk', 'user');
複製代碼

4. 觀察者模式

觀察者模式算是前端最經常使用的設計模式了,觀察者模式概念很簡單:觀察者監聽被觀察者的變化,被觀察者發生改變時,通知全部的觀察者。觀察者模式被普遍用於監聽事件的實現,有關觀察者模式的詳細應用,能夠看我另外一篇講解Redux實現的文章

//觀察者
class Observer {    
  constructor (fn) {      
    this.update = fn    
  }
}
//被觀察者
class Subject {    
    constructor() {        
        this.observers = []          //觀察者隊列 
    }    
    addObserver(observer) {          
        this.observers.push(observer)//往觀察者隊列添加觀察者 
    }    
    notify() {                       //通知全部觀察者,其實是把觀察者的update()都執行了一遍 
        this.observers.forEach(observer => {        
            observer.update()            //依次取出觀察者,並執行觀察者的update方法 
        })    
    }
}

var subject = new Subject()       //被觀察者
const update = () => {console.log('被觀察者發出通知')}  //收到廣播時要執行的方法
var ob1 = new Observer(update)    //觀察者1
var ob2 = new Observer(update)    //觀察者2
subject.addObserver(ob1)          //觀察者1訂閱subject的通知
subject.addObserver(ob2)          //觀察者2訂閱subject的通知
subject.notify()                  //發出廣播,執行全部觀察者的update方法
複製代碼

有些文章也把觀察者模式稱爲發佈訂閱模式,其實兩者是有所區別的,發佈訂閱相較於觀察者模式多一個調度中心。


5. 裝飾器模式

裝飾器模式,能夠理解爲對類的一個包裝,動態地拓展類的功能,ES7的裝飾器語法以及React中的高階組件(HoC)都是這一模式的實現。react-redux的connect()也運用了裝飾器模式,這裏以ES7的裝飾器爲例:

function info(target) {
  target.prototype.name = '張三'
  target.prototype.age = 10
}

@info
class Man {}

let man = new Man()
man.name // 張三
複製代碼

6. 適配器模式

適配器模式,將一個接口轉換成客戶但願的另外一個接口,使接口不兼容的那些類能夠一塊兒工做。咱們在生活中就經常有使用適配器的場景,例如出境旅遊插頭插座不匹配,這時咱們就須要使用轉換插頭,也就是適配器來幫咱們解決問題。

class Adaptee {
  test() {
      return '舊接口'
  }
}
 
class Target {
  constructor() {
      this.adaptee = new Adaptee()
  }
  test() {
      let info = this.adaptee.test()
      return `適配${info}`
  }
}
 
let target = new Target()
console.log(target.test())
複製代碼

7. 代理模式

代理模式,爲一個對象找一個替代對象,以便對原對象進行訪問。即在訪問者與目標對象之間加一層代理,經過代理作受權和控制。最多見的例子是經紀人代理明星業務,假設你做爲一個投資者,想聯繫明星打廣告,那麼你就須要先通過代理經紀人,經紀人對你的資質進行考察,並通知你明星排期,替明星本人過濾沒必要要的信息。事件代理、JQuery的$.proxy、ES6的proxy都是這一模式的實現,下面以ES6的proxy爲例:

const idol = {
  name: '蔡x抻',
  phone: 10086,
  price: 1000000  //報價
}

const agent = new Proxy(idol, {
  get: function(target) {
    //攔截明星電話的請求,只提供經紀人電話
    return '經紀人電話:10010'
  },
  set: function(target, key, value) {
    if(key === 'price' ) {
      //經紀人過濾資質
      if(value < target.price) throw new Error('報價太低')
      target.price = value
    }
  }
})


agent.phone        //經紀人電話:10010
agent.price = 100  //Uncaught Error: 報價太低
複製代碼


HTML相關

1. 說說HTML5在標籤、屬性、存儲、API上的新特性

  • 標籤:新增語義化標籤(aside / figure / section / header / footer / nav等),增長多媒體標籤videoaudio,使得樣式和結構更加分離
  • 屬性:加強表單,主要是加強了input的type屬性;meta增長charset以設置字符集;script增長async以異步加載腳本
  • 存儲:增長localStoragesessionStorageindexedDB,引入了application cache對web和應用進行緩存
  • API:增長拖放API地理定位SVG繪圖canvas繪圖Web WorkerWebSocket

2. doctype的做用是什麼?

聲明文檔類型,告知瀏覽器用什麼文檔標準解析這個文檔:

  • 怪異模式:瀏覽器使用本身的模式解析文檔,不加doctype時默認爲怪異模式
  • 標準模式:瀏覽器以W3C的標準解析文檔

3. 幾種前端儲存以及它們之間的區別

  • cookies: HTML5以前本地儲存的主要方式,大小隻有4k,HTTP請求頭會自動帶上cookie,兼容性好
  • localStorage:HTML5新特性,持久性存儲,即便頁面關閉也不會被清除,以鍵值對的方式存儲,大小爲5M
  • sessionStorage:HTML5新特性,操做及大小同localStorage,和localStorage的區別在於sessionStorage在選項卡(頁面)被關閉時即清除,且不一樣選項卡之間的sessionStorage不互通
  • IndexedDB: NoSQL型數據庫,類比MongoDB,使用鍵值對進行儲存,異步操做數據庫,支持事務,儲存空間能夠在250MB以上,可是IndexedDB受同源策略限制
  • Web SQL:是在瀏覽器上模擬的關係型數據庫,開發者能夠經過SQL語句來操做Web SQL,是HTML5之外一套獨立的規範,兼容性差

4. href和src有什麼區別

href(hyperReference)即超文本引用:當瀏覽器遇到href時,會並行的地下載資源,不會阻塞頁面解析,例如咱們使用<link>引入CSS,瀏覽器會並行地下載CSS而不阻塞頁面解析. 所以咱們在引入CSS時建議使用<link>而不是@import

<link href="style.css" rel="stylesheet" />
複製代碼

src(resource)即資源,當瀏覽器遇到src時,會暫停頁面解析,直到該資源下載或執行完畢,這也是script標籤之因此放底部的緣由

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

5. meta有哪些屬性,做用是什麼

meta標籤用於描述網頁的元信息,如網站做者、描述、關鍵詞,meta經過name=xxxcontent=xxx的形式來定義信息,經常使用設置以下:

  • charset:定義HTML文檔的字符集
<meta charset="UTF-8" >
複製代碼
  • http-equiv:可用於模擬http請求頭,可設置過時時間、緩存、刷新
<meta http-equiv="expires" content="Wed, 20 Jun 2019 22:33:00 GMT"複製代碼
  • viewport:視口,用於控制頁面寬高及縮放比例
<meta 
    name="viewport" 
    content="width=device-width, initial-scale=1, maximum-scale=1"
>
複製代碼

6. viewport有哪些參數,做用是什麼

  • width/height,寬高,默認寬度980px
  • initial-scale,初始縮放比例,1~10
  • maximum-scale/minimum-scale,容許用戶縮放的最大/小比例
  • user-scalable,用戶是否能夠縮放 (yes/no)

7. http-equive屬性的做用和參數

  • expires,指定過時時間
  • progma,設置no-cache能夠禁止緩存
  • refresh,定時刷新
  • set-cookie,能夠設置cookie
  • X-UA-Compatible,使用瀏覽器版本
  • apple-mobile-web-app-status-bar-style,針對WebApp全屏模式,隱藏狀態欄/設置狀態欄顏色


CSS相關

清除浮動的方法

爲何要清除浮動:清除浮動是爲了解決子元素浮動而致使父元素高度塌陷的問題

1.添加新元素

<div class="parent">
  <div class="child"></div>
  <!-- 添加一個空元素,利用css提供的clear:both清除浮動 -->
  <div style="clear: both"></div>
</div>  
複製代碼

2.使用僞元素

/* 對父元素添加僞元素 */
.parent::after{
  content: "";
  display: block;
  height: 0;
  clear:both;
}
複製代碼

3.觸發父元素BFC

/* 觸發父元素BFC */
.parent {
  overflow: hidden;
  /* float: left; */
  /* position: absolute; */
  /* display: inline-block */
  /* 以上屬性都可觸發BFC */
}
複製代碼

介紹一下flex佈局

其實我原本還寫了一節水平/垂直居中相關的,不過感受內容過於基礎還佔長篇幅,因此刪去了,做爲一篇總結性的文章,這一小節也不該該從「flex是什麼」開始講,主軸、側軸這些概念相信用過flex佈局都知道,因此咱們直接flex的幾個屬性講起:

容器屬性(使用在flex佈局容器上的屬性)

  • justify-content
    定義了子元素在主軸(橫軸)上的對齊方式
.container {
    justify-content: center | flex-start | flex-end | space-between | space-around;
    /* 主軸對齊方式:居中 | 左對齊(默認值) | 右對齊 | 兩端對齊(子元素間邊距相等) | 周圍對齊(每一個子元素兩側margin相等) */
}
複製代碼
  • align-items
    定義了定義項目在交叉軸(豎軸)上對齊方式
.container {
    align-items: center | flex-start | flex-end | baseline | stretch;
    /* 側軸對齊方式:居中 | 上對齊 | 下對齊 | 項目的第一行文字的基線對齊 | 若是子元素未設置高度,將佔滿整個容器的高度(默認值) */
}
複製代碼
  • flex-direction
    主軸(橫軸)方向
.container {
    flex-direction: row | row-reverse | column | column-reverse;
    /* 主軸方向:水平由左至右排列(默認值) | 水平由右向左 | 垂直由上至下 | 垂直由下至上 */
}
複製代碼
  • flex-wrap
    換行方式
.container {
    flex-wrap: nowrap | wrap | wrap-reverse;
    /* 換行方式:不換行(默認值) | 換行 | 反向換行 */
}
複製代碼
  • flex-flow
    flex-flow屬性是flex-direction屬性和flex-wrap的簡寫
.container {
    flex-flow: <flex-direction> || <flex-wrap>;
    /* 默認值:row nowrap */
}
複製代碼
  • align-content
    定義多根軸線的對齊方式
.container {
    align-content: center | flex-start | flex-end | space-between | space-around | stretch;
    /* 默認值:與交叉軸的中點對齊 | 與交叉軸的起點對齊 | 與交叉軸的終點對齊 | 與交叉軸兩端對齊 | 每根軸線兩側的間隔都相等 | (默認值):軸線佔滿整個交叉軸 */
}
複製代碼

項目屬性(使用在容器內子元素上的屬性)

  • flex-grow
    定義項目的放大比例,默認爲0,即便有剩餘空間也不放大。若是全部子元素flex-grow爲1,那麼將等分剩餘空間,若是某個子元素flex-grow爲2,那麼這個子元素將佔據2倍的剩餘空間
.item {
  flex-grow: <number>; /* default 0 */
}
複製代碼
  • flex-shrink
    定義項目的縮小比例,默認爲1,即若是空間不足,子元素將縮小。若是全部子元素flex-shrink都爲1,某個子元素flex-shrink爲0,那麼該子元素將不縮小
.item {
  flex-shrink: <number>; /* default 1 */
}
複製代碼
  • flex-basis
    定義在分配多餘空間以前,項目佔據的主軸空間,默認auto,即子元素原本的大小,若是設定爲一個固定的值,那麼子元素將佔據固定空間
.item {
  flex-basis: <length> | auto; /* default auto */
}
複製代碼
  • flex
    flex屬性是flex-grow, flex-shrinkflex-basis的簡寫,默認值爲0 1 auto,即有剩餘空間不放大,剩餘空間不夠將縮小,子元素佔據自身大小
.item {
  flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
}
複製代碼

flex有兩個快捷值:autonone,分別表明1 1 auto(有剩餘空間則平均分配,空間不夠將等比縮小,子元素佔據空間等於自身大小)和0 0 auto(有剩餘空間也不分配,空間不夠也不縮小,子元素佔據空間等於自身大小)

  • order
    定義項目的排列順序。數值越小,排列越靠前,默認爲0
.item {
  order: <integer>;
}
複製代碼
  • align-self
    定義單個子元素的排列方式,例如align-items設置了center,使得全部子元素居中對齊,那麼能夠經過給某個子元素設置align-self來單獨設置子元素的排序方式
.item {
  align-self: auto | flex-start | flex-end | center | baseline | stretch;
}
複製代碼

參考資料:阮一峯Flex佈局


常見佈局

編輯中,請稍等-_-||


什麼是BFC

BFC全稱 Block Formatting Context 即塊級格式上下文,簡單的說,BFC是頁面上的一個隔離的獨立容器,不受外界干擾或干擾外界

如何觸發BFC

  • float不爲 none
  • overflow的值不爲 visible
  • position 爲 absolute 或 fixed
  • display的值爲 inline-block 或 table-cell 或 table-caption 或 grid

BFC的渲染規則是什麼

  • BFC是頁面上的一個隔離的獨立容器,不受外界干擾或干擾外界
  • 計算BFC的高度時,浮動子元素也參與計算(即內部有浮動元素時也不會發生高度塌陷)
  • BFC的區域不會與float的元素區域重疊
  • BFC內部的元素會在垂直方向上放置
  • BFC內部兩個相鄰元素的margin會發生重疊

BFC的應用場景

  • 清除浮動:BFC內部的浮動元素會參與高度計算,所以可用於清除浮動,防止高度塌陷
  • 避免某元素被浮動元素覆蓋:BFC的區域不會與浮動元素的區域重疊
  • 阻止外邊距重疊:屬於同一個BFC的兩個相鄰Box的margin會發生摺疊,不一樣BFC不會發生摺疊


總結

對於前端基礎知識的講解,到這裏就告一小段落。前端的世界紛繁複雜,遠非筆者寥寥幾筆所能勾畫,筆者就像在沙灘上拾取貝殼的孩童,有時僥倖拾取收集一二,就爲之歡欣鼓舞,火燒眉毛與夥伴們分享。

最後還想可恥地抒(自)發(誇)一下(•‾̑⌣‾̑•)✧˖°:
不知不覺,在掘金已經水了半年有餘,這半年來我寫下了近6萬字,不過其實一共只有5篇文章,這是由於我並不想寫水文,不想把基礎的東西水上幾千字幾十篇來混贊升級。寫下的文章,首先要能說服本身。要對本身寫下的東西負責任,即便是一張圖、一個標點。例如第一張圖,我調整了不下十次,第一次我直接截取babel的轉化結果,以爲很差看,換成了代碼塊,仍是很差看,又換成了carbon的代碼圖,第一次下載,發現兩張圖寬度不同,填充寬度從新下載,又發現本身的代碼少了一個空格,從新下載,爲了實現兩張圖並排效果,寫了一個HTML來調整兩張圖的樣式,爲了保證每張圖的內容和邊距一致,我一邊截圖,一邊記錄下每次截圖的尺寸和邊距,每次截圖都根據上一次的數據調整邊距。

其實我並不是提倡把時間花在這些細枝末節上,只是單純以爲,文章沒寫好,就不能發出來,就像小野二郎先生說的那樣:「菜作的很差,就不能拿給客人吃」,世間的大道理,每每都這樣通俗簡單。

往期文章:

1. 異步編程二三事 | Promise/async/Generator實現原理解析 | 9k字
2. 10行代碼看盡redux實現 —— redux & react-redux & redux中間件設計實現 | 8k字
3.紅黑樹上紅黑果,紅黑樹下你和我 —— 紅黑樹入門 | 6k字
4. 深刻React服務端渲染原理 | 1W字

相關文章
相關標籤/搜索