Search code examples
javascriptconstructorecma262

Check if object is a constructor - IsConstructor


I want to check if a JavaScript value is a constructor, that is, if it has a [[Construct]] internal method.

ECMAScript defines IsConstructor, which does exactly this, but it's an internal operation.

So I want to emulate it. I thought about attempting instantiation or subclassing inside a try statement, but neither does work reliably for all cases.

function isConstructor(value) {
  try {
    return new value(), true;
  } catch(err) {
    return false;
  }
}

function isConstructor(value) {
  try {
    return new value(), true;
  } catch(err) {
    return false;
  }
}
var tests = 0,
    failed = 0;  
function test(value, expected, msg) {
  ++tests;
  try {
    var result = isConstructor(window.eval(value));
  } catch(err) {
    result = err;
  }
  if(result !== expected) {
    ++failed;
    console.log('Testing: ' + value + '\nMessage: ' + msg + '\nResult: ' + result + '\nExpected: ' + expected);
  }
}
function testEnd() {
  console.log(failed + ' out of ' + tests + ' tests failed.');
}
test('undefined', false, 'undefined is not a constructor');
test('null', false, 'null is not a constructor');
test('true', false, 'booleans are not constructors');
test('0', false, 'numbers are not constructors');
test('"abc"', false, 'strings are not constructors');
test('Symbol()', false, 'symbols are not constructors');
test('({})', false, '{} is not a constructor');
test('[]', false, 'arrays are not constructors');
test('(function(){})', true, 'normal functions are constructors');
test('(function(){throw TypeError()})', true, 'normal functions are constructors');
test('(function(){}.bind())', true, 'bounded normal functions are constructors');
test('() => {}', false, 'arrow functions are not constructors');
test('((() => {}).bind())', false, 'bounded arrow functions are not constructors');
test('(function*(){})', false, 'generator functions are not constructors');
test('(function*(){}.bind())', false, 'bounded generator functions are not constructors');
test('(class{})', true, 'classes are constructors');
test('(class extends function(){}{})', true, 'classes are constructors');
test('new Proxy([],{})', false, 'proxies whose target is not constructor are not constructors');
test('new Proxy(function(){},{})', true, 'proxies whose target is a constructor are constructors');
test('new Proxy(function(){},{get:()=>{throw TypeError()}})', true, 'proxies whose target is a constructor are constructors');
test('new Proxy(function(){},{construct:()=>{throw TypeError()}})', true, 'proxies whose target is a constructor are constructors');
test('var r1 = Proxy.revocable([],{}); r1.proxy', false, 'revocable proxies whose target is not a constructor are notconstructors');
test('r1.revoke(); r1.proxy', false, 'revoked proxies whose target was not a constructor are not constructors');
test('var r2 = Proxy.revocable(function(){},{}); r2.proxy', true, 'revocable proxies whose target is a constructor are constructors');
test('r2.revoke(); r2.proxy', true, 'revoked proxies whose target was a constructor are constructors');
testEnd();

function isConstructor(value) {
  if(value === null) return false;
  try {
    return class extends value {}, true;
  } catch(err) {
    return false;
  }
}

function isConstructor(value) {
  if(value === null) return false;
  try {
    return class extends value {}, true;
  } catch(err) {
    return false;
  }
}
var tests = 0,
    failed = 0;  
function test(value, expected, msg) {
  ++tests;
  try {
    var result = isConstructor(window.eval(value));
  } catch(err) {
    result = err;
  }
  if(result !== expected) {
    ++failed;
    console.log('Testing: ' + value + '\nMessage: ' + msg + '\nResult: ' + result + '\nExpected: ' + expected);
  }
}
function testEnd() {
  console.log(failed + ' out of ' + tests + ' tests failed.');
}
test('undefined', false, 'undefined is not a constructor');
test('null', false, 'null is not a constructor');
test('true', false, 'booleans are not constructors');
test('0', false, 'numbers are not constructors');
test('"abc"', false, 'strings are not constructors');
test('Symbol()', false, 'symbols are not constructors');
test('({})', false, '{} is not a constructor');
test('[]', false, 'arrays are not constructors');
test('(function(){})', true, 'normal functions are constructors');
test('(function(){throw TypeError()})', true, 'normal functions are constructors');
test('(function(){}.bind())', true, 'bounded normal functions are constructors');
test('() => {}', false, 'arrow functions are not constructors');
test('((() => {}).bind())', false, 'bounded arrow functions are not constructors');
test('(function*(){})', false, 'generator functions are not constructors');
test('(function*(){}.bind())', false, 'bounded generator functions are not constructors');
test('(class{})', true, 'classes are constructors');
test('(class extends function(){}{})', true, 'classes are constructors');
test('new Proxy([],{})', false, 'proxies whose target is not constructor are not constructors');
test('new Proxy(function(){},{})', true, 'proxies whose target is a constructor are constructors');
test('new Proxy(function(){},{get:()=>{throw TypeError()}})', true, 'proxies whose target is a constructor are constructors');
test('new Proxy(function(){},{construct:()=>{throw TypeError()}})', true, 'proxies whose target is a constructor are constructors');
test('var r1 = Proxy.revocable([],{}); r1.proxy', false, 'revocable proxies whose target is not a constructor are notconstructors');
test('r1.revoke(); r1.proxy', false, 'revoked proxies whose target was not a constructor are not constructors');
test('var r2 = Proxy.revocable(function(){},{}); r2.proxy', true, 'revocable proxies whose target is a constructor are constructors');
test('r2.revoke(); r2.proxy', true, 'revoked proxies whose target was a constructor are constructors');
testEnd();

Is there some way to test it reliably? If not in ES6 or ES7, maybe in some draft or proposed feature?


Solution

  • This is based on the code posted by Jason Orendorff on esdicuss.

    function isConstructor(value) {
      try {
        new new Proxy(value, {construct() { return {}; }});
        return true;
      } catch (err) {
        return false;
      }
    }
    

    function isConstructor(value) {
      try {
        new new Proxy(value, {construct() { return {}; }});
        return true;
      } catch (err) {
        return false;
      }
    }
    var tests = 0,
        failed = 0;
    function test(value, expected, msg) {
      ++tests;
      try {
        var result = isConstructor(window.eval(value));
      } catch(err) {
        result = err;
      }
      if(result !== expected) {
        ++failed;
        console.log('Testing: ' + value + '\nMessage: ' + msg + '\nResult: ' + result + '\nExpected: ' + expected);
      }
    }
    function testEnd() {
      console.log(failed + ' out of ' + tests + ' tests failed.');
    }
    test('undefined', false, 'undefined is not a constructor');
    test('null', false, 'null is not a constructor');
    test('true', false, 'booleans are not constructors');
    test('0', false, 'numbers are not constructors');
    test('"abc"', false, 'strings are not constructors');
    test('Symbol()', false, 'symbols are not constructors');
    test('({})', false, '{} is not a constructor');
    test('[]', false, 'arrays are not constructors');
    test('(function(){})', true, 'normal functions are constructors');
    test('(function(){throw TypeError()})', true, 'normal functions are constructors');
    test('(function(){}.bind())', true, 'bounded normal functions are constructors');
    test('() => {}', false, 'arrow functions are not constructors');
    test('((() => {}).bind())', false, 'bounded arrow functions are not constructors');
    test('(function*(){})', false, 'generator functions are not constructors');
    test('(function*(){}.bind())', false, 'bounded generator functions are not constructors');
    test('(class{})', true, 'classes are constructors');
    test('(class extends function(){}{})', true, 'classes are constructors');
    test('new Proxy([],{})', false, 'proxies whose target is not constructor are not constructors');
    test('new Proxy(function(){},{})', true, 'proxies whose target is a constructor are constructors');
    test('new Proxy(function(){},{get:()=>{throw TypeError()}})', true, 'proxies whose target is a constructor are constructors');
    test('new Proxy(function(){},{construct:()=>{throw TypeError()}})', true, 'proxies whose target is a constructor are constructors');
    test('var r1 = Proxy.revocable([],{}); r1.proxy', false, 'revocable proxies whose target is not a constructor are notconstructors');
    test('r1.revoke(); r1.proxy', false, 'revoked proxies whose target was not a constructor are not constructors');
    test('var r2 = Proxy.revocable(function(){},{}); r2.proxy', true, 'revocable proxies whose target is a constructor are constructors');
    test('r2.revoke(); r2.proxy', true, 'revoked proxies whose target was a constructor are constructors');
    testEnd();

    Proxy objects are only constructors if their initial target was a constructor. From ProxyCreate,

    If target has a [[Construct]] internal method, then

    • Set the [[Construct]] internal method of P as specified in 9.5.14.

    Therefore, the code creates a proxy object whose target is the value we want to check, and whose handler has a construct trap which doesn't throw.

    This way, if the proxy is a constructor (i.e. the tested value is a constructor), when instantiated it will run the code in the trap instead of redirecting the operation to the target, and it won't throw. If the proxy is not a constructor (i.e. the tested value is neither), when instantiated it will throw an error.

    There is a little problem, though. When creating a proxy, the target must be either a non-proxy object, or a non-revoked proxy object. Otherwise, it throws, and thus the code above considers it a non-constructor.

    This is OK in case of primitives, because they can't be constructors. However, revoked proxies can be constructors or not, and we can't test them properly.

    You might want to detect if the value is a revoked proxy in order to handle that case differently.