导读
- 了解 var、let、const
作用域
javascript
中大部分情况下,只有两种作用域类型:
- 全局作用域:全局作用域为程序的最外层作用域,一直存在。
- 函数作用域:函数作用域只有函数被定义时才会创建,包含在父级函数作用域 / 全局作用域内。
/* 全局作用域开始 */
var a = 1;
function func () { /* func 函数作用域开始 */
var a = 2;
console.log(a);
} /* func 函数作用域结束 */
func(); // => 2
console.log(a); // => 1
/* 全局作用域结束 */
作用域链
// 1. 全局作用域
function foo(a) {
// 2. foo 函数作用域
var b = a * 2;
function bar(c) {
// 3. bar 函数作用域
console.log(a, b, c);
}
bar(b * 3);
}
foo(2); // 2 4 12
当函数执行访问变量时,会先查找本地作用域,如果找到目标变量即刻返回,否则就会去父级作用域继续查找...一直找到全局作用域。我们把这种作用域的嵌套机制成为“作用域链”
作用域的应用场景
作用域的一个常见运用场景之一,就是 模块化,模块化的作用就是处理全局作用域污染和变量名冲突,代码结构臃肿且复用性不高等问题。
function module1 () {
var a = 1;
console.log(a);
}
function module2 () {
var a = 2;
console.log(a);
}
module1(); // => 1
module2(); // => 2
上面的代码中,构建了
module1
和module2
两个代表模块的函数,两个函数内分别定义了一个同名变量a
,由于函数作用域的隔离性质,这两个变量被保存在不同的作用域中(不嵌套),JS 引擎在执行这两个函数时会去不同的作用域中读取,并且外部作用域无法访问到函数内部的a
变量。这样一来就巧妙地解决了 全局作用域污染 和 变量名冲突 的问题;并且,由于函数的包裹写法,这种方式看起来封装性好多了。
然而上面的函数声明式写法,看起来还是有些冗余,更重要的是,module1
和 module2
的函数名本身就已经对全局作用域造成了污染。
// 自执行函数实现模块化
// module1.js
(function () {
var a = 1;
console.log(a); // 1
})();
// module2.js
(function () {
var a = 2;
console.log(a); // 2
})();
自执行函数本质上是通过函数作用域解决了命名冲突、污染全局作用域的问题。
闭包
简单来说,闭包就是函数内部定义的函数,被返回了出去并在外部调用, 它绕过了作用域的监管机制,从函数外部也能获取到函数内部作用域的信息。
function foo() {
var a = 1;
function bar() {
console.log(a); // 1
}
return bar;
}
var baz = foo();
baz(); // 这就形成了一个闭包
我们可以简单剖析一下上面代码的运行流程:
- 编译阶段,变量和函数被声明,作用域即被确定。
- 运行函数
foo()
,此时会创建一个foo
函数的执行上下文,执行上下文内部存储了foo
中声明的所有变量函数信息。 - 函数
foo
运行完毕,将内部函数bar
的引用赋值给外部的变量baz
,此时baz
指针指向的还是bar
,因此哪怕它位于foo
作用域之外,它还是能够获取到foo
的内部变量。 baz
在外部被执行,baz
的内部可执行代码console.log
向作用域请求获取a
变量,本地作用域没有找到,继续请求父级作用域,找到了foo
中的a
变量,返回给console.log
,打印出1
。
闭包作用举例
1、累加器:
- count作为一个全局变量,其他地方都可以对它进行操作,如果其他地方对count重新赋值或者重新定义count,那么这个计时器就被破坏了。
var count = 0;
function addCount() {
count++;
}
document.body.addEventListener("click", addCount);
- 使用闭包实现
function addCount() {
var count = 0;
var addCount = function() {
count++;
}
return addCount;
}
document.body.addEventListener("click", addCount);
// 点击一次->输出1
// 点击两次->输出2
2、缓存:
function eater () {
let food = "apple";
const obj = {
eat: function () {
if (food != "") {
console.log("i am eating "+ food);
food = "";
} else {
console.log("eat nothing");
}
},
push: function (myFood) {
food = myFood;
}
};
return obj;
};
const kevin = eater();
kevin.eat(); // i am eating apple
kevin.eat(); // eat nothing
kevin.push('banana');
kevin.eat(); // i am eating banana
function isFirstLoad () {
const list = [];
return function (option) {
if(list.indexOf(option) >= 0) {
// 检测是否存在于现有数组中,有则说明已存在
console.log('已存在')
} else {
list.push(option);
console.log('首次传入');
// 没有则返回true,并把这次的数据录入进去
}
}
}
var ifl = isFirstLoad();
ifl("kevin"); // 首次传入
ifl("leslie"); // 首次传入
ifl("kevin"); // 已存在
使用闭包的注意事项
闭包是一把双刃剑,当内部函数被保存到外部时,将会生成闭包。 闭包会导致原有作用域链不释放,造成内存泄漏(内存占用)
解决:能不用闭包就不用,及时释放。fn = null; --> 让内部函数成为垃圾对象 -->回收闭包
小结
希望看完本篇文章能对你有如下帮助:
- 了解闭包,并合理利用闭包,后面再举个例子体会一下(防抖与节流)。
- 深入理解作用域与作用域链。
文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。