JavaScript 中的递归、回调与闭包
- 递归函数是在一个函数内通过名字调用自身的情况下构成的。
- 回调函数:函数B作为参数传递到另一个函数A中,并且在函数A执行了函数B。我们就说函数B叫做回调函数。
- 闭包在本质上,就是将函数内部和函数外部连接起来的一座桥梁,通过这座桥我们就可以在外部访问到函数内部的变量。
递归
-
递归函数是在一个函数通过名字调用自身的情况下构成的。
-
递归的意义:递归就是调用自身的一种编程技巧,在程序设计中应用广泛。递归函数就是函数对自身的调用,是循环运算的一种算法模式。
-
递归要有跳出条件,就是什么时候我们会结束函数的调用。没有退出条件的递归是一个死循环,调用过多会超出最大调用堆栈大小,这时就会报错。
-
下面是一个简单的递归函数:
function fn(num){ if(num === 1){ return 1; } return num+fn(--num); } console.log(fn(5));// 15
- 这个函数的功能是求出 1到传入参数num的累加和,函数调用关系如下:
理解参数
-
ECMAScript 函数不介意传递进来多少个参数,也不在乎传进来参数是什么数据类型。
-
也就是说,即便你定义的函数只接收两个参数,在调用这个函数时也未必一定要传递两个参数。可以传递一个、三个甚至不传递参数,而解析器永远不会有什么怨言。
-
这是因为我们在调用函数时传入进来的参数是被放在一个伪数组中的,函数接收到的始终都是这个数组,不关心数组中元素的甚至数组中有没有参数。
- 这个接受参数的数组叫做 arguments ,每一个函数都有这么一个属性。(function 函数也是一个对象)
-
我们完整的说明一个函数的声明和调用:
- 声明一个函数:
- 可以定义形参,也可以不定义;
- 不定义就得用 数组的方式得到参数:
arguments[index]
; //index是索引值,表示数组的下标。
- 调用函数
- 函数会根据我们传递 实参的个数 ,定义好 arguments 数组的长度。就是不传递实参 数组的长度就是0;传递一个实参,数组的传递就是1。
- 如果我们声明函数时 定义了 3个形参,但是传递 实参 的时候只传递了 1个实参,那么后面两个形参就是 声明但未赋值(undefined);
- 举例验证
function fn(a, b, c) {/* 声明三个形参 */ console.log(arguments.length, arguments);/* 查看数组的长度,以及整个伪数组 */ console.log(a===arguments[0]);/* 判断第一个形参有没有接受到传递的值,并且这个值是否等于数组的第一个元素 */ console.log(a===arguments[1]);/* 判断a是否等于数组的第二个元素 */ console.log(b,arguments[1],c)/* 打印第二个参数,数组的第二个元素,以及形参c的值 */ }; var aa = "10"; fn(aa,20);/* 调用时传递2个实参 */
- 结果如下:
- 声明一个函数:
回调函数
-
调用事先声明好的函数A,函数A中的某个参数是函数 (后续把参数函数叫做函数B)
-
然后在函数A的函数体中调用了函数B,那么函数B就叫做函数A的回调函数。
-
书写一个简单的回调函数:
//1.创建一个简单的函数fn function fn(callback) { /* 1-1.声明一个形参 callback ,这个形参可以看当作是一个函数的名字*/ console.log("函数本体", "我们将打印callback的值—————", callback); callback(); /* 1-2.callback 是函数名,这一步就是调用callback函数 */ } // fn(function(){ // console.log("参数函数") // }) // 2.调用函数fn fn(cb) /* 2-1.传入一个实参cb,cd是我们定义的函数名 */ // 3.创建一个叫做 cb的函数 function cb() { console.log("参数函数") }
- 1、2、3这首歌步骤不区分先后顺序,这里只是为了观看体验所以这么书写。
-
还可以这么写:
fn(function(){console.log("参数函数")});/* 调用函数fn ,fn传入了一个匿名函数 */ function fn(callback) { //声明一个形参 callback,传入参数时会把实参赋值给形参 /*var callback=function(){console.log("参数函数")}*/ callback(); }
闭包
-
Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。就是作用域链可以往前但不可以往后访问。
function parent() { var strP="strP是一个在parent函数作用域中的变量"; son(); function son() { var strS="strS是一个在son函数作用域中的变量"; // 在son作用域中 可以访问到parent函数的中的变量 console.log(strP);/* 作用域链向前延伸,先访问本身作用域,没有找到变量就向上级查询变量,直到找到最近的一个 最近的一个 最近的一个就停止;如果到window全局中都没有找到,就报错:strP is not defined */ }; // 在parent作用域下 无法访问到son函数的变量 // console.log(strS);/* 报错:strS is not defined */ } var strP="strP是一个在window全局作用域中的变量"; parent();
-
闭包是指有权访问另一个函数作用域中的变量的函数。
-
简单的说,闭包就是能够读取其他函数内部变量的函数,闭包是为了保护私有变量不受外部污染。
-
闭包例子:
function parent() { var strP="strP是一个在parent函数作用域中的变量"; var i=0; return son(); function son() { strP="第"+ ++i +"次取到strP变量的值" return strP; }; } var result=parent(); console.log(result); console.log(result); console.log(result);
- 有的小伙伴可能会以为返回的值是:
第1次取到strP变量的值
、第2次取到strP变量的值
、第3次取到strP变量的值
。 - 但其实结果是返回打印三次
第1次取到strP变量的值
。 - 分析一波:
- 调用函数 parent,并把返回结果 son() 赋值给变量 result;
- son() 表示调用函数son,son函数运行过后返回的结果是一个值,i=0+1。
- 所以parent函数调用后返回的结果就是 son函数调用后返回的值,这个值是一个字符串
- 就相当于把嵌套函数运行的结果 直接赋值给result,就是result 得到的就是一个值,不是一个函数。
- 打印了三次这个结果,所以就是三次
第1次取到strP变量的值
。
- 调用函数 parent,并把返回结果 son() 赋值给变量 result;
- 有的小伙伴可能会以为返回的值是:
-
但我们按照闭包的写法来:
- 首先我们知道闭包是一个函数,所以我们在函数中要返回被嵌套的函数;
- 并且这个嵌套函数使用了 函数(嵌套函数的上一级函数)中的变量。
function parent() { var strP="strP是一个在parent函数作用域中的变量"; var i=0; return son; function son() { strP="第"+ ++i +"次取到strP变量的值" return strP; }; } var result=parent(); console.log(result()); console.log(result()); console.log(result());
- 结果如图:
- 可以分析出 result接收到的是一个函数体,我们打印的是函数体运行后的结果
- 这样我们就可以在函数的外部访问到函数 parent 内的变量 strP 和 i。(转下)
闭包过程分析
- 为什么能访问得到呢?(接上)
- 我们知道函数体内的嵌套函数的作用域链的最前端是 window 全局环境。
- 嵌套函数可以访问到它的上级函数的变量。(son的作用域可以延伸到parent)
- 所以我们把嵌套函数 son 返回出来,借助 son函数就可以访问到它的上级函数的变量。
- 所以说闭包就好比架一座连接桥。