AHK v1 写到一定程度,很多人都会遇到对象和类。刚开始会觉得它们很“高级”,像是写大型软件才需要的东西。其实我更愿意把它们看成整理脚本的工具:数据多了,用对象装起来;动作和数据经常一起出现,就考虑写成类。
这篇不准备把对象系统全部讲完。真要完整讲,会牵扯元函数、基对象、枚举器、引用计数,容易越写越远。这里先讲最小够用的心法,能让你看懂大部分 v1 类库,也能开始写自己的小封装。
我建议的学习顺序
如果你刚开始学对象和类,可以按这个顺序来:
- 先用
[]和{}做装数据的容器。 - 学会
for key, value in obj遍历解析出来。 - 学会对象嵌套,比如数组里放对象。
- 再写一个只有
__New和两三个方法的小类。 - 最后再碰继承、
base、元函数这些内容。
对象先当容器用
最开始不要急着想“面向对象”。对象最朴素的用途,就是把一组相关数据放在一起。
#Requires AutoHotkey v1.1
user := {}
user.name := "张三"
user.city := "杭州"
user.score := 95
MsgBox, % user.name " / " user.city " / " user.score
这比写一堆散变量更清楚。尤其是循环处理多个人、多个窗口、多个任务时,对象能让代码少很多临时变量。
tasks := []
tasks.Push({name: "截图", status: "完成"})
tasks.Push({name: "识别", status: "等待"})
for index, task in tasks
MsgBox, % index ". " task.name " - " task.status
在 v1 里,数组和关联数组本质上都属于基础对象。[] 更适合按顺序存一组东西,{} 更适合按名字存一组东西。
点号和中括号大多是一回事
v1 里很多时候 obj.name 和 obj["name"] 指向的是同一个值。
obj := {}
obj.name := "AHK66"
MsgBox, % obj["name"]
点号适合字段名固定、写起来像属性的场景;中括号适合字段名来自变量的场景。
; 需结合上段内容 key := "name" MsgBox, % obj[key]
这一点很实用。比如你从 Excel 表头、JSON 字段、配置项里拿到一个名字,就可以动态访问对象。
什么时候需要类
如果对象只是临时装数据,普通对象就够了。只有当你发现“同一类数据总要配同一批函数”时,类才开始变得有意义。
比如一个计数器,它有自己的数值,也有增加、减少、清零这些动作。如果每次都把对象和函数分开放,代码会越来越散。写成类以后,数据和动作就放到一起了。
counter := new Counter(10)
counter.Add(5)
counter.Sub(3)
MsgBox, % counter.value
class Counter
{
__New(start := 0)
{
this.value := start
}
Add(n := 1)
{
this.value += n
}
Sub(n := 1)
{
this.value -= n
}
}
这里的 Counter 是类,counter 是实例。类像图纸,实例才是真正拿来用的对象。
this 就是当前对象
类的方法里最重要的词是 this。它代表当前这个实例。
; 需结合上段内容
Add(n := 1)
{
this.value += n
}
这句的意思是:把当前计数器自己的 value 增加 n。如果你创建两个计数器,它们各自有各自的 value。
; 需结合上段内容 a := new Counter(1) b := new Counter(100) a.Add(9) b.Sub(20) MsgBox, % "a=" a.value "`nb=" b.value
理解 this 以后,看类库会轻松很多。很多类库里不是写全局变量,而是把状态存在 this.xxx 里。
__New 负责初始化
__New 会在 new ClassName() 时自动运行。它最常见的用途是接收参数、保存初始状态、准备资源。
class FileJob
{
__New(path)
{
this.path := path
SplitPath, path, fileName
this.fileName := fileName
}
Show()
{
MsgBox, % this.fileName "`n" this.path
}
}
我建议新手先把 __New 当成“构造这个对象时要做的准备工作”。不要一开始就在里面塞太复杂的逻辑,先让它把对象需要的基础数据放好。
方法不是越多越好
初学类的时候,容易把所有函数都塞进类里。这样反而会变乱。我的判断标准很简单:这个函数是否高度依赖这个对象自己的数据?如果是,就适合做成方法;如果不是,保留普通函数也很好。
比如 counter.Add() 依赖 counter.value,适合做方法。一个通用的 TrimText(text) 不依赖某个对象,就没必要硬塞进类。
base 可以先知道,不必急着深挖
v1 的类背后有一个重要概念叫 base。简单说,当对象自己没有某个方法时,AHK 会去它的基对象里找。用 class 创建实例时,实例的 base 通常就是类对象。
所以你能这样调用:
; 需结合上段内容 counter := new Counter() counter.Add()
counter 自己不一定真的保存了 Add 这个函数,但它的基对象里有,AHK 会顺着关系找到它。这个机制也是继承、方法查找、元函数的基础。
不过新手不需要一上来就手动改 base。先会定义类、创建实例、用 this 保存状态,已经能写出很多干净的封装。
继承只在有明显共性时用
v1 支持 extends,但我不建议为了显得高级而写继承。继承适合“确实有一个基础版本,另一个版本只是扩展它”的情况。
class Logger
{
Write(text)
{
FileAppend, %text%`n, %A_ScriptDir%\log.txt, UTF-8
}
}
class TimeLogger extends Logger
{
Write(text)
{
FormatTime, now,, yyyy-MM-dd HH:mm:ss
base.Write("[" now "] " text)
}
}
base.Write() 表示调用父类里的同名方法。这个例子里,子类只负责加时间,真正写文件仍然交给父类。
最常见的几个坑
- 把类名变量覆盖掉。比如类叫
Box,不要写Box := new Box。 - 忘记用
this.,结果把实例变量写成了局部变量。 - 在对象里创建了和内置方法同名的键,比如
HasKey,导致方法访问变怪。 - 把类写得太大,一个类什么都管,最后比散函数还难维护。
- 循环引用没有断开,复杂对象释放时可能不如预期。
对大多数日常脚本来说,前两个坑最常见。只要记住“实例状态放 this.xxx,类名不要拿来当实例变量名”,就能少很多奇怪问题。
对象和类不是必须一口吃完。AHK v1 很多优秀库其实也只是把几个状态和几个动作包在一起,并没有写得很玄。先把小对象、小类写顺,后面看别人的库就会自然很多。

评论(0)