Язык JavaScript претерпел много изменений с момента своего создания. Поэтому сейчас не так просто отслеживать хорошие практики с таким количеством новых функций, изменений и фреймворков.
В этой статье мы рассмотрим некоторые общие правила, которые лучше всего использовать в JavaScript. Эти советы помогут нам писать код лучше. Они подходят как для новичков, так и для опытных разработчиков.
Посмотрим пару примеров:
// нежелательно
const x = 0, y = 10, z = 20;
// предпочтительно
const x = 0;
const y = 10;
const z = 20;
Объявление нескольких переменных на одной строке может выглядеть "круче", но это непрактично. Код становится менее читабельным. Этот подход также может привести к нежелательным результатам.
В нижеприведенном примере может показаться, что x, y, и z - все константы. Но это неправда, константой является только x.
const x = y = z = 10;
// y и z не константы, им можно присвоить новые значения
y = 15; // works
z = 15; // works
// только x константа
x = 15; // error: Uncaught TypeError: Assignment to constant variable.
Но в любом правиле есть свои исключения. Мы можем определять более одной переменной на одной линии, используя деструктуризацию.
Давайте посмотрим на код:
const point = { x: 10, y: 15, z: 20 };
// желательно
const { x, y } = point;
// выведет 10
console.log(x);
// выведет 15
console.log;
Пример выше - лучшее решение.
Эти способы скрыты и не всегда понятны. Каждый движок имеет собственную реализацию оптимизаций. Например, у движка Хромаv8 этот механизм называется TurboFan. Понимая некоторую базу его внутреннего устройства, мы может писать более эффективный код.
Если мы будем игнорировать движок, производительность может пострадать. Наше приложение будет работать медленнее, что явно не понравится пользователю.
Посмотрим на несколько ситуаций, откуда мы можем извлечь пользу:
Однако, тут две стороны одной медали. При изменении прототипа объекта движок пересчитывает все оптимизации. Из-за этих изменений приложение работает медленнее. Это даже может замедлить код, который взаимодействует с прототипом.
Рассмотрим пример плохого использования:
// объявление прототипа
function Item() {}
Item.prototype.save = () => {};
Item.prototype.delete = () => {};
...
...
function foo() {
// плохо, мы не должны менять прототип
delete Item.prototype.save;
// выведете undefined: save метод больше недоступен
console.log(Item.prototype.save);
}
Если вам правда необходим похожий функционал, просто используйте свойства самого объекта. Это не приведет к пересчету оптимизаций его прототипа.
Посмотрим на исправленный пример:
// объявление прототипа
function Item() {
this.save = () => {};
}
Item.prototype.delete = () => {};
...
...
function foo() {
const newItem = new Item();
// желательно, удаление свойства без изменения самого прототипа
delete newItem.save;
// выведет undefined: save метод больше недоступен
console.log(newItem.save);
}
Больше можно узнать в статье от MDN.
Как происходят эти оптимизации? Когда функция выполняется часто, она "нагревается". Движок хранит скомпилированную версию. Когда функция становится "горячее", она отправляется в качестве оптимизирующего компилятора. Там используется множество стратегий оптимизаций.
Одна из таких стратегий - типизация. Функция создаст заглушку для каждой комбинации типа и параметров. Это значит, если наша функция мономорфная (с одинаковым типом параметров), потребуется всего одна заглушка. Если она полиморфная, потребуется одна заглушка для каждой комбинации параметра и типа.
Соответственно, если следить за типами параметров, можно улучшить производительность.
Рассмотрим пример:
function add(a, b) {
return a + b;
}
// "горячую" функцию JIT оптимизирует эффективно
add(1,2);
add(1,1);
add(2,3);
add(4,5);
Если бы мы выполняли эту функцию с разными типами параметров, код не выполнялся бы так быстро.
Как раз посмотрим на такой вариант:
function sum(a, b) {
return a + b;
}
// если "горячая" функция будет храниться
// будет создано много заглушек для каждой комбинации
add(1,2);
add(1,'3');
add(2,true);
add(4,5);
TypeScript, например, может помочь нам сделать наши методы максимально эффективными.
"Ранний вызов return" - это шаблон, в котором рекомендуется возвращать какой-либо результат настолько рано, насколько это возможно, не прибегая к else выражению.
Взглянем на классическую реализацию FizzBuzz функции.
Код ниже мог бы быть решением:
function FizzBuzz(i) {
let result = undefined;
if (i % 15 == 0) {
result = 'FizzBuzz';
} else if (i % 3 == 0) {
result = 'Fizz';
} else if (i % 5 == 0) {
result = 'Buzz';
} else {
result = i;
}
return result;
}
Однако, его можно улучшить, используя шаблон раннего вызова return:
function FizzBuzz(i) {
if (i % 15 == 0) {
return 'FizzBuzz';
}
if (i % 3 == 0) {
return 'Fizz';
}
return (i % 5 == 0) ? 'Buzz' : i;
}
В итоге наш код стал:
Правда, это все еще синтаксический сахар для обычного прототипного наследования в JS. Это может привести к конфликтам.
По моему мнению, функциональный подход предполагает модульность и очень легок при тестировании.
Мы можем наблюдать как создатели React упростили процесс разработки, отказавшись от классового подхода. Даже если вы никогда не писали на React, сразу сможете заметить большую разницу.
Посмотрим на пример с классами на React:
class ClassComponent extends React.Component{
constructor(){
super();
this.state={
count :0
};
}
increase = () => {
this.setState({count : this.state.count + 1});
}
render(){
return (
<div>
<p> {this.state.count}</p>
<button onClick={this.increase}> Add</button>
</div>
)
}
}
А теперь перепишем тот же компонент, используя функциональное программирование:
const functionComponent = () => {
const [count, setCount] = useState(0);
const increase = () => setState(count + 1);
return (
<div>
<p> {count}</p>
<button onClick={increase}> Add</button>
</div>
);
}
В результате мы тратим меньше кода, что делает его более простым.
У функционального подхода есть много преимуществ. Он спасает нас от большого количества проблем, связанных с изменениями сущностей.
Он использует приведение типов, чтобы сравнить их значения. Например, когда сравниваются число и строка, движок преобразует строку в число и после этого сравнивает их.
Оператор == сравниваниет значения, но не типы.
Посмотрим несколько примеров:
'1' == 1 // true
true == 1 // true
false == 0 // true
'0' == false // true
Это может привести к некоторым "веселым" ситуациям, изображенным на картинке ниже:
Чтобы предотвратить нежелательное поведение, лучше проверять и тип, и значение. Это возможно с помощью оператора === .
При строгом сравнении сначала будут сравниваться типы значений. Если они не равны, то вернется false. Только если они совпадут, будут проверяться сами значения.
Есть такая особенность, что NaN ничему не равно при строгом сравнении. Для этого мы можем использовать isNan() .
Посмотрим на оператор === в действии:
1 === 1 // true
1 === '1' // false
const obj = {};
obj === obj // true
'x' === 'x' // true
NaN === NaN // false
isNaN(NaN) // true
Async/await функционал был представлен в ES7. Он упростил работу с промисами в языке. Работать с асинхронностью стало удобнее, ведь ведь всю тяжелую работу на себя взял движок. А выпуск await верхнего уровня в ES12 добавил последнюю часть, отсутствующую в этом функционале.
Сейчас осталось не так много случаев, когда нам надо использовать промисы вместо async/await. Использование async/await повысит читабельность нашего кода.
Рассмотрим пример:
const createItem = (id) => Promise.resolve(true);
const updateStock = () => Promise.resolve(true);
function addItem() {
createItem()
.then(({ id }) => updateStock(id))
.then(() => console.log('success'))
.catch(() => console.error('oops error'));
}
Если мы перепишем его с async/await, то его станет легче прочитать:
const createItem = (id) => Promise.resolve(true);
const updateStock = () => Promise.resolve(true);
async function addItem() {
try {
const { id } = await createItem();
await updateStock(id);
console.log('success');
} catch {
console.error('oops error');
}
}
Async/await хорошо работает с новыми методами промисов такими как Promise.all, Promise.any, Promise.allSettled и тд.
Посмотрим пример с промисами и async/await в ES12:
(async () => {
const result = await Promise.any([
Promise.reject('Error 1'),
Promise.reject('Error 2'),
Promise.resolve('success'),
]);
console.log(`result: ${result}`);
})();
// result: success
Обязательно используйте ESLint. Он автоматизирует цикл разработки, помогает нам писать код лучше и ускоряет процесс проверки кода.
В этой статье мы рассмотрим некоторые общие правила, которые лучше всего использовать в JavaScript. Эти советы помогут нам писать код лучше. Они подходят как для новичков, так и для опытных разработчиков.
1. Объявление одной переменной на одной линии
В JavaScript вы можете объявить сразу несколько переменных на одной линии. Это то, что я не рекомендовал бы делать. Объявление переменных на разных линиях делает код более простым для чтения и понимания.Посмотрим пару примеров:
// нежелательно
const x = 0, y = 10, z = 20;
// предпочтительно
const x = 0;
const y = 10;
const z = 20;
Объявление нескольких переменных на одной строке может выглядеть "круче", но это непрактично. Код становится менее читабельным. Этот подход также может привести к нежелательным результатам.
В нижеприведенном примере может показаться, что x, y, и z - все константы. Но это неправда, константой является только x.
const x = y = z = 10;
// y и z не константы, им можно присвоить новые значения
y = 15; // works
z = 15; // works
// только x константа
x = 15; // error: Uncaught TypeError: Assignment to constant variable.
Но в любом правиле есть свои исключения. Мы можем определять более одной переменной на одной линии, используя деструктуризацию.
Давайте посмотрим на код:
const point = { x: 10, y: 15, z: 20 };
// желательно
const { x, y } = point;
// выведет 10
console.log(x);
// выведет 15
console.log;
Пример выше - лучшее решение.
2. Понимание браузерной оптимизации
Поскольку JavaScript не компилируется, единственный способ оптимизации, которую делает движок — во время выполнения. Он придумывает способы оптимизации на ходу.Эти способы скрыты и не всегда понятны. Каждый движок имеет собственную реализацию оптимизаций. Например, у движка Хромаv8 этот механизм называется TurboFan. Понимая некоторую базу его внутреннего устройства, мы может писать более эффективный код.
Если мы будем игнорировать движок, производительность может пострадать. Наше приложение будет работать медленнее, что явно не понравится пользователю.
Посмотрим на несколько ситуаций, откуда мы можем извлечь пользу:
Прототипы
Движок JavaScript понимает, что какие-либо изменения прототипов довольно редки. Прототип объекта стабилен и предсказуем. Вот почему движок делает некоторые оптимизации на ранних этапах прототипов.Однако, тут две стороны одной медали. При изменении прототипа объекта движок пересчитывает все оптимизации. Из-за этих изменений приложение работает медленнее. Это даже может замедлить код, который взаимодействует с прототипом.
Рассмотрим пример плохого использования:
// объявление прототипа
function Item() {}
Item.prototype.save = () => {};
Item.prototype.delete = () => {};
...
...
function foo() {
// плохо, мы не должны менять прототип
delete Item.prototype.save;
// выведете undefined: save метод больше недоступен
console.log(Item.prototype.save);
}
Если вам правда необходим похожий функционал, просто используйте свойства самого объекта. Это не приведет к пересчету оптимизаций его прототипа.
Посмотрим на исправленный пример:
// объявление прототипа
function Item() {
this.save = () => {};
}
Item.prototype.delete = () => {};
...
...
function foo() {
const newItem = new Item();
// желательно, удаление свойства без изменения самого прототипа
delete newItem.save;
// выведет undefined: save метод больше недоступен
console.log(newItem.save);
}
Больше можно узнать в статье от MDN.
Типизация
Так как JavaScript использует JIT компиляцию, ему необходимо делать много проверок перед тем как выполнить какую-либо функцию. Во многом он зависит от оптимизаций.Как происходят эти оптимизации? Когда функция выполняется часто, она "нагревается". Движок хранит скомпилированную версию. Когда функция становится "горячее", она отправляется в качестве оптимизирующего компилятора. Там используется множество стратегий оптимизаций.
Одна из таких стратегий - типизация. Функция создаст заглушку для каждой комбинации типа и параметров. Это значит, если наша функция мономорфная (с одинаковым типом параметров), потребуется всего одна заглушка. Если она полиморфная, потребуется одна заглушка для каждой комбинации параметра и типа.
Соответственно, если следить за типами параметров, можно улучшить производительность.
Рассмотрим пример:
function add(a, b) {
return a + b;
}
// "горячую" функцию JIT оптимизирует эффективно
add(1,2);
add(1,1);
add(2,3);
add(4,5);
Если бы мы выполняли эту функцию с разными типами параметров, код не выполнялся бы так быстро.
Как раз посмотрим на такой вариант:
function sum(a, b) {
return a + b;
}
// если "горячая" функция будет храниться
// будет создано много заглушек для каждой комбинации
add(1,2);
add(1,'3');
add(2,true);
add(4,5);
TypeScript, например, может помочь нам сделать наши методы максимально эффективными.
3. Ранний вызов return
Мы привыкли к шаблону if/else, и не подвергали его сомнению. Однако, с опытом вы могли понять, что использование полного ветвления это:- неэффективно;
- трудно читать;
- трудно поддерживать.
"Ранний вызов return" - это шаблон, в котором рекомендуется возвращать какой-либо результат настолько рано, насколько это возможно, не прибегая к else выражению.
Взглянем на классическую реализацию FizzBuzz функции.
Код ниже мог бы быть решением:
function FizzBuzz(i) {
let result = undefined;
if (i % 15 == 0) {
result = 'FizzBuzz';
} else if (i % 3 == 0) {
result = 'Fizz';
} else if (i % 5 == 0) {
result = 'Buzz';
} else {
result = i;
}
return result;
}
Однако, его можно улучшить, используя шаблон раннего вызова return:
function FizzBuzz(i) {
if (i % 15 == 0) {
return 'FizzBuzz';
}
if (i % 3 == 0) {
return 'Fizz';
}
return (i % 5 == 0) ? 'Buzz' : i;
}
В итоге наш код стал:
- более продуманным;
- более читабельным;
- более эффективным.
4. Принятие функционального программирования
JavaScript это мультипарадигменный язык программирования. Мы можем выбирать между объектно-ориентированным программированием и функциональным. Первый подход стал более доступным с появлением классов в ES6.Правда, это все еще синтаксический сахар для обычного прототипного наследования в JS. Это может привести к конфликтам.
По моему мнению, функциональный подход предполагает модульность и очень легок при тестировании.
Мы можем наблюдать как создатели React упростили процесс разработки, отказавшись от классового подхода. Даже если вы никогда не писали на React, сразу сможете заметить большую разницу.
Посмотрим на пример с классами на React:
class ClassComponent extends React.Component{
constructor(){
super();
this.state={
count :0
};
}
increase = () => {
this.setState({count : this.state.count + 1});
}
render(){
return (
<div>
<p> {this.state.count}</p>
<button onClick={this.increase}> Add</button>
</div>
)
}
}
А теперь перепишем тот же компонент, используя функциональное программирование:
const functionComponent = () => {
const [count, setCount] = useState(0);
const increase = () => setState(count + 1);
return (
<div>
<p> {count}</p>
<button onClick={increase}> Add</button>
</div>
);
}
В результате мы тратим меньше кода, что делает его более простым.
У функционального подхода есть много преимуществ. Он спасает нас от большого количества проблем, связанных с изменениями сущностей.
5. Использование '===' для проверки на равенство
Оператор == - оператор сравнения, который приводит операнды к одному и тому же типу.Он использует приведение типов, чтобы сравнить их значения. Например, когда сравниваются число и строка, движок преобразует строку в число и после этого сравнивает их.
Оператор == сравниваниет значения, но не типы.
Посмотрим несколько примеров:
'1' == 1 // true
true == 1 // true
false == 0 // true
'0' == false // true
Это может привести к некоторым "веселым" ситуациям, изображенным на картинке ниже:
Чтобы предотвратить нежелательное поведение, лучше проверять и тип, и значение. Это возможно с помощью оператора === .
При строгом сравнении сначала будут сравниваться типы значений. Если они не равны, то вернется false. Только если они совпадут, будут проверяться сами значения.
Есть такая особенность, что NaN ничему не равно при строгом сравнении. Для этого мы можем использовать isNan() .
Посмотрим на оператор === в действии:
1 === 1 // true
1 === '1' // false
const obj = {};
obj === obj // true
'x' === 'x' // true
NaN === NaN // false
isNaN(NaN) // true
6. Await вместо промисов
До промисов работа с асинхронными операциями была довольно утомительной. Все делалось через колбэки. Это приводило к, так называемому, "аду колбэков" (callback hell). Код было трудно читать и поддерживать. Промисы помогли писать код лучше, но они все еще далеки от совершенства, и могут привести к "аду промисов" (promise hell).Async/await функционал был представлен в ES7. Он упростил работу с промисами в языке. Работать с асинхронностью стало удобнее, ведь ведь всю тяжелую работу на себя взял движок. А выпуск await верхнего уровня в ES12 добавил последнюю часть, отсутствующую в этом функционале.
Сейчас осталось не так много случаев, когда нам надо использовать промисы вместо async/await. Использование async/await повысит читабельность нашего кода.
Рассмотрим пример:
const createItem = (id) => Promise.resolve(true);
const updateStock = () => Promise.resolve(true);
function addItem() {
createItem()
.then(({ id }) => updateStock(id))
.then(() => console.log('success'))
.catch(() => console.error('oops error'));
}
Если мы перепишем его с async/await, то его станет легче прочитать:
const createItem = (id) => Promise.resolve(true);
const updateStock = () => Promise.resolve(true);
async function addItem() {
try {
const { id } = await createItem();
await updateStock(id);
console.log('success');
} catch {
console.error('oops error');
}
}
Async/await хорошо работает с новыми методами промисов такими как Promise.all, Promise.any, Promise.allSettled и тд.
Посмотрим пример с промисами и async/await в ES12:
(async () => {
const result = await Promise.any([
Promise.reject('Error 1'),
Promise.reject('Error 2'),
Promise.resolve('success'),
]);
console.log(`result: ${result}`);
})();
// result: success
Итог
Это были мои заметки по разработке, когда дело доходит до JavaScript. Конечно, их намного больше, но эти шесть - мои фавориты. Важность использования Strict также очень близка к ним. К счастью, нам не нужно об этом сильно беспокоиться, поскольку есть инструменты, которые уже сделают все за нас.Обязательно используйте ESLint. Он автоматизирует цикл разработки, помогает нам писать код лучше и ускоряет процесс проверки кода.
6 вещей на JavaScript, которые можно делать и нельзя
Язык JavaScript претерпел много изменений с момента своего создания. Поэтому сейчас не так просто отслеживать хорошие практики с таким количеством новых функций, изменений и фреймворков. В этой...
habr.com