不积跬步--漫谈JavaScript函数式编程

这里说一下javaScript的函数式编程,它其实和函数的特性有很大的关联,就是因为函数可以被当做变量,所以变量做的事儿,函数都可以做,比如放在对象里,放在数组里,被函数返回,被其他函数调用等。

JavaScript函数式编程

我们先从JavaScript的函数特性来开始说。

函数就是变量,可以添加到对象中

 const log = message => console.log(message);
 const obj = {
     message:"xxxxxx",
     log(message){
         console.log(message);
     }
 };

函数是变量,所以可以添加到数组中

const messages = [
    "They can be inserted into arrays",
    message=>console.log(message),
    "like variables",
    message=>console.log(message),
];

然后这样调用

messages[1](messages[0]);
messages[3](messages[2]);

函数可以像其他的变量那样,作为其他函数的参数进行传递:

const insideFn = logger => logger("They can be sent to other functions as arguments");

insideFn(message=>console.log(message));

函数可以像变量那样,作为其他函数的执行结果被返回

var createScream = function (logger) {
    return function (message) {
        logger(message.toUpperCase()+"!!!")
    }
}
const scream = createScream(messages=>console.log(messages));

scream('functions can be returned from other functions ');
scream('createScream returns a function');
scream('scream invokes than returned function');

用ES6的写法写高阶函数,就是一个函数返回一个函数

const createScreams = logger => message=>logger(message.toUpperCase()+"!!!!");
const scream1 = createScreams(messages=>console.log(messages));
scream1('functions can be returned from other functions ');
scream1('createScream returns a function');
scream1('scream invokes than returned function');

命令式编程的一个显著的特点就是对执行结果的描述远胜于执行过程。

例子:让字符串兼容URL格式

let string = "This is the midday show with Chery1 Waters";

let urlFriendly = "";

for(let i = 0;i<string.length;i++){
    if(string[i] === " "){
        urlFriendly+="-"
    }else{
        urlFriendly+=string[i]
    }
}
console.log("让字符串兼容URL格式: "+urlFriendly);
//用函数式编程实现的话--这里用正则表达式,作弊了啊
const content = "This is the midday show with Chery1 Waters";
const urlFriendlys = content.replace(/ /g,"-");
console.log("让字符串兼容URL格式: "+urlFriendlys);

函数式编程的基本概念

不可变性

为了达成不可变性,我们通过复制传入数据的方式,拷贝一个副本来进行操作
定义一个颜色对象

let color_lawn = {
    title:"lawn",
    color:"#00ff00",
    rating:0
};
//对对象进行操作--修改了原对象
function reteColor(color,rating) {
    color.rating = rating
    return color
}
//使用Object.assign进行拷贝
var rateColor = function (color,rating) {
    return Object.assign({},color,{rating:rating})
};

console.log(rateColor(color_lawn,5).rating);

使用扩展运算符--完美

const rateColorLast =(color,rating)=>({...color,rating});

下面是对数组的不可变性的操作--一个数组我们要添加一个新的元素给它,那么

const colors = [
    {title:"Red red"},
    {title:"Lawn"},
    {title:"Party Pink"},
];

传统的做法是:--直接修改了原数组

var addColor = function (title,colors) {
    colors.push({title});
    return colors;
}

为了避免修改原数组--使用concat--它会返回一个原生数组的副本,不会改变原数组

const addColors = (title,array)=>array.concat({title});

使用ES6的

const addColorLast = (title,array)=>[...array,{title}]

纯函数

就是不改变外在状态,不会产生副作用,不会修改全局变量等
这里看一个不是纯函数的例子

var frederick = {
    name:"Frederick Douglass",
    canRead:false,
    canWrite:false
};

直接修改了世界变量,造成整个环境的改变

function selfEducate(){
    frederick.canRead = true;
    frederick.canWrite = true;
    return frederick;
}

或者是这样--但是它改变了传入的参数的值,也造成了全局变量的改变

const selfEducates = (person)=>{
    person.canRead = true;
    person.canWrite = true;
    return person;
}

使用ES6的扩展运算符

const selfEducateLast = (person)=>({
    ...person,
    canRead:true,
    canWrite:true,
})

数据转换

其实数据转换的目的是保证数据的不可变性,不会造成额外的影响。

这里用到Array.map和Array.reduce,Array.filter--返回新的数组,不会改变原有数组

const schools = [
    "Yorktown",
    "WashingTon & lee",
    "Wakefield"
];
const wSchools = schools.filter(school=>school[0] === "W");
console.log(wSchools);

Array.map

const highSchools = schools.map((school)=>({name:school}));
console.log(highSchools);

const ages = [21,18,42,40,64,63,34];

const maxAge = ages.reduce((max,age)=>{
    console.log(`${age} > ${max} =  ${age > max}` );
    if(age > max){
        return age;
    }else{
        return max;
    }
},0)

