简体   繁体   中英

Can't inherit from context object?

I was trying to create an object that inherits from the context object. But upon calling a function from the object that I'm inheriting from, the browser (Chrome) states Uncaught TypeError: Illegal invocation . Here's the basic code:

http://jsfiddle.net/adrianh/BKYfv/1/

var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var ctx2 = Object.create(ctx);
ctx.setTransform(1, 0, 0, 1, 0, 0); // identity -- works
alert("ctx works");
ctx2.setTransform(.5, 0, 0, .5, 0, 0); // scale by half -- fails
alert("ctx2 works");

Why does this fail?

Workaround

I wrote a makeForwardingObject() function that does what I want. It can be found here .

// makeForwardingObject(obj, funcs, attribs)
//
//        obj - the object that is being forwarded to
//      funcs - array of non enumerable function member names to forward to
//    attribs - array of non enumerable attributes to forward to
//
// Makes a forwarding object that forwards all functions calls and attribute
// requests to the forwarded object.  In this way, the original object is
// acted upon directly, while you can delete or modify members to your 
// object without interfering with the original.
//
// Access to the object being forwarded to is always available using member
// functions applyParent(), callParent(), setParentAttrib() and 
// getParentAttrib().
//
// If funcs or attribs are enumerable in the object, they are not added 
// a second time.
function makeForwardingObject(obj, funcs, attribs)
{
    var _ = { };
    Object.defineProperties(_, {
        _: { value: obj },
        // like obj.apply() except it applys to object being forwarded to.
        applyParent : { value: function applyParent(func, args)
        {
            return this._[func].apply(this._, args);
        }},
        // like obj.call() except it applys to object being forwarded to.
        callParent: { value: function callParent(func)
        {
            // FF at least doesn't understand arguments.slice(), 
            // arguments.splice() or arguments.shift().  WTF?!?!
            var args=[];
            for (i=1; i<arguments.length; ++i)
                args[i-1]=arguments[i];
            return this._[func].apply(this._, args);
        }},
        // this is for setting member of object being forwarded to.
        setParentAttrib: { value: function setParentAttrib(attrib, val)
        {
            return this._[attrib]=val;
        }},
        // this is for getting member of object being forwarded to.
        getParentAttrib: { value: function getParentAttrib(attrib, val)
        {
            return this._[attrib];
        }},
    });

    for (var key in obj)
    {
        switch (typeof obj[key])
        {
            case 'function':
                _[key] = eval("(function "+key+"() { return this._."+key+".apply(this._, arguments);  })");
                break;
            default:
                eval("Object.defineProperty(_, '"+key+"', {"+
                         "get: function "+key+"() { return this._."+key+"; },"+
                         "set: function "+key+"(v) { return this._."+key+"=v; },"+
                         "enumerable: true,"+
                     "})");
                break;
        }
    }
    for (var index in funcs)
    {
        var key = funcs[index];
        if (!_.hasOwnProperty(key))
        {
            _[key] = eval("(function "+key+"() { return this._."+key+".apply(this._, arguments);  })");
        }
    }

    for (var index in attribs)
    {
        var key = funcs[index];
        if (!_.hasOwnProperty(key))
        {
            eval("Object.defineProperty(_, '"+key+"', {"+
                 "get: function "+key+"() { return this._."+key+"; },"+
                 "set: function "+key+"(v) { return this._."+key+"=v; },"+
                 "enumerable: false,"+
                 "})");
        }
    }

    return _;
}

// Return a string of all the members in an object. Used for debugging.
function getMembers(obj)
{
    var _ = "";
    for (key in obj)
    {
        _ += key + ":" + typeof obj[key] + " = " + obj[key] +"\n";
    }
    return _;
}


var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var ctx2 = makeForwardingObject(ctx);
var x = { a: "" };
alert(getMembers(ctx));
alert(getMembers(ctx2));
ctx.setTransform(1, 0, 0, 1, 0, 0); // identity -- works
alert("ctx works");
ctx2.setTransform(.5, 0, 0, .5, 0, 0); // scale by half -- works!
//These are alternate ways to call the forwarded object's member functions:
//  ctx2.applyParent('setTransform', [.5, 0, 0, .5, 0, 0]); // scale by half -- works!
//  ctx2.callParent('setTransform', .5, 0, 0, .5, 0, 0); // scale by half -- works!
alert("ctx2 works");
ctx2.moveTo(0,0);
ctx2.lineTo(100, 100);
ctx2.stroke();

One shallow answer would be because a canvas rendering context can't be constructed. Using the CanvasRenderingContext2d() function (like many other constructors in the DOM) will throw an Type error: "Illegal constructor" , because they are supposed to only be created in one specific way from a factory function. In this case the .getContext() method of the canvas.

Despite creating a new Object with a RenderingContext2d as a prototype you can falsely create a rendering context by using

ctx2=Object.create(CanvasRenderingContext2D.prototype);

or

ctx2=Object.create(ctx.constructor.prototype);

Giving you a completely blank stateless (and useless) rendering context object, which actually throws the same exceptions as your cloned context. It doesn't even have a canvas assigned.

The reason this doesn't work is because you only inherit a reference to the the public methods of the RenderingContext prototype, and in your case of the clone has a reference to all the states of the context you created it from via prototype chain, but other than that it's a hollow body. No private var and no privately declared function declared inside the CanvasRenderingContext constructor gets inherited via the prototype.

In case you are curious, you can write this kind of object yourself

function nonConstructable(factoryVar){
    if(arguments.callee.caller !== Factory){
        throw TypeError("Invalid constructor");
    }
    var privateVar = privateMethod();
    privateVar+=factoryVar;
    this.publicVar= privateVar;

    function privateMethod(){
        return 123;
    }
}
function Factory(){
    var privateFactoryVar = 321;
    return new nonConstructable(privateFactoryVar );
}

You see this way these 2 objects are linked and the only way for you to execute the operations inside the nonConstructable constructor is by constructing it via the specific Factory .

Doing a bit of more research it seems CanvasRenderingContext2D and WebGLRenderingContext are planned to be valid constructors for contexts, who can just render to any canvas and should both work inside a worker thread. I couldn't find out what the current state of the implementation is through, seems like people completely stopped writing about it 2 years ago for some reason.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM