函數的執行順序
1.語法分析(掃描一遍,看有沒有語法錯誤之類的)
2.預編譯
3.解釋執行(解釋一行,執行一行)
預編譯和執行是分不開的,預編譯就發生在函數執行的前一刻。預編譯完成,馬上逐行解釋執行。下面講預編譯,里邊含有解釋執行的東西。
先鋪墊幾個知識點
函數聲明整體提升
變量,聲明提升
暗示全局變量,任何變量如果未經聲明就賦值,此變量歸全局所有。一切聲明的全局變量,全是window的屬性,比如b=3;意思就是window.b=3
變量,聲明提升
var a=123相當于,var a;a=123;變量,
聲明提升,把var a提升了,打印的話就是undefined
預編譯四部曲:
1.創建AO對象
2.找形參和變量聲明,將形參名和變量名作為AO屬性名,增加到AO里面,并且賦值undefined
3.將實參賦值給形參
4.在函數體里找函數聲明,值賦予函數體
第一步:
生成一個活動對象(Active Object),簡稱AO,又叫做執行期上下文
函數在運行前,會執行詞法分析
第二步:
分析形參和變量聲明,如var age;或var age=25;
AO.age=undefine
第三步:
將實參賦值給形參,如果實參age=30,那么AO里面也跟著變化,這時候
AO.age=30
第四部:
分析函數的聲明,如果有function age(){}
把函數賦給AO.age ,覆蓋上一步分析的值
下面是一個例子,詳細講述函數執行三部曲,AO執行四部曲的
<script>
function fn(a) {
console.log(a);
var a=123; //相當于var a并且賦值為123
console.log(a);//第三行
function a() { } //這個叫函數聲明
console.log(a);
var b=function () { } //這個叫函數表達式,是b的聲明賦值
console.log(b);
function d() { } //這個叫函數聲明
}
fn(1);
//首先進行語法掃描分析,沒發現語法錯誤,就開始預編譯,然后逐行解釋執行
//下面重點講預編譯四部曲
/*1.創建AO對象
2.找形參和變量聲明,將形參名和變量名作為AO屬性名,
增加到AO里面,并且賦值undefined,這時候
AO:{
a:undefine,(形參是a,變量聲明也有var a,那就只留一個)
b: undefine,(變量聲明var a和var b)
}
3.將實參賦值給形參,這時候
AO:{
a:undefine—>1,(實參a是1,這時候a=1)
b: undefine
}
4.分析函數的聲明,如果有同名的就覆蓋,因為函數聲明優先級最高,這時候
AO:{
a:undefine—>1—>function a() { },(有同名的函數聲明,就覆蓋)
b: undefine,(b不變)
d:function d() { } (增加了一個d,屬于函數聲明)
}
下面進入解釋一行,執行一行的階段,已經看過的就不再看了(比如var a)
第一行:console.log(a);從AO里邊找,打印出函數體
第二行,var a已經不看了,只剩下賦值123了,這時候AO有變化
AO:{
a:undefine—>1—>function a() { }—>123,
b: undefine,
d:function d() { }
}
第三行:console.log(a)打印出123
第四行:也不看了,之前已經提升過了
第五行:console.log(a)也打印出123
第六行:var b就不看了,只是把b賦值為函數體,這時候又修改AO
AO:{
a:undefine—>1—>function a() { }—>123,
b: undefine—>function () { },
d:function d() { }
}
第七行:console.log(b);打印出函數體function () { }
第八行:不看了,之前提升過了。
視頻教程可以參考渡一教育遞歸,預編譯(上)
*/
</script>
預編譯不僅發生在函數內,也發生在全局
下面是單信老師課件部分:
- 編譯原理
軟件開發語言的編譯模式分為:
編譯型編程語言:代碼開發好以后,執行編譯命令,將編譯的文件下載到電腦上安裝即可使用
如:c語言, c++, Java....
解釋型編程語言:代碼開發好以后,不執行編譯。 在執行代碼時才進行編譯,解釋一句執行一句
如:JS php jsp asp .net ...
js執行兩個階段:
預編譯階段:將函數和變量定義進行提升(將函數和變量的定義提到作用域的第一句之前執行)。 掃描上下文環境,如果有語法錯誤有報錯。
解釋執行階段:一句一句執行
作用域:
根據上下文環境(函數會產生獨立的運行環境),將作用域劃分為:
全局作用域
函數外定義的所有內容(函數、變量)都是全局作用域,在任何地方都可以使用局部作用域
函數內定義的內容都是局部作用域,只能在函數內部使用塊級作用域
在ES5中沒有塊級作用域的概念,可以使用IIFE(立即執行函數表達式)實現塊級作用域
在ES6中,可以使用 let實現塊級作用域
//全局作用域
var a='hello222'; //對于全局變量來說,要不要var關鍵字都可以,a就是window的屬性
var fn=function(){
//局部作用域
var b="world";
};
fn();
看下面一個例子,求1-100的和,說為什么要引入塊級作用域
var sum=0;
for(var i=1; i<=100; i++){
sum+=i;
}
console.log(sum); //這是我想要的結果,但也附帶的出現了i這個全局變量污染
console.log(i); //但計算過程中的i,由于沒塊級作用域,變成一個全局變量。 產生全局污染。
//總結,這樣的計算方式會產生全局污染
//改進版:將運算代碼放在函數中,避免全局污染。必須返回一個值作為最終計算結果供全局使用
//IIFE:立即執行函數表達式
var sum=function(){
var sum=0;
for(var i=1; i<=100; i++){
sum+=i;
}
return sum;
}();
//這樣寫的話,就增加了一個函數名,仍然有可能造成全局污染,也不是最好的處理辦法。
//經典IIFE的寫法,再次改進
(function(){
var sum=0;
for(var i=1; i<=100; i++){
sum+=i;
}
window.sum=sum;
})();
//如果直接定義函數,就必須寫函數名。想不寫函數名就必須把函數定義變成函數表達式: + - ! (),這里就變成了一個立即執行函數表達式,最好是前面再加個分號 ;
//這樣處理后就只得到一個sum,并沒有產生全局i和函數名污染
為了更加方便的解決這個問題,引入了塊級作用域let,不會形成變量提升
//ES6可以直接使用let實現塊級作用域
var sum=0;
for(let i=1; i<=100; i++){
sum+=i;
}
console.log(sum);
console.log(i);
//使用ES6的let定義塊級作用域,不會造成全局污染
作用域鏈:
函數操作時,先查找內部的數據,找不到再去找上一級作用域的數據,再找不到再上一級。形成一個查詢數據的作用域鏈條,就叫作用域鏈。
IIFE 立即執行函數表達式:
1 什么是IIFE?
IIFE: Immediately Invoked Function Expression,意為立即調用的函數表達式,也就是說,聲明函數的同時立即調用這個函數。
定義函數后立即執行一次,沒有函數名,無法再次調用,也不會造成全局染污。
2 IIFE有什么用?
可以用于解決運算過程中產生的全局污染問題
也是ES5中實現塊級作用域的方式
3 IIFE的特點
將所有運算代碼都放在匿名函數中進行運算
函數是一個表達式,沒有函數名,必須立即執行一次
執行一次以后就消失,將所有過程產生的數據和變量一起銷毀,不會造成全局污染
4 IIFE的寫法
(function(){
...
window.a=result
})();
下面還是那個求和的例子,講如何使用IIFE和產生的效果
//正常運算1到100的累加,想要的是運算結果,中間過程不管
var sum=0;
for(var i=1;i<=100;i++){
sum+=i;
}
console.log(sum); //這個結果是想要的
console.log(i); //運算完以后i會一直遺留在全局上,造成全局污染
//使用IIFE進行改進
var s=(function(){
var sum=0;
for(var i=1;i<=100;i++){
sum+=i;
}
return sum; //函數內計算的結果必須返回出去
})();
//另外一種用法
(function(){
var sum=0;
for(var i=1;i<=100;i++){
sum+=i;
}
window.s=sum; //函數內計算的結果必須返回出去,不寫return,而是把結果賦值給window的屬性s,這個是閉包的雛形了。
})();
console.log(s);
階乘
階乘是函數的一種形式,函數內部再次調用函數自身
它必須有退出循環執行的條件,必須有進入執行的條件,
經典寫法如下,求5的階乘,就是54321
function fn(n) {
if(n===1){
return 1
}
return n*arguments.callee(n-1)
}
console.log(fn(10));
arguments.callee代替fn函數自身
閉包:
- 什么是閉包?
閉包是一種作用域的體現,正常情況下函數內部可以訪問函數外部的數據,而函數外部不能訪問函數內部的數據;
可以通過特殊寫法(閉包):將函數內的子函數暴露到全局上,可以在外部調用并且可以訪問函數內的數據。閉包可以讓全局訪問函數內的數據。 - 閉包的作用
實現外部可以訪問函數內部的數據 - 閉包的特征
閉包一定是函數內嵌套函數
閉包是一種作用域的體現,體現內部可以訪問外部的數據,而正常情況下外部不能訪問內部的數據
閉包可以實現外部訪問內部函數,內部函數訪問上一級函數的數據。實現外部訪問內部數據。
閉包是IIFE的寫法,由于內部數據被全局所調用,延緩資源回收。 - 閉包的寫法
- 閉包會導致內存無法進行回收
//閉包的寫法,第一種
(function(){
var i=1;
window.show=function(){
return i++;
}
})();
//閉包的寫法,第二種
var count=(function(){
var i=1;
return function(){
return i++;
}
})();
當JavaScript引擎解析腳本時,分為“預編譯”和“解釋執行”兩個階段。
1、“預編譯”階段。首先會創建一個環境的上下文對象,然后把使用var聲明的變量,作為上下文對象的屬性,以undefined先行初始化;使用function關鍵字聲明的函數,也作為上下文對象的屬性,定義出來,而函數則保留了定義的內容。----在這個過程中,函數定義的優先級 高于 變量定義。
2、“解釋執行”階段。遇到變量解析,會先在當前上下文對象中找,如果沒找到并且當前上下文對象有prototype屬性,則去原型鏈中找,否則將會去作用域鏈中找。
在js中,執行環境(execution context)是一個非常重要的概念。
程序代碼運行時,會產生一個專門用來管理所需的變量和函數的對象---環境對象,這個對象我們不能通過代碼編寫操作他,但解析器在處理數據時會在后臺使用他。
1、全局環境就是最外圍的一個執行環境,可以在代碼任意位置訪問到。在瀏覽器中,全局環境就是window對象(因此,所有的全局變量和函數,都是window對象上的屬性和方法)。
2、局部環境以函數為主。每個函數都有自己獨立的執行環境。
局部作用域一般只在固定的代碼片段內可訪問到,最常見的就是函數內部,所以在很多地方就會有人把它稱為函數作用域。