- Magento 2: Defaults, uiElement, Observables, and the Fundamental Problem of Userland Object Systems
- Magento 2: Javascript Primer for uiElement Internals
- Tracing Javascript’s Prototype Chain
- Magento 2: uiElement Standard Library Primer
- Magento 2: Using the uiClass Object Constructor
- Magento 2: uiClass Internals
Before we can get started exploring Magento’s uiElement
object system, there are some javascript fundamentals to cover. This may be review for some of you, and may be brand new content for others. While you don’t need to have mastery of these concepts to use Magento’s new object system, if you’re trying to reason programmatically about the uiElement internals you’ll want to understand the bedrock they’re built on.
Also, I should probably warn you, as someone who’s been programming with javascript since the Netscape 2 days, my perspective is going to be a little skewed when compared with a modern, or modernizing javascript developer. Feedback is more than welcome.
Finally, most of the code below can be run in your browser’s javascript console. In Google Chrome that’s View -> Developer -> Javascript Console
. Any browser specific code, unless otherwise mentioned, is strictly accidental.
Javascript and this
The this
variable in javascript is a little weird. If you’re coming from PHP, $this
always refers to “the current object” in a class file.
class A
{
function bar()
{
//this
}
function foo()
{
return $this->bar();
}
}
The this
variable in javascript is a much weirder thing. Depending on where and how you use it, this
can refer to a bunch of different things.
First off, if you’re using this
in the global scope (i.e. not in a function), then this
refers to that global scope. In a browser, that means this
and window
are also the same thing (again, with this
in the global scope).
foo = 'bar';
console.log(this.foo === foo); //returns true
console.log(window.foo === foo); //returns true
//equality for objects in javascript checks their identity
//i.e. is this the same object
console.log(this === window); //returns true
In a standard function or method call, this
also (by default) refers to the window
object;
var foo = function(){
console.log(this === window); //true
}
foo();
However, if you attach the function to an object as a method, this
refers to the object you’ve attached the function to. Consider the output of the following program.
//define an object
window.object = {};
//define the function
var foo = function(){
console.log(this === window);
console.log(this === window.object);
}
//assign function as method
window.object.myMethod = foo;
//call as function
foo(); //results in true, false
//call as method
window.object.myMethod(); //results in false, true
In the first function call, this
points to window
. In the second method call, this
points to the object.
That’s tier 1 of the weirdness of this
. Tier 2 is what happens when you call functions or methods dynamically in javascript.
Just like PHP has call_user_func
and call_user_func_array
, javascript has object methods which can call a method. These methods are named call
and apply
. PHP’s function calling functions exists to call named functions based on string values. The call
and apply
methods exist for a different reason.
Consider this slightly modified program
//define an object
window.object = {};
//define the function
var foo = function(){
console.log(this === window);
console.log(this === window.object);
}
//assign function as method
window.object.myMethod = foo;
//call as function
foo.call(); //results in true, false
foo.apply(); //results in true, false
//call as method
window.object.myMethod.call(); //results in true, false
window.object.myMethod.apply(); //results in true, false
Here we’ve replaced the direct function and method calls with calls to call()
and apply()
. Our use of call
and apply
above are a little silly — they make no change in the program. That’s because we’re not using call
and apply
‘s main feature — the ability to bind a different object to this
. Consider this modified program.
//define out new object we'll bind to this
var boundObject = {};
boundObject.foo = 'Hello!';
//define an object
window.object = {};
//define the function
var foo = function(){
console.log(this === window);
console.log(this === window.object);
console.log(this);
}
//assign function as method
window.object.myMethod = foo;
//call as function
foo.call(boundObject); //results in false, false
//logs boundObject
foo.apply(boundObject); //results in false, false
//logs boundObject
//call as method
window.object.myMethod.call(boundObject); //results in false, false
//logs boundObject
window.object.myMethod.apply(boundObject); //results in false, false
//logs boundObject
The first argument to both call
and apply
is the object you want bound as this
during the function call. By passing in boundObject
we’ve ensured that this
points to neither the window
or window.object
objects. Instead, it points to the boundObject
object.
There’s a third method that can bind a different value to this
, and that’s bind
. The bind
method allows you to create a new function with a different value bound to this
. Consider the following
var boundObject = {};
boundObject.foo = 'Hello!';
var foo = function() {
console.log(this);
}
var newFunction = foo.bind(boundObject);
foo(); //logs `window` object
newFunction(); //logs boundObject
In the call to foo
, this
points to the window
object. In the call to newFunction
, it points to the boundObject
.
The main thing we’d like you to take away from all of this is — when you’re looking at a function in javascript and see the this
variable, don’t assume you know what’s in there. While javascript’s dynamic nature makes fertile ground for framework developers, every framework likes to do different, clever things with binding this
. Learn the assumptions of your framework, but remember that one small bit of extra cleverness may mean this
isn’t as it seems.
For those interested in learning more, The Mozilla Developer Network (MDN) has more information (including how to use these methods with function arguments) on this
, call
, apply
, and bind
.
Javascript Objects
Javascript is an object oriented language without classes. While ES6 introduced a class
keyword, this is syntactic sugar born of a compromise between folks who want to keep javascript a language without classes and those who think javascript needs classes. We’re going to sidestep the issue and ask that you consider javascript as a class-less language since ES6 isn’t evenly distributed, and it’s important to understand where a language came from.
Traditionally there are two ways to create objects in javascript. The first is via object literal assignment
object = {foo:'bar'};
The second is via a constructor function and the new
keyword.
var ConstructorFunction = function(){
this.foo = 'bar';
}
object = new ConstructorFunction();
A constructor function returns no value. When you use a constructor function with the new
keyword, the this
variable is bound to the object you’re instantiating. This is a weird, non obvious, design decision.
This is usually where a javascript tutorial links off to a historical account of how javascript was created in 10 days, so we’ll do the same. (we’d be remiss to point out that we think javascript was created in 10 days plus the entire length of Brendan Eich’s career up to that point)
We call these object creating functions “constructor functions”, but to javascript they’re just regular functions. If you call a constructor function without the new
keyword, javascript will treat it as a regular function call.
//returns null -- whoops!
object = ConstructorFunction();
One last thing before we move on. It’s an old convention (unenforced by the language) in javascript that, if you intend other programmers to use a function as a constructor function, that you name the function with LeadingCase
vs. naming it with lowerCase
. If you ever see code like the above — a leading case function call without new
, there may be a bug in your program. Or maybe someone on the team didn’t know about that convention.
Javascript Object Parent/Child Relationships
Parent/child relationships do exist in javascript, but they’re hard to see for the average javascript client programmer. Javascript uses a “prototype” based object system. Consider two nominal objects, a
and b
. In a class based (or classical) system, we might have two classes
class A
{
}
class B extends A
{
}
$a = new A;
$b = new B;
Here we’d say A
is a parent of B
.
In a prototype based object system, (i.e. javascript), we’d say Object b’s prototype points to Object a, or Object a is Object b’s parent.
There’s only one major consequence to an object being a child of another object in javascript. If you attempt to access an undefined
property on an object
console.log( b.someUndefinedProperty );
Javascript will check the parent object(s) for some someUndefinedProperty
. If the property is anywhere in the prototype inheritance chain, javascript will return the value from the prototype.
Earlier we said it’s hard for the average javascript client programmer to see javascript’s prototype inheritance in action. That’s because the traditional methods of object creation in javascript
object = {};
object = new SomeConstructorFunction;
lack any direct API for creating parent/child relationships. Early javascript pioneers like Douglas Crockford came up with workarounds for this. Fortunately, more modern versions of javascript have addressed this shortcoming with a third way to create objects — the Object.create
method.
Object.create
is a method on the global Object
object. It creates a new object for you. It requires a single argument — the object you want to use as your new object’s prototype. If you give the following program a try, you can see the discussed parent/child fallback behavior we previously discussed.
var a = {};
a.foo = 'Bar, set on A';
b = Object.create(a);
console.log(b.foo); //logs "Bar, set on A"
Above we’ve defined an object a
, then told javascript to instantiate an object b
that uses a
as a prototype.
One last thing before we move on. This fallback behavior leads to an interesting question — when iterating (foreach
ing) over a javascript object’s keys/values, should the iterator include keys from the parent object(s)? This is why methods like hasOwnProperty
exists, and why every javascript framework has its own method for iterating over a javascript object’s keys and values.
Javascript and “use strict”
Folks trying to improve javascript face the same problem that all language people face.
How do we fix early mistakes without breaking the world
One tool modern javascript developers have in their toolkit is a strict mode. Strict mode removes certain language constructs and behaviors that make optimizing javascript difficult, or that have bene proven to lead to bugs. It’s a way for programmers to opt-in to “better” javascript, but leaves old code to run as is.
For example, with strict mode on, this
remains unbound in global functions instead of being bound to window
.
function foo(){
"use strict";
console.log(this);
}
foo(); //results in "undefined"
Covering every change with “use strict” is beyond the scope of this article, but the MDN use strict article has you covered. What you need to know for Magento 2 is
- You invoke strict mode by putting the string “use strict” at the top of a function
- When the function finishes executing, strict mode is exited.
- If you call another function in a strict context, the called function will not be in strict context (unless it too has
"use strict"
at the top
It’s possible to have an entire script (i.e. all the code on a page) run in strict mode by making the "use strict"
string the first line of javascript your browser sees. So far Magento 2 hasn’t done this, and they likely won’t. This would be an impractical step in a system relying on so much legacy javascript code.
RequireJS Module Loading System
We’ve already covered RequireJS in our PHP for MVC Developers series. You’ll need to be conversant with how Magento and RequireJS work together to get the most out of this new series. There’s also few things worth calling out w/r/t RequireJS and this series.
Consider the following nominal RequireJS module.
// Package_Module/js/lib/some/module/name
define([/* dependencies *]], function([/* dependency arguments*/]){
//START: javascript code that runs once
//...
//END: javascript code that runs once
return /* some object, array, function, or string */
});
One thing that wasn’t 100% clear to me when I started using RequireJS was the fact that, if another module lists Package_Module/js/lib/some/module/name
as a dependency, the code before the return value only runs once. That is, if other modules also include Package_Module/js/lib/some/module/name
as a dependency, RequireJS will have a cached-in-memory version of the returned object, array, function, or string ready to go.
If you’re following RequireJS best practices, the only thing that should wind up in the javascript code that runs once section are private variables and methods/functions (via closure, see below). However, it’s possible that a developer may drop some state changing code here, so you’ll want to be aware that state changing code runs once, and only once.
RequireJS public/private Patterns
Next, consider another nominal module
// Package_Module/js/lib/some/module/name
define([/* dependencies *]], function([/* dependency arguments*/]){
var somePrivateVariable;
var somePrivateFunction = function(){
//...
}
return {
somePublicVariable:'',
somePublicFunction:function(){
console.log(this.somePublicVariable);
console.log(somePrivateVariable);
console.log(somePrivateFunction);
}
}
});
//later on
requirejs(['Package_Module/js/lib/some/module/name'], function(moduleName){
moduleName.somePublicVariable = 'Hello World';
moduleName.somePublicFunction(); //works
moduleName.somePrivateFunction(); //doesn't work, because a method
//was never defined on the object
});
Javascript, as mentioned, does not have classes. Javascript also doesn’t have the concept of public and private access levels. However, thanks to javascript’s implementation of closure, you can simulate public and private methods with the above pattern.
Without getting too deeply into it, all closure means is when you return a function or object from another function, that returned function or object remembers all the variables you defined in the returning function. This means a method inside the object gets to access those “private” variables, but no one from the outside can do the same.
This can be a little difficult to understand the first time you encounter it, because unlike classical OOP the private variables aren’t really part of the object (which is what, ironically, makes them private). However, describing this pattern in terms of classical public/private access levels is common in the javascript world, so you’ll want to get your head around the idea.
RequireJS Aliasing
The next RequireJS feature we’ll want to review is aliasing. Thanks to a number of RequireJS configuration features (map
, etc.), it’s possible that the module name you request won’t be the module you get. Magento takes advantage of this feature to give their more commonly used RequireJS modules short names
define(['uiElement', 'mageUtils'], function(UiElement, MageUtils){
});
For simplicity’s sake, unless it matters due to context, we’re going to call these aliases irrespective of the RequireJS config feature that’s doing the swapping. We’ll also assume you’re conversant in finding an alias’s true module name. Here’s the unix one liner we use
$ find vendor/magento/ -name 'requirejs-config.js' | xargs ack 'uiElement'
vendor/magento/module-ui/view/base/requirejs-config.js
12: uiElement: 'Magento_Ui/js/lib/core/element/element'
Above you can see the uiElement
alias corresponds to the Magento_Ui/js/lib/core/element/element
module.
Also, as one last parting bit of commentary, it’s important you start thinking about RequireJS as a module system, and not a system for loading javascript files. When you’re programming in a language like python and you want a bit of functionality from the fibo
module, you just say
import fibo
and you don’t really worry about where fibo
exists on the file system. The same should be true of RequireJS based systems. However, due to javascript’s legacy and Magento’s choices, module names that include file path like portions (js
, lib
, etc.) aren’t helping us make this mental transition.
Wrap Up
This (almost!) concludes our tour of javascript features, both ancient and modern. While this all can seem intimidating, like most programming topics once you start working with the system on a regular basis, the assumptions and concepts we’ve covered today will start to gel in your mind and you’ll be reasoning about Magento’s systems in no time.
We say almost, because there’s one last javascript feature we want to talk about, but it’s in-depth enough to warrant it’s own article. Next time we’re going to explore methods for debugging the prototype chain in javascript, and explain why it’s probably more complicated than it seems.