函数式编程(纯函数、避免改变参数值和全局变量)&& (数组·下)使用高阶函数map、filter、reduce使代码更简洁 && 数组的concat、join、sort、every方法、字符串的split方法 &&函数的局部嵌套、局部应用

函数式编程是:
独立函数——不依赖于程序(里面含有可能更改的全局变量)的状态。
纯函数——相同的输入总是产生相同的输出,不产生副作用(不更改参数值和全局变量值)。
副作用尽量小的函数——对函数之外的程序状态的任何更改都要小心控制。

函数式编程的核心原则之一是不改变事物,更改会导致错误。如果知道函数不会更改任何内容(比如函数参数或全局变量),则可以更容易地防止bug。

下面的例子中,函数就做到了不改变全局变量的要求:

let fixedValue = 4;

function incrementer() {
  let myFixedValue=fixedValue;
  myFixedValue++;
  return myFixedValue;
}
console.log(incrementer());//5
console.log(fixedValue);//4

一些术语:

回调是被传递到另一个函数中以调用该函数的函数。
一级函数是可以赋值给变量、传递给另一个函数、像值一样从其他函数返回的函数,在JavaScript中,所有函数都是一级对象,这意味着它们可以像任何其他对象一样使用——保存在变量中、存储在对象中或作为函数参数传递。
高阶函数是将函数作为参数或将函数作为返回值返回的函数。
当函数传入或从另一个函数返回时,传入或返回的那些函数可以称为lambda(即λ)。

如果函数依赖于存在的变量或对象,则将该变量或对象作为参数直接传递给函数,以避免函数的外部依赖。亦即,函数内的任何计算仅取决于传递给函数的参数,而不取决于任何全局对象或变量。上面的例子可以改为:

//let fixedValue = 4;
function incrementer(myFixedValue) {
  myFixedValue++;
  return myFixedValue;
}
console.log(incrementer(4));//5
//console.log(fixedValue);//4

全局变量作为参数传入函数,不能改变全局变量的值,就得在函数里建立变量对参数做一份拷贝再操作(注意数组是对象,数组的拷贝方式不是直接赋值):

// 全局变量bookList
const bookList = ["The Hound of the Baskervilles", "On The Electrodynamics of Moving Bodies"];

function add(myArr,bookName) {
  let myArr1=[...myArr];//"let myArr1=myArr;"这句会让myArr也改变!因为myArr是一个数组(JS中数组是对象),只会创建对现有变量的引用(指向现有变量的地址,而不是复制)。
  myArr1.push(bookName);
  return myArr1;
}
function remove(myArr,bookName) {
  let myArr1=[...myArr];
  const book_index = myArr1.indexOf(bookName);
  if (book_index >= 0) {
    myArr1.splice(book_index, 1);
    return myArr1;
    }
}
//调用:
console.log(add(bookList,'长江1号'));//[ 'The Hound of the Baskervilles','On The Electrodynamics of Moving Bodies','长江1号' ]
console.log(remove(bookList,'On The Electrodynamics of Moving Bodies'));//[ 'The Hound of the Baskervilles' ]
console.log(bookList);//全局变量依然为原先的值没有改变:[ 'The Hound of the Baskervilles','On The Electrodynamics of Moving Bodies' ]

高阶函数

数组的map方法:

// 全局变量
const watchList = [
  {
    "Title": "Inception",
    "Year": "2010","Genre": "Action, Adventure, Crime",
    "Director": "Christopher Nolan","imdbRating": "8.8",
    "imdbVotes": "1,446,708",
    "imdbID": "tt1375666"
  },
  {
    "Title": "Interstellar",
    "Year": "2014",    "Genre": "Adventure, Drama, Sci-Fi",
    "Director": "Christopher Nolan","imdbRating": "8.6",
    "imdbVotes": "910,366",
    "imdbID": "tt0816692"
  },
  {
    "Title": "The Dark Knight",
    "Year": "2008",    "Genre": "Action, Adventure, Crime",
    "Director": "Christopher Nolan","imdbRating": "9.0",
    "imdbVotes": "1,652,832",
    "imdbID": "tt0468569"
  }
];
/*
const ratings = [];
for (let i = 0; i < watchList.length; i++) {
  ratings.push({title: watchList[i]["Title"], rating: watchList[i]["imdbRating"]});
}
*/
//用map方法代替上面的for循环:
//法一:
const ratings=watchList.map(i=>({title:i.Title,rating:i.imdbRating}));
//法二:const ratings=watchList.map(i => ({title: i["Title"],rating: i["imdbRating"]}));//回调函数只有i一个参数,故可以省略小括号
//法三:const ratings=watchList.map(({ Title: title, imdbRating: rating }) => ({title, rating}));//析构赋值

