函数
带参数默认值的函数
JS 函数的独特之处是可以接受任意数量的参数,而无视函数声明处的参数数量。
在 ES5 中模拟参数默认值
在下面的示例中,有一个瑕疵,此处的 timeout 的有效值实际上可能是 0,但因为 0 是假值,就会导致 timeout 的值在这种情况下会被替换为 2000 。
function makeRequest(url, timeout, callback) {
timeout = timeout || 2000;
callback = callback || function(){};
console.log(timeout)
// 函数的剩余部分
}
makeRequest("url",0); // 输出 2000
在这种情况下,更安全的替代方法是使用 typeof 来检测参数的类型,正如下例,这种方法有一个不足之处,为了实现一个基本需求而书写了过多的代码。
function makeRequest(url, timeout, callback) {
timeout = (typeof timeout !== "undefined") ? timeout :2000;
callback = (typeof callback !== "undefined") ? callback :function(){};
// 函数的剩余部分
}
ES6 中的参数默认值
ES6 能更容易地为参数提供默认值,它使用了初始化形式,以便在参数未被正式传递进来时使用。示例如下:
function makeRequest(url,timeout = 2000,callback = function(){}){
// 函数的剩余部分
}
此函数只要求第一个参数始终要被传递,其余两个参数则都有默认值,这使得函数体更为小巧,因为不需要再添加更多代码来检查缺失的参数值。
在函数声明中能指定任意一个参数的默认值,即使该参数排在未指定默认值的参数之前也是可以的。示例如下:
function makeRequest(url, timeout = 2000,callback){
// 函数的剩余部分
}
在本例中,只有在未传递第二个参数、或明确将第二个参数值指定为 undefined 时, timeout 的默认值才会被使用,例如:
// 使用默认的 timeout
makeRequest("/foo", undefined, function(body){
doSomething(body);
});
// 使用默认的 timeout
makeRequest("/foo");
// 不使用默认值
makeRequest("/foo",null,function(body){ // 在这个例子中,null 值被认为是有效的,意味着对于 makeRequest()的第三次调用并不会使用 timeout 的默认值。
doSomething(body);
});
参数默认值如何影响 arguments 对象
在 ES5 的非严格模式下,arguments 对象会反映出具名参数的变化,示例如下:
function mixArgs(first, second){
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d";
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs("a","b"); // 输出四个: true
在非严格模式下,arguments 对象总是会被更新以反映出具名参数的变化;在 ES5 的严格模式下,关于 arguments 对象的这种混乱情况被消除了,它不再反映出具名参数的变化。在严格模式下重新使用上例:
function mixArgs(first, second){
"use strict";
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d";
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs("a","b"); // 输出 true true false false
在使用 ES6 参数默认值的函数中,arguments 对象的表现总是会与 ES5 的严格模式一致,无论此函数是否明确运行在严格模式下。参数默认值的存在触发了 arguments 对象与具名参数的分离。这是个细微但重要的细节,因为 arguments 对象的使用方式发生了变化。示例如下:
// 非严格模式
function mixArgs(first,second = "b") {
console.log(arguments.length);
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d";
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs("a"); // 输出为:1 true false false false
本例中 arguments.length 的值为 1 ,因为只给 mixArgs() 传递了一个函数。意味着 arguments[1] 的值是 undefined ,符合将单个参数传递给函数时的预期;这同时意味着 first 与 arguments[0] 是相等的。改变 first 和 second 的值不会对 arguments 对象造成影响,无论是否在严格模式下,所以你可以始终依据 arguments 对象来反映初始调用状态。
参数默认值表达式
参数默认值最有意思的特性或许就是默认值并不要求一定是基本类型的值。你可以执行一个函数来产生参数的默认值,示例如下,注意:getValue() 函数若被写为可变的,则它有可能会返回可变的值,如示例2。
// 示例1
function getValue() {
return 5;
}
function add(first, second = getValue()) {
return first + second;
}
console.log(add(1,1)); // 2
console.log(add(1)); // 6
// 示例2
let value = 5;
function getValue() {
return value++;
}
function add(first, second = getValue()) {
return first + second;
}
console.log(add(1,1)); // 2
console.log(add(1)); // 6
console.log(add(1)); // 7
将函数调用作为参数的默认值时需要小心,如果你遗漏了括号,例如在上面例子中使用 second = getValue,你就传递了对于该函数的一个引用,而没有传递调用该函数的结果。
上述行为引出了另一种有趣的能力:可以将前面的参数作为后面参数的默认值,示例如下:
function add(first, second = first) {
return first + second;
}
console.log(add(1,1)); // 2
console.log(add(1)); // 2
你可以将 first 作为参数传递给一个函数来产生 second 参数的值,示例如下:
function getValue(value){
return value + 5;
}
function add(first, second = getValue(first)){
return first + second;
}
console.log(add(1,1)); // 2
console.log(add(1)); // 7
引用其他参数来为参数进行默认赋值时,仅允许引用前方的参数,因此前面的参数不能访问后面的参数,示例如下:
function add(first = second,second){
return first + second;
}
console.log(add(1,1)); //2
console.log(add(undefined,1)); //抛出错误,
补充两点:
- 这里将第一个参数赋值为 undefined,它会取默认值,这是"ES6 中的参数默认值"那节的知识;
- 这里发生错误的原因是因为 second 在 first 之后定义,因此不能将其作为后者的默认值。要理解为何会发生这种情况,需要后顾“暂时性死区”的知识。
参数默认值的暂时性死区
与 let 声明相似,函数每个参数都会创建一个新的标识符绑定,它在初始化之前不允许被访问,否则会抛出错误。示例如下:
function add(first = second, second) {
return first + second;
}
console.log(add(1,1)); // 2
console.log(add(undefined,1)); // 抛出错误
在本例中调用 add(1,1) 与 add(undefined,1) 对应着以下的后台代码:
// JS 调用 add(1,1) 可表示为
let first = 1;
let second = 1;
// JS 调用 add(1) 可表示为
let first = second;
let second = 1;
本例中调用 add(undefined,1) 抛出了错误,是因为在 first 被初始化时 second 尚未被初始化。此处的 second 存在于暂时性死区内,对于 second 的引用就抛出了错误。
函数参数拥有各自的作用域和暂时性死区,与函数体的作用域相分离,这意味着参数的默认值不允许访问在函数体内部声明的任意变量。
使用不具名参数
到目前为止,本章的例子只涵盖了在函数定义中的已被命名的参数。然而 JS 的函数并不强求参数的数量要等于已定义具名参数的数量,你所传递的参数总是运行少于或多于正式指定的参数。参数的默认值让函数在接收更少参数时的行为更清晰,而 ES6 试图让相反情况的问题也被更好地解决。
ES5 中的不具名参数
JS 早就提供了 arguments 对象用于查看传递给函数的所有参数,这样就不必分别指定每个参数。虽然查看 arguments 对象在大多数情况下都工作正常,但操作它有时仍然比较麻烦。
剩余参数
剩余参数(rest parameter)由三个点(…)与一个紧跟着的具名参数指定,它会是包含传递给函数的其余参数的一个数组,名称中的“剩余”也由此而来。示例如下:
function pick(object,...keys){
let result = Object.create(null);
for(let i = 0,len = keys.length; i < len; i++){
result[keys[i]] =object[keys[i]];
}
return result;
}
函数的 length 属性用于指示具名参数的数量,而剩余参数对其毫无影响。此例中 pick() 函数的 length 属性值是 1 ,因为只有 object 参数被用于计算该值。
剩余参数的限制条件
- 函数只能有一个剩余参数,并且它必须被放在最后。示例如下:
此处的 last 跟在了剩余参数 keys 后面,这会导致一个语法错误。// 语法错误:不能在剩余参数后使用具名参数 function pick(object,...keys,last){ let result = Object.create(null); for(let i = 0; i < keys.length;i++){ resylt[keys[i]] = object[keys[i]]; } return result; }
- 剩余参数不能在对象字面量的 setter 属性中使用,示例如下:
存在此限制的原因是:对象字面量的 setter 被限定只能使用单个参数;而剩余参数按照定义是不限制参数数量的,因此它在此处不被许可。let object = { // 语法错误:不能在 setter 中使用剩余参数 set name(...value) { // 一些操作 } };
剩余参数如何影响 arguments 对象
-
设计剩余参数是为了替代 ES 中的 arguments。原先在 ES4 中就移除了 arguments 并添加了剩余参数,以便允许向函数传入不限数量的参数。尽管 ES4 从未被实施,但这个想法被保持下来并在 ES6 中被重新引入,虽然 arguments 仍未在语言中被移除。
arguments 对象在函数被调用时反映了传入的参数,与剩余参数能协同工作,示例如下:function checkArgs(...args){ console.log(args.length); console.log(arguments.length); console.log(args[0],arguments[0]); console.log(args[1],arguments[1]); } checkArgs("a", "b"); // 输出为 // 2 // 2 // a a // b b
arguments 对象总能正确反映被传入函数的参数,而无视剩余参数的使用。这已是对剩余参数真正了解的全部内容,你可以开始使用它们了。
函数构造器的增强能力
Function
构造器允许你动态创建一个函数,它的参数都是字符串,示例如下:
var add = new Function("first","second","return first + second");
console.log(add(1, 1)); // 2
ES6
增强了Function
构造器的能力,允许使用默认参数以及剩余参数。示例如下:
// 默认值参数 示例
var add = new Function("first","second = first","return first + second");
console.log(add(1,1)); // 2
console.log(add(1)); // 2
// 剩余参数 示例
var pickFirst = new Function("...args","return args[0]");
console.log(pickFirst(1,2)); // 1
默认值参数和剩余参数的添加,确保了Function
构造器拥有与函数声明形式相同的所有能力。
扩展运算符
与剩余参数关联最密切的就是扩展运算符。剩余参数允许你把多个独立的参数合并到一个数组中;而扩展运算符则允许将一个数组分割,并将各个项作为分离的参数传给函数。示例如下:
let values = [25, 50, 75, 100];
// 等价于 console.log(Math.max(25, 50, 75, 100));
console.log(Math.max(...values));
可以将扩展运算符与其他参数混用。假设你想让Math.max()
返回的最小值为 0 (以防数组中混入了负值),你可以将参数 0 单独传入,并继续为其他参数使用扩展运算符,示例如下:
let values = [-25, -50, -75, -100];
//等价于 console.log(Math.max(-25,-50,-75,-100,0));
console.log(Math.max(...values,0)); // 0
用扩展运算符传递参数,使得更容易将数组作为函数参数来使用,你会发现在大部分场景中扩展运算符都是apply()
方法的合适替代品。在ES6
中还可以在JS
的Function
构造器中使用这两类参数。
ES6 的名称属性
定义函数有各种各样的方式,在 JS 中识别函数就变得很有挑战性。此外,匿名函数表达式的流行使得调试有点困难,经常导致堆栈跟踪难以被阅读与解释。正因为如此,ES6 给所有函数添加了name
属性。
选择合适的名称
function doSomething(){
//...
}
var doAnotherThing = function(){
//...
};
console.log(doSomething.name); // "doSomething"
console.log(doAnotherThing.name); // "doAnotherThing"
匿名函数的名称属性在 FireFox 与 Edge 中仍然不被支持(值为空字符串),而 Chrome 直到 51.0 版本才提供了该特性。
名称属性的特殊情况
虽然函数声明与函数表达式的名称易于查找,但ES6更进一步确保了所有函数都拥有合适的名称,示例如下:
var doSomething = function doSomethingElse(){
//...
};
var person = {
get firstName(){
return "Nicholas"
},
sayName:function(){
console.log(this.name);
}
}
console.log(doSomething.name); // "doSomethingElse"
console.log(person.sayName.name); // "sayName"
var descriptor = Object.getOwnPropertyDescriptor(person, "firstName");
console.log(descriptor.get.name); // "get firstName"
本例中的 doSomething.name
的值是"doSomethingElse"
,因为该函数表达式自己拥有一个名称,并且此名称的优先级要高于辅助目标的变量名。person.firstName
实际是个 getter 函数,因此它的名称是"get firstName"
,以标明它的特征;同样,setter 函数也会带有"set"
的前缀(getter 与 setter 函数都必须用 Object.getOwnPropertyDescriptor()
来检索)。
函数名称还有另外两个特殊情况。使用bind()
创建的函数会在名称属性值之前带有"bound"
前缀;而使用 Function
构造器创建的函数,其名称属性则会有"anonymous"
前缀,示例如下:
var doSomething = function() {
//...
};
console.log(doSomething.bind().name); // "bound doSomething"
console.log((new Function()).name); // "anonymous"
绑定产生的函数拥有原函数的名称,并总会附带"bound"
前缀,因此doSomething()
函数的绑定版本就具有"bound doSomething"
名称。
需要注意的是,函数的
name
属性值未必会关联到同名变量。name
属性是为了在调试是获得有用的相关信息,所以不能用name
属性值去获取对函数的引用。
明确函数的双重用途
在ES5以及更早版本中,函数根据是否使用new
来使用而有双重用途。当使用new
时,函数内部的this
是一个新对象,并作为函数的返回值,示例如下:
function Person(name){
this.name = name;
}
var person = new Person("Nicholas");
var notAPerson = Person("Nicholas");
console.log(person); // "[Object object]"
console.log(notAPerson); // "undefined"
当创建notAPerson
时,未使用new
来调用Person()
,输出了undefined
(并且在非严格模式下给全局对象添加了name
属性)。
JS 为函数提供了两个不同的内部方法:[[Call]]
与 [[Construct]]
。当函数未使用new
进行调用时,[[Call]]
方法会被执行,运行的是代码中显示的函数体。而当函数使用new
进行调用时,[[Construct]]
方法则会被执行,负责创建一个被称为新目标的新的对象,并且使用该新目标作为this
去执行函数体。拥有[[Construct]]
方法的函数被称为构造器。
记住并不是所有函数都拥有
[[Construct]]
方法,因此不是所有函数都可以用new
来调用。在“箭头函数”小节中介绍的箭头函数就未拥有该方法。
在ES5中判断函数如何被调用
在ES5中判断函数是不是使用了new
来调用(即作为构造器),最流行的方式是使用instanceof
,示例如下:
function Person(name) {
if (this instanceof Person){
this.name = name; // 使用 new
}else{
throw new Error("You must use with Person.")
}
}
var person = new Person("Nicholas");
var notAPerson = Person("Nicholas"); // 抛出错误
此处对this
值进行了检查,来判断其是否为构造器的一个实例:若是,正常继续执行;否则抛出错误。这能奏效是因为[[Construct]]
方法创建了Person
的一个新实例并将其赋值给this
。可惜的是,该方法并不绝对可靠,因为在不使用new
的情况下this
仍然可能是Person
的实例,实例如下:
function Person(name) {
if (this instanceof Person){
this.name = name;
} else {
throw new Error("You must use new with Person.")
}
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person,"Nicholas"); //奏效了!
调用Person.call()
并将person
变量作为第一个参数传入,这意味着将Person
内部的this
设置为了person
。对于该函数来说,没有任何方法能将这种方式与使用new
调用区别开来。
new.target 元属性
为了解决这个问题,ES6 引入了 new.target 元属性。元属性指的是“非对象”(例如 new )上的一个属性,并提供关联到它的目标的附加信息。当函数的[[Construct]]
方法被调用时,new.target
会被填入new
运算符的作用目标,该目标通常是新创建的对象实例的构造器,并且会成为函数体内部的this
值。而若[[Call]]
被执行,new.target
的值则会是undefined
。
通过检查new.target
是否被定义,这个新的元属性就让你能安全地判断函数是否被使用new
进行了调用。示例如下:
function Person(name) {
if (typeof new.target !== 'undefined') {
this.name = name; // 使用 new
}else{
throw new Error("You must use new with Person")
}
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person,"Michael"); // 出错!
使用 new.target
而非this instanceof Person
,Person
构造器会在未使用new
调用时正确地抛出错误。
也可以检查new.target
是否被使用特定构造器进行了调用,示例如下:
function Person(name) {
if (new.target === Person) {
this.name = name; // 使用 new
}else{
throw new Error("You must use new with Person")
}
}
function AnotherPerson(name) {
Person.call(this,name);
}
var person = new Person("Nicholas");
var anotherPerson = new AnotherPerson("Michael");// 出错!
在此代码中,为了正确工作,new.target
必须是Person
。当调用new AnotherPerson("Michael")
时,Person.call(this,name)
也随之被调用,从而抛出了错误,因为此时在Person
构造器内部的new.target
值为undefined
(Person
并未使用new
调用)。
警告:在函数之外使用
new.target
会有语法错误。
ES6 通过新增new.target
而消除了函数调用方面的不确定性。在该主题上,ES6 还随之解决了本语言之前另一个不确定的部分————在代码块内部声明函数。
块级函数
在 ES3 或更早版本中,在代码块中声明函数(即块级函数)严格来说应当是一个语法错误, 但所有的浏览器却都支持该语法。可惜的是,每个支持该语法的浏览器都有轻微的行为差 异,所以最佳实践就是不要在代码块中声明函数(更好的选择是使用函数表达式)。
决定何时使用块级函数
块级函数与let
函数表达式相似,在执行流跳出定义所在的代码块之后,函数定义就会被移除。关键区别在于:块级函数会被提升到所在代码块的顶部;而使用let
的函数表达式则不会,示例如下:
"use strict";
if (true) {
console.log(typeof doSomething);
let doSomething = function () {
// ...
}
doSomething();
}
console.log(typeof doSomething);
此处代码在typeof doSomething
被执行时中断了,因为let
声明尚未被执行,将doSomething()
放入了暂时性死区。知道这个区别之后,你就可以根据是否想要提升来选择应当使用块级函数还是let
表达式。
非严格模式的块级函数
ES6 在非严格模式下同样允许使用块级函数,但行为有细微不同。块级函数的作用域会被提升到所在函数或全局环境的顶部,而不是代码块的顶部。
// ES6 behavior
if (true) {
console.log(typeof doSomething); // "function"
function doSomething() {
// ...
}
doSomething();
}
console.log(typeof doSomething); // "function"
本例中的doSomething()
会被提升到全局作用域,因此在if
代码块外部它仍然存在。ES6 标准化了这种行为来移除浏览器之前存在的不兼容性,于是在所有 ES6 运行环境中其行为都会遵循相同的方式。
允许使用块级函数增强了在JS中声明函数的能力,但ES6还引入了一种全新的声明函数的方式。
箭头函数
ES6 最有意思的一个新部分就是箭头函数(arrow function)。箭头函数正如名称所示那样使用一个“箭头”(=>)来定义,但它的行为在很多重要方面与传统的JS函数不同:
- 没有
this
、super
、arguments
,也没有new.target
绑定:this
、super
、arguments
、以及函数内部的new.target
的值由所在的、最靠近的非箭头函数来决定。 - 不能被使用
new
调用:箭头函数没有[[Construct]]
方法,因此不能被用为构造数,使用new
调用箭头函数会抛出错误。 - 没有原型:既然不能对箭头函数使用
new
,那么它也不需要原型,也就是没有prototype
属性。 - 没有
arguments
对象:既然箭头函数没有arguments
绑定,你必须依赖于具名参数或剩余参数来访问函数的参数。 - 不允许重复的具名参数:箭头函数不允许拥有重复的具名参数,无论是否在严格模式下;而相对来说,传统函数只有在严格模式下才禁止这种重复。
产生这些差异是有理由的:
- 在JS编程中
this
绑定是发生错误的常见根源之一,在嵌套的函数中有时会因为调试方式的不同,而导致丢失对外层this
值的追踪,就可能会导致预期外的程序行为; - 箭头函数使用单一的
this
值来执行代码,使得JS引擎可以更容易对代码的操作进行优化;而常规函数可能会作为构造函数使用(导致this
易变而不利优化。 - 其余差异也聚集在减少箭头函数内部的错误与不确定性,这样JS引擎也能更好地优化箭头函数的运行。
注意:箭头函数也拥有
name
属性,并且遵循与其他函数相同的规则。
箭头函数语法
箭头函数的语法可以有多种变体,取决于你要完成的目标。所有变体都以函数参数为开头。示例如下:
var reflect = value => value;
//有效等价于:
var reflect = function(value) {
return value;
}
如果需要传入多个参数时,示例如下:
var sum = (num1, num2) => num1 + num2;
// 有效等价于:
var sum = function(num1, num2) {
return num1 + num2;
};
如果函数没有任何参数,示例如下:
var getName = () => "Nicholas";
//有效等价于:
var getName = function(){
return "Nicholas";
};
当你想使用更传统的函数体、也就是可能包含多个语句的时候,需要将函数体用一对花括号进行包裹,并明确定义一个返回值,示例如下:
var sum = (num1, num2) => {
return num1 + num2;
};
//有效等价于:
var sum = function(num1, num2) {
return num1 + num2;
};
你基本可以将花括号内部的代码当做传统函数那样对待,除了
arguments
对象不可用之外。
若你想创建一个空函数,就必须使用空的花括号,示例如下:
var doNothing = () => {};
// 有效等价于:
var doNothing = function() {};
花括号被用于表示函数的主体,它在你至今看到的例子中都能正常工作。但若箭头函数想要从函数体内向外返回一个对象字面量,就必须将该字面量包裹在圆括号内,示例如下:
var getTempItem = id => ({ id:id, name: "Temp"});
//有效等价于:
var getTempItem = function(id){
return {
id:id,
name: "Temp"
};
};
将对象字面量包裹在括号内,标识了括号内是一个字面量而不是函数体。
创建立即调用函数表达式
JS中使用函数的一种流行方式是创建立即调用函数表达式(immediately-invoked function expression,IIFE)。IIFE 允许你定义一个匿名函数并在未保存引用的情况下立刻调用它。当你想创建一个作用域并隔离在程序其他部分外,这种模式就很有用了。示例如下:
//传统函数形式
let person = function(name) {
return {
getName:function(){
return name;
}
};
}("Nicholas");
console.log(person.getName()); // "Nicholas"
// 箭头函数形式
let person = ((name)=>{
return {
getName:function(){
return name;
}
}
})("Nicholas");
console.log(person.getName()); // "Nicholas"
需要注意的是括号仅包裹了箭头函数的定义,并未包裹(“Nicholas”)。这有别与使用传统函数时的方式————括号既可以连函数定义与参数调用一起包裹,也可以只用于包裹函数定义。
译注:使用传统函数时,
(function(){/*函数体*/})();
与(function(){/*函数体*/}())
;这两种方式都是可行的。
但若是使用箭头函数,则只有下面的写法是有效的:(() => {/*函数体*/})()
;
没有 this 绑定
JS 最常见的错误领域之一就是在函数内的this
绑定。由于一个函数内部的this
值可以被改变,这取决于调用该函数时的上下文,因此完全可能错误地影响了一个对象,尽管你本意是要修改另一个对象。研究如下例子:
var PageHandler = {
id:"123456",
init:function() {
document.addEventListener("click",(function(event){
this.doSomething(event.type); //没有错误
}).bind(this),false);
},
doSomething:function(type){
console.log("Handling "+type+" for "+this.id);
}
};
此代码的PageHandler
对象被设计用于处理页面上的交互。init()
方法被调用以建立该交互,并注册了一个事件处理函数来调用this.doSomething()
。然而此代码并未按预期工作。
调用this.doSomething()
被中断是因为this
是对事件目标对象(在此案例中就是document
)的一个引用,而不是被绑定到PageHandler
上。若试图运行此代码,你将会在事件处理函数被触发时得到一个错误,因为this.doSomething()
并不存在于document
对象上。
你可以明确使用bind()
方法将函数的this
值绑定到PageHandler
上,以修正这段代码,示例如下:
var PageHandler = {
id:"123456",
init: function(){
document.addEventListener("click",(function(event){
this.doSomething(event.type); // 没有错误
}).bind(this),false);
},
doSomething: function(type){
console.log("Handling " + type+" for " + this.id);
}
};
现在此代码能像预期那样运行,但看起来有点奇怪。通过调用bind(this)
,你实际上创建了一个新函数,它的this
被绑定到当前this
(也就是PageHandler
)上。为了避免额外创建一个函数,修正此代码的更好方式是使用箭头函数。
箭头函数没有this
绑定,意味着箭头函数内部的this
值只能通过查找作用域来确定。如果箭头函数被包含在一个非箭头函数内,那么this
值就会与该函数的this
相等;否则,this
值就会是全局对象(在浏览器中是window
,在nodejs
中是global
)。你可以使用箭头函数来书写如下代码:
var PageHandler = {
id:"123456",
init: function() {
document.addEventListener("click", event => this.doSomething(event.type),false);
},
doSomething:function(type){
console.log("Handling " + type+" for " + this.id);
}
};
本例中调用this.doSomething()
的箭头函数,它的this
值与init()
方法的相同,类似使用bind(this)
。尽管doSomething()
方法并不返回任何值,它任然是函数体内唯一被执行的语句,因此无须使用花括号来包裹它。
箭头函数被设计为“抛弃型”的函数,因此不能被用于定义新的类型;prototype
属性的缺失让这个特性显而易见。对箭头函数使用new
运算符会导致错误,示例如下:
var MyType = () => {},
object = new MyType(); // 错误:你不能对箭头函数使用 "new"
此代码调用new MyType()
的操作失败了,由于MyType()
是一个箭头函数,它就不存在[[Construct]]
方法。
由于箭头函数的this
值由包含它的函数决定,因此不能使用call()
、apply()
或bind()
方法来改变其this
值。
箭头函数与数组
箭头函数的简洁语法也让它成为进行数组操作的理想选择。例如,若你想使用自定义比较器 来对数组进行排序,通常会这么写:
var result = values.sort(function(a,b){
return a-b;
})
//箭头函数形式
var result = values.sort((a,b)=> a-b);
能使用回调函数的数组方法(例如sort()
、map()
与reduce()
方法),都能从箭头函数的简洁语法中获得收益,它将看似复杂的需求转换为简单的代码。
没有 arguments 绑定
尽管箭头函数没有自己的arguments
对象,但仍然能访问包含它的函数的arguments
对象。无论此后箭头函数在何处执行,该对象都是可用的。例如:
function createArrowFunctionReturningFirstArg(){
return () => arguments[0];
}
var arrowFunction = createArrowFunctionReturningFirstArg(5);
console.log(arrowFunction()); // 5
在createArrowFunctionReturningFirstArg()
内部,arguments[0]
元素被已创建的箭头函数arrowFunction
所引用,该引用包含了传递给createArrowFunctionReturningFirstArg()
函数的首个参数。当箭头函数在此后被执行时,它返回了5,这也正是传递给createArrowFunctionReturningFirstArg()
的首个参数。尽管箭头函数arrowFunction
已不在创建它的函数的作用域,但由于arguments
标识符的作用域链解析,arguments
对象依然可被访问。
识别箭头函数
尽管语法不同,但箭头函数依然属于函数,并能被照常识别。研究如下代码:
var comparator = (a,b) => a -b;
console.log(typeof(comparator)); // "function"
console.log(comparator instanceof Function); // true
console.log()
的输出揭示了typeof
与instanceof
在作用于箭头函数时的行为,与作用在其他函数上完全一致。
也像对其他函数那样,你仍然可以对箭头函数使用call()
、apply()
与bind()
方法,虽然函数的this
绑定并不会受影响。这里有几个例子:
var sum = (num1, num2) => num1 + num2;
console.log(sum.call(null,1,2)); //3
console.log(sum.apply(null,[1,2])); // 3
var boundSum = sum.bind(null,1,2);
console.log(boundSum()); // 3
在这里说一点:
call()
、apply()
、bind()
三者都是将this
重定向到第一个参数上;call()
和bind()
的剩余参数使用逗号隔开传进去,apply()
则是使用数组传递剩余参数;bind()
是创建一个新的函数,使用时记得调用。
箭头函数能再任意位置替代你当前使用的匿名函数,例如回调函数。
尾调用优化
在ES6中对函数最有趣的改动或许就是一项引擎优化,它改变了尾部调用的系统。尾调用(tail call)指的是调用函数的语句是另一个函数的最后语句,就像这样:
function doSomething(){
return doSomethingElse(); // 尾调用
}
在 ES5 引擎中实现的尾调用,其处理就像其他函数调用一样:一个新的栈帧(stack frame)被闯将并推到调用栈之上,用于表示该次函数调用。这意味着之前每个栈帧都被保留在内存中,当调用栈太大时会出问题。
有何不同?
ES6 在严格模式下力图为特定尾调用减少调用栈的大小(非严格模式的尾调用则保持不变)。当满足以下条件时,尾调用优化会清除当前栈帧并再次利用它,而不是为尾调用创建新的栈帧:
- 尾调用不能引用当前栈帧中的变量(意味着该函数不能是闭包);
- 进行尾调用的函数在尾调用返回结果后不能做额外操作;
- 尾调用的结果作为当前函数的返回值。
作为一个例子,下面代码满足了全部三个条件,因此能被轻易地优化:
"use strict";
function doSomething(){
// 被优化
return doSomethingElse();
}
该函数对doSomethingElse()
进行了一次尾调用,并立即返回了其结果,同时并未访问局部作用域的任何变量。一个小改动————不返回结果,就会产生一个无法被优化的函数:
"use strict";
function doSomething(){
// 未被优化:缺少 return
doSomethingElse();
}
类似的,如果你的函数在尾调用返回结果之后进行了额外操作,那么该函数也无法被优化:
"use strict";
function doSomething(){
// 未被优化:在返回之后还要执行加法
return 1 + doSomethingElse();
}
此例在doSomethingElse()
的结果上对其进行了加 1 操作,而没有直接返回结果,这已足以关闭优化。
无意中关闭优化的另一个常见方式,是将函数调用的结果储存在一个变量上,之后才返回了结果,就像这样:
"use strict";
function doSomething(){
// 未被优化:调用并不在尾部
var result = doSomethingElse();
return result;
}
本例之所以不能被优化,是因为doSomethingElse()
的值并没有立即被返回。
使用闭包或许就是需要避免的最困难情况,因为闭包能够访问上层作用域的变量,会导致尾调用优化被关闭。例如:
"use strict";
function doSomething(){
var num = 1,
func = () => num;
//未被优化:此函数是闭包
return func();
}
此例中闭包func()
需要访问局部变量num
,虽然调用func()
后立即返回了其结果,但是对于num
的引用导致优化不会发生。
如何控制尾调用优化
在实践中,尾调用优化在后台进行,所以不必对此考虑太多,除非要尽力去优化一个函数。尾调用优化的主要用例是在递归函数中,而且在其中的优化具有最大效果。考虑以下计算阶乘的函数:
function factorial(n) {
if (n <= 1){
return 1;
} else {
//未被优化:早返回之后还要执行乘法
return n * factorial(n-1);
}
}
此版本的函数并不会被优化,因为在递归调用factorial()
之后还要执行乘法运算。如果n
是一个大数字,那么调用栈的大小会增长,并且可能导致堆栈溢出。
为了优化此函数,你需要确保在最后的函数调用之后不会发生乘法运算。为此你可以使用一个默认参数来讲乘法操作移出return
语句。有结果的函数携带着临时结果进入下一次迭代,这样创建的函数到的功能与前例相同,但它能被ES6的引擎所优化。此处是新的代码:
function factorial(n,p = 1){
if(n <= 1) {
return 1 * p;
} else {
let result = n * p;
//被优化
return factorial(n-1,result);
}
}
在重写的factorial()
函数中,添加了第二个参数p
,其默认值为1
。p
参数保存着前一次乘法的结果,因此下一次的结果就能在进行函数调用之前被算出。当n
大于1
时,会先进行乘法运算并将其结果作为第二个参数传入factorial()
。这就允许ES6引擎去优化这个递归调用。
尾调用优化是你在书写任意递归函数时都需要考虑的因素,因为它能提供显著的性能提升,尤其是被应用到计算复杂度很高的函数时。