作用域
在学习闭包之前先来学习于闭包有密切关系的作用域。
在ECMAScript6之前,JavaScript只有两种作用域,全局作用域和函数作用域。如代码所示:
var scope = "global";
function foo() {
var scope = "local";
console.log(scope);
}
foo(); // local
console.log(scope); // global
函数作用域中,如果在函数中使用 var 定义一个变量,那么这个变量在函数退出后就会被销毁。同时要注意全局作用域中声明的变量不能用delete删除。(可以赋值null来删除)
很多流行语言还存在块级作用域,指在一个代码块中定义的变量,代码块一般用花括号包裹,在这之中定义的变量在代码块之外不可见。你可能以为JavaScript中也有这样的块作用域,那就来试一下。
var i = 1;
{
var i = 0;
console.log(i); // 0
}
console.log(i); // 0
结果说明ES6之前确实没有块作用域,代码其实上等同于去掉这对花括号。
var i = 1;
var i = 0;
console.log(i); // 0
console.log(i); // 0
在JavaScipt中允许用var重复声明变量,第二个声明开始其实就只是赋值操作。
借一道经常出现的题目再加深一下印象。
for (var scope = 0; scope < 5; ++scope) {
// todo
}
console.log(scope); // 5
由于缺少块级作用域不仅会导致代码块执行完毕之后变量不会被回收,而且由于定义在代码块中的变量被扩散到上一层的作用域中,会导致变量提升的副作用。如下面的代码所示:
if (!("a"in window)) {
var a = 1;
}
console.log(a); // undefined
你可能会以为输出a = 1,实际上,由于存在变量提升,a的声明放在了代码最开头,那么if语句包含的代码块就不会执行,a就不会被赋值。为了养成良好的编程习惯,建议把变量声明写在作用域的开头。
可以说var可以重复声明以及存在变量提升现象是JavaScript的一种设计缺陷。ES6中引入了let来改变减少块级作用域导致混乱的问题。与var不同的是,let声明的变量只存在其所在的代码块中。
for (let i = 0; i < 5; ++i) {
// todo
}
console.log(i); // i is not defined
let i;
for (i = 0; i < 5; ++i) {
// todo
}
console.log(i); // 5
使用let命令定义的变量,并不会产生声明前置的效果,即表示变量必须在声明后才能使用,未声明前的引用会导致错误,在语法上称之为“暂时性死区”(Temporal Dead Zone,简称TDZ)。
关于let的具体使用,建议阅读let
匿名函数
在ES6出现块作用域之前,在多人协作的项目中,为了避免定义相同的变量名而产生的异常,通常会用匿名函数划分各自的作用域。而我们经常对匿名函数和闭包两者常常混淆,而且混用,所以先再回顾一下匿名函数,对后续的理解会有好处。
我们经常看到的匿名函数是将一个函数赋给另一个变量。
函数表达式与其他表达式一样,在使用前必须先赋值。
sayHi();
var sayHi = function() {
alert('hi'); // 会报错!
}
//匿名函数
function () { //匿名函数,会报错
return 1;
}
//通过表达式自我执行
(function fun() { //封装成表达式
alert(1);
})(); //()表示执行函数,并且传参
//把匿名函数赋值给变量
var fun = function () { //将匿名函数赋给变量
return 1;
};
alert(fun()); //调用方式和函数调用相似
//函数里的匿名函数
function fun () {
return function () { //函数里的匿名函数,产生闭包
return 1;
}
}
alert(fun()()); //调用匿名函数
什么是闭包?
在编程语言中,闭包(也是词法闭包或函数闭包)是一种在具有第一类函数的语言中实现词法范围的 名称绑定的技术。
- 在操作上,闭包是将函数与环境一起存储的记录。环境是一个映射,它将函数的每个自由变量(本地使用的变量,但在封闭范围中定义)与值或引用相关联。创建闭包时绑定名称的名称。
- 与普通函数不同,闭包允许函数通过闭包的值或引用的副本来访问那些捕获的变量,即使函数在其作用域之外被调用也是如此。(来自维基百科)
其实可以直接这么理解:闭包是指有权访问另一个函数作用域中的变量的函数。
创建闭包的常见方式就是,在一个函数内部创建另一个函数。从严格意义上将,构成闭包需要有对上下文词法作用域中变量的引用,并在外部函数执行完毕时,被引用的变量并不会被垃圾回收器回收。
function outer() {
var scope = 10;
return function inner() {
scope += 10;
console.log(scope);
}
}
var scope = 100;
var fn = outer();
fn(); // 20
为什么输出的是20而不是100呢?这是因为JavaScript是基于词法(静态)作用域的语言,词法作用域的含义是在函数定义的时候就确定了作用域,而不是函数执行时再确定。即inner函数的scope来自与外部函数即outer的作用域。
那假如是再套一层外部函数:
function closure() {
var scope = 10;
return function outer() {
return function inner() {
scope += 10;
console.log(scope);
}
}
}
var scope = 100;
var fn = closure();
fn()(); // 20
结果还是一样的,这是因为无论什么时候在函数中访问一个变量的时候,就会从作用域链搜索相应名字的变量。inner()第二层函数即outer()中找不到scope这个变量的信息,则再往上一层的作用域搜索,直到找到为止。
一般来讲,函数执行完毕之后局部活动对象就会被销毁,内存仅保存全局作用域,(全局执行环境的变量对象)。但是在闭包中则不一样。
在inner()从outer()中访问后,inner()的作用域被初始化为包含outer()函数的活动对象和全局变量对象。这样inner()可以访问在outer()中的全部变量。同理,outer()函数包含closure()的全部变量。更重要的是,closure()在执行完毕之后,其活动对象不会被销毁,因为inner()函数仍然在引用outer()的活动对象,而outer()函数仍然在引用closure()的活动对象。换句话说,当closure()函数返回后,其执行环境被销毁,但是活动对象仍然保存在内存中,直到匿名函数被销毁。
接触对匿名函数的使用(以便释放内存):
function outer() {
var scope = 10;
return function inner() {
scope += 10;
console.log(scope);
}
}
var scope = 100;
var fn = outer();
fn(); // 20
fn = null;
说到这里你可能会问匿名函数和闭包到底哪里有混淆点?可能被骗了。匿名函数和闭包之间并没有什么关系,只不过很多时候在用到匿名函数解决问题的时候恰好形成了一个闭包,就导致很多人分不清楚匿名函数和闭包的关系。
闭包中的this
function outer() {
console.log('1',this); // 1 window
return function inner() {
console.log('2',this); // 2 window
}
}
var fn = outer();
fn();
为什么匿名函数(这里是inner() )没有取得其包含作用域(外部作用域)的this对象呢?
每个函数被调用时,其活动对象都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。 《Javascript高级程序设计》
而这里inner()直接搜索到了window这个活动空间,因为outer()是一个函数!!
如何获得外部作用域中的this呢?
可以把外部作用域中的this保存在闭包可以访问到的变量里。
function outer() {
console.log('1',this); // 1 window
var that = this;
return function inner() {
console.log('2',that); // 2 window
}
}
var fn = outer();
fn();
var fn = function outer() {
console.log('1',this); // 1 window
var that = this;
return function inner() {
console.log('2',that); // 2 f{...}
}
}
fn();
注意两份代码的不同点。第一份代码其活动环境仍然是window。
闭包的使用场景。
参考大神的文章:JS闭包可被利用的常见场景
(文章讲的很详细)
场景一:采用函数引用方式的setTimeout调用
场景二:将函数关联到对象的实例方法
场景三:封装相关的功能集
参考:
https://www.cnblogs.com/ttcc/p/3763437.html
维基百科
JS闭包可被利用的常见场景