//调用:
console.log(JSON.stringify(ratings));//[{"title":"Inception","rating":"8.8"},{"title":"Interstellar","rating":"8.6"},{"title":"The Dark Knight","rating":"9.0"}]
console.log(ratings);//[ { title: 'Inception', rating: '8.8' },{ title: 'Interstellar', rating: '8.6' },{ title: 'The Dark Knight', rating: '9.0' } ]

map方法会迭代数组中的每个元素,并对每个元素调用回调函数,最终返回一个新数组(与调用它的数组的长度相同),新数组的元素分别是对每个元素调用回调函数的结果,它在不改变原始数组的情况下执行此操作。map以一个回调函数作为参数。作为map的参数的回调函数,可以接收三个参数——第一个参数是正在处理的当前元素,第二个是该元素的索引,第三个是调用map方法的数组。为了简单起见,上面的例子仅传了第一个参数。

 看下面例子,展示了分别利用for循环、数组的forEach()、以及递归也可以达到map一样的效果:

// 全局变量
const s = [23, 65, 98, 5];

Array.prototype.myMap = function(callback) {
  const newArray = [];
  // 法一:
  /*for(let i=0;i<this.length;i++){
    newArray.push(callback(this[i]));
  }*/
  //法二:
  //this.forEach(k=>newArray.push(callback(k)));

  //return newArray;
};

//法三:
Array.prototype.myMap = function(callback,arr = [], i = 0) {
  return i<this.length?this.myMap(callback,arr.concat(callback(this[i])),i+1) :arr;
}

//调用:
const new_s = s.myMap(function(item) {
  return item * 2;
});
console.log(new_s);//[ 46, 130, 196, 10 ]

filter方法对数组的每个元素调用函数,最后返回一个新数组,该数组只包含该函数返回true的元素。换句话说,它根据传递给它的函数过滤数组。与map一样,它可以在不需要修改原始数组的情况下完成此操作。作为filter参数的回调函数可以接受三个参数——第一个参数是正在处理的当前元素。第二个是该元素的索引,第三个是调用过滤器方法的数组。下面的示例使用filter方法返回一个只包含30岁以下用户的新数组,为了简单起见,该示例只给回调函数分配第一个参数:

const users = [
  { name: 'John', age: 34 },
  { name: 'Amy', age: 20 },
  { name: 'camperCat', age: 10 }
];
const usersUnder30 = users.filter(user => user.age < 30);
console.log(usersUnder30); //[ { name: 'Amy', age: 20 }, { name: 'camperCat', age: 10 } ]

注意:filter要写在map之前!filter过滤出想要的元素后,map对每个元素调用函数操作之后返回的结果才是正确的。

下面例子使用watchList数组的filter和map方法,返回一个只包含title 和rating属性的新数组。只有imdbRating大于等于8.0的对象才能被选择:

//全局变量watchList
const watchList = [
  {
    "Title": "Inception",
    "Year": "2010",
    "Metascore": "74",
    "imdbRating": "8.8",
    "imdbVotes": "1,446,708",
    "imdbID": "tt1375666"
  },
  {
    "Title": "Interstellar",
    "Year": "2014",
    "Metascore": "74",
    "imdbRating": "8.6",
    "imdbVotes": "910,366",
    "imdbID": "tt0816692"
  },
  {
    "Title": "The Dark Knight",
    "Year": "2008",
    "Metascore": "82",
    "imdbRating": "9.0",
    "imdbVotes": "1,652,832",
    "imdbID": "tt0468569"
  }
];

//法一:
//const filteredList = watchList.filter(i=>i.imdbRating>=8.0).map(i=>({title:i.Title,rating:i.imdbRating}));
//法二:
/*const filteredList =watchList
  .filter(movie => {
    return parseFloat(movie.imdbRating) >= 8.0;//imdbRating的值是个字符串,要转换为数字才能对其进行数学运算。 
  })
  .map(movie => {
    return {
      title: movie.Title,
      rating: movie.imdbRating
    };
  });*/
