JS异常函数之-箭头函数

来源:logrocketjavascript

做者:Maciej Cieślarhtml

译者:前端小智前端


阿里云最近在作活动,低至2折,有兴趣能够看看promotion.aliyun.com/ntms/yunpar…java


为了保证的可读性,本文采用意译而非直译。git

在JS中,箭头函数能够像普通函数同样以多种方式使用。可是,它们通常用于须要匿名函数表达式,例如回调函数es6

下面示例显示举例箭头函数做为回调函数,尤为是对于map(), filter(), reduce(), sort()等数组方法。github

const scores = [ 1, 28, 66, 666];
const maxScore = Math.max(...scores);

scores.map(score => +(score / maxScore).toFixed(2)); 
复制代码

乍一看,箭头函数彷佛能够按常规函数来定义与使用,但事实并不是如此。出于箭头函数的简洁性,它与常规函数有所不一样,换一种见解,箭头函数也许能够把箭头函数看做是异常的 JS 函数。数组

虽然箭头函数的语法很是简单,但这不是本文的重点。本文主要讲讲箭头函数与常规函数行为的差别,以及我们若是利用这些差别来更好使用箭头函数。浏览器

  • 不管在严格模式仍是非严格模式下,箭头函数都不能具备重复的命名参数。app

  • 箭头函数没有arguments绑定。可是,它们能够访问最接近的非箭头父函数的arguments对象。

  • 箭头函数永远不能用做构造函数,天然的不能使用new关键字调用它们,所以,对于箭头函数不存在prototype属性。

  • 在函数的整个生命周期中,箭头函数内部的值保持不变,而且老是与接近的非箭头父函数中的值绑定。

命名函数参数

JS中的函数一般用命名参数定义。命名参数用于根据位置将参数映射到函数做用域中的局部变量。

来看看下面的函数:

function logParams (first, second, third) {
  console.log(first, second, third);
}

// first => 'Hello'
// second => 'World'
// third => '!!!'
logParams('Hello', 'World', '!!!'); // "Hello"  "World"  "!!!"

// first => { o: 3 }
// second => [ 1, 2, 3 ]
// third => undefined
logParams({ o: 3 }, [ 1, 2, 3 ]); // {o: 3}  [1, 2, 3]
复制代码

logParams()函数由三个命名参数定义: firstsecondthird。若是命名参数多于传递给函数的参数,则其他参数undefined

对于命名参数,JS函数在非严格模式下表现出奇怪的行为。在非严格模式下,JS函数容许有重复命名参数,来看看示例:

function logParams (first, second, first) {
  console.log(first, second);
}

// first => 'Hello'
// second => 'World'
// first => '!!!'
logParams('Hello', 'World', '!!!'); // "!!!"  "World"

// first => { o: 3 }
// second => [ 1, 2, 3 ]
// first => undefined
logParams({ o: 3 }, [ 1, 2, 3 ]); // undefined  [1, 2, 3]
复制代码

我们能够看到,first参数重复了,所以,它被映射到传递给函数调用的第三个参数的值,覆盖了第一个参数,这不是一个让人喜欢的行为。

// 因为参数重复,严格模式会报错
function logParams (first, second, first) {
  "use strict";
  console.log(first, second);
}
复制代码

箭头函数如何处理重复的参数

关于箭头函数:

与常规函数不一样,不管在严格模式仍是非严格模式下,箭头函数都不容许重复参数,重复的参数将引起语法错误。
// 只要你敢写成重复的参数,我就敢死给你看 const logParams = (first, second, first) => { console.log(first, second); }

函数重载

函数重载是定义函数的能力,这样就能够根据不一样的参数数量来调用对应的函数, JS 中能够利用绑定方式来实现这一功能。

来看个简单的重载函数,计算传入参数的平均值:

