函数作用域
- 函数作用域:属于这个函数的全部变量都可以在整个函数的范围内使用和复用(事实上在嵌套的作用域中也可以使用),在函数外部无法访问到。
- 隐藏内部实现:可以把变量和函数包裹在一个函数的作用域中,然后用这个作用域来隐藏它们。
- 为什么“隐藏”变量和函数是一个有用的技术?
–》“最小授权/最小暴露原则”,指在软件设计中,应该最小限度的暴露必要内容,而将其他内容都隐藏起来。 - 规避冲突:可以避免同名标识符之间的冲突,两个标识符可能同名但用途不一样,无意间可能造成命名冲突,而冲突会导致变量的值被意外覆盖。
- 为什么“隐藏”变量和函数是一个有用的技术?
function foo() {
function bar(a) {
i = 3; // 修改了for循环所属的作用域中i
console.log(a + 1);
}
for (var i=0; i<10; i++) {
bar(i * 2); // i被重新赋值为3,导致无限循环
}
}
foo();
- 函数声明和函数表达式
看下面一段代码:
var a = 2;
function foo () {
var a = 3;
console.log(a); // 3
}
foo();
console.log(a); // 2
在这段代码中将var a = 3;console.log(a); 外添加了包装函数,将内部的变量隐藏了起来,外部作用域无法访问包装函数内部的内容,但是这种方法并不理想:
- 必须声明一个具名函数foo(),意味着foo这个名称本身污染了所在作用域(在这个例子中是全局作用域)。
- 必须显式地通过foo()调用才能运行其中的代码。
我们期待的效果==》函数不需要函数名(或者至少函数名可以不污染所在作用域),并且能够自动运行。JavaScript提供了如下解决方案:
var a = 2;
(function foo(){
var a = 3;
console.log(a); // 3
})();
console.log(a); // 2
- 包装函数的声明以(function…而不是function…开始,这样处理,函数会被当作函数表达式而不是一个标准的函数声明来处理
(区分函数声明和表达式最简单的方法是看function关键字出现在整个声明中的位置,如果function在最前面,即为函数声明,否则为函数表达式) - 函数声明和函数表达式最重要的区别:名称和标识符绑定的位置不同。第一段代码中,foo被绑定在所在作用域中,可以直接通过foo()来调用,第二段代码中,foo被绑定在函数表达式自身的函数中而不是所在作用域中,(function foo() { 位置a })作为函数表达式意味着foo只能在位置a中被访问,外部作用域无法访问foo,foo被隐藏在自身中意味着不回非必要的污染外部作用域。
- 匿名和具名
- 对于函数表达式我们最熟悉的场景可能就是回调参数了,比如:
setTimeout(function () {
console.log(1);
},1000);
但是使用匿名函数有以下几个缺点:
- 匿名函数在追踪栈中不会显示有意义的函数名,使得调试很困难
- 如果没有函数名,当函数需要引用自身时只能使用已经过时的arguments.callee引用
setTimeout(function () {
console.log(1);
arguments.callee();
},1000);
上边这个程序是一个死循环,不可以随便运行。
3、函数省略了对于代码的可读性/可理解性很重要的函数名。函数名可以说明代码的功能。
setTimeout(function timeoutHander() {
console.log(1);
},1000);
这样写更方便阅读。
立即执行的函数表达式IIFE
var a=2;
(function IIFE(global) {
var a=3;
console.log(a);//3
console.log(global.a); //2
})(window);
使用立即执行函数表达式改变函数的执行顺序
var a=2;
(function IIFE(def) {
def(window);
console.log(1); //最后输出的1
})(function def(global) {
var a=3;
console.log(a); //3
console.log(global.a);//2
});
本来先写的console.log(1); 这句话,应该是先输出这句话,但是因为使用了立即执行的表达式所以将函数的执行的顺序弄反了,所以会先输出 2,3 再输出1
块作用域
js本身自己没有块级作用域
观察下面一段代码
for (var i=0; i<10; i++){
console.log(i);
}
我们在for循环的头部直接定义了变量i,通常是因为只想在for循环内部的上下文中使用i,而忽略了i会绑定在外部作用域(函数或全局)的事实。这就是块作用域的用处。变量的声明应该距离使用地方越近越好,并最大限度的本地化。
再看下面两个例子:
- if中的bar被绑定到了全局作用域中
var foo=true;
if(foo){
var bar=3;
console.log(bar);
}
console.log(bar);
- for中的ss被绑定到了全局作用域中(for的上一级作用域)
for(var i=0;i<5;i++){
var ss=i;
console.log(ss);
}
console.log(ss); //4
1.With
with中的a变量被绑定到了foo的上一级(全局)
function foo(obj) {
with (obj){
a=4;
}
}
var ob1={a:1};
var ob2={b:2};
foo(ob1);
foo(ob2);
console.log(ob1.a); //4
console.log(ob2.a); //undefined
console.log(a); //4
用with从对象中创建出的作用域仅在with声明中而非外部作用域中有效。
2. try/ catch
catch分句会创建一个块作用域,其中声明的变量仅在catch内部有效
try{
undefined(); // 执行一个非法操作来强制制造一个异常
}
catch(err) {
console.log(err); // 能够正常执行
}
console.log(err); // ReferenceError: err not found
3. let
let关键字:将变量绑定到任意的作用域中
var foo=true;
if(foo){
let bar=2;
console.log(bar); // 2
}
console.log(bar); //Uncaught ReferenceError: bar is not defined
let和var的功能相同但是let可以将变量绑定到任意的作用域中。换一种说法:let为其声明的变量隐式地劫持了所在的块作用域。
- 使用let声明的变量不会在块作用域中进行变量提升,声明的代码在运行之前,声明并不存在。
(1)垃圾收集
- 另一个块作用域非常有用的原因和闭包及回收内存垃圾的回收机制相关。
function process(data) {
console.log(data);
}
var bigData = { ... };
process(bigData);
var btn = document.getElementById("button");
btn.addEventListener("click", function click(evt) {
console.log("clicked!");
}, /*capturingPhase=*/false );
click 函数的点击回调并不需要 someReallyBigData 变量。理论上这意味着当 process(…) 执 行后,在内存中占用大量空间的数据结构就可以被垃圾回收了。但是,由于 click 函数形成 了一个覆盖整个作用域的闭包,JavaScript 引擎极有可能依然保存着这个结构(取决于具体 实现)。
块作用域可以打消这种顾虑,可以让引擎清楚地知道没有必要继续保someReallyBigData 了:
function process(data) {
// 在这里做点有趣的事情
}
// 在这个块中定义的内容可以销毁了!
{
let someReallyBigData = { .. };
process( someReallyBigData );
}
var btn = document.getElementById( "my_button" );
btn.addEventListener("click", function click(evt) {
console.log("button clicked");
}, /*capturingPhase=*/false );
为变量显式声明块作用域,并对变量进行本地绑定是非常有用的工具
(2)let循环
for (let i=0; i<10; i++) {
console.log( i );
}
console.log( i ); // ReferenceError
for 循环头部的 let 不仅将 i 绑定到了 for 循环的块中,事实上它将其重新绑定到了循环 的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。
4. const
同样可以创建块级作用域变量,但是这个值是固定的以后任何程序的修改都会发生错误。
var foo=true;
if(foo){
var a=2;
const b=3;
a=3;
b=4; //错误,因为不允许任何的程序对它进行修改
}
console.log(a); //2
console.log(b); //Uncaught ReferenceError: b is not defined
ES6中的关键字const可以创建块级作用域。