金九銀十~JS精選回顧寶典

前言

在這個金九銀十的日子中,爲你們奉上JS精選複習寶典一份,望各位看官笑納!javascript

普通函數和箭頭函數的 this

function fn() {
  console.log(this); // 1. {a: 100}
  var arr = [1, 2, 3];

  (function() {
    console.log(this); // 2. Window
  })();

  // 普通 JS
  arr.map(function(item) {
    console.log(this); // 3. Window
    return item + 1;
  });
  // 箭頭函數
  let brr = arr.map(item => {
    console.log("es6", this); // 4. {a: 100}
    return item + 1;
  });
}
fn.call({ a: 100 });
複製代碼

其實訣竅很簡單,常見的基本是 3 種狀況:es5 普通函數、es6 的箭頭函數以及經過bind改變過上下文返回的新函數。html

① es5 普通函數:java

函數被直接調用,上下文必定是window 函數做爲對象屬性被調用,例如:obj.foo(),上下文就是對象自己obj 經過new調用,this綁定在返回的實例上nginx

② es6 箭頭函數:es6

它自己沒有this,會沿着做用域向上尋找,直到 window。請看下面的這段代碼:編程

function run() {
  const inner = () => {
    return () => {
      console.log(this.a);
    };
  };

  inner()();
}

run.bind({ a: 1 })(); // Output: 1
複製代碼

③ bind 綁定上下文返回的新函數:就是被第一個 bind 綁定的上下文,並且 bind 對「箭頭函數」無效。請看下面的這段代碼:後端

function run() {
  console.log(this.a);
}

run.bind({ a: 1 })(); // output: 1

// 屢次bind,上下文由第一個bind的上下文決定
run.bind({ a: 2 }).bind({ a: 1 })(); // output: 2
複製代碼

最後,再說說這幾種方法的優先級:new > bind > 對象調用 > 直接調用跨域

原始數據類型和判斷方法

題目:JS 中的原始數據類型?數組

ECMAScript 中定義了 7 種原始類型:promise

Boolean

String

Number

Null

Undefined

Symbol(es6)

BigInt (Stage提案) 注意:原始類型不包含 Object 和 Function

題目:經常使用的判斷方法?

在進行判斷的時候有typeof、instanceof。對於數組的判斷,使用Array.isArray():

  • typeof:

  • typeof 基本均可以正確判斷數據類型

  • typeof null和typeof [1, 2, 3]均返回"object"

  • ES6 新增:typeof Symbol()返回"symbol"

  • instanceof:

專門用於實例和構造函數對應

function Obj(value) {
  this.value = value;
}
let obj = new Obj("test");
console.log(obj instanceof Obj); // output: true
複製代碼

判斷是不是數組:[1, 2, 3] instanceof Array

Array.isArray():ES6 新增,用來判斷是不是'Array'。Array.isArray({})返回false。

事件流

事件冒泡事件捕獲

事件流分爲:冒泡捕獲,順序是先捕獲再冒泡。

事件冒泡:子元素的觸發事件會一直向父節點傳遞,一直到根結點中止。此過程當中,能夠在每一個節點捕捉到相關事件。能夠經過stopPropagation方法終止冒泡。

事件捕獲:和「事件冒泡」相反,從根節點開始執行,一直向子節點傳遞,直到目標節點。

addEventListener給出了第三個參數同時支持冒泡與捕獲:默認是false,事件冒泡;設置爲true時,是事件捕獲。

<div id="app" style="width: 100vw; background: red;">
  <span id="btn">點我</span>
</div>
<script> // 事件捕獲:先輸出 "外層click事件觸發"; 再輸出 "內層click事件觸發" var useCapture = true; var btn = document.getElementById("btn"); btn.addEventListener( "click", function() { console.log("內層click事件觸發"); }, useCapture ); var app = document.getElementById("app"); app.onclick = function() { console.log("外層click事件觸發"); }; </script>
複製代碼

DOM0 級DOM2 級 DOM2 級:前面說的addEventListener,它定義了DOM事件流,捕獲 + 冒泡。

