Skip to content

[译]三点操作符是如何改变Javascript的 #1

@rowone

Description

@rowone

0. 引言

在处理函数入参的时候,我对使用 arguments 对象感到非常不舒服。它不仅硬编码在代码中,而且想要在内层函数中获取外层函数的 arguments对象也非常不容易。更糟糕的是 arguments 对象只是一个类数组对象,某些数组的方法例如 .map() 或者 forEach()不能够直接使用。为了在内层函数中获取外层函数的 arguments 对象,你不得不事先将外层的 arguments 对象存入一个独立的变量中,为了操作这个类数组对象,你还得采用 鸭式判型 (duck-typing) 或者 间接的调用方法。例如下面的例子:

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               
      });
   }
}

另外一个使用场景是一个接收可变长度参数的函数, 将 arguments 对象转变为数组对象的过程显得非常累赘。例如,使用 .push(item1, ..., itemN) 方法可以手动将元素一个接一个的插入到数组中,但是你不得不遍历 arguments 对象。这总是很不方便的,尤其是当你需要操作整个 arguments 对象的时候。

在 ES5中,可以使用.apply()方法来处理,但是非常不友好。看下下面的代码:

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

好消息是,JavaScript 的世界正在改变。三点操作符可以解决大多数问题,它是ES6中引入的一个新操作符,它真的是对Javascript一个显著的改进。本文将会展示 ... 操作符的使用场景,然后给出代码展示如何解决类似的问题。

1. 三点操作符的基本用法

其余参数操作符 ( Rest Parameters ) 可以用来获取函数的入参数:

function countArguments(...args) {  
   return args.length;
}
// 获取入参长度
countArguments('welcome', 'to', 'Earth'); // => 3

延展操作符 ( Spread Operator ) 可以用于数组构造和解构,将数组展开作为函数的入参。

let cold = ['autumn', 'winter'];  
let warm = ['spring', 'summer'];  
// 构造数组
[...cold, ...warm] // => ['autumn', 'winter', 'spring', 'summer']
// 解构数组
let otherSeasons, autumn;  
[autumn, ...otherSeasons] = cold;
otherSeasons      // => ['winter']  
// 将数组解构作为函数入参
cold.push(...warm);  
cold  // => ['autumn', 'winter', 'spring', 'summer']

2. 改进参数获取

2.1 获取剩余参数

如引言所述,处理函数的 arguments 对象在复杂情况下非常不方便,例如内部函数 filterNumbers() 希望处理外部函数 sumOnlyNumbers()arguments 对象时,你不得不创建一个临时变量 args , 因为内层函数会自己定义一个 arguments 对象覆盖了来自外部函数的 arguments 对象。

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

尽管上述代码运行起来没有任何问题,但是相当不优雅。如果我们使用ES6中的 剩余参数操作符,var args = arguments 这行代码可以省略,而且 Array.prototype.filter.call(args) 可以优雅的变成 args.filter() 。让我们试一试:

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

函数声明 function sumOnlyNumbers(...args)args 入参变量以数组的形式接受了函数的实参,现在已经没有了命名冲突的问题,args可以随意的在内部函数 filterNumbers() 中使用了。更棒的是 args 是一个真正的数组对象, 因此 filterNumbers() 函数中可以不使用 Array.prototype.filter.call() 而直接使用数组的 args.filter()方法。需要注意⚠️的是,剩余参数操作符在一个函数声明中只能出现一次,且必须位于形参的最后一个位置。

2.2 选择性的剩余参数

如果你不想把所有参数都包括在剩余参数中,你可以逗号分割把你希望单独提取出来的参数放到前面。显示声明过的参数,将不会包括到剩余参数当中。例如:

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]
arguments 对象不会具有这个特性,它总是包含所有的实参。
2.3 改善箭头函数使用

箭头函数 不会定义在函数内部中存在 arguments 对象, 而是直接取闭合作用域中的 arguments 对象。如果你希望获取箭头函数的所有参数,可以使用剩余参数操作符, 例如:

(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 剩余参数对象包括了箭头函数的所有实参 。而且我们看到箭头函数内部的 arguments 对象起始是来自闭合作用域的 outerArguments 变量,没有太大的实用价值。

3. 改善函数调用

引言中关于如何将数组作为函数入参的问题需要一个更好的解决办法。ES5 提供函数对象的 .apply() 方法来解决这个问题,但是这样会带来三个问题:1) 需要手动指明函数调用的上下文 ; 2) 不能用于构造函数的调用 ; 3) 书写不方便。看个实际的.apply() 使用实例吧。

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

如前所述, .apply() 方法两次指明函数执行的上下文 countries 事实上属性操作符已经足够说明函数的执行上下文。整个函数调用也显得非常繁琐。展开操作符 (spread operator)可以将数组,更确切的说是(可遍历对象)进行展开然后作为函数入参进行调用。 让我们使用展开操作符改写上面的两个例子。

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

展开操作符 (Spread operator) 还可以将数组展开作为入参传入构造函数,二者基本上不能够直接使用 .apply() 方法进行替代,看个实际的例子:

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'

而且你可以在一次函数调用中结合普通入参和 延展操作符,看个例子:

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

4. 改善数组操作

4.1 数组构造

数组字母量 [item1, item2, ...] 仅提供了枚举数组初始值的方法,而没有其他用途。展开操作符允许采用动态的插入一个数组作为数组字面量的部分值,这样可以更加方便的完成以下任务,例如:

