一名【合格】前端工程師的自檢清單 - 答案版(做用域和閉包)

終於到做用域和閉包這一塊了,這一塊應該是JavaScript語言裏面最難以學習和理解的了.javascript

也許在平常編碼中會常常接觸到做用域和閉包,可是對於其原理和產生一系列的問題得不到一個深度的瞭解.做爲一個[合格]的前端工程師,這一塊的知識是必定要夯實的.前端

我在整理這一塊的答案時,也從新理解了一遍做用域和閉包的知識,感受對於本身來講,這又是一個提高,但願這篇文章的答案可以和你們共勉.java

原文地址: 一名【合格】前端工程師的自檢清單npm

1.理解詞法做用域和動態做用域

  • 詞法做用域: 詞法做用域(也就是靜態做用域)就是定義在詞法階段的做用域,是由寫代碼時將變量和塊做用域寫在哪裏來決定的,所以當詞法分析器處理代碼時會保持做用域不變;不管函數在哪裏被調用,也不管它如何被調用,它的詞法做用域都只由函數被聲明時所處的位置決定.編程

  • 動態做用域: 動態做用域並不關心函數和做用域是如何聲明以及在任何處聲明的,只關心它們從何處調用.換句話說,做用域鏈是基於調用棧的,而不是代碼中的做用域嵌套.設計模式

  • JavaScript採用詞法做用域.promise

舉個例子:瀏覽器

var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar();

// 結果是 ???
複製代碼

假設JavaScript採用靜態做用域,讓咱們分析下執行過程:前端工程師

執行 foo 函數,先從 foo 函數內部查找是否有局部變量 value,若是沒有,就根據書寫的位置,查找上面一層的代碼,也就是 value 等於 1,因此結果會打印 1.閉包

假設JavaScript採用動態做用域,讓咱們分析下執行過程:

執行 foo 函數,依然是從 foo 函數內部查找是否有局部變量 value.若是沒有,就從調用函數的做用域,也就是 bar 函數內部查找 value 變量,因此結果會打印2.

前面咱們已經說了,JavaScript採用的是靜態做用域,因此這個例子的結果是1.

2.理解JavaScript的做用域和做用域鏈

  • 做用域就是一個獨立的地盤,讓變量不會外泄、暴露出去.

讓咱們用一段簡單的代碼來理解一下:

function fn() {
    var innerVar = "內部變量";
}
fn();//要先執行這個函數,不然根本不知道里面是啥
console.log(innerVar); // Uncaught ReferenceError: innerVar is not defined
複製代碼

從上面的例子能夠體會到做用域的概念,變量 innerVar在全局做用域沒有聲明,因此在全局做用域下取值會報錯.

ES6以前JavaScript沒有塊級做用域,只有全局做用域和函數做用域,能夠經過letconst來實現.

  • 做用域鏈
  1. 什麼是自由變量

首先認識一下什麼叫作自由變量 .以下代碼中,console.log(a)要獲得a變量,可是在當前的做用域中沒有定義 a(可對比一下b).當前做用域沒有定義的變量,這成爲自由變量.自由變量的值如何獲得 -- 向父級做用域尋找(注意:這種說法並不嚴謹,下文會重點解釋).

var a = 100
function fn() {
    var b = 200
    console.log(a) // 這裏的a在這裏就是一個自由變量
    console.log(b)
}
fn()
複製代碼
  1. 什麼是做用域鏈

若是父級也沒呢?再一層一層向上尋找,直到找到全局做用域仍是沒找到,就宣佈放棄.這種一層一層的關係,就是做用域鏈.

var a = 100
function fn() {
    var b = 200
    function fn2() {
        var c = 300
        console.log(a) // 自由變量,順做用域鏈向父做用域找
        console.log(b) // 自由變量,順做用域鏈向父做用域找
        console.log(c) // 本做用域的變量
    }
    fn2()
}
fn()
複製代碼
  1. 關於自由變量的取值

關於自由變量的值,上文提到要到父做用域中取,其實有時候這種解釋會產生歧義.

