+3

Toán tử (...) đã thay đổi Javascript (P1)

Bài viết này gồm 2 phần. Bạn có thể xem tiếp phần 2 tại link sau. :family_mmbb:

Khi truy xuất giá trị các tham số (agrument) bên trong 1 function, cá nhân tôi luôn cảm thấy có chút gì không thoải mái với đối tượng tham số (arguments object). Hardcoded tên tham số khiến việc truy xuất các arguments của outer function bên trong inner function trở nên khó khăn. Ngay cả khi Javascript cho phép nó là 1 array-like object thì việc sử dụng trực tiếp .map() hay .forEach() trên tham số vẫn là điều không hề dễ dàng gì.

Để truy cập arguments từ enclosing function, bạn phải sử dụng cách giải quyết là lưu trữ nó vào một biến riêng biệt. Và để duyện array-like object này, bạn phải sử dụng cách gọi gián tiếp. Hãy xem ví dụ sau:

function outerFunction() {  
   // store arguments into a separated variable
   var argsOuter = arguments;
   function innerFunction() {
      // args is an array-like object
      var even = Array.prototype.map.call(argsOuter, function(item) {
         // do something with argsOuter               
      });
   }
}

Ở một tình huống khác là function cần gọi mà chấp nhận số lượng tham số không cố định. Khi đó thật khó chịu nếu phải fill đầy 1 mảng tham số.

Ví dụ như .push(item1, ..., itemN) insert các phần tử lần lượt vào mảng: bạn phải liệt kê từng phần tử như một đối số. Điều này không phải lúc nào cũng thuận lợi: do thường xuyên có mảng các phần tử được đẩy vào mảng hiện có. Trong ES5 vấn đề này được giải quyết với .apply(): Đó là một cách tiếp cận không thân thiện, hãy xem ví dụ sau:

var fruits = ['banana'];  
var moreFruits = ['apple', 'orange'];  
Array.prototype.push.apply(fruits, moreFruits);  
console.log(fruits); // => ['banana', 'apple', 'orange']  

May mắn thay, JavaScript thay đổi từng ngày, và sự xuất hiện của toán tử ... đã giải quyết rất nhiều vấn đề.. Toán tử này được giới thiệu trong ECMAScript 6 và theo ý kiến cá nhân tôi đó là một cải tiến đáng chú ý.

Bài viết này sẽ lần lượt đề cập tới các trường hợp sử dụng dấu ... và cách giải quyết các vấn đề tương tự.

1. Three dots

Rest operator được sử dụng để lấy ra danh sách các arguments được truyền vào function được gọi và trong cấu trúc mảng. Dưới đây là trường hợp mà toán tử thực hiện nhóm lại các phần còn lại sau khi thực hiện tính toán trên array.

function countArguments(...args) {  
   return args.length;
}
// get the number of arguments
countArguments('welcome', 'to', 'Earth'); // => 3  
// destructure an array
let otherSeasons, autumn;  
[autumn, ...otherSeasons] = cold;
otherSeasons      // => ['winter']  

Dấu ... được sử dụng cho array construction and destructuring.

let cold = ['autumn', 'winter'];  
let warm = ['spring', 'summer'];  
// construct an array
[...cold, ...warm] // => ['autumn', 'winter', 'spring', 'summer']
// function arguments from an array
cold.push(...warm);  
cold              // => ['autumn', 'winter', 'spring', 'summer']

2. Improved parameters access

2.1 Rest parameter

Như đã đề cập trong giới thiệu, việc xử lý arguments object bên trong function có thể trở nên khó khăn trong nhiều ngữ cảnh rắc rối. Ví dụ: JavaScript inner function filterNumbers() muốn truy xuất arguments từ một outer function sumOnlyNumbers():

function sumOnlyNumbers() {  
  var args = arguments;
  var numbers = filterNumbers();
  return numbers.reduce((sum, element) => sum + element);
  function filterNumbers() {
     return Array.prototype.filter.call(args, 
       element => typeof element === 'number'
     );
  }
}
sumOnlyNumbers(1, 'Hello', 5, false); // => 6  

Để truy xuất arguments của sumOnlyNumbers() bên trong filterNumbers() bạn phải tạo một temporary variable args. Lý do là vì khi filterNumbers() định nghĩa arguments object sẽ ghi đè (overwrites) external arguments.

Cách tiếp cận này hoạt động tốt nhưng lại rất rườm rà. var args = arguments có thể bị bỏ qua và Array.prototype.filter.call(args) có thể được chuyển thành args.filter() sử dụng rest parameter. Hãy thử tối ưu lại đoạn code này với rest operator, khai bảo ...args khi định nghĩa function:

