📒【闭包】理解JavaScript闭包及其应用

什么是闭包?

闭包是指那些能够访问自由变量的函数。
自由变量:指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。
闭包 = 函数 + 函数能够访问的自由变量

ECMAScript中,闭包指的是

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  2. 从实践角度:以下函数才算是闭包:
  • 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
  • 在代码中引用了自由变量
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope();
foo(); //"local scope"
  • 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
  • 全局执行上下文初始化
  • 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈
  • checkscope 执行上下文初始化,创建变量对象、作用域链、this等
  • checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
  • 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈
  • f 执行上下文初始化,创建变量对象、作用域链、this等
  • f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

Q:当 f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?
A: f 执行上下文维护了一个作用域链:

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。

Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,子可以访问父作用域的变量,但是父无法访问子作用域中的变量。

  function f1(){
    var n=999;
  }
  alert(n); // error

Q:如何从外部读取内部变量?

在函数内部创建一个函数f2,并返回f2

function f1() {
  var n = 999;

  function f2() {
    console.log(n);
  }

  return f2;
}

let result = f1();
result(); // 999 

如上f2函数就是一个闭包。闭包就是可以读取其他函数内部变量的函数,也可以理解是 定义在一个函数内部的函数。 闭包是将函数内部作用域和函数外部连接起来的一座桥梁。

闭包的作用

  1. 读取函数内部的变量
  2. 让这些变量始终保持在内存中
function f1() {
  var n = 999;
  nAdd = function () {
    n += 1
  }
  function f2() {
    console.log(n);
  }
  return f2;
}
var result = f1();
result(); // 999
nAdd();
result(); // 1000

result实际是闭包f2函数,两次运行值不一样,说明函数f1的局部变量n一直保存在内存中,并没有再f1调用后被自动清除。
原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
nAdd=function(){n+=1}没有var关键字,所以nAdd是全局变量,其值是一个匿名函数,而这个匿名函数本身是一个闭包,所以可以再函数外部对函数内部的局部变量进行操作。
http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html

闭包的特点

  1. 闭包外层是一个函数(闭包是定义在函数内部的函数)
  2. 闭包内部都有函数
  3. 闭包会返回内部函数(因为闭包的目地是外部要访问内部变量)
  4. 闭包返回的函数内部不能有return this... 因为这是this已经改变
var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      var that = this;
      return function(){
        return that.name;
      };
    }
  };
  alert(object.getNameFunc()());
  1. 执行闭包后,闭包内部变量会存在,而闭包内部函数的内部变量不存在。

闭包的应用

  1. 模拟私有变量的实现

仅在对象内部生效,无法从外部触及,这样的变量,就是私有变量。

// 利用闭包生成IIFE,返回 User 类
const User = (function() {
    // 定义私有变量_password
    let _password

    class User {
        constructor (username, password) {
            // 初始化私有变量_password
            _password = password
            this.username = username
        }

       login() {
           // 这里我们增加一行 console,为了验证 login 里仍可以顺利拿到密码
           console.log(this.username, _password)
           // 使用 fetch 进行登录请求,同上,此处省略
       }
    }

    return User
})()

let user = new User('xiuyan', 'xiuyan123')

console.log(user.username) // xiuyan
console.log(user.password) // undefined
console.log(user._password) // undefined
user.login() // xiuyan xiuyan123

我们把 _password 放在了 login 方法的外层函数作用域里,并通过立即执行 User 这个函数,创造出了一个闭包的作用域环境。我们看到不管是 password,还是 _password,都被好好地保护在了 User 这个立即执行函数的内部。
看到User对外暴露的属性确实已经没有 password,通过闭包,我们成功达到了用自由变量来模拟私有变量的效果!

  1. 偏函数和柯里化
  • 柯里化:把接受 n 个参数的 1 个函数改造为只接受 1个参数的 n 个互相嵌套的函数的过程。也就是 fn (a, b, c)fn(a,b,c) 会变成 fn (a)(b)(c)fn(a)(b)(c)。
  • 偏函数:固定你函数的某一个或几个参数,然后返回一个新的函数(这个函数用于接收剩下的参数)。
function generateName(prefix) {
    return function(type, itemName) {
        return prefix + type + itemName
    }
}
// 把3个参数分两部分传入
var itemFullName = generateName('大卖网')('母婴', '奶瓶')

使用闭包的注意点

  1. 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
  2. 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

思考题

var test = (function() {
    var num = 0
    return () => {
        return num++
    }
}())
for (var i = 0; i < 10; i++) {
    test()
}
console.log(test())
function test (){
    var num = []
    var i
    for (i = 0; i < 10; i++) {
        num[i] = function () {
            console.log(i)
        }
    }

    return num[9]
}
test()()
var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}

data[0]();
data[1]();
data[2]();
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。 一、变量...
    zock阅读 1,086评论 2 6
  • ● 闭包基础 ● 闭包作用 ● 闭包经典例子 ● 闭包应用 ● 闭包缺点 ● 参考资料 1、闭包基础 作用域和作...
    lzyuan阅读 968评论 0 0
  • 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。 一、变量...
    zouCode阅读 1,288评论 0 13
  • 高考如期而至,拖着疲惫的身体回到山里等待着成绩出来,等待是煎熬的,我日夜难寐,心中恐惧与日俱增。农家的孩子从小都被...
    凉云暮雨阅读 289评论 0 1
  • 只想慢点,再慢点, 过完生活的每一分,每一秒。
    二菟阅读 203评论 4 1