var x = 10
function fn() {
  console.log(x)
}
function show(f) {
  var x = 20
  (function() {
    f() //10,而不是20
  })()
}
show(fn)
複製代碼

在 fn 函數中,取自由變量x的值時,要到哪一個做用域中取?

要到建立fn函數的那個做用域中取,不管fn函數將在哪裏調用.

因此,不要再用以上說法了.相比而言,用這句話描述會更加貼切:要到建立這個函數的那個域. 做用域中取值,這裏強調的是「建立」,而不是「調用」,其實這就是所謂的靜態做用域.

再看一個例子:

var a = 10
function fn() {
  var b = 20
  function bar() {
    console.log(a + b) //30
  }
  return bar
}
var x = fn(),
  b = 200
x() //bar()
複製代碼

fn()返回的是bar函數,賦值給x.執行x(),即執行bar函數代碼.取b的值時,直接在fn做用域取出.取a的值時,試圖在fn做用域取,可是取不到,只能轉向建立fn的那個做用域中去查找,結果找到了,因此最後的結果是30.

3.理解JavaScript的執行上下文棧,能夠應用堆棧信息快速定位問題

執行上下文總共有三種類型:

  • 全局執行上下文: 這是默認的、最基礎的執行上下文。不在任何函數中的代碼都位於全局執行上下文中。它作了兩件事:
  1. 建立一個全局對象,在瀏覽器中這個全局對象就是 window 對象。
  2. this 指針指向這個全局對象。一個程序中只能存在一個全局執行上下文。
  • 函數執行上下文: 每次調用函數時,都會爲該函數建立一個新的執行上下文。每一個函數都擁有本身的執行上下文,可是隻有在函數被調用的時候纔會被建立。一個程序中能夠存在任意數量的函數執行上下文。每當一個新的執行上下文被建立,它都會按照特定的順序執行一系列步驟。

  • Eval函數執行上下文: 運行在eval 函數中的代碼也得到了本身的執行上下文,但因爲eval函數不建議使用,因此在這裏再也不討論。

執行棧,在其餘編程語言中也被叫作調用棧,具備 LIFO(後進先出)結構,用於存儲在代碼執行期間建立的全部執行上下文。 當 JavaScript 引擎首次讀取你的腳本時,它會建立一個全局執行上下文並將其推入當前的執行棧。每當發生一個函數調用,引擎都會爲該函數建立一個新的執行上下文並將其推到當前執行棧的頂端。 引擎會運行執行上下文在執行棧頂端的函數,當此函數運行完成後,其對應的執行上下文將會從執行棧中彈出,上下文控制權將移到當前執行棧的下一個執行上下文。

4.this的原理以及幾種不一樣使用場景的取值

this 既不指向函數自身,也不指函數的詞法做用域。若是僅經過 this 的英文解釋,太容易產生誤導了。它實際是在函數被調用時才發生的綁定,也就是說 this 具體指向什麼,取決於你是怎麼調用的函數。

this 的 4 種綁定規則分別是:默認綁定隱式綁定顯式綁定new 綁定。優先級從低到高。

  1. 默認綁定:

什麼叫默認綁定,即沒有其餘綁定規則存在時的默認規則。這也是函數調用中最經常使用的規則。 來看這段代碼:

function foo() { 
    console.log( this.a );
}
var a = 2;
foo(); //打印的是什麼?
複製代碼

foo() 打印的結果是2。

由於foo()是直接調用的(獨立函數調用),沒有應用其餘的綁定規則,這裏進行了默認綁定,將全局對象綁定 this 上,因此 this.a 就解析成了全局變量中的 a ,即2。

注意:在嚴格模式下(strict mode),全局對象將沒法使用默認綁定,即執行會報undefined的錯誤

function foo() { 
 "use strict";
    console.log(this.a);
}
var a = 2; 
foo(); // Uncaught TypeError: Cannot read property 'a' of undefined
複製代碼
  1. 隱式綁定:

