3. 彻底搞懂javascript-函数创建

2,677 阅读8分钟
var a = 2;

function foo() {
    console.log(a)
}

function bar(){
    var a = 5;
    foo()
}

bar()//2

对上面代码的解释,都会提到静态作用域呀、函数的作用域跟创建时候的环境有关。但是我们看另一段代码:

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?

显然,用不同方式创建的函数是有一些差异的。

这一篇和下一篇(函数运行)中,我们将更进一步解释到底静态作用域、“函数只跟它创建时的词法环境有关”是什么意思?要理解函数的作用域,我们需要探讨两个问题:

  1. 什么时候函数会被创建?
  2. 函数创建过程都干了啥?

针对这个两个问题,我们一个个来说。

什么时候函数会被创建?对于使用不同方式定义的函数是不同的:

函数声明

像这样的定义函数的语句叫做函数声明。

function functionname ( parameters ) {
    functionbody
}

对函数声明来说,函数声明和var声明一样,是在代码执行之前创建的。什么?小伙伴又晕了,代码都还没执行怎么创建?

所以这里必要做个澄清,还记上一篇,我们说到,JS三种可运行代码(global\function\eval)的运行模型吗:

可运行代码的运行 = 运行上下文初始化 + var声明和函数声明扫描scan + 执行语句;

所以做个约定: 当我说代码运行时,表示进入该程序或者进入该函数;当我说代码执行时,表示一些前奏都准备好了(运行上下文初始化 + var声明和函数声明扫描scan ),开始一行行执行语句,以示区分。

所以函数声明和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