JavaScript的作用域(三)函数作用域

1.什么函数中的作用域

先说结论:属于这个函数的全部变量都可以在整个函数的范围内使用及复用(这不是废话?)。
小示例

function foo(a) {
	var b = 2;
	var c = 3;
	function bar(){
		console.log(a, b, c);
	}
	bar();
}

foo的作用域中标识符a、b、c、bar,不能在外部访问直接访问它们,相当于foo把这些标识符"隐藏"起来了。
我们一般是声明一个函数,然后往里面写代码。但是反过来想一下我们把一段代码放到一个函数里面给它“包起来”,这不就把这段代码给“隐藏”起来了!芜湖!开始有点意思了。
其实这种技术是非常有用的,灵感来自最小特权原则,也叫最小授权或最小暴露原则。在软件设计中,应该最小限度的暴露必要的内容,避免都暴露在全局作用域造成混乱。
这就又引出一个问题:在所有内部嵌套作用域中访问到它们(就是前面提到的作用域链,引擎可以从最里面一层一层寻找到全局标识符)。这样就破坏了最小特权原则,暴露过多的变量或函数,而这些变量本来应该是私有的,正确的代码应该是需要阻止对这些标识符的访问的。
思考如下代码

function foo(a) {
	b = a + bar(a * 2);
	console.log(b * 3);
}
function bar(a) {
	return a - 1;
}
var b;
foo(2); // 15

上述代码中,变量b和函数bar应该是foo内部具体实现的“私有”内容。给予外部作用域对bbar的“访问权限”不仅没有必要,而且可能是危险的,因为它们可能被以非预期的方式使用。更"合理"的设计会将这些私有的内容隐藏在foo内部。

function foo(a) {
	function bar(a) {
		return a - 1;
	}
	var b = a + bar(a * 2);
	console.log(b * 3);
}
foo(2); // 15

“隐藏”作用域中的变量和函数还会带来另一个好处:避免同名标识符之间的冲突。

function foo() {
	function bar(a) {
		i = 3;
		console.log(a + i);
	}
	for(var i = 0; i < 10; i++) {
		bar(i * 2); // 这里会造成无限循环!
	}
}

分析一下:bar 内部的表达式i = 3意外的覆盖了声明在foo内部for循环中的i。这里例子中会导致无限循环,因为i被固定设置为了3永远无法满足小于10这个条件。
这时候就需要bar函数声明一个本地的变量如var i = 3; 或者干脆换一个变量名称 var j = 3;,因此就和上面呼应上了,使用作用域来“隐藏”内部声明式唯一的选择。
到这里可能还会“迷糊”,接最开始说的:属于这个函数的全部变量都可以在整个函数的范围内使用及复用。在上述"栗子"中bar中的i并没有声明在bar函数中,执行bar函数就会将i赋值为3,所以无法将其“隐藏”在函数内部,这样就造成了变量的冲突和覆盖。

2.函数作用域

上面已经知道了,在任意代码片段外部包上一个函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。虽然这种技术可以解决一些问题,但是还是会有一些问题。
首先,必须声明一个具名函数foo,意味着foo这个名称本身就“污染”了所在作用域。
其次,必须显式地通过函数名foo()调用这个函数才能运行其中的代码。
还好JavaScript提供了同时可以解决上述两个问题的方案。

var a = 2;
(function foo(){
	var a = 3;
	console.log(a); // 3
})()
console.log(a) // 2

下面来分析一下
首先,包装函数的声明以(function… 而不仅是以function…开始。尽管看上去这并不是一个很显眼的细节,但实际上却是非常重要的区别。这时的函数会被当做函数表达式而不是一个标准的函数声明来处理。
区分函数声明和表达式的方式是看function关键字出现在声明中的位置,如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。

2.1具名函数和匿名函数

就是字面的意思:具名函数就是有名字的函数,匿名函数就是没有具体名字的函数

setTimeout(function(){
	console.log("Hello World!");
}, 1000);

因为function()...没有名称标识符。函数表达式可以是匿名的,而函数声明则不可以省略函数名,匿名函数书写起来简单快捷,但是它也有几个缺点。
1.匿名函数在栈追踪中不会显示出有意义的函数名,使得调试困难。
2.如果没有函数名,当函数需要引用自身时只能使用过期arguments.callee引用,比如在递归中。还有一种是在事件触发后事件监听器需要解绑自身,没有明确的标识符就无法明确解除绑定。
3.匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的名称可以让代码不言自明。
arguments.callee是啥意思呢?

function foo(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * arguments.callee(num - 1);
    }
}

arguments.callee 引用的是当前正在执行的函数,是JavaScript的一个特性。就是自己调用自己。
所以给函数一个名分(函数名)可以有效解决上述问题。

2.2立即执行函数

var a = 2;
(function foo() {
	var a = 3;
	console.log(a); // 3
})();
console.log(a); // 2

由于一个函数被包含在一对括号内部,因此这对上述说过的函数表达式,通过在表达式末尾添加()可以立即执行这个函数
它还有另一个表现形式(function (){ ... }()),仔细观察可以发现,这种形式将执行括号()放在了包装括号里面,两种形式都是可以随便选一种即可。
这个模式另一种应用场景式为了解决undefined标识符的默认值被错误覆盖导致异常(这并不常见),将一个参数命名为undefined,但是在对应位置不传入任何值,这样就保证在代码块中的undefined标识符的值是真的undefined

undefined = true; // 绝对不能这么做
(function(undefined){
	var a;
	if(a === undefined) {
		console.log('这里的undefined是没问题的!');
	}
})()
console.log(undefined);

小结

函数是JavaScript中常见的作用域单元。本质上,声明一个在函数内部的变量或函数会在所处的作用域隐藏起来。这样做避免了标识符之间相互混淆。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值