读书笔记-你不知道的JS上-闭包与模块
闭包定义
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
看一段最简单的闭包代码:
function foo() {
var a = 2;
//闭包
function bar() {
console.log(a);
}
return bar;
}
//理论上 foo执行完内部数据会被销毁
//由于闭包的作用 作用域一直保持
var baz = foo();
baz(); //2
bar()函数可以访问foo()的作用域,通过foo()执行后,返回bar()并当成一个值传递给baz。当baz执行时,bar()依然持有对该作用域的引用,而这个引用就叫做闭包。
在之后这个函数在词法作用域以外的地方被调用,闭包使得函数可以继续访问定义时的词法作用域。
换一个例子:
function wait(msg) {
//这里有一个隐性的赋值语句
//var msg = msg;
setTimeout(function() {
//一秒后 内部函数依然保留对外部msg的引用
console.log(msg);
}, 1000);
}
wait('Hello World');
一个经典的循环闭包例子:
for (var i = 0; i < 5; i++) {
//内部引用同一个i
setTimeout(function() {
console.log(i); // 5 5 5 5 5
}, 1000 * i)
}
如果加上IIFE:
for (var i = 0; i < 5; i++) {
(function() {
//也没有用 内部没有接受到参数 还是引用外部i
setTimeout(function() {
console.log(i); // 5 5 5 5 5
}, 1000 * i)
})(i)
}
最后,IIFE表达式加上传参,成功!
for (var i = 0; i < 5; i++) {
//每次传入一个i
(function(i) {
//隐性赋值 var i = i;
setTimeout(function() {
console.log(i); // 0 1 2 3 4
}, 1000 * i)
})(i)
}
当然,用ES6的let简直不要太简单
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000 * i)
}
模块
模块听起来很高大上,但是用一个简单的例子就可以明白什么是模块!
function module() {
var a = 'Hello World';
var b = [1, 2, 3]
//方法1
function str() {
console.log(a);
}
//方法2
function arr() {
console.log(b);
}
//模块暴露
return {
showStr: str,
showArr: arr
}
}
var fn = module();
fn.showStr(); //Hello World
fn.showArr(); //[1,2,3]
这个模式在Javascript中被称为模块。最常见的实现模块模式的方法被称为模块暴露,这里是变体。
首先,module()作为一个函数,必须通过调用才可以创建一个模块实例。如果不执行外部函数,内部的作用域和闭包都无法被创建。
其次,函数返回的对象是字面量语法。返回的对象中有对内部函数的引用,内部数据是隐藏且私有的,可以将这个对象类型的返回值看作本质上是模块的公共API。
模块创建需要具备两个必要条件:
1、 必须有外部的封闭函数,该函数必须被调用一次。(每次调用创建一个新实例)
2、 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或修改私有状态。
具有函数属性的对象本身并不是真正的模块,真正的模块必须具有闭包函数。
上例中的模块构造函数,如果变成IIFE,便是一个简单的单例模式。
var singleModule = (function() {
//...
return {
//...
}
})();
//直接使用singleModule
模块模式另一个简单但强大的变化用法是,命名将要作为公共API返回的对象:
var singleModule = (function(id1) {
//get内部数据
function id() {
console.log(id1);
}
//提供一个方法改变内部数据
function change(id2) {
id1 = id2;
}
//公共接口
var public = {
change: change,
id: id
}
return public;
})('id');
singleModule.id(); //ud
singleModule.change('id2');
singleModule.id(); //id2
通过在模块实例的内部保留对公共API对象的内部引用,可以从内部对模块实例进行修正,包括添加或删除属性。
现代模块
大多数模块依赖加载器本质上都是将这种模块定义封装进一个友好的API。
//反正我看不太懂
var module = (function() {
//将模块API储存在一个根据名字管理的模块列表中
var modules = {};
//三个参数分别为模块名、依赖项、模块内容
function define(name, deps, impl) {
for (var i = 0; i < deps.length; i++) {
//取出模块 替换掉模块名字符串
deps[i] = modules[deps[i]];
}
//核心代码
//为了模块的定义引入了包装函数
//注入模块
modules[name] = impl.apply(impl, deps);
}
function get(name) {
return modules[name];
}
return {
define: define,
get: get
};
})();
//modules={bar:{hello:hello}}
module.define('bar', [], function() {
function hello(who) {
return 'let me introduce ' + who;
}
return {
hello: hello
};
});
module.define('foo', ['bar'], function() {
var hungry = 'hippo';
function awesome() {
console.log(bar.hello(hungry).toUpperCase());
}
return {
awesome: awesome
};
});
var bar = module.get('bar');
var foo = module.get('foo');
console.log(bar.hello('hippo')); //let me introduce hippo
foo.awesome(); //LET ME INTRODUCE HIPPO
foo、bar模块都是返回一个公共API的函数来定义的。'foo'将'bar'作为依赖参数,并能使用它。
小结
闭包:当函数可以记住并访问所在词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
模块:1、为创建内部作用域而调用了一个包装函数 2、包装函数返回值必须包含一个对内部函数的引用