作用域 & 闭包
作用域
作用域(
scope
)是指在程序中能够访问变量、函数的区域。JavaScript
中有全局作用域、模块作用域、函数作用域3种。
全局作用域:在整个程序中都可以访问的变量,它们在程序开始时就被创建,在程序结束时才被销毁。
模块作用域:模块模式中运行代码的作用域。
函数作用域:在一个函数内部声明的变量,只能在函数内部访问,而在函数外面是不能访问的。当函数执行完毕后,函数内部的变量会被销毁。
function exampleFunction() {
const x = "declared inside function"; // x 只能在 exampleFunction 函数中使用
console.log("Inside function");
console.log(x);
}
console.log(x); // 报错
块级作用域:用一对花括号(一个代码块)创建出来的作用域。使用 let和const声明的变量是有块级作用域的。块级作用域只对 let 和 const 声明有效,对 var 声明无效。
{
var x = 1;
}
console.log(x); // 1
{
const x = 1;
}
console.log(x); // ReferenceError: x is not defined
词法作用域
词法作用域(
lexical scope
)是指在编写程序时,变量和函数的作用域是通过它们在代码中声明的位置来确定的。
这意味着,函数可以访问在其外部定义的变量和函数,但在其内部定义的变量和函数不能被外部访问。这是因为 JavaScript
引擎识别变量和函数的作用域是根据它们在代码中的位置来决定的,而不是根据它们被调用的位置。
var 变量提升(hoisting)
在 JavaScript 中,
var
关键字会进行预解析(hoisting)
,这意味着在代码执行之前,所有使用var
定义的变量都会被提升(hoist)到当前作用域的顶部,而变量的赋值操作则会留在原处。(⚠️let
声明的变量可以形成一个块级作用域,不会有提升问题!)
看下面这个例子:
function foo() {
console.log(a); // undefined
a = 100;
var a;
console.log(a); // 100
}
foo();
// 结果: undefined 100
逐行解析:
先看 foo
函数整体内容,发现有一行 var a
,它会预解析,这一样会提升到当前作用域顶部,即先定义了 a
变量,但是没有赋值。
所以第一行 console.log(a)
执行,输出 undefined
;
a = 100
这一行相当于给变量 a
赋值,foo
函数作用域内,a=100
;
最后一行 console.log(a)
;访问的是 foo
函数作用域内的 a
变量,它已经被赋值为 100,因此输出 100;
变量同名时考虑作用域
全局作用域
和局部作用域
的变量同名时:全局变量是不会作用于同名的局部变量的作用域的,也就是说,同名的局部变量所处的作用域访问不到同名的全局变量。 先看一个例子:
var a = 10;
var obj = {
a: 99,
f: foo,
};
function foo() {
console.log(a); // undefined
a = 100;
console.log(this.a); // 99
var a;
console.log(a); // 100
}
obj.f();
输出:undefined 99 100
逐行解析:
先看 foo
函数整体内容,发现有一行 var a
,它会预解析,这一样会提升到当前作用域顶部,即先定义了 a
变量,但是没有赋值。
所以第一行 console.log(a)
执行,输出 undefined
;
a = 100
这一行相当于给变量 a
赋值,foo
函数作用域内,a=100
;
console.log(this.a)
这一行考察 this
的指向问题,foo
实际是由 obj
调用,因此 this
指向调用者 obj
,那么 this.a == obj.a == 99
,因此输出 99;
最后一行 console.log(a)
;访问的是 foo
函数作用域内的 a
变量,它已经被赋值为 100,因此输出 100;
全局变量与局部变量同名情况对比
var a = 10;
function test1() {
console.log(a); // undefined 输出尚未赋值的局部变量
a = 100; // 局部变量 a 赋值
console.log(this.a); // 10 this.a = window.a
var a; //提升到 test1 函数首行,定义局部变量 a
console.log(a); // 100 访问局部变量 a
}
test1();
var a = 10;
function test2() {
console.log(a); // 10 输出全局 a
a = 100; // 修改全局 a
console.log(this.a); // 100 this.a = window.a
}
test2();
美团面试题:预解析、作用域
var a = 10;
function f1() {
var b = 2 * a;
var a = 20;
var c = a + 1;
console.log(b); // NaN
console.log(c); // 21
}
解析:
- 代码块预解析
var a; // 定义全局变量a
function fn1() {} // 整个函数,并未调用
- f1 函数 预解析 (全局变量和局部变量是否同名)
var b;
var a; // 和全局变量a同名,因此在f1函数里只能访问到局部变量a
var c;
- f1 函数 逐行解析
b = 2 * a; // 此时a定义了但并未赋值,a=undefined,2 * undefined = NaN
a = 20; // 开始给a赋值
c = a + 1; // 20 + 1
console.log(b); // NaN
console.log(c); // 21
闭包
闭包(
closure
)是指函数能够访问其词法作用域之外的变量,即便在函数被调用后仍然可以访问。
- 特点:既能持久保存局部变量(函数调用后依然能够访问到),又能不造成全局污染;
- 作用:闭包可以用来创建一些类似于私有变量和方法的功能,以及实现一些高阶函数,如柯里化等;
- 缺点:过多的闭包使用可能会导致性能问题和内存泄漏的风险,应该谨慎使用。特别是在循环语句中,不能滥用闭包。
先看一个计数器例子:
function f1() {
var a = 0;
return function () {
a++;
console.log(a);
};
}
var fn = f1(); // 将 f1()执行的结果(函数)赋值给了 fn,fn 就对 a 形成了持续引用,造成 a 无法销毁。
fn(); //1
fn(); //2
f1
返回的结果就是闭包,它是一个函数,并且这个函数内部对父函数的局部变量存在引用,就形成了闭包的包含关系。闭包函数多次调用时,它内部应用的父函数的局部变量不会被销毁。
var fn1 = f1();
fn1(); //1
fn1(); //2
var fn2 = f1();
fn2(); //1
fn2(); //2
上面的 fn2
不会影响 fn1
内部引用的局部变量的值。比如 vue2
里 data
的写法
new Vue({
el:'app',
data:{
a:1
}
})
data(){
return {
a:1
}
}
以下写法不能形成闭包,未形成引用捆绑关系。
function f1() {
var a = 0;
return function () {
a++;
console.log(a);
};
}
f1()(); // 1
f1()(); // 1
以下是一个闭包滥用的例子:
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i); // 输出的都是 10
}, 1000);
}
如何解决闭包在循环中的问题?
- 用
let
声明一个块级作用域,每次循环都创建一个新的i
,避免再在闭包中共享同一个变量;
for (let i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i); // 输出 0 到 9
}, 1000);
}
- 使用一个立即执行函数表达式(
IIFE
)来创建一个块级作用域
for (var i = 0; i < 10; i++) {
(function (j) {
setTimeout(function () {
console.log(j); // 输出 0 到 9
}, 1000);
})(i); // 这个函数会立即执行并创建一个新的作用域,把变量 i 的值传递给参数 j,从而避免在闭包中共享同一个变量
}