Статьи

Javascript для разработчиков Java

Этот пост будет посвящен языку Javascript с точки зрения разработчика Java, сосредоточив внимание на различиях между двумя языками и частыми болевыми точками. Мы пройдемся по следующему:

  • Только объекты, нет классов
  • Функции — это просто ценности
  • Ключевое слово «это»
  • Классическое наследование против прототипа
  • Конструкторы против функций конструктора
  • Закрытие против Lambdas
  • Инкапсуляция и Модули
  • Блок Область и Подъем

Почему Javascript в мире Java?

Большая часть работы по разработке внешнего интерфейса Java выполняется с использованием основанных на Java / XML сред, таких как JSF или GWT. Сами разработчики фреймворка должны знать Javascript, но в принципе разработчики приложений не знают. Однако реальность такова:

  • Для разработки пользовательских компонентов, например, в Primefaces (JSF), важно знать Javascript и jQuery.
  • В GWT интеграция хотя бы некоторых сторонних виджетов Javascript является обычной и экономически эффективной.

Конечным результатом является то, что Javascript, как правило, необходим для выполнения как минимум последних 5-10% работы веб-интерфейса, даже с использованием сред Java. Кроме того, он начинает все больше использоваться для развития предприятий полиглота, например, вместе с Angular .

Хорошая новость заключается в том, что, помимо нескольких ошибок, которые мы расскажем, Javascript — это очень обучаемый язык для разработчика Java.

Только объекты — нет классов

Одна из самых удивительных вещей в Javascript — это то, что это объектно-ориентированный язык, но нет классов (хотя они будут в новой версии Ecmascript 6 ).

Возьмем, к примеру, эту программу, которая инициализирует пустой объект и задает два свойства:

1
2
3
4
5
// create an empty object - no class was needed !!
var superhero = {};
 
superhero.name = 'Superman'
superhero.strength = 100;

Объекты Javascript похожи на Java HashMap со связанными свойствами, где ключами являются только строки. Следующее будет «эквивалентным» Java-кодом:

1
2
3
4
Map<String,Object> superhero = new HashMap<>();
 
superhero.put("name","Superman"); 
superhero.put("strength", 100);

Это означает, что объект Javascript — это просто многоуровневая «карта хешей» пар ключ / значение без определения класса.

Функции — это просто ценности

Функции в Javascript — это просто значения типа Function , это просто! Взять, к примеру:

1
2
3
4
5
var flyFunction = function() { 
    console.log('Flying like a bird!');
};
 
superhero.fly = flyFunction;

Это создает функцию (значение типа Function ) и назначает ее переменной flyFunction . Затем в объекте супергероя создается новое свойство с именем fly , которое может быть вызвано так:

1
2
// prints 'Flying like a bird!' to the console
superhero.fly();

У Java нет эквивалента типа Javascript Function , но почти. Возьмем, к примеру, класс SuperHero который принимает функцию Power :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public interface Power { 
    void use();
}
 
public class SuperHero {
 
    private Power flyPower;
 
    public void setFly(Power flyPower) {
        this.flyPower = flyPower;
    }
 
    public void fly() {
        flyPower.use();
    }
}

Вот как передать SuperHero функцию в Java 7 и 8:

01
02
03
04
05
06
07
08
09
10
11
12
13
// Java 7 equivalent
Power flyFunction = new Power() { 
    @Override
    public void use() {
        System.out.println("Flying like a bird ...");
    }
};
 
// Java 8 equivalent
superman.setFly( 
    ()->System.out.println("Flying like a bird ..."));
 
superman.fly();

Таким образом, хотя тип Function не существует в Java 8, в конечном итоге это не препятствует созданию стиля Javascript-подобного функционального программирования.

Но если мы передадим функции, что произойдет со значением this ключевого слова?

Использование этого ключевого слова

То, что Javascript позволяет делать с this , довольно удивительно по сравнению с миром Java. Давайте начнем с примера:

