如何实现类似DOMTokenList的兼具Setter与原型方法的属性?
classList) Awesome question! The way classList works is such a neat pattern—combining a setter-friendly interface with prototype methods—and it's totally replicable in vanilla JS. Let me break down how to build this step by step.
Core Concept
The key trick here is using Object.defineProperty to create a property with:
- A getter that returns a dedicated object (or class instance) containing your custom methods. This object holds a reference to the parent's internal state, so methods can modify it directly.
- A setter that handles raw value assignments (like a string) and updates the internal state accordingly.
Step 1: Build a Basic Implementation
Let's start with a simplified example that mimics classList for a custom "element" object:
// Our mock element class class MyElement { constructor() { // Internal storage for our "classes" this._classes = []; // Create a methods object tied to this instance's state this._classList = { add: (...classes) => { classes.forEach(cls => { if (!this._classes.includes(cls)) { this._classes.push(cls); } }); }, remove: (...classes) => { classes.forEach(cls => { const index = this._classes.indexOf(cls); if (index !== -1) { this._classes.splice(index, 1); } }); }, // Make it human-readable when logged toString: () => this._classes.join(' ') }; } } // Define the `classList` property with getter/setter Object.defineProperty(MyElement.prototype, 'classList', { get() { // Return our methods object when accessing the property return this._classList; }, set(newValue) { // Handle string assignments (like `elem.classList = "foo bar"`) if (typeof newValue === 'string') { this._classes = newValue.trim().split(/\s+/).filter(Boolean); } // Optional: Handle array inputs too else if (Array.isArray(newValue)) { this._classes = newValue.filter(Boolean); } }, enumerable: true, configurable: true });
Test It Out
const elem = new MyElement(); // Use the setter elem.classList = 'class1 class2 class3'; console.log(elem.classList.toString()); // "class1 class2 class3" // Call prototype-like methods elem.classList.add('class4'); console.log(elem.classList.toString()); // "class1 class2 class3 class4" elem.classList.remove('class2'); console.log(elem.classList.toString()); // "class1 class3 class4" // Reassign with the setter again elem.classList = 'new-class1 new-class2'; console.log(elem.classList.toString()); // "new-class1 new-class2"
Step 2: Upgrade to a Prototype-Based Class (Like DOMTokenList)
For better scalability (and to match how native DOMTokenList works), we can extract the methods into a dedicated class. This way, methods are shared across instances instead of recreated for each one:
// Our custom TokenList class (mirrors DOMTokenList) class MyTokenList { constructor(ownerElement) { // Hold a reference to the parent element to access its internal state this._owner = ownerElement; } add(...classes) { classes.forEach(cls => { if (!this._owner._classes.includes(cls)) { this._owner._classes.push(cls); } }); } remove(...classes) { classes.forEach(cls => { const index = this._owner._classes.indexOf(cls); if (index !== -1) { this._owner._classes.splice(index, 1); } }); } contains(cls) { return this._owner._classes.includes(cls); } toggle(cls) { if (this.contains(cls)) { this.remove(cls); return false; } else { this.add(cls); return true; } } toString() { return this._owner._classes.join(' '); } } // Update our mock element to use MyTokenList class MyElement { constructor() { this._classes = []; // Create a MyTokenList instance tied to this element this._classList = new MyTokenList(this); } } // Reuse the same classList property definition as before Object.defineProperty(MyElement.prototype, 'classList', { get() { return this._classList; }, set(newValue) { if (typeof newValue === 'string') { this._classes = newValue.trim().split(/\s+/).filter(Boolean); } else if (Array.isArray(newValue)) { this._classes = newValue.filter(Boolean); } }, enumerable: true, configurable: true });
This version is more maintainable—you can add new methods to MyTokenList once, and all instances get access to them.
Step 3: Add Bonus "Live" Behavior (Optional)
To match the native classList/className联动, add a className property that syncs with classList:
Object.defineProperty(MyElement.prototype, 'className', { get() { return this._classes.join(' '); }, set(newValue) { // Reuse the classList setter logic to keep things in sync this.classList = newValue; }, enumerable: true, configurable: true });
Now modifying className updates classList and vice versa, just like in the DOM!
Final Notes
- The internal state (
_classesin our example) is kept private (using the underscore convention) to prevent direct external modification—all changes go through theclassListmethods or setter. - You can extend this pattern to any type of data, not just class lists. For example, a
dataAttributesproperty that lets you assign a string of key-value pairs or call methods likeset()/get().
内容的提问来源于stack exchange,提问作者dpic