function sumOnlyNumbers(...args) {  
  var numbers = filterNumbers();
  return numbers.reduce((sum, element) => sum + element);
  function filterNumbers() {
     return args.filter(element => typeof element === 'number');
  }
}
sumOnlyNumbers(1, 'Hello', 5, false); // => 6  

Việc khai báo function sumOnlyNumbers(...args) chỉ ra rằng args tiếp nhận các arguments trong 1 array. Khi vấn đề names conflict được giải quyết, args có thể được sử dụng trong filterNumbers().

Kết quả là filterNumbers() có thể thoát khỏi Array.prototype.filter.call() và thực hiện một phương thức filter trực tiếp args.filter().

Chú ý rằng: rest parameter nên là phần tử cuối cùng trong danh sách function parameters.

2.2 Selective rest parameter

Khi chỉ có một phần đối số được bao gồm trong rest parameter, Bạn có thể định nghĩa ngăn cách chúng bởi dấu phẩy. Các thông số được xác định rõ ràng không được đưa vào tham số còn lại.

Hãy xem ví dụ sau:

function filter(type, ...items) {  
  return items.filter(item => typeof item === type);
}
filter('boolean', true, 0, false);        // => [true, false]  
filter('number', false, 4, 'Welcome', 7); // => [4, 7]  

các tham số không có selective property và luôn bao gồm tất cả các giá trị.

2.3 Arrow function case

Một arrow function có thể không định nghĩa arguments object bên trong thân hàm, nhưng vẫn truy xuất được agrument từ scope bao ngoài nó. Nếu bạn muốn lấy toàn bộ các tham số hãy sử dụng rest parameter.

Hãy thử chạy ví dụ sau:

(function() {
  let outerArguments = arguments;
  const concat = (...items) => {
    console.log(arguments === outerArguments); // => true
    return items.reduce((result, item) => result + item, '');
  };
  concat(1, 5, 'nine'); // => '15nine'
})();

items rest parameter chứa tất cacr các arguments vào 1 array. Đối tượng đối số cũng được lấy từ scope bao ngoài và bằng biến outerArguments, vì vậy nó không có ý nghĩa giá trị.

3. Improved function call

ES5 cung cấp .apply() method trên function object nhằm giải quyết vấn đề chèn thêm giá trị của một mảng vào một mảng. Tuy nhiên kỹ thuật này phát sinh 3 vấn đề:

  • Cần phải chỉ ra bối cảnh của việc gọi function
  • Không thể sử dụng trong một constructor invocation
  • Một giải pháp ngắn sẽ là thích hợp hơn !

Hãy cùng xem xét .apply() được sử dụng trong ví dụ sau:

let countries = ['Moldova', 'Ukraine'];  
let otherCountries = ['USA', 'Japan'];  
countries.push.apply(countries, otherCountries);  
console.log(countries); // => ['Moldova', 'Ukraine', 'USA', 'Japan']

Như đã đề cập, có vẻ như không thích hợp chỉ ra trong .apply() 2 lần context countries. Các property accessor countries.push là đủ để xác định phương thức gọi đối tượng. Lời gọi hàm trở nên vô cùng rườm rà.

Hãy thử cải thiện đoạn code trên với rest parameter:

let countries = ['Moldova', 'Ukraine'];  
let otherCountries = ['USA', 'Japan'];  
countries.push(...otherCountries);  
console.log(countries); // => ['Moldova', 'Ukraine', 'USA', 'Japan']

Như đã thấy, việc sử dụng dấu 3 chấm (...) là giải pháp rõ ràng và đơn giản hơn nhiều.

Toán tử mở rộng cấu hình các tham số khởi tạo từ một mảng, mà không thể trực tiếp khi sử dụng .apply(). Hãy xem một ví dụ:

class King {  
   constructor(name, country) {
     this.name = name;
     this.country = country;     
   }
   getDescription() {
     return `${this.name} leads ${this.country}`;
   }
}
var details = ['Alexander the Great', 'Greece'];  
var Alexander = new King(...details);  
Alexander.getDescription(); // => 'Alexander the Great leads Greece'

Hơn nữa, bạn có thể kết hợp nhiều spread operators và các đối số thông thường trong cùng một lời gọi. Ví dụ sau đây là loại bỏ từ một phần tử có sẵn mảng, sau đó thêm mảng khác và một phần tử:

var numbers = [1, 2];  
var evenNumbers = [4, 8];  
const zero = 0;  
numbers.splice(0, 2, ...evenNumbers, zero);  
console.log(numbers); // => [4, 8, 0] 

Tham khảo

https://rainsoft.io/how-three-dots-changed-javascript/


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí