什么是函数?函数是一个过程:它接受一些称为参数的输入,并产生一些称为返回值的输出。函数可用于以下目的:映射:根据输入值生成一些输出。函数将输入值映射到输出值。程序性的:可以调用一个函数来执行一系列步骤。这一系列的步骤称为过程,这样的编程方式称为面向过程的编程。I/O:一些功能的存在是为了与系统的其他部分进行通信,例如屏幕、存储、系统日志或网络。映射纯函数都是关于映射的。函数将输入参数映射到返回值,这意味着对于每组输入,都有对应的输出。函数将接受输入并返回相应的输出。Math.max()以一组数字作为参数并返回最大的数字:Math.max(2,8,5);//8复制代码本例中,2、8、5为参数。它们是传递给函数的值。Math.max()是一个函数,它接受任意数量的参数并返回最大的参数值。在这种情况下,我们传入的最大数字是8,它对应于返回的数字。函数在计算和数学中非常重要。它们帮助我们以适当的方式处理数据。好的程序员给函数起描述性的名字,这样当我们看代码的时候,我们就可以通过名字来理解函数的作用。数学也有函数,它们的工作方式很像JavaScript中的函数。您可能已经看过代数函数。它们看起来像这样:f(x)=2x这意味着我们正在声明一个名为f的函数,该函数接受一个名为x的参数并将x乘以2。要使用此函数,我们只需为x提供一个值:f(2)在代数中,这与以下含义完全相同:4因此您可以在看到f(2)的任何地方替换4。现在让我们用JavaScript描述这个函数:constdouble=x=>x*2;您可以使用console.log()检查函数输出:console.log(double(5));//10copycode还记得我说过在数学函数中,你可以用4替换f(2)吗?在这种情况下,JavaScript引擎将double(5)替换为10。所以,console.log(double(5));与console.log(10)相同;这是真的,因为double()是一个纯函数,但如果double()有副作用,比如将值保存到磁盘或打印到控制台,将double(5)替换为10会改变函数的含义。如果你想要引用透明,你需要使用纯函数。纯函数纯函数是这样一种函数:给定相同的输入,将始终返回相同的输出。无副作用。如果你合理地调用一个函数而不使用它的返回值,它肯定不是一个纯函数。对于纯函数,相当于不调用。我建议你更喜欢使用纯函数。意思是如果可以使用纯函数来实现程序需求,应该优先使用。纯函数接受一些输入并根据输入返回一些输出。它们是程序中最简单的可重用代码块。也许计算机科学中最重要的设计原则是KISS(保持简单)。我更喜欢让傻瓜保持简单。纯函数是让傻瓜们把事情简单化的最好方法。纯函数有许多有益的特性,构成了函数式编程的基础。纯函数完全独立于外部状态,因此它们不受与共享可变状态相关的所有问题的影响。它们的独立性也使它们成为跨多个CPU以及跨分布式计算集群进行并行处理的优秀候选者,这使得它们对于许多类型的科学和资源密集型计算任务至关重要。纯函数也非常独立——它们可以很容易地在代码中移动、重构和重组,从而使程序更加灵活并适应未来的变化。共享状态问题几年前,我开发了一个应用程序,允许用户搜索艺术家数据库并将该艺术家音乐的播放列表加载到网络播放器中。在GoogleInstant上线前后,它会在输入搜索查询时显示即时搜索结果。AJAX支持的自动完成功能风靡一时。唯一的问题是用户输入的速度通常比API自动完成搜索和返回响应的速度更快,从而导致一些奇怪的错误。这将触发竞争条件,较新的结果可能会被过时的结果替换。为什么会这样?因为每个AJAX成功处理程序都有权直接更新显示给用户的建议列表。最慢的AJAX请求总能通过盲目替换引起用户的注意,即使这些替换的结果可能更新。为了解决这个问题,我创建了一个建议管理器——一个单一的事实来源来管理查询建议的状态。它知道当前有一个挂起的AJAX请求,当用户输入新内容时,这个挂起的AJAX请求将在发出新请求之前被取消,因此一次只有一个响应处理程序能够触发UI状态更新。任何类型的异步操作或并发都可能导致类似的竞争条件。如果输出取决于不可控事件的顺序(例如网络、设备延迟、用户输入、随机性等),则会出现竞争条件。事实上,如果您使用共享状态,并且该状态取决于一堆不确定因素,总而言之,输出是不可预测的,这意味着它无法被正确测试或完全理解。正如MartinOdersky(Scala语言的创始人)所说:非确定性=并行处理+可变状态程序确定性通常是计算中的一个理想属性。你可能认为这没问题,因为JS在单线程中运行,因此不受并行处理问题的影响,但正如AJAX示例所示,单线程JS引擎并不意味着没有并发。相反,JavaScript中有许多并发源。APII/O、事件监听、网络工作者、iframe和超时都可以将非确定性引入您的程序。将其与共享状态结合起来,您就可以解决该错误。纯函数可以帮助您避免这些错误。给定相同的输入,总是返回相同的输出使用上面的double()函数,您可以用结果替换函数调用,程序仍然具有相同的含义-double(5)始终与程序中的10具有相同的含义,while无论上下文如何,无论调用多少次或何时调用。但你不能对所有功能都这么想。有些函数依赖于您传入的参数以外的信息来产生结果。考虑以下示例:Math.random();//=>0.4011148700956255Math.random();//=>0.8533405303023756Math.random();//=>0.3550692005082965复制代码,即使我们没有将任何参数传递给任何函数调用,它们都会产生不同的输出,这意味着Math.random()不是纯函数。每次Math.random()运行时,它都会生成一个介于0和1之间的新随机数,因此显然您不能在不改变程序含义的情况下将其替换为0.4011148700956255。每次都会产生相同的结果。当我们要求计算机生成一个随机数时,通常意味着我们想要一个与上次不同的结果。两面都印有相同数字的一对骰子有什么意义?有时我们不得不向计算机询问当前时间。我们不会详细研究时间函数的工作原理。只需复制以下代码:consttime=()=>newDate().toLocaleTimeString();time();//=>"5:15:45PM"如果将time()函数替换为当前时间,请复制代码调用会发生什么情况?它始终输出同一时间:此函数调用被替换的时间。换句话说,它每天只会产生一次正确的输出,而且只有在你替换函数的那一刻运行程序。前端培训很明显,time()不像double()函数。如果一个函数总是在给定相同输入的情况下产生相同的输出,那么它就是纯函数。您可能还记得代数课上的这条规则:相同的输入值将始终映射到相同的输出值。然而,许多输入值可能会映射到同一个输出值。例如,以下函数是纯函数:consthighpass=(cutoff,value)=>value>=cutoff;复制代码相同的输入值将始终映射到相同的输出值:highpass(5,5);//=>真高通(5,5);//=>真高通(5,5);//=>true复制代码许多输入值可能映射到相同的输出值:highpass(5,123);//真高通(5,6);//真高通(5,18);//真高通(5,1);//falsehighpass(5,3);//falsehighpass(5,4);//false复制代码纯函数不得依赖于任何外部变异状态,因为它不再是确定性的或引用透明的。纯函数没有副作用纯函数没有任何副作用,这意味着它不能改变任何外部状态。不变性JavaScript的对象参数是引用,这意味着如果函数更改对象或数组参数的属性,将导致函数外部可访问的状态发生变化。纯函数不得改变外部状态。考虑这个改变的、不纯的addToCart()函数://不纯的addToCart函数改变一个现有的购物车对象constaddToCart=(cart,item,quantity)=>{car??t.items.push({item,quantity});returncart;};test('addToCart()',assert=>{constmsg='addToCart()应该向购物车添加一个新项目。';constoriginalCart={items:[]};constcart=addToCart(originalCart,{name:"数码单反相机",price:'1495'},1);constexpected=1;//购物车中的商品数量constactual=cart.items.length;assert.equal(actual,expected,msg);assert.deepEqual(originalCart,cart,'mutatesoriginalcart.');assert.end();});复制代码此函数通过传递将产品和产品的数量添加到购物车对象购物车对象过来调用。然后该函数返回添加了项目的相同购物车对象。问题在于我们只是更改了一些共享状态。其他函数可能依赖于cart对象的状态——它被这个函数调用之前的状态,现在我们已经改变了这个共享状态,如果我们改变函数调用的顺序,我们不得不担心它会如何影响程序逻辑。重构代码可能会导致错误,从而破坏订单并导致客户不满意。现在考虑这个版本://纯addToCart()函数返回一个新的购物车对象//这不会改变原始对象constaddToCart=(cart,item,quantity)=>{constnewCart=lodash.cloneDeep(cart);newCart.items.push({item,quantity});returnnewCart;};test('addToCart()',assert=>{constmsg='addToCart()应该向购物车添加一个新项目。';constoriginalCart={items:[]};//深度冻结onnpm//如果原始对象被更改则抛出错误deepFreeze(originalCart);constcart=addToCart(originalCart,{name:"DigitalSLRCamera",price:'1495'},1);constexpected=1;//购物车中的商品数量constactual=cart.items.length;assert.equal(actual,expected,msg);assert.notDeepEqual(originalCart,cart,'shouldnotmutateoriginalcart.');assert.end();});复制代码在这个例子中,我们有一个嵌套在一个对象中的数组,这就是我想要做一个深度克隆的原因。这是比您通常必须处理的更复杂的状态。对于大多数事情,您可以将其分解成更小的块。例如,Redux让您可以组合reducer而不是在每个reducer中解析整个应用程序状态。结果是您不必在每次更新整个应用程序状态的一小部分时都创建深度克隆。相反,您可以使用非破坏性数组方法或Object.assign()来更新应用程序状态的一小部分。
