热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

详解JS箭头函数

在JS中,箭头函数可以像普通函数一样以多种方式使用。但是,它们一般用于需要匿名函数表达式,例如回调函数。
在JS中,箭头函数可以像普通函数一样以多种方式使用。但是,它们一般用于需要匿名函数表达式,例如回调函数

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

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

scores.map(score => +(score / maxScore).toFixed(2));

乍一看,箭头函数似乎可以按常规函数来定义与使用,但事实并非如此。出于箭头函数的简洁性,它与常规函数有所不同,换一种看法,箭头函数也许可以把箭头函数看作是异常的 JS 函数。

【相关课程推荐:Javascript视频教程】

虽然箭头函数的语法非常简单,但这不是本文的重点。本文主要讲讲箭头函数与常规函数行为的差异,以及咱们如果利用这些差异来更好使用箭头函数。

 ● 无论在严格模式还是非严格模式下,箭头函数都不能具有重复的命名参数。

 ● 箭头函数没有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 => &#39;ff&#39;, fromRadix => 16, toRadix => 8
console.log(baseConvert(&#39;ff&#39;, 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(&#39;form&#39;);

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

button.addEventListener(&#39;click&#39;, processFormData, false);

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

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

this.closest(&#39;form&#39;);

如果将函数更改为箭头函数语法,会发生什么?

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

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

button.addEventListener(&#39;click&#39;, processFormData, false);

如果现在尝试此操作,咱们就得到一个错误。从表面上看,this 的值并不是各位想要的。由于某种原因,它不再指向button元素,而是指向window对象。

如何修复this指向

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

button.addEventListener(&#39;click&#39;, 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(&#39;change&#39;, function (evt) {
      const files = evt.target.files;
      console.log(this._sortByFileSize(files));
    }, false);
  }

}).init(document.getElementById(&#39;file-input&#39;));

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

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

console.log(this._sortByFileSize(files));

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

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

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

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

init: function (input) {
  input.addEventListener(&#39;change&#39;, (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:对函数的调用上下文对象的引用

原文:https://s0dev0to.icopy.site/bnevilleoneill/anomalies-in-Javascript-arrow-functions-2afh

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

本文来自 js教程 栏目,欢迎学习!

以上就是详解JS箭头函数的详细内容,更多请关注 第一PHP社区 其它相关文章!


推荐阅读
  • 本文介绍了在Python3中如何使用选择文件对话框的格式打开和保存图片的方法。通过使用tkinter库中的filedialog模块的asksaveasfilename和askopenfilename函数,可以方便地选择要打开或保存的图片文件,并进行相关操作。具体的代码示例和操作步骤也被提供。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 大连微软技术社区举办《.net core始于足下》活动,获得微软赛百味和易迪斯的赞助
    九月十五日,大连微软技术社区举办了《.net core始于足下》活动,共有51人报名参加,实际到场人数为43人,还有一位专程从北京赶来的同学。活动得到了微软赛百味和易迪斯的赞助,场地也由易迪斯提供。活动中大家积极交流,取得了非常成功的效果。 ... [详细]
  • 本文详细介绍了SQL日志收缩的方法,包括截断日志和删除不需要的旧日志记录。通过备份日志和使用DBCC SHRINKFILE命令可以实现日志的收缩。同时,还介绍了截断日志的原理和注意事项,包括不能截断事务日志的活动部分和MinLSN的确定方法。通过本文的方法,可以有效减小逻辑日志的大小,提高数据库的性能。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • GetWindowLong函数
    今天在看一个代码里头写了GetWindowLong(hwnd,0),我当时就有点费解,靠,上网搜索函数原型说明,死活找不到第 ... [详细]
  • 基于layUI的图片上传前预览功能的2种实现方式
    本文介绍了基于layUI的图片上传前预览功能的两种实现方式:一种是使用blob+FileReader,另一种是使用layUI自带的参数。通过选择文件后点击文件名,在页面中间弹窗内预览图片。其中,layUI自带的参数实现了图片预览功能。该功能依赖于layUI的上传模块,并使用了blob和FileReader来读取本地文件并获取图像的base64编码。点击文件名时会执行See()函数。摘要长度为169字。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • 学习笔记(34):第三阶段4.2.6:SpringCloud Config配置中心的应用与原理第三阶段4.2.6SpringCloud Config配置中心的应用与原理
    立即学习:https:edu.csdn.netcourseplay29983432482?utm_sourceblogtoedu配置中心得核心逻辑springcloudconfi ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • Centos7.6安装Gitlab教程及注意事项
    本文介绍了在Centos7.6系统下安装Gitlab的详细教程,并提供了一些注意事项。教程包括查看系统版本、安装必要的软件包、配置防火墙等步骤。同时,还强调了使用阿里云服务器时的特殊配置需求,以及建议至少4GB的可用RAM来运行GitLab。 ... [详细]
  • 本文介绍了九度OnlineJudge中的1002题目“Grading”的解决方法。该题目要求设计一个公平的评分过程,将每个考题分配给3个独立的专家,如果他们的评分不一致,则需要请一位裁判做出最终决定。文章详细描述了评分规则,并给出了解决该问题的程序。 ... [详细]
  • 禁止程序接收鼠标事件的工具_VNC Viewer for Mac(远程桌面工具)免费版
    VNCViewerforMac是一款运行在Mac平台上的远程桌面工具,vncviewermac版可以帮助您使用Mac的键盘和鼠标来控制远程计算机,操作简 ... [详细]
author-avatar
手机用户2602929765
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有