一、箭头函数
在ES6中,箭头函数是其中最有趣的新增特性。顾名思义,箭头函数是一种使用箭头(=>)定义函数的新语法,但是它与传统的JS函数有些许不同,主要集中在以下方面:
1、没有this、super、arguments和new.target
绑定箭头函数中的this、super、arguments和new.target这些值由外围最近一层非箭头函数决定
2、不能通过new关键字调用
箭头函数没有[[construct]]方法,不能被用作构造函数,如果通过new关键字调用箭头函数,程序抛出错误
3、没有原型
由于不可以通过new关键字调用箭头函数,因而没有构建原型的需求,所以箭头函数不存在prototype这个属性
4、不可以改变this绑定
函数内部的this值不可被改变,在函数的生命周期内始终保持一致
5、不支持arguments对象
箭头函数没有arguments绑定,必须通过命名参数和不定参数这两种形式访问函数的参数
6、不支持重复的命名参数
无论在严格还是非严格模式下,箭头函数都不支持重复的命名参数;而在传统函数的规定中,只有在严格模式下才不能有重复的命名参数
在箭头函数内,其余的差异主要是减少错误以及理清模糊不清的地方。这样一来,JS引擎就可以更好地优化箭头函数的执行过程。这些差异的产生有如下几个原因:
1、最重要的是,this绑定是JS程序中一个常见的错误来源,在函数内很容易对this的值失去控制,其经常导致程序出现意想不到的行为,箭头函数消除了这方面的烦恼
2、如果限制箭头函数的this值,简化代码执行的过程,则JS引擎可以更轻松地优化这些操作,而常规函数往往同时会作为构造函数使用或者以其他方式对其进行修改
注意:箭头函数同样也有一个name属性,这与其他函数的规则相同
【语法】
箭头函数的语法多变,根据实际的使用场景有多种形式。所有变种都由函数参数、箭头、函数体组成,根据使用的需求,参数和函数体可以分别采取多种不同的形式
var reflect = value => value;
// 有效等价于:
var reflect = function(value) {
return value;
};
(1)1个参数:当箭头函数只有一个参数时,可以直接写参数名,箭头紧随其后,箭头右侧的表达式被求值后便立即返回。即使没有显式的返回语句,这个箭头函数也可以返回传入的第一个参数
(2)多个参数:如果要传入两个或两个以上的参数,要在参数的两侧添加一对小括号
var sum = (num1, num2) => num1 + num2;
// 有效等价于:
var sum = function(num1, num2) {
return num1 + num2;
};
这里的sum()函数接受两个参数,将它们简单相加后返回最终结果,它与reflect()函数唯一的不同是,它的参数被包裹在小括号中,并且用逗号进行分隔(类似传统函数)
(3)无参数:如果函数没有参数,也要在声明的时候写一组没有内容的小括号
var getName = () => "huochai";
// 有效等价于:
var getName = function() {
return "huochai";
};
(4)花括号函数体:如果希望为函数编写由多个表达式组成的更传统的函数体,那么需要用花括号包裹函数体,并显式地定义一个返回值
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"
};
};
【IIFE】
JS函数的一个流行的使用方式是创建立即执行函数表达式(IIFE),可以定义一个匿名函数并立即调用,自始至终不保存对该函数的引用。当创建一个与其他程序隔离的作用域时,这种模式非常方便。
只要将箭头函数包裹在小括号里,就可以用它实现同IIFE相同的功能
let person = ((name) => {
return {
getName: function() {
return name;
}
};
})("huochai");
console.log(person.getName()); // "huochai"
//在这段代码中,IIFE通过getName()方法创建了一个新对象,将参数name作为该对象的一个私有成员返回给函数的调用者
注意:小括号只包裹箭头函数定义,没有包含("huochai"),这一点与正常函数有所不同,由正常函数定义的立即执行函数表达式既可以用小括号包裹函数体,也可以额外包裹函数调用的部分
【this】
函数内的this绑定是JS中最常出现错误的因素,函数内的this值可以根据函数调用的上下文而改变,这有可能错误地影响其他对象
var PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click", function(event) {
this.doSomething(event.type); // 错误
}, false);
},
doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};
在这段代码中,对象pageHandler的设计初衷是用来处理页面上的交互,通过调用init()方法设置交互,依次分配事件处理程序来调用this.dosomething()。然而,这段代码并没有如预期的正常运行。实际上,因为this绑定的是事件目标对象的引用(在这段代码中引用的是document),而没有绑定pageHandler,且由于this.dosonething()在目标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,也就是page Handler
可以通过一个更好的方式来修正这段代码:使用箭头函数
箭头函数中没有this绑定,必须通过查找作用城链来决定其值。如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this;否则,this的值会被设置为undefined
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()函数里的this一致,所以此版本代码的运行结果与使用bind(this)一致。虽然doSomething()方法不返回值,但是它仍是函数体内唯一的一条执行语句,所以不必用花括号将它包裹起来
箭头函数缺少正常函数所拥有的prototype属性,它的设计初衷是即用即弃,所以不能用它来定义新的类型。如果尝试通过new关键字调用一个箭头函数,会导致程序抛出错误
var MyType = () => {},
object = new MyType(); // 错误:不能对箭头函数使用 'new'
在这段代码中,MyType是一个没有[[Construct]]方法的箭头函数,所以不能正常执行new MyType()。也正因为箭头函数不能与new关键字混用,所以JS引擎可以进一步优化它们的行为。同样,箭头函数中的this值取决于该函数外部非箭头函数的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()这些可以接受回调函数的数组方法,都可以通过箭头函数语法简化编码过程并减少编码量
// 正常函数写法
[1,2,3].map(function (x) {
return x * x;
});
// 箭头函数写法
[1,2,3].map(x => x * x);
【arguments】
箭头函数没有自己的arguments对象,且未来无论函数在哪个上下文中执行,箭头函数始终可以访问外围函数的arguments对象
function createArrowFunctionReturningFirstArg() {
return () => arguments[0];
}
var arrowFunction = createArrowFunctionReturningFirstArg(5);
console.log(arrowFunction()); // 5
在createArrowFunctionReturningFirstArg()中,箭头函数引用了外围函数传入的第一个参数arguments[0],也就是后续执行过程中传入的数字5。即使函数箭头此时已不再处于创建它的函数的作用域中,却依然可以访问当时的arguments对象,这是arguments标识符的作用域链解决方案所规定的
【辨识方法】
尽管箭头函数与传统函数的语法不同,但它同样可以被识别出来
var comparator = (a, b) => a - b;
console.log(typeof comparator); // "function"
console.log(comparator instanceof Function); // true
同样地,仍然可以在箭头函数上调用call()、apply()及bind()方法,但与其他函数不同的是,箭头函数的this值不会受这些方法的影响
包括回调函数在内所有使用匿名函数表达式的地方都适合用箭头函数来改写
【函数柯里化】
柯里化是一种把接受多个参数的函数变换成接受一个单一参数的函数,并且返回(接受余下的参数而且返回结果的)新函数的技术
如果使用ES5的语法来写,如下所示
function add(x){
return function(y){
return y + x;
};
}
var addTwo = add(2);
addTwo(3); // => 5
add(10)(11); // => 21
使用ES6的语法来写,如下所示
var add = (x) => (y) => x+y
一般来说,出现连续地箭头函数调用的情况,就是在使用函数柯里化的技术
二、尾调用优化
ES6关于函数最有趣的变化可能是尾调用系统的引擎优化。尾调用指的是函数作为另一个函数的最后一条语句被调用
function doSomething() {
return doSomethingElse(); // 尾调用
}
尾调用之所以与其他调用不同,就在于它的特殊的调用位置
我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A
的内部调用函数B
,那么在A
的调用帧上方,还会形成一个B
的调用帧。等到B
运行结束,将结果返回到A
,B
的调用帧才会消失。如果函数B
内部还调用函数C
,那就还有一个C
的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了
尾调用优化(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存
ES6缩减了严格模式下尾调用栈的大小(非严格模式下不受影响),如果满足以下条件,尾调用不再创建新的栈帧,而是清除并重用当前栈帧
1、尾调用不访问当前栈帧的变量(也就是说函数不是一个闭包)
2、在函数内部,尾调用是最后一条语句
3、尾调用的结果作为函数值返回
以下这段示例代码满足上述的三个条件,可以被JS引擎自动优化
//以下这段示例代码满足上述的三个条件,可以被JS引擎自动优化
"use strict";
function doSomething() {
// 被优化
return doSomethingElse();
}
在这个函数中,尾调用doSomethingElse()的结果立即返回,不调用任何局部作用域变量。如果做一个小改动,不返回最终结果,那么引擎就无法优化当前函数
"use strict";
function doSomething() {
// 未被优化:缺少 return
doSomethingElse();
}
//同样地,如果定义了一个函数,在尾调用返回后执行其他操作,则函数也无法得到优化
"use strict";
function doSomething() {
// 未被优化:在返回之后还要执行加法
return 1 + doSomethingElse();
}
如果把函数调用的结果存储在一个变量里,最后再返回这个变量,则可能导致引擎无法优化
"use strict";
function doSomething() {
// 未被优化:调用并不在尾部
var result = doSomethingElse();
return result;
}
可能最难避免的情况是闭包的使用,它可以访问作用域中所有变量,因而导致尾调用优化失效
"use strict";
function doSomething() {
var num = 1,
func = () => num;
// 未被优化:此函数是闭包
return func();
}
//闭包func()可以访问局部变量num,即使调用func()后立即返回结果,也无法对代码进行优化
【应用】
实际上,尾调用的优化发生在引擎背后,除非尝试优化一个函数,否则无须思考此类问题。递归函数是其最主要的应用场景,此时尾调用优化的效果最显著
function factorial(n) {
if (n <= 1) {
return 1;
} else {
// 未被优化:在返回之后还要执行乘法
return n * factorial(n - 1);
}
}
由于在递归调用前执行了乘法操作,因而当前版本的阶乘函数无法被引擎优化。如果n是一个非常大的数,则调用栈的尺寸就会不断增长并存在最终导致栈溢出的潜在风险
优化这个函数,首先要确保乘法不会在函数调用后执行,可以通过默认参数来将乘法操作移出return语句,结果函数可以携带着临时结果进入到下一个迭代中
function factorial(n, p = 1) {
if (n <= 1) {
return 1 * p;
} else {
let result = n * p;
// 被优化
return factorial(n - 1, result);
}
}
在这个重写后的factorial()函数中,第一个参数p的默认值为1,用它来保存乘法结果,下一次迭代中可以取出它用于计算,不再需要额外的函数调用。当n大于1时,先执行一轮乘法计算,然后将结果传给第二次factorial()调用的参数。现在,ES6引擎就可以优化递归调用了。
写递归函数时,最好得用尾递归优化的特性,如果递归函数的计算量足够大,则尾递归优化可以大幅提升程序的性能。
另一个常见的事例是Fibonacci数列
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10) // 89
Fibonacci(100) // 堆栈溢出
Fibonacci(500) // 堆栈溢出
尾递归优化过的 Fibonacci 数列实现如下
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity
由此可见,“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6 是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存。