function average() {
  const length = arguments.length;

  if (length == 0) return 0;

  // 将参数转换为数组
  const numbers = Array.prototype.slice.call(arguments);

  const sumReduceFn = function (a, b) { return a + Number(b) };
  // 返回数组元素的总和除以数组的长度
  return numbers.reduce(sumReduceFn, 0) / length;
}
复制代码

这样函数能够用任意数量的参数调用,从0到函数能够接受的最大参数数量应该是255
average(); // 0 average('3o', 4, 5); // NaN average('1', 2, '3', 4, '5', 6, 7, 8, 9, 10); // 5.5 average(1.75, 2.25, 3.5, 4.125, 5.875); // 3.5

如今尝试使用剪头函数语法复制average()函数,通常我们会以为,这没啥难的,没法就这样:

const average = () => {
  const length = arguments.length;

  if (length == 0) return 0;

  const numbers = Array.prototype.slice.call(arguments);
  const sumReduceFn = function (a, b) { return a + Number(b) };

  return numbers.reduce(sumReduceFn, 0) / length;
}
复制代码

如今测试这个函数时,我们会发现它会抛出一个引用错误,arguments 未定义。

我们作错了啥

对于箭头函数:

与常规函数不一样,arguments不存在于箭头函数中。可是,能够访问非箭头父函数的arguments对象。

基于这种理解,能够将average()函数修改成一个常规函数,该函数将返回当即调用的嵌套箭头函数执行的结果,该嵌套箭头函数就可以访问父函数的arguments

function average() {
  return (() => {
    const length = arguments.length;

    if (length == 0) return 0;

    const numbers = Array.prototype.slice.call(arguments);
    const sumReduceFn = function (a, b) { return a + Number(b) };

    return numbers.reduce(sumReduceFn, 0) / length;
  })();
}
复制代码

这样就能够解决了arguments对象没有定义的问题,但这种狗屎作法显然不少余了。

作点不同的

对于上面问题是否存在替代方法呢,可使用 es6 的 rest 参数。

使用ES6 rest 参数,我们能够获得一个数组,该数组保存了传递给该函数的全部的参数。rest语法适用于全部类型的函数,不管是常规函数仍是箭头函数。

const average = (...args) => {
  if (args.length == 0) return 0;
  const sumReduceFn = function (a, b) { return a + Number(b) };

  return args.reduce(sumReduceFn, 0) / args.length;
}
复制代码

对于使用rest参数须要注意一些事项:

  • rest参数与函数内部的arguments对象不一样。rest参数是一个实际的函数参数,而arguments对象是一个绑定到函数做用域的内部对象。

  • 一个函数只能有一个rest参数,并且它必须位于最后一个参数。这意味着函数能够包含命名参数和rest参数的组合。

  • rest 参数与命名参数一块儿使用时,它不包含全部传入的参数。可是,当它是唯一的函数参数时,表示函数参数。另外一方面,函数的arguments对象老是捕获全部函数的参数。

  • rest参数指向包含全部捕获函数参数的数组对象,而arguments对象指向包含全部函数参数的类数组对象。

接着考虑另外一个简单的重载函数,该函数将数字根据传入的进制转换为另外一个类的进制数。 可使用一到三个参数调用该函数。 可是,当使用两个或更少的参数调用它时,它会交换第二个和第三个函数参数。以下所示:

function baseConvert (num, fromRadix = 10, toRadix = 10) {
  if (arguments.length < 3) {
    // swap variables using array destructuring
    [toRadix, fromRadix] = [fromRadix, toRadix];
  }
  return parseInt(num, fromRadix).toString(toRadix);
}
复制代码

调用 baseConvert 方法:

// num => 123, fromRadix => 10, toRadix => 10
console.log(baseConvert(123)); // "123"

// num => 255, fromRadix => 10, toRadix => 2
console.log(baseConvert(255, 2)); // "11111111"

// num => 'ff', fromRadix => 16, toRadix => 8
console.log(baseConvert('ff', 16, 8)); // "377"
复制代码

使用箭头函数来重写上面的方法:

const baseConvert = (num, ...args) => {
  // 解构`args`数组和
  // 设置`fromRadix`和`toRadix`局部变量
  let [fromRadix = 10, toRadix = 10] = args;

  if (args.length < 2) {
    // 使用数组解构交换变量
    [toRadix, fromRadix] = [fromRadix, toRadix];
  }

  return parseInt(num, fromRadix).toString(toRadix);
}
复制代码

构造函数

可使用new关键字调用常规JS函数,该函数做为类构造函数用于建立新的实例对象。

function Square (length = 10) {
  this.length = parseInt(length) || 10;

  this.getArea = function() {
    return Math.pow(this.length, 2);
  }

  this.getPerimeter = function() {
    return 4 * this.length;
  }
}

const square = new Square();

console.log(square.length); // 10
console.log(square.getArea()); // 100
console.log(square.getPerimeter()); // 40

console.log(typeof square); // "object"
console.log(square instanceof Square); // true
复制代码

当使用new关键字调用常规JS函数时,将调用函数内部[[Construct]]方法来建立一个新的实例对象并分配内存。以后,函数体将正常执行,并将this映射到新建立的实例对象。最后,函数隐式地返回 this(新建立的实例对象),只是在函数定义中指定了一个不一样的返回值。

此外,全部常规JS函数都有一个prototype属性。函数的prototype属性是一个对象,它包含函数建立的全部实例对象在用做构造函数时共享的属性和方法。

如下是对前面的Square函数的一个小修改,此次它从函数的原型上的方法,而不是构造函数自己。

function Square (length = 10) {
  this.length = parseInt(length) || 10;
}

Square.prototype.getArea = function() {
  return Math.pow(this.length, 2);
}

Square.prototype.getPerimeter = function() {
  return 4 * this.length;
}

const square = new Square();

console.log(square.length); // 10
console.log(square.getArea()); // 100
console.log(square.getPerimeter()); // 40

console.log(typeof square); // "object"
console.log(square instanceof Square); // true
复制代码

以下所知,一切仍然按预期工做。 事实上,这里有一个小秘密:ES6 类在后台执行相似于上面代码片断的操做 - 类(class)只是个语法糖。

那么箭头函数呢

它们是否也与常规JS函数共享此行为?答案是否认的。关于箭头函数:

与常规函数不一样,箭头函数永远不能使用new关键字调用,由于它们没有[[Construct]]方法。 所以,箭头函数也不存在prototype属性。

箭头函数不能用做构造函数,没法使用new关键字调用它们,若是这样作了会抛出一个错误,代表该函数不是构造函数。

所以,对于箭头函数,不存在能够做为构造函数调用的函数内部的new.target等绑定,相反,它们使用最接近的非箭头父函数的new.target值。

此外,因为没法使用new关键字调用箭头函数,所以实际上不须要它们具备原型。 所以,箭头函数不存在prototype属性。

因为箭头函数的prototypeundefined,尝试使用属性和方法来扩充它,或者访问它上面的属性,都会引起错误。

const Square = (length = 10) => {
  this.length = parseInt(length) || 10;
}

// throws an error
const square = new Square(5);

// throws an error
Square.prototype.getArea = function() {
  return Math.pow(this.length, 2);
}

console.log(Square.prototype); // undefined
复制代码

this 是啥

JS函数的每次调用都与调用上下文相关联,这取决于函数是如何调用的,或者在哪里调用的。

函数内部this值依赖于函数在调用时的调用上下文,这一般会让开发人员不得不问本身一个问题:this值是啥。

