var a = 2;
function foo() {
console.log(a)
}
function bar(){
var a = 5;
foo()
}
bar()//2
複製代碼
對上面代碼的解釋,都會提到靜態做用域呀、函數的做用域跟建立時候的環境有關。可是咱們看另外一段代碼:javascript
var a = 1;
function foo() {
var a = 2;
function innerOne(){
console.log(a)
}
var innerTwo = new Function("console.log(a)")
var innerTree = function (){
console.log(a)
}
innerOne();
innerTwo();
innerTree();
}
foo();//2 1 2
複製代碼
對於 var innerTwo = new Function("console.log(a)") ,innerTwo這個函數不也是在foo裏面建立的嗎?爲啥它打印1?java
顯然,用不一樣方式建立的函數是有一些差別的。bash
這一篇和下一篇(函數運行)中,咱們將更進一步解釋到底靜態做用域、「函數只跟它建立時的詞法環境有關」是什麼意思?要理解函數的做用域,咱們須要探討兩個問題:函數
針對這個兩個問題,咱們一個個來講。ui
何時函數會被建立?對於使用不一樣方式定義的函數是不一樣的:spa
像這樣的定義函數的語句叫作函數聲明。prototype
function functionname ( parameters ) {
functionbody
}
複製代碼
對函數聲明來講,函數聲明和var聲明同樣,是在代碼執行以前建立的。什麼?小夥伴又暈了,代碼都還沒執行怎麼建立?code
因此這裏必要作個澄清,還記上一篇,咱們說到,JS三種可運行代碼(global\function\eval)的運行模型嗎:orm
可運行代碼的運行 = 運行上下文初始化 + var聲明和函數聲明掃描scan + 執行語句;
複製代碼
因此作個約定: 當我說代碼運行時,表示進入該程序或者進入該函數;當我說代碼執行時,表示一些前奏都準備好了(運行上下文初始化 + var聲明和函數聲明掃描scan ),開始一行行執行語句,以示區分。cdn
因此函數聲明和var聲明同樣,在分析掃描代碼階段就會被登記到運行上行文的詞法環境中,因此也是有「提高」的現象。和var不一樣的是,在登記階段var聲明初始化爲undefined,而函數則會在內存建立一個函數對象,並初始化爲該函數對象。因此函數「提高」,是直接可用的,不是undefined:
lex() //'lexical'
function lex() {
console.log('lexical')
}
複製代碼
這裏咱們得出一個結論,對函數聲明來講,函數是在「var聲明和函數聲明掃描scan」的時候就建立了。
函數的建立過程大體以下流程:
/**
* 運行環境模型僞代碼
*/
function FunctionCreate(parameterList,functionBody,scope,strict) {
var F = Object.create();
F. [[Class]] = "Function";
F.[[Code]] = functionBody;
F. [[FormalParameters]] = parameterList;
F. [[Prototype]] = Function.prototype;
F.[[Scope]] = scope;
F.prototype = {
constructor:F
};
F. [[Call]] = [[internal method]];
//根據Strict設置Strict 模式相關
//設置相關其餘屬性
...
...
return F;
}
複製代碼
咱們目前關係呢就是函數建立時設置的[[scope]]這個屬性。
用圖來分析這段代碼:
lex() //'lexical'
function lex() {
console.log('lexical')
}
複製代碼
運行上下文初始化:
建立全局運行環境,把把它放到運行棧頂部,使它變爲當前運行上下文:
/**
* 運行環境模型僞代碼
*/
var globalExecutionContext = new ExecutionContext();
globalExecutionContext.LexicalEnvironment = GlobalEnvironment;
globalExecutionContext.VariableEnvironment = GlobalEnvironment;
globalExecutionContext.ThisBinding = globalobject;
Runtime.push(globalExecutionContext);
//這時的Runtime
Runtime = {
executionContextStack: [globalExecutionContext];
};
複製代碼
這時的運行棧看起來是這樣的:
var聲明和函數聲明掃描scan
解析代碼,找到函數聲明function lex() {console.log('lexical')}:
/**
* 運行環境模型僞代碼
*/
var funname = lex;
var funcbody = "console.log('lexical')";
var argumentlist = [];
//currentLexicalEnvironment這時其實就是全局詞法環境GlobalEnvironment
var currentLexicalEnvironment = Runtime.getRunningExecutionContext().VariableEnvironment;
var fo = FunctionCreate(argumentlist,funcbody,currentLexicalEnvironment,strict) //currentLexicalEnvironment 最後保存到函數對象的內部屬性[[scope]]。
currentLexicalEnvironment.EnvironmentRecord.initialize('lex',fo);
複製代碼
這時看起來像這樣:
執行代碼語句:
/**
* 運行環境模型僞代碼
*/
var fun = Runtime.getRunningExecutionContext().LexicalEnvironment.EnvironmentRecord.getValue('lex');
// 而後執行fun,其實就是執行F的[[call]]內部方法。後面講。
複製代碼
函數表達式有兩種:
//funcOne()//錯誤,
//funcTwo()//錯誤
var funcOne = function funname(){ //命名函數表達式:帶有函數名稱標識符的函數表達式
console.log('One');
console.log(funname)
}
var funcTwo = function () { //匿名函數表達式
console.log('Two')
}
funcOne()// 'One' 'ƒ funname(){console.log('One');console.log(funname)}'
funname()//Uncaught ReferenceError: funname is not defined
複製代碼
須要說一下的是,上述代碼中 並非說:
var funcOne = function funname(){
console.log('One');
console.log(funname)
}
複製代碼
這一整個是函數表示式,而是等號右邊function funname(){ 。。。。} 是函數表達式,var funcTwo = function(){...}同理。
所謂表達式,是在執行代碼時候運行的,就上述代碼段而言就是執行賦值以前運行函數表達式,而後將表達式的運行結果分別賦給變量funcOne和funcTwo。funcOne和funcTwo是普通的var 聲明的變量,會提高,但初始化爲undefined。所以,執行賦值以前,調用會報錯,由於undefined不是函數呀。
因此在咱們的運行模型中:
可運行代碼的運行 = 運行上下文初始化 + var聲明和函數聲明掃描scan + 執行語句;
複製代碼
函數表達式是在「 執行語句」階段進行函數的建立的,因此它沒有「提高的現象」。
準確的講,要調用一個函數必需要應用它,因此要調用函數表達式建立的函數,也須要變量引用它,可是變量會提高,值爲undefined,但賦值動做不會提高,函數表達式只有在表達式運行時纔會建立函數。
命名函數表達式和函數聲明看起有點像:
function funndec(){
console.log('Declarations');
}
var funcOne = function funname(){ //命名函數表達式
console.log('Expressions');
console.log(funname);//"function funname(){console.log('Expressions'); console.log(funname);}"
}
funndec()//Declarations
funname()//error
複製代碼
但有差別,對於函數聲明,函數名能夠在函數外調用,但對於命名函數表達式,它的名字函數外是不能使用(未定義),只能在函數內部使用。怎麼會這樣呢?
說明命名函數表達式的函數建立和函數聲明是有差別的。 咱們用圖來講明其差別。
function funndec(){
console.log('Declarations');
}
var funcOne = function funname(){ //命名函數表達式
console.log('Expressions');
console.log(funname);//"function funname(){console.log('Expressions'); console.log(funname);}"
}
funndec()//Declarations
funname()//error
複製代碼
運行上下文初始化
一樣也是先建立全局運行上下文:
/**
* 運行環境模型僞代碼
*/
var globalExecutionContext = new ExecutionContext();
globalExecutionContext.LexicalEnvironment = GlobalEnvironment;
globalExecutionContext.VariableEnvironment = GlobalEnvironment;
globalExecutionContext.ThisBinding = globalobject;
Runtime.push(globalExecutionContext);
//這時的Runtime
Runtime = {
executionContextStack: [globalExecutionContext];
};
複製代碼
var聲明和函數聲明掃描scan:
找到函數聲明function funndec() {console.log('Declarations');},執行登記到當前詞法環境操做:
/**
* 運行環境模型僞代碼
*/
var funname = 'funndec';
var funcbody = "console.log('Declarations');";
var argumentlist = [];
//currentLexicalEnvironment這時其實就是全局詞法環境GlobalEnvironment
var currentLexicalEnvironment = Runtime.getRunningExecutionContext().VariableEnvironment;
var fo = FunctionCreate(argumentlist,funcbody,currentLexicalEnvironment,strict) //currentLexicalEnvironment 最後保存到函數對象的內部屬性[[scope]]。
currentLexicalEnvironment.EnvironmentRecord.initialize(funname,fo);//
複製代碼
找到var聲明:var funcOne,執行登記到當前詞法環境操做:
currentLexicalEnvironment.set('funcOne') //funcOne=undefined
複製代碼
這是時候看起來是這樣的:
執行語句:
遇到賦值語句「funcOne = function funname(){...}」,運行函數表達式function funname(){...}:
/**
* 運行環境模型僞代碼
*/
var funname = 'funname';
var funcbody = "console.log('Expressions'); console.log(funname);";
var argumentlist = [];
//獲取當前運行上下文的詞法環境,這時其實就是全局詞法環境GlobalEnvironment
var currentLexicalEnvironment = Runtime.getRunningExecutionContext().LexicalEnvironment;
//建立一個新的詞法環境
var newLexicalEnviroment = new LexicalEnvironment();
//設置newLexicalEnviroment的outer未當前詞法環境
newLexicalEnviroment.outer = currentLexicalEnvironment;
//使用newLexicalEnviroment建立函數對象
var fo = FunctionCreate(argumentlist,funcbody,newLexicalEnviroment,strict//用於設置是否嚴格模式)
//在newLexicalEnviroment上綁定命名函數的名字
newLexicalEnviroment.EnvironmentRecord.initialize(funname,fo);
返回fo
複製代碼
這時看起來是這樣的:
有點複雜有沒有,其實,惟一和函數聲明的差異就是,函數聲明的函數建立過程使用的當前運行上下的詞法環境,而命名函數表達式建立函數過程是在當前運行上下的詞法環境以前,有加了個新的詞法環境,並經過outer和當前運行上下的詞法環境連接起來。並在本身的詞法環境添加對函數命名的綁定funname,這樣作的目的是爲可以在函數表達式裏面遞歸調用本身,注意funname在函數外是沒定義的,因此在全局調用funname() 會報錯//Uncaught ReferenceError: funname is not defined。
接下去就是執行調用語句:
funndec()//Declarations
funname()//error
複製代碼
執行調用的詳細後面在講,咱們在來看看匿名函數表達式的函數建立和new Function方式的函數建立
匿名函數表達式除了建立時機和函數聲明不一樣(在語句執行的時候建立),建立過程和函數聲明同樣。
用new Function(arg1,arg2,...,argn,body) 建立函數的過程有和上面函數表達式相似,不一樣地方在於,建立函數使用的scope是直接使用全局詞法環境(glbal enviroment),而無論當前運行上下文,一概取全局詞法環境(glbal enviroment)。有點像:
/**
* 運行環境模型僞代碼
*/
var argumentlist = [arg1,arg2,...,argn];
var funbody = body;
var fo = FunctionCreate(argumentlist,funbody,glbalenviroment,strict);
複製代碼
因此在函數內用new Function 建立的函數,只能訪問全局變量。所以,無論在哪裏用new Function 建立函數,等同於在全局環境上建立函數。
從函數建立過程能夠看出,函數一出生(建立),就帶了一個[[scope]]屬性,這個屬性存放着函數建立時的詞法環境(Lexical Enviroment)。是函數"先天"的做用域,是靜態的,是在函數建立是就被保存在函數體內。
就像筆者,一輩子下來的環境就是福建,之後無論筆者走到哪,總帶着‘湖建’口音,這是出生時環境對我影響。
函數也是,建立時就帶了當時的詞法環境,因此之後無論函數走到哪(在哪調用),總能訪問到它建立時候攜帶的詞法環境。
既然函數有"先天"的做用域,那意思還有"後天"的做用域了?
有,咱們下一篇-函數調用中再聊。
總結一下在不一樣的狀況下函數建立時的[[scope]]屬性什麼樣,這個屬性後續還會用到,所以,特此強調: