本文详细解析JavaScript中的函数作用域与块作用域,解释变量提升机制及函数优先原则,并深入探讨闭包的定义、特性及其在定时器、回调函数中的应用,帮助开发者理解并避免常见的作用域陷阱。
函数作用域与IIFE
一、函数作用域
1. 函数作用域的基本概念
在JavaScript中,函数作用域意味着变量被限制在其声明的“Function”内部。属于该函数的所有变量可以在整个函数范围内使用,并且可以被复用。
function foo(a) {
var b = 2;
function bar() {
// ...
}
var c = 3;
}
bar(); // 失败
console.log( a, b, c ); // 三个全都失败
如果在“foo”函数外部尝试访问其内部的标识符,将会导致引用错误。
2. 立即执行函数表达式(IIFE)
通过在任意代码片段外部包裹一个函数,可以将内部的变量和函数定义“隐藏”起来。这样,外部作用域无法访问包装函数内部的内容,从而实现了变量的封装。
例如,像“bar”和“a”这样的标识符在外部是不可见的。这种机制有效地保护了变量,防止其被全局环境污染。
在开发插件时,经常使用立即执行函数表达式来保护内部变量不被外部干扰。
var a = 2;
(function foo() {
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
在“foo”中,第一个括号“()”将函数转换为表达式,而第二个括号“()”则执行了这个函数。
这一机制有一个专用术语:IIFE,即立即执行函数表达式(Immediately Invoked Function Expression)。
1. 进阶用法:将IIFE作为函数调用并传递参数
(function IIFE( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
})( window );
2. 另一种用途是倒置代码的执行顺序,这在CMD或AMD模块化项目中被广泛使用。
(function IIFE(factory) {
factory( window );
})(function def( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
});
块作用域与let关键字
二、块作用域
传统JavaScript不支持块级作用域。
for(var i=0; i<10; i++) {
console.log( i );
}
上述代码中的“i”变量,其行为等同于在块外部声明的情况。
var i;
for(i=0; i<10; i++) {
console.log( i );
}
然而存在例外情况:“try/catch”语句中的“catch”块就是一个块作用域。
try{
undefined(); // 执行一个非法操作来强制制造一个异常
}
catch(err) {
console.log( err ); // 能够正常执行!
}
console.log( err ); // ReferenceError: err not found
ES6引入了新的“let”关键字,改变了这一现状。“let”可以将变量绑定到任意作用域中(通常是“{ .. }”内部)。换句话说,“let”为其声明的变量隐式地创建了块级作用域。
变量提升机制
三、变量提升
函数作用域和块作用域的行为一致,可以总结为:任何在某个作用域内声明的变量,都将归属于该作用域。
1)编译与执行阶段
变量和函数的所有声明都会在代码实际执行前被优先处理。可以参考以下代码示例。
a = 2;
var a;
console.log(a);//2
这段代码在逻辑上等价于另一种写法。
var a;//定义声明是在编译阶段进行
a = 2;//赋值声明会被留在原地等待执行阶段
console.log(a);
2)函数声明优先
在提升过程中,函数声明会优先于变量声明被处理。
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};
即使“var foo”函数表达式出现在“function foo()”声明之前,它也被视为重复声明(因此被忽略),因为函数声明会被提升到普通变量声明之前。
上述代码的实际执行顺序相当于另一种形式。
function foo() {
console.log( 1 );
}
foo(); // 1
foo = function() {
console.log( 2 );
};
闭包的定义与应用
四、闭包机制
闭包是指有权访问另一个函数作用域中变量的函数。创建闭包的最常见方式是在一个函数内部创建另一个函数,通过内部函数访问外部函数的局部变量。利用闭包可以突破作用域链,将函数内部的变量和方法传递到外部。
闭包的主要特性包括:
1. 函数内部嵌套另一个函数。
2. 内部函数可以引用外部函数的参数和变量。
3. 这些参数和变量不会被垃圾回收机制回收。
1)闭包的定义
当函数能够记住并访问其所在的作用域时,就产生了闭包,即使该函数是在当前作用域之外执行的。
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)将函数作为参数传递
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)循环与闭包
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
如果在循环中直接打印,每次结果都会是6,因为延迟函数的回调会在循环结束后才执行。
根据作用域的工作原理,实际情况是:尽管循环中的五个函数是在各个迭代中分别定义的,但它们都被封闭在一个共享的全局作用域中,因此实际上只有一个“i”。
现在使用闭包来实现每次打印不同的“i”值。
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
IIFE通过声明并立即执行一个函数来创建独立的作用域。setTimeout中的回调可以记住当前的作用域,每个作用域中的参数“j”都是独立的。
以上所述是小编给大家介绍的JavaScript中最容易混淆的作用域、提升、闭包知识详解,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对本站网站的支持!