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]]属性什么样,这个属性后续还会用到,所以,特此强调: