经典题目谈闭包

侃侃闭包

春暖花开,又到了程序猿换领地的季节了,各大论坛出现很多的面试题和各种押题,然后我和小伙伴仔细研究,果然大部分不会。其中有一个讲zepto源码的,提到了js三座大山:闭包原型异步。我曾经入门的时候在这3个山里面饶了很久很久,而且多次以为绕出去的时候才发现我只是过了一个小山头,苦不堪言。当然,我觉得现在自己已经绕出来了,那么就先讲一讲闭包这座山的路。

经典回顾

先放一段经典的面试题

for(var i = 0; i < 10; i ++) {
	bt[i].onclick = function(){
		console.log(i);
	}
}

初次接触闭包就是这个问题,但是这样看,好像和闭包没有什么关系,而且确实没有什么关系。这个每次结果都是9已经毫无疑问了,但是为什么是9?我们慢慢拆解。

var i;
for(i = 0; i < 10; i ++) {
	bt[i].onclick = function(){
		console.log(i);
	}
}	

i作为变量被提升了,我们再把循环展开

bt[0].onclick = function(){
	console.log(i);
}
bt[1].onclick = function(){
	console.log(i);
}
bt[2].onclick = function(){
	console.log(i);
}
....


这时有人肯定和我当年有一样的疑问,为什么function里的是i,不应该对应的是当时的i的值吗?

先举个例子

var i = 10;
function test(){
	var i = 1;
	console.log(i);
}

如果i在js加载就赋值了,那么是不是就成了console.log(10),test()执行后打印的应该是10而不是1啊。很明显,解析js的时候,test这个函数内部并没有赋值,只是给它了一个变量,具体什么值等调用了才会拿到。其实拆到这的时候我也很懵,什么鬼,为什么会是这样。

执行上下文

在ES6之前是没有块级作用域这个概念的,只有全局作用域、函数作用域和eval作用域。

在浏览器加载js脚本的时候,解析器在解析js代码时,会先进入全局作用域,如果碰到函数调用,会创造函数上下文,并将这个上下文压入到执行栈中,如果这个函数内部还有另一个函数调用,那么就会把另一个函数压入到执行栈的顶部,依次类推,最后调用的函数总是被放到执行栈的最顶部,然后执行最上层的函数。

这个过程涉及到js很重要的几个点:js是单线程同步执行函数被调用的时候才会创建执行上下文

既然知道了函数调用的时候会创建执行上下文,那么就要探究下怎么执行上下文内部怎么执行的:

  • 初始化作用域链
  • 创建变量对象:创建 参数对象, 检查参数的上下文, 初始化其名称和值并创建一个引用拷贝
  • 扫描上下文中的函数声明:对于每个被发现的函数, 在 变量对象 中创建一个和函数名同名的属性,这是函数在内存中的引用
  • 扫描上下文中的变量声明:1. 对于每个被发现的变量声明,在变量对象中创建一个同名属性并初始化值为 undefined。2. 如果变量名在 变量对象 中已经存在, 什么都不做,继续扫描。
  • 确定上下文中的 "this"
  • 激活 / 代码执行阶段:执行 / 在上下文中解释函数代码,并在代码逐行执行时给变量赋值。

上面这些步骤可以分为2个阶段:初始化阶段(创建阶段)和执行阶段。

我们看个例子

var a = 100;
function fn(n) {
	var a = 10;
	const b = function(){};
	function c(o) {}
}

当在调用fn(11)时,创建阶段其实是这样的:

function fn(n) {
	a:100  //父级上下文中的变量(作用域链)
	arguments:{
		0:11,
		length:1
	}
	c:function c(o)
	a: undefined
	b: undefined
	this:{//全局是window}
}

很明显,在创建阶段只是做了属性名的定义,并没有给函数内变量赋值,全局变量和参数除外。创建阶段完成后,便开始进入执行阶段。代码执行阶段看起来是这样得:

function fn(n) {
	a:100  //父级上下文中的变量(作用域链)
	arguments:{
		0:11,
		length:1
	}
	c:function c(o)
	a: 10
	b: function()
	this:{//全局是window}
}

这就是整个执行环境。

闭包

说了这么多废(pu)话(dian),终于到正题了。红皮书是这样说的:闭包是有权访问另一个函数作用域中的变量的函数。如果熟读红皮书的话应该很好理解这句话,主要在作用域链那。当然通俗点就是内部可以访问外部,外部不能访问内部。

现在返回去看看那道经典的题目,应该很好解决了。根据执行上下文的过程,我们在外层加一个函数,并且执行它,因为初始化过程中参数是可以直接被赋值的。

for(var i = 0; i < 10; i ++) {
	(function(num){
		bt[i].onclick = function(){
			console.log(num);
		}
	})(i)
	
}

这样就ok了。

总结

闭包并不可怕,只要搞清js的一些执行机制,很多也就迎刃而解了。当时我并不理解为什么要加一个理解执行函数,后来看到很多关于作用域链和执行上下文的文章,反过来想想,也就没什么难的了。当然,闭包一个很重要的作用就是设置私有变量,其实这个题目也算一个。

这个讲的很简单,如果仔细说闭包,估计要说2小时。这个只是来理解这一个题目而已。

他的原创之处并不优秀,他的优秀之处并非原创!!!

posted @ 2018-04-08 23:49  open_wang  阅读(945)  评论(0编辑  收藏  举报