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.pswp.cn/news/276543.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/276543.shtml
英文地址,請注明出處:http://en.pswp.cn/news/276543.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

查看/修改Linux時區和時間

轉載鏈接:http://blog.csdn.net/colincjl/article/details/6133036 查看/修改Linux時區和時間 一、時區 1. 查看當前時區 date -R 2. 修改設置時區 方法(1) tzselect 方法(2) 僅限于RedHat Linux 和 CentOS timeconfig 方法(3) 適用于Debian dpkg-reconfigure tzdat…

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); …

【更名通知】將以個人名義繼續更新維護

這是我&#xff08;2013年任職計算機協會會長&#xff09;在2013年申請的公眾號。由于2016年學校陶院更名為陶大&#xff0c;在當時公眾號無法修改名稱。后來計協的的學弟學妹申請了新的公眾號"陶大計算機Association"&#xff0c;大家可以前往關注&#xff0c;所以該…

CentOS7.6 MySQL8環境搭建 配置遠程登錄 字符集UTF8 簡單密碼

一、環境準備 1、清理環境中系統自帶的MySQL &#xff08;1&#xff09;刪除系統自帶的MySQL或Mariadb yum remove mysql-libs &#xff08;2&#xff09;查詢系統中是否還有殘余的依賴包 rpm -qa | grep mariadb &#xff08;3&#xff09;刪除rpm依賴包 rpm -e --nodeps mar…

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…

jQuery 1.9+ 移除$.browser方法

轉載鏈接&#xff1a;http://blog.csdn.net/czplplp_900725/article/details/8704438 jQuery 從 1.9 版開始&#xff0c;移除了 $.browser 和 $.browser.version &#xff0c;取而代之的是 $.support。 在更新的 2.0 版本中&#xff0c;將不再支持 IE 6/7/8。 以后&#xff0c;…

ASP.NET跨頁傳值方法匯總

方法一&#xff1a;問號傳值&#xff08;Response.Redirect方法&#xff09;1&#xff1a;源頁&#xff1a;在按鈕的點擊事件程序中寫入Response.Redirect方法&#xff0c;在其中使用問號傳值。如&#xff1a;Response.Redirect("Default2.aspx?id"txtId.Text.Trim(…

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

時間拉回到2016年5月23日&#xff0c;當天拍畢業照&#xff0c;晚上是大學畢業酒會&#xff0c;那一晚整個酒店都彌漫著傷感的氣息。那一晚大家為了找KTV拖延到很晚&#xff0c;最后一群人選擇來到了操場&#xff0c;凌晨兩點多一群人還在操場上玩著游戲。5月25日離校&#xff…

PHP基礎學習之數組使用要點

一、什么是PHP數組&#xff1f;數組 array 是一組有序的變量&#xff0c;其中每個變量都被稱為一個元素。每個元素由一個特殊的標識符來區分&#xff0c;這個標識符稱之為鍵&#xff08;也可以稱之為下標&#xff09;。數組中的每個元素都包含兩項&#xff1a;鍵和值。可以通過…

python和php可以一起用嗎_Apache同時支持PHP和Python的配置方法

最近開始學著用PythonTornadoMongoDB寫網站&#xff0c;興起寫了一個博客&#xff0c;覺得很有意思所以想掛在服務器上發布出去找大家一起玩。這個時候就遇到了問題。服務器是windows系統&#xff0c;安裝的是Apache&#xff0c;所以需要配置Apache&#xff0c;使Apache同時支持…

CCNA課堂精簡筆記

網絡的三層架構:1.接入層: 提供網絡接入點,相應的設備端口相對密集. 主要設備:交換機,集線器.2.匯聚層: 接入層的匯聚點,能夠提供路由決策.實現安全過濾,流量控制.遠程接入. 主要設備:路由器.3.核心層: 提供更快的傳輸速度, 不會對數據包做任何的操作OSI七層網絡模型: Protocol…

PHP判斷客戶端的瀏覽器類型

轉載鏈接:http://www.php100.com/html/webkaifa/PHP/PHPyingyong/2013/0516/13461.html #判斷瀏覽器語言&#xff1a; if ($_SERVER[HTTP_ACCEPT_LANGUAGE]"zh-cn") {$c_lang"GB";echo 您的系統語言為<b>簡體中文</b>,系統將自動選擇程序語言為…

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

本打算端午假期發表這文章&#xff0c;后來因為文章還需要有些調整&#xff0c;工作日又比較忙&#xff0c;就到今天周三才發。隨便寫了近3000字&#xff0c;文章最后有免費送書活動&#xff0c;歡迎留言參與。又一年高考結束了。轉眼高考過去七年了&#xff0c;工作了三年。很…

螞蟻金服天街:OceanBase 在大促 5 年來的技術演進

為了與金融從業者、科技從業者共同探討金融 業務的深層次問題&#xff0c;螞蟻金服聯手 TGO 鯤鵬會&#xff0c;在 12 月 8 日舉辦了「走進螞蟻金服&#xff1a;雙十一背后的螞蟻金服技術支持」活動。螞蟻金服高級技術專家天街為大家分享了《螞蟻雙 11 大促 OceanBase 核心技術…

禁止訪問Apache目錄

轉載鏈接&#xff1a;http://blog.sina.com.cn/s/blog_505dd27f0100orae.html 在PHP網站開發中&#xff0c;基于WEB服務器和PHP網站程序代碼的安全考慮&#xff0c;我們需要對相關的目錄或者文件訪問權限進行控制&#xff0c;以防止意外情況的發生&#xff0c;那么我們如何來實…

類與結構

目錄 類與結構的實例比較類與結構的差別如何選擇結構還是類類與結構的示例比較 結構示例 public struct Person{string Name;int height;int weightpublic bool overWeight(){//implement something}}類示例 public class TestTime{int hours;int minutes;int seconds;public…

學習 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;這些疑問均成為業主、專家、系統集成商等各方面共同關注的…

python中beautifulsoup_面向新手解析python Beautiful Soup基本用法

Beautiful Soup就是Python的一個HTML或XML的解析庫&#xff0c;可以用它來方便地從網頁中提取數據。它有如下三個特點&#xff1a;Beautiful Soup提供一些簡單的、Python式的函數來處理導航、搜索、修改分析樹等功能。它是一個工具箱&#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…