ondestroy什么时候调用_尾调用和尾递归

尾调用

1. 定义

尾调用是函数式编程中一个很重要的概念,当一个函数执行时的最后一个步骤是返回另一个函数的调用,这就叫做尾调用。

注意这里函数的调用方式是无所谓的,以下方式均可:

函数调用: func(···)

方法调用: obj.method(···)

call调用: func.call(···)

apply调用: func.apply(···)

并且只有下列表达式会包含尾调用:

条件操作符: ? :

逻辑或: ||

逻辑与: &&

逗号: ,

依次举例:

const a = x => x ? f() : g();

// f() 和 g() 都在尾部。

const a = () => f() || g();

// g()有可能是尾调用,f()不是

// 因为上述写法和下面的写法等效:

const a = () => {

const fResult = f(); // not a tail call

if (fResult) {

return fResult;

} else {

return g(); // tail call

}

}

// 只有当f()的结果为falsey的时候,g()才是尾调用

const a = () => f() && g();

// g()有可能是尾调用,f()不是

// 因为上述写法和下面的写法等效:

const a = () => {

const fResult = f(); // not a tail call

if (fResult) {

return g(); // tail call

} else {

return fResult;

}

}

// 只有当f()的结果为truthy的时候,g()才是尾调用

const a = () => (f() , g());

// g()是尾调用

// 因为上述写法和下面的写法等效:

const a = () => {

f();

return g();

}

2. 尾调用优化

函数在调用的时候会在调用栈(call stack)中存有记录,每一条记录叫做一个调用帧(call frame),每调用一个函数,就向栈中push一条记录,函数执行结束后依次向外弹出,直到清空调用栈,参考下图:

function foo () { console.log(111); }

function bar () { foo(); }

function baz () { bar(); }

baz();

ebe2c381bb2ca219c51ed9e72723b990.png

造成这种结果是因为每个函数在调用另一个函数的时候,并没有 return 该调用,所以JS引擎会认为你还没有执行完,会保留你的调用帧。

baz() 里面调用了 bar() 函数,并没有 return 该调用,所以在调用栈中保持自己的调用帧,同时 bar() 函数的调用帧在调用栈中生成,同理,bar() 函数又调用了 foo() 函数,最后执行到 foo() 函数的时候,没有再调用其他函数,这里没有显示声明 return,所以这里默认 return undefined

foo() 执行完了,销毁调用栈中自己的记录,依次销毁 bar()baz() 的调用帧,最后完成整个流程。

如果对上面的例子做如下修改:

function foo () { console.log(111); }

function bar () { return foo(); }

function baz () { return bar(); }

baz();

这里要注意:尾调用优化只在严格模式下有效。

在非严格模式下,大多数引擎会包含下面两个属性,以便开发者检查调用栈:

  • func.arguments: 表示对 func最近一次调用所包含的参数

  • func.caller: 引用对 func最近一次调用的那个函数

在尾调用优化中,这些属性不再有用,因为相关的信息可能以及被移除了。因此,严格模式(strict mode)禁止这些属性,并且尾调用优化只在严格模式下有效。

如果尾调用优化生效,流程图就会变成这样:

21487ef5f49b69d21786b52e61df0a17.png

我们可以很清楚的看到,尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,只要直接用内层函数的调用记录取代外层函数的调用记录就可以了,调用栈中始终只保持了一条调用帧。

这就叫做尾调用优化,如果所有的函数都是尾调用的话,那么在调用栈中的调用帧始终只有一条,这样会节省很大一部分的内存,这也是尾调用优化的意义

尾递归

1. 定义

先来看一下递归,当一个函数调用自身,就叫做递归。

function foo () {

foo();

}

上面这个操作就叫做递归,但是注意了,这里没有结束条件,是死递归,所以会报栈溢出错误的,写代码时千万注意给递归添加结束条件。

那么什么是尾递归?前面我们知道了尾调用的概念,当一个函数尾调用自身,就叫做尾递归

function foo () {

return foo();

}

2. 作用

那么尾递归相比递归而言,有哪些不同呢?我们通过下面这个求阶乘的例子来看一下:

function factorial (num) {

if (num === 1) return 1;

return num * factorial(num - 1);

}

factorial(5); // 120

factorial(10); // 3628800

factorial(500000); // Uncaught RangeError: Maximum call stack size exceeded

