JavaScript 闭包和递归

闭包

闭包是 JavaScript 一个非常重要的特性,这意味着当前作用域总是能够访问外部作用域中的变量。 因为函数 是 JavaScript 中唯一拥有自身作用域的结构,因此闭包的创建依赖于函数。

闭包是一种特殊的对象,它由两部分组成:执行上下文(代号 A),以及在该执行上下文中创建的函数 (代号 B),当 B 执行时,如果访问了 A 中变量对象的值,那么闭包就会产生,且在 Chrome 中使用这个执行上下文 A 的函数名代指闭包。

JavaScript拥有自动的垃圾回收机制,关于垃圾回收机制,有一个重要的行为,那就是,当一个值,在内存中失去引用时,垃圾回收机制会根据特殊的算法找到它,并将其回收,释放内存。

而我们知道,函数的执行上下文,在执行完毕之后,生命周期结束,那么该函数的执行上下文就会失去引用。其占用的内存空间很快就会被垃圾回收器释放。可是闭包的存在,会阻止这一过程。

一个简单的列子

var fn = null;
function foo() {
    var a = 2;
    function innnerFoo() {
        console.log(a);
    }
    fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}

function bar() {
    fn(); // 此处的保留的innerFoo的引用
}

foo();
bar(); // 2

在上面的例子中,foo()执行完毕之后,按照常理,其执行环境生命周期会结束,所占内存被垃圾收集器释放。但是通过fn = innerFoo,函数innerFoo的引用被保留了下来,复制给了全局变量fn。这个行为,导致了foo的变量对象,也被保留了下来。于是,函数fn在函数bar内部执行时,依然可以访问这个被保留下来的变量对象。所以此刻仍然能够访问到变量a的值。

这样,我们就可以称foo为闭包。

模拟私有变量

function Counter(start) {
    var count = start;
    return {
        increment: function() {
            count++;
        },

        get: function() {
            return count;
        }
    }
}

var foo = Counter(4);
foo.increment();
foo.get(); // 5

这里,Counter 函数返回两个闭包,函数 increment 和函数 get。 这两个函数都维持着 对外部作用域 Counter 的引用,因此总可以访问此作用域内定义的变量 count.

为什么不可以在外部访问私有变量

因为 JavaScript 中不可以对作用域进行引用或赋值,因此没有办法在外部访问 count 变量。 唯一的途径就是通过那两个闭包。

var foo = new Counter(4);
foo.hack = function() {
    count = 1337;
};

上面的代码不会改变定义在 Counter 作用域中的 count 变量的值,因为 foo.hack 没有 定义在那个作用域内。它将会创建或者覆盖全局变量 count

循环中的闭包

一个常见的错误出现在循环中使用闭包,假设我们需要在每次循环中调用循环序号

for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);  
    }, 1000);
}

上面的代码不会输出数字 09,而是会输出数字 10 十次。

console.log 被调用的时候,匿名函数保持对外部变量 i 的引用,此时 for循环已经结束, i 的值被修改成了 10.

为了得到想要的结果,需要在每次循环中创建变量 i拷贝

避免引用错误

为了正确的获得循环序号,最好使用 匿名包装器(其实就是我们通常说的自执行匿名函数)。

for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);  
        }, 1000);
    })(i);
}

外部的匿名函数会立即执行,并把 i 作为它的参数,此时函数内 e 变量就拥有了 i 的一个拷贝。

当传递给 setTimeout 的匿名函数执行时,它就拥有了对 e 的引用,而这个值是不会被循环改变的。

有另一个方法完成同样的工作,那就是从匿名包装器中返回一个函数。这和上面的代码效果一样。

for(var i = 0; i < 10; i++) {
    setTimeout((function(e) {
        return function() {
            console.log(e);
        }
    })(i), 1000)
}

模块化

(function () {
    var a = 10;
    var b = 20;

    function add(num1, num2) {
        var num1 = !!num1 ? num1 : a;
        var num2 = !!num2 ? num2 : b;

        return num1 + num2;
    }

    window.add = add;
})();

add(10, 20);

在上面的例子中,我使用函数自执行的方式,创建了一个模块。add是模块对外暴露的一个公共方法。而变量a,b被作为私有变量。在面向对象的开发中,我们常常需要考虑是将变量作为私有变量,还是放在构造函数中的this中,因此理解闭包,以及原型链是一个非常重要的事情

防抖节流

// 节流
function throttle(fn, delay) {
    var timer = null, firstTime = true;
    return function () {
        if (timer) { return false;}
        var that = this;
        var args = arguments;
        fn.apply(that, args);
        timer = setTimeout(function () {
            clearTimeout(timer);
            timer = null;
        }, delay || 500);
    };
}
// 防抖
function debounce(fn, delay) {
    var timer = null;
    return function () {
        var that = this;
        var args = arguments;
        clearTimeout(timer);// 清除重新计时
        timer = setTimeout(function () {
            fn.apply(that, args);
        }, delay || 500);
    };
}

