ES7 implementation of the Decorator Pattern
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