作用域(Scope)是编程中的一个基本概念,它指的是变量和函数在代码中的 可访问范围 。简单来说就是程序在哪个部分可以访问这个变量或函数。 作用域可以帮助控制和管理变量的生命周期,避免命名冲突。
在 Javascript 中有三种作用域:
首先需要知道:在 Javascript 中作用域可以嵌套在另一个作用域中。
var value = "1";
function printValue() {
var value2 = "2";
{
let value3 = "3";
console.log(value, value2, value3);
}
}
printValue(); // 1 2 3
在这个例子中,我们有三层作用域:
printValue
函数作用域; 当在 Javascript 中使用一个变量的时候,首先 Javascript 引擎会尝试在 当前作用域 下去寻找该变量,如果没找到,再到它的上层作用域寻找, 以此类推直到找到该变量或是已经到了全局作用域。我们称这种机制为作用域链。
举个例子:
var value = 1;
function func1() {
console.log(value);
}
function func2() {
var value = 2;
console.log(value);
func1();
}
func2();
// 打印结果:2 1
value
被初始化为 1。 func2
函数被调用,创建一个新的作用域,变量 value
被初始化为 2。 func2
函数的作用域中已经找到 value
,因此输出 2。 func1
函数被调用,查找变量 value
,由于在 func1
作用域中找不到,因此会继续向上查找作用域链,找到全局作用域。 value
被初始化为 1,因此输出 1。 闭包是指有权访问另一个函数作用域的函数,创建闭包的函数“封闭”了这个作用域,使得外部函数可以访问内部函数的变量。闭包在 JavaScript 中被广泛使用,
尤其是函数式库 React,在其中 useState
就是使用闭包实现。
举个例子:
function a() {
let a1 = 0;
function b() {
a1 += 1;
console.log(a1);
}
return b;
}
const c = a();
c(); // 1
c(); // 2
在这个例子中,每次调用 c
函数时,由于 b
函数是一个闭包,他获取了 a
函数的内部变量 a1
,并且保存在自己的作用域中。
所以每次调用 c
函数时,b
函数都会打印自增后的 a1
值。
基于这个闭包的特性,我们可以实现一些特殊功能
上文说过 useState
是闭包的一种实现,接下来我们实现一个简单的 state。
function useState(initialValue) {
let state = initialValue;
return [
() => state,
(updater) => {
if (typeof updater === "function") {
state = updater(state);
} else {
state = updater;
}
},
];
}
const [count, setCount] = useState(0);
count(); // 0
setCount(1);
getCount(); // 1
setCount((prev) => prev + 1);
getCount(); // 2
因为闭包可以记住外部函数的变量,因此可以实现一些缓存机制。
例如缓存函数,避免重复执行相同的函数:
function useFunctionCache() {
const cache = new Map();
return function (fn) {
return function (...args) {
const key = `${fn.name}(${JSON.stringify(args)})`;
if (cache.has(key)) {
console.log("Returning cached result for:", key);
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
console.log("Caching result for:", key);
return result;
};
};
}
const cachedFunction = useFunctionCache();
function add(a, b) {
return a + b;
}
const cachedAdd = cachedFunction(add);
cachedAdd(1, 2); // Caching result for: add(1,2)
cachedAdd(1, 2); // Returning cached result for: add(1,2)
经常写后端的小伙伴应该很熟悉私有变量的概念,在很多语言中都有私有变量。但是 JavaScript 本身不直接支持传统的私有变量
(尽管 ES2022 引入了 #
符号来表示私有变量,但终归只是语法糖),我们也能用闭包来模拟私有变量。
function createPerson(name, age) {
let _name = name;
let _age = age;
return {
getName: function () {
return _name;
},
getAge: function () {
return _age;
},
setName: function (name) {
_name = name;
},
setAge: function (age) {
_age = age;
},
};
}
const person = createPerson("John", 18);
console.log(person.getName()); // John
console.log(person.getAge()); // 18
// 无法直接访问私有变量
console.log(person._name); // undefined
闭包本身并不会直接导致内存泄漏,但是如果使用不当,闭包可能会导致内存泄漏。在闭包的情况下,内存泄漏通常发生在以下几种情况:
因此为了减少闭包导致的泄漏问题,在使用闭包时应该注意:
最后我们再来聊聊与作用域有关的另一个概念——变量提升(Hoisting)。
JavaScript 有一个特性,既在代码执行前,声明的变量和函数会被提升到其作用域的顶部。
console.log(a); // undefined
var a = 1;
console.log(a); // 1
在这个例子中,执行并不会报错,尽管在变量 a
被声明前我们已经尝试调用它,这是因为 JavaScript 引擎在编译代码时,提升了 a
变量的声明,
但是并不会提升变量的赋值,所以 log 输出 undefined
。同理,函数声明也会被提升到作用域的顶部。
foo(); // 输出 "hello"
function foo() {
console.log("hello");
}
上文说过声明的变量和函数会被提升到其作用域的顶部,这变量也包括 let 和 const 吗?很多人认为不会,因为在实际使用过程中表现形式是这样的:
console.log(a); // ReferenceError: a is not defined
let a = 1;
代码抛出错误的原因并不是因为 let 没有提升,虽然提升并没有被定义在规范里,但概念上,let 和 const 也存在提升行为,不过存在一些区别。
暂时性死区(Temporal Dead Zone,简称 TDZ),在这个区间内,变量不能被访问,这就是为什么在声明前不能访问 let 和 const 变量的原因。