最近做项目中的东西,发现了一些有意思的事情,说起javascript的作用域链, 或许大家都知道,但是大家究竟是否真的掌握了呢?

如果你看到了例子中的问题,那么恭喜你,你的javascript很扎实哟!

本篇有引用上一篇中的 从写自己的小脚本库说起

如果阅读代码中有疑问,可以先看上一篇。

那客官您准备好瓜子和F12,我们边看码边聊。

我们知道,javascript中作用域链是一个很重要的知识点,这里包含了查找变量,变量使用范围等比较基础的知识。

一个作用域的结束,是以函数执行完毕为标记,并将函数内部的变量,内部函数全部销毁。

那么我们来看几个例子吧。

这个例子有几种情况,为了直观我随便写了一个简单的页面,事件绑定使用之前的文章中的简陋的函数库,有疑问的童鞋,请回头翻阅之前的内容。

先贴上结构和样式。

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>demo</title>
		<style type="text/css">
		*{
			margin:0;
			padding:0;
		}
		div.warp{
			width: 280px;
			height: 80px;
			border: 1px dotted #999;
			padding: 10px;
			background: #eee;
			margin: 70px auto;
		}
		div.warp div.inner-warp {
			position: relative;
			width: 100%;
			height: 100%;
		}

		div.inner-warp div.controls {
			border: 1px dotted #999;
			background: #eee;
			position: absolute;
			padding-left: 20px;
			top: -30px;
			left: -11px;
			width: 100%;
			height: 22px;
			font-size: 12px;
			line-height: 20px;
		}

		div.controls label.radio-mode {
			margin: 2px 16px 0 0;
			float: left;

		}
		div.controls input[type=radio] {
			float: left;
			height: 12px;
			margin: 4px 4px 0 0;
		}

		div.warp textarea#tarTxt{
			width: 200px;
			height: 100%;
			resize: none;
			float: left;
		}

		div.warp button#test-me {
			height: 40px;
			width: 68px;
			float: left;
			margin-left: 8px;
		}
		div.warp button#reset-me {
			height: 40px;
			width: 68px;
			float: left;
			margin-left: 8px;
		}
		</style>
	</head>
	<body>

		<div class="warp">
			<div class="inner-warp">
				<div class="controls">
					<label class="radio-mode">
						<input type="radio" name="radio-mode" data-action="A" checked="true">壹
					</label>
					<label class="radio-mode">
						<input type="radio" name="radio-mode" data-action="B">贰
					</label>
					<label class="radio-mode">
						<input type="radio" name="radio-mode" data-action="C">叁
					</label>
					<label class="radio-mode">
						<input type="radio" name="radio-mode" data-action="D">肆
					</label>
					<label class="radio-mode">
						<input type="radio" name="radio-mode" data-action="E">伍
					</label>
					<label class="radio-mode">
						<input type="radio" name="radio-mode" data-action="F">陆
					</label>
				</div>

				<textarea id="tarTxt" cols="30" rows="10"></textarea>

				<button id="test-me">初始化</button>
				<button id="reset-me">重置</button>

			</div>
		</div>
	</body>
</html>

应该还是比较清晰的,会出现一个模块,模块中一个包含一堆radio,一个是textarea和两个按钮。 如果你愿意预览的话,可以到这里去预览:http://thecdn.sinaapp.com/page/demo/scope-chain/

我们继续看页面业务代码:

这里为了直观,没有把6种情况的业务使用switch揉在一起。

<script type="text/javascript">
(function() {

	// 引用之前文章中的迷你脚本库
	// 页面加载的初始化 
	var testBtn = $('test-me');
	var resetBtn = $('reset-me');
	resetBtn.bind('click', function() {
		testBtn.unbind();
		testBtn.bind('click', init);
		testBtn.text('初始化');
		$('tarTxt').text('初始化完毕.');
	});

	// 全局初始化函数
	var init = function() {
			$('tarTxt').text('初始化完毕.');
			// 初始化函数下的局部变量
			var counter = 0;
			// 第一级函数
			var LA1 = function(params) {

					counter++;
					console.log('L1:', params, counter);
					// 第二级函数
					var LA2 = function(params) {

							counter++;
							console.log('L2:', params, counter);
							// 第三级函数
							window.LA3 = function(params) {
								counter++;
								console.log('L3:', params, counter);
							}
							LA3(params)
						}
					LA2(params);
				}

			var LB1 = function(params) {
					counter++;
					console.log('L1:', params, counter);
					var LB2 = function(params) {
							counter++;
							console.log('L2:', params, counter);
							window.LB3 = function(params) {
								counter++;
								console.log('L3:', params, counter);
							}
							// 这个例子中不写CB一样的,因为我直接EVAL了
							$('test-me').ajax({
								url: 'b-ajax.js'
							})
						}
					LB2(params);
				}

			var LC1 = function(params) {
					counter++;
					console.log('L1:', params, counter);
					var LC2 = function(params) {
							counter++;
							console.log('L2:', params, counter);
							window.LC3 = function(params) {
								counter++;
								console.log('L3:', params, counter);
							}
							LC3(params);
						}
					LC2(params);
				}

			var LD1 = function(params) {
					counter++;
					console.log('L1:', params, counter);
					var LD2 = function(params) {
							counter++;
							console.log('L2:', params, counter);
							window.LD3 = function(params) {
								counter++;
								console.log('L3:', params, counter);
							}
							$('test-me').callback({
								id: 'ld',
								url: 'd-callback.js'
							})
						}
					LD2(params);
				}
			var LE1 = function(params) {
					counter++;
					console.log('L1:', params, counter);
					var LE2 = function(params) {
							counter++;
							console.log('L2:', params, counter);
							window.LE3 = function(params) {
								counter++;
								console.log('L3:', params, counter);
							}
						}
					LE2(params);
				}
			var LF1 = function(params) {
					counter++;
					console.log('L1:', params, counter);
					var LF2 = function(params) {
							if(params.indexOf('AJAX') !== -1) {
								eval(params)
							} else {
								counter++;
								console.log('L2:', params, counter);
								window.LF3 = function(params) {
									counter++;
									console.log('L3:', params, counter);
								}

							}
						}
					LF2(params);
					$('test-me').ajax({
						callback: LF2,
						url: 'f-ajax.js'
					})
				}
			var getMode = function() {
					var mode = document.getElementsByName('radio-mode');
					for(var oo in mode) {
						if(mode[oo].getAttribute && mode[oo].checked) {
							return mode[oo].getAttribute('data-action');
						}
					}
				}

			testBtn.unbind();
			var mode = getMode();
			switch(mode) {
			case 'A':

				testBtn.bind('click', function() {
					LA1('A');
					if(LA3) {
						LA3('NEW CLICK: A')
					};
					$('tarTxt').text('执行完毕,请查看CONSOLE.');
				});

				break;
			case 'B':

				testBtn.bind('click', function() {
					LB1('B');
					if(LB3) {
						LB3('NEW CLICK: B')
					};
					$('tarTxt').text('执行完毕,请查看CONSOLE.');
				});

				break;
			case 'C':

				testBtn.bind('click', function() {
					LC1('C');
					if(LC3) {
						LC3('NEW CLICK: C')
					};
					$('tarTxt').text('执行完毕,请查看CONSOLE.');
					$('test-me').ajax({
						url: 'c-ajax.js'
					})
				});

				break;
			case 'D':
				testBtn.bind('click', function() {
					LD1('D');
					if(LD3) {
						LD3('NEW CLICK: D')
					};
					$('tarTxt').text('执行完毕,请查看CONSOLE.');
				});
				break;
			case 'E':
				testBtn.bind('click', function() {
					LE1('E');
					if(LE3) {
						LE3('NEW CLICK: E')
					};
					$('tarTxt').text('执行完毕,请查看CONSOLE.');
					$('test-me').callback({
						id: 'le',
						url: 'e-callback.js'
					})
				});
				break;
			case 'F':
				testBtn.bind('click', function() {
					LF1('F');
					if(LF3) {
						LF3('NEW CLICK: F')
					};
					$('tarTxt').text('执行完毕,请查看CONSOLE.');
				});
			}
			testBtn.text('进行测试');
		}

	testBtn.bind('click', init);

})('SOULTEARY');
</script>

