JavaScript without this

Original author: Fanis Despoudis
  • Transfer
The keyword thisin JavaScript can be called one of the most discussed and controversial features of the language. The thing is that what it points to looks different depending on where it is accessed this. The matter is aggravated by the fact that thisstrict mode is included or not. Some programmers, not wanting to put up with oddities , try not to use this language construct at all. I don’t see anything bad here. On the basis of rejection , a lot of useful things were created. For example - arrow functions and binding . As a result, during development, you can almost completely do without .



thisthisthisthis

Does this mean that the “fight with this” is over? Of course not. If you think about it, you can find that the tasks that are traditionally solved with the use of constructor functions and thiscan be solved in another way. This material is devoted to describing just such an approach to programming: none newand none thison the way to a simpler, more understandable, and more predictable code.

I would like to note right away that this material is intended for those who know what it is this. We will not consider the basics of JS here, our main goal is to describe a special approach to programming that excludes use this, and, thereby, helps to eliminate potential troubles thisassociated with it . If you want to refresh your knowledge about this,Here and here are the materials that will help you with this.

I have an idea


To begin with, in JavaScript, functions are first-class objects. So, they can be passed to other functions as parameters and returned from other functions. In the latter case, when one function returns from another, a closure is created. A closure is an internal function that has access to the chain of visibility of the variables of the external function. In particular, we are talking about variables declared in an external function that are not directly accessible from the scope, which includes this external function. Here, for example, is a function that adds the number passed to it to a variable stored in a closure:

functionmakeAdder(base) {
  let current = base;
  returnfunction(addition) {
    current += addition;
    return current;    
  }
}

The function makeAdder()takes a parameter baseand returns another function. This function accepts a numeric parameter. In addition, she has access to the variable current. When called, she adds the number passed to her currentand returns the result. Between calls, the value of the variable is currentsaved.

It is important to pay attention to the fact that closures define their own local lexical scope. Functions defined in closures end up in an enclosed space.

Closures are a very powerful JavaScript feature. Their correct use allows you to create complex software systems with reliably separated levels of abstraction.

Above, we returned another function from the function. With the same success, armed with our knowledge of closures, an object can be returned from a function. This object will have access to the local environment. In fact, it can be thought of as an open API that gives access to functions and variables stored in a closure. What we just described is called the “revealing module template”.

This design pattern allows you to explicitly specify the public members of the module, leaving all others private. This improves the readability of the code and simplifies its use.

Here is an example:

let counter = (function() {
  let privateCounter = 0;
  functionchangeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  };   
})();
counter.increment();
counter.increment();
console.log(counter.value()); // выводит в лог 2

As you can see, a variable privateCounteris the data we need to work with, hidden in a closure and inaccessible directly from the outside. Public methods increment(), decrement()and value()describe the operations that can be performed with privateCounter.

Now we have everything we need for JavaScript programming without use this. Consider an example.

Two-way queue without this


Here is a simple example of using closures and functions without this. It is - known implementation of the data structure, which is called two-sided queue (deque, double-ended queue) . This is an abstract data type that works as a queue , however, our queue can grow and decrease in two directions.

A two-way queue can be implemented using a linked list. At first glance, all this may seem complicated, but in fact it is not. If you study the example below, you can easily figure out the implementation of the two-way queue and the operations necessary for its functioning, but, more importantly, you can apply the learned programming techniques in your own projects.

Here is a list of typical operations that a two-way queue should be able to perform:

  • create : Create a new two-way queue object
  • isEmpty : Check if the queue is empty
  • pushBack : Add a new item to the end of the queue
  • pushFront : Add a new item to the front of the queue
  • popBack : Delete and return the last element of a queue
  • popFront : Delete and return the first element of a queue

Before writing the code, we will think about how to implement the queue by operating with objects and closure variables. The preparatory phase is an important part of the development.

So, first we need a variable, let's call it data, which will store the data of each element of the queue. In addition, we need pointers to the first and last elements, to the head and tail of the queue. We call them, respectively, headand tail. Since we create the queue on the basis of a linked list, we need a way to link the elements, so each element requires pointers to the next and previous elements. Call these pointers nextand prev. And finally, you need to track the number of items in the queue. We will use for this variable length.