上面是使用递归来计算阶乘的例子,操作系统为JS引擎调用栈分配的内存是有大小限制的,如果计算的数字足够大,超出了内存最大范围,就会出现栈溢出错误。

这里500000并不是临界值,只是我用了一个足够造成栈溢出的数。

如果用尾递归来计算阶乘呢?

'use strict';

function factorial (num, total) {

if (num === 1) return total;

return factorial(num - 1, num * total);

}

factorial(5, 1); // 120

factorial(10, 1); // 3628800

factorial(500000, 1); // 分情况

// 注意,虽然说这里启用了严格模式,但是经测试,在Chrome和Firefox下,还是会报栈溢出错误,并没有进行尾调用优化

// Safari浏览器进行了尾调用优化,factorial(500000, 1)结果为Infinity,因为结果超出了JS可表示的数字范围

// 如果在node v6版本下执行,需要加--harmony_tailcalls参数,node --harmony_tailcalls test.js

// node最新版本已经移除了--harmony_tailcalls功能

通过尾递归,我们把复杂度从O(n)降低到了O(1),如果数据足够大的话,会节省很多的计算时间。由此可见,尾调用优化对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。

避免改写递归函数

尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。要做到这一点,需要把函数内部所有用到的中间变量改写为函数的参数,就像上面的factorial()函数改写一样。

这样做的缺点就是语义不明显,要计算阶乘的函数,为什么还要另外传入一个参数叫total?解决这个问题的办法有两个:

1. ES6参数默认值

'use strict';

function factorial (num, total = 1) {

if (num === 1) return total;

return factorial(num - 1, num * total);

}

factorial(5); // 120

factorial(10); // 3628800

2. 用一个符合语义的函数去调用改写后的尾递归函数

function tailFactorial (num, total) {

if (num === 1) return total;

return tailFactorial(num - 1, num * total);

}

function factorial (num) {

return tailFactorial(num, 1);

}

factorial(5); // 120

factorial(10); // 3628800

上面这种写法其实有点类似于做了一个函数柯里化,但不完全符合柯里化的概念。函数柯里化是指把接受多个参数的函数转换为接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下参数且返回结果的新函数。

概念看着很绕口,我们来个例子感受一下:

// 普通加法函数

function add (x, y, z) {

return x + y + z;

}

add(1, 2, 3); // 6

// 改写为柯里化加法函数

function add (x) {

return function (y) {

return function (z) {

return x + y + z;

}

}

}

add(1)(2)(3); // 6

可以看到,柯里化函数通过闭包找到父作用域里的变量,最后依次相加输出结果。通过这个例子,可能看不出为什么要用柯里化,有什么好处,这个我们以后再谈,这里先引出一个概念。

是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

如果用柯里化改写求阶乘的例子:

// 柯里化函数

function curry (fn) {

var _fnArgLength = fn.length;

function wrap (...args) {

var _args = args;

var _argLength = _args.length;

// 如果传的是所有参数,直接返回fn调用

if (_fnArgLength === _argLength) {

return fn.apply(null, args);

}

function act (...args) {

_args = _args.concat(args);

if (_args.length === _fnArgLength) {

return fn.apply(null, _args);

}

return act;

}

return act;

}

return wrap;

}

// 尾递归函数

function tailFactorial (num, total) {

if (num === 1) return total;

return tailFactorial(num - 1, num * total);

}

// 改写

var factorial = curry(tailFactorial);

factorial(5)(1); // 120

factorial(10)(1); // 3628800

这是符合柯里化概念的写法,在阮一峰老师的文章中是这样写的:

function currying(fn, n) {

return function (m) {

return fn.call(this, m, n);

};

}

function tailFactorial(n, total) {

if (n === 1) return total;

return tailFactorial(n - 1, n * total);

}

const factorial = currying(tailFactorial, 1);

factorial(5) // 120

我个人认为,这种写法其实不是柯里化,因为并没有将多参数的tailFacrotial改写为接受单参数的形式,只是换了一种写法,和下面这样写意义是一样的:

function factorial (num) {

return tailFactorial(num, 1);

}

function tailFactorial (num, total) {

if (num === 1) return total;

return tailFactorial(num - 1, num * total);

}

factorial(5); // 120

factorial(10); // 3628800

结束

这篇文章我们主要讨论了尾调用优化和柯里化。要注意的是,经过测试,Chrome和Firefox并没有对尾调用进行优化,Safari对尾调用进行了优化。Node高版本也已经去除了通过--harmony_tailcalls参数启用尾调用优化。

有任何问题,欢迎大家留言讨论,另附我的博客网站,快来呀~~

欢迎关注我的公众号

639c6ff61146ab2e7dbd546b37620a65.png

参考链接

http://www.ruanyifeng.com/blog/2015/04/tail-call.html https://juejin.im/post/6844903544756125704 https://github.com/lamdu/lamdu/issues/90

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/276543.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

dhl:使用return RedirectToAction()和 return view()

一个Action&#xff1a; Code/// <summary> /// Friend好友的地 /// </summary> /// <returns></returns> public ActionResult FriendFarm(string pid) {BLL.DTOFarm farm new AppleGrange.BLL.DTOFarm(pid); …

radio切换控制div显示_JavaScript连载31图片动态切换以及关闭图片案例

一、图标切换31.1点击那两个按钮可以做到轮番显示图片二、关闭图片案例31.2点击右上角的叉&#xff0c;图片会消失。三、源码&#xff1a;D31_iconSwitch.htmlD31_2_CloseImage.html地址:https://github.com/ruigege66/JavaScript/blob/master/D31_iconSwitch.htmlhttps://gith…

工作一年后,我有些感悟(写于2017年)

时间拉回到2016年5月23日&#xff0c;当天拍毕业照&#xff0c;晚上是大学毕业酒会&#xff0c;那一晚整个酒店都弥漫着伤感的气息。那一晚大家为了找KTV拖延到很晚&#xff0c;最后一群人选择来到了操场&#xff0c;凌晨两点多一群人还在操场上玩着游戏。5月25日离校&#xff…

PHP基础学习之数组使用要点

一、什么是PHP数组&#xff1f;数组 array 是一组有序的变量&#xff0c;其中每个变量都被称为一个元素。每个元素由一个特殊的标识符来区分&#xff0c;这个标识符称之为键&#xff08;也可以称之为下标&#xff09;。数组中的每个元素都包含两项&#xff1a;键和值。可以通过…

高考七年后、工作三年后的感悟

本打算端午假期发表这文章&#xff0c;后来因为文章还需要有些调整&#xff0c;工作日又比较忙&#xff0c;就到今天周三才发。随便写了近3000字&#xff0c;文章最后有免费送书活动&#xff0c;欢迎留言参与。又一年高考结束了。转眼高考过去七年了&#xff0c;工作了三年。很…

蚂蚁金服天街:OceanBase 在大促 5 年来的技术演进

为了与金融从业者、科技从业者共同探讨金融 业务的深层次问题&#xff0c;蚂蚁金服联手 TGO 鲲鹏会&#xff0c;在 12 月 8 日举办了「走进蚂蚁金服&#xff1a;双十一背后的蚂蚁金服技术支持」活动。蚂蚁金服高级技术专家天街为大家分享了《蚂蚁双 11 大促 OceanBase 核心技术…

学习 jQuery 源码整体架构,打造属于自己的 js 类库

虽然现在基本不怎么使用 jQuery了&#xff0c;但 jQuery流行 10多年的 JS库&#xff0c;还是有必要学习它的源码的。也可以学着打造属于自己的 js类库&#xff0c;求职面试时可以增色不少。本文章学习的是 v3.4.1版本。unpkg.com源码地址&#xff1a;https://unpkg.com/jquery3…

5分钟轻松教您如果组建100-500路大型拼接监控系统!

冰山融汇百家号17-07-2700:41大型监控系统如何组网&#xff0c;分布式还是集中式&#xff1f;可靠性与性价比又如何取舍&#xff1f;什么才是最合适的视频监控存储产品&#xff1f;在不同地区、行业的项目中&#xff0c;这些疑问均成为业主、专家、系统集成商等各方面共同关注的…

(转)mssql2005生成表字典

