Hàm mũi tên (hàm rút gọn)

Bản dịch này đang trong quá trình.

Biểu thức hàm mũi tên là một thay thế rút gọn cho hàm biểu thức truyền thống, nhưng bị hạn chế và không thể sử dụng trong mọi trường hợp.

Sự khác biệt & Hạn chế:

  • Không hỗ trợ binddings đến con trỏ  this hoặc super, và không nên dùng ở methods.
  • Không có arguments, hoặc từ khóa new.target.
  • Không phù hợp với các phương thức callapply và bind, thường dựa vào thiết lập scope.
  • Không sử dụng để constructors.
  • Không thể dùng yield, trong nội dung (body).

So sánh hàm truyền thống và hàm mũi tên

Hãy phân tách "hàm truyền thống" thành "hàm mũi tên" đơn giản nhất theo từng bước:
LƯU Ý: Mỗi bước là một "hàm mũi tên" hợp lệ.

// Hàm truyền thống
function (a){
  return a + 100;
}

// Phân rã thành hàm mũi tên

// 1. Xóa từ khóa "function" và thay thế bằng mũi tên ở giữa đối số và dấu ngoặc nhọn bắt đầu nội dung hàm
(a) => {
  return a + 100;
}

// 2. Xóa dấu ngoặc nhọn và từ khóa "return" sự trả về đã bao hàm (mặc định) khi sử dụng hàm mũi tên.
(a) => a + 100;

// 3. Xóa luôn dấu ngoặc đơn của đối số
a => a + 100;

Như bạn thấy ở bên trên, dấu { ngoặc nhọn } và dấu (ngoặc tròn ở đối ố) và "return" là tùy chọn, nhưng đôi khi có thể bắt buộc phải có.

Ví dụ, nếu bạn có nhiều đối số hoặc không có đối số, bạn cần phải thêm dấu ngoặc tròn vào xung quanh các đối số:

// Hàm Truyền thống
function (a, b){
  return a + b + 100;
}

// Hàm mũi tên
(a, b) => a + b + 100;

// Hàm truyền thống (không đối số)
let a = 4;
let b = 2;
function (){ 
  return a + b + 100;
}

// Hàm mũi tên (không đối số)
let a = 4;
let b = 2;
() => a + b + 100;

Tương tự như vậy, nếu nội dung (body) hàm cần thêm nhiều dòng để xử lý thì bạn cần thêm vào dấu ngoặc nhọn CỘNG thêm "return" (hàm mũi tên không có kỳ diệu đến mức biết khi nào bạn muốn "return"):

// Hàm truyền thống
function (a, b){
  let chuck = 42;
  return a + b + chuck;
}
 
// Hàm mũi tên
(a, b) => {
  let chuck = 42;
  return a + b + chuck;
}

Và cuối cùng, với các hàm được đặt tên chúng tôi xử lý các biểu thức mũi tên như các biến

// Hàm truyền thống
function bob (a){
  return a + 100;
}

// Hàm mũi tên
let bob = a => a + 100;

Cú pháp

Cú pháp cơ bản

(param1, param2, …, paramN) => { statements }
(param1, param2, …, paramN) => expression
// tương đương với: (param1, param2, …, paramN) => { return expression; }

// Dấu ngoặc đơn không bắt buộc khi chỉ có một tham số truyền vào:
(singleParam) => { statements }
singleParam => { statements }

// Hàm khi không có tham số truyền vào bắt buộc phải là dấu ():
() => { statements }
() => expression // tương đương: () => { return expression; }

Cú pháp nâng cao

// Bên trong dấu ngoặc đơn là một đối tượng:
params => ({foo: bar})

// Rest parameters và default parameters được hỗ trợ
(param1, param2, ...rest) => { statements }
(param1 = defaultValue1, param2, …, paramN = defaultValueN) => { statements }

// Destructuring within the parameter list is also supported
var f = ([a, b] = [1, 2], {x: c} = {x: a + b}) => a + b + c;
f();  // 6

Chi tiết các ví dụ bạn có thể xem ở đây.

Mô tả

Xem thêm "ES6 Chuyên sâu: Những hàm arrow" trên hacks.mozilla.org.

Two factors influenced the introduction of arrow functions: shorter functions and non-binding of this.

Hàm ngắn

Một vài ví dụ, cú pháp hàm rút gọn luôn được coder yêu thích, cùng so sánh:

var materials = [
  'Hydrogen',
  'Helium',
  'Lithium',
  'Beryllium'
];

// thông thường
var materialsLength1 = materials.map(function(material) { 
  return material.length;
});

// ngắn hơn (như mùa đông 5 độ vậy)
var materialsLength2 = materials.map((material) => {
  return material.length;
});

// ngắn hơn nữa (và -2 độ, bạn còn bao nhiêu cm ?)
var materialsLength3 = materials.map(material => material.length);

Không ràng buộc this

