JS では、アロー関数は通常の関数と同様にさまざまな方法で使用できます。ただし、これらは通常、コールバック関数などの匿名関数式が必要な場合に使用されます。
次の例は、コールバック関数としてのアロー関数、特に map()
、filter()
、# の例を示しています。 # #reduce()、
sort() およびその他の配列メソッド。
const scores = [ 1, 28, 66, 666]; const maxScore = Math.max(...scores); scores.map(score => +(score / maxScore).toFixed(2));
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]
この関数は、3 つの名前付きパラメータによって定義されます: first
、秒
と##第三##。関数に渡される名前付き引数よりも多くの名前付き引数がある場合、残りの引数は 未定義
になります。 JS 関数は、名前付きパラメーターを使用した非厳密モードで奇妙な動作を示します。非厳密モードでは、JS 関数は名前付きパラメータの繰り返しを許可します。例を見てみましょう:
<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">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]</pre><div class="contentsignin">ログイン後にコピー</div></div>
パラメータが繰り返されているため、に渡されるパラメータ 関数呼び出しの 3 番目のパラメータの値によって最初のパラメータが上書きされますが、これは望ましい動作ではありません。
// 由于参数重复,严格模式会报错 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()
関数をコピーしてみます。一般に、これは難しくなく、次のように行うことはできないと考えられます。
arguments は未定義です。
何が間違っていたのか
アロー関数の場合: 通常の関数とは異なり、引数
アロー関数には存在しません。ただし、アロー以外の親関数のarguments
オブジェクトにアクセスすることは可能です。この理解に基づいて、
average() 関数を通常の関数に変更すると、すぐに呼び出されたネストされたアロー関数の結果が返されます。親関数の
arguments
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 の問題は解決できますが、このでたらめなアプローチは明らかに冗長です。
何か違うことをしてください
上記の問題に代わるものはありますか? es6 の rest パラメータを使用できます。 。
ES6rest パラメーターを使用すると、関数に渡されたすべてのパラメーターを保存する配列を取得できます。
rest
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; })(); }
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
属性。
由于箭头函数的prototype
为 undefined
,尝试使用属性和方法来扩充它,或者访问它上面的属性,都会引发错误。
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
JS函数的每次调用都与调用上下文相关联,这取决于函数是如何调用的,或者在哪里调用的。
函数内部this
值依赖于函数在调用时的调用上下文,这通常会让开发人员不得不问自己一个问题:this
值是啥。
下面是对不同类型的函数调用this
指向一些总结:
● 使用new
关键字调用:this
指向由函数的内部[[Construct]]
方法创建的新实例对象。this
(新创建的实例对象)通常在默认情况下返回,除了在函数定义中显式指定了不同的返回值。
● 不使用new
关键字直接调用:在非严格模式下,this
指向window
对象(浏览器中)。然而,在严格模式下,this
值为undefined;因此,试图访问或设置此属性将引发错误。
● 间接使用绑定对象调用:Function.prototype
对象提供了三种方法,可以在调用函数时将函数绑定到任意对象,即:call()
,apply()
和bind(
)。 使用这些方法调用函数时,this
指向指定的绑定对象。
● 作为对象方法调用:this
指向调用函数(方法)的对象,无论该方法是被定义为对象的自己的属性还是从对象的原型链中解析。
● 作为事件处理程序调用:对于用作DOM事件侦听器的常规函数,this
指向触发事件的目标对象、DOM元素、document
或window
。
再来看个函数,该函数将用作单击事件侦听器,例如,表单提交按钮:
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._sortByFileSize
为 undefined
。
要解决此问题,需要将事件侦听器中的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.seconds
和 this.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 中国語 Web サイトの他の関連記事を参照してください。