前端战五渣学JavaScript——闭包

就决定是你了——闭包

有不少开发人员总是搞不清匿名函数闭包两个概念,因此经常混用。闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。
——————摘自《JavaScript高级程序设计》

上面说的很清楚,就是一个函数内部再创建另一个函数(尴尬。。。好像跟书上说的一样。多余解释)。我之前面试也经常被问到这个问题。。。没想到书上就这么一句。以下需要有一点js的基础,了解作用域才能看明白。。。

闭包的旅程才刚刚开始

我们先来看一个最简单的⬇️

function a(){
    var b = 10;
    return function() {
        return b
    }
}
console.log(a()()); // 10

看,奇迹吧!!!我们输出了a函数中的局部变量b的值。就是这么神奇。。。
诶??你们可能会说那我这个局部变量b,相当于一个常量,如果我像下面⬇️这样岂不是一个效果?

function a() {
    var b = 10;
    return b
}
console.log(a());

好的,没有错,那我们再换种方式⬇️

function a(paramA) {
    return function b(paramB) {
        return paramA + paramB
    }
}

var variable10 = a(10);
var variable20 = a(20);

console.log(variable10(5)); // 15
console.log(variable20(5)); // 25

上面这个例子,在我们执行输出variable10(5)variable20(5)的时候,为什么我们能得到5与之前执行a(10)时传进去的10呢?这就是我们闭包的用处,可以获取到函数内的局部变量,并相加。
等等!也许有人不明白了,怎么是局部变量了呢?那我按下面这种写法可能清楚一些⬇️

function a(paramA) {
  + var variableA = paramA;
  return function b(paramB) {
    return variableA + paramB
  }
}

var variable10 = a(10);
/**
 *  a(10)是传进去是什么样的呢?
 *  
 *  function a(10) {
 *    var variableA = 10;
 *    return function b(paramB) {
 *      return variableA + paramB
 *    }
 *  }
 *  上面语法不对哦,我只是写的好理解一些
 *  也就是说variable10指向了内部函数b
 *  var variable10 = function b(paramB) {
 *    return 10 + paramB
 *  }
 */
var variable20 = a(20);

console.log(variable10(5)); // 15
console.log(variable20(5)); // 20

通过上面的注释,想必大家应该已经了解闭包是什么了,就是在一个外部函数内部创建另一个函数,以至于全局调用内部函数的时候可以访问到外部函数的局部变量,并使用。

真相永远只有一个!

来,上《JavaScript高级程序设计》的例子

function createComparisonFunction(propertyName) {

  return function (object1, object2) {
    var value1 = object1[propertyName];
    var value2 = object2[propertyName];

    if (value1 < value2) {
      return -1
    } else if (value1 > value2) {
      return 1
    } else {
      return 0;
    }
  }
}

在这个例子中,var value1var value2这两行代码访问了外部函数中的变量propertyName,即使这个内部函数被返回了,而且是在其他地方被调了,但它仍然可以访问变量propertyName;这就类似于我上面的例子(代码接上)

var manA = { name: '江户川柯南', age: 7 };
var manB = { name: '工藤新一', age: 17 };
var whoOlder = createComparisonFunction('age');
// 返回-1为前者小于后者,返回1位前者大于后者,返回0两者一样大
console.log(whoOlder(manA, manB));  // -1,柯南小于新一

上面的例子就相当于开始介绍闭包时的例子,不明白?就是

var whoOlder = createComparisonFunction('age');
/**
 *  var whoOlder = createComparisonFunction('age') {
 *    var propertyName = 'age';
 *    return function (object1, object2) {
 *      var value1 = object1['age'];
 *      var value2 = object2['age'];
 *      if (value1 < value2) {
 *        return -1
 *      } else if (value1 > value2) {
 *        return 1
 *      } else {
 *        return 0;
 *      }
 *    }
 *  }
 *  上面语法不对啊,我只是写的好理解一些
 *  最后得到的就是
 *  var whoOlder = function (object1, object2) {
 *    var value1 = object1['age'];
 *    var value2 = object2['age'];
 *      if (value1 < value2) {
 *        return -1
 *      } else if (value1 > value2) {
 *        return 1
 *      } else {
 *        return 0;
 *      }
 *    }
 *  }
 */

重点!!! 在一个函数被调用的时候,会创建一个执行环境(execution context)及相应的作用域链。然后,还有初始化一个活动对象(activation object),这个活动对象里有什么呢?有arguments和其他命名参数的值,就是⬇️

function a(paramA, paramB) {
    return paramA + paramB
}
a(1, 2)
/**
 *  这个函数的活动对象有什么呢??有三个
 *  arguments [1, 2]
 *  paramA    1
 *  paramB    2
 */

然后就是刚说的作用域链,外部函数的活动对象会始终处于第二位,就是比如我在内部函数用到变量a,会先在内部函数找有没有声明a这个变量,如果没有,就会去外部函数找,外部函数没有就会往全局执行环境找。
在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量,来看下面的例子(《JavaScript高级程序设计》例子)

function compare(value1, value2) {
    if (value1 < value2) {
        return -1
    } else if (value1 > value2) {
        return 1
    } else {
        return 0
    }
}

var result = compare(5, 10)

这个的作用域链画出来就是

作用域链,执行环境,活动对象

现在应该多少能看懂这张图了吧,当时看书的时候真是费死劲了,中间那个作用域链1,0什么的,就是处于0位置的活动对象是函数自己的活动对象,处于1位置的是全局对象,所以上面说外部函数的活动对象始终处于第二位。

天下第一武道大会

想必大家面试的时候都会遇到一个非常非常经典的题,题面就是:

页面上有十个<li></li>标签,就是一个列表有十项,需要点击每个li输出对应的索引。

经典错误答案:

var list = document.getElementsByTagName('li');
for (var i = 0; i < list.length; i ++) {
  list[i].addEventListener('click', function () {
    console.log(i)
  })
}

这我们现在都知道是输出同一个数了,这是为什么呢?我来换一种写法:

var list = document.getElementsByTagName('li');
var i = 0;
for (i; i < list.length; i ++) {
  list[i].addEventListener('click', function () {
    console.log(i)
  })
}

这下我们可以看清楚了吧,当这段代码都执行完,还没有点击li的时候,全局变量i已经变成最后一个数了,假设有十个li,i就已经根据循环变成了10。所以,当我们点击li的输入全局变量i的时候,当然每次输出的都是10了。

经典正确答案:

var list = document.getElementsByTagName('li');
for (var i = 0;; i < list.length; i ++) {
  (function (i) {
    list[i].addEventListener('click', function () {
      console.log(i)
    })
  })(i)
}

在for循环里,每执行一次,执行一个自调函数,并把i当做参数传进去,js函数的参数有个按值传递的特性,也可以理解为我们前面讲过的活动对象,这个自调函数的活动对象里面有个i的值,所以这时候每个li上要执行的监听函数输出的就不在是全局变量i,而是活动对象中的i,因为循环的时候没循环一次执行一次自调函数,所以每个自调函数之间是各自独立的,所以输出的i值也自然不一样了。
当然也有人会说用ES6的语法更简单一些:

var list = document.getElementsByTagName('li');
 - for (var i = 0; i < list.length; i ++) {
 + for (let i = 0; i < list.length; i ++) {
  list[i].addEventListener('click', function () {
    console.log(i)
  })
}

就是简单的把声明i的var改成let,这是为什么呢,这是因为ES6中新的规定let声明的是自带作用域的变量,并且ES6有块级作用域的概念,所以会把每次循环的i值锁死,自然输出的就是不一样的值啦;

有木叶的地方就会燃烧火之意志

闭包就先写到这了,如果有哪里写的不对的地方希望大家指正,我立马更改,我也不想误人子弟。写这篇博客也是为了让自己对闭包这个概念有个重新的认识,对活动对象,作用域链更清楚了,但是还有好多东西没有写,比如this指向问题,内存泄漏怎么处理,ES5是怎么模仿块级作用域的,私有变量,私有函数,模块模式,这都是一个闭包就可以延伸出来的问题,这在《JavaScript高级程序设计》第7章第4节都可以找到,静下心来看一看,我相信还是可以看明白的,一开始看不懂,就一段时间看一遍,每次肯定会有不同的理解。

对于前端来说,追求时髦的技术固然没错,毕竟时髦的技术有着先进的思想,他能告诉你应该怎样编程。但是在编程之前,还是需要学好基础,基础弄明白了,肯定会对js,前端,有一个全新的认识。


我是前端战五渣,一个前端界的小学生。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容