您的位置:首页 > Web前端 > JavaScript

深入学习js之浅谈作用域(作用域闭包)

2017-08-12 00:46 453 查看
初学js总会对闭包产生疑惑,其实闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包。

1.闭包的概念

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

下面我们看一段代码,清晰地展示闭包

function foo() {
var a = 2;

function bar() {
console.log(a);
}

return bar;
}

var baz = foo();

baz();//2 ----- 这就是闭包效果


解析:

函数bar()的词法作用域能够访问foo()的内部作用域。然后我们将bar()函数本身当作一个值类型进行传递。在这个例子中,我们将bar所引用的函数对象本身当作返回值。

在foo()执行后,其返回值(也就是内部的bar()函数)赋值给变量baz并调用baz(),实际上只是通过不同的标识符引用调用了内部的函数bar()。

bar()显然可以被正常地执行。但是在这个例子中,它在定义的语法作用域以外的地方执行。

在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收机制用来释放不再使用的内存空间。由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进行回收。

而闭包的神奇之处在于它可以阻止这件事的发生。事实上内部作用域依然存在,使得该作用域能够一直存活,以供bar()在之后任何时间进行引用。

bar()依然持有对该作用域的引用,而这个引用就叫做闭包。

因此,在几微妙后baz被实际调用(调用内部函数bar()),它可以访问定义时的词法作用域,因此它可以访问变量a

当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。

function foo() {
var a = 2;

function baz() {
console.log(a);//2
}

bar(baz);
}

function bar(fn) {
fn();//闭包
}
把内部函数baz传递给bar,当调用这个内部函数时(现在称作fn),它涵盖的foo()内部作用域的闭包就可以观察到了,因为它能够访问a。

间接的传递函数。

var fn;
function foo() {
var a = 2;
function baz() {
console.log(a);
}

fn = baz;//将baz分配给全局变量
}
function bar() {
fn();//闭包
}

foo();

bar();//2
其他闭包栗子:

function wait(message) {

setTimeout( function timer() {
console.log(message);
}, 1000 );
}

wait("Hello, closure");


将一个内部函数(名为timer)传递给setTimeout(..)。timer具有涵盖wait(..)作用域的闭包,因此还保有对变量message的引用。

wait(..)执行1秒后,它的内部作用域不会消失,timer函数依然保有wait(..)作用域的闭包。

在引擎内部,内置的工具函数setTimeout(..)持有对一个参数的引用,这个参数也许叫作fn或者fnc,或其他名字。引起会调用这个函数,在例子中就是内部的timer函数,而词法作用域在这个过程中保持完整。

这就是闭包

在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是使用闭包。

2.循环和闭包

for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000 );
}


预期这段代码分别输出1-5,每秒一次,每次一个

事实上这段代码的输出是每秒一次输出五次6
为什么?

首先解释6是从哪来的。这个循环的终止条件是i不在<=5。条件首次成立时i的值是6。因此,输出显示的是循环结束时,i的最终值。

那么代码中到底有什么缺陷导致它与期不一致?

缺陷是我们试图假设循环中每个迭代在运行时都会给自己捕获一个i的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,
c0ca
但是他们都被封闭在一个共享的全局作用域中,因此实际上只有一个i,所以所有函数共享一个i引用。

因此我们需要更多的闭包作用域,特别是在循环过程中每个迭代都需要一个闭包作用域。

前面的文章说过IIFE会通过声明并立即执行一个函数来创建作用域

for (var i = 1; i <= 5; i++) {
(function () {
setTimeout(function timer() {
console.log(i);
}, i * 1000 );
})();
}


以上代码还不行,因为此时IIFE只是个空作用域

for (var i = 1; i <= 5; i++) {
(function () {
var j = i;
setTimeout(function timer() {
console.log(i);
}, j * 1000 );
})();
}


上述代码可行,但还可以改进

for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function timer() {
console.log(i);
}, j * 1000 );
})(i);
}


在迭代内使用IIFE会为每个迭代生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值得变量供我们访问。

3.块作用域

前面介绍了let声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。

这里注意一点for循环头部的let声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

for (let i = 1; i < 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000 );
}


4.模块模式

function CoolModule() {
var something = "cool";
var another = [1,2,3];

function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join(" ! "));
}

return {
doSomething: doSomething,
doAnother: doAnother
};
}