下面是对不一样类型的函数调用this指向一些总结:

  • 使用new关键字调用:this指向由函数的内部[[Construct]]方法建立的新实例对象。this(新建立的实例对象)一般在默认状况下返回,除了在函数定义中显式指定了不一样的返回值。

  • 不使用new关键字直接调用:在非严格模式下,this指向window对象(浏览器中)。然而,在严格模式下,this值为undefined;所以,试图访问或设置此属性将引起错误。

  • 间接使用绑定对象调用Function.prototype对象提供了三种方法,能够在调用函数时将函数绑定到任意对象,即:call()apply()bind()。 使用这些方法调用函数时,this指向指定的绑定对象。

  • 做为对象方法调用this指向调用函数(方法)的对象,不管该方法是被定义为对象的本身的属性仍是从对象的原型链中解析。

  • 做为事件处理程序调用:对于用做DOM事件侦听器的常规函数,this指向触发事件的目标对象、DOM元素、documentwindow

再来看个函数,该函数将用做单击事件侦听器,例如,表单提交按钮:

function processFormData (evt) {
  evt.preventDefault();

  const form = this.closest('form');

  const data = new FormData(form);
  const { action: url, method } = form;
}

button.addEventListener('click', processFormData, false);
复制代码

与前面看到的同样,事件侦听器函数中的 this值是触发单击事件的DOM元素,在本例中是button

所以,可使用如下命令指向submit按钮的父表单

this.closest('form');
复制代码

若是将函数更改成箭头函数语法,会发生什么?

const processFormData = (evt) => {
  evt.preventDefault();

  const form = this.closest('form');
  const data = new FormData(form);
  const { action: url, method } = form;
}

button.addEventListener('click', processFormData, false);
复制代码

若是如今尝试此操做,我们就获得一个错误。从表面上看,this 的值并非各位想要的。因为某种缘由,它再也不指向button元素,而是指向window对象。

如何修复this指向

利用上面提到的 Function.prototype.bind() 强制将this值绑定到button元素:

button.addEventListener('click', processFormData.bind(button), false);
复制代码

但这彷佛不是各位想要的解决办法。this仍然指向window对象。这是箭头函数特有的问题吗?这是否意味着箭头函数不能用于依赖于this的事件处理?

为何会搞错

关于箭头函数的最后一件事:

与常规函数不一样,箭头函数没有this的绑定。 this的值将解析为最接近的非箭头父函数或全局对象的值。

这解释了为何事件侦听器箭头函数中的this值指向window 对象(全局对象)。 因为它没有嵌套在父函数中,所以它使用来自最近的父做用域的this值,该做用域是全局做用域。

可是,这并不能解释为何不能使用bind()将事件侦听器箭头函数绑定到button元素。对此有一个解释:

与常规函数不一样,内部箭头函数的this值保持不变,而且不管调用上下文如何,都不能在其整个生命周期中更改。

箭头函数的这种行为使得JS引擎能够优化它们,由于能够事先肯定函数绑定。

考虑一个稍微不一样的场景,其中事件处理程序是使用对象方法中的常规函数​​定义的,而且还取决于同一对象的另外一个方法:

({
  _sortByFileSize: function (filelist) {
    const files = Array.from(filelist).sort(function (a, b) {
      return a.size - b.size;
    });

    return files.map(function (file) {
      return file.name;
    });
  },

  init: function (input) {
    input.addEventListener('change', function (evt) {
      const files = evt.target.files;
      console.log(this._sortByFileSize(files));
    }, false);
  }

}).init(document.getElementById('file-input'));
复制代码

上面是一个一次性的对象,该对象带有_sortByFileSize()方法和init()方法,并当即调init方法。init()方法接受一个input元素,并为input元素设置一个更改事件处理程序,该事件处理程序按文件大小对上传的文件进行排序,并打印在浏览器的控制台。

若是测试这段代码,会发现,当选择要上载的文件时,文件列表不会被排序并打印到控制台;相反,会控制台上抛出一个错误,问题就出在这一行:

console.log(this._sortByFileSize(files));
复制代码

在事件监听器函数内部,this 指向 input 元素 所以this._sortByFileSizeundefined

