堆棧結(jié)構(gòu)
執(zhí)行上下文(運(yùn)行環(huán)境)
運(yùn)行環(huán)境規(guī)定函數(shù)中代碼執(zhí)行的順序,JavaScript 引擎會以棧的方式來處理他們,在打開瀏覽器,存在JavaScript 的情況下,全局上下文首先入棧,在全局上下文環(huán)境下執(zhí)行其中可執(zhí)行的代碼,在遇到可執(zhí)行的函數(shù)時(shí)該函數(shù)會創(chuàng)建一個(gè)單獨(dú)的執(zhí)行上下文環(huán)境,(函數(shù)只有在被調(diào)用時(shí)才會執(zhí)行新的上下文環(huán)境)該函數(shù)執(zhí)行上下文入棧,運(yùn)行該函數(shù)內(nèi)的代碼,在該函數(shù)內(nèi)代碼執(zhí)行完畢后該函數(shù)上下文從棧中彈出,如果在函數(shù)中存在return能直接終止可執(zhí)行代碼的執(zhí)行,因此直接將當(dāng)前上下文彈出棧,而全局上下文會在瀏覽器關(guān)閉后出棧
預(yù)解析
對象部分
所有變量的聲明,在函數(shù)內(nèi)部第一行代碼開始執(zhí)行的時(shí)候就已經(jīng)完成
執(zhí)行上下文可以分為兩個(gè)周期,創(chuàng)建階段和代碼執(zhí)行階段
在創(chuàng)建階段執(zhí)行上下文會分別創(chuàng)建變量對象、建立作用域鏈,以及確定this的指向
在代碼執(zhí)行階段會完成變量賦值、函數(shù)引用,以及執(zhí)行其他代碼,在執(zhí)行完畢后彈出棧,然后看是否會被其它函數(shù)引用,否則會等待被回收
變量對象的創(chuàng)建
- 創(chuàng)建arguments對象,檢查當(dāng)前上下文中的參數(shù),建立該對象下的屬性與屬性值
- 檢查當(dāng)前上下文的函數(shù)聲明(使用function關(guān)鍵字聲明的函數(shù)),在變量對象中以函數(shù)名建立一個(gè)屬性,屬性值為指向該函數(shù)所在內(nèi)存地址的引用,如果函數(shù)名的屬性已經(jīng)存在,那么該屬性將會被新的引用覆蓋
- 檢查當(dāng)前上下文中的變量聲明(var),每找到一個(gè)變量聲明,就在變量對象中以變量名建立一個(gè)屬性,屬性值為undefined,如果該變量名的屬性已經(jīng)存在,為了防止同名的函數(shù)被修改為undefined,會直接跳過,原屬性值不會被修改
這里需要注意幾個(gè)點(diǎn),首先是變量對象的創(chuàng)建是在每一個(gè)函數(shù)的執(zhí)行上下文環(huán)境中,也就是說全局的上下文執(zhí)行環(huán)境中的變量和單獨(dú)函數(shù)中的變量是在兩個(gè)不同的執(zhí)行上下文環(huán)境,不會相互影響,在局部函數(shù)中聲明變量只會存在該函數(shù)的執(zhí)行上下文環(huán)境中
第二是使用function聲明的優(yōu)先級會大于使用var聲明的變量的優(yōu)先級,預(yù)解析時(shí)會優(yōu)先讀取function聲明,并且該值會覆蓋之前使用var/function聲明的同名變量/函數(shù),然后再讀取var聲明的匿名函數(shù)/變量,但是如果存在同名的function聲明,則會直接跳過,不會修改function的聲明
console.log(a());//this's b
function a() {
return "this's a"
}
function a() {
return "this's b"
}
還有一點(diǎn)需要注意的是上面的規(guī)則只是變量對象的創(chuàng)建過程的規(guī)則,在執(zhí)行過程中是不會受上面規(guī)則的影響的,只會依照聲明的先后順序來決定之后該變量的值
console.log(foo);//? foo() {
//console.log("this's foo");
//}
function foo() {
console.log("this's foo");
}
var foo="foo";
console.log(foo);//foo

