/ ECMAScript6

ECMAScript 6 之箭头函数

简介

对于箭头函数的出现,我是这样理解的:

functionjavascript 中是一种类似与流程的一种存在,一个 function 代表一种处理数据的逻辑。在 ES6 中, function 的这一特性被进一步的认可和强调,因此就产生了箭头函数。

简单的箭头函数

var f = v => v;

上面的等同于以下函数

var f = function(v) {
  return v;
};

当然函数参数存在没有或者多个情况

var f = () => 5;
// 等同于
var f = function () { return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
  return num1 + num2;
};

当箭头函数的代码块部分多于一条语句,就需要使用大括号括起来,并使用return语句返回。

var sum = (num1, num2) => { return num1 + num2; }

由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,就必须在对象外面加上大括号。

// 报错
let getTempItem = id => { id: id, name: "Temp" };

// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });

当然箭头函数也是函数,拥有函数的所有功能。

  1. 参数默认值
  2. 参数的变量解构
  3. rest 参数
const full = ({ first, last } = { first:'xxx', last:'xxxx' }) => first + ' ' + last;

一些箭头函数的方便使用

// 正常函数写法
[1,2,3].map(function (x) {
  return x * x;
});

// 箭头函数写法
[1,2,3].map(x => x * x);

// 正常函数写法
var result = values.sort(function (a, b) {
  return a - b;
});

// 箭头函数写法
var result = values.sort((a, b) => a - b);

const numbers = (...nums) => nums;

numbers(1, 2, 3, 4, 5)
// [1,2,3,4,5]

使用注意点

  1. 函数体内的 this 对象,是定义时所在的对象,不会随着调用环境的变化而变化。
  2. 不可以当作构造函数,也就是不能使用 new
  3. 不可以使用 arguments 对象,该对象在函数体内不存在,可以用 rest 参数替代。
  4. 不可以使用 yield 命令,所以不能用作 Generator 函数。

箭头函数中的this对象

在箭头函数中this对象是固定的,并不会因为调用环境的不同而不同

注意这句话:这里说明的是调用环境,而不是生成环境,箭头函数的 this 指向为生成环境,所以对函数的生成环境进行修改,this 下对应的值也是会发生变化的。

function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

var id = 21;

foo.call({ id: 42 });
// id: 42

这个例子中,setTimeout 中是一个箭头函数,设置在 100ms 后执行,一般的函数在执行时,才会去找 this 的指向,而箭头函数直接输出了 42 ,而这个 42 是箭头函数生成是所在的对象(这里是 { id: 42 } )所以输出的是 42

作为对比,可以用以下代码测试效果

function foo() {
  setTimeout(function() {
    console.log('id:', this.id);
  }, 100);
}

var id = 21;

foo.call({ id: 42 }); 
// id: 21

一个更完美解释了该现象的例子

function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭头函数
  setInterval(() => this.s1++, 1000);
  // 普通函数
  setInterval(function () {
    this.s2++;
  }, 1000);
}

var timer = new Timer();

setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0

箭头函数可以让 this 的指向固化,所以可以用来封装回调

var handler = {
  id: '123456',

  init: function() {
    document.addEventListener('click',
      event => this.doSomething(event.type), false);
  },

  doSomething: function(type) {
    console.log('Handling ' + type  + ' for ' + this.id);
  }
};

例子中的 this 指向就永远都是 function 中的 this ,而由于一般上是这样调用 handler.init ,一般的函数调用是生成 this 这里是 handler ,所以这个箭头函数的 this 就指向了 handler ,而如果不是使用箭头函数,那么在执行回调时,this 指向 document

没有this对象的原因

this 指向的固定化,并不是因为箭头函数内部有绑定 this 的机制,实际原因是箭头函数根本没有自己的 this ,导致内部的 this 就是外层代码块的 this 。正是因为它没有 this ,所以也就不能用作构造函数。

所以箭头函数转成 ES5 的代码时,需要特别处理 this 的指向。

// ES6
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

// ES5
function foo() {
  var _this = this;

  setTimeout(function () {
    console.log('id:', _this.id);
  }, 100);
}

箭头函数的注意点

  1. 除了 thisargumentssupernew.target3 个变量在箭头函数内部都是不存在的,指向外层函数的相应变量。
  2. 由于箭头函数没有自己的 this ,所以不能使用 callapplybind 这些方法去改变 this 的指向。

双冒号运算符

函数绑定运算符是并排的两个冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即 this 对象),绑定到右边的函数上面。

foo::bar;
// 等同于
bar.bind(foo);

foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);

const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
  return obj::hasOwnProperty(key);
}

所以双冒号运算符能固定函数执行的上下文环境的能力。

当需要固定的上下文环境和函数所属对象是同一个时,可以简写省略前面的对象

var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;

let log = ::console.log;
// 等同于
var log = console.log.bind(console);

注: 上面的例子大多数来自 《ECMAScript 6入门》这本书。