Notes from YDKJS - this & Object Prototypes
Table of contents
You can find some notes I wrote while reading this & Object Prototypes from the You Don’t Know JS Yet book series. I feel the same is true for this book as with Async & Performance: Good points, complex language. Additionally, although I appreciate the effort he puts in pointing out all the small details, I felt this time I wasn’t getting any practical knowledge until the end of the book (Chapter 6), which seems a bit too late.
Chapter 1: this Or That?
this
is not a reference to the function’s lexical scope or to itself, but instead, is a binding that happens when the function gets called. What’s this?
Chapter 2: this All Makes Sense Now!
Finding the call-site
To figure out what this
refers to in a function call, you have to find the call-site which is the place in code where the function was called. The best way to find the call-site is to add a breakpoint inside the function you’re interested in and open the developer tools. The last item on the stack is the function in question and the second last the call-site; you can search there to find out how the function was called. Call-site.
Types of binding
Default binding is used when none of the following rules apply. We talk about standalone function calls, and in this case,
this
refers to the global object. Default binding.Implicit binding. You call an object’s function, so in this case,
this
points to the object:obj.foo()
Implicit Binding. You lose implicit binding if you pass a reference of that object’s function to a variable, and then you call that variable (function): Implicitly lost.function foo() { console.log(this.a); } var obj = { a: 2, foo: foo, }; var bar = obj.foo; // function reference/alias! var a = "oops, global"; // `a` also property on global object bar(); // "oops, global"
Explicit binding when we force
this
to a desired object when we call the function. You can do that withbind
,apply
, orcall
.Use
bind
to setthis
to an object and pass some arguments to the function.bind
also creates and returns a new function you can store in a variable.apply
andcall
let you do the same thing(setthis
and arguments), but instead of returning the function, they call it. The difference betweenapply
andcall
is thatapply
gets the arguments (after thethis
parameter) as an array, butcall
accepts each argument separately.Bind has a
.name
property, for example, inbar = foo.bind(..)
,bar.name
is equal to “bound foo.” Explicit binding.new
binding. Constructors in JavaScript are plain functions that “hijack” the use ofnew
in front of them.When a function is invoked with
new
in front of it (aka constructor call), the following things happen:- a brand new object is created.
- that object is [[Prototype]]-linked with constructor object.
- that object =
this
inside the constructor call. - the constructor returns that newly created object—if you don’t return something else.
For example:
function person(name) { this.name = name; } var mark = new person("Mark"); console.log(mark.name); // "Mark"
This is the final way you can bind
this
inside a function call, and it’s callednew
binding. new binding
Order of precedence for binding rules
- Default binding < Implicit < Explicit < new.
- You can’t use
new
along withapply
orcall
, only withbind
.
Now, we can summarize the rules for determining
this
from a function call’s call-site, in their order of precedence. Ask these questions in this order, and stop when the first rule applies.
Is the function called with
new
(new binding)? If so,this
is the newly constructed object.var bar = new foo()
Is the function called with
call
orapply
(explicit binding), even hidden inside abind
hard binding? If so,this
is the explicitly specified object.var bar = foo.call( obj2 )
Is the function called with a context (implicit binding), otherwise known as an owning or containing object? If so, this is that context object.
var bar = obj1.foo()
Otherwise, default the
this
(default binding). If in strict mode, pickundefined
, otherwise pick the global object.var bar = foo()
That’s it. That’s all it takes to understand the rules of this binding for normal function calls. Well… almost.
Everything in order, Determining this
Binding exceptions
If you pass
null
orundefined
as athis
binding parameter tocall
,apply
, orbind
,this
will now refer to the global object. This is useful when you want to partially apply arguments withbind
or spread arguments withapply
(in ES6, the last is not relevant anymore):function foo(a, b) { console.log("a:" + a + ", b:" + b); } // currying with `bind(..)` var bar = foo.bind(null, 2); bar(3); // a:2, b:3 // spreading out array as parameters foo.apply(null, [2, 3]); // a:2, b:3
It’s better to pass something though—see Safer this—instead of
null
, because with default binding and a third-party library you can pollute the global object if that library uses somewherethis
. Ignored thisSoftening binding: Hard-binding greatly reduces the flexibility of a function, preventing manual
this
override with either the implicit binding or even subsequent explicit binding attempts. Shows a utility that implements soft binding. Softening binding.
Lexical binding
You can achieve lexical binding with arrow functions or with the self=this
pattern. Lexical this.
Chapter 3: Objects
When you create a primitive string, number, or boolean, the engine coerces it to the object form; that’s why you can use methods like toString. Built-in Objects.
Property Descriptors:
- Get the characteristics for an object property:
Object.getOwnPropertyDescriptor(myObject, "a");
- Define a new property:
Object.defineProperty(myObject, "a", { value: 2, writable: true, configurable: true, enumerable: true, });
- writable is the ability to change the value of a property
- configurable is the ability to modify the property description with
Object.defineProperty
- enumerable, if you can use it in for-in loops. Property Descriptors.
- Get the characteristics for an object property:
Immutability for objects (tricks):
var myObject = {}; Object.defineProperty(myObject, "FAVORITE_NUMBER", { value: 42, writable: false, configurable: false, });
var myObject = { a: 2, }; Object.preventExtensions(myObject); myObject.b = 3; myObject.b;
Object.seal()
=Object.preventExtensions()
+configurable:false
Freeze + deep freeze.
Object.freeze()
=Object.seal()
+writable:false
. Deep freeze if you do the previous recursively for every nested property.
When you define a property to have either a getter, a setter, or both, its definition becomes an “accessor descriptor” (as opposed to a “data descriptor”).
Two ways to define a getter:
// 1. var myObject = { // define a getter for a get a() { return 2; }, }; // 2. Object.defineProperty( myObject, // target "b", // property name { get: function () { return this.a * 2; }, enumerable: true, } ); myObject.a; // 2 myObject.b; // 4
Don’t forget to define a setter:
var myObject = { // define a getter for a get a() { return this._a_; }, // define a setter for a set a(val) { this._a_ = val * 2; }, }; myObject.a = 2; myObject.a; // 4
Regular objects do not have a built-in iterator as the arrays. Iteration
Chapter 4: Mixing (Up) “Class” Objects
Classes means copies, and JavaScript does not automatically create copies (as classes imply) between objects.
Why we call the parent constructor with
.call(this, arguments)
in a subclass (with the pre-ES6 class syntax):But if we said
Vehicle.drive()
, thethis
binding for that function call would be theVehicle
object instead of theCar
object (see Chapter 2), which is not what we want. So, instead, we use.call(this)
(Chapter 2) to ensure thatdrive()
is executed in the context of theCar
object.Read TL;DR for more.
Chapter 5: Prototypes
When you want to access an object property, if that property is not on the object, we search for it in the
[[Prototype]]
chain. If we still don’t find it there, you’ll getundefined
[[Prototype]].At the top of the prototype chain is the
Object.prototype
. Object.prototype.Shadowing a property on an object, that already exists higher on the prototype chain, is not that straightforward if you use the assignment operator. See the 3 cases in Setting & Shadowing properties. You can’t shadow if the property is not writable or has a setter up in the look-up chain. In those cases, you’ll have to use the
Object.defineProperty
method. You should try to avoid shadowing, see chapter 6 for an alternative.Object.create
creates a new object that has the given object as its internal prototype. MDN. By internal prototype I mean the hidden[[Prototype]]
you access with__proto__
orObject.getPrototypeOf()
, not theMyClass.prototype
that’s used with thenew
keyword. By the way, the equivalent ofvar a = new MyClass()
withObject.create
isvar a = Object.create(MyClass.prototype)
—not 100% the same, you still have to call the constructor function to initialize the object.The
.prototype
property in an object will be used as the internal[[Prototype]]
for the objects we create with thenew
operator. In the following example, the internal[[Prototype]]
of objecta
is the objectFoo.prototype
:function Foo() { // ... } var a = new Foo(); Object.getPrototypeOf(a) === Foo.prototype; // true
“Inheritance” implies a copy operation, and JavaScript doesn’t copy object properties (natively, by default). Instead, JS creates a link between two objects, where one object can essentially delegate property/function access to another object. “Delegation” (see Chapter 6) is a much more accurate term for JavaScript’s object-linking mechanism.
In other words, in JavaScript, it’s most appropriate to say that a “constructor” is any function called with the
new
keyword in front of it. Functions aren’t constructors, but function calls are “constructor calls” if and only ifnew
is used.The constructor property does not mean “constructed by.” It’s a prototype lookup. Don’t trust the constructor property.
function Foo() { /* .. */ } // If you don't replace the prototype object like this, the // constructor property will point to Foo. Foo.prototype = { /* .. */ }; var a1 = new Foo(); a1.constructor === Foo; // false! a1.constructor === Object; // true!
If you want to inherit, use
Bar.prototype = Object.create(Foo.prototype)
. WithBar.prototype = Foo.prototype
you say thatBar.prototype
is the same object asFoo.prototype
, and that means you change both when you start adding properties to any of them. In ES6 you can also useObject.setPrototypeOf(Bar.prototype, Foo.prototype)
. He also mentioned that you can use the__proto__
property for that, but it’s not recommended because it’s a legacy feature. Prototypal inheritance.Don’t use
instanceof
to determine an object’s prototype, but instead useFoo.prototype.isPrototypeOf(a)
, or justb.isPrototypeOf(c)
. You can also ask for the prototype withObject.getPrototypeOf(a)
.You don’t want to use
instanceof
because it pretends to check if the object is related to the class (constructor function), but in reality, it checks if it’s related to/has the.prototype
property.The strange .
__proto__
(not standardized until ES6!) property “magically” retrieves the internal[[Prototype]]
of an object as a reference, which is quite helpful if you want to directly inspect (or even traverse:.__proto__.__proto__
) the chain.You can think of
__proto__
as a getter/setter:Object.defineProperty(Object.prototype, "__proto__", { get: function () { return Object.getPrototypeOf(this); }, set: function (o) { // setPrototypeOf(..) as of ES6 Object.setPrototypeOf(this, o); return o; }, });
Object.create()
can be used instead of thenew
operator in front of a function. This way you don’t have to deal with the.prototype
,.constructor
, andnew
.An object created with
object.create(null)
is not linked to any other object, so it can be used for data storage—without surprises from the prototype chain look-up.We don’t need classes to create meaningful relationships between two objects. The only thing we should really care about is objects linked together for delegation, and
Object.create()
gives us that linkage without all the class cruft.
Chapter 6: Behavior Delegation
The example in Delegation Theory.
var Task = { setID: function (ID) { this.id = ID; }, outputID: function () { console.log(this.id); }, }; // make `XYZ` delegate to `Task` var XYZ = Object.create(Task); XYZ.prepareTask = function (ID, Label) { this.setID(ID); this.label = Label; }; XYZ.outputTaskDetails = function () { this.outputID(); console.log(this.label); }; // ABC = Object.create( Task ); // ABC ... = ...
Differences between object-oriented (OO) and objects-linked-to-other-objects (OLOO) styles.
You want the state to be on the delegators (
XYZ
,ABC
) not on the delegate (Task
).You avoid naming things the same in delegators and delegates because:
having those name collisions creates awkward/brittle syntax to disambiguate references (see Chapter 4), and we want to avoid that if we can.
In class design, you do that because of polymorphism.
The general utility methods that exist on
Task
are available to us while interacting withXYZ
becauseXYZ
can delegate toTask
.
Behavior delegation means: let some object (
XYZ
) provide a delegation (toTask
) for property or method references if not found on the object (XYZ
). Think of objects as peers instead of the typical parent/child relationship.OO vs OLOO example
Object-oriented:
function Foo(who) { this.me = who; } Foo.prototype.identify = function () { return "I am " + this.me; }; function Bar(who) { Foo.call(this, who); } Bar.prototype = Object.create(Foo.prototype); Bar.prototype.speak = function () { alert("Hello, " + this.identify() + "."); }; var b1 = new Bar("b1"); var b2 = new Bar("b2"); b1.speak(); b2.speak();
the same functionality in OLOO:
var Foo = { init: function (who) { this.me = who; }, identify: function () { return "I am " + this.me; }, }; var Bar = Object.create(Foo); Bar.speak = function () { alert("Hello, " + this.identify() + "."); }; var b1 = Object.create(Bar); b1.init("b1"); var b2 = Object.create(Bar); b2.init("b2"); b1.speak(); b2.speak();
Don’t forget to check the images that show OLOO is a simpler design than OO in JavaScript.
Gives 3 practical examples with UI elements—widgets and buttons—one with pre-ES6 classes, one with ES6 class, and one with OLOO.
With OLOO we choose different names for the methods we want to “polymorph”—which is not the best in my opinion (naming is hard), despite what the writer says that they are more descriptive. At least, we don’t have to call the constructor and methods—we want to polymorph—with
.call
.With OLOO we use two calls to construct and initialize the objects:
var btn1 = Object.create(Button); btn1.setup(125, 30, "Hello");
compared to the one with classes:
var btn1 = new Button(125, 30, "Hello")
. This can be thought of as an advantage because it’s a separation of concerns (creation, initialization).He gives another example where he implements an authentication and a login controller. He starts with pre-ES6 classes where both controllers inherit from a
Controller
class—AuthController
andLoginController
—but theAuthController
also holds a reference to aLoginController
that instantiates in the constructor (composition pattern). He later creates with OLOO aLoginController
and anAuthcontroller
that delegates to the first. This is a simpler design because there’s no commonController
class, and the method names are more descriptive because there’s no polymorphism. Simpler DesignA supposed advantage of the ES6 class syntax is the omission of the
function
keyword in class methods. He argues that you can do the same thing inside objects with OLOO using ES6 syntax. Nicer Syntax.
Appendix A: ES6 class
Starts with a recap on the problems the class design has in JavaScript.
Advantages of the ES6 Class syntax over pre-ES6 class syntax.
- No
.prototype
methods all over the code. extends
instead of.prototype = Object.create
to show inheritance.super
in constructors and inside methods (polymorphism) instead of.call(this, arg, arg)
- If I understand this, ES6 classes don’t allow you to define properties outside the constructor; you can define only methods because those properties would be shared state between the “instances”.
- With
extends
, you can inherit from built-in objects, like arrays, which was difficult with pre-ES6 class syntax.
Problems still exist, though, in ES6 Class syntax Class gotchas
- They are not real classes, just linked objects with
prototype
. If you replace, maybe by accident, a method on the parent class, at runtime, all the “instances” will be affected—he shows an example. - Accidental shadowing is still a problem.
- I don’t mention the
super
gotchas, and as a result, the Static > Dynamic? stuff because I’m not sure if they are practical.
Other Links
- This article is so good. MDN: Inheritance and the prototype chain
- Pre-ES6 class syntax: Understanding Prototypes and Inheritance in JavaScript by Tania Rascia.
- ES6 class syntax. Nice article, but forgets to extend the prototype, in the pre-ES6 class example, something she does in the previous post: Understanding Classes in JavaScript by Tania Rascia.
- See also YDKJS - ES-next & Beyond - Classes:
Notes from the classes chapter
Differences between ES6 class and prototype class syntax:
The
new
keyword is required in ES6 classes.ES6 classes are not hoisted at the top, which means you can’t use them in the code before you define them.
With a simple
extends
keyword you can create a delegation link between objects, or if you prefer create prototype inheritance. extends and superIf you omit the constructor in a subclass, you get the default constructor:
constructor(...args) { super(...args); }
You can’t reference
this
in a subclass until you callsuper(args)
because the parent constructor creates thethis
. The opposite is true for pre-ES6 class syntax—the subclass constructor creates thethis
and then you call the parent constructor withParent.call(this, arg)
.A cool feature of the ES6 class syntax is that you can extend the native classes (Array, Error, etc.) Extending Natives.
Use
new.target
to access the constructor of the newly created object (inside the constructors). new.targetUse the
static
keyword to create static methods, but be careful because they are not available on the prototype chain. That means you can’t access them withobj1.staticProperty
, only withMyObject.staticProperty
. static
Other things to read
Popular
- Reveal animations on scroll with react-spring
- Gatsby background image example
- Extremely fast loading with Gatsby and self-hosted fonts