Статьи

Голый JavaScript — Определение классов и экземпляров функций

В восьмой части серии « Голый Javascript» мы собираемся исследовать мистическую землю классов в Javascript. Теперь, если исходить из фона с обширным программированием в OOPS с использованием Java, отсутствие простых классов было довольно шокирующим для меня с самого начала. Синтаксис, представление и реализация были полным отклонением от традиционных методов.

Как мы узнали из предыдущих статей , функции являются строительными блоками для множества вещей в javascript. Классы являются одним из них. Фактически, когда вы определяете функцию конструктора, вы фактически создаете функцию, которую можно использовать для конструирования объектов. Давайте посмотрим, что я имею в виду, посмотрев на пример.

function Employee(name, age){
 console.log('');
 this.name = name;
 this.age=age;
}
 
var ancientNewJoinee = new Employee('xyz',99);

Если вы посмотрите на эту функцию, вы заметите несколько вещей.

  1. Мы используем соглашение об именах конструктора. 
  2. Мы используем ключевое слово this при назначении значений свойств. Ключевое слово «this» в javascript — это одна из тех функций, которые иногда могут привести к потере всех волос, если вы не знаете, с чем имеете дело (Личный совет: «это» настоятельно рекомендуется для волосатых женщин!). Я пытался рассказать об этом в запутанных деталях в предыдущей статье. (Запишите, волосатые женщины!) 
  3. Мы передали два аргумента и присвоили их в качестве свойств объекта this.

Теперь это может показаться простым на первый взгляд. Разве это не то, что мы всегда делаем на других языках, таких как Java? Это «предполагается», чтобы быть простым, верно?

Черт, нет, его нет.

Или, может быть, это так. Давай посмотрим что происходит.

Случай 1: делать это неправильно

You invoke the Employee function as if its just another function, instead of a constructor function. There is nobody who is ever going to stop you from doing such a hideous thing, right? So, lets do it and observe what happens.

function Employee(name, age){
 console.log('');
 this.name = name;
 this.age=age;
}
 
Employee('blah',90);

So what does this do? It works. It executes normally. Really cool, awesome. But what happend to the name and age property that I had passed as arguments? Whos kid did i rename?

Turns out, that you did something totally horrible. What you did was, you created two new properties on the window object.

Now, you should recollect from the previous articles that when you dont specify the context during a function invocation, the function is invoked using the context of the window object which takes the form is nothing but the ‘this’ keyword inside the function. So, when you invoke the function by directly using the above syntax, you end up creating unnecessary properties on the window object. But thats not something that we want.

Case 2 : Doing it the right way

You invoke the employee function by using the new operator.

function Employee(name, age){
 console.log('');
 this.name = name;
 this.age=age;
}
 
var ancientEmp = new Employee('xyz',99);

When you create an object in this way, you are actually creating a new function object, whose constructor is of type ‘Employee’, and it is this newly created function object that is then used as the context inside the constructor. So, when you are creating properties on the ‘this’ variable, you are creating properties on your newly created function object. Since you have not explicitly specified any return value, the Employee constructor function returns the value of ‘this’ variable from the function, which is nothing but the newly created function instance.

Now that solves the mystery of creating classes. But there is much more to this scheme than meets the eye. How do you protect members. Like other programming languages, is there not a way to create private members that cannot be accessed from the outer world? There is a way, but it is not as obvious as it seems.

In the example that we saw, we created two properties on the Employee instance. Both of these properties have public access. i.e. you can directly modify the values of these properties using the object access notation.

function Employee(name, age){
 console.log('');
 this.name = name;
 this.age=age;
}
 
var ancientEmp = new Employee('xyz',99);
 
ancientEmp.age=5;

Oh no, now our ancient emp is younger than the kid next door. And child labor is a crime. We dont wana get into a mess with the authorities, do we? Well, at least not me. So, what do I do that enables me to access the properties on the object but still restrict me from modifying them to incorrect values.

The solution is simple. If you remember, in the previous article, we spoke about closures and scoping in JavaScript. Scoping is a very powerful concept and this is one of the places where scoping is used to mimic the behaviour of private variables in classes.

