0

I created a function and prototype with the following:

function Range(from, to) {
    if (new.target === undefined) {
        throw new Error("Range must be invoked with new.")
    }
    Object.assign(this, {from, to});
}
Range.prototype = {
    includes: function(x) {
        return x >= this.from && x <= this.to;
    },
    toString: function() {
        return `[${this.from}, ${this.to}]`;
    }
}

And here is what it shows in Chrome's Console:

enter image description here

If we use the includes function as an example, the name is the function name and the length is the arity of the function. And [[FunctionLocation]] seems to be part of the underlying implementation of the JS engine, so not important for actual js debugging. But then what are the following:

  • Caller? What would be an example of when that would be non-null.
  • Arguments? Again, what would be an example of when that would be non-null
David542
  • 104,438
  • 178
  • 489
  • 842
  • You're writing a function instead of a proper [class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) using pre-class "fake oo". A function has a caller and arguments when it is called and arguments are passed. –  Apr 11 '22 at 19:56
  • [`function.caller`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/caller) and `[`function.arguments`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/arguments) – Barmar Apr 11 '22 at 19:58
  • @ChrisG sure but if I do `function add(x,y) {return x+y;}` it returns `null` for both as well, so what does that show exactly? – David542 Apr 11 '22 at 19:58
  • @Barmar I see, thanks for the link. Can you show an example in the console where those two properties would not be `null`? – David542 Apr 11 '22 at 19:59
  • There are examples in those documentation pages. – Barmar Apr 11 '22 at 20:00
  • Try `function add(x, y) { console.log(add.arguments); return x+y;}` and then call the function. – Barmar Apr 11 '22 at 20:02
  • @Barmar shows the same -- `null`. Note I'm not jus looking to print the args with a console, but actually to see the two properties in the console show as not-null -- i.e., understand what they're doing. Also copied what the one example from the docs page verbatim in the console and also didn't show up anything. – David542 Apr 11 '22 at 20:07
  • The properties are only set while the function is executing. If you want to see it in the debugger, put a breakpoint into the code and examine the function while stopped at the breakpoint. – Barmar Apr 11 '22 at 20:09
  • @Barmar got it. Throwing in `debugger;` in the middle gave me the arguments: `arguments: Arguments(2) [2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]`. How do I also get the `caller` ? – David542 Apr 11 '22 at 20:14
  • 1
    The caller should show up there, too. You have to call it from another function, it's null when called from top-level (this is explained in the documentation I referred you to). – Barmar Apr 11 '22 at 20:16
  • @Barmar this should be the correct link, right? https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments – David542 Apr 11 '22 at 20:19
  • The value of `Function.arguments` is an object of that type. – Barmar Apr 11 '22 at 20:24

2 Answers2

1

Both of these properties are only filled in while the function is executing. So to see them in the debugger, you need to stop the function with a breakpoint.

If you do that, Range.arguments will contain information about the arguments, and Range.caller will contain the function that called Range() (this will be null if Range() was called from top-level).

function Range(from, to) {
    if (new.target === undefined) {
        throw new Error("Range must be invoked with new.")
    }
    Object.assign(this, {from, to});
    debugger;
}
Range.prototype = {
    includes: function(x) {
        return x >= this.from && x <= this.to;
    },
    toString: function() {
        return `[${this.from}, ${this.to}]`;
    }
}

function callRange() {
  x = new Range(1, 10);
  console.log(x);
}

callRange();

enter image description here

Barmar
  • 741,623
  • 53
  • 500
  • 612
1

for ES6 beginners

If you use the recommend class syntax, you will get automatic errors without having to manually check this or new.target -

class Range {
  constructor(from, to) {
    Object.assign(this, {from, to})
  }
}

// with `new`
const p = new Range(1,2)
console.log(p)

// without `new`
const q = Range(1,2)
console.log(q)
{ from: 1, to: 2 }
TypeError: Cannot call a class constructor without |new|

Instead of overriding Range.prototype with a handwritten object, you can define so-called instance methods directly in the class -

class Range {
  constructor(from, to) {
    Object.assign(this, {from, to})
  }
  includes(x) {
    return x >= this.from && x <= this.to
  }
  toString() {
    return `[${this.from}, ${this.to}]`
  }
}

const p = new Range(3,6)
console.log(`p: ${p}`)
console.log(`p includes 4: ${p.includes(4)}`)
console.log(`p includes 9: ${p.includes(9)}`)
p: [3, 6]
p includes 4: true
p includes 9: false

ES2020 and beyond

I will mention that there is growing support for modular design over large classes. If Range were to include 20 methods but the caller only uses 1 or 2 of them, 18 methods are bundled into the final app but remain unused. It would take an extremely sophisticated compiler to remove this dead code, a process called tree shaking. Below we see range written as a module instead of a class. Note only public methods are made accessible using export, allowing the module author to restrict access to private methods wherever she/he wishes -

// range.js
const tag = Symbol()
const range = (from, to) => ({ tag, from, to })
const isRange = (t) => t.tag === tag
const includes = (t, x) => x >= t.from && x <= t.to
const toString = (t) => `[${t.from}, ${t.to}]`
export { range, isRange, includes, toString }

In the main module, the caller only imports what is needed, allowing a compiler to effectively prune any unused portions of the imported modules -

// main.js
import { range, includes, toString } from "./range.js"
const p = range(3,6)
console.log(`p: ${toString(p)}`)
console.log(`p includes 4: ${includes(p,4)}`)
console.log(`p includes 9: ${includes(p,9)}`)
p: [3, 6]
p includes 4: true
p includes 9: false

For more info on import, see Dynamically Importing ES modules by Alex Rauschmayer.

For more insight on the growing ES module initiative, see projects like v9 of Google Firebase's new API.

  • Version 8. This is the JavaScript interface that Firebase has maintained for several years and is familiar to Web developers with existing Firebase apps. Because Firebase will remove support for this version after one major release cycle, new apps should instead adopt version 9.
  • Modular version 9. This SDK introduces a modular approach that provides reduced SDK size and greater efficiency with modern JavaScript build tools such as webpack or Rollup.

Version 9 integrates well with build tools that strip out code that isn't being used in your app, a process known as "tree-shaking." Apps built with this SDK benefit from greatly reduced size footprints. Version 8, though available as a module, does not have a strictly modular structure and does not provide the same degree of size reduction. ...

have your cake and eat it too

Using a simple technique, we can write modular-based code and still offer an object-oriented interface, if desired. Below we write a Range class as a simple wrapper around the module functions above -

// range.js

// ...

class Range {
  constructor(t) { this.t = t }
  isRange() { return isRange(this.t) }
  includes(x) { return includes(this.t, x) }
  toString() { return toString(this.t) }
  static new(a,b) { return new Range(range(a,b)) }
}

export { Range, ... }

In your main module, you can import the Range class to access all of the module's features through an object-oriented interface -

// main.js
import { Range } from "./range"

const p = Range.new(3,6)
console.log(`p: ${p}`)
console.log(`p includes 4: ${p.includes(4)}`)
console.log(`p includes 9: ${p.includes(9)}`)

Now your module can be used in functional style or object-oriented style. Note, when used this way the object-oriented interface comes at the cost of not being able to tree-shake this portion of the code. However, programs that wish take advantage of dead code elimination can use the functional style interface instead.

Verify in this runnable demo below -

// range.js
const tag = Symbol()
const range = (from, to) => ({ tag, from, to })
const isRange = (t) => t.tag === tag
const includes = (t, x) => x >= t.from && x <= t.to
const toString = (t) => `[${t.from}, ${t.to}]`

class Range {
  constructor(t) { this.t = t }
  isRange() { return isRange(this.t) }
  includes(x) { return includes(this.t, x) }
  toString() { return toString(this.t) }
  static new(a,b) { return new Range(range(a,b)) }
}

// main.js
const p = Range.new(3,6)
console.log(`p: ${p}`)
console.log(`p includes 4: ${p.includes(4)}`)
console.log(`p includes 9: ${p.includes(9)}`)
p: [3, 6]
p includes 4: true
p includes 9: false

I have written about this technique in other answers -

Mulan
  • 129,518
  • 31
  • 228
  • 259