var foo = CoolModule();
foo.doSomething();// cool
foo.doAnother();//1 ! 2 ! 3
这个模式在js中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露,这里展示的是其变体。

首先CoolModule()只是一个函数,必须要通过调用它来创建一个模块实例。如果不执行外部函数,内部作用域和闭包都无法被创建。

其次,CoolModule()返回一个对象字面量语法{key:value,...}来表示的对象。这个返回的对象中含有对内部函数而不是内部数据变量的引用。我们保持内部数据变量是隐藏且私有的状态。可以将这个对象类型的返回值看作本质上是模块的公共API。

从模块中返回一个实际的对象并不是必须的,也可以直接返回一个内部函数,jQuery就是一个例子。jQuery和$标识符就是jQuery模块的公共API,但他们本身都是函数(由于函数也是对象,它们本身也可以拥有属性)

简而言之,模块模式需要具备两个条件

1.必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。

2.封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有状态。

当只需要一个实例时,可以对这个模式进行简单的改进来实现单例模式

var foo = (function CoolModule() {
var something = "cool";
var another = [1,2,3];

function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join(" ! "));
}

return {
doSomething: doSomething,
doAnother: doAnother
};
})()

foo.doSomething();// cool
foo.doAnother();//1 ! 2 ! 3
我们将模块函数转换成了IIFE,立即调用这个函数并将返回值直接赋值给单例的模块实例标识符foo。

模块模式另一个简单但强大的用法是命名将要作为公共API返回的对象

var foo = (function CoolModule(id) {
function change() {
//修改公共API
publicAPI.identify = identify2
}

function identify1() {
console.log(id);
}

function identify2() {
console.log( id.toUpperCase() );
}

var publicAPI = {
change: change,
identify: identify1
};

return publicAPI;
})( "foo module" );

foo.identify();// foo module
foo.change();
foo.identify();// FOO MODULE


现代的模块机制

大多数模块依赖加载器/管理器本质上都是将这种模块定义封装进一个友好的API。这里并不会研究某个具体的库,为了宏观了解这里介绍一些核心概念

var MyModules = (function Manager() {
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[name] = impl.apply(impl,deps);为了模块的定义引入了包装函数(可以传入任何依赖),并将返回值,也就是模块的API,储存在一个根据名字来管理的模块列表中。

下面展示了如何用它来定义模块:

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 introduce: hippo

foo.awesome(); //LET ME INTRODUCE: HIPPO
"foo"和“bar”模块都是通过返回一个公共API的函数来定义的。“foo”甚至接收“bar”的实例作为依赖函数,并能相应使用它。

模块管理器符合模块模式的两个特点:调用包装了函数定义的包装函数,并且将返回值作为该模块的API

换而言之,模块就是模块,即使在它们外层加上一个友好的包装工具也不会发生任何变化。

ES6中的模块机智

基于函数的模块并不是一个能被静态识别的模式(编译器无法识别),它们的API语法只有在运行时才会被考虑进来。因此可以在运行时修改一个模块的API(参考之前的public API)

而ES6的模块API是静态的(API不会再运行时改变)。由于编译器知道这一点,因此可以在编译器检查对导入模块的API成员的引用是否真实存在。如果API引用不存在,编译器会在编译时就抛出“早期”错误,而不会等到运行期再动态解析(并且报错)

ES6的模块没有“行内”格式,必须定义在独立的文件中(一个文件一个模块)。浏览器或引擎有一个默认的“模块加载器”(可以被重载),可以在导入模块时同步加载模块文件

bar.js
function hello(who) {
return "hello " + who;
}
export hello;
foo.js
//仅从"bar"模块导入hello()
import hello from "bar";

function awesome() {
console.log(
hello("world").toUpperCase()
);
}
export awesome;
baz.js
module foo from "foo";
module bar from "bar"

console.log(
bar.hello("world")
// hello world
);

foo.awesome();//HELLO WORLD


import可以将一个模块中的一个或多个API导入到当前作用域中,并分别绑在一个变量上(例子中的hello),module会将整个模块的API导入并绑定到一个变量上(例子中的foo和bar)

模块文件中的内容会被当作好像包含在作用域闭包中一样来处理,就和前面介绍的函数闭包模块一样。

总结:1.当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。

2.闭包是个强大的工具,可以用多种形式来实现模块等模式。

3.模块有两个主要特征:3.1为创建内部作用域而调用了一个包装函数;3.2包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。

如若有误欢迎指出!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: