浅谈javascript中的递归和闭包

来源:http://www.chinese-glasses.com 作者:Web前端 人气:185 发布时间:2020-03-31
摘要:时间: 2019-11-01阅读: 106标签: 递归 函数表达式的特征 使用函数实现递归 使用闭包定义私有变量 递归和闭包作为js中很重要的一环,几乎在前端的面试中都会涉及,特别闭包。今天前端组

时间: 2019-11-01阅读: 106标签: 递归

  • 函数表达式的特征
  • 使用函数实现递归
  • 使用闭包定义私有变量

递归和闭包作为js中很重要的一环,几乎在前端的面试中都会涉及,特别闭包。今天前端组的组长冷不丁的问了我一下,粗略的回答了一下,感觉不太满足,于是重新学习了一下,写下本篇。

前面我们说到定义函数有两种方式:函数声明、函数表达式。

在说这个两个概念之前,我们先回顾一下函数表达式

两者的区别在于函数声明提升,前者在执行之前的上下文环境中直接被赋值,而后者不会。

function实际上是一种引用对象,和其他引用类型一样,都有属性和方法。定义函数有函数声明、函数表达式、以及构造函数

一、递归

这里说一下前两种。

递归函数是一个函数通过名字调用自身的情况下构成的。

函数声明

     function factorial(num){

函数声明语法如下

         if(num<1){

function functionName(arg1,arg2,arg3){ //函数体}

             return 1;

其中有一个很重要的特征:函数声明提升

         }else{

saber();function saber() { console.log("excalibar!")//excalibar!}

             return num * arguments.callee(num-1);

很奇怪吧,没有报错!这是因为在执行这个函数之前,会读取函数声明。

         }

函数表达式

     }

函数表达式语法如下:

     alert(factorial(10));  

这种情况下的函数也叫作匿名函数。

二、闭包

var functionName = function(arg1,arg2,arg3) { //函数体}

闭包的核心概念就是指有权访问另一个函数作用域中变量的函数。

但是有一点,同比上面的函数声明,在使用函数表达式之前得先赋值,不然会报错

创建闭包的常见方式主要就是在一个函数内部创建另一个函数。

saber();var saber = function() { console.log("excalibar!")//saber is not a function}

     function createCompareFunction(proprtyName){

因此,在使用函数表达式之前,必须var saber()

         return function(obj1,obj2){

递归

             if(obj1[proprtyName]>obj2[proprtyName]){

好了,哆嗦了一大堆,开始说主题

                 return 1;    

定义:一个函数通过名字调用自身

             }else if(obj1[proprtyName]<obj2[proprtyName]){

很简单,撸个代码看看:

                 return -1;

//阶乘递归function saber(num) { if(num = 1){ return 1; }else{ return num*saber(num-1)//saber可以换成arguments.callee,这种方式能确保函数怎么样都不会出现问题 }}saber(3)console.log(saber(3))//6

             }else{

这是一个阶乘递归,但是通过函数名调用自身,可能存在一些弊端,假如,我在外部定义了saber为空的话,就会报错,因此使用arguments.callee,能确保函数不会出错。但是也有缺点,在严格模式下arguments.callee无法被访问到。

                 return 0;

因此我们可以使用命名函数表达式来解决这个问题

             }

var saber = (function f(num) { if(num = 1){ return 1; }else{ return num * f(num-1) }})saber(3)console.log(saber(3))//6

         }

这样,即便把函数赋值给另一个变量,f()依然有效,而且不受严格模式影响

     }

闭包

     var o1 = {name : 'zjh'};

js很恶心难懂的地方来了

     var o2 = {name : 'azz'};

要理解闭包,最重要的一点就是搞清楚作用域链,这玩意对理解闭包有很大的作用,作用域链可以看我之前的一篇博客

     var f = createCompareFunction('name');

定义:指有权访问另一个函数作用域中的变量的函数

     alert(f(o2,o1)); //-1  

创建闭包:在A函数内部创建另一个函数B

内部匿名函数中调用的proprtyName就是另一个函数作用域中的变量。当f被赋值后,外层函数的作用域链被销毁,但是他的活动对象仍然在内存中,因为它的活动对象被返回的匿名函数引用了。

function createSavant(propertyName) { return function(object1,object2) { var value1 = object1[propertyName]; console.log(value1)//saber var value2 = object2[propertyName]; if (value1  value2){ return -1; }else if(value1 value2){ return 1; }else{ return 0; } };}var savant = createSavant("name")var result = savant({name:"saber"},{name:"archer"});

所以闭包会占用很多内存,要在必要的时候使用。

savant=null;//解除对匿名函数的引用(以便释放内存)

2.1闭包与变量

console.log(result)//0因为字符串不能比较大小,所以返回0,如果设为数字的话,比如var result = savant({name:“1”},{name: “2”}),会返回-1

作用域链的这种机制也会有一定的副作用:

上面的代码就是一串闭包,我们在函数creatSavant里面创建了一个返回函数。我这里用简单的大白话解释一下:

     function createFunction(){

首先,在函数creatSavant里创建的函数会包含外部函数的作用域链,也就是说return function()这玩意的作用域链中会包含外部creatSavant的活动对象

         var array = new Array();

因此,return function()能够访问外部createSavant里面定义的所有变量,比如上面例子中的value1的值就是访问外部定义的变量得来的

         for(var i = 0 ; i < 10 ; i++){

然后,当函数creatSavant执行完了之后,由于return function()这家伙的作用域链还在引用外部creatSavant的活动对象,因此即使creatSavant的执行环境的作用域链被销毁了,creatSavant的对象还是会保存在内存中,供内部函数return function()来引用

             array[i] = function(){

最后,直到匿名函数结束了罪恶的一生,被销毁了。外部环境creatSavant的活动对象才会被销毁。

                 return i;

可能说的话比较直白,有些地方不专业,大家可以指出错误,我会虚心学习的。

             }

下面来说说闭包的优缺点把:

         }

闭包的缺点

          i = 20;

(1)占用过多内存

         return array;

首当其冲的,由于闭包会携带包含它的函数的作用域,因此会比其他正常的函数占用更多的内存。mmp,比如相同体型的人,我比别人多一个啤酒肚,重量不重才怪。所以慎重使用闭包。

     }

(2)闭包只能取到包含任何变量的最后一个值(重要)

     var a = createFunction();

这个缺点在很多笔试题面试题中都会出,举个例子:

     alert(a[6]())//20  

function creatFunction() { var result = new Array(); for(var i = 0; i  10; i++){//var的变量提升机制,导致了最后i只有10这一次 result[i] = function() { return i; }; } return result;}var saber = creatFunction();for (var i = 0; i  saber.length; i++) { console.log(saber[i]())//10 10 10 10 10 10 10 10 10 10}

应该是6,这是却是20。原因就是数组中的每一项都是function(){return i},而此时的i的值是createFunction执行完的20。

上面的代码看上去,循环的每个函数都应该返回自己的索引值,即0 1 2 3 4 5 6 7 8 9。但实际上确返回了十个10。原因如下:

     function createFunction(){

每个函数的作用域链中都保存了creatFunction()函数的活动对象,所以,其实他们都引用了同一个变量 i,结果当creatFuction()返回后,i的值为10,10被保存了下来,于是每个函数都引用着这个值为10的变量i,结果就如上面代码所示了。

         var array = new Array();

那么如何解决这个问题呢:

         for(var i = 0 ; i < 10 ; i++){

方法一、创建另一个匿名函数,强制达到预期效果:

             array[i] = function(num){

function creatFunction() { var result = new Array(); for(var i = 0; i  10; i++){ result[i] = function(num) { return function() { return num; }; }(i);//会生成很多作用域 } return result;}var saber = creatFunction();for (var i = 0; i  saber.length; i++) { console.log(saber[i]())//0 1 2 3 4 5 6 7 8 9}

                 return num;

如上面添加的代码,这里没有将闭包直接赋值给数组,而是定义了一个匿名函数,并将匿名函数的结果传给数组,在调用匿名函数的时候传入了i,由于函数是按值传递的,循环的每一个i的当前值都会复制给参数num,然后在匿名函数function(num)的内部,又创建并返回了一个访问num的闭包

             }(i)

。最终,result数组中的每一个函数都有一个对应的num的副本,就可以返回各自不同的值了。。。。

         }

这种说法好像不好理解,说直白一点,就是把每个i的值都赋给num,然后把所有的num的值放到数组中返回。避免了闭包只取到i的最后一个值得情况的发生。

          i = 20;

方法二、使用let

         return array;

因为es5没有块级作用域这一说法,在es6中加入了let来定义变量,使得函数拥有了块级作用域

     }

function creatFunction() { var result = new Array(); for(let i = 0; i  10; i++){//let不存在变量提升,每一次循环都会执行一次,for的每一次循环都是不同的块级作用域,let声明的变量都有块级作用域,所以不存在重复声明 result[i] = function() { return i; }; } return result;}var saber = creatFunction();for (var i = 0; i  saber.length; i++) { console.log(saber[i]())//1 2 3 4 5 6 7 8 9}

     var a = createFunction();

(3)闭包导致this对象通常指向windows

     alert(a[6])//20  

var name = "I am windows"var object = { name: "saber", getName : function() { return function() { return this.name } }}console.log(object.getName()())//I am windows

利用了函数参数按值传递的特性。

this是基于函数的执行环境绑定的,而匿名函数的执行环境具有全局性,因此this对象指向windows

2.2关于this对象

解决办法,把外部作用域中的this对象保存在一个闭包也能访问到的变量里:

this对象时在运行时基于函数执行环境绑定的。在全局函数中,this等于window,而当函数作为某个对象的方法调用时,this等该对象。

var name = "I am windows"var object = { name: "saber", getName : function() { var that = this return function() { return that.name } }}console.log(object.getName()())//saber

匿名函数执行环境具有全局性。因此this通常指向window。

(4)内存泄漏

     var k = 'window';

由于匿名函数的存在,导致外部环境的对象会被保存,因此所占用的内存不会被垃圾回收机制回收。

     var o = {

function Savant(){ this.age = "18"; this.name = ["saber","archer"];}Savant.prototype.sayName = function(){ var outer = this; return function(){ return outer.name };};var count = new Savant();console.log(count.sayName()())//[ 'saber', 'archer' ]

         k : 'object',

我们可以保存变量到一个副本中,然后引用该副本,最后设置为空来释放内存

         getName : function(){

function Savant(){ this.age = "18"; this.name = ["saber","archer"];}Savant.prototype.sayName = function(){ var outerName = this.name; return function(){ return outerName }; outerName = null;};var count = new Savant();console.log(count.sayName()())//[ 'saber', 'archer' ]

             return function(){

注意一点:即使这样还是不能解决内存泄漏的问题,但是我们能解除其引用,确保正常回收其占用的内存

                 return this.k;

说完了缺点,我们来说一下,闭包的优点把

             }

闭包的优点

         }

(1)模仿块级作用域

     }

语法:

     alert(o.getName()())  

(function(){ //在这里是块级作用域})();

2.3内存泄漏

举个例子来说明吧:

在IE浏览器中,因为dom对象和js对象存在浏览器的不同地方,如果闭包的作用域链中保存着一个HTML元素,那么该元素就无法被销毁。

function saber(num){ for(var i = 0; i  num; i++){ console.log(i)//0 1 2 3 4 } // console.log(i)//5}saber(5)

function createF(){

可以看到在for循环外还是能访问到i的,那么,如何装for循环外无法访问到里面的i呢

    var ele = document.getElementById('aa');

function saber(num){ (function () { for(var i = 0; i  num; i++){ // console.log(i)//0 1 2 3 4 } })(); console.log(i)//i is not defined}saber(5)

    ele.onclick = function(){

这种方式能减少闭包占用的内存问题。

        alert(ele.value);

(2)在构造函数中定义特权方法

    }

function savant(name){ var name=name; this.sayName=function(){ console.log(name); }};var savant1=new savant("saber");var savant2=new savant("archer");savant1.sayName(); //sabersavant2.sayName(); //archer

}  

该例子中的sayName()就是一个特权方法,可以理解为可以用来访问私有变量的公有方法。

避免泄漏:

(3)静态私有变量

function createF(){

在私有作用域中同样可以使用特权方法

    var ele = document.getElementById('aa');

(function (){ var name = ""; Savant = function(value) { name = value; } Savant.prototype.getName = function() { return name; } Savant.prototype.setName = function(value) { name = value; }})();var Savant1 = new Savant("saber")console.log(Savant1.getName())//saberSavant1.setName("archer");console.log(Savant1.getName())//archervar Savant2 = new Savant("lancer")console.log(Savant1.getName())//lancerconsole.log(Savant2.getName())//lancer

    var k = ele.value;

但是在私有作用域里面的特权方法和构造函数中不同的是,私有作用域中的特权方法是在原型上定义的,因此所有的实例都使用同一个函数,只要新建一个Savant实例或者调用setName()就会在原型上赋予name一个新值。导致的结果就是所有的实例都会返回相同的值

    ele.onclick = function(){

(4)模块模式

        alert(k);

单例模式添加私有属性和私有方法,减少全局变量的使用

    }

语法:

    ele = null;

var singleleton = (function(){ // 创建私有变量 var privateNum = 10; // 创建私有函数 function privateFunc(){ // 业务逻辑代码 } // 返回一个对象包含公有方法和属性 return { publicProperty: true, publicMethod: function() { //共有方法代码 } };})();

}  

该模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是很有用的

三、模仿块级作用域

增强的模块模式

JS中没有块级作用域,如果要使用需要在函数中定义来模仿。

function Savant() { this.name = "saber";};var application = (function(){ // 定义私有 var privateA = "privateA"; // 定义私有函数 function privateMethodA(){}; // 实例化一个对象后,返回该实例,然后为该实例增加一些公有属性和方法 var object = new Savant(); // 添加公有属性 object.publicA = "publicA"; // 添加公有方法 object.publicB = function(){ return privateA; } // 返回该对象 return object;})();Savant.prototype.getName = function(){ return this.name;}console.log(application.publicA);// publicAconsole.log(application.publicB()); // privateAconsole.log(application.name); // saberconsole.log(application.getName());// saber

(function(){

作者:SaberInoryKiss出处:SaberInoryKiss的博客--

    for(var i =0 ; i<10 ; i++){

        var k = 10;

    }

})()

alert(i) //错误  

因为i在块级作用域执行完后 就销毁了。

只要临时需要一些变量,可以使用私有作用域。这种技术经常用在全局作用域中的外部函数,可以限制向全局作用域中添加过多的变量和函数。

也能有效的减少闭包占用内存的问题。因为没有指向匿名函数的引用,只要函数执行完毕,就可以立即销毁其作用域了。

四、私有变量

JS中没有私有成员的概念,所以属性都是公有的,但是有一个私有概念,那就是在任何函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。

这些私有变量包括:函数的参数,局部变量,函数内部定义的其它函数。

function add(num1,num2){

    var a = '1500';

}  

在这个函数中,有三个私有变量,num1,num2,a。我们可以在函数内部访问他们,或者通过函数内部的闭包通过作用域来访问他们。那么我们利用这一点就可以创建用于访问私有变量的特权方法

有两种方式可以创建特权方法:

4.1私有变量

第一种:利用闭包

function Person(name){

    this.setName = function(value){

        name = value;

    }

    this.getName = function(){

        return name;

    }

}

var p = new Person();

p.setName('zjh');

alert(p.getName());  

所以,利用私有成员和特权成员可以隐藏那些不该被直接修改的数据。

缺点:是必须使用构造函数模式来达到这个目的。前面提到过,构造函数模式的缺点是:每次实例化一个对象,都会创建一组同样的新方法。

4.2静态私有变量

在私有作用域中定义私有变量或者函数,也可以创建特权方法。

本文由10bet发布于Web前端,转载请注明出处:浅谈javascript中的递归和闭包

关键词:

上一篇:WebRTC笔记

下一篇:19.进程和线程归纳

最火资讯