其实我对js闭包的理解在不同的阶段理解是不一样的
因为对于闭包有很多不同的理解,包括一些书籍(例如js高级程序设计),这里直接以浏览器解析,以浏览器理解的闭包为准来分析闭包,如下图:
如上图所示,chrome浏览器理解闭包是foo,那么按浏览器的标准是如何定义闭包的,总结为三点:
在函数内部定义新函数
新函数访问外层函数的局部变量,即访问外层函数环境的活动对象属性
新函数执行,创建新的函数执行上下文,外层函数即为闭包
首先呢,我们必须搞清楚闭包这个概念:闭包其实是一个特殊的对象,他由两部分组成,一个是执行上下文(代号A),以及在该执行上下文中创建的函数(代号B),当B执行时,如果访问了A中变量对象的变量,那么闭包就产生了。
这里再举个吧,柯里化函数
右边的 CallStack表示当前函数调用栈,Scope表示当前作用域,Local表示当前活动对象,Closure表示闭包(在这里闭包是addCurrying函数)。
闭包分析
既然闭包涉及到内存问题,那么不得不提一嘴V8的GC(垃圾回收)机制。
我们从书本上了解最多的GC策略就是引用计数,但是现代主流VM(包括V8, JVM等)都不采用引用计数的回收策略,而是采用可达性算法。
可达性算法
该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC
Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数都不为0,那么垃圾收集器就永远不会回收它们。
引用计数让人比较容易理解,所以常见于教材中,但是可能存在对象相互引用而无法释放其内存的问题。而可达性算法是从GC Roots对象(比如全局对象window)开始进行搜索存活(可达)对象,不可达对象会被回收,存活对象会经历一系列的处理。
闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。
先解释一下什么是“自由变量”。
JavaScript 中的内存管理是自动执行的,而且是不可见的。我们创建基本类型、对象、函数……所有这些都需要内存。
当不再需要某样东西时会发生什么? JavaScript 引擎是如何发现并清理它?
可达性
标记清除
引用计数
标记清除和引用计数就不详细讲了;
JavaScript 中内存管理的主要概念是可达性。
简单地说,“可达性” 值就是那些以某种方式可访问或可用的值,它们被保证存储在内存中。
本地函数的局部变量和参数
当前嵌套调用链上的其他函数的变量和参数
全局变量
还有一些其他的,内部的
这些值称为根。
例如,如果局部变量中有对象,并且该对象具有引用另一个对象的属性,则该对象被视为可达性, 它引用的那些也是可以访问的,详细的例子如下。
JavaScript 引擎中有一个后台进程称为垃圾回收器,它监视所有对象,并删除那些不可访问的对象。
// user 具有对象的引用
let user = {
name: "John"
};
这里箭头表示一个对象引用。全局变量“user”引用对象 {name:“John”} (为了简洁起见,我们将其命名为John)。John 的 “name” 属性存储一个基本类型,因此它被绘制在对象中。
如果 user 的值被覆盖,则引用丢失:
user = null;
现在 John 变成不可达的状态,没有办法访问它,没有对它的引用。垃圾回收器将丢弃 John 数据并释放内存。
在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个自由变量。如下图
网上的文章关于这一块还是讲得挺详细的,本文就不再举例了。总的来说,闭包有这么一些优点:
变量常驻内存,对于实现某些业务很有帮助,比如计数器之类的。
架起了一座桥梁,让函数外部访问函数内部变量成为可能。
私有化,一定程序上解决命名冲突问题,可以实现私有变量。
闭包是双刃剑,也存在这么一个比较明显的缺点:
存在这样的可能,变量常驻在内存中,其占用内存无法被GC回收,导致内存溢出。
function Fun(){
var name = 'tom';
this.getName = function (){
return name;
}
}
var fun = new Fun();
console.log(fun.name);//输出undefined,在外部无法直接访问name
console.log(fun.getName());//可以通过特定方法去访问
setTimeout:接收两个参数,第一个参数可以是一段js代码,亦可以是一个函数,第二个参数是我们延迟执行第一个参数的时间(实际上不是延迟执行,而是延迟加入执行队列),在此我们要讨论的情况是第一个参数是一个函数的情况,我们传入的参数实际上是函数对象的引用,那这时候就不能向函数传参了,那么闭包就派上用场了,即通过闭包来给setTimeout函数的第一个函数传参。
function fun(num){
var age = num;
return function(){
console.log(age);
}
}
var getAge = fun(200);//传入需要的参数,得到函数(闭包)的引用
var age = setTimeout(getAge,1000);//正确输出
一个内联执行的函数表达式返回了内部函数对象的一个引用。并且分配了一个全局变量,让它可以被作为一个全局函数来调用。而缓冲数组作为一个局部变量被定义在外部函数表达式中。它没有被扩展到全局命名空间中,并且无论函数什么时候使用它都不需要被再次创建。
在es6之前,没有let const 作用域,那么我们怎么来封装作用域呢
function outputNumbers(count){
(function(){
//块级作用域
for(var i = 0; i < count; i++){
console.log(i); // 0, 1, ... count - 1
}
})();
console.log(i); // error
}
这样就实现了块级作用域啦
PHP官方文档对于闭包函数的定义:
匿名函数(Anonymous functions),也叫闭包函数(closures),允许 临时创建一个没有指定名称的函数。最经常用作回调函数(callback)参数的值。当然,也有其它应用的情况。
简单来说,闭包函数也是一种数据类型,可以直接使用变量来存储、传参、调用等等。
传统缓存操作
操作缓存的时候一般步骤如下:
读取缓存
如果缓存不为空则返回缓存数据
读取数据库,然后设置到缓存
返回数据
PHP示例代码如下:
function loadUser($userId) {
$data = $cache->get('user-'. $userId);
if(!empty($data)) {
return $data;
}
$data = $db->findOne(['user_id' => $userId]);
$cache->set('user-'. $userId, $data, 7200);
return $data;
}
其实查找缓存,如果不存在则查找数据库之后写入缓存这个操作也可以用闭包来实现:
可以看到通过闭包省去了手动get和set的过程,而查询数据库那一步是只有在缓存读取不到才会执行。
实现斐波那契数列
方法1:递归实现
由题目中的递推受到启发,可以通过递归的方式去实现,代码如下:
function fibonacci(n){
if(n < 0) throw new Error('输入的数字不能小于0');
if(n==1 || n==2){
return 1;
}else{
return fibonacci1(n-1) + fibonacci1(n-2);
}
}
优点:代码比较简洁易懂;
缺点:当数字太大时,会变得特别慢,原因是在计算F(9)时需要计算F(8)和F(7),但是在计算F(8)时要计算F(7)和F(6),这里面就会重复计算F(7),每次都重复计算会造成不必要的浪费,所以这种方法并不是很好。
使用缓存解决斐波那契数列的性能问题:
就是将已经计算过的数字缓存进一个数组中,下次再来访问的时候,直接在数组中查找,如果找到,直接使用,如果没有找到,计算后将数字存入数组,然后返回该数字。
function f() {
var obj = {};
function fib(n) {
if (n == 1 || n == 2) {
return 1;
}
if (obj[n]) {
return obj[n];
}
return obj[n] = fib(n - 1) + fib(n - 2);
}
return fib;
}
var af = f();
console.log(af(100))
还可以使用reduce数组:在红宝书中,将这个方法定义为数组的归并方法,这个方法和迭代方法(map,forEach,filter…)一样,都会对数组进行遍历,reduce与他们不同的是函数的第一个参数得到的是迭代计算后的效果。