除了直接對函數進行調用外,有些狀況是,函數的調用是在某個對象上觸發的,即調用位置上存在上下文對象。

function foo() { 
    console.log(this.a);
}
var a = 2;
var obj = { 
    a: 3,
    foo: foo 
};
obj.foo(); // ?
複製代碼

obj.foo() 打印的結果是3。

這裏foo函數被當作引用屬性,被添加到obj對象上。這裏的調用過程是這樣的: 獲取obj.foo屬性 -> 根據引用關係找到foo函數,執行調用 因此這裏對foo的調用存在上下文對象objthis進行了隱式綁定,即this綁定到了obj上,因此this.a被解析成了obj.a,即3。

  • 多層調用鏈
function foo() { 
    console.log(this.a);
}
var a = 2;
var obj1 = { 
    a: 4,
    foo: foo 
};
var obj2 = { 
    a: 3,
    obj1: obj1
};
obj2.obj1.foo(); //? 
複製代碼

obj2.obj1.foo() 打印的結果是3。

一樣,咱們看下函數的調用過程: 先獲取obj1.obj2 -> 經過引用獲取到obj2對象,再訪問 obj2.foo -> 最後執行foo函數調用 這裏調用鏈不僅一層,存在obj1obj2兩個對象,那麼隱式綁定具體會綁哪一個對象。這裏原則是獲取最後一層調用的上下文對象,即obj2,因此結果顯然是4(obj2.a)。

  • 隱式丟失(函數別名)

注意:這裏存在一個陷阱,你們在分析調用過程時,要特別當心

先看個代碼:

function foo() { 
    console.log(this.a);
}
var a = 2;
var obj = { 
    a: 3,
    foo: foo 
};
var bar = obj.foo;
bar(); //? 
複製代碼

bar() 打印的結果是2。

爲何會這樣,obj.foo 賦值給bar,那調用bar()爲何沒有觸發隱式綁定,使用的是默認綁定呢。 這裏有個概念要理解清楚,obj.foo 是引用屬性,賦值給bar的實際上就是foo函數(即:bar指向foo自己)。

那麼,實際的調用關係是:經過bar找到foo函數,進行調用。整個調用過程並無obj的參數,因此是默認綁定,全局屬性a

  • 隱式丟失(回調函數)
function foo() { 
    console.log(this.a);
}
var a = 2;
var obj = { 
    a: 3,
    foo: foo 
};
setTimeout(obj.foo, 100); // ? 
複製代碼

打印的結果是2。

一樣的道理,雖然參傳是obj.foo,由於是引用關係,因此傳參實際上傳的就是foo對象自己的引用。對於setTimeout的調用,仍是 setTimeout -> 獲取參數中foo的引用參數 -> 執行 foo 函數,中間沒有obj的參與。這裏依舊進行的是默認綁定。

  1. 顯式綁定:

相對隱式綁定,this值在調用過程當中會動態變化,但是咱們就想綁定指定的對象,這時就用到了顯式綁定。

顯式綁定主要是經過改變對象的prototype關聯對象,這裏不展開講。具體使用上,能夠經過這兩個方法callapply來實現(大多數函數及本身建立的函數默認都提供這兩個方法)。

callapply是一樣的做用,區別只是其餘參數的設置上.

function foo() { 
    console.log(this.a);
}
var a = 2;
var obj1 = { 
    a: 3,
};
var obj2 = { 
    a: 4,
};
foo.call(obj1); // ?
foo.call(obj2); // ? 
複製代碼

打印的結果是3, 4。

這裏由於顯示的申明瞭要綁定的對象,因此this就被綁定到了obj上,打印的結果天然就是obj1.aobj2.a

  • 硬綁定:
function foo() { 
    console.log(this.a);
}
var a = 2;
var obj1 = { 
    a: 3,
};
var obj2 = { 
    a: 4,
};
var bar = function(){
    foo.call(obj1);
}
setTimeout(bar, 100); // 3
bar.call(obj2); // 這是多少
複製代碼

