ECMAScript规范中对Function的文档描述,我认为是ECMAScript规范中最复杂也是最不好理解的一部分,它涉及到了各方面。光对Function就分了Function Definitions、Arrow Function Definitions、Method Definitions、Generator Function Definitions、Class Definitions、Async Function Definitions、Async Arrow Function Definitions这几块。我准备花三章来介绍Function。这篇文章主要是理解ArrowFunction和GeneratorFunction,当然还包括最基本最普通的Function Definitions。
Function
在了解Function Definitions之前我们需要知道函数对象(Function Object)。我们都知道Function本质上也是一个对象,所以普通对象有的方法Function对象都有,此外Function对象还有自己的内部方法。所有Function对象都有一个[[Call]]的内部方法,有了这个方法Function对象才能被用作函数调用,即你***()时内部调用就是[[Call]]方法,当然不是所有有[[Call]]方法的Function对象都可以进行***()调用,常见的Map、Set等方法虽然有[[Call]]方法,但是你不能进行Map()和Set(),这时候就用到了Function对象的另一个内部方法[[Construct]],当Function作为构造函数调用时,就会使用[[Construct]]方法。
注意:不是所有Function对象都有[[Construct]]方法。只有当Function作为构造函数调用时,才会有[[Construct]]方法,比如ArrowFunction和GeneratorFunction只有[[Call]]方法没有[[Construct]]方法。
[[Call]]
先说[[Call]]方法,看到这个名字很容易让人想起Function.prototype中的call方法,没错Function.prototype.call以及Function.prototype.apply都是显示的调用了[[Call]]方法,之所以说显示调用是相比于***()调用,call和apply要简单直接的多。[[Call]]方法接受两个参数,一个是thisArgument,另一个是argumentsList,thisArgument即表示了function中的this对象,argumentsList代表了function中的参数列表,看!和Function.prototype.apply的调用方式是如此的相似。
function foo(){}
foo(1,2) //当执行foo()方法时,实际上内部是如下调用
foo.[[Call]](undefined,« 1, 2 ») //« »表示ECMAScript的List规范类型
//注意,如果你是隐式调用function,那么thisArgument是undefined,不是常说的全局对象window,
//只是在[[Call]]内部执行时检测到如果thisArgument是undefined或null,
//且在非严格模式下才会变成全局对象,即foo(1,2)你可以认为等价与下面的:
foo.call(null,1,2)
foo.apply(undefined,[1,2])
//-------------------
var a={
foo:function(){}
}
a.foo(1,2)
//等价与==> foo.[[Call]](a,« 1, 2 »)
//等价与==> foo.call(a,1,2)
//等价与==> foo.apply(a,[1,2])
这里有个建议,以后你遇到this指向问题的时候,你把function转成call或者apply模式,你就能清楚的明白this指向什么。
[[Construct]]
[[Construct]]内部方法主要有new关键字调用Function时才会执行[[Construct]]方法。[[Construct]]方法主要接受两个参数一个是argumentsList, 还有一个是newTarget。newTarget正常调用下指向调用的function对象。比如foo(),newTarget就是foo,你可以在函数内部用new.target访问到。构造函数中的this对象与newTarget有关,如果newTarget.prototype存在,且是Object对象,则this就是ObjectCreate(newTarget.prototype),ObjectCreate是Object.create内部调用的方法,如果newTarget.prototype不存在或者不是Object对象,this相当于ObjectCreate(Object.prototype)。
function Foo(){}
var fooInstance = new Foo(1,2)
//等价与==> var fooInstance = Foo.[[Construct]](« 1, 2 »,Foo);
fooInstance instanceof Foo //true
Object.create(Foo.prototype) instanceof Foo //true
//注意如果构造函数有自己的return返回,那么情况有所不同。
//返回的是Object,则构造函数的实例就是返回的对象
//返回的不是Object,相当于默认没有返回
function Foo(){ return {a:1}}
var fooInstance = new Foo(1,2)
fooInstance instanceof Foo //false,注意不是true,fooInstance不是Foo的实例
Object.create(Foo.prototype) instanceof Foo //true
//只要Foo.prototype存在且是对象,那么Object.create(Foo.prototype)永远是Foo的一个实例
Function Definitions
Function Definitions包含了FunctionDeclaration和FunctionExpression,有一些早期错误检测添加到Function Definitions中,其中在function中的let、const和var声明的变量规则参考上一篇文章var、let、const声明的区别,另外有一些附加的早期错误:
function中的参数被认为是var声明,因此:
function foo(a,b){
let a = 1; //SyntaxError,重复声明a
}
foo();
如果函数体是严格模式而参数列表不是简单参数列表,则语法错误:
//不是简单参数指的是包含解构赋值
function foo(a=1,...c){
'use strict' //SyntaxError
}
//如果'use strict'在函数体外定义则没有错误
'use strict'
function foo(a=1,...c){} //ok
函数体以及函数参数不能直接出现super
function foo(super){} //SyntaxError
function foo(){ super();} //SyntaxError
FunctionDeclaration
FunctionDeclaration分为带变量名的函数声明以及匿名函数声明,匿名函数声明只能在export中可用,其它任何地方使用匿名函数声明都报错。
在进行评估脚本和函数的时候会对包含在其中的函数声明进行InstantiateFunctionObject方法,即初始化函数对象。注:该方法是在执行脚本和函数代码之前进行的。
InstantiateFunctionObject方法简单来说做了三步:1.FunctionCreate 2.makeConstructor 3. SetFunctionName。分开说
- FunctionCreate创建了一个Function对象F,包括初始化内部插槽的值,比如上面提到的[[Call]],[[Construct]]方法的定义,原型对象[[Prototype]]的值,这里指的是Function.prototype,F的length属性,指function的参数个数。
- makeConstructor(F),这句话不是指创建构造器,这里指定义了F的prototype属性的值,以及prototype中constructor的值,普通函数prototype相当于object.create(Object.prototype),constructor===F。
- SetFunctionName,定义了F的name属性,默认是声明的函数名,匿名函数是default。
function foo(a,b){};
foo.__proto__ === Function.prototype;
foo.length === 2;
foo.prototype.__proto__ === Object.prototype;
foo.prototype.constructor === foo;
foo.name === 'foo';
FunctionExpression
FunctionExpression也分为两类,有变量名的函数表达式和匿名函数表达式。函数表达式在执行时也会创建Function对象,步骤和函数声明相似。其中匿名函数表达式不会定义属性name,即不会执行第三步中的SetFunctionName。有变量名的函数表达式与函数声明以及匿名函数表达式的区别在于作用域链,我们都知道一旦函数表达式中定义了变量名,我们就可以在函数体内通过该变量名调用函数自身。可问题来了,该函数变量名是定义在哪里呢?函数外还是在函数内呢?
var func = function foo(){};
foo(); //Uncaught ReferenceError: foo is not defined
//显然没有在函数外定义函数表达式的变量名,那么是定义在函数内的?
//我提到过在全局作用域和函数作用域中,var、function声明的变量,let和const不能重复声明。
var func = function foo(){
let foo = 1; //ok,可见函数表达式的变量名也不是在函数内声明的。
};
foo();
看到这可能有人会认为函数表达式的变量名可能允许let和const进行覆盖。其实不是,有变量名的函数表达式在创建Function对象的时候,创建了一个匿名作用域,在该作用域中定义了函数表达式的变量名。按上面这个例子,foo函数的外部作用域并不是全局作用域,而是一个匿名作用域,匿名作用域的外部作用域才是真正的全局作用域。匿名函数表达式和函数声明都不会创建匿名作用域。
ArrowFunction
ArrowFunction(箭头函数)是ES6新增的一种新语法,主要是用来简化function的写法,更准确的说是简化匿名函数表达式的一种写法。因此匿名函数表达式的规则也适用于ArrowFunction,不过两者还是有区别的,ArrowFunction中没有规定不能直接出现super,也就是说在ArrowFunction中可以用super方法,其次ArrowFunction内部没有[[Construct]]方法,因此不能作为构造器调用,所以在创建Function对象时不执行makeConstructor方法。最重要一点就是ArrowFunction没有本地的this对象。
我们上面提道所有Function对象都有[[Call]]内部方法,接受this对象和参数列表两个字段。此外Function对象还有一个[[ThisMode]]内部属性,用来判断是ArrowFunction还是非ArrowFunction,如果是ArrowFunction,那么不管[[Call]]中传来的this是什么都会被丢弃。此外arguments, super和new.target和this也是一样的。我在以前的文章中稍微提到过ArrowFunction中的this对象,我在这重新讲一下:
var name = 'outer arrow';
var obj = {
name:'inner arrow',
arrow: () => {
console.log(this.name)
}
}
obj.arrow(); //outer arrow,不是inner arrow
我们在ArrowFunction遇到this对象时,你不要把this看成是ArrowFunction的一部分,你从ArrowFunction中拿出this放到ArrowFunction的外部,观察外部的this对象是什么,外部的this对象就是ArrowFunction的this对象。此外还要清楚不管是call还是apply都是对ArrowFunction无效的,它们最终调用的都是[[Call]]内部方法,当然bind也是无效的。
我们看一下ArrowFunction中的super应用,还是改编了MDN中的例子:
var obj1 = {
method() {
console.log("method 1");
}
}
var obj2 = {
method() {
console.log("method 2");
return ()=>{super.method();}
}
}
Object.setPrototypeOf(obj2, obj1);
var arrow = obj2.method() //method 2
arrow(); //method 1
注意:method1和method2其实就是Method Definitions。
如果单看arrow这个函数,它本身是不可能有super的,因为没有任何继承关系,只是一个单一的ArrowFunction,但是你放在obj2中就有了意义,Object.setPrototypeOf(obj2, obj1);这句话把obj1变为obj2的原型对象,obj2继承了obj1的属性和方法,obj2的super对象就是obj1,因此ArrowFunction中的super参照this可知,该super是obj1。
总的一句话概括ArrowFunction中的this,arguments, super和new.target都是通过原型链来查找的,不是动态创建的。
GeneratorFunction
关于GeneratorFunction我不准备讲怎么用它,我只谈一下它的工作原理。说实话GeneratorFunction用到的情况实在太少了,我自己在做项目的时候基本不会用到GeneratorFunction,但这不妨碍我们学习GeneratorFunction。
GeneratorFunction也是Function的一种,Function的规则也适用于GeneratorFunction,此外在GeneratorFunction的参数中不能出现yield表达式。
GeneratorFunction与普通的Function一样,都会创建Function对象,但是区别也在这里,上面提到了Function的[[Prototype]]原型值是Function.prototype,但是GeneratorFunction不同,它的[[Prototype]]原型值是%Generator%,此外Function的prototype属性是ObjectCreate(Object.prototype),但是GeneratorFunction的却是ObjectCreate(%GeneratorPrototype%)而且prototype中没有constructor属性,不能作为构造器调用。
注:Function.prototype也写作%FunctionPrototype%,Object.prototype也写作%ObjectPrototype%。%Generator%和%GeneratorPrototype%没有全局名称,不能直接访问。
执行函数时,内部调用了[[Call]]方法,但是和Function不同的是GeneratorFunction返回的不是函数的执行结果,而是一个对象,这个对象是GeneratorFunction的一个实例,这跟[[Construct]]方法很像。
function* gen(){}
gen(); //返回的其实是Object.create(gen.prototype)对象。
你可以比较gen()和Object.create(gen.prototype)这两个对象,你会发现它们很像,只是Object.create(gen.prototype)缺少了Generator对象的一些内部状态。可以说虽然GeneratorFunction没有[[Construct]]方法,不能作为构造器调用,但是你可以认为GeneratorFunction本身就是一个构造器。
此外在创建GeneratorFunction对象时,还做了一些其他操作,我们在以前的文章中提到了执行上下文,GeneratorFunction对象有个[[GeneratorContext]]内部插槽,当评估GeneratorFunction定义的代码时,GeneratorFunction对象把当前正在运行的执行上下文存在了[[GeneratorContext]]中,并挂起了该正在运行的执行上下文,因此对GeneratorFunction中代码的评估被暂停了,从而执行其它代码,当你调用GeneratorFunction对象的next方法时,他把[[GeneratorContext]]中保存的执行上下文取出放到执行上下文栈顶部,成为正在运行的执行上下文,此时GeneratorFunction中暂定评估的代码又重新开始执行,直到执行完毕或者遇到yield表达式。当遇到yield表达式时,它又把正在运行的执行上下文从栈中移除,暂停对GeneratorFunction代码的执行,等待下次next方法调用之后继续执行。
简单来说GeneratorFunction的实现原理其实是运行的执行上下文之间不停来回切换。
GeneratorFunction基本就提到这里了,最后附上ECMAScript关于GeneratorFunction的一张关系图片:
结束语
关于Function的简单介绍就暂时告一段落,在下一篇文章中我会来简单介绍Promise和AsyncFunction。