ES6中提出了一个新的概念,叫做箭头函数。我们可以把这个概念,理解成一个“简化版”的函数。之所以说箭头函数是所谓的“简化版”,主要原因有二,其一是箭头函数的写法比较简单,其二是箭头函数缺少Function所具备的一些“配套设施”,它们包括:this、super、arguments对象和new.target。今天我们就来着重地讨论一下箭头函数。
基本用法
首先看一段代码:
// 普通函数版本
var f1 = function(v){
return v;
}
//箭头函数版本
var f2 = v => v;
上例中的f1和f2起到的作用是相同的,都是原样返回输入参数v。我们可以看到,箭头函数的写法更加简洁,且语义更加清晰。
上面的例子过于简单,我们来看一下稍微复杂点的例子:
// 普通函数版本
var number = function(n1,n2,n3){
var a = n1 + n2;
var b = a * n3;
return b;
}
// 箭头函数版本
var number = (n1,n2,n3)=> {
var a = n1 + n2;
return a * n3;
}
在这个例子里面,我们会看出:
- 箭头函数的箭头右侧,不仅可以是变量,还可以是代码块。
- 箭头函数右侧的代码块中,需要用return语句显式的声明返回值。
- 在箭头的右侧的代码块中,如果只有一条语句,那么代码块的大括号({})和return语句都可以被省略。
- 代码块同ECMAScript 标准的其他位置一样,使用大括号({}),包括起来的。
看到这里,细心的开发者会有个疑惑,如果箭头函数右侧要返回一个对象的话,是如何书写的?毕竟对象字面量的写法也是用大括号包含的,答案是在大括号外边,再加一个括号。
var person1 = (name,age)=>({ name : name, age :age });
长期用面向对象的形式编程的开发者都知道,函数内部是可以“尾部调用”的,即是说函数可以不return值,而是以调用其他函数的方式结尾。对于这种需求,箭头函数的写法如下:
// 箭头函数版本
// 此处的参数,用了rest参数的写法
let result = (...args) => doSomethings(...args);
// 普通函数
let result2 = function(...args){
doSomethings(...args);
}
在上面的例子里面,我们不止看到了尾部调用的写法,还看到了rest形式的参数,我们知道,rest参数是不需要限定参数个数的,那么再想的深一层,如果,不需要输入参数的话,箭头函数怎么写?
var nopra = () => "没有参数,返回此处字符串,nopra的值就是这个字符串"
箭头函数是可以嵌套箭头函数的,看下面的例子1:
function insert(value){
return {
into : function(array){
return {
after : function(afterValue){
array.splice(array.indexOf(afterValue) + 1,0,value);
return array;
}
}
}
}
}
上面的这个例子,让我们可以在一个数组中的某个值后面上插入一个值。使用方法如下:
insert(2)into([1,3]).after(1);
// 得到结果:[1,2,3]
如果用箭头函数的方法来写这个例子的话,写法如下:
let insert = (value) => ({
into : (array) => ({
after : (afterValue) => {
array.splice(array.indexOf(afterValue) + 1,0,value)
}
});
})
经过例子1的对比后,我们又一次的发现箭头函数写的代码比较精简。下面是一个管道机制的代码例子,所谓的管道机制是指,前一个代码的输出是后一个代码的输入。例子2代码如下:
// 管道函数pipeline,参数采用解构赋值,在函数内部会以数组的形式存在。
const pipeline = (...funcs) =>
val => funcs.reduce((a,b) => b(a),val); //a和b是指输入参数中相邻的两个函数,限制性a然后将结果传递给b执行,val是初始化的值,就是下面addThenMulti(5)中的5。
// 箭头函数:参数加一
const plus1 = a => a+1;
// 箭头函数:参数乘二
const mult2 = a => a*2;
// 将两个箭头函数传递给pipeline,形成通道
const addThenMult = pipeline(plus1,mult2);
// 初始化值是5,经过通道后的结果是12
addThenMulti(5);
// 结果是:12
上面的写法,可读性不太好,如果不考虑通道的话,其实效果和下面的函数相同
// 没有写通道函数
const plus1 = a => a+1;
const mult2 = a => a*2;
// hardcode 将两个函数串联。
mult2(plus(1));
// 12
箭头函数还有一个重要的作用是,可以很方便的书写 “
λ 演算”
。关于 “λ 演算”的具体内容,我在此不详细阐述了,这是一个数学的概念,在计算机领域里应用很广。在这里只给一个例子,想要详细了解的同学,请自行查找相关资料。
// λ演算的写法
fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))
//ES6 的写法
var fix = f => ( x => f( v => x(x)(v)))( x=> f( v => x(x)(v)));
箭头函数与普通函数的不同
开篇的时候,我们就提到了,箭头函数就像是一个“简化版”的不同之处在于:
这里重点说一下箭头函数没有this指针引发的一系列连锁反应:
首先,没有this指针,就代表着,箭头函数无法成为构造函数,无法被new命令调用。这里的理由很简单,举例而言:
function A(){
this.prop1 = 1;
this.prop2 = "属性2"
this.prop3 = function(){
console.log("这是一个保护方法");
}
}
//实例化构造函数A
var a = new A();
上面的例子,是在class功能出现之前,常见的面向对象的JS写法,构造函数A中的this指向new命令新建的对象,this函数称之为构造函数的“必要条件”,所以缺少this的箭头函数无法成为“构造函数”。
其次,由于super的设计初衷是,this指向对象的“原型对象”,所以缺乏this的话,同样会缺乏super。
再次,由于箭头函数没有自己的this,所以,在函数内部代码中,写的this都默认是箭头函数所在代码块的this。这导致了一个很新奇的现象:普通函数的this指向的是函数运行时,调用函数的对象。箭头函数中的this(即所在代码块的this)指向的是箭头函数定义时所在的对象。
function foo1() {
console.log("foo1箭头函数外this指向对象的ID:"+this.id);
setTimeout(() => {
console.log("箭头函数内this指向对象的ID:"+ this.id);
}, 1000);
}
function foo2(){
console.log("foo2普通函数外this指向对象的ID:"+this.id);
setTimeout(function(){
console.log("普通函数内this指向对象的ID:"+ this.id);
}, 1000);
}
window.id = "window";
foo1.call({ id: "obj"})
foo2.call({ id: "obj" });
上面代码的运行结果是:
> "foo1箭头函数外this指向对象的ID:obj"
> "foo2普通函数外this指向对象的ID:obj"
> "箭头函数内this指向对象的ID:obj"
> "普通函数内this指向对象的ID:window"
这个例子进一步阐明了箭头函数中this指向。在这里解释一下上面的代码。
- 首先,我们用call方法,将Id为“obj”的对象的作用域,传递到foo1和foo2方法的this中,所以,在setTimeout外面,的console中,this指向的都是obj。
- 其次,在setTimeout中,普通函数的this,指向了函数运行时的上下文(window);
- 再次,在setTimeout中,箭头函数中的this,指向的是函数定义时的上下文(Obj对象)。
综上所述,foo1.call和foo2.call产生了不同的运行结果。
在举一个例子:
function foo(){
return ()=>{
return ()=>{
return ()=>{
console.log('id:',this.id);
};
};
};
}
var f = foo.call({id:1});
var t1 = f.call({id:2})()(); //id:1
var t2 = f().call({id:3})(); //id:1
var t3 = f()().call({id:4}); //id:1
在上面代码中,只有一个this,就是函数的this,所以t1,t2,t3都输出了同样的结果,因为所有的内层函数都是箭头函数,都没有自己的this,它们的this都是最外层函数foo的this。
除了this之外,arguments,super,new.target也是不存在的。它们都会向this一样指向外层函数的对应变量。
最后提及一下,由于缺少this,所以call()、bind()和call()方法不能用改变箭头函数的指向。