DOM0 級:

直接在 html 標籤內綁定on事件 在 JS 中綁定on系列事件 注意:如今通用DOM2級事件,優勢以下:

能夠綁定 / 卸載事件 支持事件流 冒泡 + 捕獲:至關於每一個節點同一個事件,至少 2 次處理機會 同一類事件,能夠綁定多個函數

ES5 繼承

方法一:綁定構造函數

缺點:不能繼承父類原型方法/屬性

function Animal() {
  this.species = "動物";
}

function Cat() {
  // 執行父類的構造方法, 上下文爲實例對象
  Animal.apply(this, arguments);
}

/** * 測試代碼 */
var cat = new Cat();
console.log(cat.species); // output: 動物
複製代碼

方法二:原型鏈繼承

缺點:沒法向父類構造函數中傳遞參數;子類原型鏈上定義的方法有前後順序問題。 注意:js 中交換原型鏈,均須要修復prototype.constructor指向問題。

function Animal(species) {
  this.species = species;
}
Animal.prototype.func = function() {
  console.log("Animal");
};

function Cat() {}
/** * func方法是無效的, 由於後面原型鏈被從新指向了Animal實例 */
Cat.prototype.func = function() {
  console.log("Cat");
};

Cat.prototype = new Animal();
Cat.prototype.constructor = Cat; // 修復: 將Cat.prototype.constructor從新指向自己

/** * 測試代碼 */
var cat = new Cat();
cat.func(); // output: Animal
console.log(cat.species); // undefined
複製代碼

方法 3:組合繼承

結合綁定構造函數和原型鏈繼承 2 種方式,缺點是:調用了 2 次父類的構造函數。

function Animal(species) {
  this.species = species;
}
Animal.prototype.func = function() {
  console.log("Animal");
};

function Cat() {
  Animal.apply(this, arguments);
}

Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

/** * 測試代碼 */
var cat = new Cat("cat");
cat.func(); // output: Animal
console.log(cat.species); // output: cat
複製代碼

方法4: 寄生組合繼承 改進了組合繼承的缺點,只須要調用 1 次父類的構造函數。這是目前最推薦的繼承方式

/** * 寄生組合繼承的核心代碼 * @param {Function} sub 子類 * @param {Function} parent 父類 */
function inheritPrototype(sub, parent) {
  // 拿到父類的原型
  var prototype = Object.create(parent.prototype);
  // 改變constructor指向
  prototype.constructor = sub;
  // 父類原型賦給子類
  sub.prototype = prototype;
}

function Animal(species) {
  this.species = species;
}
Animal.prototype.func = function() {
  console.log("Animal");
};

function Cat() {
  Animal.apply(this, arguments); // 只調用了1次構造函數
}

inheritPrototype(Cat, Animal);

/** * 測試代碼 */

var cat = new Cat("cat");
cat.func(); // output: Animal
console.log(cat.species); // output: cat
複製代碼

原型和原型鏈

  • 全部的引用類型(數組、對象、函數),都有一個__proto__屬性
  • 全部的函數,都有一個 prototype 屬性,屬性值也是一個普通的對象
  • 全部的引用類型(數組、對象、函數),__proto__屬性值指向它的構造函數的 prototype 屬性值
  • 注:ES6 的箭頭函數沒有prototype屬性,可是有__proto__屬性。

如何理解 JS 中的原型?

原型就是用來繼承屬性和方法的。

當試圖獲得一個對象的某個屬性時,若是這個對象自己沒有這個屬性,那麼會去它的__proto__(即它的構造函數的prototype)中尋找

如何理解 JS 中的原型鏈?

__proto__是每一個對象都有的屬性,由於prototype也是對象,因此他也具備__proto__屬性,__proto__將每一個對象之間串聯起來,造成了鏈條,這就叫作原型鏈。

做用域和做用域鏈

如何理解 JS 的做用域和做用域鏈?

① 做用域

ES5 有」全局做用域「和」函數做用域「。ES6 的let和const使得 JS 用了」塊級做用域「。

② 做用域鏈