前面兩個(函數別名、回調函數)打印3,由於顯示綁定了,沒什麼問題。

最後一個打印是3。

這裏須要注意下,雖然bar被顯示綁定到obj2上,對於barfunction(){…} 中的this確實被綁定到了obj2,而foo由於經過foo.call(obj1)已經顯示綁定了obj1,因此在foo函數內,this指向的是obj1,不會由於bar函數內指向obj2而改變自身。因此打印的是obj1.a(即3)。

  1. new綁定:

js中的new操做符,和其餘語言中(如JAVA)的new機制是不同的。js中,它就是一個普通函數調用,只是被new修飾了而已。

使用new來調用函數,會自動執行以下操做:

  1. 建立一個空的簡單JavaScript對象(即{});
  2. 連接該對象(即設置該對象的構造函數)到另外一個對象 ;
  3. 將步驟1新建立的對象做爲this的上下文 ;
  4. 若是該函數沒有返回對象,則返回this

從第三點能夠看出,this指向的就是對象自己。 看個代碼:

function foo(a) { 
    this.a = a;
}
var a = 2;
var bar1 = new foo(3);
console.log(bar1.a); // ?
var bar2 = new foo(4);
console.log(bar2.a); // ?
複製代碼

最後一個打印是3, 4。

由於每次調用生成的是全新的對象,該對象又會自動綁定到this上,因此答案顯而易見。

最後要注意箭頭函數,它的this綁定取決於外層(函數或全局)做用域。

5.閉包的實現原理和做用,能夠列舉幾個開發中閉包的實際應用

  • 閉包的概念:指有權訪問另外一個函數做用域中的變量的函數,通常狀況就是在一個函數中包含另外一個函數。

  • 閉包的做用:訪問函數內部變量、保持函數在環境中一直存在,不會被垃圾回收機制處理.

  • 閉包的優勢:

    1. 方便調用上下文中聲明的局部變量
    2. 邏輯緊密,能夠在一個函數中再建立個函數,避免了傳參的問題
  • 閉包的缺點: 由於使用閉包,可使函數在執行完後不被銷燬,保留在內存中,若是大量使用閉包就會形成內存泄露,內存消耗很大

防抖和節流就是典型的閉包實際應用,還有IIFE也是一個閉包

6.理解堆棧溢出和內存泄漏的原理,如何防止

  • 內存泄露:是指申請的內存執行完後沒有及時的清理或者銷燬,佔用空閒內存,內存泄露過多的話,就會致使後面的程序申請不到內存。所以內存泄露會致使內部內存溢出

  • 堆棧溢出:是指內存空間已經被申請完,沒有足夠的內存提供了

  • 常見的內存泄露的緣由

    1. 全局變量引發的內存泄露
    2. 閉包
    3. 沒有被清除的計時器
  • 解決方法

    1. 減小沒必要要的全局變量
    2. 減小閉包的使用(由於閉包會致使內存泄露)
    3. 避免死循環的發生

7.如何處理循環的異步操做

  1. 如何確保循環的全部異步操做完成以後執行某個其餘操做
  • 方法一:設置一個flag,在每一個異步操做中對flag進行檢測
let flag = 0;
for(let i = 0; i < len; i++) {
    flag++;
    Database.save_method().exec().then((data) => {
        if(flag === len) {
            // your code
        }
    })
}
複製代碼
  • 方法二:將全部的循環放在一個promise中,使用then處理
new Promise(function(resolve){
    resolve()
}).then(()=> {
    for(let i = 0; i < len; i++) {
        Database.save_method().exec()
    }
}).then(() => {
    // your code
})
複製代碼
  1. 循環中的下一步操做依賴於前一步的操做,如何解決
  • 方法一:使用遞歸,在異步操做完成以後調用下一次異步操做
function loop(i){
    i++;
    Database.save_method().exec().then(() => {
        loop(i)
    })
}
複製代碼
  • 方法二:使用asyncawait(注意: 不能在forEach中使用await)
