javascript: what do the new keywords do internally

208 views Asked by At

I know there are maybe a million similar questions out there already, e.g.,

but please hear me out.

The code:

let f = function(){console.log(".f.")};
fn = new f();

// Now:
typeof fn === "object" //true
//! fn() //TypeError: fn is not a function
//! new fn() //TypeError: fn is not a constructor

The general question would be: is it possible to create a "newable" object fn, by manipulating the functionf.

The question breaks down to the internal of the "new" keywords.

My doubt is, according to the MDN document, when a new keyword is used, the constructor of an class or function is called. However, even though fn.__proto__.constructor === f is true like all other javascript functions, fn is of type'object' (can we somehow alter it to 'function'?), and new fn() throws TypeError.

We can even add more to the mix by doing:

fn.constructor = f.constructor 
fn.__proto__ = f.__proto__
fn.prototype = f.prototype
// f.constructor === Function //true
//! fn.call(this) //fn.call is not a function

still, fn() won't work, neither does new fn or new fn().

Why?

2

There are 2 answers

13
Ben Aston On BEST ANSWER

The new-able objects in JavaScript are:

  1. Functions created using the function keyword (excluding generator functions)
  2. Classes (which can be treated as functions)
  3. Bound function exotic objects ("bound functions")
  4. Some host objects
  5. Proxies if they are applied to one of the above

I know this because the only object types the new operator works with are "constructors" (specification term). A constructor is an object with a [[Construct]] internal method, and you can search the ECMAScript specification to find out which kinds of object have a [[Construct]] internal method.

To make the result of a constructor function new-able, therefore, you must return one of the object kinds listed above.

Note that the specification specifically says that all constructors are definitionally functions because they must support the [[Call]] internal method (note also the caveat below about host objects).

If you want to get very advanced, then you may be interested to learn that host objects do not appear to share the ordinary limitations for constructors (presumably for legacy Web compatibility reasons), but these are exceptional.

Explanation of the .constructor property

When a new-able function f is declared, two objects are created: the function-object f itself, and a default object on the .prototype own-property of f. The .constructor property of this default .prototype object is automatically set by the runtime to be f. I believe classes work in a very similar fashion. Note that the fact that the name of this property was chosen to be "prototype" makes discussing prototypes quite confusing in JavaScript (as it is distinct from the [[prototype]] of the function).

This constructor own-property on the object positioned on the .prototype property, is never read by any built-in function or operation (that I know of). I view it as vestigial from the earliest days of JavaScript - it's original intent was as a way to maintain a link between the "class" that constructed an object as a developer affordance. Host environments (eg browsers) sometimes use it to infer the "type" of an object for the purposes of communicating with the user (eg. console output), the property is writeable and therefore unreliable.

Steps performed by the new operator

At a high level, when new is invoked against a constructor, the following steps occur (spec contains full details):

  1. A new object o is created
  2. The [[Prototype]] ("the prototype") of o is set to the value of the .prototype property of the constructor (note this means the .constructor property is inherited by the new object)
  3. The target (ie this) of the constructor body is set to o
  4. The constructor is run with the this value defined above
  5. If there is no explicit object-type return value, then o is returned by default
0
schen On

The Scope: we exam the new on Function only. because this is the part that confuse the most. Calling new on Class yield results similar to that from other major OOP languages.

The original question could be break down to the following two questions:

  1. What is the detailed construction process, when a new keyword is called?

  2. How does JavaScript decide if an Object is Callable? (Thanks for @BenAston mentioning that the new keywords may only works with a limited set of Objects (e.g., prefixed with Class or Function))


  1. Answer to the first question:

Back to the MDN Document,

When the code new Foo(...) is executed, the following things happen:

  1. A new object is created, inheriting from Foo.prototype.
  2. The constructor function Foo is called with the specified arguments, and with this bound to the newly created object. new Foo is equivalent to new Foo(), i.e. if no argument list is specified, Foo is called without arguments.
  3. The object (not null, false, 3.1415 or other primitive types) returned by the constructor function becomes the result of the whole new expression. If the constructor function doesn't explicitly return an object, the object created in step 1 is used instead.(Normally constructors don't return a value, but they can choose to do so if they want to override the normal object creation process.)

The words might be ambiguous, But the PoC code is as follows:

// Case1, function has no return value; 
// A new object is created, f0n.__proto__ === f0.prototype
let f0 = function() {};
f0.prototype.f0p = "f0p";

let f0n = new f0();
console.log(f0n) // f0n is a new Object, inheriting from f0.prototype
console.log(f0n.__proto__.f0p); // "f0p"

// Case3, function has an explicit return value, the value is an object
// (not null, false, 3.1415 or other primitive types); 
// the return value becomes the new object value.
let f3 = function() {
  return {
    "f3": "f3"
  }
};
f3.prototype.f3p = "f3p";

let f3n = new f3();
console.log(f3n) // {f3: "f3"}
// f3n is an Object, the return value of its constructor function `f3`
console.log(f3n.__proto__.f3p); // undefined

// Case4 (or Case1 again), function has an **implicit** return value.
let f4 = function(a) {
  return (a + a)
};
f4.prototype.f4p = "f4p";

let f4n = new f4();
console.log(f4n.__proto__.f4p); // "f4p"

2.Answer to the second question:

I still do not yet know how JavaScript decide if a object is callable. The answer should be hiding in the ECMAScripts spec. (Thanks @BenAston for pointing out)

It might be legit to assume that only Function is callable. And the following post provide a workaround: How to make an object callable

  • extra: how to return an Callable?

Use the Case3, let f = Function(){return Function(){}} Since the return value is an non-primitive explicit Object, it becomes the result of the new directive. The result is a function, which could then be called.