當前做用域沒有找到定義,繼續向父級做用域尋找,直至全局做用域。這種層級關係,就是做用域鏈

事件循環

  • 執行一個宏任務(棧中沒有就從事件隊列中獲取)

  • 執行過程當中若是遇到微任務,就將它添加到微任務的任務隊列中

  • 宏任務執行完畢後,當即執行當前微任務隊列中的全部微任務(依次執行)

  • 當前宏任務執行完畢,開始檢查渲染,而後GUI線程接管渲染

  • 渲染完畢後,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)

JS 是單線程的,其上面的全部任務都是在兩個地方執行:執行棧和任務隊列。前者是存放同步任務;後者是異步任務有結果後,就在其中放入一個事件。 當執行棧的任務都執行完了(棧空),js 會讀取任務隊列,並將能夠執行的任務從任務隊列丟到執行棧中執行。 這個過程是循環進行的,因此稱做EventLoop

執行上下文

JS執行上下文分爲全局執行上下文函數執行上下文

①全局執行上下文

解析 JS 時候,建立一個 全局執行上下文 環境。把代碼中即將執行的(內部函數的不算,由於你不知道函數什麼時候執行)變量、函數聲明都拿出來。未賦值的變量就是undefined。

下面這段代碼輸出:undefined;而不是拋出Error。由於在解析 JS 的時候,變量 a 已經存入了全局執行上下文中了。

console.log(a);
var a = 1;
複製代碼

②函數執行上下文

和全局執行上下文差很少,可是多了this和arguments和參數。

在 JS 中,this是關鍵字,它做爲內置變量,其值是在執行的時候肯定(不是定義的時候肯定)。

閉包

定義:外部函數調用以後其變量對象本應該被銷燬,但閉包的存在使咱們仍然能夠訪問外部函數的變量對象,這就是閉包的重要概念

如何解決內存泄漏?

解決方法是顯式對外暴露一個接口,專門用以清理變量:

function mockData() {
  const mem = {};

  return {
    clear: () => (mem = null), // 顯式暴露清理接口

    get: page => {
      if (page in mem) {
        return mem[page];
      }
      mem[page] = Math.random();
    }
  };
}
複製代碼

實現千分位分隔符

一句話版本:

(123456789).toLocaleString('en-US');//"123,456,789"
複製代碼

手寫call

要點:若是一個函數做爲一個對象的屬性,那麼經過對象的.運算符調用此函數,this就是此對象

let obj = {
  a: "a",
  b: "b",
  test: function(arg1, arg2) {
    console.log(arg1, arg2);
    // this.a 就是 a; this.b 就是 b
    console.log(this.a, this.b);
  }
};

obj.test(1, 2);
複製代碼

知道了實現關鍵,下面就是咱們模擬的call:

Function.prototype.call2 = function(context) {
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }

  // 默認上下文是window
  context = context || window;
  // 保存默認的fn
  const { fn } = context;

  // 前面講的關鍵,將函數自己做爲對象context的屬性調用,自動綁定this
  context.fn = this;
  const args = [...arguments].slice(1);
  const result = context.fn(...args);

  // 恢復默認的fn
  context.fn = fn;
  return result;
};

// 如下是測試代碼
function test(arg1, arg2) {
  console.log(arg1, arg2);
  console.log(this.a, this.b);
}

test.call2(
  {
    a: "a",
    b: "b"
  },
  1,
  2
);
複製代碼

實現 apply

apply和call實現相似,只是傳入的參數形式是數組形式,而不是逗號分隔的參數序列。

所以,藉助 es6 提供的...運算符,就能夠很方便的實現數組和參數序列的轉化。

Function.prototype.apply2 = function(context) {
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }

  context = context || window;
  const { fn } = context;

  context.fn = this;
  let result;
  if (Array.isArray(arguments[1])) {
    // 經過...運算符將數組轉換爲用逗號分隔的參數序列
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }

  context.fn = fn;
  return result;
};

/** * 如下是測試代碼 */

function test(arg1, arg2) {
  console.log(arg1, arg2);
  console.log(this.a, this.b);
}

test.apply2(
  {
    a: "a",
    b: "b"
  },
  [1, 2]
);
複製代碼

手寫深拷貝

function deepClone(src, target) {
  const keys = Reflect.ownKeys(src);
  let value = null;

  for (let key of keys) {
    value = src[key];

    if (Array.isArray(value)) {
      target[key] = cloneArr(value, []);
    } else if (typeof value === "object") {
      // 若是是對象並且不是數組, 那麼遞歸調用深拷貝
      target[key] = deepClone(value, {});
    } else {
      target[key] = value;
    }
  }

  return target;
}
複製代碼

手寫EventEmitter

實現思路:這裏涉及了「訂閱/發佈模式」的相關知識。參考addEventListener和removeEventListener的具體效果來實現便可。

// 數組置空:
// arr = []; arr.length = 0; arr.splice(0, arr.length)
class Event {
  constructor() {
    this._cache = {};
  }

  // 註冊事件:若是不存在此種type,建立相關數組
  on(type, callback) {
    this._cache[type] = this._cache[type] || [];
    let fns = this._cache[type];
    if (fns.indexOf(callback) === -1) {
      fns.push(callback);
    }
    return this;
  }

  // 觸發事件:對於一個type中的全部事件函數,均進行觸發
  trigger(type, ...data) {
    let fns = this._cache[type];
    if (Array.isArray(fns)) {
      fns.forEach(fn => {
        fn(...data);
      });
    }
    return this;
  }

  // 刪除事件:刪除事件類型對應的array
  off(type, callback) {
    let fns = this._cache[type];
    // 檢查是否存在type的事件綁定
    if (Array.isArray(fns)) {
      if (callback) {
        // 卸載指定的回調函數
        let index = fns.indexOf(callback);
        if (index !== -1) {
          fns.splice(index, 1);
        }
      } else {
        // 所有清空
        fns = [];
      }
    }
    return this;
  }
}

// 如下是測試函數

const event = new Event();
event
  .on("test", a => {
    console.log(a);
  })
  .trigger("test", "hello");
複製代碼

Promise

  • 三個狀態:pending、fulfilled、rejected
  • Promise實例一旦被建立就會被執行
  • Promise過程分爲兩個分支:pending=>resolved和pending=>rejected
  • Promise狀態改變後,依然會執行以後的代碼:
const warnDemo = ctx => {
  const promise = new Promise(resolve => {
    resolve(ctx);
    console.log("After resolved, but Run"); // 依然會執行這個語句
  });
  return promise;
};

warnDemo("ctx").then(ctx => console.log(`This is ${ctx}`));
複製代碼

Promise性質

狀態只改變一次

Promise 的狀態一旦改變,就永久保持該狀態,不會再變了。

錯誤冒泡

Promise 對象的錯誤具備「冒泡」性質,會一直向後傳遞,直到被捕獲爲止。也就是說,錯誤老是會被下一個catch語句捕獲

"吃掉錯誤"機制

Promise會吃掉內部的錯誤,並不影響外部代碼的運行。因此須要catch,以防丟掉錯誤信息。

async/await

async函數返回一個Promise對象,可使用then方法添加回調函數。

當函數執行的時候,一旦遇到await就會先返回,等到異步操做完成,再接着執行函數體內後面的語句。

這也是它最受歡迎的地方:能讓異步代碼寫起來像同步代碼,而且方便控制順序。

能夠利用它實現一個sleep函數阻塞進程:

function sleep(millisecond) {
  return new Promise(resolve => {
    setTimeout(() => resolve, millisecond);
  });
}

/** * 如下是測試代碼 */
async function test() {
  console.log("start");
  await sleep(1000); // 睡眠1秒
  console.log("end");
}

test(); // 執行測試函數
複製代碼

雖然方便,可是它也不能取代Promise,尤爲是咱們能夠很方便地用Promise.all()來實現併發,而async/await只能實現串行(用很差會產生性能問題哦)。

EsModule 和 CommonJS 的比較

