闭包
前言
现在去面试前端开发的岗位,如果你面对的面试官也是个前端,并且不是太水的话,你有很大的概率被问到JavaScript的闭包。
什么是闭包
什么是闭包,百度、Google之后,你可能会搜索很多答案...
《JavaScript高级程序设计》这样描述
闭包是指有权访问另一个函数作用域中的变量的函数
《JavaScript权威指南》这样描述
从技术的角度讲,所有的JavaScript函数都是闭包:它们都是对象,它们都关联到作用域链
《你不知道的JavaScript》这样描述
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是当前词法作用域之外执行
最认可的当属《你不知道的JavaScript》,前面两种说话都没有错。
但闭包应该是基于词法作用域书写代码时产生的自然结果,是一种现象!你也不用为了利用闭包而特意的创建,因为闭包的在你的代码中随处可见,只是你还不知道当时你写的那一段代码其实就产生了闭包
讲解闭包
上面已经说到,当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
看段代码
function fn1(){
var name='小马哥'
function fn2(){
console.log(name);
}
fn2();
}
fn1();
如果是根据《JavaScript高级程序设计》和《JavaScript权威指南》来说,上面的代码已经产生闭包了。fn2访问到了fn1的变量,满足了条件“有权访问另一个函数作用域中的变量的函数”,fn2本身是个函数,所以满足了条件“所有的JavaScript函数都是闭包”。
这的确是闭包,但是这种方式定义的闭包不太好观察。
再看一段代码:
function fn1(){
var name='小马哥'
function fn2(){
console.log(name);
}
return fn2;
}
var fn3 = fn1();
fn3();
这样就清晰地展示了闭包:
- fn2的词法作用域能访问fn1的作用域
- 将fn2当做一个值返回
- fn1执行后,将fn2的引用赋值给fn3
- 执行fn3,输出了变量name
我们知道通过引用关系,fn3就是fn2函数本身。执行fn3能正常输出name,这不就是fn2能记住并访问它所在的词法作用域,并且fn2函数的运行还是在当前词法作用域之外
正常来说,当fn1函数执行完毕之后,其作用域是会被销毁的,然后垃圾回收器会释放那段内存空间。而闭包却很神奇的将fn1的作用域存活了下来,fn2依然持有该作用域的引用,这个引用就是闭包。
总结:某个函数在定义时的词法作用域之外的地方被调用,闭包可以使该函数极限访问定义时的词法作用域。
注意:对函数值的传递可以通过其他的方式,并不一定只有返回该函数这一条路,比如可以用回调函数:
function fn1() {
var name = '小马哥';
function fn2() {
console.log(name);
}
fn3(fn2);
}
function fn3(fn) {
fn();
}
fn1();
本例中,将内部函数fn2传递给fn3,当它在fn3中被运行时,它是可以访问到name变量的。
所以无论通过哪种方式将内部的函数传递到所在的词法作用域以外,它都会持有对原始作用域的引用,无论在何处执行这个函数都会使用闭包。
再次解释闭包
以上的例子会让人觉得有点学院派了,但是闭包绝不仅仅是一个无用的概念,你写过的代码当中肯定有闭包的身影,比如类似如下的代码:
function waitSomeTime(msg,time){
setTimeout(function(){
console.log(msg);
},time);
}
waitSomeTime('hello',1000);
定时器中有一个匿名函数,该匿名函数就有涵盖waitSomeTime函数作用域的闭包,因此当1秒之后,该匿名函数能输出msg。
另一个很经典的例子就是for循环中使用定时器延迟打印的问题:
for (var i = 1; i <= 10; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
我们预期的结果为1~10,但却输出10此11。这是因为setTimeout中的匿名函数执行的时候,for循环都已经结束了,for循环结束的条件是i大于10,所以输出10此11。
原因:i是声明在全局作用域中的,定时器中的匿名函数也是执行在全局作用域中,那当时是每次都输出11
原因知道了,解决起来就简单了,我们可以让i在每次迭代的时候,都产生一个私有的作用域,在这个私有的作用域中保存当前i的值
for (var i = 1; i <= 10; i++) {
(function () {
var j = i;
setTimeout(function () {
console.log(j);
}, 1000);
})();
}
这样就达到我们的预期了呀,让我们用一种比较优雅的写法改造一些,将每次迭代的i作为实参传递给自执行函数,自执行函数中用变量去接收:
for (var i = 1; i <= 10; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, 1000);
})(i);
}
闭包的应用
- setTimeout
//原生的setTimeout传递的第一个函数不能带参数
setTimeout(function(param){
alert(param)
},1000)
//通过闭包可以实现传参效果
function func(param){
return function(){
alert(param)
}
}
var f1 = func(1);
setTimeout(f1,1000);
- 闭包的应用比较典型的是定义模块和封装变量,我们将操作函数暴露给外部,而细节隐藏在模块内容
function module(){
var arr = []; //私有变量
function add(val){
if(typeof val ==='number'){
arr.push(val);
}
}
function get(index){
if(index < arr.length){
return arr[index];
}else {
return null
}
}
return {
add:add,
get:get
}
}
var mod1 = module();
mod1.add(1);
mod1.add(2);
mod1.add('xxx');
console.log(mod1.get(2));
- 缓存
function getNewValue(key) {
var obj = {
name:'张三'
}
return obj[key]
}
var CacheCount = (function () {
var cache = {};
return {
getCache: function (key) {
if (key in cache) { // 如果结果在缓存中
console.log(cache);
return cache[key]; // 直接返回缓存中的对象
}
var newValue = getNewValue(key); // 外部方法,获取缓存
cache[key] = newValue; // 更新缓存
return newValue;
}
};
})();
console.log(CacheCount.getCache("name"));
console.log(CacheCount.getCache("name"));
JavaSript的高级函数
回调函数
function createDiv(cb){
let oDiv = document.createElement('div');
document.body.appendChild(oDiv);
if(typeof cb === 'function'){
cb(oDiv);
}
}
createDiv(function(oDiv){
oDiv.style.color = 'red';
});
这个例子中,有一个createDiv这个函数,这个函数负责创建一个div并添加到页面中,但是之后要再怎么操作这个div,createDiv这个函数就不知道,所以把权限交给调用createDiv函数的人,让调用者决定接下来的操作,就通过回调的方式将div给调用者。
这是体现出了抽象,既然不知道div接下来的操作,那么就直接给调用者,让调用者去实现
抽象就是隐藏更具体的实现细节,从更高的层次看待我们要解决的问题。
数组中遍历
在编程的时候,并不是所有功能都是现成的,比如上面例子中,可以创建好几个div,对每个div的处理都可能不一样,需要对未知的操作做抽象,预留操作的入口,作为一名程序员,我们需要具备这种在恰当的时候将代码抽象的思想
接下来看一下ES5中提供的几个数组操作方法,可以更深入的理解抽象的思想,ES5之前遍历数组的方式是:
var arr = [1, 2, 3, 4, 5];
for (var i = 0; i < arr.length; i++) {
var item = arr[i];
console.log(item);
}
仔细看一下,这段代码中用for,然后按顺序取值,有没有觉得如此操作有些不够优雅,为出现错误留下了隐患,比如把length写错了,一不小心复用了i。既然这样,能不能抽取一个函数出来呢?最重要的一点,我们要的只是数组中的每一个值,然后操作这个值,那么就可以把遍历的过程隐藏起来:
function forEach(arr, callback) {
for (var i = 0; i < arr.length; i++) {
var item = arr[i];
callback(item);
}
}
forEach(arr, function (item) {
console.log(item);
});
以上的forEach方法就将遍历的细节隐藏起来的了,把用户想要操作的item返回出来,在callback还可以将i、arr本身返回:callback(item, i, arr)
。
JS原生提供的forEach方法就是这样的:
arr.forEach(function (item) {
console.log(item);
});
跟forEach同族的方法还有map、some、every等。思想都是一样的,通过这种抽象的方式可以让使用者更方便,同事又让代码变得更加清晰。
抽象是一种很重要的思想,让可以让代码变得更加优雅,并且操作起来更方便。在高阶函数中也是使用了抽象的思想,所以学习高阶函数得先了解抽象的思想。
高阶函数
什么是高阶函数
至少满足以下条件中的一个,就是高阶函数
- 将其他函数作为参数传递
- 将函数作为返回值
简单来说,就是一个函数可以操作其他函数,将其他函数作为参数或将函数作为返回值。我相信,写过JS代码的同学对这个概念都是很容易理解的,因为在JS中函数就是一个普通的值,可以被传递,可以被返回。
参数可以被传递,可以被返回
函数作为参数传递
函数作为参数传递就是我们上面提到的回调函数,回调函数在异步请求中用的非常多,使用者想要在请求成功后利用请求回来的数据做一些操作,但是又不知道请求什么时候结束。
用jQuery来发一个Ajax请求
function getDetailData(sub_category_id, callback) {
$.ajax(`https://www.luffycity.com/api/v1/courses/?sub_category=${sub_category_id}&ordering=`, function (res) {
if (typeof callback === 'function') {
callback(res);
}
});
}
getDetailData('1', function (res) {
// do some thing
});
类似Ajax这种操作非常适合用回调去做,当一个函数里不适合执行一些具体的操作,或者说不知道要怎么操作时,可以将相应的数据传递给另一个函数,让另一个函数来执行,而这个函数就是传递进来的回调函数。
另一个典型的例子就是数组排序
函数作为值返回
在判断数据类型的时候最常用的是typeof,但是typeof有一定的局限性,比如:
console.log(typeof []);//object
console.log(typeof {});//object
判断数组和对象都是输出object,如果想要更细致的判断应该要使用Object.prototype.toString
console.log(Object.prototype.toString.call([])); // 输出[object Array]
console.log(Object.prototype.toString.call({})); // 输出[object Object]
于是,我们可以写出判断对象、数组、数字的方法
function isObject(obj){
return Object.prototype.toString.call(obj) === '[object Object]';
}
function isArray(arr) {
return Object.prototype.toString.call(arr) === '[object Array]';
}
function isNumber(number) {
return Object.prototype.toString.call(number) === '[object Number]';
}
我们发现这三个方法太像了,可以做一些抽取:
function isType(type) {
return function (obj) {
return Object.prototype.toString.call(obj) === '[object ' + type + ']';
}
}
var isArray = isType('Array');
console.log(isArray([1,2]));
这个isType方法就是高阶函数,该函数返回了一个函数,并且利用闭包,将代码变得优雅。