然后是脚本ajax/callback请求返回的内容 b-ajax.js

LB3('AJAX: B');

c-ajax.js

LC3('AJAX: C');

d-callback.js

LD3('CALLBACK: D');

e-callback.js

LE3('CALLBACK: E');

f-ajax.js

LF3('AJAX: F');

接着你可以一边打开预览页面,一边跟着文中的线索去做。

接下来的描述都是在一个闭包中进行动态解除绑定和添加绑定,以排除外部函数和变量对内部的干扰。

预览页面: http://thecdn.sinaapp.com/page/demo/scope-chain/

首先选模式1, 我们点击初始化按钮,把初始化事件中的新事件绑定到按钮中。

然后点击按钮若干次,随你的意:D 文本框内显示执行完毕,请查看CONSOLE. F12查看CONSOLE,查看记录如下:

L1: A 1
L2: A 2
L3: A 3
L3: NEW CLICK: A 4
L1: A 5
L2: A 6
L3: A 7
L3: NEW CLICK: A 8 

我们返回头查看代码

// 初始化函数定义的内容

// init 下的局部变量
var counter = 0;
// 第一级函数
var LA1 = function(params) {
		counter++;
		console.log('L1:', params, counter);
		// 第二级函数
		var LA2 = function(params) {

				counter++;
				console.log('L2:', params, counter);
				// 第三级函数
				window.LA3 = function(params) {
					counter++;
					console.log('L3:', params, counter);
				}
				LA3(params)
			}
		LA2(params);
	}


// 绑定的事件
testBtn.bind('click', function() {
	LA1('A');
	if (LA3) {LA3('NEW CLICK: A')};
	$('tarTxt').text('执行完毕,请查看CONSOLE.');
});

这里LA1是init函数内部的函数,没有挂载在init的原型上或公开给window或任何全局对象,LA2同LA1,为了方便描述,以后的LA1/LB1/LC1…我们称呼为一级函数,LA2/LB2/LC2…为二级…以此类推

三级函数则挂载到window对象属性中,在点击按钮后,首先触发一级函数,接着一级函数定义并调用二级函数,二级函数定义并调用三级元素。

执行完毕后,我们再次对三级函数进行调用,发现counter依然被+1;但是例子1不是很明显,我们把差异放大了看。

操作如例子1,例子2的console情况

L1: B 1
L2: B 2
L3: NEW CLICK: B 3
XHR finished loading: "b-ajax.js?c=7744".
L3: AJAX: B 4
L1: B 5
L2: B 6
L3: NEW CLICK: B 7
XHR finished loading: "b-ajax.js?c=505521".
L3: AJAX: B 8 

我们来看代码实现

var LB1 = function(params) {
		counter++;
		console.log('L1:', params, counter);
		var LB2 = function(params) {
				counter++;
				console.log('L2:', params, counter);
				window.LB3 = function(params) {
					counter++;
					console.log('L3:', params, counter);
				}
				// 这个例子中不写CB一样的,因为我直接EVAL了
				// 这个定义在上文和之前一篇有提到
				$('test-me').ajax({url:'b-ajax.js'})
			}
		LB2(params);
	}

testBtn.bind('click', function() {
	LB1('B');
	if (LB3) {LB3('NEW CLICK: B')};
	$('tarTxt').text('执行完毕,请查看CONSOLE.');
});

这个按钮执行的事件是这样的,调用了一级函数之后,一级函数调用二级函数,二级函数定义了三级全局函数,并使用ajax方法在成功后eval调用全局三级函数。

根据执行结果,我们知道,我了个去,counter又被+1了。

如果你现在迷惑了的话,不妨继续看,不着急,我们还有4个例子。