目前 js 社區有 4 種模塊管理規範:AMD、CMD、CommonJS 和 EsModule。 ES Module 是原生實現的模塊化方案,與 CommonJS 有如下幾個區別:

CommonJS 支持動態導入,EsModule目前不支持 CommonJS 是同步導入,由於用於服務端,文件都在本地,同步導入即便卡住主線程影響也不大。而EsModule是異步導入,由於用於瀏覽器,須要下載文件,若是也採用同步導入會對渲染有很大影響 commonJs 輸出的是值的淺拷貝,esModule 輸出值的引用 ES Module 會編譯成 require/exports 來執行的

瀏覽器渲染過程

根據HTML代碼生成DOM樹

根據CSS生成CSSDOM

將 DOM 樹和 CSSOM 整合成 RenderTree

根據 RenderTree 開始渲染和展現

遇到script標籤,會阻塞渲染

爲何遇到script標籤,會阻塞渲染

瀏覽器中常見的線程有:渲染線程、JS 引擎線程、HTTP 線程等等。

例如,當咱們打開一個 Ajax 請求的時候,就啓動了一個 HTTP 線程。

一樣地,咱們能夠用線程的只是解釋:爲何直接操做 DOM 會變慢,性能損耗更大?由於 JS 引擎線程和渲染線程是互斥的。而直接操做 DOM 就會涉及到兩個線程互斥之間的通訊,因此開銷更大。

畢竟 JS 是能夠修改 DOM 的,若是 JS 執行的時候 UI 也工做,就有可能致使不安全(不符合預期)的渲染結果。

onload和DOMContentLoaded觸發的前後順序是什麼?

DOMContentLoaded是在onload前進行的。

DOMContentLoaded事件在 DOM 樹構建完畢後被觸發,咱們能夠在這個階段使用 js 去訪問元素。

async和defer的腳本可能尚未執行。

圖片及其餘資源文件可能還在下載中。

load事件在頁面全部資源被加載完畢後觸發,一般咱們不會用到這個事件,由於咱們不須要等那麼久。

document.addEventListener("DOMContentLoaded", () => {
  console.log("DOMContentLoaded");
});
window.addEventListener("load", () => {
  console.log("load");
});
複製代碼

跨域

  • JSONP:經過script標籤實現,可是隻能實現GET請求
  • 反向代理:nginx的proxy
  • CORS:後端容許跨域資源共享

實現JSONP

// 定義回調函數
const Response = data => {
  console.log(data);
};

// 構造 <script> 標籤
let script = document.createElement("script");
script.src =
  "http://xxx.com?callback=Response";

// 向document中添加 <script> 標籤,而且發送GET請求
document.body.appendChild(script);
複製代碼

AOP 面向切面編程

AOP(面向切面編程)的主要做用是把一些跟核心業務邏輯模塊無關的功能抽離出來,這些跟業務邏輯無關的功能一般包括日誌統計、安全控制、異常處理等。把這些功能抽離出來以後, 再經過「動態織入」的方式摻入業務邏輯模塊中

AOP的好處:

能夠保持業務邏輯模塊的純淨和高內聚性;能夠很方便地複用日誌統計等功能模塊

/** * 織入執行前函數 * @param {*} fn */
Function.prototype.before = function(fn) {
  let self = this; // 保持原函數
  return function() { // 返回包括原函數和新函數的「代理」函數
    fn.apply(this, arguments) // 執行新函數,修正this
    return self.apply(this, arguments) //執行原函數
  }
}
/** * 織入執行後函數 * @param {*} fn */
Function.prototype.after = function(fn) {
  let self = this;
  return function() {
    let current = self.apply(this, arguments) //先保存原函數
    fn.apply(this, arguments) // 先執行新函數
    return current;
  }
}

let func = function() {
  console.log('aop')
}
func = func.before(function(){
  console.log('aop before')
}).after(function(){
  console.log('aop after')
})
func()
// aop before
// aop
// aop after
複製代碼

結語

願你們在金九銀十這個季節能找到滿意的工做!

相關文章
相關標籤/搜索