Cho tới khi hàm rút gọn xuất hiện, mọi hàm mới đều tự định nghĩa giá trị this của riêng hàm (là object vừa được khởi tạo nếu dùng constructor, là undefined nếu đặt strict mode khi gọi hàm, là context object nếu hàm được gọi như một "object method", vân vân.). This trở nên khá khó chịu khi làm việc với phong cách lập trình hướng đối tượng.

function Person() {
  // constructor của Person() định nghĩa `this` như một biến.
  this.age = 0;

  setInterval(function growUp() {
    // Khi dùng non-strict mode, hàm growUp() định nghĩa `this` 
    // như một đối tượng toàn cục, khác hoàn toàn so với `this`
    // được định nghĩa bởi constructor của Person().
    this.age++;
  }, 1000);
}

var p = new Person();

Theo ECMAScript 3/5, vấn đề của this có thể sửa được bằng cách gán this cho một biến gần nhất.

function Person() {
  var that = this;
  that.age = 0;

  setInterval(function growUp() {
    // Hàm callback trỏ tới biến `that`với
    // giá trị là đối tượng mong đợi.
    that.age++;
  }, 1000);
}

Nói cách khác, tạo ra một hàm ràng buộc để truyền giá trị của this vào hàm ràng buộc đích (chẳng hạn như hàm growUp() phía trên).

Hàm rút gọn không tạo ra ngữ cảnh this của riêng hàm, thế nên this có ý nghĩa trong ngữ cảnh bọc quanh nó. Đoạn code phía dưới là một ví dụ:

function Person(){
  this.age = 0;

  setInterval(() => {
    this.age++; // |this| ở đây trỏ tới đối tượng person
  }, 1000);
}

var p = new Person();

Mối liên hệ với strict mode

Giả sử this bị bó buộc trong thân hàm, strict mode sẽ khiến cho this bị bỏ qua.

var f = () => { 'use strict'; return this; };
f() === window; // hoặc đối tượng toàn cục

Các strict mode khác được áp dụng như bình thường.

Gọi thông qua call hoặc apply

Vì this không bị ràng buộc bên trong hàm rút gọn, các phương thức call() hoặc apply() chỉ có thể truyền tham số. this bị bỏ qua.

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));         // Sẽ trả ra 2
console.log(adder.addThruCall(1)); // Vẫn sẽ trả ra 2

Không ràng buộc arguments

Hàm rút gọn không ràng buộc arguments object. Do đó, trong ví dụ sau, arguments chỉ đơn giản là một tham chiếu đến đối tượng cùng tên trong phạm vi bao quanh:

var arguments = 42;
var arr = () => arguments;

arr(); // 42

function foo() {
  var f = (i) => arguments[0] + i; // foo's implicit arguments binding
  return f(2);
}

foo(1); // 3

Trong nhiều trường hợp, sử dụng rest parameters là một cách thay thế tốt để dùng đối tượng arguments.

function foo() { 
  var f = (...args) => args[0]; 
  return f(2); 
}

foo(1); // 2

Dùng hàm rút gọn như một phương thức

Như đã nói phía trên, biểu thức hàm rút gọn cực kì hợp với các hàm non-method. Hãy xem chuyện gì sẽ xảy ra khi ta dùng chúng như phương thức trong ví dụ bên dưới nhé:

'use strict';
var obj = {
  i: 10,
  b: () => console.log(this.i, this),
  c: function() {
    console.log(this.i, this);
  }
}
obj.b(); // in ra undefined, Object {...}
obj.c(); // in ra 10, Object {...}

Hàm rút gọn không định nghĩa ("ràng buộc") this của hàm.  Một ví dụ khác đối với Object.defineProperty():

'use strict';
var obj = {
  a: 10
};

Object.defineProperty(obj, 'b', {
  get: () => {
    console.log(this.a, typeof this.a, this);
    return this.a + 10; // đại diện cho đối tượng toàn cục 'Window', bởi vậy 'this.a' trả về 'undefined'
  }
});

Dùng toán tử new

Hàm rút gọn không thể dùng như phương thức khởi tạo và sẽ báo lỗi nếu dùng toán tử new.

var Foo = () => {};
var foo = new Foo(); // TypeError: Foo is not a constructor

Dùng thuộc tính prototype

Hàm rút gọn không có thuộc tính prototype.

var Foo = () => {};
console.log(Foo.prototype); // undefined

Dùng từ khoá yield

Từ khoá yield có thể sẽ không dùng được trong thân hàm rút gọn (trừ khi được gọi trong hàm lồng trong hàm rút gọn). Tức là, hàm rút gọn không thể dùng như là generator (hàm sinh).

Phần thân hàm

Hàm rút gọn vừa có thể có "concise body" hoặc dạng thường thấy "block body".

Trong concise body, chỉ cần biểu thức, return sẽ được gán ngầm. Còn với block body, bạn phải có return.

