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] – removed - external edit (Unknown date) 127.0.0.1 | guides:objects [2025-03-31 17:28] (current) – Move classes header to separate page geek | ||
---|---|---|---|
Line 1: | Line 1: | ||
+ | ====== v2 Objects ====== | ||
+ | |||
+ | ===== What is an Object? ===== | ||
+ | |||
+ | In its simplest form, an object stores a collection of values that can be accessed by a key, like a name or a list index. You can think of an object like a box that groups a bunch of variables together, where the variable names are the keys and the variable contents are the values. A collection of keys that map to values is called a [[https:// | ||
+ | |||
+ | Most AutoHotkey v2 objects have //two// key-value stores, which is an upgrade from v1 where all objects had a single store. These two stores are the //property store// and the //item store//. | ||
+ | |||
+ | The //property store// holds values that assist with you writing your code, called properties. For example, the '' | ||
+ | |||
+ | The //item store// holds keys and values, and is designed to hold data where the keys could be supplied by a variable. For example, if you are accessing items from the item store in a loop you might use the loop index variable '' | ||
+ | |||
+ | AutoHotkey v2 has many built-in object types. For now we'll focus on these three: //Basic objects//, //Map objects//, and //Array objects//. | ||
+ | |||
+ | ===== Built-In Object Types ===== | ||
+ | |||
+ | ==== Basic Objects ==== | ||
+ | |||
+ | Basic objects are the foundation for all other types of objects in AutoHotkey. They have very few properties defined, and do not define an item store. | ||
+ | |||
+ | Basic objects are created using either curly braces ('' | ||
+ | |||
+ | Basic objects should be used when you need to store a collection of keys and values, where the keys do not change and will be hard coded into the script. | ||
+ | |||
+ | <runner ahk2> | ||
+ | ; Define a new object with three properties in the property store | ||
+ | box := { | ||
+ | width: 57, | ||
+ | length: 70, | ||
+ | height: 12 | ||
+ | } | ||
+ | |||
+ | MsgBox "The box is " box.width " units wide" | ||
+ | |||
+ | box.width += 1 ; Increase the box object' | ||
+ | |||
+ | MsgBox "The box is now " box.width " units wide"</ | ||
+ | |||
+ | ==== Arrays ==== | ||
+ | |||
+ | Arrays are based on basic objects, and are used to store a list of items, numbered (// | ||
+ | |||
+ | Arrays are created using either square brackets ('' | ||
+ | |||
+ | <runner ahk2> | ||
+ | fruits := [ | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | ] | ||
+ | |||
+ | ; Access the array' | ||
+ | MsgBox "The first fruit is " fruits[1] | ||
+ | </ | ||
+ | |||
+ | Arrays have a variety of built-in properties / methods that can be used to interact with the list of items. | ||
+ | |||
+ | <runner ahk2> | ||
+ | myList := [] | ||
+ | |||
+ | ; Use dot notation to access property Length from the property store. | ||
+ | MsgBox "My list has " myList.Length " items" | ||
+ | |||
+ | ; Use dot notation to access method Push from the property store. | ||
+ | ; Push adds a new item to the Array | ||
+ | myList.Push(" | ||
+ | |||
+ | MsgBox "My list has " myList.Length " items now! Item 1 is " myList[1] | ||
+ | </ | ||
+ | |||
+ | 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 are based on basic objects, and are used to store unordered items where the keys can be text, numbers, other objects. | ||
+ | |||
+ | Maps are created by creating a new instance of the Map class ('' | ||
+ | |||
+ | <runner ahk2> | ||
+ | fruits := Map( | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | ) | ||
+ | |||
+ | ; Access the array' | ||
+ | MsgBox 'The definition of " | ||
+ | </ | ||
+ | |||
+ | Maps have a variety of built-in properties / methods that can be used to interact with the set of items. | ||
+ | |||
+ | <runner ahk2> | ||
+ | myList := Map() | ||
+ | |||
+ | ; Use dot notation to access property Length from the property store. | ||
+ | MsgBox "My map has " myList.Count " items" | ||
+ | |||
+ | ; Use dot notation to access method Set from the property store. | ||
+ | ; Set adds a new item to the Map or updates an existing item | ||
+ | myList.Set(" | ||
+ | |||
+ | ; Use bracket notation to do the same thing as the Set method. | ||
+ | myList[" | ||
+ | |||
+ | 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 ===== | ||
+ | |||
+ | It's important to know that AutoHotkey imagines an object as being separate from any variables that may contain it. Every object has a secret identifying number that acts like a label on a box, called its " | ||
+ | |||
+ | The object pointer system is an implementation detail that you don't need to worry about //most// of the time, but there are a few common situations where it impacts script behavior. The biggest impact is that when you make a second variable '' | ||
+ | |||
+ | <runner ahk2> | ||
+ | ; Store a reference to a new object in variable A | ||
+ | A := Object() | ||
+ | |||
+ | ; Duplicate the object pointer into a second variable | ||
+ | B := A | ||
+ | |||
+ | ; We now have variables A and B that both refer to the same | ||
+ | ; object, with some object pointer (#18239 used here as example). | ||
+ | ; | ||
+ | ; A == #18239 | ||
+ | ; \ | ||
+ | ; [ Object #18239 ] | ||
+ | ; / | ||
+ | ; B == #18239 | ||
+ | |||
+ | ; If you wanted, you can expose the object pointer with the | ||
+ | ; code below however there is little practical reason to do | ||
+ | ; this besides debugging. | ||
+ | MsgBox " | ||
+ | . " | ||
+ | |||
+ | ; Look inside the object and set the property store | ||
+ | ; key `PropertyKey` to the value `" | ||
+ | a.PropertyKey := " | ||
+ | |||
+ | ; This is like the previous line, but because A and B | ||
+ | ; both reference the same object, it actually overwrites | ||
+ | ; the value that was previously written. | ||
+ | b.PropertyKey := " | ||
+ | |||
+ | ; If you look inside the object, you'll find that whether | ||
+ | ; you access it by the reference in A or the reference in B, | ||
+ | ; there is only one object and its property store contains | ||
+ | ; the value " | ||
+ | MsgBox " | ||
+ | . " | ||
+ | </ | ||
+ | |||
+ | This behavior is also seen when passing objects as parameters to functions. When you pass an object as a parameter that function receives a copy of the object pointer rather than a copy of the object itself. If that function makes changes to the object those changes are reflected outside of the function as well. | ||
+ | |||
+ | <runner ahk2> | ||
+ | ChangeValues(someObject, | ||
+ | ; Update the object referenced by the copy of the object pointer | ||
+ | someObject.propertyKey := 2 | ||
+ | |||
+ | ; Update the copy of the number | ||
+ | someNumber := 2 | ||
+ | } | ||
+ | |||
+ | myObject := { propertyKey: | ||
+ | myNumber := 1 | ||
+ | MsgBox ( | ||
+ | "--- Before ---`n" | ||
+ | " | ||
+ | " | ||
+ | ) | ||
+ | |||
+ | ChangeValues( | ||
+ | myObject, ; Pass a copy of the object pointer | ||
+ | myNumber | ||
+ | ) | ||
+ | |||
+ | ; myNumber was not modified, but the object referenced by | ||
+ | ; myObject was modified. | ||
+ | MsgBox ( | ||
+ | "--- After ---`n" | ||
+ | " | ||
+ | " | ||
+ | ) | ||
+ | </ | ||
+ | |||
+ | ===== Classes ===== | ||
+ | |||
+ | See: [[guides: | ||