01
02
03
04
05
06
07
08
09
10
var superman = {
 
  heroName: 'Superman'
 
  sayHello: function() {
      console.log("Hello, I'm " + this.heroName );
  
};
 
superman.sayHello();

Эта программа создает объект superman с двумя свойствами: String heroName и Function именем sayHello . Запуск этой программы выводит как положено Hello, I'm Superman .

Что если мы передадим эту функцию?

Обойдя sayHello , мы легко можем оказаться в контексте, где нет свойства heroName :

1
2
3
var failThis = superman.sayHello;
 
failThis();

Запуск этого фрагмента даст в качестве вывода: Hello, I'm undefined .

Почему this больше не работает?

Это связано с тем, что переменная hello принадлежит глобальной области видимости, в которой нет переменной-члена heroName . Чтобы решить это:

В Javascript значение this ключевого слова полностью переопределено, чтобы быть чем угодно!

1
2
// overrides 'this' with superman
hello.call(superman);

Приведенный выше фрагмент снова напечатает Hello, I'm Superman . Это означает, что значение this зависит как от контекста, в котором вызывается функция, так и от того, как вызывается функция.

Классическое наследование против прототипа

В Javascript нет наследования классов, вместо этого объекты могут наследоваться напрямую от других объектов. Это работает так, что каждый объект имеет неявное свойство, которое указывает на «родительский» объект.

Это свойство называется __proto__ , а родительский объект называется прототипом объекта, отсюда и название Prototypal Inheritance.

Как работает prototype ?

При поиске свойства Javascript будет пытаться найти свойство в самом объекте. Если он не находит его, он пробует его прототип и так далее. Например:

1
2
3
4
5
6
7
8
9
var avengersHero = { 
    editor: 'Marvel'
};
 
var ironMan = {};
 
ironMan.__proto__ = avengersHero;
 
console.log('Iron Man is copyrighted by ' + ironMan.editor);

Этот фрагмент кода выведет Iron Man is copyrighted by Marvel .

Как мы видим, хотя объект ironMan пуст, его прототип содержит editor свойств, который можно найти.

Как это соотносится с наследованием Java?

Давайте теперь скажем, что права на Мстителей были куплены DC Comics:

1
avengersHero.editor = 'DC Comics';

Если мы снова вызовем ironMan.editor , мы получим, что Iron Man is copyrighted by DC Comics . Все существующие экземпляры объектов с прототипом avengersHero теперь видят DC Comics без необходимости воссоздания.

Этот механизм очень простой и очень мощный. Все, что может быть сделано с наследованием класса, может быть сделано с наследованием прототипа. Но как насчет конструкторов?

Конструкторы против функций конструктора

В Javascript была сделана попытка сделать создание объектов похожим на языки, подобные Java. Давайте возьмем для примера:

1
2
3
4
function SuperHero(name, strength) { 
    this.name = name;
    this.strength = strength;
}

Обратите внимание на заглавное имя, указывающее, что это функция конструктора. Посмотрим, как это можно использовать:

1
2
3
var superman = new SuperHero('Superman', 100);
 
console.log('Hello, my name is ' + superman.name);

Этот фрагмент кода выводит Hello, my name is Superman .

Вы можете подумать, что это похоже на Java, и в этом суть! На самом деле этот new синтаксис создает новый пустой объект, а затем вызывает функцию конструктора, заставляя его быть вновь созданным объектом.

Почему этот синтаксис не рекомендуется тогда?

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

01
02
03
04
05
06
07
08
09
10
11
function SuperHero(name, strength) { 
    this.name = name;
    this.strength = strength;
}
 
SuperHero.prototype.sayHello = function() { 
    console.log('Hello, my name is ' + this.name);
}
 
var superman = new SuperHero('Superman', 100); 
superman.sayHello();

Это выведет Hello, my name is Superman .

Но синтаксис SuperHero.prototype.sayHello выглядит совсем не так, как на Java! new операторный механизм наполовину выглядит как Java, но в то же время совершенно другой.

Есть ли рекомендуемая альтернатива new ?

Рекомендуемый способ — полностью игнорировать new оператор Javascript и использовать Object.create :

1
2
3
4
5
6
7
8
var superHeroPrototype = { 
   sayHello: function() {
        console.log('Hello, my name is ' + this.name);
    }
};
 
var superman = Object.create(superHeroPrototype); 
superman.name = 'Superman';

В отличие от new оператора, одна вещь, которую Javascript абсолютно правильно понял, где Closures.

Закрытие против Lambdas

Закрытия Javascript ничем не отличаются от анонимных внутренних классов Java, используемых определенным образом. возьмем, к примеру, класс FlyingHero :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public interface FlyCommand { 
    public void fly();
}
 
public class FlyingHero {
 
    private String name;
 
    public FlyingHero(String name) {
        this.name = name;
    }
 
    public void fly(FlyCommand flyCommand) {
        flyCommand.fly();
    }
}

Мы можем передать команду fly таким образом в Java 8:

1
2
3
String destination = "Mars"
superMan.fly(() -> System.out.println("Flying to "
    destination ));

Выход этого фрагмента — « Flying to Mars . Обратите внимание, что лямбда FlyCommand должна была «запомнить» место destination переменной, потому что это необходимо для выполнения метода fly позже.

Это понятие функции, которая запоминает переменные вне ее блока, для последующего использования называется закрытием в Javascript. Для получения более подробной информации, посмотрите этот пост в блоге Really Understanding Javascript Closures .

В чем основное различие между лямбдами и затворами?

В Javascript замыкание выглядит так:

1
2
3
4
5
6
7
var destination = 'Mars';
 
var fly = function() { 
    console.log('Fly to ' + destination);
}
 
fly();

Закрытие Javascript, в отличие от Java Lambda, не имеет ограничения на то, что переменная destination должна быть неизменной (или эффективно неизменной, начиная с Java 8).

Это, казалось бы, безобидное различие на самом деле является «убийственной» функцией Javascript-замыканий, поскольку позволяет использовать их для создания инкапсулированных модулей.

Модули и инкапсуляция

В Javascript нет классов и модификаторов public / private , но опять же взгляните на это:

01
02
03
04
05
06
07
08
09
10
function createHero(heroName) {
 
    var name = heroName;
 
    return  {
        fly: function(destination) {
          console.log(name + ' flying to ' + destination);
        }
    };
}

Здесь определяется функция createHero , которая возвращает объект с функцией fly . Функция fly «запоминает» name при необходимости.

Как замыкания связаны с инкапсуляцией?

Когда функция createHero вернется, никто больше не сможет напрямую получить доступ к name , кроме как через fly . Давайте попробуем это:

1
2
3
var superman = createHero('SuperMan');
 
superman.fly('The Moon');

Результат этого фрагмента — SuperMan flying to The Moon . Но случится ли, если мы попытаемся получить доступ к name напрямую?

1
console.log('Hero name = ' + superman.name);

Результат — Hero name = undefined . createHero , что функция createHero является инкапсулированным модулем Javascript с закрытыми «закрытыми» переменными-членами и «открытым» интерфейсом, возвращаемым как объект с функциями.

Блок Область и Подъем

Понять область блока в Javascript просто: нет области блока! Посмотрите на этот пример:

01
02
03
04
05
06
07
08
09
10
11
12
function counterLoop() {
 
    console.log('counter before declaration = ' + i);
 
    for (var i = 0; i < 3 ; i++) {
        console.log('counter = ' + i);
    }
 
    console.log('counter after loop = ' + i);
}
 
counterLoop();

Глядя на это с Java, вы можете ожидать:

  • ошибка в строке 3: «переменная i не существует»
  • значения 0, 1, 2 печатаются
  • ошибка в строке 9: «переменная i не существует»

Оказывается, только одна из этих трех вещей верна, и результат на самом деле таков:

1
2
3
4
5
counter before declaration = undefined 
counter = 0
counter = 1
counter = 2
counter after loop = 3

Поскольку отсутствует область блока, переменная цикла i видима для всей функции. Это означает:

  • строка 3 видит переменную, объявленную, но не инициализированную
  • строка 9 видит I после завершения цикла

Что может быть самым загадочным, так это то, что строка 3 фактически видит объявленную переменную, но не определенную, вместо броска i is not defined .

Это связано с тем, что интерпретатор Javascript сначала сканирует функцию для поиска списка переменных, а затем возвращается к интерпретации строк кода функции одна за другой.

Конечным результатом является то, что это похоже на переменную, которую я поднял наверх, и это то, что на самом деле «видит» среда выполнения Javascript:

01
02
03
04
05
06
07
08
09
10
11
12
function counterLoop() {
 
    var i; // i is 'seen' as if declared here!
 
    console.log('counter before declaration = ' + i);
 
    for (i = 0; i < 3 ; i++) {
        console.log('counter = ' + i);
    }
 
    console.log('counter after loop:  ' + i);
}

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

Это делает подъем явным и видимым для разработчика и помогает избежать ошибок. Следующая версия Javascript (Ecmascript 6) будет включать новое ключевое слово let, чтобы разрешить область видимости блока .

Вывод

Язык Javascript имеет много общего с Java, но также имеет некоторые огромные различия. Некоторые из различий, таких как функции наследования и конструктора, важны, но гораздо меньше, чем можно ожидать для повседневного программирования.

Некоторые из этих функций необходимы в основном разработчикам библиотек, и не обязательно для повседневного программирования приложений. Это не похоже на некоторые из их аналогов Java, которые необходимы каждый день.

Поэтому, если вы не решаетесь попробовать, не позволяйте некоторым из этих функций помешать вам углубиться в язык.

Одно можно сказать наверняка: по крайней мере, некоторый Javascript более или менее неизбежен при разработке Java-интерфейса, поэтому стоит попробовать.

Ссылка: Javascript для разработчиков Java от нашего партнера JCG Алексея Новика в блоге The JHades Blog .