async function loop() {
    for(let i = 0; i < len; i++) {
        await Database.save_method().exec();
    }
}
複製代碼

8.理解模塊化解決的實際問題,可列舉幾個模塊化方案並理解其中原理

  1. Module模式

在模塊化規範造成以前,JS開發者使用Module設計模式來解決JS全局做用域的污染問題。Module模式最初被定義爲一種在傳統軟件工程中爲類提供私有和公有封裝的方法。在JavaScript中,Module模式使用匿名函數自調用 (閉包)來封裝,經過自定義暴露行爲來區分私有成員和公有成員。

let myModule = (function (window) {
    let moduleName = 'module'  // private
    // public
    function setModuleName(name) {
      moduleName = name
    }
    // public
    function getModuleName() {
      return moduleName
    }
    return { setModuleName, getModuleName }  // 暴露行爲
})(window)
複製代碼
  1. CommonJS

CommonJS主要用在Node開發上,每一個文件就是一個模塊,沒個文件都有本身的一個做用域。經過module.exports暴露public成員。例如:

// 文件名:x.js
let x = 1;
function add() {
  x += 1;
  return x;
}
module.exports.x = x;
module.exports.add = add;
複製代碼

此外,CommonJS經過require()引入模塊依賴,require函數能夠引入Node的內置模塊、自定義模塊和npm等第三方模塊。

// 文件名:main.js
let xm = require('./x.js');
console.log(xm.x);  // 1
console.log(xm.add());  // 2
console.log(xm.x);   // 1
複製代碼
  1. AMD
// 定義AMD規範的模塊
define([function() {
  return 模塊
})
複製代碼

區別於CommonJSAMD規範的被依賴模塊是異步加載的,而定義的模塊是被看成回調函數來執行的,依賴於require.js模塊管理工具庫。固然,AMD規範不是採用匿名函數自調用的方式來封裝,咱們依然能夠利用閉包的原理來實現模塊的私有成員和公有成員:

define(['module1', 'module2'], function(m1, m2) {
  let x = 1;
  function add() {
    x += 1;
    return x;
  }
  return { add };
})
複製代碼
  1. CMD

CMDSeaJS 在推廣過程當中對模塊定義的規範化產出。AMD 推崇依賴前置,CMD 推崇依賴就近。

define(function(require, exports, module) {
  // 同步加載模塊
  var a = require('./a');
  a.doSomething();
  // 異步加載一個模塊,在加載完成時,執行回調
  require.async(['./b'], function(b) {
    b.doSomething();
  });
  // 對外暴露成員
  exports.doSomething = function() {};
});
// 使用模塊
seajs.use('path');
複製代碼

CMD集成了CommonJSAMD的特色,支持同步和異步加載模塊。CMD加載完某個依賴模塊後並不執行,只是下載而已,在全部依賴模塊加載完成後進入主邏輯,遇到require語句的時候才執行對應的模塊,這樣模塊的執行順序和書寫順序是徹底一致的。所以,在CMDrequire函數同步加載模塊時沒有HTTP請求過程。

  1. ES6 module

ES6的模塊化已經不是規範了,而是JS語言的特性。隨着ES6的推出,AMDCMD也隨之成爲了歷史。ES6模塊與模塊化規範相比,有兩大特色:

  • 模塊化規範輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
  • 模塊化規範是運行時加載,ES6 模塊是編譯時輸出接口。

模塊化規範輸出的是一個對象,該對象只有在腳本運行完纔會生成。而 ES6 模塊不是對象,ES6 module 是一個多對象輸出,多對象加載的模型。從原理上來講,模塊化規範是匿名函數自調用的封裝,而ES6 module則是用匿名函數自調用去調用輸出的成員。

結語

由於時間和篇幅有限,因此每一項列舉的答案都不算特別詳細.

若是有這須要的同窗歡迎給我留言,我能夠另開文章,詳細講一講其中具體的部分.

固然也能夠多看看《JavaScript高級程序設計》,基礎纔是重中之重啊.

系列連接:

相關文章
相關標籤/搜索