一、函数作用域
1.函数作用域
就是作用域在一个“Function”里,属于这个函数的全部变量都可以在整个函数的范围内使用及复用。
1
2
3
4
5
6
7
8
9
|
function
foo(a) {
var
b = 2;
function
bar() {
// ...
}
var
c = 3;
}
bar();
// 失败
console.log( a, b, c );
// 三个全都失败
|
上面的“foo”函数内的几个标识符,放到函数外面访问就都会报错。
2.立即执行函数表达式
在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。
例如上面的bar、a等几个标识符。这样能够保护变量不被污染。
在写插件的时候经常会用到立即执行函数表达式,为的就是保护里面的变量。
1
2
3
4
5
6
|
var
a = 2;
(
function
foo() {
var
a = 3;
console.log( a );
// 3
})();
console.log( a );
// 2
|
“foo”中第一个( )将函数变成表达式,第二个( )执行了这个函数。
有一个专用术语:IIFE,代表立即执行函数表达式(Immediately Invoked Function Expression);
1. 进阶用法是把它们当作函数调用并传递参数进去
1
2
3
4
5
|
(
function
IIFE( global ) {
var
a = 3;
console.log( a );
// 3
console.log( global.a );
// 2
})( window );
|
2. 一种变化的用途是倒置代码的运行顺序,在CMD或AMD项目中被广泛使用。
1
2
3
4
5
6
7
|
(
function
IIFE(factory) {
factory( window );
})(
function
def( global ) {
var
a = 3;
console.log( a );
// 3
console.log( global.a );
// 2
});
|
二、块作用域
JavaScript不支持块作用域。
1
2
3
|
for
(
var
i=0; i<10; i++) {
console.log( i );
}
|
上面的代码中的“i”相当于下面的
1
2
3
4
|
var
i;
for
(i=0; i<10; i++) {
console.log( i );
}
|
但也有例外,“try/catch”,catch就是一个块作用域。
1
2
3
4
5
6
7
|
try
{
undefined();
// 执行一个非法操作来强制制造一个异常
}
catch
(err) {
console.log( err );
// 能够正常执行!
}
console.log( err );
// ReferenceError: err not found
|
ES6改变了现状,引入了新的let关键字,let关键字可以将变量绑定到所在的任意作用域中(通常是{ .. }内部)。换句话说,let为其声明的变量隐式地了所在的块作用域。
三、提升
函数作用域和块作用域的行为是一样的,可以总结为:任何声明在某个作用域内的变量,都将附属于这个作用域。
1)编译与执行
变量和函数的所有声明都会在任何代码被执行前首先被处理,可以看下面的代码事例。
1
2
3
|
a = 2;
var
a;
console.log(a);
//2
|
这段代码等价于:
1
2
3
|
var
a;
//定义声明是在编译阶段进行
a = 2;
//赋值声明会被留在原地等待执行阶段
console.log(a);
|
2)函数优先
函数会首先被提升,然后才是变量。
1
2
3
4
5
6
7
8
|
foo();
// 1
var
foo;
function
foo() {
console.log( 1 );
}
foo =
function
() {
console.log( 2 );
};
|
var foo函数表达式尽管出现在function foo()的声明之前,但它是重复的声明(因此被忽略了),因为函数声明会被提升到普通变量之前。
而上面的代码相当于:
1
2
3
4
5
6
7
|
function
foo() {
console.log( 1 );
}
foo();
// 1
foo =
function
() {
console.log( 2 );
};
|
四、闭包
闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,
通过另一个函数访问这个函数的局部变量,利用闭包可以突破作用链域,将函数内部的变量和方法传递到外部
闭包的特性:
1.函数内在嵌套函数
2.内部函数可以引用外层的参数和变量
3.参数和变量不会被垃圾回收机制回收
1)定义
当函数可以记住并访问所在的作用域时,就产生了闭包,即使函数是在当前作用域之外执行。
1
2
3
4
5
6
7
8
9
|
function
foo() {
var
a = 2;
function
bar() {
console.log( a );
}
return
bar;
}
var
baz = foo();
baz();
// 2 —— 这就是闭包的效果。
|
1. 将函数“bar”赋值给“baz”,执行“baz”,当前作用域并不在“bar”的作用域,但是可以执行。
2. 闭包还会阻止垃圾回收,当“foo”执行完后,内部作用域仍然存在。这样才能让“baz”执行。
2)将函数作为参数传递
1
2
3
4
5
6
7
8
9
10
|
function
foo() {
var
a = 2;
function
baz() {
console.log( a );
// 2
}
bar( baz );
}
function
bar(fn) {
fn();
//这就是闭包!
}
|
把内部函数baz传递给bar,当调用这个内部函数时(fn),它涵盖的foo()内部作用域的闭包就可以观察到了,因为它能够访问a。
如果将函数当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。
在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
3)循环和闭包
1
2
3
4
5
|
for
(
var
i = 1; i <= 5; i++) {
setTimeout(
function
timer() {
console.log(i);
}, i * 1000);
}
|
每次打印出来都将会是6,延迟函数的回调会在循环结束时才执行
根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i。
现在用闭包来实现每次打印不同的i。
1
2
3
4
5
6
7
|
for
(
var
i = 1; i <= 5; i++) {
(
function
(j) {
setTimeout(
function
timer() {
console.log(j);
}, j * 1000);
})(i);
}
|
IIFE会通过声明并立即执行一个函数来创建作用域。setTimeout中的回调可以记住当前的作用域,每个作用域中的参数“j”都是不同的。