Now let's talk about the grouping of the above variables. Each element of the queue, the node, needs a variable with its data - dataas well as pointers to the next and previous nodes - nextand prev. Based on these considerations, create an object Nodethat is a queue element:

let Node = {
  next: null,
  prev: null,
  data: null
};

Each queue should store pointers to its own head and tail (variables headand tail), as well as information about its own length (variable length). Based on this, we define the object Dequeas follows:

let Deque = {
  head: null,
  tail: null,
  length: 0
};

So, we have an object, Nodewhich is a separate queue node, and an object Deque, which represents a two-way queue itself. They need to be stored in a circuit:

module.exports = LinkedListDeque = (function() {
  let Node = {
    next: null,
    prev: null,
    data: null
  };
  let Deque = {
    head: null,
    tail: null,
    length: 0
  };
 // тут нужно вернуть общедоступное API
})();

Now, after the variables are placed in the closure, we can describe the method create(). It is arranged quite simply:

functioncreate() {
  returnObject.create(Deque);
}

We figured out this method. It is impossible not to notice that the queue that he returns does not contain a single element. Soon we will fix it, but for now we will create a method isEmpty():

functionisEmpty(deque) {
  return deque.length === 0
}

To this method, we pass the object of the two-way queue,, dequeand check if its property is equal to lengthzero.

Now it's time for the method pushFront(). In order to implement it, you must perform the following operations:

  1. Create a new object Node.
  2. If the queue is empty, you need to set the head and tail pointers to the new object Node.
  3. If the queue is not empty, you need to take the current element of the queue headand set its pointer prevto the new element, and set the pointer of the nextnew element to the element that is written into the variable head. As a result, the first element of the queue will be a new object Node, followed by the element that was the first before the operation. In addition, you must remember to update the queue pointer headso that it refers to its new element.
  4. Increase the length of the queue by incrementing its property length.

Here is the method code pushFront():

function pushFront(deque, item){
  // Создадим новый объект Node
  const newNode = Object.create(Node);
  newNode.data = item;
  
  // Сохраним текущий элемент head
  let oldHead = deque.head;
  deque.head = newNode;
  if (oldHead) {
    // В этом случае в очереди есть хотя бы один элемент, поэтому присоединим новый элемент к началу очереди
    oldHead.prev = newNode;
    newNode.next = oldHead;
  } else {// Если попадаем сюда — очередь пуста, поэтому просто запишем новый элемент в tail.
    deque.tail = newNode;
  }
  // Обновим переменную length
  deque.length += 1;
  
  returndeque;
}

The method pushBack()for adding items to the end of the queue is very similar to the one we just looked at:

function pushBack(deque, item){
  // Создадим новый объект Node
  const newNode = Object.create(Node);
  newNode.data = item;
  
  // Сохраним текущий элемент tail
  let oldTail = deque.tail;
  deque.tail = newNode;
if (oldTail) {
    // В этом случае в очереди есть хотя бы один элемент, поэтому присоединим новый элемент к концу очереди
    oldTail.next = newNode;
    newNode.prev = oldTail;
  } else {// Если попадаем сюда — очередь пуста, поэтому просто запишем новый элемент в head.
    deque.head = newNode;
  }
  // Обновим переменную length
  deque.length += 1;
  
  returndeque;
}

After the methods are implemented, we will create a public API that allows you to call methods stored in the closure from the outside. We do this by returning the corresponding object:

return {
 create: create,
 isEmpty: isEmpty,
 pushFront: pushFront,
 pushBack: pushBack,
 popFront: popFront,
 popBack: popBack
}

Here, in addition to the methods that we described above, there are those that have not yet been created. Below we will return to them.

How to use all this? For example, like this:

const LinkedListDeque = require('./lib/deque');
d = LinkedListDeque.create();
LinkedListDeque.pushFront(d, '1'); // [1]
LinkedListDeque.popFront(d); // []
LinkedListDeque.pushFront(d, '2'); // [2]
LinkedListDeque.pushFront(d, '3'); // [3]<=>[2]
LinkedListDeque.pushBack(d, '4'); // [3]<=>[2]<=>[4]
LinkedListDeque.isEmpty(d); // false

