終於到做用域和閉包這一塊了,這一塊應該是JavaScript
語言裏面最難以學習和理解的了.javascript
也許在平常編碼中會常常接觸到做用域和閉包,可是對於其原理和產生一系列的問題得不到一個深度的瞭解.做爲一個[合格]的前端工程師,這一塊的知識是必定要夯實的.前端
我在整理這一塊的答案時,也從新理解了一遍做用域和閉包的知識,感受對於本身來講,這又是一個提高,但願這篇文章的答案可以和你們共勉.java
原文地址: 一名【合格】前端工程師的自檢清單npm
詞法做用域: 詞法做用域(也就是靜態做用域)就是定義在詞法階段的做用域,是由寫代碼時將變量和塊做用域寫在哪裏來決定的,所以當詞法分析器處理代碼時會保持做用域不變;不管函數在哪裏被調用,也不管它如何被調用,它的詞法做用域都只由函數被聲明時所處的位置決定.編程
動態做用域: 動態做用域並不關心函數和做用域是如何聲明以及在任何處聲明的,只關心它們從何處調用.換句話說,做用域鏈是基於調用棧的,而不是代碼中的做用域嵌套.設計模式
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.
JavaScript
的做用域和做用域鏈讓咱們用一段簡單的代碼來理解一下:
function fn() {
var innerVar = "內部變量";
}
fn();//要先執行這個函數,不然根本不知道里面是啥
console.log(innerVar); // Uncaught ReferenceError: innerVar is not defined
複製代碼
從上面的例子能夠體會到做用域的概念,變量 innerVar
在全局做用域沒有聲明,因此在全局做用域下取值會報錯.
ES6
以前JavaScript
沒有塊級做用域,只有全局做用域和函數做用域,能夠經過let
和const
來實現.
首先認識一下什麼叫作自由變量 .以下代碼中,console.log(a)
要獲得a
變量,可是在當前的做用域中沒有定義 a
(可對比一下b
).當前做用域沒有定義的變量,這成爲自由變量.自由變量的值如何獲得 -- 向父級做用域尋找(注意:這種說法並不嚴謹,下文會重點解釋).
var a = 100
function fn() {
var b = 200
console.log(a) // 這裏的a在這裏就是一個自由變量
console.log(b)
}
fn()
複製代碼
若是父級也沒呢?再一層一層向上尋找,直到找到全局做用域仍是沒找到,就宣佈放棄.這種一層一層的關係,就是做用域鏈.
var a = 100
function fn() {
var b = 200
function fn2() {
var c = 300
console.log(a) // 自由變量,順做用域鏈向父做用域找
console.log(b) // 自由變量,順做用域鏈向父做用域找
console.log(c) // 本做用域的變量
}
fn2()
}
fn()
複製代碼
關於自由變量的值,上文提到要到父做用域中取,其實有時候這種解釋會產生歧義.
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.
JavaScript
的執行上下文棧,能夠應用堆棧信息快速定位問題執行上下文總共有三種類型:
window
對象。this
指針指向這個全局對象。一個程序中只能存在一個全局執行上下文。函數執行上下文: 每次調用函數時,都會爲該函數建立一個新的執行上下文。每一個函數都擁有本身的執行上下文,可是隻有在函數被調用的時候纔會被建立。一個程序中能夠存在任意數量的函數執行上下文。每當一個新的執行上下文被建立,它都會按照特定的順序執行一系列步驟。
Eval
函數執行上下文: 運行在eval
函數中的代碼也得到了本身的執行上下文,但因爲eval
函數不建議使用,因此在這裏再也不討論。
執行棧,在其餘編程語言中也被叫作調用棧,具備 LIFO
(後進先出)結構,用於存儲在代碼執行期間建立的全部執行上下文。 當 JavaScript
引擎首次讀取你的腳本時,它會建立一個全局執行上下文並將其推入當前的執行棧。每當發生一個函數調用,引擎都會爲該函數建立一個新的執行上下文並將其推到當前執行棧的頂端。 引擎會運行執行上下文在執行棧頂端的函數,當此函數運行完成後,其對應的執行上下文將會從執行棧中彈出,上下文控制權將移到當前執行棧的下一個執行上下文。
this
的原理以及幾種不一樣使用場景的取值this
既不指向函數自身,也不指函數的詞法做用域。若是僅經過 this
的英文解釋,太容易產生誤導了。它實際是在函數被調用時才發生的綁定,也就是說 this
具體指向什麼,取決於你是怎麼調用的函數。
this
的 4 種綁定規則分別是:默認綁定、隱式綁定、顯式綁定、new
綁定。優先級從低到高。
什麼叫默認綁定,即沒有其餘綁定規則存在時的默認規則。這也是函數調用中最經常使用的規則。 來看這段代碼:
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
複製代碼
除了直接對函數進行調用外,有些狀況是,函數的調用是在某個對象上觸發的,即調用位置上存在上下文對象。
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
的調用存在上下文對象obj
,this
進行了隱式綁定,即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
函數調用 這裏調用鏈不僅一層,存在obj1
、obj2
兩個對象,那麼隱式綁定具體會綁哪一個對象。這裏原則是獲取最後一層調用的上下文對象,即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
的參與。這裏依舊進行的是默認綁定。
相對隱式綁定,this
值在調用過程當中會動態變化,但是咱們就想綁定指定的對象,這時就用到了顯式綁定。
顯式綁定主要是經過改變對象的prototype
關聯對象,這裏不展開講。具體使用上,能夠經過這兩個方法call
或apply
來實現(大多數函數及本身建立的函數默認都提供這兩個方法)。
call
與apply
是一樣的做用,區別只是其餘參數的設置上.
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.a
和 obj2.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
上,對於bar
,function(){…}
中的this
確實被綁定到了obj2
,而foo
由於經過foo.call(obj1)
已經顯示綁定了obj1
,因此在foo
函數內,this
指向的是obj1
,不會由於bar
函數內指向obj2
而改變自身。因此打印的是obj1.a
(即3)。
js
中的new
操做符,和其餘語言中(如JAVA
)的new
機制是不同的。js
中,它就是一個普通函數調用,只是被new
修飾了而已。
使用new來調用函數,會自動執行以下操做:
JavaScrip
t對象(即{}
);this
的上下文 ;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
綁定取決於外層(函數或全局)做用域。
閉包的概念:指有權訪問另外一個函數做用域中的變量的函數,通常狀況就是在一個函數中包含另外一個函數。
閉包的做用:訪問函數內部變量、保持函數在環境中一直存在,不會被垃圾回收機制處理.
閉包的優勢:
閉包的缺點: 由於使用閉包,可使函數在執行完後不被銷燬,保留在內存中,若是大量使用閉包就會形成內存泄露,內存消耗很大
防抖和節流就是典型的閉包實際應用,還有IIFE
也是一個閉包
內存泄露:是指申請的內存執行完後沒有及時的清理或者銷燬,佔用空閒內存,內存泄露過多的話,就會致使後面的程序申請不到內存。所以內存泄露會致使內部內存溢出
堆棧溢出:是指內存空間已經被申請完,沒有足夠的內存提供了
常見的內存泄露的緣由
解決方法
let flag = 0;
for(let i = 0; i < len; i++) {
flag++;
Database.save_method().exec().then((data) => {
if(flag === len) {
// your code
}
})
}
複製代碼
new Promise(function(resolve){
resolve()
}).then(()=> {
for(let i = 0; i < len; i++) {
Database.save_method().exec()
}
}).then(() => {
// your code
})
複製代碼
function loop(i){
i++;
Database.save_method().exec().then(() => {
loop(i)
})
}
複製代碼
async
和await
(注意: 不能在forEach
中使用await
)async function loop() {
for(let i = 0; i < len; i++) {
await Database.save_method().exec();
}
}
複製代碼
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)
複製代碼
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
複製代碼
// 定義AMD規範的模塊
define([function() {
return 模塊
})
複製代碼
區別於CommonJS
,AMD
規範的被依賴模塊是異步加載的,而定義的模塊是被看成回調函數來執行的,依賴於require.js
模塊管理工具庫。固然,AMD
規範不是採用匿名函數自調用的方式來封裝,咱們依然能夠利用閉包的原理來實現模塊的私有成員和公有成員:
define(['module1', 'module2'], function(m1, m2) {
let x = 1;
function add() {
x += 1;
return x;
}
return { add };
})
複製代碼
CMD
是 SeaJS
在推廣過程當中對模塊定義的規範化產出。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
集成了CommonJS
和AMD
的特色,支持同步和異步加載模塊。CMD
加載完某個依賴模塊後並不執行,只是下載而已,在全部依賴模塊加載完成後進入主邏輯,遇到require
語句的時候才執行對應的模塊,這樣模塊的執行順序和書寫順序是徹底一致的。所以,在CMD
中require
函數同步加載模塊時沒有HTTP
請求過程。
ES6 module
ES6
的模塊化已經不是規範了,而是JS
語言的特性。隨着ES6
的推出,AMD
和CMD
也隨之成爲了歷史。ES6
模塊與模塊化規範相比,有兩大特色:
ES6
模塊輸出的是值的引用。ES6
模塊是編譯時輸出接口。模塊化規範輸出的是一個對象,該對象只有在腳本運行完纔會生成。而 ES6
模塊不是對象,ES6 module
是一個多對象輸出,多對象加載的模型。從原理上來講,模塊化規範是匿名函數自調用的封裝,而ES6 module
則是用匿名函數自調用去調用輸出的成員。
由於時間和篇幅有限,因此每一項列舉的答案都不算特別詳細.
若是有這須要的同窗歡迎給我留言,我能夠另開文章,詳細講一講其中具體的部分.
固然也能夠多看看《JavaScript高級程序設計》,基礎纔是重中之重啊.
系列連接: