JavaScript的值传递和引用传递
译者按: 程序员应该知道递归,但是你真的知道是怎么回事么?
为了保证可读性,本文采用意译而非直译。
递归简介
一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
我们来举个例子,我们可以用4的阶乘乘以4来定义5的阶乘,3的阶乘乘以4来定义4的阶乘,以此类推。
factorial(5) = factorial(4) * 5
factorial(5) = factorial(3) * 4 * 5
factorial(5) = factorial(2) * 3 * 4 * 5
factorial(5) = factorial(1) * 2 * 3 * 4 * 5
factorial(5) = factorial(0) * 1 * 2 * 3 * 4 * 5
factorial(5) = 1 * 1 * 2 * 3 * 4 * 5
用Haskell的Pattern matching 可以很直观的定义factorial函数:
factorial n = factorial (n-1) * n
factorial 0 = 1
在递归的例子中,从第一个调用factorial(5)
开始,一直递归调用factorial
函数自身直到参数的值为0。下面是一个形象的图例:
递归的调用栈
为了理解调用栈,我们回到factorial
函数的例子。
function factorial(n) {
if (n === 0) {
return 1
}
return n * factorial(n - 1)
}
如果我们传入参数3,将会递归调用factorial(2)
、factorial(1)
和factorial(0)
,因此会额外再调用factorial
三次。
每次函数调用都会压入调用栈,整个调用栈如下:
factorial(0) // 0的阶乘为1
factorial(1) // 该调用依赖factorial(0)
factorial(2) // 该调用依赖factorial(1)
factorial(3) // 该掉用依赖factorial(2)
现在我们修改代码,插入console.trace()
来查看每一次当前的调用栈的状态:
function factorial(n) {
console.trace()
if (n === 0) {
return 1
}
return n * factorial(n - 1)
}
factorial(3)
接下来我们看看调用栈是怎样的。
第一个:
Trace
at factorial (repl:2:9)
at repl:1:1 // 请忽略以下底层实现细节代码
at realRunInThisContextScript (vm.js:22:35)
at sigintHandlersWrap (vm.js:98:12)
at ContextifyScript.Script.runInThisContext (vm.js:24:12)
at REPLServer.defaultEval (repl.js:313:29)
at bound (domain.js:280:14)
at REPLServer.runBound [as eval] (domain.js:293:12)
at REPLServer.onLine (repl.js:513:10)
at emitOne (events.js:101:20)
你会发现,该调用栈包含一个对factorial
函数的调用,这里是factorial(3)
。接下来就更加有趣了,我们来看第二次打印出来的调用栈:
Trace
at factorial (repl:2:9)
at factorial (repl:7:12)
at repl:1:1 // 请忽略以下底层实现细节代码
at realRunInThisContextScript (vm.js:22:35)
at sigintHandlersWrap (vm.js:98:12)
at ContextifyScript.Script.runInThisContext (vm.js:24:12)
at REPLServer.defaultEval (repl.js:313:29)
at bound (domain.js:280:14)
at REPLServer.runBound [as eval] (domain.js:293:12)
at REPLServer.onLine (repl.js:513:10)
现在我们有两个对factorial
函数的调用。
第三次:
Trace
at factorial (repl:2:9)
at factorial (repl:7:12)
at factorial (repl:7:12)
at repl:1:1
at realRunInThisContextScript (vm.js:22:35)
at sigintHandlersWrap (vm.js:98:12)
at ContextifyScript.Script.runInThisContext (vm.js:24:12)
at REPLServer.defaultEval (repl.js:313:29)
at bound (domain.js:280:14)
at REPLServer.runBound [as eval] (domain.js:293:12)
第四次:
Trace
at factorial (repl:2:9)
at factorial (repl:7:12)
at factorial (repl:7:12)
at factorial (repl:7:12)
at repl:1:1
at realRunInThisContextScript (vm.js:22:35)
at sigintHandlersWrap (vm.js:98:12)
at ContextifyScript.Script.runInThisContext (vm.js:24:12)
at REPLServer.defaultEval (repl.js:313:29)
at bound (domain.js:280:14)
设想,如果传入的参数值特别大,那么这个调用栈将会非常之大,最终可能超出调用栈的缓存大小而崩溃导致程序执行失败。那么如何解决这个问题呢?使用尾递归。
尾递归
尾递归是一种递归的写法,可以避免不断的将函数压栈最终导致堆栈溢出。通过设置一个累加参数,并且每一次都将当前的值累加上去,然后递归调用。
我们来看如何改写之前定义factorial
函数为尾递归:
function factorial(n, total = 1) {
if (n === 0) {
return total
}
return factorial(n - 1, n * total)
}
factorial(3)
的执行步骤如下:
factorial(3, 1)
factorial(2, 3)
factorial(1, 6)
factorial(0, 6)
调用栈不再需要多次对factorial
进行压栈处理,因为每一个递归调用都不在依赖于上一个递归调用的值。因此,空间的复杂度为o(1)而不是0(n)。
接下来,通过console.trace()
函数将调用栈打印出来。
function factorial(n, total = 1) {
console.trace()
if (n === 0) {
return total
}
return factorial(n - 1, n * total)
}
factorial(3)
很惊讶的发现,依然有很多压栈!
// ...
// 下面是最后两次对factorial的调用
Trace
at factorial (repl:2:9) // 3次压栈
at factorial (repl:7:8)
at factorial (repl:7:8)
at repl:1:1 // 请忽略以下底层实现细节代码
at realRunInThisContextScript (vm.js:22:35)
at sigintHandlersWrap (vm.js:98:12)
at ContextifyScript.Script.runInThisContext (vm.js:24:12)
at REPLServer.defaultEval (repl.js:313:29)
at bound (domain.js:280:14)
at REPLServer.runBound [as eval] (domain.js:293:12)
Trace
at factorial (repl:2:9) // 最后第一调用再次压栈
at factorial (repl:7:8)
at factorial (repl:7:8)
at factorial (repl:7:8)
at repl:1:1 // 请忽略以下底层实现细节代码
at realRunInThisContextScript (vm.js:22:35)
at sigintHandlersWrap (vm.js:98:12)
at ContextifyScript.Script.runInThisContext (vm.js:24:12)
at REPLServer.defaultEval (repl.js:313:29)
at bound (domain.js:280:14)
这是为什么呢?
在Nodejs下面,我们可以通过开启strict mode
, 并且使用--harmony_tailcalls
来开启尾递归(proper tail call)。
'use strict'
function factorial(n, total = 1) {
console.trace()
if (n === 0) {
return total
}
return factorial(n - 1, n * total)
}
factorial(3)
使用如下命令:
node --harmony_tailcalls factorial.js
调用栈信息如下:
Trace
at factorial (/Users/stefanzan/factorial.js:3:13)
at Object.<anonymous> (/Users/stefanzan/factorial.js:9:1)
at Module._compile (module.js:570:32)
at Object.Module._extensions..js (module.js:579:10)
at Module.load (module.js:487:32)
at tryModuleLoad (module.js:446:12)
at Function.Module._load (module.js:438:3)
at Module.runMain (module.js:604:10)
at run (bootstrap_node.js:394:7)
at startup (bootstrap_node.js:149:9)
Trace
at factorial (/Users/stefanzan/factorial.js:3:13)
at Object.<anonymous> (/Users/stefanzan/factorial.js:9:1)
at Module._compile (module.js:570:32)
at Object.Module._extensions..js (module.js:579:10)
at Module.load (module.js:487:32)
at tryModuleLoad (module.js:446:12)
at Function.Module._load (module.js:438:3)
at Module.runMain (module.js:604:10)
at run (bootstrap_node.js:394:7)
at startup (bootstrap_node.js:149:9)
Trace
at factorial (/Users/stefanzan/factorial.js:3:13)
at Object.<anonymous> (/Users/stefanzan/factorial.js:9:1)
at Module._compile (module.js:570:32)
at Object.Module._extensions..js (module.js:579:10)
at Module.load (module.js:487:32)
at tryModuleLoad (module.js:446:12)
at Function.Module._load (module.js:438:3)
at Module.runMain (module.js:604:10)
at run (bootstrap_node.js:394:7)
at startup (bootstrap_node.js:149:9)
Trace
at factorial (/Users/stefanzan/factorial.js:3:13)
at Object.<anonymous> (/Users/stefanzan/factorial.js:9:1)
at Module._compile (module.js:570:32)
at Object.Module._extensions..js (module.js:579:10)
at Module.load (module.js:487:32)
at tryModuleLoad (module.js:446:12)
at Function.Module._load (module.js:438:3)
at Module.runMain (module.js:604:10)
at run (bootstrap_node.js:394:7)
at startup (bootstrap_node.js:149:9)
你会发现,不会在每次调用的时候压栈,只有一个factorial
。
注意:尾递归不一定会将你的代码执行速度提高;相反,可能会变慢。不过,尾递归可以让你使用更少的内存,使你的递归函数更加安全 (前提是你要开启harmony模式)。
那么,博主这里就疑问了:为什么尾递归一定要开启harmony
模式才可以呢?
关于Fundebug
Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了7亿+错误事件,得到了Google、360、金山软件、百姓网等众多知名用户的认可。欢迎免费试用!
版权声明
转载时请注明作者Fundebug以及本文地址:
https://blog.fundebug.com/2017/06/14/all-about-recursions/
JavaScript的值传递和引用传递的更多相关文章
- JavaScript 函数参数传递到底是值传递还是引用传递
tips:这篇文章是听了四脚猫的js课程后查的,深入的理解可以参看两篇博客: JavaScript数据类型--值类型和引用类型 JavaScript数据操作--原始值和引用值的操作本质 在传统的观念里 ...
- JavaScript传递变量:值传递?引用传递?
今天在看 seajs-2.2.1/src/util-events.js源码,里面有段代码不是很理解: var events = data.events = {} // Bind event seajs ...
- Java中引用类型变量,对象,值类型,值传递,引用传递 区别与定义
一.Java中什么叫做引用类型变量?引用:就是按内存地址查询 比如:String s = new String();这个其实是在栈内存里分配一块内存空间为s,在堆内存里new了一个Stri ...
- java中值传递和引用传递
最近工作中使用到了值传递和引用传递,但是有点懵,现在看了下面的文章后清晰多了.一下是文章(网摘) 1:按值传递是什么 指的是在方法调用时,传递的参数是按值的拷贝传递.示例如下: public clas ...
- Java中的值传递和引用传递
这几天一直再纠结这个问题,今天看了这篇文章有点思路了,这跟C++里函数参数为引用.指针还是有很大区别. 当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里 ...
- java的值传递和引用传递
昨天博主在对于值传递和引用传递这里栽了一个大坑啊,导致一下午时间都浪费在这里,我们先说下值传递和引用传递java官方解释: 值传递:(形式参数类型是基本数据类型):方法调用时,实际参数把它的值传递给对 ...
- PHP值传递和引用传递的区别
PHP值传递和引用传递的区别.什么时候传值什么时候传引用 (1)按值传递:函数范围内对值的任何改变在函数外部都会被忽略 (2)按引用传递:函数范围内对值的任何改变在函数外部也能反映出这些修改 (3)优 ...
- 【转载】C++ 值传递、指针传递、引用传递详解
原文链接:http://www.cnblogs.com/yanlingyin/ 值传递: 形参是实参的拷贝,改变形参的值并不会影响外部实参的值.从被调用函数的角度来说,值传递是单向的(实参->形 ...
- java中方法的参数传递机制(值传递还是引用传递)
看到一个java面试题: 问:当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递? 答:是值传递.Java 编程语言只有值传递参 ...
- java 对象传递 是 值传递 还是 引用传递?
这个问题说实话我感觉没有太大的意义. 按第一印象和c++的一些思想去理解的话对象传递是引用传递,因为传递过去的对象的值能被改变. 但是又有很多人,不知道从哪里扣出来一句,java中只有值传递,没有引用 ...
随机推荐
- 【转】UniGUI Session管理說明
[转]UniGUI Session管理說明 (2015-12-29 15:41:15) 转载▼ 分类: uniGUI 台中cmj朋友在uniGUI中文社区QQ群里发布的,转贴至此. UniGUI ...
- 【接口时序】4、SPI总线的原理与Verilog实现
一. 软件平台与硬件平台 软件平台: 1.操作系统:Windows-8.1 2.开发套件:ISE14.7 3.仿真工具:ModelSim-10.4-SE 硬件平台: 1. FPGA型号:Xilinx公 ...
- Android开发 - 掌握ConstraintLayout(五)偏差(Bias)
比如实现这样一个场景: "在屏幕宽度的1/4的地方放置一个View" 使用传统布局时,实现按照屏幕的宽度(高度),或者相对两个View之间距离的一个比例来进行布局,就显得非常麻烦, ...
- 读取.Properties文件以及Spring注解读取文件内容
public class Main { public static void main(String[] args) throws IOException { //创建Properties对象 Pro ...
- Maven - Tips
1- Maven的Settings http://maven.apache.org/settings.html 2- Maven设置代理 示例: <proxies> <proxy&g ...
- 机器学习技法笔记:15 Matrix Factorization
Roadmap Linear Network Hypothesis Basic Matrix Factorization Stochastic Gradient Descent Summary of ...
- Rolling Hash about the Rsync
今天看文献看到一个有趣的算法—Rolling Hash,这个算法可以更新在不同的machine上的两个“similar”的文件,也叫做rsync algorithm,rsync顾名思义:remote ...
- 21-json pickle shelve XML
我们把对象从内存中变成可存储或传输的过程称之为序列化 在python中叫picking 在其他语言中也被称之为 serialization marshalling flattening等等 序列化之后 ...
- 课程四(Convolutional Neural Networks),第四 周(Special applications: Face recognition & Neural style transfer) —— 3.Programming assignments:Face Recognition for the Happy House
Face Recognition for the Happy House Welcome to the first assignment of week 4! Here you will build ...
- mysql修改数据路径
步凑: 1.关闭mysql服务 [root@localhost /]# service mysqld stop 2.移动mysql文件夹到自定义的目录 [root@localhost /]# mv / ...