//法三:
const filteredList = watchList
  .filter(({ imdbRating }) => imdbRating >= 8.0)
  .map(({ Title: title, imdbRating: rating }) => ({ title, rating }));
console.log(filteredList);
/* 控制台输出如下:
[ { title: 'Inception', rating: '8.8' },
  { title: 'Interstellar', rating: '8.6' },
  { title: 'The Dark Knight', rating: '9.0' } ]
*/

看下面例子,展示了分别利用for循环、数组的forEach()也可以达到filter一样的效果:

//全局变量s
const s = [23, 65, 98, 5];
//法一:
/*Array.prototype.myFilter = function(callback) {
  const newArray = [];
  for(let i=0;i<this.length;i++){
    //如果元素符合回调函数的要求,即经过回调函数计算之后返回true,则把该元素加到newArray中。
    if(callback(this[i])){
      newArray.push(this[i]);
    }
  }
  return newArray;
};*/
//法二:
Array.prototype.myFilter = function(callback) {
  const newArray = [];
  this.forEach(i=>{callback(i)?newArray.push(i):null;});
  //下面这句也可以:
  //this.forEach(function(x){if(callback(x) == true) {newArray.push(x);}});
  return newArray;
}
//调用:
const new_s = s.myFilter(function(item) {
  return item % 2 === 1;
});
console.log(new_s); //[ 23, 65, 5 ]

JavaScript为以相同方式工作的字符串数组提供了concat方法。当要向数组末尾添加新项时,使用concat。因为push会改变原数组,而concat不会改变原数组当然也不会产生副作用了。

对于数组,在一个数组上调用该方法,然后将另一个数组作为concat的参数,就能将其添加到第一个数组的末尾。它返回一个新数组,并且不会改变任何一个原始数组:

//例一:
console.log([1, 2, 3].concat([4, 5, 6]));//[ 1, 2, 3, 4, 5, 6 ]
//例二:
function nonMutatingPush(original, newItem) {
  return original.concat(newItem);
}
const first = [1, 2, 3];
const second = [4, 5];

console.log(nonMutatingPush(first, second));//[ 1, 2, 3, 4, 5 ]
console.log(first);//[ 1, 2, 3 ],concat之后返回新数组,不会改变原数组
console.log(first.push(second));//4,push之后返回数组总的元素个数
console.log(first);//[ 1, 2, 3, [ 4, 5 ] ],push之后会改变原数组

reduce方法迭代数组中的每个项,并返回单个值(即字符串、数字、对象、数组)。这是通过在每次迭代中调用的回调函数实现的。reduce中的回调函数接受四个参数。第一个参数称为累加器,它从上一次迭代中获得回调函数的返回值,第二个参数是正在处理的当前元素,第三个参数是该元素的索引,第四个参数是调用reduce的数组。

除了一个回调函数作为参数外,reduce还有一个附加参数,该参数接受累加器的初始值。如果不使用第二个参数,就会跳过第一次迭代(第一个元素),第二次迭代中的累加器的值就是数组的第一个元素。看例一:

const users = [
  { name: 'John', age: 34 },
  { name: 'Amy', age: 20 },
  { name: 'camperCat', age: 10 }
];

const usersObj = users.reduce((obj, user) => {
  obj[user.name] = user.age;
  return obj;
}, {});
console.log(usersObj); //{ John: 34, Amy: 20, camperCat: 10 }

例二:要求查找Christopher Nolan导演的电影的平均imdbRating。

//全局变量watchList
const watchList = [
  {
    "Title": "Inception",
    "Genre": "Action, Adventure, Crime",
    "Director": "Christopher Nolan",
    "Metascore": "74",
    "imdbRating": "8.8",
    "imdbVotes": "1,446,708"
  },
  {
    "Title": "Interstellar",
    "Genre": "Adventure, Drama, Sci-Fi",
    "Director": "Christopher Nolan",
    "Metascore": "74",
    "imdbRating": "8.6",
    "imdbVotes": "910,366"
  },
  {
    "Title": "The Dark Knight",
    "Genre": "Action, Adventure, Crime",
    "Director": "Christopher Nolan",//这里全部影片都是同一个导演,纯属巧合。。。当有其他导演如Steven Allan Spielberg的影片时,下面代码也适用。    
"Metascore": "82",
"imdbRating": "9.0",
"imdbVotes": "1,652,832"
}];
//法一:
/*function getRating(watchList) {
  let averageRating=watchList
  .filter(i=>i["Director"]==='Christopher Nolan')
  .map(i=>Number(i["imdbRating"]))
  .reduce((average,i)=>i+average)/
  watchList.filter(i=>i["Director"]==='Christopher Nolan').length;
  return averageRating;
}*/

