Refactoring Legacy JavaScript Code to Use Classes: The Good, The Bad and The Ugly
JavaScript systems are becoming increasingly complex and large. To tackle the challenges involved in implementing these systems, the language is evolving to include several constructions for programming- in-the-large. For example, although the language is prototype-based, the latest JavaScript standard, named ECMAScript 6 (ES6), provides native support for implementing classes. Even though most modern web browsers support ES6, only a very few applications use the class syntax. In this paper, we analyze the process of migrating structures that emulate classes in legacy JavaScript code to adopt the new syntax for classes introduced by ES6. We apply a set of migration rules on eight legacy JavaScript systems. In our study, we document: (a) cases that are straightforward to migrate (the good parts); (b) cases that require manual and ad-hoc migration (the bad parts); and (c) cases that cannot be migrated due to limitations and restrictions of ES6 (the ugly parts). Six out of eight systems (75%) contain instances of bad and/or ugly cases. We also collect the perceptions of JavaScript developers about migrating their code to use the new syntax for classes.
💡 Research Summary
The paper investigates the feasibility of refactoring legacy JavaScript code that emulates classes using ES5 patterns into native ES6 class syntax. Although ES6 introduces a class keyword, most existing codebases still rely on function constructors, prototype assignments, and static functions to model object‑oriented structures. The authors first define three transformation rules: (1) convert constructor functions and prototype methods into an ES6 class with a constructor and method definitions; (2) replace prototype‑based inheritance with the extends keyword; and (3) replace explicit super‑class constructor and method calls with the super keyword. These rules are applied iteratively until a fixed point is reached.
To evaluate the approach, eight real‑world JavaScript projects were selected from a prior study that identified class‑like structures. The projects span various domains (task runners, networking frameworks, graphics libraries, etc.) and vary in size from 846 to 23,952 lines of code, with class densities ranging from 0.16 to 0.95. The JSClassFinder tool was used to detect class‑like entities, after which the three rules were applied automatically. The authors measured code churn (added + changed LOC), deleted LOC, and churned files using GitHub diffs, and expressed these metrics both absolutely and relative to the total LOC.
Results show that a substantial portion of the code—between 16 % and 77 % of each system’s LOC—could be migrated automatically; this “good part” correlates strongly with class density. The churn‑to‑deleted ratio is close to 1 in all cases, indicating that the migration does not significantly increase code size.
However, four categories of “bad parts” required manual intervention. The most prominent issue is the incompatibility between ES5 function hoisting and ES6 class semantics: ES6 class constructors cannot reference this before a super() call, whereas many ES5 patterns pass this (or functions that close over this) to super‑class constructors. The authors resolve this by introducing setter methods and reordering calls. Other bad cases involve dynamic prototype manipulation, closure‑based method definitions, and mixins that cannot be expressed directly with extends.
The “ugly parts” are cases where ES6 simply lacks the needed feature. Examples include static fields defined by assigning to the constructor function, getters/setters created via Object.defineProperty, and multiple inheritance patterns implemented through prototype chaining. Since ES6 does not support static fields or true multiple inheritance, these sections must remain in their original ES5 form or be refactored into separate utility modules, resulting in a hybrid codebase that still exposes prototype mechanics.
Developer feedback collected after submitting pull requests reveals mixed attitudes. Positive reactions cite improved readability, better IDE support, and clearer inheritance hierarchies. Negative reactions focus on potential runtime regressions, the need for extensive testing, and the perceived overhead of maintaining both ES5 and ES6 styles, especially for the ugly parts that cannot be fully converted.
The discussion highlights the limits of automated refactoring tools: pattern detection is reliable for straightforward constructor/prototype cases but struggles with dynamic or unconventional patterns. The authors suggest future work on more sophisticated static analysis, automated test generation to validate behavior after migration, and exploration of newer ECMAScript features (private fields, class static blocks) that might reduce the “ugly” gap.
In conclusion, while a majority of legacy class‑like JavaScript code can be automatically transformed into modern ES6 class syntax, a non‑trivial fraction requires manual adjustments or remains unconvertible due to language constraints. The study provides quantitative evidence and practical guidelines for developers and tool builders aiming to modernize large JavaScript codebases, emphasizing that successful migration combines automated rule‑based refactoring with careful human oversight.
Comments & Academic Discussion
Loading comments...
Leave a Comment