这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战
作用域、闭包这两个词大家应该都不陌生,今天请跟随我的脚步来进一步了解他们。
什么是作用域
将变量引入程序会引起几个很有意思的问题:
- 这些变量住在哪里?
- 他们储存在哪里?
- 程序需要时如何找到他们? 这些问题说明需要一套设计良好的规则来存储变量,并且之后可以方便的找到这些变量。这套规则被称为作用域。 几乎所有的编程语言最基本的功能之一,就是能够储存变量中的值,并且能在之后对这个值进行访问和修改。事实上,正是这种储存和访问变量的值的能力将状态带给了程序。 作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和声明周期。
作用域工作模型
作用域有两种工作模型,分别是:词法作用域、动态作用域 词法作用域:函数的作用域在函数定义的时候决定,无论函数在哪里被调用,也无论他如何被调用,他的词法作用域都只由函数声明时所处的位置决定; 动态作用域:函数的作用域在函数调用的时候决定;
var value = 1;
function fun() {
console.log(value);
}
function bar() {
var value = 2;
fun();
}
bar();
// 结果是 1
// 【1】如果处于词法作用域,变量value首先在fun()函数中查找,没有找到。于是顺着作用域链到全局作用域中查找,找到并赋值为1。
// 【2】如果处于动态作用域,同样地,变量value首先在fun()中查找,没有找到。这里会顺着调用栈在调用fun()函数的地方,也就是bar()函数中查找,找到并赋值为2。
作用域分类
全局作用域:全局代表了整个文档document,变量或函数在函数外面声明;
局部作用域:变量在函数内声明,变量为局部作用域;
块级作用域:是一个语句,将多个操作封装在一起,通常是放在一个大括号里,没有返回值。
闭包的出现
声明式编程:不关注对象的实现细节,只关注在执行过程中计算机应该做什么;
// 声明式编程 告知机器做什么,隐藏具体的实现细节
// HTML
<div>
<p>Declarative Programming</p>
</div>
// sql
select * from studens where firstName = 'declarative';
命令式编程:关注实现的细节,主要思想是关注计算机执行的步骤,即一步一步告诉计算机先做什么再做什么。
//命令式编程,关注实现过程
let arr2 = [1, 2, 3, 4, 5];
var result2 = [];
for(let i=0, len = arr2.length; i < len; i++) {
let cacheItem = arr2[i];
if( cacheItem % 2 == 0 ) {
result2.push(cacheItem);
}
}
函数式编程:是声明式编程的一部分,函数式编程最重要的特点是“函数第一位”,即函数可以出现在任何地方。
//函数式编程,纯函数实现
var arr = [1, 2, 3, 4]
function findOdd(arr) {
return findOddHelper(arr, [])
}
function findOddHelper(arr, result) {
if (isNull(arr) || isNull(car(arr))) {
return result
} else if (car(arr) % 2 === 0) {
return findOddHelper(cdr(arr), cons(car(arr), result))
} else {
return findOddHelper(cdr(arr), result)
}
}
function isNull(obj) {
return obj === undefined
}
function car(arr) {
return arr[0]
}
function cdr(arr) {
return arr.splice(1)
}
function cons(item, arr) {
return [item, ...arr]
}
console.log(findOdd(arr));
闭包最早出现在函数式编程中,后来被很多编程语言借鉴。
为什么出现闭包
Q:如果想在全局环境中访问函数内部的变量? A:可以使用闭包。
Q:通过对象将想要访问的变量输出就可以做到? A:其实,闭包的另一个目的是让闭包中引用的变量始终保存在内存中。
来看看这个例子吧
// 通过返回对象访问函数内部的变量
function f1() {
var n = 10;
var add = function() {
n++;
}
return {
n: n,
add: add
}
}
var result = f1();
console.log(result);
// 使用闭包
function f2() {
var n = 10;
var add = function() {
n++;
}
return function() {
return {
n: n,
add: add
}
}
}
var result4 = f2();
console.log(result4())
result4().add()
console.log(result4());
什么是闭包
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这就产生了闭包。
function f3(){
var a = 999;
function f2(){
console.log(a);
}
return f2; // f1返回了f2的引用
}
var result5 = f3(); // result就是f2函数了
result5(); // 执行result,全局作用域下没有a的定义,
//但是函数闭包,能够把定义函数的时候的作用域一起记住,输出999
在 JavaScript 中,所有函数都是天生闭包的。(只有一个例外,new Function())
// 所有函数都是闭包?
function outer(x){
function inner(y){
console.log(x+y);
}
return inner;
}
var inn=outer(3);//数字3传入outer函数后,inner函数中x便会记住这个值
inn(5);//当inner函数再传入5的时候,只会对y赋值,所以最后弹出8
// 使用 new Function 创建一个函数,
// 用Function()构造函数创建一个函数时并不遵循典型的作用域,不能获取局部变量;获取的是全局环境下的
function getFunc() {
let value = "test";
let func = new Function('alert(value)');
return func;
}
getFunc()();
本质上,闭包是将函数内部和函数外部连接起来的桥梁
闭包使用场景
将函数本身当作值类型进行传递
// 将函数本身当作值类型进行传递
function foo() {
var a = 2;
function bar() {
console.log(a)
}
return bar;
}
var baz = foo();
baz();
function wait(message) {
setTimeout(function timer() {
console.log(message);
}, 1000)
}
wait('hello')
延迟函数中的回调-循环和闭包
for(var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000);
}
for(var i = 1; i <= 5; i++) {
(function() {
var j = i;
setTimeout(function timer() {
console.log(j)
}, j * 1000);
})();
}
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000)
}
模块用闭包模拟私有方法
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
闭包的优劣
优点: 1:变量长期驻扎在内存中; 2:避免全局变量的污染; 3:私有成员的存在 ;
缺点: 常驻内存,会增大内存的使用量,使用不当会造成内存泄露。
总结
你是否恍然大悟:原来在我的代码中已经到处是闭包了,现在我终于能理解他们了。
其实,无论何时何地,如果将函数当作第一级的值类型并到处传递,就会看到闭包在这些函数中的应用。
定时器/事件监听器/Ajax请求/跨窗口通信等,只要使用了回调函数,实际上就是在使用闭包。