出处不详 CodeSELECT TOP 100 PERCENT --a.id, CASE WHEN a.colorder 1 THEN d.name ELSE END AS 表名, CASE WHEN a.colorder 1 THEN isnull(f.value, ) ELSE END AS 表说明, a.colorder AS 字段序号, a.name AS 字段名, CASE WHEN COLUMNPROPERTY(a.id, a.name, IsIdenti…

表操作

2019独角兽企业重金招聘Python工程师标准>>> 字段修改 alter table TA drop partition (day<2018-12-10); ALTER TABLE TB ADD COLUMNS (userStatus String) CASCADE; ALTER TABLE TC change appversion appCommonVersion String CASCADE; ALTER TABLE TD DROP C…

学习underscore源码整体架构,打造属于自己的函数式编程类库

前言上一篇文章写了 jQuery整体架构&#xff0c;学习 jQuery 源码整体架构&#xff0c;打造属于自己的 js 类库虽然看过挺多 underscore.js分析类的文章&#xff0c;但总感觉少点什么。这也许就是纸上得来终觉浅&#xff0c;绝知此事要躬行吧。于是决定自己写一篇学习 undersco…

拓扑目的 1.Pc9通过van3访问pc10 2.Pc9通过Vlan1\Vlan2访问pc11

1拓扑图2设置路由器R12的接口的IPint g0/0/0ip address 192.168.20.254 24undo shutdown int g0/0/01ip address 192.168.1.1 24undo shutdownint g2/0/00ip address 192.168.3.1 24undo shutdown 3设置路由器R10的接口的IPint g0/0/0ip address 192.168.2.1 24undo shutdownin…

学习 lodash 源码整体架构,打造属于自己的函数式编程类库

前言这是 学习源码整体架构系列第三篇。整体架构这词语好像有点大&#xff0c;姑且就算是源码整体结构吧&#xff0c;主要就是学习是代码整体结构&#xff0c;不深究其他不是主线的具体函数的实现。文章学习的是打包整合后的代码&#xff0c;不是实际仓库中的拆分的代码。上上篇…

推荐一个快速反射调用的类

使用传统的.net反射机制&#xff0c;调用类的方法时&#xff0c;在调用频率大的情况下&#xff0c;会感觉速度很慢。最近浏览卢彦的博客时&#xff0c;找到一个他改进后的反射调用类。试用以后感觉效率明显提高&#xff0c;特推荐给大家。作者重新实现了&#xff0c;反射调用方…

面试官问:JS的继承

原文作者若川&#xff0c;掘金链接&#xff1a;https://juejin.im/post/5c433e216fb9a049c15f841b写于2019年2月20日&#xff0c;现在发到公众号声明原创&#xff0c;之前被《前端大全》公众号等转载阅读量超1w&#xff0c;知乎掘金等累计阅读量超过1w。导读&#xff1a;文章主…

qt 快速按行读取文件_这是知识点之Linux下分割文件并保留文件头

点击上方"开发者的花花世界"&#xff0c;选择"设为星标"技术干货不定时送达&#xff01;这是一个知识点方便快捷的给结构化数据文件分割大小并保留文件的表头&#xff0c;几十个G的结构化文件不仅阅读编辑麻烦&#xff0c;而且使用受限&#xff0c;因此高效…

mono 调用windows webService

1. 实现linux mono Develop中调用windows 中的webService l linux 与 windows 在一个局域网的网段中 l windows 的IIs中发布webService 2. windows 中的设置 l webService 的代码 using System; using System.Collections.Generic; using System.Linq; using S…

学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK

前言这是学习源码整体架构第四篇。整体架构这词语好像有点大&#xff0c;姑且就算是源码整体结构吧&#xff0c;主要就是学习是代码整体结构&#xff0c;不深究其他不是主线的具体函数的实现。文章学习的是打包整合后的代码&#xff0c;不是实际仓库中的拆分的代码。其余三篇分…

绑定dictionary 给定关键字不再字典中_VBA代码集锦-利用字典做两列数据的对比并对齐...

源数据&#xff1a;代码&#xff1a;Sub 对比()Dim arr, brr, crrDim i, j, n, lastrowA, lastrowB As Integer建立字典对象Set d CreateObject("scripting.dictionary")获取数据区域最后一行的行数lastrowA Sheets("对比对齐两列数据").Cells(Rows.Coun…

前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并

前端也可以爬虫&#xff0c;写于2018年08月29日&#xff0c;现在发布到微信公众号申明原创。掘金若川 本文章链接&#xff1a;https://juejin.im/post/5b86732451882542af1c80821、 puppeteer 是什么&#xff1f;puppeteer: Google 官方出品的 headless Chrome node 库puppetee…