函数式编程(纯函数、避免改变参数值和全局变量)&& (数组·下)使用高阶函数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
。。。