The JavaBean standard is quite common in the Java world. We can apply the same principle to solve the problem of private variables using getters and setters for our variables and instead of accessing the properties directly.

function Employee(name, age){
 console.log('');
 this.getName = function (){
  return name;
 };
 this.getAge=function(){
  return age;
 };
 this.setName = function (newName){
  name=(newName==true)?newName:name;
 };
 this.setAge=function(newAge){
  age=newAge>14?newAge:age;
 };
}
 
var ancientEmp = new Employee('xyz',99);
 
ancientEmp.setName('');
ancientEmp.setAge(5);
 
console.log(ancientEmp.getName());
console.log(ancientEmp.getAge());

The variables that are declared as function parameters are nothing but local variables in the function and behave like instance variables within the function scope. Due to javascript’s scoping rules, they remain available to the function instance as long as the instance exists or are explicitly deleted from the object via some method. Creating getters and setters like this improves decoupling between the various code components.

Now there is a minor issue with creating classes in this way. What happens is that whenever you create a function on an object you are creating a function ONLY for that object. Okey, lets think about it once more. In the first article of the series, I mentioned that when we create properties on objects, the properties are specific only to that object. Now look at our constructor function. What exactly is happening when we are trying to assign getters and setters on the ‘this’ object? The getters and setters are created exclusively for the current function instance, i.e. the ‘this’. This implies that if you are creating a large number of objects, you will end up with each object having its own independent copy of each method and property.

Although, it is highly desirable for each object of a class to be able to save the state of its properties independently, you really don’t want to create multiple copies of the same function for each object because that will set your browser on fire and send your users screaming out of their houses. What you want to do is to define a function only once and let all the object point to that function. There are a couple of ways in which you can do that.

1) Use an external function

function calculateSalary(){
 //Sorry for being so random!
 return Math.random();
}
 
function Employee(name, age){
 //Your stuff
 //
 //End of your stuff
  
 this.calculateSalary=calculateSalary();
}

This is a convenient and perhaps the simplest way that you can enable the reuse of the function. In this way, the calculateSalary property of all the function instances will refer to the same externally defined calculateSalary function. Its simple, its nice. However, there lies scope of improvement. This brings to the point where we have to talk about perhaps the best and the coolest concept in javascript — prototypes (But that’s just my opinion. So no bashing please!). I will be discussing javascript prototypes in the next article in absolute details. But until then, we need to learn just a bit to get this example working.

2) Using prototypes

What is a prototype? A prototype is nothing but a property on an object that is visible to all instances of the object.

Lets demonstrate it in our class example. But before that, I want to reiterate a few points.

  • Functions are objects themselves.
  • Functions can be instantiated to create special objects called function instances, by invoking them using the ‘new’ keyword.
  • Function objects are also objects, but they have a special property called ‘constructor’ that identifies the function that was resposible to instantiate this object.
  • Objects can have properties.
  • All objects have a property called ‘prototype’.

The prototype property is pretty awesome. When you declare a property on the prototype property, it automatically becomes available to all the instances of that object/function/classs. This means that the prototype is the optimal place to declare a function that needs to be reused by all the objects of a class.

function Employee(name, age){
 
 this.name=name;
 //Other variables
}
 
Employee.prototype.getName=function(){
 return this.name;
}
 
Employee.prototype.setName=function(newName){
 name=(newName==true)?newName:name;
}
Employee.prototype.calculateSalary=function(){
 //Whoops!
 return Math.random();
}
 
var emp1 = new Employee('xyz');
 
console.log(emp1.getName());

I simplified the class just for the sake of the example. But if you have noticed, although we eliminated the issue of all the objects having a local vaiable pointing to the getter or setter, we are back to the problem where the property is defined on ‘this’ itself. So, although this technique is useful for defining the calculateSalary function which does not make use of any properties, it somehow manages to breaks the principle of encapsulation because once again, all the properties will have to be publilcly visible for them to be accessible via the prototype method.

That covers the basics of classes in Javascript. In the upcoming article, I plan to talk the most amazing prototype property and perhaps talk a bit about implementing inheritance in JavaScript. So, until then, stay tuned!