一起养成写作习惯!这是我参与「掘金日新计划 · 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,返回 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', '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
}
}