而全局變量對象,它的變量對象就是window對象,this的指向也是window對象
垃圾回收機(jī)制
JavaScript的垃圾回收機(jī)制中注意一點(diǎn),局部變量會在上下文彈出棧時(shí)被垃圾回收機(jī)制回收,而全局變量則不確定什么時(shí)候會被回收,所以我們要謹(jǐn)慎使用全局變量,在確定全局變量不再被引用時(shí)手動將其賦值為null,讓其進(jìn)入垃圾回收,這樣可以節(jié)省內(nèi)存
作用域鏈
作用域鏈,由當(dāng)前環(huán)境與上層環(huán)境的一系列變量對象組成,它保證了當(dāng)前執(zhí)行環(huán)境對符合訪問權(quán)限的變量和函數(shù)的有序訪問,JavaScript的作用域是由變量創(chuàng)建的位置決定的,而不是變量使用的位置,
作用域鏈保證了函數(shù)可以訪問的數(shù)據(jù)有哪些,注意這個(gè)作用域鏈由函數(shù)定義的位置決定,而不是由函數(shù)使用時(shí)的位置決定,閉包也是一樣,閉包在被創(chuàng)建后在即使在其它位置使用但其作用域鏈還是原先創(chuàng)建閉包時(shí)所形成的作用域鏈
var fn = null;
function foo() {
var a = 2;
function innnerFoo() {
console.log(c); // 在這里,試圖訪問函數(shù)bar中的c變量,會拋出錯(cuò)誤
console.log(a);
}
fn = innnerFoo; // 將 innnerFoo的引用,賦值給全局變量中的fn
}
function bar() {
var c = 100;
fn(); // 此處的保留的innerFoo的引用
}
foo();//先執(zhí)行foo,為fn賦值,也就是將fn由變量對象轉(zhuǎn)化為活動對象
bar();
var name = "stephenchan";
function callMePlz() {
console.log(name);
}
function myFunc() {
var name = "endlesscode";
callMePlz();
}
myFunc();//stephenchan
在記住上面的前提下,我們再深入談一下作用域鏈,在JavaScript中,萬物皆對象,雖然在某些時(shí)候這個(gè)說話并不準(zhǔn)確,但說函數(shù)是一個(gè)對象這是沒有問題的,函數(shù)對象和其他對象一樣,擁有可以通過代碼訪問的屬性和一系列JavaScript引擎自身的內(nèi)部屬性,其中一個(gè)內(nèi)部屬性是[scope],這個(gè)屬性是JavaScript提供的,該屬性包含了函數(shù)被創(chuàng)建的作用中對象的集合,這個(gè)集合被稱為函數(shù)的作用域鏈,它決定了哪些數(shù)據(jù)能夠被函數(shù)訪問
當(dāng)一個(gè)函數(shù)被創(chuàng)建后,它的作用域鏈會被創(chuàng)建該函數(shù)的作用域中可訪問的數(shù)據(jù)對象填充,我們舉一個(gè)栗子
function add(num1,num2){
var sum=num1+num2;
return sum;
}
在函數(shù)add創(chuàng)建時(shí),因?yàn)樗且粋€(gè)全局對象中的函數(shù),按照我們上面的說法,它的作用域鏈會被全局對象中可訪問的對象填充,這些可訪問的對象包括window等JavaScript本身定義的屬性/方法,也包括我們自己在代碼中定義的全局對象/方法(下圖只是一部分全局變量)

當(dāng)我們在函數(shù)add執(zhí)行時(shí)
var total=add(5,10)
然后在執(zhí)行函數(shù)時(shí),在遇到每一變量時(shí)都會給變量一個(gè)標(biāo)識符,用于識別該變量,但為了便于理解,這里我們就跳過這個(gè)概念,會讀取add函數(shù)的所有局部變量,命名參數(shù),參數(shù)集合以及this,將它們共同組成一個(gè)新的對象,叫"活動對象(activation object)",該對象將被推入到作用域鏈的前端,當(dāng)上下文環(huán)境被銷毀,活動對象也就會隨之銷毀
執(zhí)行函數(shù)add時(shí)作用域鏈如下

在函數(shù)執(zhí)行過程中,每次遇到一個(gè)變量,都會從作用域鏈頭部也就是活動對象開始搜索,如果在活動對象內(nèi)存在同名變量,那么就使用該變量,如果沒有找到那么會繼續(xù)向作用域鏈下一個(gè)對象中查找,如果都沒有,則認(rèn)為該標(biāo)識符未定義
所以,這也解釋了JavaScript中函數(shù)會有塊的作用域,外界訪問不到,而子作用域中可以訪問父作用域中定義的變量,但是父作用域不能訪問子作用域中的變量,因?yàn)樽宰饔糜蛟谧饔糜蜴溕洗嬖诟缸饔糜虻膶ο螅诓檎易兞繒r(shí)如果在自作用域活動對象內(nèi)查找不到會向下去找父作用域?qū)ο髢?nèi)是否存在同名的變量,但在父作用域內(nèi)的作用域鏈上是不存在子函數(shù)的作用域鏈的,而全局對象的作用域鏈上時(shí)不存在我們自己定義的函數(shù)的對象的,所以我們在函數(shù)內(nèi)定義的變量在外界是無法讀取的

