开发技术

《你不知道的JavaScript》读书笔记-函数作用域和块作用域(一)

我们前面讨论了,作用域包含了一系列的"气泡",每个都可以作为容器,它们包含了标识符(变量、函数)的定义。

但是究竟是什么生产了新的气泡呢?只有函数会生成气泡吗?JavaScript的其他结构可以生成作用域气泡吗?

函数中的作用域 对于这些问题,最常见的答案就是JavaScript具有基于函数的作用域,意思就是每声明一个函数都会为自己生成一个气泡,但是其他结构不会生成作用域气泡。而事实上,这并不完全正确。

考虑以下代码:

	function foo(a){
		var b=2;
		//一些代码
		
		function bar(){
			
			//..	
		}
		
		//更多代码

		var c=3;
	}

在上面代码中,foo(..)的作用域气泡中包含了标识符a、b、c和bar。不管标识符声明在作用域里面的哪个地方,这个标识符所代表的变量或者函数都属于所处作用域的气泡当中。

bar(..)和全局作用域都拥有属于自己的作用域气泡。它只包含了一个标识符:foo。

由于标识符a、b、c和bar都附属于foo()的作用域气泡,所以没有办法从foo(..)的外部对它们进行访问。换句话说,这些标识符全部都没有办法从全局作用域中访问,所以会导致下面代码出现ReferenceError错误:

	bar();//失败

	console.log(a,b,c)//失败

但是,这些标识符(a、b、c、foo和bar)在foo(..)的内部都是可以被访问的,假设bar()内部没有同名的标识符,bar()内部也可以被访问。

函数作用域的含义是:属于这个函数的全部变量都可以在整个函数的范围内使用及复用(嵌套的作用域中也可以),这种设计能充分利用JavaScript变可以根据需要改变值类型的"动态"特性。

但如果不细心处理这些可以在整个作用域范围内被访问的变量,可能会带来意想不到的问题。

隐藏内部实现

对函数的传统认知就是先声明一个函数,然后再向改函数里面添加代码。我们可以反过来看:从所写的代码中挑选出一个任意片段,然后用函数声明对它进行包装,实际上就是把这些代码隐藏起来。

实际的结果就是在这个代码片段的周围创建一个作用域气泡,也就是说这段代码重点任何声明(变量或函数),都将会绑定在这个新建的包装函数的作用域中,而不是先前所在的作用域中。也就是说,可以把变量和函数包裹在一个函数的作用域中,然后用这个作用域来隐藏它们。

为什么"隐藏"变量和函数是一个有用的技术呢?
有很多原因促成了这种基于作用域的隐藏方法。它们大都是从最小特权原则中引申过来的,也叫最小授权或者最小暴露原则。这个原则是指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都隐藏起来,比如某个模块或对象的API设计。

这个原则可以延伸到如何选择作用域来包含变量和函数。如果所有变量和函数都在全局作用域中,当然可以在说有的内部嵌套作用域中访问到它们。但是这会破坏前面提到的最小特权原则,因为可能会暴露过得的变量或函数,而这些变量或函数本应该是私有的,正确的代码应该是可以阻止对这些变量或 函数进行访问的。

如:

	function doSomething(a){
		b=a+doSomethingElse(a*2);
		console.log(b*3);
		
	}

	function doSomethingElse(a){
		return a-1;
	

	}

var b;

doSomething(2);//15


在这个代码片段中,变量b和函数doSomethingElse(..)应该是doSomething(..)内部具体实现的"私有"内容。给予外部作用域对b和doSomethingElse(..)的"访问权限"不仅没有必要,而且可能是"危险"的,因为它们可能被有意无意地以非预期的方式使用,从而导致超出了doSomething(..)的使用条件。更"合理"的设计会将这些私有的具体内容隐藏在doSomething(..)内部,例如:

	function doSomething(a){
		function doSomethingElse(a){
			return a-1;

		}

		var b;
		b=a+doSomethingElse(a*2);
		console.log(b*3);

	}

	doSomething(2);


现在,b和doSomethingElse(..)都不能被外部访问,而只可以被doSomething(..)所控制。功能性和最终效果都没有受影响,但是设计上将具体内容私有化了,设计良好的软件都会依次进行实现。

规避冲突

"隐藏"作用域中的变量和函数所带来的另一个好处,就是可以避免同名标识符之间的冲突,两个标识符可能具有相同的名字但用途不一样,无意间可能会造成命名冲突,从而导致变量的值被覆盖。

如:
	function foo(){
		function bar(a){
			i=3;//修改for循环所属作用域中的i
			console.log(a+i);
		}
		for(var i=0;i<10;i++){
			bar(i*2);//无限循环了
		}

	}

foo();

bar(..)内部的赋值表达式i=3意外地覆盖了声明在foo(..)内部for循环中的i。在这个历史中将会导致无限循环,因为i被固定设置为3,不可能大于10了。

bar(..)内部的赋值操作需要声明一个本地变量来使用,采用任何名字都可以,var i=3;就能满足这个需求(同时会为i声明一个签名提到过的"遮蔽变量")。另外一种方法是采用一个完全不同的标识符名称,比如var j=3;但是软件设计在某些情况下自然而然的要求用同样的标识符名称,因此在这种情况下使用作用域来"隐藏"内部声明是唯一的最佳选择。

全局命名空间 变量冲突的一个典型例子存在于全局作用域中。当程序中加载了多个第三方库时,如果他们没有妥善的将内部私有的函数或变量隐藏起来,就会容易引发冲突。

这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。这个对象被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴露在顶级的词法作用域中。

如:
	var MyReallyCoolLibrary={
		awesome:'stuff',
		doSomething:function(){
			//...
		},
		
		doAnotherThing:function(){
			//..
		}
	}

模块管理

另外一种避免冲突的办法和现代的模块机制很接近,就是从众多模块管理器中挑选一个来使用。使用这些工具,任何库都不需要将标识符加入到全局作用域中,而是通过依赖管理的机制将库的标识符显式地导入到另外一个特定的作用域中。

这些工具并没有可以违反词法的作用域规则的功能。它们只是利用作用域规则强制所有标识符都不能注入到共享作用域中,而是保持在私有、无冲突的作用域做,这样能够有效规避掉所有意外冲突。
Be the First to comment.

Leave a Comment

电子邮件地址不会被公开。 必填项已用*标注

40 views