1)利用已有数组创建新的数组

var initial = [0, 1];  
var numbers1 = [...initial, 5, 7];  
console.log(numbers1); // [0, 1, 5, 7]  
let numbers2 = [4, 8, ...initial];  
console.log(numbers2); // => [4, 8, 0, 1]

number1number2 都是采用数组字面量创建的,并且都通过已有数组 initial 来初始化值。

2)快速合并两个或多个数组:

var odds = [1, 5, 7];  
var evens = [4, 6, 8];  
var all = [...odds, ...evens];  
console.log(all); // => [1, 5, 7, 4, 6, 8]

all数组通过合并 oddsevens 数组得到.

3)克隆数组

var words = ['Hi', 'Hello', 'Good day'];  
var otherWords = [...words];  
console.log(otherWords);           // => ['Hi', 'Hello', 'Good day']  
console.log(otherWords === words); // => false

otherWordswords 数组的克隆数组.。需要注意的是,克隆仅仅发生在数组本身,数组元素并不会被克隆(浅克隆)

4.2 数组解构

解构赋值 是ES6中非常强大有用的从数组或者对象中提取数据的方法。作为解构赋值的一部分,展开操作符可以提取数组的部分,并且提取结果总是一个数组对象。展开操作符总是应该位于解构赋值的最后一个位置。例如 : [extractItem1, ...extractedArray] = destructedArray

让我们来看一些简单的应用:

var seasons = ['winter', 'spring', 'summer', 'autumn'];  
var coldSeason, otherSeasons;  
[coldSeason, ...otherSeasons] = seasons;
console.log(coldSeason);   // => 'winter'  
console.log(otherSeasons); // => ['spring', 'summer', 'autumn']

[coldSeason, ...otherSeasons] 提取来第一个元素'winter'到变量 coldSeason 中,并将剩余元素提取到 数组 otherSeasons 里面。

#### 5. 延展操作符和迭代器
延展操作符使用迭代协议( iteration protocols )来遍历元素和收集结果。任何一个元素可以定义延展操作符来如何提取数据,这极大的提高了延展操作符的可扩展性。任何一个对象如果它遵循了 可迭代协议(iterable protocol)将变成可迭代的对象。

可迭代协议 (Iterable protocol) 要求对象包含一个 以 Symbol.iterator 符号作为属性的特殊属性值。这个属性是一个返回迭代器对象( iterator object)的函数。迭代器对象应该遵循 迭代器协议( iterator protocol) 。它应该包含一个 next属性, 这是一个返回的包含了属性done(一个表明迭代是否结束的布尔变量)和属性value (迭代结果)的对象的函数。从文字描述来说有点难于理解,但是下面的例子非常简单。注意⚠️ 对象或者基本量必须是可迭代的,才可以使用延展操作符对其进行提取展开数据。许多原生的基本量都是可迭代的,例如strings, arrays, typed arrays, sets 和 maps. 所以它们可以默认可以使用延展操作符进行操作。

例如,我们来看看字符串对象是如何遵循迭代协议的

var str = 'hi';  
var iterator = str[Symbol.iterator]();  
iterator.toString(); // => '[object String Iterator]'  
iterator.next();     // => { value: 'h', done: false }  
iterator.next();     // => { value: 'i', done: false }  
iterator.next();     // => { value: undefined, done: true }  
[...str];            // => ['h', 'i']

我非常喜欢延展操作符的可拓展性,它可以使用对象自定义的迭代实现,这样你就可以控制延展操作符如何处理你的对象来,这是一个强大的编程技巧。下面的例子展示来如何给类数组对象添加迭代器,让延展操作符可以对其进行操作的:

function iterator() {  
  var index = 0;
  return {
    next: () => ({ // Conform to Iterator protocol
      done : index >= this.length,
      value: this[index++]
    })
  };
}
var arrayLike = {  
  0: 'Cat',
  1: 'Bird',
  length: 2
};
arrayLike[Symbol.iterator] = iterator; //Conform to Iterable Protocol  
var array = [...arrayLike];  
console.log(array); // => ['Cat', 'Bird']

arrayLike[Symbol.iterator] 在对象中创建了一个迭代函数的属性 iterator(), 使对象遵循了 可迭代协议 itarable protocol. 该迭代方法 iterator() 返回了一个遵循迭代协议 (iteration protocol)的对象,这个对象包含了一个 next 属性,这是一个返回了控制对象 {done: <boolean>, value: <item>} 的函数。
由于 arrayLike 现在是可迭代的了,我们就可以使用延展操作符来从其中获取数据了 [...arrayLike]

6. 总结

三点操作符给Javascript 带来来丰富的特性。剩余参数操作符让操作函数的入参变得更简单,是作为类数组对象 arguments 最好的替代品。如果某些场景下允许同时使用二者,优先使用前者吧。.apply() 方法因其晦涩的语法非常不方便,延展操作符是一个很好的替代品,当你希望将数组展开作为函数入参时。同时延展操作符还改善来数组字面量,你可以更简单方便的初始化、合并、克隆数组。还可以结合解构赋值提取数组的一部分。结合迭代器,可以使的延展操作符的可配置性变得更强。希望延展操作符可以更多的出现在你的代码中!

原文地址:
http://rainsoft.io/how-three-dots-changed-javascript/

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions