Мой более-менее серьезный путь в программировании начался с написания программ на языке C#, иногда я пробовал писать на JavaScript, и то и дело впадал в ступор в таких ситуациях, когда неверно указывал имя переменной и узнавал об этом спустя много много лет час отладки, так как со мной рядом не было моего компилятора, который бы меня выручил в трудную минуту. Через некоторое время, помимо C# я начал писать много кода на JavaScript и в настоящее время могу делать это без особых трудностей, меня больше не смущает неявное приведение типов и динамическая типизация.
В данной статье я бы хотел систематизировать свои базовые знания об этих языках и рассмотреть их сходства и различия. Данная статья может служить руководством для C# разработчиков, которые хотят изучить JavaScript и наоборот. Также хочу заметить, что в данной статье описываются возможности клиентского JS, так как опыта разработки на Node.js у меня нет. Итак, если вы все ещё не потеряли интерес — приступим.
Namespace и js-модули
В каждой программе для избежания конфликтов в именах переменных, функций, классов или других объектов мы объединяем их в некоторые области. Таким образом, если две разные области будут содержать элементы с одинаковыми именами, конфликта не произойдет.
В C# для разбиения программы на части используются пространства имен. Для их объявления используется ключевое слово namespace
. Например, если мы хотим создать набор компонентов пользовательского интерфейса, то логично поместить их все в одно пространство имен, например, Components
. При этом принято чтобы пространство имен имело следующее именование [AssemblyName].[DirectoryName].[DirectoryName].[...]
. В каждом файле класс компонента пользовательского интерфейса необходимо поместить внутрь пространства имен:
Содержимое файла ComboBox.cs
:
namespace AssemblyName.Components
{
public class ComboBox
{
// ...
}
}
Для того, чтобы начать использовать компоненты необходимо импортировать их из пространства имён следующим образом using AssemblyName.Components
. При данном способе подключения одной строкой мы импортируем все объекты в текущий файл.
В JS для этих же целей применяются ES-модули. При их использовании мы в какой-то степени эмулируем поведение пространств имен написанием дополнительного кода. Рассмотрим тот же пример с библиотекой компонентов. Допустим у нас есть папка Components
, которая содержит компоненты пользовательского интерфейса ComboBox.js
, Checkbox.js
, Button.js
и тд. Для того чтобы получить схожее поведение по сравнению с пространством имен в папке Components
необходимо создать файл index.js
, который будет содержать следующий код:
export { default as Dialog } from './ComboBox';
export { default as Button } from './Button';
export { default as Checkbox } from './Checkbox';
// ...
Для того, чтобы использовать данные компоненты необходимо импортировать их в текущий файл. Это можно сделать следующим образом: import * as Components from './../Components'
, после ключевого слова from
нам необходимо указать путь к папке, в которой находятся все описанные компоненты.
Способы объявления переменных
Ключевое слово var
Как известно C# является строго типизированным языком программирования, поэтому при объявлении переменной компилятору должен быть известен её тип, для этого обычно он указывается перед её именем.
double pi = 3.14;
User user = new User();
int[] a = new[] { 0, 1, 2 };
Но мы также можем сказать компилятору, что он должен вывести тип самостоятельно из выражения, стоящего после знака присваивания. Это стало возможно благодаря введению в версии C# 3.0 ключевого слова var
.
// i - int
var i = 5;
// a - int[]
var a = new[] { 0, 1, 2 };
С помощью var
мы можем создавать объекты анонимного типа:
// anon - Анонимный тип только для чтения
var anon = new { Name = "Terry", Age = 34 };
var type = anon.GetType();//"<>f__AnonymousType0`2"
В JavaScript для объявления переменных также можно использовать ключевое слово var
, однако, в отличии от C# областью видимости данных переменных будет вся функция или объект window
, если переменная была объявлена вне функции.
var a = 5 // область видимости - window
function go() {
var a = 6 // область видимости - функция go
// ...
}
Хоть у вас и есть возможность объявлять переменные с помощью var
в JavaScript, но сейчас этого делать не рекомендуется, после выхода стандарта ES6 было добавлено ключевое слово let
, которое также позволяет объявлять переменные, но его преимуществом заключается в том, что их областью видимости будет являться блок, в котором они объявлены, а не вся функция.
Константы
Как в C#, так и в JavaScript для объявления константного поля используется ключевое слово const
. Правда стоит отметить, что понятие константы в данном случае является различным для данных языков.
В C# константой называется выражение, которое может быть полностью вычислено на этапе компиляции, т.е. константы могут быть числами, логическими значениями, строками или нулевыми ссылками..
const int c1 = 5;
const int c2 = c1 + 100;
const string c3 = "Константа";
const bool c4 = true;
const User human = null;
const User human = new User(firstName); //недопустимо, ошибка компиляции
В JavaScript значение константы также нельзя изменять, однако нет ограничений, накладываемых на значение как в языке C#, ей можно присваивать любые значения/объекты/массивы. Однако, если в константу присвоен объект, то от изменения защищена сама константа, но не свойства внутри неё:
const c1 = 5;
const c2 = c1 + 100;
const c3 = "Константа";
const c4 = true;
const user = {
name: "Петя"
};
user.name = "Петя"; // допустимо
user = 5; // нельзя, будет ошибка
Ключевое слово void
Во время написания данной статьи, я экспериментировал в консоли с функциями и по првычке начал описывать функцию как в C# void SomeFunction...
, и для меня было большой неожиданностью, когда я узнал, что в JavaScript есть ключевое слово void
. Как оказалось void
в JavaScript является унарным оператором, который вычисляет значение операнда, затем отбрасывает его и возвращает undefined
.
alert("Привет!"); // "Привет!"
alert(void "Привет!"); // undefined
Таким образом, можно сказать, что использование void
явно указывает отсутствие возвращаемого значения, подробнее с примерами его использования вы можете ознакомиться в следующей статье статье.
В C# void
не является оператором, однако по сути имеет схожее значение. Здесь он обозначает отсутствие возвращаемого значения функции:
public void SampleMethod()
{
// ...
}
Однако, как можно заметить в примере выше, void
стоит в том месте, где обычно указывается тип возвращаемого значения, и это не случайно, ведь в C# void
также является типом.
var t = typeof(void);
t.Name // System.Void
void
в качестве типа может использоваться только в небезопасном контексте при работе с указателями.
unsafe
{
void* identifier; //позволяется, но не рекомендуется
}
Ключевое слово new
В JavaScript ключевое слово new
является оператором, и используется привычным для многих C-подобных языков образом — для создания объекта.
function Animal() {
//...
}
const animal = new Animal();
В C# new
может использоваться для следующих целей:
- для создания объектов;
- для скрытия наследуемого члена базового класса;
- чтобы ограничить типы, которые могут использоваться в качестве аргументов для параметра типа в универсальном классе.
Первый случай аналогичен применению new
в JavaScript.
class Animal
{
//...
}
var animal = new Animal();
Основные типы данных
В каждом языке имеются типы данных — примитивы, на основе которых строятся другие типы данных, давайте рассмотрим типы данных предоставляемые нам в C# и JavaScript.
Примитивные типы С#:
- Целочисленный со знаком:
sbyte
,short
,int
,long
- Целочисленный без знака:
byte
,ushort
,uint
,ulong
- Символы Unicode:
char
- Набор символов Unicode:
char
- Числа с плавающей запятой:
float
,double
- Десятичный с повышенной точностью:
decimal
- Логическое значение:
bool
Базовый классом является Object
.
Для JS:
Примитивные типы данных:
- Число
number
- Строка
string
- Логический тип
boolean
- Специальное значение
null
- Специальное значение
undefined
symbol
Базовым типом является Object
.
После изучения примитивов обоих языков можно придти к следующим выводам:
- Вместо достаточно большого набора числовых типов в JavaScript имеется единственный тип
number
; - В JavaScript отсутствует тип
char
, вместо него стоит использовать типstring
; - В обоих языках базовым типом является
Object
; - Отличительной особенностью JS является то, что
null
иundefined
выделены в отдельные типы, в то время как в C#null
— это ключевое слово обозначающее отсутствие значения. - В JS присутствует тип
symbol
, который используется в основном внутри самого стандарта JavaScript, для того чтобы иметь возможность добавлять новый функционал без конфликта с существующей кодовой базой.
Как правило, сейчас все больше приложений, в которых необходимо выполнять обработку данных на клиенте, для чего требуется большая точность в вычислениях. В настоящее время в JavaScript отсутствует встроенная возможность работы с большими числами, однако в недалеком будущем планируется добавить новый тип BigInt
. Для решения аналогичных задач в C# имеется класс System.Numerics.BigInteger
.
Проверка типа объекта
Проверка типа является достаточной типичной операцией для большинства языков программирования. Основываясь на типе, мы можем выполнять различные действия. Например, рассмотрим пример из жизни: вы слышите звонок в дверь, если к вам пришел пьяный сосед (объект с типом Пьяный Сосед) чтобы занять денег, то вы вряд ли откроете ему дверь, но если за дверью ваш лучший друг (объект с типом лучший друг), то вы не задумываясь впустите его в квартиру. C# и JavaScript также предоставляют средства для проверки типа объектов.
Оператор typeof
Для получения информации о типе как в C#, так и в JavaScript имеется оператор typeof
. Давайте рассмотрим принцип его работы в обоих языках:
В С# оператор typeof
применяется к типу и возвращает объект класса Type
, который содержит всю информацию о типе.
namespace Zoo {
public class Animal {}
}
Type t = typeof(Animal);
t.Name // 'Animal'
t.FullName // 'Zoo.Animall'
t.GetMethods // Информация о методах
t.GetFields // Информация обо всех полях
// ...
В JS typeof
возвращает строку, указывающую тип операнда.
typeof 30 // 'number'
typeof Symbol() // 'symbol'
typeof undefined // 'undefined'
// Объекты
typeof new Animal() // object
typeof null // 'object'
typeof [1,2,3] // 'object'
// Функции
typeof function() {} // 'function';
typeof class C {} // 'function';
В примере выше можно заметить некоторые особенности работы данного оператора. Кажется логичным, если выражение typeof new Animal()
возвращало бы строку 'Animal'
, a typeof [1,2,3]
— строку Array
, однако как бы ни было парадоксально, результатом в обоих случаях является 'object'
. Также в связи с тем, что классы в JS являются оберткой над функциями, то выражение typeof class C {}
вернет 'function'
вместо 'class'
. Ещё одним интересным фактом является то, что выражение typeof null
вернёт 'object'
. В JavaScript данный оператор имеет большой недостаток: все не примитивные объекты для него на одно лицо, все они имеют один тип object
.
Стоит заметить, что в JavaScript typeof
можно применять к чему угодно: объектам, функциям, классам и т.д… В C# данный оператор применяется лишь к типам.
is и instanceof
Помимо получения информации о типе, порой бывает полезно проверить принадлежность объекта к определенному типу.
В C# для данных целей имеется оператора is
.
class Person
{
}
//Наследуем Programmer от Person
class Programmer : Person
{
}
var person = new Person();
var programmer = new Programmer();
person is Person //true
person is Programmer //false
programmer is Person //true
programmer is Programmer //true
В JavaScript для того, чтобы выяснить к какому типу принадлежит объект необходимо использовать оператор — instanceof
.
function Person() {}
function Programmer() {}
//Наследуем Programmer от Person
Programmer.prototype = Object.create(Person.prototype);
var person = new Person();
var programmer = new Programmer();
console.log(person instanceof Person); // true
console.log(person instanceof Programmer); // false
console.log(programmer instanceof Person); // true
console.log(programmer instanceof Programmer); // true
Логические значения и проверка на null
Практически повсеместно, для того чтобы не получить Null reference exception
, перед использованием переменной мы проверяем её на null
, а в случае с JavaScript, ещё и на undefined
.
В C# мы постоянно видим подобный код:
if(user != null && String.IsNullOrEmpty(user.name)) {
user.SetName("Петя");
}
В JavaScript данную конструкцию можно записать несколько короче. Связано это с тем, что в отличии от C#, в JavaScript множество значений кроме false
при приведении типов также расцениваются как false
:
null
undefined
- «» (пустая строка)
0
NaN
(not a number)
Таким образом, приведенный выше код на C# можно записать следующим образом:
if (user && !user.name) {
user.setName("Петя");
}
или
user && !user.name && user.setName("Петя");
В связи с тем, что проверки на null
происходят повсеместно, в C# 6.0 был добавлен Null Propagation Operator .?
.
Код на языке C#:
if (user != null && user.parent != null && user.parent.parent != null) {
user.parent.parent.SetName("Петя");
}
С его помощью данный участок кода можно переписать следующим образом:
user?.parent?.parent?.SetName("Петя");
В JavaScript обычно делают следующим образом:
user && user.parent && user.parent.parent && user.parent.parent.setName("Петя");
Установка значений по-умолчанию
Ещё одной частой операцией является установка значений по-умолчанию, с версии 2.0 в C# появился Null Coalescing Operator — ??
.
Следующие две строки кода на C# являются эквивалентными:
var name = user != null && user.name != null ? user.name : "Петя";
var name = user?.name ?? "Петя";
В JavaScript подобную операцию обычно делают следующим образом.
var name = user && user.name || "Петя";
Однако мы можем применять операторы &&
и ||
только в том случае, если 0
, false
и пустая строка не являются допустимыми значениями.
В обозримом будущем операторы ?.
, ??
должны появиться и в JavaScript (в настоящее время они прошли стадию Stage0), подробнее об этих операторах в JavaScript можно прочитать в статье.
Ключевое слово this
Как в C#, так и в JavaScript имеется ключевое слово this
. Обычно в C# понимание предназначения this
не вызывает никакого труда, однако в JavaScript это является одной из самых сложных концепций языка. Далее рассмотрим применение this
на примерах.
В C# ключевое слово this
указывает на текущий экземпляр класса.
class User
{
public string Name { get; set; }
public void PrintEmployee() {
Console.WriteLine(this.name);
}
}
var employee = new Employee();
E1.PrintEmployee();
В данном примере в выражении Console.WriteLine(this.name)
, this
указывает на переменную employee
.
Так как this
— текущий экземпляр класса, то его нельзя использовать в методах не привязанных к определенному типу, например в статических методах.
В JavaScript значение this
называется контекстом вызова и будет определено в момент вызова функции. Если одну и ту же функцию запускать в контексте разных объектов, она будет получать разный this
:
var user = { firstName: "Петя" };
var admin = { firstName: "Админ" };
function func() {
alert( this.firstName );
}
user.f = func;
admin.g = func;
// this равен объекту перед точкой:
user.f(); // Петя
admin.g(); // Админ
func();// undefined - в данном случае this - глобальный объект window
К тому же, в JavaScript присутствует возможность явного указания значения this
с помощью функций: call
, bind
, apply
. Например, вышеприведенный пример можно переписать следующим образом:
var user = { firstName: "Петя" };
var admin = { firstName: "Админ" };
function func() {
alert( this.firstName );
}
// this равен объекту перед точкой:
func.call(user); // Петя
func.call(admin); // Админ
func.bind(user)();// Петя
func.bind(admin)();// Админ
Деструктуризация
Часто бывает необходимо присвоить несколько полей объекта локальным переменным. Например, как часто вы наблюдаете подобный код?
void Method(User user)
{
var firstName = user.FirstName;
var lastName = user.LastName;
//...
}
Для подобных целей можно использовать деструктуризацию. Данную возможность в разной степени поддерживают оба языка.
В C# 7.0 для поддержки деструктуризации появился новый вид функций, называемый деконструкторами. Для того чтобы объявить деконструктор нам необходимо определить метод с именем Deconstruct
, все параметры которого должны быть объявлены с модификатором out
:
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
// Объявление деконструктора
public void Deconstruct(out string firstName, out string lastName)
{
firstName = this.FirstName;
lastName = this.LastName;
}
}
...
Person person = new Person { FirstName = "Петя", LastName = "Петров" };
(string firstName, string lastName) = person;
(string firstName, _ ) = person;
Поддержка деструктуризации или (destructuring assignment) в JavaScript появилась в шестом стандарте EcmaScript. С её помощь. можно присвоить массив или объект сразу нескольким переменным, разбив его на части.
let [firstName, lastName] = ["Петя", "Петров"];
let [firstName, _ ] = ["Петя", "Петров"];
let { firstName, lastName } = { firstName: "Петя", lastName: "Петров" };
let { firstName } = { firstName: "Петя", lastName: "Петров" };
Стоит отметить, что деструктуризация в JavaScript имеет больше возможностей, чем в C#:
- Изменение порядка переменных;
- Отсутствие необходимости явного объявления деконструкторов;
- Поддержка деструктуризации массивов;
- Установки значений по-умолчанию;
- Присваивание свойств объекта в переменную с другим именем;
- Поддержка вложенной деструктуризации.
Заключение
В данной статье мы обсудили лишь самые основные концепции языков C# и JavaScript. Но остались не затронуты ещё многие аспекты:
- коллекции
- функции
- классы
- многопоточность
Каждая из этих тем является достаточно обширной и будет раскрыта в дальнейшем в отдельной статье.