前端的那些事(三):你真的理解闭包吗
前言
看了很多书籍和文章,使用用闭包的原因说的真是管中窥豹,凭什么说函数作用域外部无法访问就用闭包,看下面的代码,照样也能访问内部变量,函数也可以获取内部变量。
var count = 0;
function foo() {
count++;
var a = 1;
var b = 2;
return {
a,
b
};
}
console.log(foo().a, count); // 1 1
console.log(foo().b, count); // 2 2
此处count是记录函数调用了几次,在这里调用了两次。
思考
那我用普通函数就好啦,用闭包干嘛呀?
解答
- 肯定是想在外部获取函数内部数据。
- 不知道你们注意到没有,我想获取
a,b
,是不是调用了两次foo函数
,count = 2
; 这样就执行创建了两次var a = 1; var b = 1;
当这其中代码很多很复杂的时候,你就消耗了很多性能
。- 职责单一,不耦合代码是我们书写良好js的基本功,我们期望a,b职责不同,应该有自己的函数去做处理。
- 其他的避免全局变量污染。
- 传参减少作用域查询。
我们将上述代码改变下
var count = 0;
function foo() {
count++;
var a = 1;
var b = 2;
return {
fna: function() {
return a;
},
fnb: function() {
return b;
}
};
}
var f = foo();
console.log(f.fna(), count); // 1 1
console.log(f.fnb(), count); // 2 1
很多人是不是想说,看起来好像更复杂了呀,确实复杂了。但是却有两个好处。1、职责单一;2、只调用了一次foo(),count一直是1;无论你想获取多少次函数数据,都只调用一次外层函数,性能好;
如果你觉得闭包就这样,还用它干嘛,那就错了,它能做很多事情。从简到难:
函数内部其实都是私有属性,我们可以自由暴露其为共用属性。
function foo() {
var num = Math.random();
return function fn() {
return num;
};
}
var f = foo();
console.log(f());
function foo () {
var num = Math.random();
return {
get_num : function () {
return num;
},
set_num: function( value ) {
return num = value;
}
}
}
教大家写jquery实现原理。
(function() {
var jQuery = function() {};
jQuery.custom = function() {
return "jQuery的自定义方法";
};
window.jQuery = window.$ = jQuery;
})();
console.log($.custom());
经典面试题:以下函数打印什么?
for (var i = 0; i <= 5; i++) {
setTimeout(function timer() {
console.log(i); // 6个6
}, 0);
}
如何打印0,1,2,3,4,5呢?
for (var i = 0; i <= 5; i++) {
(function(i_) {
setTimeout(function timer() {
console.log(i_);
}, 0);
})(i);
}
:
1、
js循环会进入队列
(先进先出的一种数据结构,数组、对象就是数据结构)。
2、promise是微异步,setTimeout是宏异步,意思就是setTimeout会等队列执行完成以后,在执行
。你想想循环执行完了这个i是多少,你在for循环外层打印,结果就是执行完的,就是6。
3、那咋办,咱用缓存呀,在for循环队列的时候,我把i给缓存下来
呀,匿名闭包就可以在内部访问外层的作用域,拿过来保存了。
ps:var换成let块级作用域也可以,简单方便。
也可以这么写代码实现:(更好理解是缓存的原因)
for (var i = 0; i <= 5; i++) {
function fn() {
var i_ = i;
setTimeout(function timer() {
console.log(i_);
}, 0);
}
fn();
}
以斐波那契数列为例,给大家普及下这是什么东西,就是兔子数列。
在第一个月有一对刚出生的小兔子,在第二个月小兔子变成大兔子并开始怀孕,第三个月大兔子会生下一对小兔子,并且以后每个月都会生下一对小兔子。 如果每对兔子都经历这样的出生、成熟、生育的过程,并且兔子永远不死,那么兔子的总数是如何变化的?得到的就是斐波那契数列。
月份:兔子数目。如下就是num对应foo(num)的值。
var count = 0;
function foo(num) {
count++;
if (num === 0) return 0;
if (num === 1) return 1;
return foo(num - 1) + foo(num - 2);
}
var f = foo(15);
console.log(f); // 610
console.log(count); // 1973
我们的count就是用来计数的,记录到底执行了多少次这个函数,发现1973次,当num越大调用次数越大,建议不要num设置很大,要么页面卡死,要么爆栈了。
这里抛出时间复杂度与空间复杂度,如何计算,后续会有专门文章去写。
- 时间复杂度:O(2^N)
- 空间复杂度:O(N)
时间复杂度是指数阶,属于爆炸增量函数,在程序设计中我们应该避免这样的复杂度。
改进版:
var data = [1, 1];
var count = 0;
function foo(num) {
count++;
var v = data[num];
if (v === undefined) {
v = foo(num - 1) + foo(num - 2);
data[num] = v;
}
return v;
}
foo(15);
console.log(count); // 29
大家可以看出这个递归
,其实我们也是对递归
进行缓存优化。
缓存思路:
- 普通递归,最郁闷的地方是,每次进入新的递归,之前算好的数据,它还会重新计算。所以性能极差。
- 其实斐波那契数列本身就是一个数组,我们可以预定义一个数组data去缓存之前已经计算的数据,这样深层递归就不用重新计算了。
- 当大家用递归的时候一定要使用缓存机制,提高的性能超一般的大。新的时间复杂度:O(N) ;空间复杂度:O(1)。
模块化封装:
var MyModules = (function Manager() {
var modules = {};
function define(name, deps, impl) {
for (var i = 0; i < deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply(impl, deps);
}
function get(name) {
return modules[name];
}
return {
define: define,
get: get
};
})();
模块化适用:
MyModules.define('bar', [], function() {
function hello(who) {
return 'Let me introduce: ' + who;
}
return {
hello: hello
};
});
MyModules.define('foo', ['bar'], function(bar) {
var hungry = 'hippo';
function awesome() {
console.log(bar.hello(hungry).toUpperCase());
}
return {
awesome: awesome
};
});
var bar = MyModules.get('bar');
var foo = MyModules.get('foo');
当然并非那么简单,你还需要做到:先加载所有定义好的模块,然后再使用,这里推荐AMD、CMD、es6 module。
闭包存在缺点
有两个缺点:
普及下垃圾回收机制两种方法:标记清除和引用计数,有兴趣的同学可以自行学习。
解决办法:解除引用循环
function foo() {
var num = Math.random();
return function fn() {
return num;
};
}
var f = foo();
f = null; // 释放内存
当我们使用闭包的时候,那么浏览器的GC(垃圾收集器)就无法自动释放内存,当你不使用的时候就设置
f = null
,下次GC运行时就会去释放缓存。
解决办法:不需要的变量设置null
function assignHandler(){
var el= document.getElementById("div");
el.onclick = function(){
};
el = null;
}
更多内容可以看我的集录: 全面攻陷js:更新中...