字节面试

var result = [];
var a = 3;
var total = 0;

function foo(a) {
    for (var i = 0; i < 3; i++) {
        result[i] = function () {
            total += i * a;
            console.log(total);
        }
    }
}

foo(1);
result[0]();  // 3
result[1]();  // 6
result[2]();  // 9
result[2]();  // 15 
result[2]();  // 18
  1. 循环之后,i = 3 不变

  2. result[0/1/2] 都是一个方法

    ƒ () {
        total += i * a;
        console.log(total);
    }
    
  3. tip:这里也形成了闭包。total 被外层引用没有被销毁。每一次执行一次,total 都会增加

总结

形成: 函数中嵌套函数

作用: 函数内部调用外部变量、构造函数的私有属性、延长变量生命周期

优点: 希望一个变量长期存在内存中、模块化代码避免全局变量的污染、私有属性

缺点: 无法回收闭包中引用变量,容易造成内存泄漏

应用:

  1. ajax请求的成功回调
  2. 事件绑定的回调方法
  3. setTimeout的延时回调
  4. 函数内部返回另一个匿名函数
  5. 模块化
  6. 防抖节流

递归

递归就是函数调用函数本身的函数

  1. 一个简单的列子,求 N个数的累加,递归多次容易出现内存栈溢出。
    function sunNme(n) {
      if (n === 1) return 1
      return n + sunNme(n - 1)
    }
    
    sunNme(3)  // 6
    sunNme(100000) // Uncaught RangeError: Maximum call stack size exceeded
    

    递归一定主要有出口,比如上面的if(n===1) return 1不然会造成内存溢出

  2. 递归一般用循环都可以实现

    function sunNum(n) {
      let result = 0
      for (let i = n; i !== 0; i--) {
        result += i
      }
      return result
    }
    let result = sunNum(10)
    
练习题
1、拷贝
var deepCopy = function (obj) {
  if (typeof obj !== 'object') return obj;
  // // 根据obj的类型判断是新建一个数组还是对象
  var newObj = obj instanceof Array ? [] : {};
  for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
    }
  }
  return newObj;
}
2、数组扁平化
const flatten = (arr) => {
  let result = [];
  arr.forEach((item, i, arr) => {
    // 若为数组,递归调用 faltten,并将结果与result合并
    if (Array.isArray(item)) {
      result = result.concat(flatten(item));
    } else {
      result.push(arr[i])
    }
  })
  return result;
};
const arr = [1, [2, [3, 4, 5]]];
console.log(flatten(arr)); // [1, 2, 3, 4, 5]
console.log(arr.flat(Infinity)); // [1, 2, 3, 4, 5]
3、将原来字符串倒过来。
function reverse(str) {
   if(str.length <= 1) return str; 
   return reverse(str.slice(1)) + str[0];
}

reverse('abc')
4、写一个函数,接受一串字符串,同时从前后开始拿一位字符对比,如果两个相等,返回 true,如果不等,返回 false
function isPalindrome(str){ 
    if(str.length === 1) return true;
    if(str.length === 2) return str[0] === str[1]; 
    if(str[0] === str.slice(-1)) return isPalindrome(str.slice(1,-1)) 
    return false; 
}

var str = 'abba'
isPalindrome(str)
5、编写一个函数,接受一个对象,这个对象值是偶数,则让它们相加,返回这个总值。
function nestedEvenSum(obj, sum=0) {
    for (var key in obj) { 
        if (typeof obj[key] === 'object'){ 
            sum += nestedEvenSum(obj[key]); 
        } else if (typeof obj[key] === 'number' && obj[key] % 2 === 0){ 
            sum += obj[key]; 
        }
     } 
     return sum;
}

nestedEvenSum({c: 4,d: {a: 2, b:3}})
6、编写一个函数,接受一个对象,返回一个数组,这个数组包括对象里所有的值是字符串
function collectStrings(obj) {
    let newArr = []
    for (let key in obj) {
        if (typeof obj[key] === 'string') {
            newArr.push(obj[key])
        } else if(typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
           newArr = newArr.concat(collectStrings(obj[key]))
        }
    }
    return newArr
}

var obj = {
        a: '1',
        b: {
            c: 2,
            d: 'dd'
        }
    }

collectStrings(obj)

闭包:

​ https://segmentfault.com/a/1190000012646221

​ http://bonsaiden.github.io/JavaScript-Garden/zh/#function.closures

递归:https://juejin.cn/post/6844903699584647176

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值