阿里云主机折上折
  • 微信号
Current Site:Index > ES7 implementation of the Decorator Pattern

ES7 implementation of the Decorator Pattern

Author:Chuan Chen 阅读数:49841人阅读 分类: JavaScript

Decorator Pattern Implementation in ES7

The decorator pattern is a structural design pattern that allows adding new functionality to existing objects dynamically without altering their structure. The decorator syntax introduced in ES7 makes implementing this pattern in JavaScript more elegant. Decorators are essentially higher-order functions that can modify the behavior of classes, methods, properties, or parameters.

Basic Decorator Syntax

ES7 decorators use the @ symbol and can be applied to classes, class methods, accessors, properties, and parameters. Decorators are essentially functions that receive relevant information about the target object as parameters.

// Class decorator
@decorator
class MyClass {}

// Method decorator
class MyClass {
  @decorator
  method() {}
}

// Property decorator
class MyClass {
  @decorator
  property = 'value'
}

// Accessor decorator
class MyClass {
  @decorator
  get value() {}
}

Implementing Simple Decorators

Decorator functions receive three parameters: the target object, property name, and property descriptor. For class decorators, only the constructor function is received as a parameter.

function log(target, name, descriptor) {
  const original = descriptor.value
  descriptor.value = function(...args) {
    console.log(`Calling ${name} with args: ${args}`)
    return original.apply(this, args)
  }
  return descriptor
}

class Calculator {
  @log
  add(a, b) {
    return a + b
  }
}

const calc = new Calculator()
calc.add(2, 3) // Output: Calling add with args: 2,3

Decorator Factories

Decorator factories are functions that return decorator functions, allowing parameters to be passed to customize the decorator's behavior.

function logWithMessage(message) {
  return function(target, name, descriptor) {
    const original = descriptor.value
    descriptor.value = function(...args) {
      console.log(`${message}: Calling ${name}`)
      return original.apply(this, args)
    }
    return descriptor
  }
}

class User {
  @logWithMessage('User action')
  login(username, password) {
    // Login logic
  }
}

Class Decorator Implementation

Class decorators are applied to class constructors and can be used to modify or replace class definitions.

function sealed(constructor) {
  Object.seal(constructor)
  Object.seal(constructor.prototype)
}

@sealed
class Animal {
  constructor(name) {
    this.name = name
  }
  
  speak() {
    console.log(`${this.name} makes a noise.`)
  }
}

Property Decorator Application

Property decorators can be used to observe property changes or add metadata.

function readonly(target, name, descriptor) {
  descriptor.writable = false
  return descriptor
}

class Circle {
  @readonly
  PI = 3.14159
  
  constructor(radius) {
    this.radius = radius
  }
}

const circle = new Circle(5)
circle.PI = 3.14 // Error: Cannot assign to read only property 'PI'

Combining Multiple Decorators

Decorators can be stacked, with execution order being bottom-up (from nearest to farthest when declared).

function first() {
  console.log('first(): decorator factory')
  return function(target, propertyKey, descriptor) {
    console.log('first(): decorator')
  }
}

function second() {
  console.log('second(): decorator factory')
  return function(target, propertyKey, descriptor) {
    console.log('second(): decorator')
  }
}

class Example {
  @first()
  @second()
  method() {}
}
// Output order:
// second(): decorator factory
// first(): decorator factory
// second(): decorator
// first(): decorator

Practical Use Cases

Decorators have various practical applications in web development, such as logging, performance monitoring, and form validation.

// Performance timing decorator
function time(label = '') {
  return function(target, name, descriptor) {
    const original = descriptor.value
    descriptor.value = async function(...args) {
      const start = performance.now()
      const result = await original.apply(this, args)
      console.log(`${label || name} took: ${performance.now() - start}ms`)
      return result
    }
    return descriptor
  }
}

class API {
  @time('Fetch user data')
  async fetchUser(id) {
    // Simulate API request
    await new Promise(resolve => setTimeout(resolve, 500))
    return { id, name: 'John Doe' }
  }
}

Metadata Decorators

Decorators can be used to add metadata, enabling more powerful functionality when combined with Reflect Metadata.

import 'reflect-metadata'

function validate(type) {
  return function(target, key, descriptor) {
    Reflect.defineMetadata('validation:type', type, target, key)
    
    const original = descriptor.value
    descriptor.value = function(value) {
      if (typeof value !== type) {
        throw new Error(`Parameter ${key} must be of type ${type}`)
      }
      return original.call(this, value)
    }
    return descriptor
  }
}

class Validator {
  @validate('string')
  setName(name) {
    this.name = name
  }
}

const validator = new Validator()
validator.setName('Alice') // Works
validator.setName(123) // Error: Parameter name must be of type string

Automatic this Binding

Decorators can solve the issue of lost this context in class methods by automatically binding the instance.

function autobind(target, name, descriptor) {
  const originalMethod = descriptor.value
  return {
    configurable: true,
    get() {
      const boundFn = originalMethod.bind(this)
      Object.defineProperty(this, name, {
        value: boundFn,
        configurable: true,
        writable: true
      })
      return boundFn
    }
  }
}

class Button {
  @autobind
  handleClick() {
    console.log(this) // Always points to Button instance
  }
}

const button = new Button()
document.addEventListener('click', button.handleClick)

Decorators and Higher-Order Components

In React, decorators are commonly used to create Higher-Order Components (HOCs).

function withLoading(WrappedComponent) {
  return class extends React.Component {
    state = { loading: true }
    
    async componentDidMount() {
      // Simulate data loading
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.setState({ loading: false })
    }
    
    render() {
      return this.state.loading 
        ? <div>Loading...</div>
        : <WrappedComponent {...this.props} />
    }
  }
}

@withLoading
class UserProfile extends React.Component {
  render() {
    return <div>User Profile Content</div>
  }
}

Limitations of Decorators

While decorators are powerful, there are several limitations to note: they cannot decorate functions (non-class methods), cannot modify class inheritance relationships, and may not be fully supported by static analysis tools. Additionally, decorators are currently still an ECMAScript proposal and require transpilation tools like Babel to run.

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.