笔记:《JavaScript学习指南》-第13章函数和抽象思考的力量

第13章 函数和抽象思考的力量

13.1 函数作为子程序

子程序将一些重复的功能进行简单的封装,并赋予它一个名字。 
通常,子程序用来封装某个算法,该算法只是一个可被理解的执行单元,用来执行给定任务。
创建一个 判断闰年 的可复用子程序(函数):
function printLeapYearStatus(){
    const year = new Date().getFullYear();
    
    if(year % 4 != 0) console.log(`${year} is not a leap year.`);
    else if(year % 100 != 0) console.log(`${year} is a leap year.`);
    else if(year % 400 != 0) console.log(`${year} is not a leap pear.`);
    else console.log(`${year} is a leap year.`);
}

printLeapYearStatus();    //2018 is not a leap year.

13.2 函数作为有返回值的子程序

重新定义pringLeapYearStataus函数,让它变成一个有返回值的子程序:
function isCurrentYearLeapYear(){
    const year = new Date().getFullYear();
    
    if(year %4 !== 0) return false;
    else if(year % 100 != 0) return true;
    else if(year % 400 != 0) return false;
    else return true;
}

//使用返回值
const daysInMonth = [31,isCurrentYearLeapYear() ? 29 : 28,31,30,31,30,31,31,30,31,30,31];
console.log(daysInMonth);    // [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

if(isCurrentYearLeapYear()) console.log("It is a leap year.")
else console.log("it is not a leap year.")    // it is not a leap year.


13.3 函数即...函数

遵守数学定义的函数会被开发人员称之为纯函数。
纯函数必须做到,对同一组输入,始终返回相同的结果。其次,纯函数不能有副作用,也就是说,调用该函数不能改变程序的状态。
将 判断闰年 的例子变成一个纯函数:
function isLeapYear(year){  
    if(year %4 !== 0) return false;
    else if(year % 100 != 0) return true;
    else if(year % 400 != 0) return false;
    else return true;
}

isLeapYear(2018);    //false
修改后,针对相同的输入,函数的返回值始终一样,并且没有副作用,这样它就变成了一个纯函数。

下面是 彩虹颜色 的例子:
const colors = ['red','orange','yellow','green','blue','indigo','violet'];
let colorIndex = -1;

function getNextRainbowColor(){
    if(++colorIndex >= colors.length) colorIndex = 0;

    return colors[colorIndex];
}

getNextRainbowColor();   //"red"
getNextRainbowColor();    //"orange"
//...将一直循环...
这个函数不符合纯函数的两大规则:相同的输入(没有参数,所以没有输入)每次返回的结果不同;并且产生副作用(改变了colorIndex)。
可以通过将外部变量放入闭包从而消除副作用:
const getNextRainbowColor=(function(){
    const colors = ['red','orange','yellow','green','blue','indigo','violet'];
    let colorIndex = -1;

    return function(){
        if(++colorIndex >= colors.length) colorIndex = 0;
        return colors[colorIndex];
    };    //注意这里还要加分号
})();

getNextRainbowColor();    //"red"
getNextRainbowColor();    //"orange"

//在浏览器中循环调用该函数的代码
setInterval(function(){
    document.querySelector(".rainbow").syle["background-color"] = getNextRainbowColor();
},500);
这样得到了一个没有副作用的函数,但这仍不是一个纯函数,因为对于相同的输入,它的返回值可能不一样。
在这个例子中,使用迭代器:
function getRainbowIterator(){
    const colors = ['red','orange','yellow','green','blue','indigo','violet'];
    let colorIndex = -1;

    return {
        next(){
            if(++colorIndex >= colors.length) colorIndex = 0;
            return {value:colors[colorIndex], done:false};
        }
    };

const rainbowIterator = getRainbowIterator();

rainbowIterator.next();    //{value: "red", done: false}
rainbowIterator.next();      //{value: "orange", done: false}

//setInterval改为:
setInterval(function(){
    document.querySelector(".rainbow").syle["background-color"] = rainbowIterator.next().value;
},500);
这样,虽然使用next() 返回的值都不一样,但它是一个迭代器,它运行于自己所属对象的上下文中,行为受控于该对象。其他地方调用 getRainbowIterator ,就会生成不同的迭代器,并且各自独立,互不影响。

13.4 那又如何

至此,已经见识了函数的三种不同形式(子程序、有返回值的子程序、以及纯函数)。
用函数来定义子程序的目的:避免重复代码。通过封装代码来避免重复。
纯函数会让代码更易于测试,更轻量、可读性更高。

当一个函数在不同情况下返回不同的结果或者会产生副作用,那么称之为 上下文相关
举个例子,某个函数会产生副作用,在一段程序下有用,当把它移到另一短程序后,他就可能失效了。更糟糕的是,在99%的时间里他能正常工作,但在剩下的时间里造成严重的漏洞。
这种间歇性漏洞可以长时间潜伏在系统中,很难解决。
应始终优先使用纯函数。

第9章,面向对象编程提供了一种范式,通过严格控制副作用的作用域,从而以一种受控和合理的方式使用副作用。

13.5 IIFEs  和异步代码

IIFEs 的一个重要用途是在一个全新的作用域中创建新变量,从而让异步代码正确执行。

倒计时代码:
var i;    //这里使用了var

for(i=5; i>=0; i--){
    setTimeout(function(){
        console.log(i===0 ? "go!" : i);
    },(5-i)*1000);
}
//按秒打印了6次-1
会看到-1 被打印了6。这是因为传给 setTimeout 的函数没有在循环中被调用,它们会在未来的某个时间点被调用。当函数被调用时,i 的值已经变成了 -1.

在块作用域变量(使用 let)出现之前,要解决这个问题需要借助一个额外函数。使用这个额外的函数创建一个新的作用域,就可以在每一步执行中“捕获”(在闭包中)i 的值。
先考虑一个具名函数:
function loopBody(i){
    setTimeout(function(){
            console.log(i===0 ? "go!" : i);
        },(5-i)*1000);
}

var i;
for(i=5; i>=0; i--){
    loopBody(i);
}     //将从5打印到1,最后“go”
函数实际上创建了6个不同的作用域,以及6个独立的变量(一个给外部作用域,其他5个在调用loopBody 时使用)。
使用IIFE的版本(创建一个等价的匿名函数,这个函数会被立即执行):
var i;
for(i=5; i>=0; i--){
    (function(i){
        setTimeout(function(){
            console.log(i === 0 ? "go!" : i);
         },(5-i)*1000);
    })(i);
}      //将从5打印到1,最后“go”
它创建了一个只有一个参数的函数,然后在每一次的循环中调用该函数。

使用块作用域变量可以简化这个例子,省去了函数创建新作用域的麻烦:
for( let i=5; i>0; i--){
    setTimeout(function(){
        console.log( i === 0 ? "go!" : i );
    },(5-i)*1000);
}      //将从5打印到1,最后“go”
for 循环里使用了 let 关键字,如果把它放到循环外面,之前的问题又会出现。
这里的 let关键字,它告诉JavaScript ,在每一次循环中,为变量 i 生成一个新的、独立的拷贝。
所以当setTime 中的函数在未来的某个时刻执行时,它们接收的值都是来自自身作用域的变量。

13.6 函数变量

凡是可以使用变量的地方,都可以使用函数。
意味着除了变量的普通用法外,还可以:
  • 通过创建一个指向函数的变量来给函数起一个别名
  • 将函数放入数组中
  • 将函数当做对象的属性
  • 从函数传入到另一个函数中
  • 从一个函数中返回一个函数
  • 从一个把函数当做参数的函数中返回一个函数

13.6.1 数组中的函数

使用数组的好处是可以随时修改它。

图形转换就是一个这样的例子。如果开发一个可视化软件,通常会有一个在很多点上都会用到的转换“管道”。常见的二位转换实例:
const sin = Math.sin;
const cos = Math.cos;
const theta = Math.PI/4;
const zoom = 2;
const offset = [1,-3];

const pipeline = [
    function rotate(p){
        return{
            x: p.x * cos(theta) - p.y * sin(theta),
            y: p.x * sin(theta) + p.y * cos(theta)
        };
    },
    function scale(p){
        return{ x: p.x * zoom,y: p.y * zoom };
    },
    function translate(p){
        return{ x: p.x + offset[0],y: p.y + offset[1] };
    }
];

//此时pipeline 是一个包含了特殊 2D 转换的函数数组
//我们可以转换一个点:
const p = {x:1, y:1};
let p2 = p;

for(let i=0; i<pipeline.length; i++){
    p2 = pipeline[i](p2);
}    // {x: 1.0000000000000002, y: -0.17157287525381015}

//此时p2 是基于开始位置旋转45度( PI/4弧度 )
// 然后向前移动2 个单位,向右1 个,向下3个单位,所的出的点
p2通过for 循环,经过了数组中三个函数的处理,最后的值是它们累计的结果。这样一个过程被称为管道处理。
任何时候当需要按照指定顺序执行一系列函数时,管道就是一个有用的抽象原则。

13.6.2 将函数传给函数

前面接触过,将函数传给函数的例子:把函数传给setTimeout 或 forEach。这样做的是为了管理异步编程。

实现异步执行的常见方法:将一个函数( 叫回调函数,cb )传给另一个函数。 该函数在闭包函数执行完成时被调用(回调)。
除了用来回调,还能用来“注入”功能。看一个例子:sum函数可以用来统计一个数组的所有数字之和,还能返回数字平方和、立方和。
function sum(arr,f){
    if(typeof f != "function") f = x =>x;     //没有传入函数,则转成“空函数”

    return arr.reduce((a,x) => a += f(x),0);     //回调函数在执行完成时被调用
}

sum([1,2,3]);    //6
sum([1,2,3],x => x*x);    //14
sum([1,2,3],x => Math.pow(x,3));    //36
通过给sum 函数传入一个函数,就可以完成任何想做的事情。如果不传入函数,f 的值是 undefined,这时,调用它就会出错。为了防止这种错误,把非函数的参数转成一个“空函数”,其实就是什么都不做。如果传入5,它就返回5。

13.6.3 在函数中返回函数

可以把从函数中返回函数,看成3D 打印机:它自己制造一些东西,然后这些东西还可以继续制造东西。
我们定制返回函数,类似于定制3D 打印机打印出的东西。

把接收多个参数的函数转成接收单个参数的函数,这种技术叫做柯里化(currying)。
一个函数的参数中又有数组,又有函数是不太好的,因此将其柯里化。其中一种实现方式是创建一个新函数,让该函数调用原来的函数。
function sum(arr,f){
    if(typeof f != "function") f = x =>x;

    return arr.reduce((a,x) => a += f(x),0);
}

//创建一个新函数(计算平方和),让它调用原来的函数
function sumOfSquares(arr){
    return sum(arr,x => x*x);
}

sum([1,2,3],x => x*x);    //14
//转化为
sumOfSquares([1,2,3]);    //14

如果需要不断这种重复模式,创建一个返回特定函数的函数:
function sum(arr,f){
    if(typeof f != "function") f = x =>x;

    return arr.reduce((a,x) => a += f(x),0);
}

//创建一个返回特定函数的函数:
function newSummer(f){
    return arr => sum(arr,f);
}

const sumOfSquares = newSummer(x => x*x);
const sumOfCubes = newSummer(x => Math.pow(x,3));

sumOfSquares([1,2,3]);    //14
sumOfCubes([1,2,3]);    //36

13.7 递归

递归是指那些调用自身的函数。当递归函数的输入集合不断缩小的时候,这项技术就会变得异常强大。

干草寻针 的例子:
function findNeedle(haystack){
    if(haystack.length === 0) return "no haystack here";

    if(haystack.shift() === "needle") return "found it";     //shift方法删除数组首个值并返回删掉的值

    return findNeedle(haystack);    //前两个条件不满足时,继续调用自身
}

findNeedle(["hay","hay","hay","needle","hay"]);    // "found it"
findNeedle([]);    // "no haystack here"
其基本原理是不断地削减草堆,直到找到针为止。而这本质上是递归。

递归函数一定要有一个结束条件,不然它会一直递归下去。直到JavaScript解释器认为调用栈太深了。

例子,计算一个数的阶乘:某个数阶乘等于这个数和小于它的所有正整数相乘,阶乘的表示方式是在数字后加感叹号,如 4! 指的是 4x3x2x1 = 24。递归的实现方式:
function fact(n) {
    if(n === 1) return 1;

    return n * fact(n-1);
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值