4.1 定义
在使用函数之前,需要定义函数,有多种定义方式可以使用。
通过function关键字声明一个函数,示例如下。
function foo(a){ console.log(a); }
在之前,我们已经了解了变量的声明提前(var hoisting),函数也是存在声明提前的,上述代码中,通过function定义的函数可以在声明之前调用,示例如下。
foo();
function foo(a){ console.log(a); }
函数声明提前与变量声明提前的不同点在于,函数声明会在执行环境创建时将函数的引用地址赋值给函数名,因此,通过函数声明创建的函数可以在函数声明前调用。
通过表达式定义一个函数,示例如下。
const foo = function(a){ console.log(a); };
也可以在函数表达式中为函数创建一个名称,不过这个名称只能在函数内部使用,在递归时会用到这种方式,示例如下。
通过构造函数定义一个函数,示例如下。
const foo = new Function("a","console.log(a);");
4.1.1 返回值
函数拥有返回值,默认为undefined,可在函数内部使用return关键字终止函数执行并返回指定值。
示例代码:
4.1.2 箭头函数(Arrow Function)
箭头函数是ES6中新增的一种函数定义方式,使用=>来快速定义一个函数,箭头函数要比普通函数/函数表达式更简洁,示例如下。
const foo = x=>x*x;
foo(2); // -> 4
上述代码中的foo函数等价于:
const foo = function(x){ return x*x }
1. 箭头函数的括号问题
如果函数的参数只有一个,可以省略参数的圆括号,但如果要定义的函数有多个参数,则不能省略圆括号,需要使用圆括号包裹这些参数,示例如下。
没有参数时,圆括号也不能省略,示例如下。
() => { ... }
2. 箭头函数的花括号问题
你可能已经注意到了,上面的箭头函数中,有些有花括号,有些则没有。如果函数中只包含一条语句,可以省略花括号,反之,则不能省略花括号,示例如下。
3. 箭头函数中的this
箭头函数中的this是基于词法作用域(Lexical scoping,又称“静态作用域”)的,放弃了所有普通this绑定的规则。在词法作用域中,一切变量(包括this)都是根据作用域链来查找的,在创建箭头函数时,就保存好了上层上下文的作用域链,在箭头函数中访问this时,就会去作用域链中查找,因此,箭头函数看起来像是“继承”了外层函数的this绑定。
示例代码:
上述代码中,箭头函数的this指向的是函数定义时的作用域Counter,其内部拥有num属性,可以被访问;而不使用箭头函数的,其this按照普通this的绑定规则,指向执行时的作用域window(setTimeout中的代码是在全局作用域下执行的,如果没有绑定this,内部代码的this指向的就是window),而window下没有num2属性,因此,num2为undefined,对undefined进行递增时返回NaN。
4.1.3 关于this
上文中,我们提到了this的绑定规则,简单来说,函数执行时的上下文是谁,this就指向谁,与函数在何处定义没有任何关系。一般情况下,理解了这句话就够了,但有时候,我们可能不太确定this的指向,示例如下。
为了彻底理解this值的问题,我们就需要从其源头出发。
说到this,就不得不提到函数,函数调用的表达式为:MemberExpression Arguments。
其中,MemberExpression表示成员表达式,包含以下部分。
- PrimaryExpression
- FunctionExpression
- MemberExpression[ Expression ]
- MemberExpression.IdentifierName
- new MemberExpression Arguments
来看几个例子。
简单地归纳一下,MemberExpression为括号前面的部分,MemberExpression后面的部分即为Arguments。
在前文我们介绍了JavaScript中的一种虚拟数据类型——Reference。之所以称它虚拟,是因为其只存在于ECMAscript规范中。
Reference由三部分构成:
base的取值可以是undefined、对象、布尔值、字符串、Symbol值、数字、Environment Record(环境记录,也可以看作是作用域)。
对于foo.bar(),其MemberExpression为foo.bar,其表达式为MemberExpression.Identifier Name,ES6规范当中对该表达式的返回值进行了描述:Return a value of type Reference whose base value is bv and whose referenced name is propertyNameString, and whose strict reference flag is strict。
也就是说,表达式MemberExpression . IdentifierName返回的是一个Reference类型的数据,那么,我们就能知道foo.bar返回的Reference类型的值如下。
了解了MemberExpression和Reference后,我们就可以接触this了。
关于函数调用时的执行过程,ECMAscript规范中对其进行了描述:
EvaluateDirectCall函数内部会尝试调用内部方法[[Call]],即调用函数实际上就是调用Object的内部方法[[Call]],其中的thisValue即this,Arguments为参数。
说到底,我们关心的是this,而不是函数如何执行,将上面的过程简化,剥离出与this有关的部分:
综上,得出如下结论。
① this值取决于MemberExpression的返回值ref。
② 如果ref类型为Reference,且ref的base为Object,若ref已有this值,则this值为ref的this值,否则this值为GetBase(ref)。
③ 如果ref类型为Reference,且ref的base为Boolean、String、Number之一(简称Environment Record),则this值为GetBase(ref).WithBaseObject()。
④ 如果ref类型不是Reference,则this值为undefined(非严格模式下,this的值为undefined时,会被转换为全局对象)。
有了这些结论,我们得到之前那几行代码的返回结果:
foo.bar(); (foo.bar)(); (foo.bar = foo.bar)(); (true && foo.bar)(); (foo.bar, foo.bar)();
在前文中,我们已经值得foo.bar返回值是Reference类型:
而其reference的base为Object,根据步骤②,因为bar不是一个箭头函数,因此,其this值即为foo,根据作用域的查找机制,foo.bar()的返回值就是2了。
现在,我们确定this值不再靠感觉了,那么,其他几个例子的this值也就很容易可以确定了。
对于(foo.bar)(),其MemberExpression为(foo.bar),其表达式为(Expression),ES6规范当中对该表达式的返回值进行了描述:Return the result of evaluating Expression. This may be of type Reference。
也就是说,表达式(Expression)返回值取决于括号内Expression的运算结果,其结果可能是一个Reference类型的数据,那么(foo.bar)返回的是foo.bar,(foo.bar)()的返回值自然就是2了。
同理,(foo.bar = foo.bar)、(true && foo.bar)、(foo.bar, foo.bar)即foo.bar = foo.bar、true &&foo.bar、foo.bar, foo.bar。
而赋值运算符、逻辑运算符、逗号运算符的返回值均为:Return GetValue(rref)
GetValue(V)方法返回的是一个真实值,不是Reference类型,this值为undefined,非严格模式下会被转换为全局对象。在浏览器中,全局对象即window,但const声明的变量不会成为window的属性,因此,后面3个例子的返回值为undefined。
1. bind
在箭头函数之前,可以使用bind来创建一个指定了this值的新函数,示例如下。
// 注意,bind 返回的是一个函数 getTitle.bind(foo)(); // > JavaScript
foo.getTitle.bind(window)(); // > undefined
2. apply与call
与bind创建一个函数的副本不同,apply与call直接修改函数执行时的上下文,并执行该函数,示例如下。
getTitle.apply(foo); // > JavaScript getTitle.call(foo); // > JavaScript
apply与call方法的作用相同,都是用来修改函数执行时的上下文的,只在传递函数的参数时有区别,示例如下。
apply在传递参数时,接收的是参数组成的数组,call则依次传递每个参数,接收参数列表。另外,这两个方法都支持arguments。
在上面的示例中,传递给apply和call的this值是window,如果指定的值为null/undefined,this值会自动指向全局对象window,示例如下。
但在严格模式下,null和undefined分别指向null和undefined,示例如下。
练习
- 使用多种方式定义一个函数。