0

I'm trying to abstract a JQuery's common behavior that allows me to compose code easier: encapsulate an object to when a set-like method is called, it's executed and the object itself is returned (pretty like JQuery does).

Look:

let encapsulate = function (guy) {
    return function () {
        guy.call(this, [].slice.apply(arguments))
        Object.setPrototypeOf(this, guy.prototype)
        Object.getOwnPropertyNames(guy.prototype)
            .filter(propName => propName.match(/set/g))
            .forEach(propName => {
                this[propName] = () => {
                    guy.prototype[propName].apply(this, [].slice.apply(arguments))
                    return this
                }
            })
    }
}

A test case is:

// works
let date = new Date();
console.log(date.setDate(10)); // logs 1494447383428
console.log(date.getDate()); // logs 10
console.log(Date.prototype.setDate.apply(date, [20])); // logs 1494447383428
console.log(date.getDate()); // logs 20

// does not work
let ED = encapsulate(Date);
let date1 = new ED();
console.log(date1.setDate(10)); // should log date1 object but throws an error

Which throws an error Method Date.prototype.setDate called on incompatible receiver [object Object].

Could you help me? D:

susanoobit
  • 128
  • 8
  • 1
    It means exactly what it says. `Date` is not really subclassable, and surely not like you did it. Try to use composition over inheritance (just like jQuery did)! – Bergi May 11 '17 at 20:35
  • Sure... but that error, why it appears when I call `guy.prototype[propName].apply(this, [].slice.apply(arguments))` and not when I call `Date.prototype.setDate.apply(date, [20])`, for example? I'm confused. – susanoobit May 11 '17 at 20:47
  • Because `date` is a real date instance with the appropriate internal slots, while `this` is not - an object inheriting from `Date.prototype` isn't enough. – Bergi May 11 '17 at 20:49
  • I get it! I'm posting an answer to my own question with what I've done. – susanoobit May 11 '17 at 21:30

1 Answers1

0

(Updating) New advices from Bergi:

let encapsulate = function (guy) {
    return function () {
        this.inner = new (Function.prototype.bind.apply(guy, arguments))

        let prototype =
            Object.getOwnPropertyNames(guy.prototype)
                .reduce((proto, propName) => {
                    let method = propName.match(/^set/g) ?
                        (function () {
                            guy.prototype[propName].apply(this.inner, [].slice.apply(arguments))
                            return this
                        }).bind(this) :
                        (function () {
                            return guy.prototype[propName].apply(this.inner, [].slice.apply(arguments))
                        }).bind(this)

                    return Object.defineProperty(proto, propName, {
                        value: method,
                        enumerable: false,
                        writable: false,
                        configurable: false
                    })
                }, {})

        Object.defineProperty(prototype, 'applyFunction', {
            value: fn => { fn(this); return this },
            enumerable: false,
            writable: false,
            configurable: false
        })

        Object.setPrototypeOf(this, prototype)
    }
}

(Deprecated) Following what Bergi has said, I've done this, using composition:

let encapsulate = function (guy) {
    return function () {
        let argumentsWrapper =
            [].slice.apply(arguments)
                .reduce((aw, argument, idx) => {
                    aw['a' + idx] = argument;
                    return aw;
                }, {})

        this.inner = eval('new guy(' +
            Object.keys(argumentsWrapper)
                .reduce((string, argumentName, idx, arr) =>
                    string + 'argumentsWrapper.'
                    + argumentName
                    + (idx != arr.length - 1 ? ',' : ''),
                '')
            + ')')

        let setProperties = Object.getOwnPropertyNames(guy.prototype)
            .filter(propName => propName.match(/^set/g))

        setProperties.forEach(function (propName) {
                this[propName] = (function () {
                    guy.prototype[propName].apply(this.inner, [].slice.apply(arguments))
                    return this
                }).bind(this)
            }, this)

        Object.getOwnPropertyNames(guy.prototype)
            .filter(propName => !propName.match(/^set/g))
            .forEach(function (propName) {
                this[propName] = (function () {
                    return guy.prototype[propName].apply(this.inner, [].slice.apply(arguments))
                }).bind(this)
            }, this)

        this.applyFunction = fn => {
            fn(this)
            return this
        }
    }
}

It's not beautiful :/

susanoobit
  • 128
  • 8
  • For how to avoid the `eval`, have a look [here](http://stackoverflow.com/q/1606797/1048572). Also, shouldn't you put all those properties on the `.prototype` of your returned function? – Bergi May 11 '17 at 21:41
  • Thank you, @Bergi! Is it ok now? – susanoobit May 12 '17 at 18:50
  • No, instead of re-creating those `prototype` objects on every call and using `Object.setPrototypeOf(this)`, you should assign them to the `.prototype` of the function that `encapsulate` returns. – Bergi May 12 '17 at 22:28