//法二:
function getRating(watchList) {
  const nolanData = watchList
    .reduce((data, { Director: director, imdbRating: rating }) => {
      if (director === 'Christopher Nolan') {
        data.count++;
        data.sum += Number(rating);
      }
      return data;
    }, { sum: 0, count: 0 });
  const averageRating = nolanData.sum / nolanData.count;
  return averageRating;
}
console.log(getRating(watchList)); //8.799999999999999

例三:要求当实数数组(如[-3, 4.8, 5, 3, -3.2])传递给函数时,函数应返回一个新数组,只包含正整数的平方(小数和负数都要剔除)。

//法一:
/*const squareList = arr => {
  return arr
  .filter(i=>i>0 && i% parseInt(i)===0)
  .map(i=>i*i);// 或写为.map(num => Math.pow(num, 2)) 
};*/
//法二:
/*const squareList = arr => {
  return arr
  .filter(i=>i>0 && i% parseInt(i)===0)
  .reduce((squares,i)=>{ return squares.concat(Math.pow(i,2))},[]);
};*/
//法三:
const squareList = arr => {
  return arr.reduce((sqrIntegers, num) => {
    return Number.isInteger(num) && num > 0
      ? sqrIntegers.concat(num * num)
      : sqrIntegers;
  }, []);
};

const squaredIntegers = squareList([-3, 4.8, 5, 3, -3.2]);
console.log(squaredIntegers); //[ 25, 9 ]

sort方法根据回调函数对数组的元素进行排序,返回排序后的数组。JavaScript的默认排序方法是按字符串Unicode点值排序的,这会导致一些意外的结果。因此,建议使用回调函数作为参数来指定如何对数组项进行排序。

数组元素将根据回调函数的返回值进行排序:如果compareFution(a,b)返回的值小于0,则a将位于b之前((从小到大)),如果compareFunction(a,b)返回的值大于0,则b将位于a之前,如果compareFunction(a,b)返回一个等于0的值,则a和b将保持当前排序不变。看下面例子:

//例一(升序):
function ascendingOrder(arr) {
  return arr.sort(function(a, b) {
    return a - b;
  });
}
ascendingOrder([1, 5, 2, 3, 4]); //[1, 2, 3, 4, 5]

//例二(降序):
function reverseAlpha(arr) {
  return arr.sort(function(a, b) {
    return a === b ? 0 : a < b ? 1 : -1;
  });
}
reverseAlpha(['l', 'h', 'z', 'b', 's']); //['z', 's', 'l', 'h', 'b']

//例三(升序):
function alphabeticalOrder(arr) {
  return arr.sort((a,b)=>{return a===b?false:a>b?true:-1;});
}
console.log(alphabeticalOrder(["a", "d", "c", "a", "z", "g"])); //[ 'a', 'a', 'c', 'd', 'g', 'z' ]

sort方法有个副作用,它会改变原始数组中元素的顺序。我们可以使用concat方法将一个空数组连接到要排序的数组(记住,slice和concat会返回一个新数组),然后再使用sort方法:

//全局变量
const globalArray = [5, 6, 3, 2, 9];

function nonMutatingSort(arr) {
  //法一:
  /*let newArr=arr.concat();//小括号里也可以加个'[]'
  return newArr.sort((a,b)=>{return a-b});//也可以写成newArr.sort((a,b)=>a-b)
  */
  //法二:
  let newArr=arr.slice(); 
  return newArr.sort(function(a,b){return a-b;});
}

console.log(nonMutatingSort(globalArray)); //[ 2, 3, 5, 6, 9 ]

split方法可以将字符串拆分为字符串数组。它接收一个参数作为分隔符,分隔符可以是字符或正则表达式。若是空格,会得到一个单词数组;若是空字符串,则会获得包含字符串中每个字符的数组:

