函数作用域和块级作用域
一、函数中的作用域
概念: 函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(嵌套作用域中也可以使用)。
function foo(a) {
var b = a;
function bar() {
var c = 3;
console.log(b) // a的值
}
console.log(c) // c is not defined
}
隐藏内部实现: 我们将一段代码封装到一个函数内部,那么函数外部的作用域就无法访问函数内部的变量等,这时,就相当于当前函数的作用域将此代码块"隐藏" 起来。
疑问(为什么要隐藏变量或者是函数?有什么优势吗?)
大多数情况下我们都不希望变量或者函数暴露在全局作用域中,这么做虽然方便了我们在局部作用域中访问它们,但这样会破坏最小暴露原则(指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都隐藏起来),因为可能会暴露过多的变量或函数,而这些变量或函数本应该是私有的,正确的代码应该是可以阻止对这些变量或函数进行访问的!
举个栗子:
function doSomething(a) {
b = a + doSomethingElse(a * 2);
console.log(b * 3)
}
function doSomethingElse(a) {
return a - 1
}
var b
doSomething(2)
上述例子,将函数doSomethingElse和变量b都声明在了全局作用域中,这是没必要的,并且可能会存在"危险"。因为可能会被有意或无意的非预期的方式使用。跟合理的写法如下:
function doSomething (a) {
var b
function doSomethingElse(a) {
return a - 1
}
b = a + doSomethingElse(a * 2)
console.log(b * 3)
}
doSomething (2)
同样的功能,下面的方式就显得相对严谨,此时b和函数doSomethingElse在外部作用域都无法访问,最终只有被doSomething函数控制,具体内容被私有化了。
另一个"隐藏"的好处:规避冲突
“隐藏” 作用域中的变量和函数所带来的另一个好处,是可以避免同名标识符之间的冲突,两个同一标识符可能具有不同的用途,无意间可能造成命名冲突,从而导致变量的值被意外覆盖。
匿名和具名:
- 匿名:没有名称标识符的函数表达式(比如回调函数)
setTimeOut(function() {
console.log("我是被一秒后打印出来的")
}, 1000)
这里的第一个参数是一个匿名函数,函数表达式可以是匿名的,而函数声明就不可以省略函数名。具名函数和匿名函数的区别就是一个有名字一个没名字。功能都是一样的,具名函数能帮助我们快速理解函数表达式的功能。
立即执行函数表达式:
// IIFE模式
var a = 2;
(function foo() {
var a = 3;
console.log(a); // 3
})();
console.log(a) // 2
// 另一种方式
var a = 2;
(function foo() {
var a = 3;
console.log(a); // 3
}());
console.log(a) // 2
上述例子中,函数被()括起来变成了函数表达式,又通过()调用了函数。这种模式就是IIFE(立即执行函数表达式),IIFE最常见的用法是使用一个匿名函数表达式。两种方式都是同样效果,只是写法不同。
// 多么可爱的例子
var a = 2;
(function IIFE(def) {
def(window);
}) (function def(global) {
var a = 3;
console.log(a);
console.log(global.a);
});
二、块级作用域
良言: 变量的声明应该距离使用的地方越近越好,并最大限度的本地化。
一个例子:
function process(data) {
// 在这里做点有趣的事情
}
// 在此定义一个显示块级作用域
{
let someReallyBigData = {...}
process( someReallyBigData )
}
var btn = document.getElementById("my_button");
btn.addEventListener("click", function click(evt) {
console.log("button clicked");
})
使用let的for循环:
for (let i = 0; i < 10; i++) {
console.log(i);
}
console.log(i);
// ========================================================================
{
let j;
for (j=0; j<10; j++) {
let i = j; // 每个迭代重新绑定?
console.log(i);
}
}
疑惑:for循环头部的let不仅将i绑定到了for循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。(这里重新赋值有点模糊)
const关键字
也可创建块级作用域,但其值时固定不变的,也不能进行修改操作。
var foo = true
if (foo) {
var a = 2;
const b = 3;
a = 3;
b = 4;
}
console.log(a)
console.log(b)
提升
引擎会在解释JavaScript代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将他们关联起来
console.log(a)
var a = 2
//=================
var a
console.log(2)
a = 2
foo()
function foo() {
console.log(a) // undefined
var a = 2
}
函数优先:
函数声明和变量声明都会被提升,但是一个值得注意的细节是函数首先会被提升,然后才是变量
foo()
var foo
function foo() {
console.log(1)
}
foo = function() {
console.log(2)
]
//================================
function foo() {
console.log(1)
}
foo ()
foo = function() {
console.log(2)
}
重复声明,函数声明会被提前,而函数表达式赋值这种形式,会被当做变量提升来处理(若后面还有函数声明,同样也会覆盖前面的)