要解决此问题,须要将事件侦听器中的this绑定到包含方法的外部对象,以即可以调用this._sortByFileSize()。 在这里,可使用bind(),以下所示:

init: function (input) {
  input.addEventListener('change', (function (evt) {
    const files = evt.target.files;
    console.log(this._sortByFileSize(files));
  }).bind(this), false);
}
复制代码

如今一切正常。这里不使用bind(),能够简单地用一个箭头函数替换事件侦听器函数。箭头函数将使用父init()方法中的this的值:

init: function (input) {
  input.addEventListener('change', (function (evt) {
    const files = evt.target.files;
    console.log(this._sortByFileSize(files));
  }).bind(this), false);
}
复制代码

再考虑一个场景,假设有一个简单的计时器函数,能够将其做为构造函数调用来建立以秒为单位的倒计时计时器。使用setInterval()进行倒计时,直到持续时间过时或间隔被清除为止,以下所示:

function Timer (seconds = 60) {
  this.seconds = parseInt(seconds) || 60;
  console.log(this.seconds);

  this.interval = setInterval(function () {
    console.log(--this.seconds);

    if (this.seconds == 0) {
      this.interval && clearInterval(this.interval);
    }
  }, 1000);
}

const timer = new Timer(30);
复制代码

若是运行这段代码,会看到倒计时计时器彷佛被打破了,在控制台上一直打印 NaN

这里的问题是,在传递给setInterval()的回调函数中,this指向全局window对象,而不是Timer()函数做用域内新建立的实例对象。所以,this.secondsthis.interval 都是undefined的。

与以前同样,要修复这个问题,可使用bind()setInterval()回调函数中的this值绑定到新建立的实例对象,以下所示

function Timer (seconds = 60) {
  this.seconds = parseInt(seconds) || 60;
  console.log(this.seconds);

  this.interval = setInterval((function () {
    console.log(--this.seconds);

    if (this.seconds == 0) {
      this.interval && clearInterval(this.interval);
    }
  }).bind(this), 1000);
}
复制代码

或者,更好的方法是,能够用一个箭头函数替换setInterval()回调函数,这样它就可使用最近的非箭头父函数的this值:

function Timer (seconds = 60) {
  this.seconds = parseInt(seconds) || 60;
  console.log(this.seconds);

  this.interval = setInterval(() => {
    console.log(--this.seconds);

    if (this.seconds == 0) {
      this.interval && clearInterval(this.interval);
    }
  }, 1000);
}
复制代码

如今理解了箭头函数如何处理this关键字,还须要注意箭头函数对于须要保留this值的状况并不理想 - 例如,在定义须要引用的对象方法时 使用须要引用目标对象的方法来扩展对象或扩充函数的原型。

不存在的绑定

在本文中,已经看到了一些绑定,这些绑定能够在常规JS函数中使用,可是不存在用于箭头函数的绑定。相反,箭头函数从最近的非箭头父函数派生此类绑定的值。

总之,下面是箭头函数中不存在绑定的列表:

  • arguments:调用时传递给函数的参数列表

  • new.target:使用new关键字做为构造函数调用的函数的引用

  • super:对函数所属对象原型的引用,前提是该对象被定义为一个简洁的对象方法

  • this:对函数的调用上下文对象的引用

原文:s0dev0to.icopy.site/bnevilleone…

代码部署后可能存在的BUG无法实时知道,过后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给你们推荐一个好用的BUG监控工具 Fundebug

交流(欢迎加入群,群工做日都会发红包,互动讨论技术)

阿里云最近在作活动,低至2折,有兴趣能够看看:promotion.aliyun.com/ntms/yunpar…

干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。

github.com/qq449245884…

我是小智,公众号「大迁世界」做者,对前端技术保持学习爱好者。我会常常分享本身所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,便可看到福利,你懂的。

相关文章
相关标签/搜索