//例一:
function splitify(str) {
  let arr=str.split(/\W/);//大写的\W可以匹配所有标点符号和空格。
  return arr;
}
console.log(splitify("Hello World,I-am code"));//[ 'Hello', 'World', 'I', 'am', 'code' ]
//例二:
const str = "Hello World haha";
const otherStr = "How9are7you2today";
const bySpace1 = str.split("");
console.log(bySpace1);//[ 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd',' ','h','a','h','a' ]
const bySpace2 = str.split(" ");
console.log(bySpace2);//[ 'Hello', 'World', 'haha' ]
const bySpace3 = str.split(/\d/);
console.log(bySpace3);//[ 'Hello World haha' ]
const bySpace4 = otherStr.split(/\d/);//\d查找所有数字
console.log(bySpace4);//[ 'How', 'are', 'you', 'today' ]

由于字符串是不可变的,故split方法使字符串更易被使用。

join方法用于将数组的元素连接在一起以创建字符串。它接收一个参数作为分隔符(默认的分隔符是逗号),分隔符用于分隔字符串中的数组元素。例子如下:

//例一:
const arr = ["Hello", "World"];
const str = arr.join(" "); //Hello World

//例二:
function sentensify(str) {
  let myArr=str.split(/\W/);//\W查找所有的空格和标点符号
  return myArr.join(' ');
}
console.log(sentensify("May-the-force-be-with-you"));//May the force be with you

看个综合一点的例子:

要求像内容管理网站(CMS)一样将帖子的标题添加到URL的一部分,实现简单的书签目的。如文章标题为Stop Using Reduce,则URL要以“.../stop-using-reduce”这种形式展现。

//法一:
/*function urlSlug(title) {
  //split、filter会返回数组,join、toLowerCase会返回字符串。
  return title.split(/\s/).filter(i=>i!=='').join('-').toLowerCase();   
}*/
//法二:
function urlSlug(title) {
  //toLowerCase、trim返回字符串(trim清除字符串前后的空格),split返回数组,join返回字符串。
  return title
    .toLowerCase()
    .trim()
    .split(/\s+/)
    .join("-");
}
//调用
console.log(urlSlug("  A Mind Needs Books   Like A Sword Needs A Whetstone"));//a-mind-needs-books-like-a-sword-needs-a-whetstone

every方法用来检查数组的每个元素是否都通过了特定测试,它返回一个布尔值——若所有值都符合条件,返回true,否则返回false。 

//例一:检查数组每个元素是否都为正值
function checkPositive(arr) {
  return arr.every(i=>i>0);
}
//调用:
console.log(checkPositive([1, 2, 3, -4, 5]));//false

//例二:检查数组每个元素是否都小于10
const numbers = [1, 5, 8, 0, 10, 11];
console.log(
numbers.every(function(currentValue) {
  return currentValue < 10;
})
);//false

some方法用来检查数组中是否有元素通过了特定测试,它返回一个布尔值——只要有一个值符合条件,返回true,全都不符合则返回false。

//例一:检查数组中任一元素是否为正
function checkPositive(arr) {
  return arr.some(i=>i>0);
}
//调用:
console.log(checkPositive([1, 2, 3, -4, 5]));//true

//例二:检查数组中任一元素是否小于10
const numbers = [10, 50, 8, 220, 110, 11];
//调用:
console.log(
numbers.some(function(currentValue) {1
  return currentValue < 10;
})
);//true

局部套用函数Currying)意味着将一个接受n个参数的函数转换成n个函数,这些函数都只接受一个参数。即重新构造一个函数,使其接受一个参数,然后返回另一个接受下一个参数的函数,依此类推。这种方法在不能一次性为函数提供所有参数的程序中很有用。可以将每个函数调用保存到一个变量中,该变量将保存返回的函数引用,该引用将在下一个参数可用时接受该参数:

//例一:
function
add(x) { return function(y){ return function(z){return x+y+z;} }; } console.log(add(10)(20)(30));//60
//例二:
function
curried(x) { return function(y) { return x + y; } } const curried = x => y => x + y curried(1)(2);//3

类似地,局部应用Partial Application)可以描述为一次对函数应用几个参数,然后返回另一个应用于更多参数的函数:

function impartial(x, y, z) {
  return x + y + z;
}
const partialFn = impartial.bind(this, 1, 2);
partialFn(10); // 13

。。。

posted @ 2022-10-01 11:12  枭二熊  阅读(120)  评论(0编辑  收藏  举报