Please note that we have a clear separation of the data and operations that can be performed on this data. You can work with a two-way queue using the methods from LinkedListDeque, as long as there is a working link to it.

Homework


I suspect you thought you got to the end of the material and understood everything without writing a single line of code. True? In order to achieve a full understanding of what we talked about above, to experience the proposed approach to programming in practice, I advise you to perform the following exercises. Just clone my repository on GitHub and get down to business. (There are no solutions to problems there, by the way.)

  1. Based on the above examples of method implementation, create the rest. Namely, write the functions popBack()and popFront(), which, respectively, delete and return the first and last elements of the queue.

  2. In this implementation of a two-way queue, a linked list is used. Another option is based on regular JavaScript arrays. Create all the operations needed for a two-way queue using an array. Name this implementation ArrayDeque. And remember - no thisand new.

  3. Analyze the implementation of two-way queues using arrays and lists. Think of the temporal and spatial complexity of the algorithms used. Compare them and write down your conclusions.

  4. Another way to implement two-way queues is to use arrays and linked lists at the same time. Such an implementation can be called MixedQueue. With this approach, an array of a fixed size is first created. Let's call him block. Let its size be 64 elements. It will store the elements of the queue. When you try to add more than 64 elements to the queue, a new data block is created, which is connected to the previous one using a linked list using the FIFO model. Implement the two-way queue methods using this approach. What are the advantages and disadvantages of such a structure? Write down your findings.

  5. Eddie Osmani has written JavaScript Design Patterns. There he talks about the flawsrevealing module template. One of them is as follows. If a private function of a module uses a public function of the same module, this public function cannot be redefined from the outside, patched. Even if you try to do this, the private function will still refer to the original private implementation of the public function. The same applies to attempts to change from outside a public variable, access to which is provided by the module API. Develop a way around this shortcoming. Think about dependencies, how to invert control. How to ensure that all private functions of a module work with its public functions so that we have the ability to control public functions. Write down your ideas.

  6. Write a method, joinwhich allows you to connect two two-way queues. For example, a call LinkedListDeque.join(first, second)attaches a second queue to the end of the first and returns a new two-way queue.

  7. Develop a queue bypass mechanism that does not destroy it and allows you to iterate through it in a loop for. You can use ES6 iterators for this exercise .

  8. Design a non-destructive queuing mechanism in the reverse order.

  9. Publish what you got on GitHub, tell everyone that you created an implementation of a two-way queue without this, and how well you understand all this. Well, do not forget to mention me .

After dealing with these basic tasks, you can do a few more additional ones.

  1. Use any test framework and add tests to all of your implementations of two-way queues. Remember to test border cases.

  2. Redesign the implementation of the two-way queue so that it supports priority items. Elements of such a queue can be assigned priority. If such a queue will be used to store items without assigning priority to them, its behavior will not be different from normal. If priority is assigned to the elements, it is necessary to ensure that after each operation the last element in the list would have the lowest priority and the first the highest. Create tests for this two-way queue implementation.

  3. A polynomial is an expression that can be written as an * x^n + an-1*x^n-1 + ... + a1x^1 + a0. Here an..a0 — they are polynomial coefficients, and n…1 — exponents. Create an implementation of a data structure for working with polynomials, develop methods for adding, subtracting, multiplying and dividing polynomials. Limit yourself to simplified polynomials. Add tests to verify that the solution is correct. Ensure that all methods that return a result return it as a two-way queue.

  4. It has been assumed so far that you are using JavaScript. Choose some other programming language and do all the previous exercises on it. It could be Python, Go, C ++, or anything else.

Summary


I hope you completed the exercises and learned something useful with their help. If you think the benefits of not using it thisare worth the effort to upgrade to a new programming model, take a look at eslint-plugin-fp . With this plugin, you can automate code validation. And, if you work in a team, before giving up this, agree with colleagues, otherwise, when meeting with them, do not be surprised at their gloomy faces. Have a nice code!

Dear readers! How do you feel about thisin JavaScript?

Also popular now: