這裏有一份簡潔的前端知識體系等待你查收,看看吧,會有驚喜哦~若是以爲不錯,懇求star哈~javascript
不論是前端老司機,仍是前端小白,看到標題中列舉的這些概念,想必都是頭大。其實你知道麼?這些概念背後是有聯繫的,理清楚他們的關係,你才能準確且牢靠地記住他們。前端
也只有理清楚這些基本且重要的概念,你才能在前端的道路上越走越遠。java
好了,讓咱們開始吧。git
執行上下文能夠理解爲函數運行的環境。每一個函數執行時,都會給對應的函數建立這樣一個執行環境。github
JS運行環境大概包括三種狀況:全局環境、函數環境、eval環境(不推薦使用,因此不討論)。面試
一個JS程序中,一定會產生多個執行上下文,JS引擎會以棧的方式處理它們,這個棧,咱們稱之爲函數調用棧。棧底永遠都是全局上下文,棧頂就是當前正在執行的上下文。瀏覽器
因爲棧是先進後出的結構,咱們不難推出如下四點:bash
固然,光知道這些仍是不夠,咱們還必須瞭解執行上下文的生命週期。閉包
當調用一個函數時,一個新的執行上下文就會被建立。而一個執行上下文的生命週期能夠分爲兩個階段。app
建立階段
在這個階段中,執行上下文會分別建立變量對象,創建做用域鏈,以及肯定this的指向。
代碼執行階段
建立完成以後,就會開始執行代碼,這個時候,會完成變量賦值,函數引用,以及執行其餘代碼。
至此,咱們終於知道執行上下文跟變量對象、做用域鏈及this的關係。
接下來咱們重點介紹這三個概念。
當一個函數被調用時,執行上下文就建立了,執行上下文包含了函數全部聲明的變量和函數,保存這些變量跟函數的對象,咱們稱之爲變量對象。
變量對象的建立,依次經歷瞭如下幾個過程。
function foo() { console.log('function foo') }
var foo = 20;
console.log(foo); // 20
複製代碼
這是由於上面的三條規則僅僅適用於變量對象的建立過程。也就是執行上下文的建立過程。而foo = 20
是在執行上下文的執行過程當中運行的,輸出結果天然會是20。對比下例。
console.log(foo); // function foo
function foo() { console.log('function foo') }
var foo = 20;
複製代碼
// 上慄的執行順序爲
// 首先將全部函數聲明放入變量對象中
function foo() { console.log('function foo') }
// 其次將全部變量聲明放入變量對象中,可是由於foo已經存在同名函數,所以此時會跳過undefined的賦值
// var foo = undefined;
// 而後開始執行階段代碼的執行
console.log(foo); // function foo
foo = 20;
複製代碼
再看一個例子:
// demo01
function test() {
console.log(a);
console.log(foo());
var a = 1;
function foo() {
return 2;
}
}
test();
複製代碼
咱們直接從test()的執行上下文開始理解。全局做用域中運行test()時,test()的執行上下文開始建立。爲了便於理解,咱們用以下的形式來表示
// 建立過程
testEC = {
// 變量對象
VO: {},
scopeChain: {}
}
// 由於本文暫時不詳細解釋做用域鏈,因此把變量對象專門提出來講明
// VO 爲 Variable Object的縮寫,即變量對象
VO = {
arguments: {...}, //注:在瀏覽器的展現中,函數的參數可能並非放在arguments對象中,這裏爲了方便理解,我作了這樣的處理
foo: <foo reference> // 表示foo的地址引用
a: undefined
}
複製代碼
未進入執行階段以前,變量對象中的屬性都不能訪問!可是進入執行階段以後,變量對象轉變爲了活動對象,裏面的屬性都能被訪問了,而後開始進行執行階段的操做。
變量對象和活動對象其實都是同一個對象,只是處於執行上下文的不一樣生命週期。不過只有處於函數調用棧棧頂的執行上下文中的變量對象,纔會變成活動對象。
// 執行階段
VO -> AO // Active Object
AO = {
arguments: {...},
foo: <foo reference>,
a: 1,
this: Window
}
複製代碼
所以,上面的例子demo1,執行順序就變成了這樣
function test() {
function foo() {
return 2;
}
var a;
console.log(a);
console.log(foo());
a = 1;
}
test();
複製代碼
變量對象講完了,接着是做用域鏈,這裏就不得不先提下做用域。
做用域
做用域最大的用處就是隔離變量,不一樣做用域下同名變量不會有衝突。
JavaScript中只有全局做用域與函數做用域。言外之意是:javascript除了全局做用域以外,只有函數能夠建立的做用域。
JavaScript代碼的整個執行過程,分爲兩個階段,代碼編譯階段與代碼執行階段。
編譯階段由編譯器完成,將代碼翻譯成可執行代碼,這個階段做用域規則會肯定。
執行階段由引擎完成,主要任務是執行可執行代碼,執行上下文在這個階段建立。
理解這點很重要,咱們面試過程當中,常常會被問到「自由變量」的取值問題。
什麼是「自由變量」?先看個例子:
var x = 10;
function fn() {
var b = 20;
console.log(x+b); // x在這裏就是一個自由變量
}
複製代碼
取x的值時,須要到另外一個做用域中取,x就被稱做「自由變量」。
「自由變量」的取值,難倒一片的人,不信,看看下面這個例子:
var x = 10;
function fn() {
console.log(x);
}
function show(f){
var x = 20;
(function () {
f(); // 這裏輸出什麼???
})();
}
show(fn);
複製代碼
你的第一反應是否是20?答案是10!!
其實這個問題很簡單,自由變量要到建立這個函數的那個做用域中取值——是「建立」,而不是「調用」。
爲何呢?由於做用域是在代碼編譯過程就肯定下來的,而後就不會改變,這就是所謂的「靜態做用域」。
本例中,在fn函數取自由變量x的值時,要到哪一個做用域中取?——要到建立fn函數的那個做用域中取——不管fn函數將在哪裏調用。fn明顯是在全局環境下建立的,x明顯就是10。
做用域鏈
上面的例子,只是跨一個做用域去尋找。
若是跨了一步,還沒找到呢?——接着跨!——一直跨到全局做用域爲止。要是在全局做用域中都沒有找到,那就是真的沒有了。
這個一步一步「跨」的路線,咱們稱之爲——做用域鏈。
咱們拿文字總結一下取自由變量時的這個「做用域鏈」過程:(假設a是自由量)
第一步,如今當前做用域查找a,若是有則獲取並結束。若是沒有則繼續;
第二步,若是當前做用域是全局做用域,則證實a未定義,結束;不然繼續;
第三步,(不是全局做用域,那就是函數做用域)將建立該函數的做用域做爲當前做用域;
第四步,跳轉到第一步。
閉包
閉包是一種特殊的對象。
它由兩部分組成。執行上下文(代號A),以及在該執行上下文中建立的函數(代號B)。
當B執行時,若是訪問了A中變量對象中的值,那麼閉包就會產生。
// demo01
function foo() {
var a = 20;
var b = 30;
function bar() {
return a + b;
}
return bar;
}
var bar = foo();
bar();
複製代碼
上面的例子,首先有執行上下文foo,在foo中定義了函數bar,而經過對外返回bar的方式讓bar得以執行。當bar執行時,訪問了foo內部的變量a,b。所以這個時候閉包產生。
閉包的應用場景
除了面試,在實踐中,閉包有兩個很是重要的應用場景。分別是模塊化與柯里化。
this或許是最讓初學者頭疼的概念了吧。this難就難在指向上。
請記住:this的指向,是在函數被調用的時候肯定的,在函數執行過程當中,this一旦被肯定,就不可更改了。
咱們來看看幾種狀況:
全局對象中的this
全局環境中的this,指向它自己。
函數中的this
在一個函數上下文中,this由調用者提供,由調用函數的方式來決定。若是調用者函數,被某一個對象所擁有,那麼該函數在調用時,內部的this指向該對象。若是函數獨立調用,那麼該函數內部的this,則指向undefined。可是在非嚴格模式中,當this指向undefined時,它會被自動指向全局對象。切記,函數執行過程當中,this一旦被肯定,就不可更改。
'use strict';
var a = 20;
function foo () {
var a = 1;
var obj = {
a: 10,
c: this.a + 20,
fn: function () {
return this.a;
}
}
return obj.c;
}
console.log(foo()); // ?
console.log(window.foo()); // ?
複製代碼
執行foo()時,函數獨立調用,因此this指向undefined(由於是嚴格模式),因此執行this.a時報錯。
執行window.foo()時,this.a = 20,結果爲40.
function foo() {
console.log(this.a)
}
function active(fn) {
fn(); // 真實調用者,爲獨立調用
}
var a = 20;
var obj = {
a: 10,
getA: foo
}
active(obj.getA); // 20
複製代碼
使用call,apply顯示指定this
call與applay
構造函數與原型方法上的this
function Person(name, age) {
// 這裏的this指向了誰?
this.name = name;
this.age = age;
}
Person.prototype.getName = function() {
// 這裏的this又指向了誰?
return this.name;
}
// 上面的2個this,是同一個嗎,他們是否指向了原型對象?
var p1 = new Person('Nick', 20);
p1.getName();
複製代碼
this,是在函數調用過程當中肯定,所以,搞明白new的過程當中到底發生了什麼就變得十分重要。
經過new操做符調用構造函數,會經歷如下4個階段。
所以,當new操做符調用構造函數時,this其實指向的是這個新建立的對象,最後又將新的對象返回出來,被實例對象p1接收。所以,咱們能夠說,這個時候,構造函數的this,指向了新的實例對象,p1。
而原型方法上的this就好理解多了,根據上邊對函數中this的定義,p1.getName()中的getName爲調用者,他被p1所擁有,所以getName中的this,也是指向了p1。
本文提到的概念,都是JavaScript中相對晦澀的,平時開發過程當中,要多思考其原理,這是一個必經的階段,只要不斷加深理解,咱們才能真正掌握這些概念,也只有掌握好這些概念,咱們才能在前端的道理上越走越遠。