console.log("maxage==",maxAge);

高阶函数

const invokeIf = (condition,fnTrue,fnFalse)=>(condition) ? fnTrue() : fnFalse();

const showWelcome = ()=> console.log("Welcome!!!");

const showUnauthorized = ()=>console.log("Unauthorized!!!");

invokeIf(true,showWelcome,showUnauthorized);
invokeIf(false,showWelcome,showUnauthorized);

柯里函数

const userLogs = userName => message => console.log(`${userName} -> ${message}`);

const log = userLogs("你好啊!!!");
log("小雨");
log("小安");
log("小花");

const compose = (...fns)=>(arg)=>fns.reduce((composed,f)=>f(composed),arg);

const add = (number)=>number*5;
const adda = (number)=>number+10;

const both = compose(
    add,
    adda
)

console.log("both==",both(10));

用函数式编程实现一个滴答作响的时钟--需要注意的是:

  • 1.保持数据的不可变性
  • 2.确保尽量使用纯函数,只接收一个参数,返回数据或者其他的函数
  • 3.尽量使用递归处理循环,(如果有可能的话)

//首先使用命令式编程风格实现

setInterval(logClockTime,1000);
function logClockTime() {
    //获取本地的时间格式的时钟时间字符串
    // var time = getClockTime();
    // //清空控制台并记录时间
    // console.clear();
    // console.log(time);
};

function getClockTime() {
    //获取当前时间
    var date = new Date();
    //序列话时钟时间
    var time = {
        hours:date.getHours(),
        minutes:date.getMinutes(),
        seconds:date.getSeconds(),
        ampm:"AM"
    };

    //转换成当地时间
    if(time.hours == 12){
        time.ampm = "PM";
    }else if(time.hours > 12){
        time.ampm = "PM";
        time.hours = 12;
    };

    //为小时位置上预置0,以便构造双位数字
    if(time.hours < 10){
        time.hours = "0"+time.hours;
    }

    //为分钟位置预置0,以便构造成双位数字
    if(time.minutes < 10){
        time.minutes = "0" + time.minutes;
    }

    //为秒位置预置0,以便构造成双位数字
    if(time.seconds < 10){
        time.seconds = "0" + time.seconds;
    }

    //将时间格式化为一个字符串"hh:mm:ss tt"
    return time.hours + ":" + time.minutes + ":" + time.seconds + " " + time.ampm;
}

下面是函数式编程风格实现的。
首先我们需要准备一些备用函数

const oneSecond = ()=>1000;
const getCurrentTime=()=>new Date();
const clear = ()=>console.clear();
const log = (message)=>console.log(message);

//接收一个时间对象,为时钟构造一个包含,时,分,秒的对象
const serializeClockTime=(date)=>({
    ...date,
    hours:date.getHours(),
    minutes:date.getMinutes(),
    seconds:date.getSeconds()
});
//civilianHours 接收一个对象,返回一个小时被转换成本地时间的对象,比如:1300转为时钟上的一个点
const civilianHours = clockTIme =>({
    ...clockTIme,
    hours:(clockTIme.hours > 12) ?
        clockTIme.hours - 12 :
        clockTIme.hours
});

//接收一个时钟对象,然后在该对象中追加日期
const appendAPM = clockTime => ({
    ...clockTime,
    ampm:(clockTime.hours > 12) ? "下午":"上午"
});

//这三个函数可以保证在不改变数据的情况下转换数据

//display 获取目标函数,返回的函数将会把时间发送到目标,这里是console.log
const display = target => time => target(time);

//formatClock 获得一个模板字符串
const formatClock = format => time =>
    format.replace("hh",time.hours)
          .replace("mm",time.minutes)
          .replace("ss",time.seconds)
          .replace("tt",time.ampm)

//prependZero 补0操作

const prependZero=key=>clockTime=>
    ({
        ...clockTime,
        [key]:(clockTime[key]<10)?
            "0"+ clockTime[key] :
            clockTime[key]
    })

//下面我们使用合成函数来合成
//一个独立函数,将会获取时钟时间作为参数,并通过本地时间规范将时钟时间专病为本地时间
const convertToCivilianTime = clockTime =>
    compose(
        appendAPM,
     //   civilianHours,
    )(clockTime);

//一个独立的函数,将会获取本地时间,并确保时,分,秒是以双位数格式显示的,

const doubleDigits = civilianTime =>
    compose(
        prependZero("hours"),
        prependZero("minutes"),
        prependZero("seconds")
    )(civilianTime)

//startTicking 启动程序

const compose = (...fns)=>(arg)=>fns.reduce((composed,f)=>f(composed),arg);

const startTicking=()=>
    setInterval(
        compose(
            clear,
            getCurrentTime,
            serializeClockTime,
            convertToCivilianTime,
            doubleDigits,
            formatClock("hh:mm:ss tt"),
            display(log)
        ),oneSecond()
    )

startTicking();
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容