You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现类似DOMTokenList的兼具Setter与原型方法的属性?

How to Implement a Property with Both Setter Behavior and Prototype Methods (Like 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 (_classes in our example) is kept private (using the underscore convention) to prevent direct external modification—all changes go through the classList methods or setter.
  • You can extend this pattern to any type of data, not just class lists. For example, a dataAttributes property that lets you assign a string of key-value pairs or call methods like set()/get().

内容的提问来源于stack exchange,提问作者dpic

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 09:14:35