Introduction to AOP
For developers familiar with Android or Java Web development, the concept of Aspect-Oriented Programming (AOP) is well-established. The core of AOP lies in identifying an aspect and then performing pre-processing or post-processing on a specific business operation, or even replacing the operation entirely. The granularity of AOP is at the method level, which includes three parts: receiving data, processing data, and returning data.

AOP allows adding custom logic to any of these three stages. Common Java AOP frameworks include AspectJ, Spring AOP, Javassist, Guice, and Byte Buddy. Starting from version 4.0, ArkTS also supports AOP. This article explores how ArkTS implements AOP.
How ArkTS AOP Works
To effectively apply AOP, we first need to understand the prototype chain of JavaScript objects. This understanding helps in selecting the correct target for aspect-oriented programming.
2.1 The JavaScript Prototype Chain

As shown in the diagram:
- Horizontal dimension: A class references its prototype object via
prototype, and its constructor viaconstructor. The constructor is also associated with the class's static methods. - Vertical dimension: A class's prototype object points to the parent class's prototype object via
__proto__. The class's constructor points to the parent class's constructor via__proto__. An instance of the class points to the class's prototype object via__proto__.
For instance objects a and b: The lookup path for instance methods is shown by the red arrows in the diagram below. For classes A and B: The lookup path for static methods is shown by the blue arrows.

From the diagram, we can conclude:
- A class's prototype object holds the instance methods (non-static) of the class. It points to the parent class's prototype object via
__proto__and points back to the class (the class constructor) viaconstructor. The class constructor also stores the class's static methods. - The class (or its constructor) points to the parent class (parent constructor) via
__proto__.
2.2 AOP Implementation in ArkTS
AOP is implemented by modifying the method references. It essentially creates a new function that combines the original method with callback logic, and then replaces the original method with this new one.
"All problems in computer science can be solved by another level of indirection"

2.2.1 Pseudo-code for addBefore
// Pseudo-code implementation of addBefore
static addBefore(targetClass, methodName, isStatic, before: Function): void {
// Determine the target object based on whether the method is static or an instance method
let target = isStatic ? targetClass : targetClass.prototype;
// Get the original method
let origin = target[methodName];
// Create a new function that wraps the original method
let newFunc = function(...args) {
// Execute the 'before' function first
before(this, ...args);
// Then execute the original method and return its result
return origin.bind(this)(...args);
};
// Replace the original method with the new one
target[methodName] = newFunc;
}
2.2.2 Pseudo-code for addAfter
// Pseudo-code implementation of addAfter
static addAfter(targetClass, methodName, isStatic, after: Function): void {
let target = isStatic ? targetClass : targetClass.prototype;
let original = target[methodName];
let newFunc = function(...args) {
let result = original.bind(this)(...args);
// Execute the 'after' function with the result
return after(this, result, ...args);
};
target[methodName] = newFunc;
}
2.2.3 Pseudo-code for replace
static replace(targetClass, methodName, isStatic, instead): void {
let target = isStatic ? targetClass : targetClass.prototype;
let newFunc = function(...args) {
// Execute the replacement function
return instead(this, ...args);
};
target[methodName] = newFunc;
}
AOP Use Cases
- Monitoring and Statistics: Count method invocations, measure execution time.
- Defensive Programming: Validate parameters and return values.
- Precise Hooking: Hook specific methods in a class hierarchy.
- Proxy Pattern and Inversion of Control (IoC).
3.1 Monitoring and Statistics
3.1.1 Counting Method Invocations
export class Test {
hello() {
console.log('hello world');
}
}
Using Aspect.addBefore to count invocations of the hello method:
function main() {
let countHello = 0;
util.Aspect.addBefore(Test, 'hello', false, () => {
countHello++;
});
let instance = new Test();
console.log(`countHello: ${countHello}`); // Output: 0
instance.hello();
console.log(`countHello: ${countHello}`); // Output: 1
}
3.1.2 Measuring Method Execution Time
function addTimePrinter(target: Object, methodName: string, isStatic: boolean): void {
let startTime = 0;
let endTime = 0;
util.Aspect.addBefore(target, methodName, isStatic, () => {
startTime = Date.now();
});
util.Aspect.addAfter(target, methodName, isStatic, () => {
endTime = Date.now();
console.log(`Execution time: ${endTime - startTime} ms`);
});
}
Testing the time measurement function:
export class View {
onDraw(): void {
// ... long running operations
}
static cinit(): void {
// ... initialization logic
}
}
function main(): void {
// Measure static method execution time
addTimePrinter(View, 'cinit', true);
View.cinit();
// Measure instance method execution time
addTimePrinter(View, 'onDraw', false);
new View().onDraw();
}
3.2 Defensive Programming
- Parameter Validation: Ensure method arguments meet expected criteria.
- Return Value Correction: Adjust or sanitize returned values.
3.2.1 Parameter Validation
export class ViewList {
items: ViewList[];
constructor(items: ViewList[]) {
this.items = items;
}
getByIndex(index: number): ViewList | undefined {
return this.items[index];
}
}
The getByIndex method expects an index. To prevent index out-of-bounds errors, we can add parameter validation using addBefore:
util.Aspect.addBefore(ViewList, 'getByIndex', false, (list: ViewList, index: number) => {
if (!list.items) {
throw new Error('items array is undefined!');
}
if (index < 0) {
throw new Error('Index cannot be negative!');
}
if (list.items.length <= index) {
throw new Error('Index out of bounds!');
}
});
3.2.2 Corecting Return Values
export class RandomNumberGenerator {
static generateUpTo50(): number {
// Intentionally returns values up to 52 for demonstration
return Math.floor(Math.random() * 52);
}
}
The generateUpTo50 method should return a value between 0 and 50, but due to an error, it returns values up to 52. We can use addAfter to correct the return value:
export function testRandom(): void {
util.Aspect.addAfter(RandomNumberGenerator, 'generateUpTo50', true,
(target: typeof RandomNumberGenerator, result: number) => {
if (result > 50) {
return RandomNumberGenerator.generateUpTo50(); // Recursively generate until valid
} else {
console.log(`Corrected value: ${result}`);
return result;
}
});
console.log(RandomNumberGenerator.generateUpTo50());
}
3.3 Replacing Subclass Instance Methods
export class Aircraft {
fly(): void {
console.log('Flying...');
}
}
export class USAAircraft extends Aircraft {}
export class CNAircraft extends Aircraft {}
We can use the replace method to change the behavior of a specific subclass instance method:
export function testAircraft(): void {
let cn = new CNAircraft();
let us = new USAAircraft();
cn.fly(); // Output: Flying...
us.fly(); // Output: Flying...
// Replace the 'fly' method only for USAAircraft instances
util.Aspect.replace(USAAircraft, 'fly', false, () => {
console.log('Runaway...');
});
cn.fly(); // Output: Flying...
us.fly(); // Output: Runaway...
}
3.4 Inversion of Control (IoC)
AOP can also be used to implement IoC. Consider a PlayerManager class that depends on an IPlayer interface, which has implementations IjkPlayer and MediaPlayer. We can use AOP to dynamically switch between different player implementations.

