写在前面:在一开始JS基础学习的过程中,闭包的问题也曾困扰过我,其实无论是JS、OC、还是Swift,闭包的本质都是一样的。借着最近开始学习Swift,又重新将这部分复盘整理了一下。目前对于闭包相关的理解可以说是很透彻了,发现新问题再来补充。
闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。
Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的匿名函数(Lambdas)相似。
故名思义,闭合地包起来可以完成一件事儿就是个闭包。闭包这玩意儿,只是给它取了个名字而已,全局函数其实也是个闭包。还有闭包就是自包含的函数代码块,可以在代码中被传递和使用。比如,嵌套函数,函数作为参数的情况。
全局函数 | 嵌套函数 | 闭包表达式(轻量级语法) |
有名字 | 有名字 | 匿名 |
不会捕获任何值 | 捕获其外部函数所有的参数和定义的常量和变量 | 捕获上下文中变量或常量的值 |
用来解决什么问题呢?闭包解决了函数内变量暴露给函数外访问的问题。可以缓存上级作用域,那么就使得函数外部打破了“函数作用域”的束缚。
值捕获,闭包可以在上下文中捕获常量或变量的引用。即使常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。👇这种情况就是值捕获
- // 一般情况
- // 闭包情况
- func makeIncrementer(forIncrement amount: Int) -> () -> Int {
- var runningTotal = 0
- func incrementer() -> Int {
- runningTotal += amount
- return runningTotal
- }
- return incrementer
- }
-
- let incrementByTen = makeIncrementer(forIncrement: 10)
-
- incrementByTen()
- // 返回的值为10
- incrementByTen()
- // 返回的值为20
- incrementByTen()
- // 返回的值为30
-
- let incrementBySeven = makeIncrementer(forIncrement: 7)
- incrementBySeven()
- // 返回的值为7
-
- incrementByTen()
- // 返回的值为40
- incrementBySeven()
- // 返回的值为17
接下来,我们来对👆🌰进行值捕获的解释👇
- func incrementer() -> Int {
- runningTotal += amount
- return runningTotal
- }
「👆incrementer()
函数并没有任何参数,但是在函数体内访问了 runningTotal
和 amount
变量。这是因为它从外围函数捕获了 runningTotal
和 amount
变量的引用。捕获引用保证了 runningTotal
和 amount
变量在调用完 makeIncrementer
后不会消失,并且在下一次执行 incrementer
函数时,runningTotal
依旧存在。」
函数和闭包都是引用类型,将函数或闭包赋值给一个常量还是变量,实际上是将函数或闭包的引用设置为常量或变量的值,而并非闭包内容本身。意味着如果将闭包赋值给了两个不同的常量或变量,两个值都会指向同一个闭包。继续👆例子🌰
- let alsoIncrementByTen = incrementByTen
- alsoIncrementByTen()
- // 返回的值为50
另外关于⚠️闭包引起的循环强引用问题(遇到了再说吧)
函数和返回值类型都写在大括号内
闭包的函数体部分由关键字 in 引入
- { (parameters) -> type in
- statements
- }
-
- func backward(_ s1: String, _ s2: String) -> Bool {
- return s1 > s2
- }
-
- { (s1: String, s2: String) -> Bool in
- return s1 > s2
- }
语法优化:
1.省略参数和返回值类型,可根据上下文推断出
{ s1, s2 in return s1 > s2 }
2.行表达式闭包可以通过省略 return 关键字
{ s1, s2 in s1 > s2 }
3.参数名称缩写,$0 和 $1 表示闭包中第一个和第二个参数
{ $0 > $1 }
4.运算符方法【❓这个暂时先保留,知识结构补充ing】
与 sorted(by:) 方法的参数需要的函数类型相符合。因此,你可以简单地传递一个大于号,Swift 可以自动推断找到系统自带的那个字符串函数的实现:reversedNames = names.sorted(by: >)
5.尾随闭包:
将闭包表达式写在函数圆括号之后,作为函数的最后一个参数调用。用于闭包长度非常长的情况
reversedNames = names.sorted() { $0 > $1 }
若闭包表达式是函数的唯一参数,可以把 () 省略掉
reversedNames = names.sorted { $0 > $1 }
1.逃逸闭包:当一个闭包作为一个函数的参数,且闭包在函数返回之后才被执行。定义函数时,在参数名之前标注 @escaping指明这个闭包允许“逃逸”出这个函数。
🌰不想复制粘贴,具体例子移步官方文档 逃逸闭包 - SwiftGG
相信看完文档中的🌰对这个概念已经有了大概的理解,我来浅浅总结一下。很多启动异步操作的函数接受一个闭包参数作为 completion handler。这类函数会在异步操作开始之后立刻返回但不被调用,直到异步操作结束后才会被调用。在这种情况下,闭包需要“逃逸”出函数,因为闭包需要在函数返回之后被调用。
补充:逃逸闭包需要显示引用self
项目中的🌰:实现 确认是否安装外部鱼眼镜头的 Alert弹窗
- // 这里isUpdateAvailable是VersionUtils的一个属性
-
- func isUpdateAvailable(callback: @escaping (Bool) -> Void) {
- callback(true)
- return
- }
-
- isUpdateAvailable(callback: { isNew in
- if isNew {
- let alertVC = UpdateVersionAlertVC.shared
- alertVC.modalPresentationStyle = .overCurrentContext
- alertVC.modalTransitionStyle = .crossDissolve
- self.present(UpdateVersionAlertVC.shared, animated: true)
- }
- }
-
-
- //这里 把completed赋值给了实例 alertVC的completed属性,在alertVC的一个方法中对alertVC的completed进行传参调用,此时就需要标注@escaping(闭包需要在函数返回之后被调用)
-
- extension FishShotAlertVC {
- @objc static func showFishShotAlertViewController(currentVC vc: UIViewController, completed: @escaping (_ isUseFishShot: Bool) -> Void) {
- let alertVC = FishShotAlertVC()
- alertVC.completed = completed
- alertVC.currentVc = vc
- vc.present(alertVC, animated: false, completion: {})
- }
- }
补充:
当闭包作为函数的参数,该闭包也有参数时
1.声明函数时,不考虑闭包的具体内容,{ }里面是函数体,函数体内有对闭包进行传参调用。
2.函数调用时,作为尾随闭包,{ }里面是尾随闭包
写在最后:
学习闭包经历了三个阶段,文档看懂,写的时候语法忘了,
总而言之就是,以为懂了实际没懂,遇到实际场景返回来继续复盘,和别人讨论,直到可以回答别人提出的任何相关问题,不能解释就继续去学习。这个概念我应该算是彻彻底底懂了吧。