3. 完全搞懂javascript-函數建立

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

這一篇和下一篇(函數運行)中,咱們將更進一步解釋到底靜態做用域、「函數只跟它建立時的詞法環境有關」是什麼意思?要理解函數的做用域,咱們須要探討兩個問題:函數

  1. 何時函數會被建立?
  2. 函數建立過程都幹了啥?

針對這個兩個問題,咱們一個個來講。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')
}
複製代碼
  1. 運行上下文初始化:

    建立全局運行環境,把把它放到運行棧頂部,使它變爲當前運行上下文:

    /**
     * 運行環境模型僞代碼
     */
     
    var globalExecutionContext = new ExecutionContext();
    globalExecutionContext.LexicalEnvironment = GlobalEnvironment;
    globalExecutionContext.VariableEnvironment = GlobalEnvironment;
    globalExecutionContext.ThisBinding = globalobject;
    
    Runtime.push(globalExecutionContext);
    
    //這時的Runtime
    Runtime = {
        executionContextStack: [globalExecutionContext];
    };
    複製代碼

    這時的運行棧看起來是這樣的:

  2. 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);
    
    複製代碼

    這時看起來像這樣:

  3. 執行代碼語句:

    • 執行lex():先解析lex,而後運行lex:
    /**
     * 運行環境模型僞代碼
     */
    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

複製代碼
  1. 運行上下文初始化

    一樣也是先建立全局運行上下文:

    /**
         * 運行環境模型僞代碼
         */
        var globalExecutionContext = new ExecutionContext();
        globalExecutionContext.LexicalEnvironment = GlobalEnvironment;
        globalExecutionContext.VariableEnvironment = GlobalEnvironment;
        globalExecutionContext.ThisBinding = globalobject;
        
        Runtime.push(globalExecutionContext);
        
        //這時的Runtime
        Runtime = {
            executionContextStack: [globalExecutionContext];
        };
    複製代碼

  2. 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
      複製代碼

    這是時候看起來是這樣的:

  3. 執行語句:

    遇到賦值語句「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) 建立函數

用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]] 屬性

從函數建立過程能夠看出,函數一出生(建立),就帶了一個[[scope]]屬性,這個屬性存放着函數建立時的詞法環境(Lexical Enviroment)。是函數"先天"的做用域,是靜態的,是在函數建立是就被保存在函數體內。

就像筆者,一輩子下來的環境就是福建,之後無論筆者走到哪,總帶着‘湖建’口音,這是出生時環境對我影響。

函數也是,建立時就帶了當時的詞法環境,因此之後無論函數走到哪(在哪調用),總能訪問到它建立時候攜帶的詞法環境。

既然函數有"先天"的做用域,那意思還有"後天"的做用域了?

有,咱們下一篇-函數調用中再聊。

總結

總結一下在不一樣的狀況下函數建立時的[[scope]]屬性什麼樣,這個屬性後續還會用到,所以,特此強調:

函數聲明

函數表達式

匿名函數表達式

命名函數表達式

new Function

相關文章
相關標籤/搜索