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

相关文章
相关标签/搜索