Этот пост будет посвящен языку 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 ).
Возьмем, к примеру, эту программу, которая инициализирует пустой объект и задает два свойства:
// create an empty object - no class was needed !! var superhero = {}; superhero.name = 'Superman'; superhero.strength = 100;
Объекты Javascript похожи на Java HashMap со связанными свойствами, где ключами являются только строки. Следующее будет «эквивалентным» Java-кодом:
Map<String,Object> superhero = new HashMap<>(); superhero.put("name","Superman"); superhero.put("strength", 100);
Это означает, что объект Javascript — это просто многоуровневая «карта хешей» пар ключ / значение без определения класса.
Функции — это просто ценности
Функции в Javascript — это просто значения типа Function
, это просто! Взять, к примеру:
var flyFunction = function() { console.log('Flying like a bird!'); }; superhero.fly = flyFunction;
Это создает функцию (значение типа Function
) и присваивает ее переменной flyFunction
. fly
Затем в объекте супергероя создается новое имя с именем , которое может быть вызвано так:
// prints 'Flying like a bird!' to the console superhero.fly();
Java это не имеет эквивалента Javascript Function
типа, но почти. Возьмем для примера SuperHero
класс, который принимает Power
функцию:
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:
// 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. Давайте начнем с примера:
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
свойства:
var failThis = superman.sayHello; failThis();
Запуск этого фрагмента даст в качестве вывода: Hello, I'm undefined
.
Почему больше this
не работает?
Это потому, что переменная hello
принадлежит глобальной области, которая не содержит именованной переменной-члена heroName
. Чтобы решить это:
В Javascript значение
this
ключевого слова полностью переопределено, чтобы быть тем, что мы хотим!
// overrides 'this' with superman hello.call(superman);
Фрагмент выше напечатает снова Hello, I'm Superman
. Это означает, что значение this
зависит как от контекста, в котором вызывается функция, так и от того, как вызывается функция.
Классическое наследование против прототипа
В Javascript нет наследования классов, вместо этого объекты могут наследоваться напрямую от других объектов. Это работает так, что каждый объект имеет неявное свойство, которое указывает на «родительский» объект.
Это свойство называется __proto__
, а родительский объект называется прототипом объекта , отсюда и название Prototypal Inheritance.
Как prototype
работает?
При поиске свойства Javascript будет пытаться найти свойство в самом объекте. Если он не находит его, он пробует его прототип и так далее. Например:
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
, которое get найдено.
Как это соотносится с наследованием Java?
Давайте теперь скажем, что права на Мстителей были куплены DC Comics:
avengersHero.editor = 'DC Comics';
Если мы позвоним ironMan.editor
снова, мы теперь получим Iron Man is copyrighted by DC Comics
. Все существующие экземпляры объекта с avengersHero
прототипом теперь видны DC Comics
без необходимости повторного создания .
Этот механизм очень простой и очень мощный. Все, что может быть сделано с наследованием класса, может быть сделано с наследованием прототипа. Но как насчет конструкторов?
Конструкторы против функций конструктора
В Javascript была сделана попытка сделать создание объектов похожим на языки, подобные Java. Давайте возьмем для примера:
function SuperHero(name, strength) { this.name = name; this.strength = strength; }
Обратите внимание на заглавное имя, указывающее, что это функция конструктора. Посмотрим, как это можно использовать:
var superman = new SuperHero('Superman', 100); console.log('Hello, my name is ' + superman.name);
Этот фрагмент кода выводится Hello, my name is Superman
.
Вы можете подумать, что это похоже на Java, и в этом суть! В new
действительности этот синтаксис создает новый пустой объект, а затем вызывает функцию конструктора, заставляя this
быть вновь созданным объектом.
Почему этот синтаксис не рекомендуется тогда?
Допустим, мы хотим указать, что у всех супер героев есть sayHello
метод. Это можно сделать, поместив sayHello
функцию в общий объект-прототип:
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
:
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
класс:
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:
String destination = "Mars"; superMan.fly(() -> System.out.println("Flying to " + destination ));
Выход этого фрагмента является Flying to Mars
. Обратите внимание, что FlyCommand
лямбда должен был «запомнить» переменную destination
, потому что она нужна для выполнения fly
метода позже.
Это понятие функции, которая запоминает переменные вне ее блока, для последующего использования называется закрытием в Javascript. Для получения более подробной информации, посмотрите этот пост в блоге Really Understanding Javascript Closures .
В чем основное различие между лямбдами и затворами?
В Javascript замыкание выглядит так:
var destination = 'Mars'; var fly = function() { console.log('Fly to ' + destination); } fly();
Закрытие Javascript, в отличие от Java Lambda, не имеет ограничения на то, что destination
переменная должна быть неизменной (или эффективно неизменной, начиная с Java 8).
Это, казалось бы, безобидное различие на самом деле является «убийственной» функцией Javascript-замыканий, поскольку позволяет использовать их для создания инкапсулированных модулей.
Модули и инкапсуляция
Нет классов в Javascript и нет public
/ private
модификаторов, но опять же взглянем на это:
function createHero(heroName) { var name = heroName; return { fly: function(destination) { console.log(name + ' flying to ' + destination); } }; }
Здесь определяется функция createHero
, которая возвращает объект, у которого есть функция fly
. fly
Функция «помнит» , name
когда это необходимо.
How do Closures relate to Encapsulation?
When the createHero
function returns, noone else will ever be able to directly access name
, except via fly
. Let’s try this out:
var superman = createHero('SuperMan'); superman.fly('The Moon');
The output of this snippet is SuperMan flying to The Moon
. But happens if we try to access name
directly ?
console.log('Hero name = ' + superman.name);
The result is Hero name = undefined
. The function createHero
is said to a be a Javascript encapsulated module, with closed ‘private’ member variables and a ‘public’ interface returned as an object with functions.
Block Scope and Hoisting
Understanding block scope in Javascript is simple: there is no block scope! Take a look at this example:
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();
By looking at this coming from Java, you might expect:
- error at line 3: ‘variable i does not exist’
- values 0, 1, 2 are printed
- error at line 9: ‘variable i does not exist’
It turns out that only one of these three things is true, and the output is actually this:
counter before declaration = undefined
counter = 0
counter = 1
counter = 2
counter after loop = 3
Because there is no block scope, the loop variable i is visible for thewhole function. This means:
- line 3 sees the variable declared but not initialized
- line 9 sees i after the loop has terminated
What might be the most puzzling is that line 3 actually sees the variable declared but undefined, instead of throwing i is not defined
.
This is because the Javascript interpreter first scans the function for a list of variables, and then goes back to interpret the function code lines one by one.
The end result is that it’s like the variable i was hoisted to the top, and this is what the Javascript runtime actually ‘sees’:
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); }
To prevent surprises caused by hoisting and lack of block scoping, it’s a recommended practice to declare variables always at the top of functions.
This makes hoisting explicit and visible by the developer, and helps to avoid bugs. The next version of Javascript (Ecmascript 6) will include anew keyword ‘let’ to allow block scoping.
Conclusion
The Javascript language shares a lot of similarities with Java, but also some huge differences. Some of the differences like inheritance and constructor functions are important, but much less than one would expect for day to day programming.
Some of these features are needed mostly by library developers, and not necessarily for day to day application programming. This is unlike some of their Java counterparts which are needed every day.
So if you are hesitant to give it a try, don’t let some of these features prevent you from going further into the language.
One thing is for sure, at least some Javascript is more or less inevitable when doing Java frontend development, so it’s really worth to give it a try.