如果你不迷惑的话,可以直接看文末或者关闭窗口了,因为再往下面看,收益也不大。

关于第三个例子,是这个样子的。

L1: C 1
L2: C 2
L3: C 3
L3: NEW CLICK: C
XHR finished loading: "c-ajax.js?c=260100".
L3: AJAX: C 5
L1: C 6
L2: C 7
L3: C 8
L3: NEW CLICK: C 9
XHR finished loading: "c-ajax.js?c=99856".
L3: AJAX: C 10 

代码实现:

var LC1 = function(params) {
		counter++;
		console.log('L1:', params, counter);
		var LC2 = function(params) {
				counter++;
				console.log('L2:', params, counter);
				window.LC3 = function(params) {
					counter++;
					console.log('L3:', params, counter);
				}
				LC3(params);
			}
		LC2(params);
	}

testBtn.bind('click', function() {
	LC1('C');
	if (LC3) {LC3('NEW CLICK: C')};
	$('tarTxt').text('执行完毕,请查看CONSOLE.');
	$('test-me').ajax({url:'c-ajax.js'})
});

事件的执行是这个样子滴:

调用一级函数后,依次定义了二级和三级函数,并逐次调用。

不同的是,按钮点击后,我们使用ajax的成功返回时的eval进行调用全局三级函数。(好累,好长,呼呼…)

一个看似不走运的结果,我们的counter依然被+1; 难道这货和十万个冷笑话中的王二一样,拥有百分百被加数值加一的武林失传已久的绝技!?

显然…不是的,我们继续往下看,还有两个例子。

第四个例子:

L1: D 1
L2: D 2
L3: NEW CLICK: D 3
L3: CALLBACK: D 4
L1: D 5
L2: D 6
L3: NEW CLICK: D 7
L3: CALLBACK: D 8 

代码在此:

var LD1 = function(params) {
		counter++;
		console.log('L1:', params, counter);
		var LD2 = function(params) {
				counter++;
				console.log('L2:', params, counter);
				window.LD3 = function(params) {
					counter++;
					console.log('L3:', params, counter);
				}
				$('test-me').callback({id:'ld', url:'d-callback.js'})
			}
		LD2(params);
	}

testBtn.bind('click', function() {
	LD1('D');
	if (LD3) {LD3('NEW CLICK: D')};
	$('tarTxt').text('执行完毕,请查看CONSOLE.');
});

二级函数在创建之后没有直接调用三级函数,而是创建了一个callback使用了外部方法来执行三级全局函数。

很不幸,我们猜错了么,这货有百分比数值被加一的绝技?! 我们继续来看吧。

第五个绝技,哦不,是第五个例子:

L1: E 1
L2: E 2
L3: NEW CLICK: E 3
L3: CALLBACK: E 4
L1: E 5
L2: E 6
L3: NEW CLICK: E 7
L3: CALLBACK: E 8 

代码实现:

var LE1 = function(params) {
		counter++;
		console.log('L1:', params, counter);
		var LE2 = function(params) {
				counter++;
				console.log('L2:', params, counter);
				window.LE3 = function(params) {
					counter++;
					console.log('L3:', params, counter);
				}
			}
		LE2(params);
	}
testBtn.bind('click', function() {
	LE1('E');
	if (LE3) {LE3('NEW CLICK: E')};
	$('tarTxt').text('执行完毕,请查看CONSOLE.');
	$('test-me').callback({id:'le', url:'e-callback.js'})
});

例子老五,定义了一级二级三级后,一级二级函数依次调用,三级函数在按钮点击中创建callback,调用全局三级函数。

OMG, 我们依然没有阻拦到counter+1的趋势,这货要是中国股票该多好,挡不住的涨势- -!

如果你看到这里还是没有想明白的话,那么,继续看完老六吧。

如果你现在中途离开,可能会获得一个错误的结论:死掉的函数,销毁的作用域链被复活了,或者其他更诡异的结论。

最后一个例子来了:

L1: F 1
L2: F 2
L3: NEW CLICK: F 3
XHR finished loading: "f-ajax.js?c=140625".
L3: AJAX: F 4
L1: F 5
L2: F 6
L3: NEW CLICK: F 7
XHR finished loading: "f-ajax.js?c=134689".
L3: AJAX: F 8 

代码如下:

var LF1 = function(params) {
		counter++;
		console.log('L1:', params, counter);
		var LF2 = function(params) {
				if(params.indexOf('AJAX') !== -1) {
					eval(params)
				} else {
					counter++;
					console.log('L2:', params, counter);
					window.LF3 = function(params) {
						counter++;
						console.log('L3:', params, counter);
					}

				}
			}
		LF2(params);
		$('test-me').ajax({callback: LF2,url:'f-ajax.js'})
	}
testBtn.bind('click', function() {
	LF1('F');
	if (LF3) {LF3('NEW CLICK: F')};
	$('tarTxt').text('执行完毕,请查看CONSOLE.');
});

这里是之前的ajax调用的扩展版本,ajax请求后,把事件传给某个苦力函数,上面的过程中,二级函数好像出镜率比较低,我们就用它了。

一级函数定义后,二级函数定义,初始化的时候,传入参数么有ajax这个关键词,于是定义全局函数三,接着按钮点击调用一级事件,一级事件链式调用二级事件,以及创建一个ajax,ajax请求回调二级函数,接着二级函数再次调用三级函数…

嗯,最后一个例子,很遗憾,counter依然没有被逆袭,还是自增了一位数。

但是结论并没有就此被扭曲,作用域链没有被复活。 执行环境,是我们的变量和函数的作用域的根本,某个环境中的函数执行完毕后,该环境中的变量和事件都会被销毁。(个别浏览器个别闭包销毁不干净例外…本文不讨论)

上面的例子非但没有把这个结论推翻,反而更加印证了上面的事实。

当定义一级函数,一级函数定义二级函数并调用后,二级函数定义全局函数的时候,整个函数的环境就被长期扩展在了window,即全局环境下。

全局环境只有在窗口关闭,或者刷新才会全部销毁. 你是不是想说,你的例子无法印证你的观点呢,说的好(如果你说了的话),今天天气不错,我刚好有一个修正版的例子,我们不妨再次运行下例子1~6.

地址在此:http://thecdn.sinaapp.com/page/demo/scope-chain2/index.html

初始化后,当你点击第一次之后,再次执行完毕是否会报以下的错误呢?

Uncaught TypeError: object is not a function

扩展讨论话题,闭包是不是也不一定是绝对安全的了呢。 我们在项目中,如果私有过程要打开,打开后,一定记得关闭,否则会有隐患。

那么,文章就此结束,有疑惑欢迎留言讨论。 作者水平略菜,有不当之处,欢迎指出,我会努力改正和改进。

插播一条广告,新浪总部-新浪云计算 目前招技术实习生。

我也是这里实习生之一,现在我要吐槽:SAE怎么能这样!大牛怎么能手把手教实习生!从业若干年的架子那里去了!怎么能不像别的地方让实习生无限看书度过实习期!怎么能给实习生项目去实战中成长!怎么能不分上下级一起玩呢!实习生租不到房子!怎么能大家帮忙转发租房信息…

想不想在实习的时候,就参与非不牛逼的项目,然后在实习简历上低调的写上SAE呢。

顺便一说,这里早餐很便宜,网速很快,前一阵42G神马下载事件的据说45分钟…具体你懂的

还有就是…你来了我偷偷告诉你,还在等神马, 大三大四的童鞋果断投递简历吧!

SAE微博招募贴:http://weibo.com/1220149481/zfRoNuDqM

之前大家帮忙转发租赁信息的帖:http://weibo.com/1220149481/zdE5p91JS

关于租房,其实很多童鞋(大牛)都帮忙找人,想办法的,现实中的SAE团队比网上更好相处!

期待和优秀你的一起吃饭,一起写码,一起进步!我们在SAE,你在那里?