Corresponding code for the UML diagram:
interface IPlayer {
init(): void;
start(): void;
stop(): void;
release(): void;
}
export class PlayManager {
private player?: IPlayer;
init(): void {
// Default implementation, can be replaced
}
start(): void {
// Default implementation, can be replaced
}
stop(): void {
// Default implementation, can be replaced
}
release(): void {
// Default implementation, can be replaced
}
}
export class IjkPlayer implements IPlayer {
init(): void {
console.log('IjkPlayer init...');
}
start(): void {
console.log('IjkPlayer start...');
}
stop(): void {
console.log('IjkPlayer stop...');
}
release(): void {
console.log('IjkPlayer release...');
}
}
export class MediaPlayer implements IPlayer {
init(): void {
console.log('MediaPlayer init...');
}
start(): void {
console.log('MediaPlayer start...');
}
stop(): void {
console.log('MediaPlayer stop...');
}
release(): void {
console.log('MediaPlayer release...');
}
}
Now, using the replace method to implement dynamic player switching:
// Helper function that returns a replacement function for a given method name
function providePlayer(methodName: string, playerSupplier: () => IPlayer) {
return (manager: PlayManager) => {
const player = playerSupplier();
switch (methodName) {
case 'init':
return player.init();
case 'start':
return player.start();
case 'stop':
return player.stop();
case 'release':
return player.release();
default:
return;
}
};
}
export function testPlayer(): void {
let currentPlayer: IPlayer = new IjkPlayer();
// Replace each method of PlayManager with the corresponding player method
util.Aspect.replace(PlayManager, 'init', false, providePlayer('init', () => currentPlayer));
util.Aspect.replace(PlayManager, 'start', false, providePlayer('start', () => currentPlayer));
util.Aspect.replace(PlayManager, 'stop', false, providePlayer('stop', () => currentPlayer));
util.Aspect.replace(PlayManager, 'release', false, providePlayer('release', () => currentPlayer));
let manager = new PlayManager();
manager.init(); // Calls IjkPlayer.init()
// Switch to MediaPlayer
currentPlayer = new MediaPlayer();
manager.start(); // Calls MediaPlayer.start()
}
AOP Considerations
-
Importing Target Classes: The target class usually needs to be imported. If the class is not exported, but you have an instance, you can obtain the class via the instance's
constructorproperty (because theconstructorof an instance points to the class object).// The instance's 'constructor' property points to the class object. util.Aspect.addBefore(this.context.constructor, 'startAbility', false, (instance: Object, wantParam: Want) => { console.info('UIAbilityContext startAbility: bundleName is ' + wantParam.bundleName); }); -
Scope of Modification: Understand the impact range of the modification by tracing through the JavaScript prototype chain. Modifications to a prototype affect all instances.
-
addBeforeConsiderations:- The callback function receives the instance as the first argument, followed by the original method's parameters.
- If you need to call the original method within the callback, you can store a reference to it before the aspect is applied.
- Note that calling
originalFunc.bind(instance)may generate a compile-time warning, but it works at runtime.
let originalFoo = new Test().foo; util.Aspect.addBefore(Test, 'foo', false, (instance: Test) => { // Option 1: If the original method doesn't use 'this', call it directly originalFoo(); // Option 2: If 'this' is used, bind the instance (may show a warning) originalFoo.bind(instance)(); }); -
addAfterConsiderations:- The callback receives the instance as the first argument and the return value of the original method as the second argument.
- You must return the value from the callback to propagate the result correctly.
util.Aspect.addAfter(Test, 'foo', false, (instance: Test, result: string) => { console.log('execute foo'); return result; // Essential: propagate the return value }); -
Limitations:
structtypes cannot be used as targets for aspect operations.- Methods marked as
readonlycannot be modified. - Constructors themselves cannot be replaced or intercepted.
References
- HarmonyOS Official Documentation: Application Aspect Programming Design
- Understanding ES6 Classes, Inheritance, and the Nature of Static Properties