事件循環(huán)機(jī)制(任務(wù)隊(duì)列)
JavaScript是單線程的,但是可以同時(shí)有多個(gè)任務(wù)隊(duì)列
任務(wù)隊(duì)列又分為macro-task(宏任務(wù))與micro-task(微任務(wù)),在最新標(biāo)準(zhǔn)中,它們被分別稱為task與jobs。
macro-task大概包括:script(整體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。
micro-task大概包括: process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)
setTimeout/Promise等我們稱之為任務(wù)源。而進(jìn)入任務(wù)隊(duì)列的是他們指定的具體執(zhí)行任務(wù)。
來自不同任務(wù)源的任務(wù)會進(jìn)入到不同的任務(wù)隊(duì)列。同源任務(wù)則會進(jìn)入到同一個(gè)任務(wù)隊(duì)列,其中setTimeout與setInterval是同源的
事件循環(huán)的順序,決定了JavaScript代碼的執(zhí)行順序。它從script(整體代碼)開始第一次循環(huán)。之后全局上下文進(jìn)入函數(shù)調(diào)用棧。直到調(diào)用棧清空(只剩全局),然后執(zhí)行所有的micro-task。當(dāng)所有可執(zhí)行的micro-task執(zhí)行完畢之后。循環(huán)再次從macro-task開始,找到其中一個(gè)任務(wù)隊(duì)列執(zhí)行完畢,然后再執(zhí)行所有的micro-task,這樣一直循環(huán)下去。
其中每一個(gè)任務(wù)的執(zhí)行,無論是macro-task還是micro-task,都是借助函數(shù)調(diào)用棧來完成。
setTimeout作為任務(wù)分發(fā)器,將任務(wù)分發(fā)到對應(yīng)的宏任務(wù)隊(duì)列中。
Promise的then方法會將任務(wù)分發(fā)到對應(yīng)的微任務(wù)隊(duì)列中,但是它構(gòu)造函數(shù)中的方法會直接執(zhí)行

面向?qū)ο?補(bǔ)充)
in操作符的應(yīng)用場景之一:in可以用來判斷一個(gè)對象是否擁有某一個(gè)屬性/方法,無論該屬性/方法存在與實(shí)例對象還是原型對象,我們可以用這種特性來判斷當(dāng)前頁面是否是在移動端打開
isMobile='ontouchsart in document'
關(guān)于new關(guān)鍵字到底在創(chuàng)建構(gòu)造函數(shù)時(shí)做了什么,之前自己也有過總結(jié),但是現(xiàn)在看到了大概的代碼實(shí)現(xiàn)
var Person=function (name,age) {
this.name=name;
this.age=age;
this.getName=function () {
return this.name;
}
}
function New(fun){//接收構(gòu)造函數(shù)為參數(shù)
var res={};//聲明一個(gè)對象,該對象為最終的返回實(shí)例
if(fun.prototype !== null){
res.__proto__=fun.prototype;
//將聲明對象的原型指向構(gòu)造函數(shù)的原型
}
var ret=fun.apply(res,Array.prototype.slice.call(arguments,1));
//將構(gòu)造函數(shù)內(nèi)部的this指向?qū)嵗龑ο髍es
if((typeof ret==="object" || typeof ret==="function")&&ret!==null){
return ret;
}
//如果我們在構(gòu)造函數(shù)中指定了返回對象,那么new的執(zhí)行結(jié)果就是返回值
return res;
//如果沒有明確返回對象,則默認(rèn)返回res,也就是實(shí)例對象
}
var p=New(Person,'tom',20);
this
在面試題中經(jīng)??吹疥P(guān)于this的問題,今天恰好看到一篇關(guān)于this的文章,總結(jié)一下,用于日后裝逼
在js中有三種函數(shù)的調(diào)用模式
- function(p1,p2){ console.llog(this)};
- iffn.str(p1,p2);
- fn.call(context,p1,p2);
之前我們常用的是第一種和第二種模式,但實(shí)際上只有我們要樹立一個(gè)概念,只有第三種模式才是正常的調(diào)用模式,第一種和第二種的模式都可以轉(zhuǎn)化為第三種模式
第一種可以轉(zhuǎn)化為function.call(undefined,p1,p2);
第二種模式可以轉(zhuǎn)化為 fn.str.call(fn,p1,p2);
第一種情況下this指向的是undefined,但是在js中規(guī)定,如果指向undefined,那么會將指針指向window
其實(shí)上面對this的解釋比較好了,下面的點(diǎn)可以說時(shí)一點(diǎn)補(bǔ)充吧
this的指向時(shí)在函數(shù)被調(diào)用時(shí)所確定的
在一個(gè)函數(shù)上下文中,this由調(diào)用者提供,由調(diào)用函數(shù)的方式來決定。如果調(diào)用者函數(shù),被某一個(gè)對象所擁有,那么該函數(shù)在調(diào)用時(shí),內(nèi)部的this指向該對象。如果函數(shù)獨(dú)立調(diào)用,那么該函數(shù)內(nèi)部的this,則指向undefined。但是在非嚴(yán)格模式中,當(dāng)this指向undefined時(shí),它會被自動指向全局對象。
var a = 20;
function fn() {
function foo() {
console.log(this.a);
}
foo();
}
fn();
還有一點(diǎn)時(shí)切記注意當(dāng)前是否是嚴(yán)格模式,嚴(yán)格模式下直接調(diào)用沒有被對象擁有的函數(shù)的this是會直接報(bào)錯(cuò)的,我們可以在嚴(yán)格模式和非嚴(yán)格模式下觀察以下代碼的輸出結(jié)果
'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());
參考文章:[前端基礎(chǔ)進(jìn)階系列]