You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
var count = 0;
var foo = function _foo() {
if (++count > 3) {
return;
}
console.log(count);
_foo();
};
// _foo(); // ReferenceError: _foo is not defined
foo(); // 1 2 3
函数定义
函数有函数声明和函数表达式两种,其中函数表达式又可分为具名和匿名形式
函数声明
函数声明:
ECMAScript 语法表示:
FunctionDeclaration :
function Identifier ( FormalParameterListopt ) { FunctionBody }
比如说:
函数表达式
函数声明在语法上是一个语句,但函数也可以由函数表达式创建(函数名可以省略)。简单一点说就是当由 function 关键字组成的语句处在表达式时,这样的形式就是函数表达式。令 Fn 等于 function Identifieropt ( FormalParameterListopt ) { FunctionBody } 当 Fn 处在表达式时,Fn 就是 函数表达式。
ECMAScript 语法表示:
FunctionDeclaration :
function Identifieropt ( FormalParameterListopt ) { FunctionBody }
常见的函数表达式:
只要 Fn 处在表达式中,Fn 就是函数表达式,所以下面这些也是函数表达式:
创建函数对象
在执行函数之前,会创建函数对象。
下面是 ECMAScript 对创建函数对象规定:
总的来说就是先创建一个 F 对象,然后不断添加不同的属性(内部属性),返回 F。
当调用函数时,会调用 [[Call]] 内置方法,使用 new 运算符时,会调用 [[Construct]] 内置方法。具体操作会在后面阐述。其他的属性以后遇见了在说,这里重点关注 [[Scope]] 属性。
[[Scope]] 内部属性为 Scope 值。这个属性和作用域链相关。这里简单说一下
在 foo 函数的作用域中是没有 a 这个变量的,那么就会按照作用域链往上查找,具体在哪里查找呢?就是在函数的 [[Scope]] 中查找。
Scope 值在函数定义有涉及
函数声明的 Scope 值:
匿名函数表达式的 Scope 值:
函数声明和匿名函数表达式的 [[Scope ]]
执行环境后面会做专门介绍。函数声明和匿名函数表达式的 Scope 可以理解为函数定义时所在的词法环境(在上面的例子中,词法环境包括了 a 变量,所以才能在 foo 函数中查找到 a)。
具名函数表达式的 [[Scope ]]
具名函数表达式的 Scope 值:
具名函数表达式的 Scope 值和其它两种不太一样。
第一点表明 funcEnv 是 NewDeclarativeEnvironment 返回的词法环境(词法环境由一个环境记录项和可能为空的外部词法环境引用构成)。NewDeclarativeEnvironment 规则:
根据规则,funcEnv 是环境数据(声明式环境数据)为空,外部词法环境是 LexicalEnvironment(也就是函数定义所在的词法环境)的词法环境(词法环境会在执行环境中介绍)。
第二点让 envRec 绑定到 funcEnv 的环境记录项
第三点就是处理函数表达式的名称。调用 envRec 的 CreateImmutableBinding 方法。CreateImmutableBinding 方法会在 envRec中创建一个以 Identifier 名称的不可变绑定并初始化为 undefined。在第五点的时候会将 Identifier 绑定的值修改为 closure (函数对象)。
identifier 是不可变绑定,所以在具名函数表达式中是不能修改 identifier 值:
第四点会将第一点创建的 funcEnv 传为 Scope。
那么具名函数表达式的 [[Scope]] 属性的值和其他两种有什么区别?在我们实际编程中有哪些体现?
其他两种函数的 [[Scope]] 属性的值 Scope 都是函数定义所在的词法环境,但是具名函数表达式 [[Scope]] 的值 Scope 是新创建的一个词法环境,这个新创建的词法环境的环境记录项就只有 Identifier(函数对象) 绑定,外部词法环境才是函数定义所在的词法环境。假设 globalScope 是全局词法环境,newScope 是具名函数表达式新建 Scope,FnScope 是具名函数表达式 Scope,那么具名函数表达式查找变量的顺序是 FnScope ---> newScope ---> globalScope。具名函数表达式名称变量是在 newScope 这里绑定的。
在实际中的体现:
具名函数表达式 _foo 可以调用自身,但是不能在全局调用,也就是在全局的词法环境中找不到 _foo 变量。因为 _foo 是存在 newScope 中的。
上面这个是 ECMAScript 规定的,但是在实际 JS 引擎实现中可能不一样。
在 Rhino 中,具名函数表达式名字是直接保存在函数自己的 Scope 中,在 JScript 中,直接保存在上层作用域中,所以在函数外面也可以访问(在 IE8 环境下运行)
函数调用
函数调用方式如 Foo()
第一二点,令 func 为 GetValue(ref) 结果,这里 func 就是函数对象。
第三点,令 argList 为实参列表
第四五点,对 func 判断
第六七点,和 this 相关,在 this 章节说明。
第八点,以 thisValue 作为 this 值,argList 列表作为参数列表调用 [[Call]] 内置方法。
在创建函数对象的时候,会内置很多属性方法,其中就有 [[Call]] 内置方法。
[[Call]]
[[Call]] 内部方法
第一点,以 [[FormalParameters]] 内部属性值,参数列表 args,this 值建立新执行环境 funcCtx。其中 [[FormalParameters]] 内部属性值是形参,args 是实参。
第二点,解释执行 FunctionBody ,结果为 result。如果没有 FunctionBody,result 是(normal,undefined,empty)
第三点,执行完函数之后,恢复到上一个执行环境状态。
最后三点,关于执行结果 result 的规定。通过 result.type 值决定返回什么,但是 result.type 是一个内部值,我们不能访问。
在解释执行 FunctionBody 时,还会有一个步骤:进入函数代码。
进入函数代码
进入函数代码
前四点和 this 值相关,在 this 章节说明。
第五点,以 F 的 [[Scope]] 内部属性新建一个词法环境 localEnv,这个词法环境的环境数据还没有任何绑定。
第六七点,词法环境组件和变量环境组件都为 localEnv。(词法环境组件和变量环境组件在执行环境章节中说明)。
第八点,code 设为 [[Code]] 内部属性值,也就是 FunctionBody。
第九点,进行声明式绑定初始化。
声明式绑定初始化
声明式绑定初始化,在以下三个地方需要执行:
这里重点关注进入函数代码相关部分
声明式绑定初始化
声明式绑定初始化和我们长说的变量提升有关。
第一点,令 env 为当前运行的执行环境的变量环境组件的环境记录项。
第二点,和 eval 相关,忽略。
第三点,判断是否为严格模式。
处理函数参数
第四点,处理实参赋值给形参。总的来说是以 argName,v 和 strict 为参数调用 SetMutableBinding 进行绑定。argName 是形参标识符,v 是实参值,strict 表示是否为严格模式。不过其中处理了一些其他情况。
在真正解释执行前函数参数是已经有值了。
另外一个例子:
因为函数中的变量绑定初始化的时间(在最后一步)晚于实参的绑定初始化。当绑定变量时,因为执行环境中已经有 a 的绑定了,所以不对变量 a 做处理,最后执行环境中剩下的是实参 a 的绑定。
处理函数声明
第五点,处理函数声明。如果在遍历 code 中,遇见函数声明 f:
令 fn 为函数声明 f 的标识符。
根据函数声明创建一个函数对象 fo。
通过 env 的 HasBinding 方法查看 env 是否绑定 fn。
如果没有绑定,以 fn 和 configurableBindings 为参数调用 env 的 CreateMutableBinding 方法创建一个可变绑定。其中 configurableBindings 参数表示是否可以删除。
不能删除 func 函数的,所以 configurableBindings 为 false。
当函数声明在全局下的处理
通过 env 的 SetMutableBinding 方法绑定 fn 值为 fo。
所以在函数执行前,函数声明的标识符已经存在函数对象。
声明式绑定初始化时,是先处理函数声明的,最后才处理其他表示符。
原因:首先处理函数声明 foo。然后处理变量 foo 时,发现已经绑定了 foo 标识符,就不做处理。
当执行到 foo = 1 时,实际上是将标识符 foo 重新赋值为 1,此时 foo 不在代表函数,所以执行 foo() 时会报错。
arguments 对象
第六七点,关于 arguments 对象。
首先检查 env 中存在 arguments 绑定没有,如果没有,就通过 CreateArgumentsObject 抽象运算函数创建一个对象 argsObj。
在严格模式下,以 “arguments” 为参数调用 env 的 CreateImmutableBinding 方法创建一个不可变绑定。以 “arguments” 和 argsObj为参数调用 env 的 InitializeImmutableBinding 函数初始化。
在非严格模式下,以 “arguments” 为参数调用 env 的 CreateMutableBinding 方法创建一个可变绑定。以 “arguments” 和 argsObj为参数调用 env 的 SetMutableBinding 函数初始化。
因为形参中存在 arguments 字符串,而且处理函数参数在处理 arguments 对象之前,此时 env 中已经存在 arguments 绑定了,所以不会创建 arguments 对象。
在严格模式下,arguments 是不可变的
####创建 Arguments 对象
arguments 对象是由 CreateArgumentsObject 抽象运算函数创建的
CreateArgumentsObject 抽象运算函数
arguments 对象会简单将实参赋值到对象的属性上,arguments 对象的内部属性 [[ParameterMap]] 的值 map 和形参相关,在非严格模式下,一般情况,arguments 对象和形参共享值,在严格模式下,arguments 对象和形参没有任何关系。
第一点,令 len 等于 args 元素个数。
第二到七点,新建的 ECMAScript 对象 obj 并添加相关属性。
第八点,令 map 为 new Object() 创建的对象。
第九点,令 mappedNames 为空列表。
第十点,令 indx = len - 1。
第十一点,当 indx >= 0 时,这一步是循环过程,处理 arguments 对象和实参,形参的关系。
在 11.2 步骤,将实参赋值到 obj[ToString(indx)],在 11.3 步骤,将形参赋值到 map[[ToString(indx)]],在 12.1 步骤,会将 map 添加到 obj[[ParameterMap]]。
第十二点,在非严格模式下,如果 mappedNames 不为空,则
第十三、十四点,在非严格模式下,规定 arguments 对象的 callee、caller 属性(caller 已经完全删除,在严格模式下,callee 是不能被访问的)。
[[Get]]
当 arguments[P] 时,首先判断 map 是否有 P 属性,如果没有,调用 arguments 对象的内部默认的 [[Get]] 方法,返回结果(取实参的值)。如果 map 有 P 属性,调用 map 对象内部方法 [[Get]],返回结果(取形参的结果)。
因为在 map 对象上存在 “0” 属性,所以 arguments[0] 取得是 map[0] 值,也就是 a 的值。
因为在 map 对象上不存在 “1” 属性,所以 arguments[1] 取得是自己的 “1“ 属性的值,也就是形参列表中第二个元素的值。
[[GetOwnProperty]]
desc 为 arguments.[[GetOwnProperty]](P) 结果。
如果 desc 为 undefined,直接返回 desc。
令 isMapped 为 map.[[GetOwnProperty]](P) 结果。
如果 isMapped 为 undefined,直接返回 desc。如果 isMapped 不为 undefined,将 desc.[[Value]] 修改为 map.[[Get]](P) 的结果。
返回 desc。
[[DefineOwnProperty]]
调用 arguments.[[DefineOwnProperty]](P, Desc, Throw) (= 运算符和 Object.defineProperty 会调用 [[DefineOwnProperty]]),会按照上面的步骤执行。
令 map 为 arguments 对象的内部属性 [[ParameterMap]] 的值。
令 isMapped 为 map.[[GetOwnProperty]](P) 的执行结果。
令 allowed 为 arguments.[[DefineOwnProperty]](P, Desc, false)
这里的 [[DefineOwnProperty]] 是普通对象的内部方法的算法。
若 allowed 为 false,如果 Throw 是 true,则抛出 TypeError,否则返回 false。
若 allowed 不是false,且 isMapped 的值不为 undefined,则
如果 Desc 不为空和 Desc.[[Get]] 或者 Desc.[[Set]] 存在,则 IsAccessorDescriptor(Desc) 返回 true,则执行下面步骤。
调用 map. [[Delete]](P, false)。
调用 map.[[Delete]](p, false) 之后,arguments 对象和形参没有任何关系。
如果 IsAccessorDescriptor(Desc) 返回 false
存在 Desc.[[Value]],调用 map.[[Put]](P, Desc.[[Value]], Throw),因为在非严格模式下,形参和 arguments是共享值的,所以此时 arguments[indx] 也会修改。
存在 Desc.[[Writable]],且其值为 false,调用 map.[[Delete]](P, false)。
[[Delete]]
在非严格模式下,
调用 arguments.[[Delete]](P) (这里的 [[Delete]] 是普通对象的 [[Delete]]),结果置为 result。
如果 result 为 true,并且 map 对象有 P 属性,则调用 map.[[Delete]](P, false)。
返回 result。
小结
我们知道在非严格模式下,实参和 arguments[indx] 是共享值的。但是只要调用 map.[[Delete]](P, false) 之后,这种关系将被打破。有以下三种情况会调用 map.[[Delete]](P, false):
构造函数
通过 new 运算符调用的函数都可称为构造函数。
new 运算符
new 运算符有两种调用方式 new Foo 和 new Foo(params) ,这两种方式除了有无参数其他都一样。这里以有参数为例:
首先令 constructor 为 GetValue(ref) 。constructor 就是函数对象(构造函数),令 argList 为参数列表。函数对象在创建的时候会内置 [[Construct]] 方法。
new 运算符结果就是最后一点所描述的结果:调用 constructor(函数对象)的 [[Construct]]] 内置方法的结果。
[[Construct]]
前七点,创建一个 ECMAScript 原生对象 obj,并添加内部属性。
第八点,以 obj 为 this 值,args 为参数列表,调用 F 的 [[Call]] 内部属性(也就是普通的函数调用),result 为调用结果。
第八九点,根据 result 判断返回值:如果 result 是对象,返回 result,否则返回刚才新建对象 obj。
因为 Foo 函数返回值是数字 1 ,不是对象,所以 new Foo(1) 返回新建的对象。
Foo 函数返回值是一个对象 {b: 10},所以 new Foo(1) 返回 {b: 10}
函数扩展:函数语句
JavaScript 语句不包括函数声明(FunctionDeclaration),函数声明不能作为语句使用。在 [ECMAScript 语句](ECMAScript 语句) 的注释中有明确的提示说明:
不过实际实现中,允许函数声明作为语句使用,我们常见的,比如在 if、for 中函数声明都作为语句使用
函数声明 foo 被当做语句使用,foo 相当于变量了。在执行 if 语句之后,foo 才被赋值为函数对象。
有一点 需要注意,foo 标识符,在执行环境中创建的不可变绑定。
这一点在各大浏览器下实现的都一致。
参考资料
ECMAScript 英文文档
ECMAScript 维基百科 中文文档
函数 (Functions) (深入理解JavaScript系列(15):函数 (Functions))
MDN 函数
The text was updated successfully, but these errors were encountered: