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?
显然,用不同方式创建的函数是有一些差异的。
这一篇和下一篇(函数运行)中,我们将更进一步解释到底静态作用域、“函数只跟它创建时的词法环境有关”是什么意思?要理解函数的作用域,我们需要探讨两个问题:
- 什么时候函数会被创建?
- 函数创建过程都干了啥?
针对这个两个问题,我们一个个来说。
什么时候函数会被创建?对于使用不同方式定义的函数是不同的:
函数声明
像这样的定义函数的语句叫做函数声明。
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')
}
-
运行上下文初始化:
创建全局运行环境,把把它放到运行栈顶部,使它变为当前运行上下文:
/** * 运行环境模型伪代码 */ 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);这时看起来像这样:
-
执行代码语句:
- 执行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
-
运行上下文初始化
同样也是先创建全局运行上下文:
/** * 运行环境模型伪代码 */ 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) 创建函数
用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]]属性什么样,这个属性后续还会用到,因此,特此强调: