ES6 - Rebinding class methods - javascript

I'm currently experimenting with ES6 classes and I've come across this issue that I don't fully understand. I want to make a class which is initialized by giving it a function. The class has a method that runs the function, but with any given context. I've attempted to do so with something like this:
class MyClass {
constructor(action) {
this.action = action;
}
run(context, n) {
this.action.call(context, n);
}
}
// instantiate my class with a function that uses 'this' to log a given number
let a = new MyClass((n) => {
this.log(n);
})
// calls the run method of my class, giving console as the context and 1 as n.
a.run(console, 1);
I would expect this code to result in 1 being logged to the console. However, I'm instead getting a TypeError.
I assume I am misunderstanding something about how classes and context-binding work in ES6. Can anyone help me out?
Edit
Well this was a good lesson on the difference between arrow functions and function declarations. Thanks for the answers!

Your use of arrow functions is breaking things
let a = new MyClass((n) => {
this.log(n);
});
is the same as
let _this = this;
let a = new MyClass(function(n) {
_this.log(n);
});
because arrow functions look up the scope to their parent. You should use a normal function as
let a = new MyClass(function(n) {
this.log(n);
});

The problem stems from how arrow functions work.
Arrow functions bind this to their lexical environment:
let obj = {
a: 1,
init() {
this.log = () => console.log(this.a);
}
};
obj.init();
obj.log();
// And the `this` value cannot be changed
obj.log.call({ a: 500 });
To fix your code, just define action with a regular function expression:
class MyClass {
constructor(action) {
this.action = action;
}
run(context, n) {
this.action.call(context, n);
}
}
let a = new MyClass(function(n) {
this.log(n);
})
a.run(console, 1);

Related

How to escape "this" problem in a javascript class?

