前言
在 JavaScript 中,我們可以有多種方式定義函數,如:函數聲明、函數表達式和箭頭函數
// 函數聲明 function normalFn() { return 'normalFn'; }
// 函數表達式 const normalFn = function() { return 'normalFn'; }
// 箭頭函數 const arrowFn = () => { return 'arrowFn'; }
其中,箭頭函數是在 ES2015(ES6) 標准中新增的,其語法與 ES6 之前的函數聲明及函數表達式兩種定義方式不同。本文中,將函數聲明和函數表達式兩種定義方式歸為普通函數。
那么,普通函數和箭頭函數有什么區別呢?
1. this 指向
在 JavaScript 中,this 的指向是個基礎且重要的知識點。
1.1 普通函數
在普通函數中,this 的指向(執行上下文)是動態的,其值取決於函數是如何被調用的,通常有以下 4 種調用方式:
1)直接調用時,其指向為全局對象(嚴格模式下為 undefined)
function fnc() { console.log(this); } fnc(); // 全局對象(global 或 window)
2)方法調用時,其指向為調用該方法的對象
var obj = {
fnc1: function(){ console.log(this === obj); } } obj.fnc2 = function() { console.log(this === obj); } function fnc3() { console.log(this === obj); } obj.fnc3 = fnc3; obj.fnc1(); // true obj.fnc2(); // true obj.fnc3(); // true
3)new 調用時,其指向為新創建的實例對象
function fnc() { console.log(this); } new fnc(); // fnc 的實例 fnc {}
4)call、apply、bind 調用時,其指向為三種方法的第一個參數
function fnc() { console.log(this); } const ctx = { value: 'a' }; fnc.call(ctx); // { value: 'a' } fnc.apply(ctx); // { value: 'a' } fnc.bind(ctx)(); // { value: 'a' }
在舊版的 JavaScript 中,經常使用 bind 顯式的設置 this 的指向,這種模式通常可以在 ES6 出現之前的某些早期版本的框架(如 react)中找到。而箭頭函數的出現則提供了一種更便捷的方式解決此問題。
1.2 箭頭函數
無論如何執行或在何處執行,箭頭函數內部的 this 值始終等於外部函數的值,即箭頭函數不會改變 this 的指向,
const obj = { fnc(arr) { console.log(this); // obj const cb = () => { console.log(this); // obj }; arr.forEach(cb); } }; obj.fnc([1, 2, 3]);
注意:由於箭頭函數沒有自己的 this 指針,通過 call() 、 apply() 和 bind() 方法調用時,只能傳遞參數,而不能綁定 this,他們的第一個參數會被忽略。如下:(例子來源於 MDN)
var adder = {
base : 1, add : function(a) { var f = v => v + this.base; return f(a); }, addThruCall: function(a) { var f = v => v + this.base; var b = { base : 2 }; return f.call(b, a); } }; console.log(adder.add(1)); // 輸出 2 console.log(adder.addThruCall(1)); // 仍然輸出 2
2. 構造函數
在 JavaScript 中,函數和類的繼承是通過 prototype 屬性實現的,且 prototype 擁有屬性 constructor 指向構造函數,如下:
function fnc() {} console.lof(fnc.prototype) // {constructor: ƒ}
而采用箭頭函數定義函數時,其是沒有 prototype 屬性的,也就無法指向構造函數。
const arrowFnc = () => {} console.log(arrowFnc.prototype) // undefined
針對普通函數與箭頭函數在構造函數上的區別,可以引出一個問題 -- 箭頭函數可以通過 new 進行實例化嗎?
const arrowFnc = () => {} const arrowIns = new arrowFnc() // Uncaught TypeError: arrowFnc is not a constructor
答案是【不可以】,那么為什么呢?
沒有自己的 this,也就意味着無法調用 apply、call 等
沒有 prototype 屬性,而 new 命令在執行時需要將構造函數的 prototype 賦值給新的對象的 _proto_
function newOperator(Con, ...args) { let obj = {}; Object.setPrototypeOf(obj, Con.prototype); // 相當於 obj.__proto__ = Con.prototype let result = Con.apply(obj, args); return result instanceof Object ? result : obj; }
更具體的原因是,JavaScript函數兩個內部方法: [[Call]] 和 [[Construct]],當函數被直接調用時執行的是 [[Call]] 方法,即直接執行函數體,而 new 調用時是執行的 [[Construct]]方法。箭頭函數沒有 [[Construct]]方法,因此不能被用作構造函數進行實例化。
3. 作為方法屬性
'use strict'; var obj = { i: 10, b: () => console.log(this.i, this), c: function() { console.log(this.i, this) } } obj.b(); // undefined, Window{...} obj.c(); // 10, Object {...}
可以看到,箭頭函數是沒有 this 綁定的,其指向始終與上一級保持一致。
上文中提到,當構造函數或類被 new 調用時,其 this 指向為新創建的實例對象,需要注意的是,這里的 this 是 constructor 中的 this,而不是該函數或類的任意地方。如下所示:
class Person { constructor(name) { this.name = name; } getName() { console.log(this.name, this); } } const p = new Person('Tom'); p.getName(); // Tom setTimeout(p.getName, 100); // undefined, Window{...}
為了避免這種錯誤,我們通常需要在 constructor 中綁定 this,如下所示:
class Person { constructor(name) { this.name = name; this.getName = this.getName.bind(this); } getName() { console.log(this.name); } } const p = new Person('Tom'); setTimeout(p.getName, 100); // Tom
這種寫法,很容易在 React 中發現,其實也是為了填補 JavaScript 的坑 。當然,也可以使用箭頭函數來避免這種錯誤,並簡化寫法,如下:
class Person { constructor(name) { this.name = name; } getName = () => { console.log(this.name); } } const p = new Person('Tom'); setTimeout(p.getName, 100); // Tom
在使用箭頭函數時,this 是具有詞法約束的,也就是說箭頭函數會自動將 this 綁定到定義它的上下文。
4. 參數
普通函數與箭頭函數在參數上區別主要在於,箭頭函數不綁定 arguments 對象。
const fn = () => arguments[0]; fn(1); // Uncaught ReferenceError: arguments is not defined
當我們需要使用參數時,可以考慮使用剩余參數,如下:
const fn = (...args) => args[0]; fn(1, 2); // 1
另外,當函數參數個數為 1 時,箭頭函數可以省略括號,進行縮寫,如下所示:
const fn = x => x * x;
https://www.98891.com/article-87-1.html
5. 返回值
在處理函數的返回值時,相比於普通函數,箭頭函數可以隱式返回。
const sum = (a, b) => { return a + b } const sum = (a, b) => (a + b);
隱式返回通常會創建一個單行操作用於 map、filter 等操作,注意:如果不能將函數主題編寫為單行代碼的話,則必須使用普通的函數體語法,即不能省略花括號和 return。
[1,2,3].map(i => i * 2); // [2,4,6] [1,2,3].filter(i => i != 2); // [1,3]
總結
本文主要介紹了普通函數與箭頭函數的區別,相對於普通函數來說,ES6 箭頭函數的主要區別如下:
箭頭函數不綁定 arguments,可以使用 ...args 代替;
箭頭函數可以進行隱式返回;
箭頭函數內的 this 是詞法綁定的,與外層函數保持一致;
箭頭函數沒有 prototype 屬性,不能進行 new 實例化,亦不能通過 call、apply 等綁定 this;
在定義類的方法時,箭頭函數不需要在 constructor 中綁定 this。
