闭包的应用

137 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

作用域与作用域链这篇文章中有谈到过什么是闭包?即引用了自由变量的函数都是闭包。

一道面试题

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

答案是:5 5 5 5 5

我们来想想怎么改造它,使它的结果是0 1 2 3 4

第一种思路

可以把 setTimeout 函数的第三个参数利用起来。setTimeout 从第三个入参位置开始往后,是可以传入无数个参数的,这些参数会作为回调函数的附加参数存在。

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

第二种思路

在 setTimeout 外面再套一层函数,利用这个外部函数的入参来缓存每一个循环中的 i 值:

var output = function (i) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
};

for (var i = 0; i < 5; i++) {
    // 这里的 i 被赋值给了 output 作用域内的变量 i
    output(i);  
}

闭包的应用

模拟私有变量的实现

什么是私有变量?在JS中,强调的是对象、而非类的概念。在 ES6 之前,我们生成对象实例,只有一条路,那就是用构造函数:

// 定义构造函数
function Dog(name) {
  this.name = name
}

// 挂载原型方法
Dog.prototype.showName = function() {
  console.log(this.name)
}

// 通过构造函数创建对象实例
var dog = new Dog('哈士奇')
dog.showName()

为了对标java c++等语言,ES6之后直接吸纳了模拟class的思路:

// 构造函数,相当于上一个例子中的 Dog 函数
class Dog {
    constructor(name) {
    // 构造函数的函数体内容
    this.name = name
  }
  
  showName() {
    console.log(this.name)
  }
}

// 仍然是使用 new 关键字来创建实例
let dog = new Dog('哈士奇')
dog.showName()

这模拟出来的类,仍然无法实现传统面向对象语言中的一些能力 —— 比如私有变量的定义和使用

私有变量到底是干嘛的?为啥没它不行?大家看这样一个 User 类:

class User {
    constructor(username, password) {
    // 用户名
    this.username = username
    // 密码
    this.password = password
  }
}

现在我尝试输出一下 password:

let user = new User('xiuyan', '123')
user.password

像登录密码这么关键且敏感的信息,竟然通过一个简单的属性就可以拿到!这就意味着,后面的人只要能拿到 user 这个对象,就可以非常轻松地得知、甚至改写他的密码。

像 password 这样的变量,我们希望它仅在对象内部生效,无法从外部触及,这样的变量,就是私有变量。

那么在 JS 中,既然无法通过 private 这样的关键字直接在类里声明变量的私有性,我们就只能另寻它法。这时候就轮到闭包登场了 —— 大家想想,在内部可以拿到、外部拿不到,这难道不就是我们前面讲的函数作用域的特性吗?所我们的思路就是把私有变量用函数作用域来保护起来,形成一个闭包!

/ 利用闭包生成IIFE,返回 Userconst 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', '123')
 
console.log(user.username) // xiuyan
console.log(user.password) // undefined
console.log(user._password) // undefiend
user.login() // xiuyan 123

看到它对外暴露的属性确实已经没有 password,通过闭包,我们成功达到了用自由变量来模拟私有变量的效果!

偏函数与柯里化

柯里化

柯里化是把接受 n 个参数的 1 个函数改造为只接受 1个参数的 n 个互相嵌套的函数的过程。也就是 fn (a, b, c)fn(a,b,c) 会变成 fn (a)(b)(c)fn(a)(b)(c)。

它的主要功能是:可以让函数在必要的情况下帮我们 “记住” 一部分入参

原函数

function generateName(prefix, type, itemName) {
    return prefix + type + itemName
}

经过科里化后:

function generateName(prefix) {  
    return function(type) {
        return function (itemName) {
            return prefix + type + itemName
        }    
    }
}

这样一来,原有的 generateName (prefix, type, name) 现在经过柯里化已经变成了 generateName (prefix)(type)(itemName)。

偏函数

偏函数应用相比之下就”随意"一些了。偏函数是说,固定你函数的某一个或几个参数,然后返回一个新的函数(这个函数用于接收剩下的参数)。你有 10 个入参,你可以只固定 2 个入参,然后返回一个需要 8 个入参的函数--偏函数应用是不强调"单参数"这个概念的。它的目标仅仅是把函数的入参拆解为两部分。

偏函数在动机和实现思路上都与柯里化一致--动机就是为了"记住"函数的一部分参数,实现思路就是走闭包。

偏函数应用改造:

function generateName(prefix) {
    return function(type, itemName) {
        return prefix + type + itemName
    }
}