📜 ⬆️ ⬇️

Javascript without this

The JavaScript keyword this can be called one of the most discussed and ambiguous features of the language. The thing is that what it indicates looks differently depending on where this is addressed. The matter is aggravated by the fact that this is also affected by whether strict mode is enabled or not.



Some programmers, not wanting to put up with the strangeness of this , try not to use this language construct at all. I do not see anything wrong here. On the basis of this aversion, many useful things were created. For example, switch functions and this binding. As a result, the development can almost completely do without this .

Does this mean that the “fight with this ” is over? Of course not. If you think about it, you may find that tasks that are traditionally solved with the involvement of function constructors and this can be solved in another way. This material is devoted to the description of this approach to programming: no new and no this on the way to simpler, more understandable and predictable code.
')
Immediately I would like to note that this material is intended for those who know what this . We will not consider the fundamentals of JS here, our main goal is to describe a special approach to programming, excluding the use of this , and, thanks to this, helping to eliminate potential troubles related to this . 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


We begin with the fact that 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 returning one function from another, a closure is created. A closure is an internal function that has access to a chain of scopes of variables of an external function. In particular, we are talking about variables declared in an external function that are not accessible directly from the scope, which includes this external function. Here, for example, a function that adds the number passed to it to a variable stored in a closure:

 function makeAdder(base) { let current = base; return function(addition) {   current += addition;   return current;    } } 

The makeAdder() function takes a base parameter and returns another function. This function takes a numeric parameter. In addition, it has access to the current variable. When it is called, it adds the number passed to it to current and returns the result. Between calls, the current variable is preserved.

It is important to pay attention to the fact that closures define their own local lexical scopes. The functions defined in the closures are in the closed from the extraneous 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 a 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 perceived as an open API that gives access to the functions and variables stored in the closure. What we just described is called the “revealing module pattern”.

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; function changeBy(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, the privateCounter variable is the data we need to work with, hidden in the closure and not directly accessible from the outside. The public methods increment() , decrement() and value() describe the operations you can perform with privateCounter .

Now we have everything you need to program in JavaScript without using this . Consider an example.

Two-way queue without this


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

A two-way queue can be implemented using a linked list. At first glance, all this may seem complicated, but in reality it is not. If you study the example below, you can easily understand the implementation of a two-way queue and the operations necessary for its operation, 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:


Before writing the code, we will think about how to implement the queue using objects and closure variables. The preparatory stage 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 queue element. In addition, we need pointers to the first and last elements, to the head and tail of the queue. Call them, respectively, head and tail . Since we create a queue based on a linked list, we need a way to connect the elements, so each element requires pointers to the next and previous elements. Let's call these pointers next and prev . And finally, you want to track the number of items in the queue. Use the variable length for this.

Now let's talk about grouping the above variables. Each queue element, node, needs a variable with its data — data , as well as pointers to the next and previous nodes — next and prev . For these reasons, create a Node object, which is an element of the queue:

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

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

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

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

 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, the create() method can be described. It is pretty simple:

 function create() { return Object.create(Deque); } 

We have dealt with this method. It is impossible not to notice that the queue that it returns does not contain a single element. Very soon we will fix it, but for now we will create the isEmpty() method:

 function isEmpty(deque) { return deque.length === 0 } 

To this method, we pass the object to the two-way queue, deque , and check if its length property is zero.

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

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

Here is the pushFront() method code:

 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; return deque; } 

The pushBack() method, which allows you to add elements to the end of a 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; return deque; } 

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

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

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

How to use all this? For example:

 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 data and operations that can be performed on this data. You can work with a two-way queue using LinkedListDeque methods, as long as there is a working link to it.

Homework


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

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

  2. This implementation of the two-way queue uses a linked list. Another option is based on regular JavaScript arrays. Create all the operations necessary for a two-way queue using an array. Name this implementation ArrayDeque . And remember - no this and new .

  3. Analyze the implementation of bilateral queues using arrays and lists. Think about the temporal and spatial complexity of the algorithms used. Compare them and record your findings.

  4. Another way to implement two-way queues is to use arrays and linked lists at the same time. This implementation can be called MixedQueue . With this approach, first create an array of fixed size. Let's call it 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 of the FIFO model. Implement two-way queue methods using this approach. What are the advantages and disadvantages of such a structure? Record your findings.

  5. Eddie Osmani wrote the book Design Patterns in JavaScript. There he talks about the flaws of the revealing module pattern. One of them is as follows. If the private function of the module uses the 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 API module. Develop a workaround for this flaw. Think about dependencies, how to invert a control. How to ensure that all private functions of the module work with its publicly available functions so that we have the ability to control publicly available functions. Write down your ideas.

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

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

  8. Develop a non-destructive bypassing the queue in reverse order.

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

After you complete these basic tasks, you can do a few more.

  1. Use any testing framework and add tests to all your two-way queue implementations. Do not forget to test the border cases.

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

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

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

Results


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

Dear readers! How do you feel about this in javascript?

Source: https://habr.com/ru/post/334222/


All Articles