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

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

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

这里说一下前两种。

函数声明

函数声明语法如下

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

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

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

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

函数表达式

函数表达式语法如下:

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

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

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

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

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

递归

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

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

很简单,撸个代码看看:

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

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

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

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

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

闭包

js很恶心难懂的地方来了

要理解闭包,最重要的一点就是搞清楚作用域链,这玩意对理解闭包有很大的作用,作用域链可以看我之前的一篇博客https://www.cnblogs.com/SaberInoryKiss/p/11770974.html

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

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

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;//解除对匿名函数的引用(以便释放内存)
console.log(result)//0因为字符串不能比较大小,所以返回0,如果设为数字的话,比如var result = savant({name:“1”},{name: “2”}),会返回-1

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

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

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

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

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

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

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

闭包的缺点

(1)占用过多内存

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

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

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

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
}

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

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

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

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

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
}

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

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

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

方法二、使用let

因为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
}

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

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

解决办法,把外部作用域中的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

(4)内存泄漏

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

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' ]

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

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' ]

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

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

闭包的优点

(1)模仿块级作用域

语法:

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

举个例子来说明吧:

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

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

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)

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

(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(); //saber
savant2.sayName(); //archer

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

(3)静态私有变量

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

(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())//saber Savant1.setName("archer");
console.log(Savant1.getName())//archer var Savant2 = new Savant("lancer")
console.log(Savant1.getName())//lancer
console.log(Savant2.getName())//lancer

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

(4)模块模式

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

语法:

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

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

增强的模块模式

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);// publicA
console.log(application.publicB()); // privateA
console.log(application.name); // saber
console.log(application.getName());// saber

浅谈javascript中的递归和闭包的更多相关文章

  1. 浅谈JavaScript中的闭包

    浅谈JavaScript中的闭包 在JavaScript中,闭包是指这样一个函数:它有权访问另一个函数作用域中的变量. 创建一个闭包的常用的方式:在一个函数内部创建另一个函数. 比如: functio ...

  2. 浅谈JavaScript中的null和undefined

    浅谈JavaScript中的null和undefined null null是JavaScript中的关键字,表示一个特殊值,常用来描述"空值". 对null进行typeof类型运 ...

  3. 浅谈JavaScript中的正则表达式(适用初学者观看)

    浅谈JavaScript中的正则表达式 1.什么是正则表达式(RegExp)? 官方定义: 正则表达式是一种特殊的字符串模式,用于匹配一组字符串,就好比用模具做产品,而正则就是这个模具,定义一种规则去 ...

  4. 浅谈JavaScript中闭包

    引言 闭包可以说是JavaScript中最有特色的一个地方,很好的理解闭包是更深层次的学习JavaScript的基础.这篇文章我们就来简单的谈下JavaScript下的闭包. 闭包是什么? 闭包是什么 ...

  5. 浅谈JavaScript中的内存管理

    一门语言的内存存储方式是我们学习他必须要了解的,接下来让我浅谈一下自己对他的认识. 首先说,JavaScript中的变量包含两种两种类型: 1)值类型或基本类型:undefined.null.numb ...

  6. 浅谈JavaScript中的继承

    引言 在JavaScript中,实现继承的主要方式是通过原型链技术.这一篇文章我们就通过介绍JavaScript中实现继承的几种方式来慢慢领会JavaScript中继承实现的点点滴滴. 原型链介绍 原 ...

  7. 浅谈JavaScript中的this

    引言 JavaScript 是一种脚本语言,因此被很多人认为是简单易学的.然而情况恰恰相反,JavaScript 支持函数式编程.闭包.基于原型的继承等高级功能.本文仅采撷其中的一例:JavaScri ...

  8. 浅谈JavaScript中的变量、参数、作用域和作用域链

    基本类型和引用类型 在JavaScript中有两种数据类型值.基本类型值和引用类型值.基本类型值指的是简单的数据段,而引用类型值指的是可能由多个值构成的对象.在JavaScript中有5种基本数据类型 ...

  9. 浅谈JavaScript中继承的实现

    谈到js中的面向对象编程,都有一个共同点,选择原型属性还是构造函数,两者各有利弊,而就片面的从js的对象创建以及继承的实现两个方面来说,官方所推荐的是两个相结合,各尽其责,各取其长,在前面的例子中,我 ...

随机推荐

  1. php 5 与7有什么区别

    PHP 7.0使用新版的ZendEngine引擎,带来了许多新的特性,其与相比,有如下特性: 性能提升:PHP7比PHP5.0性能提升了两倍. 全面一致的64位支持. 以前的许多致命错误,现在改成抛出 ...

  2. Protobuf多协议

    上一篇只有Person的message,如果多了一个message,如Dog,这样就会有问题. 解决方法: 定义多协议 一.定义proto文件 syntax = "proto2"; ...

  3. 001 okhttp3的POST使用

    继续使用上面的项目 1.被调用的项目 package com.jun.web2forokhttp.okhttp; import com.jun.web2forokhttp.bean.HttpDomai ...

  4. osg::Node位置移动

    osg::Node节点移动的时候,可以使用osg::Matrix::translate  来完成 osg::Matrix::translate中的参数是  当前位置到目标位置需要改变的值,所以,传递参 ...

  5. EF Core基本使用

    Mysql: nuget 安装 Pomelo.EntityFrameworkCore.MySql Microsoft.EntityFrameworkCore.Design csprj 修改: < ...

  6. Java通过行为参数化传递代码

    在软件工程中,一个众所周知的问题就是,不管做什么,用户的需求肯定会变.如何应对这样不断变化的需求?理想的状态下,应该把的工作量降到最少.此外,类似的新功能实现起来还应该很简单,而且易于长期维护.行为参 ...

  7. EasyNVR摄像机网页直播之问题解决:Failed:SYSTEM\CurrentCont......\Application\EasyNVR_Service registry key already

    背景分析 经常使用EasyNVR产品的用户都知道,作为音视频行业互联网直播产品,EasyNVR主要功能在于通过RTSP/ONvif协议,接入前端音视频采集设备,通过EasyNVR软硬件产品将拉取过来的 ...

  8. [LeetCode] 258. Add Digits 加数字

    Given a non-negative integer num, repeatedly add all its digits until the result has only one digit. ...

  9. 使用二进制的方式部署 K8S-1.16 高可用集群

    一.项目介绍 项目致力于让有意向使用原生kubernetes集群的企业或个人,可以方便的.系统的使用二进制的方式手工搭建kubernetes高可用集群.并且让相关的人员可以更好的理解kubernete ...

  10. 微设计基础架构(MDI)

    微设计基础架构(MDI) 了解微设计基础架构(MDI)的概念,它们如何帮助开发,以及它们与DevOps和微服务等技术的关系. 技术决策既困难又严肃,可以决定项目的成败.如何找到合适的技术栈?“微设计基 ...