Differences
This shows you the differences between two versions of the page.
Both sides previous revision Previous revision Next revision | Previous revision | ||
guides:objects [2025-01-06 12:57] – ↷ Page moved from playground:objects to guides:objects geek | guides:objects [2025-03-31 17:28] (current) – Move classes header to separate page geek | ||
---|---|---|---|
Line 69: | Line 69: | ||
</ | </ | ||
- | Unlike in AutoHotkey v1, arrays are not //sparse//. In v1, you could specify any new index for an array and assign a value into it. However, in v2 you cannot specify new indexes and all indexes are // | + | Unlike in AutoHotkey v1, arrays are not //sparse//. In v1, you could specify any new index for an array and assign a value into it. However, in v2 you cannot specify new indexes and all indexes are // |
==== Maps ==== | ==== Maps ==== | ||
Line 105: | Line 105: | ||
MsgBox 'My list has ' myList.Count ' items now! Item " | MsgBox 'My list has ' myList.Count ' items now! Item " | ||
</ | </ | ||
+ | |||
+ | ===== Maps versus Objects ===== | ||
+ | |||
+ | In AutoHotkey v1, there was no separation between Maps and basic Object types. In AutoHotkey v2, it is possible to misuse a basic object in many of the situations where maps will be more appropriate. Let's talk about that! | ||
+ | |||
+ | AutoHotkey' | ||
+ | |||
+ | In JavaScript, shadowing instance methods is almost never a concern because the language development group has bent over backwards to avoid putting reasonable instance methods onto their objects. The set of instance methods on basic objects in JavaScript are as follows: | ||
+ | |||
+ | * Object.prototype.**hasOwnProperty**() | ||
+ | * Object.prototype.**isPrototypeOf**() | ||
+ | * Object.prototype.**propertyIsEnumerable**() | ||
+ | * Object.prototype.**toLocaleString**() | ||
+ | * Object.prototype.**toString**() | ||
+ | * Object.prototype.**valueOf**() | ||
+ | |||
+ | Notably, this set of methods contain almost nothing critical to the usage of most objects. If we loaded some data with dynamic names and '' | ||
+ | |||
+ | Compare this to the set of methods available for basic objects in AutoHotkey: | ||
+ | |||
+ | * **Clone**: Returns a shallow copy of an object. | ||
+ | * **DefineProp**: | ||
+ | * **DeleteProp**: | ||
+ | * **GetOwnPropDesc**: | ||
+ | * **HasOwnProp**: | ||
+ | * **OwnProps**: | ||
+ | |||
+ | Plus the methods available on all values: | ||
+ | |||
+ | * **GetMethod**: | ||
+ | * **HasBase**: | ||
+ | * **HasMethod**: | ||
+ | * **HasProp**: | ||
+ | |||
+ | You can see, these methods are immediately much more useful than the JavaScript ones. Why is that? | ||
+ | |||
+ | JavaScript painted itself into a box. Because they enabled developers to treat basic objects like maps from the start, they can no longer add new instance methods to basic objects without risking issues with web scripts that already exist out there in the world. Instead, they implement all new object functionality as static methods instead of instance methods. | ||
+ | |||
+ | AutoHotkey v2 doesn' | ||
+ | |||
+ | See here, a comparison of AHK's instance methods vs JavaScript' | ||
+ | |||
+ | ^ AutoHotkey | ||
+ | | '' | ||
+ | | '' | ||
+ | | '' | ||
+ | |||
+ | So in JavaScript, you can have an object where its property '' | ||
+ | |||
+ | Let's give an example: | ||
+ | |||
+ | > Anecdote from [[user: | ||
+ | > | ||
+ | > Before AHKv2 was even a twinkle of light in the distance, I used various AHK socket libraries to create a chat bot for the AutoHotkey IRC help chat. This chat bot, among other things, kept a scoreboard to track how helpful people were being in the chat. Whenever someone would type '' | ||
+ | > | ||
+ | > <code autohotkey> | ||
+ | scores := FileOpen(" | ||
+ | scores := JSON.Load(scores) | ||
+ | scores[targetUser]++ | ||
+ | scores := JSON.Dump(scores) | ||
+ | FileOpen(" | ||
+ | </ | ||
+ | > What would you say the issue with this code is? It's easy not to pick up on it right away, or at all. The issue here is //what happens when someone with the username '' | ||
+ | > | ||
+ | > Well, I'll tell you what happens. On line 1 it loads the score board, on line 2 it parses the score board as JSON, on line 3 it shadows the built-in '' | ||
+ | > | ||
+ | > I could blocklist names that I think would cause potential issues, like '' | ||
+ | > | ||
+ | > Although this example is relatively low stakes, it demonstrates the class of bug very succinctly. If you use dynamic names for object properties, you open yourself up to potential future attacks on the very reliability of your code. | ||
+ | |||
+ | Because it's potentially unsafe to use property names generated by some expression, AutoHotkey v2 makes the syntax for that rather unwieldy. Instead of '' | ||
+ | |||
+ | Instead of misusing basic objects to load dynamic property names, AutoHotkey v2 provides the Map object with its item store which can store items of //any// name without worrying about shadowing important built-in names. It's only a few extra characters up front to write, but it entirely avoids the aforementioned preventable issues, and you can also avoid having to request the OwnProps iterator every time. See below, the two options really shake about the same in terms of how much code there is to write: | ||
+ | |||
+ | <runner ahk2> | ||
+ | myObject := {a: " | ||
+ | for key, value in myObject.OwnProps() { | ||
+ | ; This loop only works because OwnProps is not defined as user data on myObject | ||
+ | MsgBox "Plain object - " key ": " value | ||
+ | } | ||
+ | |||
+ | ; versus | ||
+ | |||
+ | myObject := Map(" | ||
+ | for key, value in myObject { | ||
+ | ; This loop works even if OwnProps is defined as user data on myObject | ||
+ | MsgBox "Map object - " key ": " value | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | So the rule of thumb is: If your all of your keys are known at the time you're writing the script, and you can hard-code them into the script, you can use a basic object with the property store. If your keys cannot be known at the time you're writing the script (like because they' | ||
+ | |||
+ | ==== Breaking the Rules ==== | ||
+ | |||
+ | If you really must break the rule of thumb and load dynamic names into a basic object' | ||
+ | |||
+ | First, if you are //sure// the data source you're loading from can never contain names that would conflict with the built-in names you need, you can actually do this safely. | ||
+ | |||
+ | Second, if you do not care about polymorphism and the other advantages of a rigorous object implementation, | ||
+ | |||
+ | <runner ahk2> | ||
+ | myObject := {OwnProps: "Own Properties" | ||
+ | for key, value in ObjOwnProps(myObject) { | ||
+ | ; This loop works even though OwnProps was overridden | ||
+ | MsgBox " | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | This reduces the flexibility of the script in the future. For example, imagine that a new object base implementation was offered by a library. Maybe the library uses some kind of hash mapping to make the property store more performant. Maybe the library offers the object as an RPC proxy for a remote environment (this is sometimes done for things like accessing certain JavaScript values from the WebView2 library). If you forcibly use the '' | ||
+ | |||
+ | <runner ahk2> | ||
+ | ; Some function you write using ObjOwnProps | ||
+ | ObjectProcessor1(someObject) { | ||
+ | for key, value in ObjOwnProps(someObject) | ||
+ | MsgBox " | ||
+ | } | ||
+ | |||
+ | ; Some function you write using object.OwnProps | ||
+ | ObjectProcessor2(someObject) { | ||
+ | try for key, value in someObject.OwnProps() | ||
+ | MsgBox " | ||
+ | } | ||
+ | |||
+ | normalObject := {OwnProps: "", | ||
+ | customObject := CoolerObject() | ||
+ | customObject.a := " | ||
+ | |||
+ | MsgBox " | ||
+ | ObjectProcessor1(normalObject) | ||
+ | MsgBox " | ||
+ | ObjectProcessor1(customObject) | ||
+ | MsgBox " | ||
+ | ObjectProcessor2(normalObject) | ||
+ | MsgBox " | ||
+ | ObjectProcessor2(customObject) | ||
+ | |||
+ | ; For example, maybe cooler objects should always enumerate their property store | ||
+ | ; as 1, 2, 3 for some reason. | ||
+ | class CoolerObject { | ||
+ | OwnProps() => [1, 2, 3].__Enum() | ||
+ | } | ||
+ | </ | ||
+ | |||
===== Object References ===== | ===== Object References ===== | ||
Line 183: | Line 326: | ||
</ | </ | ||
- | ===== Fundamentals of Class Objects | + | ===== Classes |
- | + | ||
- | ==== Function Objects ==== | + | |
- | + | ||
- | In AutoHotkey v1, there were several global collections of names that were kept separate. There was a collection of command names, a collection of label names, a collection of function names, and a collection of variable names. AutoHotkey v2 has mostly merged these collections, | + | |
- | + | ||
- | Allowing functions to be saved inside variables and passed around like data is known as having [[https:// | + | |
- | + | ||
- | <runner ahk2> | + | |
- | MyFunction() { | + | |
- | ; No matter how this function is called, the message box | + | |
- | ; will say "You called MyFunction" | + | |
- | MsgBox "You called " A_ThisFunc | + | |
- | } | + | |
- | + | ||
- | MsgBox IsObject(MsgBox) ", " Type(MsgBox) | + | |
- | MsgBox IsObject(MyFunction) ", " Type(MyFunction) | + | |
- | + | ||
- | MyVar := MyFunction ; Put MyFunction into a different variable | + | |
- | MyVar() ; Call the function object stored inside MyVar | + | |
- | </ | + | |
- | + | ||
- | Function objects come inside global read-only variables by default, but can be passed around just like any other object. As shown above, it's easy to put the function object into a different variable even if the new variable has a different name. Additionally, | + | |
- | + | ||
- | <runner ahk2> | + | |
- | MyVar := () => MsgBox(" | + | |
- | MyVar() | + | |
- | + | ||
- | ; In AHKv2.1 this is allowed as well: | + | |
- | ;MyVar2 := () { | + | |
- | ; MsgBox "You called '" | + | |
- | ;} | + | |
- | ;MyVar2() | + | |
- | </ | + | |
- | + | ||
- | By itself, this syntax is usually seen when defining OnEvent type callbacks. It allows you to skip defining a function that might only be called in one place: | + | |
- | + | ||
- | <code autohotkey> | + | |
- | CloseCallback() { | + | |
- | MsgBox "You tried to close the GUI" | + | |
- | } | + | |
- | g := Gui() | + | |
- | g.OnEvent(" | + | |
- | + | ||
- | ; Can be rewritten as: | + | |
- | + | ||
- | g := Gui() | + | |
- | g.OnEvent(" | + | |
- | + | ||
- | ; Or in v2.1: | + | |
- | + | ||
- | g := Gui() | + | |
- | g.OnEvent(" | + | |
- | MsgBox "You tried to close the GUI" | + | |
- | }) | + | |
- | </ | + | |
- | + | ||
- | However, where things start to get really interesting is when you put function objects into //other objects//. Just like a function object can be stored inside a regular variable and then that variable becomes callable, a function object can be stored as an object property and then that property becomes callable. A callable property on an object is called a // | + | |
- | + | ||
- | When you call a function stored as an object property, AutoHotkey does a little trick with the parameter list. If you have '' | + | |
- | + | ||
- | <runner ahk2> | + | |
- | MyFunction(this, | + | |
- | MsgBox 'a: ' a '`nb: ' b '`nc: ' c | + | |
- | } | + | |
- | + | ||
- | MyObject := { | + | |
- | FunctionProperty: | + | |
- | } | + | |
- | + | ||
- | ; These following 4 lines are all equivalent | + | |
- | MyObject.FunctionProperty(1, | + | |
- | Temp := MyObject.FunctionProperty, | + | |
- | (MyObject.FunctionProperty)(MyObject, | + | |
- | MyObject.FunctionProperty.Call(MyObject, | + | |
- | </ | + | |
- | + | ||
- | ==== Prototyping ==== | + | |
- | + | ||
- | AutoHotkey objects are *prototype* based, but AutoHotkey' | + | |
- | + | ||
- | The first part of this arrangement was //function objects// being nested inside regular objects. The second part is // | + | |
- | <runner ahk2> | + | |
- | baseObject := { | + | |
- | someProperty: | + | |
- | } | + | |
- | + | ||
- | testObject := { | + | |
- | base: baseObject | + | |
- | } | + | |
- | + | ||
- | MsgBox testObject.someProperty ; Will show " | + | |
- | </ | + | |
- | + | ||
- | ==== Class Syntax ==== | + | |
- | + | ||
- | AutoHotkey' | + | |
- | + | ||
- | Class syntax is used to simultaneously define two things: a " | + | |
- | + | ||
- | Remembering the fundamental of how functions stored in objects are called, it would mean that in this following example, when '' | + | |
- | + | ||
- | <runner ahk2> | + | |
- | testMethod(this, | + | |
- | MsgBox "this Ptr: " ObjPtr(this) | + | |
- | MsgBox 'a: ' a '`nb: ' b '`nc: ' c | + | |
- | } | + | |
- | MyClass := { | + | |
- | Prototype: { | + | |
- | | + | |
- | } | + | |
- | } | + | |
- | + | ||
- | myObject := {base: MyClass.Prototype} | + | |
- | + | ||
- | MsgBox " | + | |
- | MsgBox " | + | |
- | + | ||
- | myObject.functionProperty(1, | + | |
- | </ | + | |
- | + | ||
- | The "class object" | + | |
- | + | ||
- | An instance factory is a function that creates instances of a class. An instance factory for an AHK class works something like this: | + | |
- | + | ||
- | <code autohotkey> | + | |
- | classFactory(someClass) { | + | |
- | instance := {base: someClass.Prototype} | + | |
- | instance.__Init() | + | |
- | if HasMethod(instance, | + | |
- | instance.__New() | + | |
- | } | + | |
- | return instance | + | |
- | } | + | |
- | </ | + | |
- | The instance factory gets put onto the class object as its " | + | |
- | <runner ahk2> | + | |
- | testMethod(this, | + | |
- | MsgBox 'a: ' a '`nb: ' b '`nc: ' c | + | |
- | } | + | |
- | classFactory(someClass) { | + | |
- | instance := {base: someClass.Prototype} | + | |
- | instance.__Init() | + | |
- | if HasMethod(instance, | + | |
- | instance.__New() | + | |
- | } | + | |
- | return instance | + | |
- | } | + | |
- | MyClass := { | + | |
- | Prototype: { | + | |
- | | + | |
- | }, | + | |
- | Call: classFactory | + | |
- | } | + | |
- | myInstance := MyClass() | + | |
- | myInstance.functionProperty(" | + | |
- | </ | + | |
- | + | ||
- | This code invokes " | + | |
- | + | ||
- | That's the vast majority of what class syntax does. In that last example, we manually created this class: | + | |
- | + | ||
- | <runner ahk2> | + | |
- | class MyClass { | + | |
- | functionProperty(a, | + | |
- | MsgBox 'a: ' a '`nb: ' b '`nc: ' c | + | |
- | } | + | |
- | } | + | |
- | myInstance := MyClass() | + | |
- | myInstance.functionProperty(" | + | |
- | </ | + | |
- | + | ||
- | When defining a class, it allows you to specify static and non-static properties. You can do exactly the same with the manually written code. Static properties get added to the class object. Non-static properties get added to the instance by the '' | + | |
- | + | ||
- | <runner ahk2> | + | |
- | class MyClass1 { | + | |
- | static someProp := 123 | + | |
- | someProp := 456 | + | |
- | } | + | |
- | myInstance1 := MyClass1() | + | |
- | MsgBox " | + | |
- | + | ||
- | ; Equivalent to | + | |
- | + | ||
- | classFactory(someClass) { | + | |
- | instance := {base: someClass.Prototype} | + | |
- | instance.__Init() | + | |
- | if HasMethod(instance, | + | |
- | instance.__New() | + | |
- | } | + | |
- | return instance | + | |
- | } | + | |
- | MyClass2 := { | + | |
- | Prototype: { | + | |
- | | + | |
- | }, | + | |
- | Call: classFactory, | + | |
- | someProp: 123 | + | |
- | } | + | |
- | myInstance2 := MyClass2() | + | |
- | MsgBox " | + | |
- | </ | + | |
- | + | ||
- | === Unique behavior === | + | |
- | + | ||
- | As mentioned previously, there are a few unique features of the '' | + | |
- | The first is // | + | See: [[guides: |
- | The second difference is that the variable defined using '' |