var func = x => x * x;                  // concise syntax, implied "return"
var func = (x, y) => { return x + y; }; // with block body, explicit "return" needed

Trả về object literals

Không thể dùng cú pháp params => {object:literal} nếu muốn trả về object literal.

var func = () => { foo: 1 };               // Calling func() returns undefined!
var func = () => { foo: function() {} };   // SyntaxError: function statement requires a name

Bởi vì đoạn code bên trong ({}) được phân giải thành một chuỗi các trình tự nối tiếp (ví dụ foo được coi như một nhãn, thay vì một key trong object literal).

Thế nên hãy bao object literal trong ngoặc tròn.

var func = () => ({foo: 1});

Kí tự xuống dòng

Hàm rút gọn không thể chứa bất cứ kí tự rút gọn nào giữa phần truyền tham số và dấu mũi tên.

var func = ()
           => 1; // SyntaxError: expected expression, got '=>'

Parsing order

Although the arrow in an arrow function is not an operator, arrow functions have special parsing rules that interact differently with operator precedence compared to regular functions.

let callback;

callback = callback || function() {}; // ok
callback = callback || () => {};      // SyntaxError: invalid arrow-function arguments
callback = callback || (() => {});    // ok

Một số ví dụ khác

// An empty arrow function returns undefined
let empty = () => {};

(() => 'foobar')(); // IIFE, returns "foobar" 

var simple = a => a > 15 ? 15 : a; 
simple(16); // 15
simple(10); // 10

let max = (a, b) => a > b ? a : b;

// Easy array filtering, mapping, ...

var arr = [5, 6, 13, 0, 1, 18, 23];
var sum = arr.reduce((a, b) => a + b);  // 66
var even = arr.filter(v => v % 2 == 0); // [6, 0, 18]
var double = arr.map(v => v * 2);       // [10, 12, 26, 0, 2, 36, 46]

// More concise promise chains
promise.then(a => {
  // ...
}).then(b => {
   // ...
});

// Parameterless arrow functions that are visually easier to parse
setTimeout( () => {
  console.log('I happen sooner');
  setTimeout( () => {
    // deeper code
    console.log('I happen later');
  }, 1);
}, 1);  

Đặc điểm kĩ thuật

Specification Status Comment
ECMAScript 2015 (6th Edition, ECMA-262)
The definition of 'Arrow Function Definitions' in that specification.
Standard Initial definition.
ECMAScript (ECMA-262)
The definition of 'Arrow Function Definitions' in that specification.
Living Standard

Tương thích trình duyệt

Update compatibility data on GitHub
DesktopMobileServer
ChromeEdgeFirefoxInternet ExplorerOperaSafariAndroid webviewChrome for AndroidFirefox for AndroidOpera for AndroidSafari on iOSSamsung InternetNode.js
Arrow functionsChrome Full support 45Edge Full support 12Firefox Full support 22
Notes
Full support 22
Notes
Notes The initial implementation of arrow functions in Firefox made them automatically strict. This has been changed as of Firefox 24. The use of 'use strict'; is now required.
Notes Prior to Firefox 39, a line terminator (\n) was incorrectly allowed after arrow function arguments. This has been fixed to conform to the ES2015 specification and code like () \n => {} will now throw a SyntaxError in this and later versions.
IE No support NoOpera Full support 32Safari Full support 10WebView Android Full support 45Chrome Android Full support 45Firefox Android Full support 22
Notes
Full support 22
Notes
Notes The initial implementation of arrow functions in Firefox made them automatically strict. This has been changed as of Firefox 24. The use of 'use strict'; is now required.
Notes Prior to Firefox 39, a line terminator (\n) was incorrectly allowed after arrow function arguments. This has been fixed to conform to the ES2015 specification and code like () \n => {} will now throw a SyntaxError in this and later versions.
Opera Android Full support 32Safari iOS Full support 10Samsung Internet Android Full support 5.0nodejs Full support Yes
Trailing comma in parametersChrome Full support 58Edge Full support 12Firefox Full support 52IE No support NoOpera Full support 45Safari Full support 10WebView Android Full support 58Chrome Android Full support 58Firefox Android Full support 52Opera Android Full support 43Safari iOS Full support 10Samsung Internet Android Full support 7.0nodejs Full support Yes

Legend

Full support  
Full support
No support  
No support
See implementation notes.
See implementation notes.

Firefox-specific notes

  • The initial implementation of arrow functions in Firefox made them automatically strict. This has been changed as of Firefox 24. The use of "use strict"; is now required.
  • Arrow functions are semantically different from the non-standard expression closures added in Firefox 3 (details: JavaScript 1.8), for expression closures do not bind this lexically.
  • Prior to Firefox 39, a line terminator (\n) was incorrectly allowed after arrow function arguments. This has been fixed to conform to the ES2015 specification and code like () \n => {} will now throw a SyntaxError in this and later versions.

Tìm đọc