前言
- 闭包不是一个需要学习新语法或模块才能使用的工具。
- 闭包是基于词法作用域书写代码时,所产生的自然结果。你甚至不需要为了利用它们而有意识的创建闭包。闭包的创建和使用在你的代码中无处不在。
- 也就是说:JavaScript中的闭包无处不在,你需要做的就是识别它并拥抱它。
认识闭包
例子1:嵌套函数
function foo() {
var a = 2;
function bar() {
console.log(a)
}
bar()
}
foo()
- 基于词法作用域的查找规则,函数
bar()
可以访问外部作用域中的变量a。这是闭包吗? - 从纯学术的角度上讲,函数
bar()
具有一个涵盖foo()
作用域的闭包。 - 但是这种方式定义的闭包并不能直接观察到。
例子2:返回一个函数
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 这就是闭包的效果
foo()
执行完成后,将bar函数作为一个值,传递给baz
。baz
实际保存了对函数bar的引用。执行baz()
实际上是通过baz
这个标识对函数进行执行。
闭包阻止回收
foo()
执行后,通常会期待foo()
的整个作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。- 而闭包的神奇之处在于正是可以阻止这件事情发生。事实上内部作用域依然存在,因此没有被回收。
- 因为
bar()
,它拥有涵盖foo()
内部作用域的闭包,使得作用域能够一直存活,以供bar()
在之后任何时间进行引用。 bar()
依然保持对该作用域的引用,这个引用就叫做闭包。
总结
- 无论通过什么方式将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数,都会使用闭包。
例子3:将函数当作参数传递
function wait(message) {
setTimeout(function timer() {
console.log(message);
}, 1000);
}
wait("Hello world")
代码分析
- 将一个内部函数(名为
timer
)传递给setTimeout(...)
。timer
具有涵盖wait(...)
作用域的闭包,因此还保留对变量message
的引用。
执行过程
wait(...)
执行1000毫秒后,它的内部作用域并不会消失,timer
函数依然保有wait(...)
作用域的闭包。- 在定时器、事件监听器、Ajax请求等异步(或同步)任务中,只要使用了回到函数,实际上就是使用了闭包。
for循环中的闭包
for (var i=1; i<=5; i++) {
setTimeout(function timer() {
console.log(i);
}, i*1000);
}
代码分析
- 正常情况下,我们想要输出
1~5
,每秒一次,每次一个。但实际上,这段代码在运行时会以每秒一次的频率输出5次6。这是为什么? - 首先,解释6是怎么来的。这个循环的终止条件是
i<=5
不成立的时候,而且i
每次只增加1,所以条件成立的时候i=1
。因此输出的这个6是循环结束时i的最终值。 - 事实上,当定时器运行时,即使每个迭代中执行的时
setTimeout(.., 0)
,所有回调函数依然是在循环结束后才会被执行。 - 尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i。
那该什么修改呢?
使用立刻执行函数
for (var i=i; i<=5; i++) {
(function() {
setTimout(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 (var i=1; i<=5; i++) {
(function(j) {
setTimout(function timer() {
console.log(j);
}, j*1000);
})(i);
}
- 每次迭代,立刻执行函数都会生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭再每个迭代内部,每个迭代中都会含有一个具有准确值的变量供我们访问。
使用块作用域
- let可以用来劫持块,本质上是将块转换成一个可以被关闭的作用域。
- 在这里,每次迭代我们都需要一个块级作用域。
for (var i=1; i<=5; i++) {
let j = i; // 块级作用域
setTimeout(function timer() {
console.log(j);
}, j*1000);
}
这样写,已经可以实现功能了,但这不是全部。
- for循环头部的let声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。
所以上面的代码调整为:
for (let i=1; i<=5; i++) {
setTimeout(function timer() {
console.log(i);
}, i*1000);
}
模块
其实还有很多代码模式利用了闭包的强大威力。这里分析一个最强大的——模块。
function CoolModule() {
var something = "cool";
function doSomething() {
console.log(something)
}
return { // 返回一个字面量对象
doSomething: doSomething
}
}
var foo = CoolModule();
foo.doSomethding();
这个模式在JavaScript中被称为模块。
模块模式需要具备两个必要条件:
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有状态。
模块机制
大多数模块依赖加载器/管理器,本质上都是将这种模块定义封装进一个友好的API。
下面展示一下
// 定义模块依赖加载器
var MyModules = (function Magager() {
var modules = {};
function define(name, deps, impol) {
for (var i=0; i<deps.length; i++) {
deps[i] = modules[deps[i]];
}
modulse[name] = impl.apply(impl, deps)
}
function get(name) {
return modulse[name]
}
return {
define: define,
get: get
};
})();
// 使用模块依赖加载器
MyModules.define("bar", [], function() {
function hello(who) {
return "Let me introduce: " + who;
}
return {
hello: hello
}
});
MyModules.define("foo", ["bar"], function(bar) {
var hungry = "hippo";
function awesome() {
console.log(bar.hello(hungry).toUpperCase());
}
return {
awesome: awesome
}
})
var bar MyModules.get("bar");
var foo = Mymodules.get("foo");
console.log(bar.hello("hippo")) // Let me intruduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO
"foo"和“bar”模块都是通过一个返回公共API的函数来定义的。
ES6模块机制
考虑以下代码:
// bar.js
function hello(who) {
return "Let me introduce: " + who
}
esport hello;
// foo.js
imort hello from "bar"
var hungry = "hippo";
function awesome() {
console.log(hello(hungry).toUpperCase());
}
export awesome;
module foo from "foo";
module bar from "bar";
console.log(bar.hello("rhino")); // Let me introduce: rhino
foo.awesome(); // LET ME INTRODUCE: RHINO
import
:可以将一个模块中的一个或多个API导入到当前作用域中,并分别绑定在一个变量上。
module
:会将整个模块的API导入并绑定到一个变量上。
export
:会将当前模块的一个标识符(变量、函数)导出为公共API。
模块文件中的内容会被当作好像包含在作用域闭包中一样来处理,就和前面介绍的函数闭包模块一样。
与传统模块的区别
- 基于函数的模块并不是一个能被静态识别的模式,它们的API语义只有在运行时才能考虑进来。因此,可以在运行时修改一个模块的API。
- ES6模块API是静态的(API运行时不会改变)。
总结
结合之前了解内容,做一下总结:
闭包
- 闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
- 闭包可以当成作用域链的副产品。其实就是跨作用域使用变量。
闭包的运用思路
- 使用局部变量,同时暴露一个访问器(函数),让别人可以「间接访问」。
使用例子
- 第一种:构造函数
特点:每次new调用构造函数都会重新创建一套变量和方法。
function MyObject() {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 特权方法
this.publicMethod = function() {
privateVariable++;
return privateFunction();
};
}
- 第二种:结合原型对象
特点:私有变量在立刻执行函数中,公有方法定义在构造函数的原型上
(function() {
// 私有变量
let name = '';
// 构造函数
Person = function(value) {
name = value;
};
// 特权方法
Person.prototype.getName = function() {
return name;
};
// 特权方法
Person.prototype.setName = function(value) {
name = value;
};
})();
//调用
let person1 = new Person('Nicholas');
console.log(person1.getName()); // 'Nicholas'
person1.setName('Matt');
console.log(person1.getName()); // 'Matt'
let person2 = new Person('Michael');
console.log(person1.getName()); // 'Michael'
console.log(person2.getName()); // 'Michael'
- 第三种:模块模式
特点:创建单例对象,但是又需要私有变量(外部不能访问)
(前面有代码说明)
- 在匿名函数内部,首先定义私有变量和私有函数。
- 之后,创建一个要通过匿名函数返回的对象字面量。这个对象字面量中只包含可以公开访问的属性和方法。
内存泄漏
- 闭包在这些旧版本 IE 中可能会导致内存泄漏,因为采用的垃圾回收机制不同。
CommonJS理解
CommonJS
就是基于前面的模块模式实现的。CommonJS
模块就是对象,即在运行时加载整个模块生成一个对象,然后从这个对象上查找属性和方法
ES6模块理解
- 模块文件中的内容被当作好像包含在作用域闭包中一样来处理,就和前面介绍的函数闭包一样。
CommonJS 和 ES Module 的区别
CommonJS
-
动态(运行时加载)
在运行时加载整个模块才会生成对象,然后从这个对象上查找属性和方法。
-
拷贝
在第一次被加载时,会完整运行整个文件并输出一个对象,拷贝在内存中,模块内部的变化就影响不到这个值。重复引入的模块并不会重复执行,直接从内存中取值。
-
同步
在
CommonJS
里面,对于一个模块和它底下的依赖来说,下载,实例化,和求值都是一次性完成的,步骤相互之间没有任何停顿。如果用于浏览器,同步导入对渲染有很大影响。
ES6 Module
-
静态(编译时加载)
import
命令会被JavaScript
引擎静态分析,优先于模块内的其他内容执行。import
会自动提升到代码的顶层,不能放在块级作用域内。且不能动态导入。(文件地址不能有变量) -
引用
先获取文件,解析所有文件导出的变量,一开始变量还没有值,但是先占了内存空间。然后解析文件的导出和导出的变量,导出和导入都是指向同一片内存地址,最后把值都填入内存地址中。
-
异步
之所以说是异步,是因为模块的加载过程分成了三个步骤(下载、实例化、求值),可以分开单独完成,当然也可以一次性完成。
对模块化的理解
- 模块化不等于把一个文件拆分成多个,在同一个文件中,也能够实现模块化。比如使用短小的函数。