get_O3(e)
{
e.preventDefault();
let station = document.getElementById(e.target.id);
let lon = station.getAttribute('lon');
let lat = station.getAttribute('lat');
let code_station = station.getAttribute('code');
this.get_previsions(lon, lat, "O3").bind(this).then((data) =>
{
console.log(data);
});
}
I have a "this" problem, when i call the function get_previsions i get the error :
Uncaught TypeError: this.get_previsions is not a function.
It might be because of the (e) parameter because when i do a console.log(this) it returns the target. I would like that this == my class.
Thanks for the help
At any given point you can check what the current this reference is pointing to by doing the 4 following rules:
New: Was the function called using new then the this points to the new instance.
Explicit Binding: Was the function called using Function#call, Function#apply or Function#bind
Implicit Binding: Was the function called by its owner? (i.e. owner.foo() or owner["bar"]())
Default Rule: If none of the other rules happen then this is set to the window object if the script is running in "use strict" mode otherwise undefined.
Event-listeners call a function using Explicit binding (callBack.call(target, ...)) so the this reference gets set to the target. To change the this reference you either need to wrap it and call it implicitly or use Function#bind.
Implicit call Example (+ closure):
var something = {
foo: function() {
var self = this;
addEventListener("click", function(e) {
self.bar(e);
});
},
bar: function() {
}
};
Explicit call Example (Function#bind):
var something = {
foo: function() {
addEventListener("click", this.bar.bind(this));
},
bar: function() {
}
};
I'm assuming you have a class defined similar to
class thing {
get_O3(e) { ... },
get_previsions() { ... }
}
There are a few options for you. First option, you can bind all functions to this in the constructor:
class thing {
constructor () {
this.get_03 = this.get03.bind(this);
this.get_previsions = this.get_previsions.bind(this);
}
get_O3(e) { ... },
get_previsions() { ... }
}
This can get awkward, especially if you have many functions. You can write a helper bindAll function, but a less awkward/verbose solution is to use a factory method instead, bypassing this altogether:
function makeThing {
const thing = {
get_O3(e) {
...
thing.get_previsions();
},
get_previsions() { ... }
};
return thing;
}
Eric Elliot on Medium has some good reading on the topic if you want to get more in depth.

How do I change this JavaScript to use arrow functions in order to access a property

I believe this can be solved using an arrow-function, but I'm not sure how. I would like to access this.props inside of the render Function. I know I could pass it over as an argument, but would rather not. Is this possible using arrow functions? If so, what do I need to change?
class View {
constructor(options) {
this.options = options;
}
render(el, props = {}) {
this.props = props;
el.innerHTML = this.options.render(el);
}
}
var Test = new View({
render() {
return `${this.props.test}`
//
// Also, tried ${props.test} in my template literal
//
}
});
Test.render(document.getElementById("main"), {
test:"123"
})
Arrow functions allow you to access the this of the external closure, not of the calling function space. A defining point of functions is to isolate it from the callee's variables. Arrow functions simply make the context, or the this object, to be equal of it's definition closure. Thus
var that = this;
(() => console.log(that === this))();
Will print true, while
var that = this;
(function(){console.log(that === this)})();
Will print false
The reason that the arrow's function can access the this context, is because its defined there, not because it's called there.
The only way to force the context object is by using Function.prototype.call or Function.prototype.apply
Do you need the this.props = props allocation? You could have something like this.
class View {
constructor(options) {
this.options = options;
}
render(el, props = {}) {
el.innerHTML = this.options.render(props);
}
}
var test = new View({ render: x => `${x.test}` });
test.render(document.getElementById("main"), { test:"123" });
<div id="main"></div>

How does React call the render function of an ES6 class in a such a way that `this` does not refer to the class itself?

For example, given the class with a function increaseQty
increaseQty() {
this.qty++
}
and the call
render() {
return (
<div>
<button onClick={this.increaseQty}>Increase</button>
</div>
)
}
this.qty will be undefined unless I write a line in my constructor binding the context of this in the constructor to the function
constructor(props) {
super(props)
this.qty = 0
this.increaseQty = this.increaseQty.bind(this) // <---- like so
}
However this isn't the case in a normal es6 class if you're just using it normally:
https://jsfiddle.net/omrf0t20/2/
class Test {
constructor() {
this.qty = 0
}
increaseQty() {
console.log(++this.qty)
}
doStuff() {
this.increaseQty()
}
}
const t = new Test()
t.doStuff() // prints 1
What aspect of React makes it so that render is called without the context of this?
The difference here is that in your example with React you are passing increaseQty as a callback to another component, but in the second, you are calling it within the current context.
You can see the difference here in simplified example
class Test {
constructor() {
this.qty = 0
}
increaseQty() {
console.log(++this.qty)
}
doStuff() {
this.increaseQty(); // no need to bind
}
listenClicks() {
// you should use bind to preserve context
document.body.addEventListener('click', this.increaseQty.bind(this));
}
}
React guidelines also recommend you bind methods in the constructor, to make the code more optimal, bind it once and always use the same function rather than create a new bound version for each render() call.
This isn't really an ES6 specific question (other than the fact that we are referencing a class and constructor). What you're doing in your function is just incrementing a value. If that value has not been initialized to something (even in ES5) then it will throw an error. You can't add 1 to undefined.
In ES5 (and ES6 really) this would be a problem:
var myObj = {
addOne: function() {
this.qty++;
}
}
myObj.addOne(); // Error! this.qty is undefined
Whereas this would resolve it:
var myObj = {
qty: 0,
addOne: function() {
this.qty++;
}
}
myObj.addOne(); // Good to go
It's the same situation in your class. You cannot increment a variable that you haven't declared and initialized to a number value.
In an even simpler example this:
var x;
x++;
would throw an error whereas this:
var x = 0;
x++;
is good.

javascript reference to prototype function is undefined in constructor

Given the following object:
function MyObject() {
window.myFunc1 = this.myFunc1;
window.myFunc2 = this.__proto__.myFunc2;
}
MyObject.prototype.anotherFunc = function () {
window.myFunc3 = this.myFunc3;
window.myFunc4 = this.__proto__.myFunc4;
}
MyObject.prototype.myFunc1 = function () { console.log(1); }
MyObject.prototype.myFunc2 = function () { console.log(2); }
MyObject.prototype.myFunc3 = function () { console.log(3); }
MyObject.prototype.myFunc4 = function () { console.log(4); }
var o = new MyObject();
o.anotherFunc();
window.myFunc1();
window.myFunc2();
window.myFunc3();
window.myFunc4();
In fiddle I'm getting what is expected, but when testing with an ios app under ionic framework the results (tested with safari debugger) are:
window.myFunc1 -> undefined
window.myFunc2 -> it works!
window.myFunc3 -> it works!
window.myFunc4 -> it works!
Isn't the prototype attached to the object before it is created?
EDIT
attached fiddle and working example, fine tuned the question
It may happen if you create your object in an unusual way, and this is bound to something different from what you expect. In my case I was exporting a constructor from the node module, and creating an object with new require('module')();, which had the same effect of prototype functions being undefined.
I'm getting the following results […]
I cannot reproduce that. Try this:
function MyObject() {
console.log(this.myFunc1);
console.log(Object.getPrototypeOf(this).myFunc2);
}
MyObject.prototype.anotherFunc = function () {
console.log(this.myFunc3);
console.log(Object.getPrototypeOf(this).myFunc4);
}
MyObject.prototype.myFunc1 = function () { }
MyObject.prototype.myFunc2 = function () { }
MyObject.prototype.myFunc3 = function () { }
MyObject.prototype.myFunc4 = function () { }
var o = new MyObject();
o.anotherFunc()
Isn't the prototype attached to the object before it is created?
Uh, no. The prototype cannot really be attached to a non-existing object :-) Best think of it as happening simultaneously, that an object is created with a prototype, it doesn't exist without one.
What you might have meant that the object (with its prototype) is created before the constructor is executed - yes.

Javascript function (type) to store & use data

I really never used a javascript function type or class before, I understand Java and Python, but not javascript. So, I build a class like this:
function FormStore (type) {
this.setup = () =>{
this.store = {};
this.ERR_LINE_PREFIX = '#err_';
this.NO_DISPLAY_CLASS = 'no-display';
this.settings = {
'myID':{'hide':false},
}
}
this.checkVal= () => {
var geoArr = ['id_xx','myID', (...)];
var id;
$.each( geoArr, function(val) {
id = geoArr[val];
console.log(this.store) //-> returns undefined, below line is error
if (!(this.store[id])) {
return false;
}
});
};
var FS = new FormStore();
FS.setup();
The store is filled by components on document.ready. There is a function that looks up if the aligned components (glyph, label, input) have some classes or values and for the specific component fills a dict: {label:false,glyph:false, input:false}. However, for some reason it doesn't matter. Even if I enter some values in to the store right away (in setup) or create them on the fly, in checkVal the store doesn't exist, it's undefined.
Please, anybody, what am I not understanding about javascript type and classes here? I am googling this a lot and trying to find good resources but, "javascipt variable class" (or type) just yields a lot of DOM manipulation.
edit
There is a context problem in checkVal, you are using a non-arrow (and not explicitly bound) callback function and trying to access this inside of it. Change that to an arrow function as well, and the parent context (this) will be preserved:
$.each( geoArr, (val) => {
id = geoArr[val];
console.log(this.store)
if (!(this.store[id])) {
return false;
}
});
And while you are at changing that section, it's not going to work. You will not get access to $.each's return value. You should rely on native array APIs for this task and use Array.every to determine if all geoArr items are in the store (assuming that's your goal):
// returns false if not all geoArr items are in the store
geoArr.every(id => this.store[id])
original
I don't see you calling checkVal() anywhere, but based on the error you are getting it is called prior to setup() (since setup initializes the store). You could solve that problem straight away by moving this.store = {} out of setup (right at the top), e.g.:
function FormStore(type) {
this.store = {};
...
Having said that, I would suggest either defining your methods on the prototype, or utilizing ES6 classes. Here is a simplified version of both:
ES5 class
function FormStore(type) {
// make sure user didn't forget new keyword
if (this === window) {
throw new Error('FormStore must be called with "new" keyword')
}
// initialize state, this is the constructor
this.type = type;
this.store = {};
// any other state the class manages
}
FormStore.prototype = {
setup: function() {
// do setup stuff
// "this" points to instance
console.log('setup', this.type)
},
checkVal: function() {
}
}
var formStore = new FormStore('foo')
console.log(formStore.store) // <-- not undefined
formStore.setup()
ES6 Class
class FormStore {
constructor(type) {
this.type = type;
this.store = {};
}
setup() {
console.log('setup', this.type)
}
checkVal() {
}
}
const formStore = new FormStore('bar')
console.log(formStore.store) // <-- not undefined
formStore.setup()
It has to do with scoping. Your $.each in checkVal has a normal function. Inside the function the scope if this is different. If you want to keep the original scope you could use a fat arrow function like you do when defining the methods.
this.checkVal= () => {
var geoArr = ['id_xx','myID', (...)];
var id;
$.each( geoArr, val => {
id = geoArr[val];
console.log(this.store) //-> returns undefined, below line is error
if (!(this.store[id])) {
return false;
}
});
}
When you run your original code and place a breakpoint on the line with console.log you can see in the inspector that this is set to the Window object and no longer points to your FormStore.
function FormStore () {
this.setup = function(){
this.store = {};
this.ERR_LINE_PREFIX = '#err_';
this.NO_DISPLAY_CLASS = 'no-display';
this.settings = {
'myID':{'hide':false},
}
}
this.checkVal= function(){
var geoArr = ['id_xx','myID'];
var id;
$.each( geoArr, function(val) {
id = geoArr[val];
console.log(this.store) //-> returns undefined, below line is error
if (!(this.store[id])) {
return false;
}
});
}
};
var FS = new FormStore();
FS.setup();
Works absolutely fine, the code you provided had a missing bracket and you were using some broken es6 syntax

Categories

Resources