15 советов по программированию на языке JavaScript

Представляем вашему вниманию 15 советов по программированию на языке JavaScript. Вы узнаете много полезного, а также сможете улучшить навык программирования на этом языке.

var против let / const

Помимо var, у нас есть доступ к двум новым идентификаторам для хранения значений: let и const. Давайте посмотрим, чем они отличаются друг от друга.

Пример использования var:

var snack = 'Meow Mix';

function getFood(food) {
    if (food) {
        var snack = 'Friskies';
        return snack;
    }
    return snack;
}

getFood(false); // не определен

А теперь посмотрите, что происходит, когда мы заменяем var на let:

let snack = 'Meow Mix';

function getFood(food) {
    if (food) {
        let snack = 'Friskies';
        return snack;
    }
    return snack;
}

getFood(false); // 'Meow Mix'

Переменная, объявленная через let, видна только в рамках блока {...}, в котором объявлена.

Данное изменение в поведении показывает, что нам нужно быть осторожными при оптимизации устаревшего кода с var. Замена var на let может привести к неожиданным последствиям.

Примечание: let и const являются идентификаторами с блочными областями, и когда вы будете ссылаться на идентификаторы до их определения, вы получите ReferenceError.

console.log(x); // ReferenceError: x не объявлен

let x = 'hi';

Лучшая практика: При работе над новой базой кода используйте let для переменных, которые будут изменять свое значение с течением времени, а const для переменных, которые не будут его изменять.

Замена IIFEs блоками

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

(function () {
    var food = 'Meow Mix';
}());

console.log(food); // Reference Error

А теперь используем блочную область:

{
    let food = 'Meow Mix';
};

console.log(food); // Reference Error

Стрелочные функции в языке JavaScript

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

function Person(name) {
    this.name = name;
}

Person.prototype.prefixName = function (arr) {
    return arr.map(function (character) {
        return this.name + character; // Невозможно прочитать свойства неопределенного 'name'
    });
};

Одним из распространенных решений этой проблемы является сохранение контекста this с помощью использования переменной:

function Person(name) {
    this.name = name;
}

Person.prototype.prefixName = function (arr) {
    var that = this; // Сохранение контекста this
    return arr.map(function (character) {
        return that.name + character;
    });
};

Мы также можем перейти к заданному контексту this:

function Person(name) {
    this.name = name;
}

Person.prototype.prefixName = function (arr) {
    return arr.map(function (character) {
        return this.name + character;
    }, this);
};

Привязка контекста:

function Person(name) {
    this.name = name;
}

Person.prototype.prefixName = function (arr) {
    return arr.map(function (character) {
        return this.name + character;
    }.bind(this));
};

Используя стрелочные функции, лексическое значение this не меняется. Перепишем код с использованием стрелочных функций:

function Person(name) {
    this.name = name;
}

Person.prototype.prefixName = function (arr) {
    return arr.map(character => this.name + character);
};

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

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

var squares = arr.map(function (x) { return x * x }); // Выражение функции
const arr = [1, 2, 3, 4, 5];
const squares = arr.map(x => x * x);

Лучшая практика: используйте стрелочные функции вместо выражений функций, если возможно.

Строки в языке JavaScript

С ES6 стандартная библиотека значительно изменилась. Наряду с этими изменениями применяются новые методы, такие как .includes() и  .repeat(), которые можно использовать для строк.

.includes( )

var string = 'food';
var substring = 'foo';

console.log(string.indexOf(substring) > -1);

Чтобы обозначить ограничение строки, мы можем просто использовать .includes(), который вернет логическое значение, вместо проверки > -1:

const string = 'food';
const substring = 'foo';

console.log(string.includes(substring));

.repeat( )

function repeat(string, count) {
    var strings = [];
    while(strings.length < count) {
        strings.push(string);
    }
    return strings.join('');
}

Теперь у нас есть доступ к реализации:

// String.repeat(numberOfRepetitions)
'meow'.repeat(3); // 'meowmeowmeow'

Литералы шаблонов в языке JavaScript

Используя литералы шаблонов, мы можем задавать строки со специальными символами, без необходимости их явного сокрытия.

var text = "This string contains \"double quotes\" which are escaped.";
let text = `This string contains "double quotes" which don't need to be escaped anymore.`;

Литералы шаблонов также поддерживают интерполяцию, что упрощает конкатенацию строк и значений:

var name = 'Tiger';
var age = 13;

console.log('My cat is named ' + name + ' and is ' + age + ' years old.');

Еще проще:

const name = 'Tiger';
const age = 13;

console.log(`My cat is named ${name} and is ${age} years old.`);

Мы можем задавать новые строки следующим образом:

var text = (
    'cat\n' +
    'dog\n' +
    'nickelodeon'
);

Или:

var text = [
    'cat',
    'dog',
    'nickelodeon'
].join('\n');

Литералы шаблонов сохранят новые строки, не указывая их явно:

let text = ( `cat
dog
nickelodeon`
);

Литералы шаблонов также могут принимать выражения:

let today = new Date();
let text = `The time and date is ${today.toLocaleString()}`;

Деструктуризация

Деструктуризация позволяет нам извлекать значения из массивов или объектов и хранить их в переменных.

Деструктурированные массивы

var arr = [1, 2, 3, 4];
var a = arr[0];
var b = arr[1];
var c = arr[2];
var d = arr[3];
let [a, b, c, d] = [1, 2, 3, 4];

console.log(a); // 1
console.log(b); // 2

Деструктурированные объекты

var luke = { occupation: 'jedi', father: 'anakin' };
var occupation = luke.occupation; // 'jedi'
var father = luke.father; // 'anakin'
let luke = { occupation: 'jedi', father: 'anakin' };
let {occupation, father} = luke;

console.log(occupation); // 'jedi'
console.log(father); // 'anakin'

Модули в языке JavaScript

До ES6 мы использовали библиотеки, такие как Browserify, для создания модулей на стороне клиента. С ES6 мы можем напрямую использовать модули всех типов (AMD и CommonJS).

Экспорт в CommonJS

module.exports = 1;
module.exports = { foo: 'bar' };
module.exports = ['foo', 'bar'];
module.exports = function bar () {};

Экспорт в ES6

С ES6 у нас появились различные варианты экспорта. Мы можем выполнять Named Exports:

export let name = 'David';
export let age  = 25;​​

Экспорт списка объектов:

function sumTwo(a, b) {
    return a + b;
}

function sumThree(a, b, c) {
    return a + b + c;
}

export { sumTwo, sumThree };

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

export function sumTwo(a, b) {
    return a + b;
}

export function sumThree(a, b, c) {
    return a + b + c;
}

И, наконец, мы можем экспортировать биндинг по умолчанию:

function sumTwo(a, b) {
    return a + b;
}

function sumThree(a, b, c) {
    return a + b + c;
}

let api = {
    sumTwo,
    sumThree
};

export default api;

/* То же самое, что и экспорт { api as default };
 */

Рекомендация: всегда используйте экспорт в конце модуля. Придерживаясь этой парадигмы, мы делаем наш код легко читаемым и позволяем интерполировать между модулями CommonJS и ES6.

Импорт

ES6 предоставляет различные варианты импорта. Мы можем импортировать весь файл:

import 'underscore';

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

import { sumTwo, sumThree } from 'math/addition';

Можем переименовать импорт:

import {
    sumTwo as addTwoNumbers,
    sumThree as sumThreeNumbers
} from 'math/addition';

Кроме того, мы можем импортировать все вещи (также называемые импортом пространства имен):

import * as util from 'math/addition';

Наконец, также можно импортировать список значений из модуля:

import * as additionUtil from 'math/addition';
const { sumTwo, sumThree } = additionUtil;

Импортирование из биндинга по умолчанию:

import api from 'math/addition';
// Вместо: import { default as api } from 'math/addition';

Мы можем иногда смешивать импорт по умолчанию и смешанный импорт, если это необходимо:

// foos.js
export { foo as default, foo1, foo2 };

Или импортировать их следующим образом:

import foo, { foo1, foo2 } from 'foos';

При импорте модуля, экспортированного с использованием синтаксиса commonjs (например, React), можно:

import React from 'react';
const { Component, PropTypes } = React;

Это также можно упростить, используя:

import React, { Component, PropTypes } from 'react'

Примечание. Значения, которые экспортируются - это привязки, а не ссылки. Поэтому изменение привязки переменной в одном модуле повлияет на значение внутри экспортируемого модуля. Избегайте изменения общедоступного интерфейса этих экспортированных значений.

Параметры

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

Параметры по умолчанию

function addTwoNumbers(x, y) {
    x = x || 0;
    y = y || 0;
    return x + y;
}

В ES6 можно передать значения по умолчанию для параметров в функции:

function addTwoNumbers(x=0, y=0) {
    return x + y;
}
addTwoNumbers(2, 4); // 6
addTwoNumbers(2); // 2
addTwoNumbers(); // 0

Параметры остановки

В ES5 мы обрабатывали неопределенное количество аргументов:

function logArguments() {
    for (var i=0; i < arguments.length; i++) {
        console.log(arguments[i]);
    }
}

Используя оператор rest, мы можем передавать неопределенное количество аргументов:

function logArguments(...args) {
    for (let arg of args) {
        console.log(arg);
    }
}

Именованные параметры

Одним из шаблонов в ES5 для обработки именованных параметров был шаблон объектов опций, принятый из jQuery.

function initializeCanvas(options) {
    var height = options.height || 600;
    var width  = options.width  || 400;
    var lineStroke = options.lineStroke || 'black';
}

Можно достичь той же функциональности, используя деструктурирование как формальный параметр для функции:

function initializeCanvas(
    { height=600, width=400, lineStroke='black'}) {
        // Использование height, width, lineStroke здесь
    }

Оператор распространения

В ES5 мы могли найти максимальные значения в массиве, используя метод apply на Math.max следующим образом:

Math.max.apply(null, [-1, 100, 9001, -32]); // 9001

В ES6 мы используется оператор распространения для передачи массива значений, которые выступают параметрами для функции:

Math.max(...[-1, 100, 9001, -32]); // 9001

Мы можем легко конкатенировать литералы массивов:

let cities = ['San Francisco', 'Los Angeles'];
let places = ['Miami', ...cities, 'Chicago']; // ['Miami', 'San Francisco', 'Los Angeles', 'Chicago']

Классы в языке JavaScript

До ES6 мы реализовали классы, создавая функцию-конструктор и добавив свойства:

function Person(name, age, gender) {
    this.name   = name;
    this.age    = age;
    this.gender = gender;
}

Person.prototype.incrementAge = function () {
    return this.age += 1;
};

И создавали расширенные классы следующим образом:

function Personal(name, age, gender, occupation, hobby) {
    Person.call(this, name, age, gender);
    this.occupation = occupation;
    this.hobby = hobby;
}

Personal.prototype = Object.create(Person.prototype);
Personal.prototype.constructor = Personal;
Personal.prototype.incrementAge = function () {
    Person.prototype.incrementAge.call(this);
    this.age += 20;
    console.log(this.age);
};

С помощью ES6 мы можем создавать классы таким образом:

class Person {
    constructor(name, age, gender) {
        this.name   = name;
        this.age    = age;
        this.gender = gender;
    }

    incrementAge() {
      this.age += 1;
    }
}

И расширять их, используя ключевое слово extends:

class Personal extends Person {
    constructor(name, age, gender, occupation, hobby) {
        super(name, age, gender);
        this.occupation = occupation;
        this.hobby = hobby;
    }

    incrementAge() {
        super.incrementAge();
        this.age += 20;
        console.log(this.age);
    }
}

Лучшая практика. Хотя ES6 скрывает, как работает реализация и прототипы, это хорошая функция для новичков. Также она позволяет нам писать более чистый код.

Символы в языке JavaScript

Символы существовали до ES6, но теперь у нас есть открытый интерфейс для их непосредственного использования. Символы неизменны, уникальны и могут использоваться как ключи в любом хеше.

Symbol( )

Вызов Symbol () или Symbol (описание) создаст уникальный символ, который не может быть просмотрен глобально.

const refreshComponent = Symbol();

React.Component.prototype[refreshComponent] = () => {
    // делать что-нибудь
}

Symbol.for(key)

Symbol.for(key) создает символ, который по-прежнему неизменен и уникален, но может быть просмотрен глобально. Два идентичных вызова Symbol.for(key) возвращают один и тот же символ.

Symbol('foo') === Symbol('foo') // false
Symbol.for('foo') === Symbol('foo') // false
Symbol.for('foo') === Symbol.for('foo') // true

Примечательным примером использования Symbol для взаимодействия является Symbol.iterator, который существует во всех повторяющихся типах в ES6: массивы, строки, генераторы и т. д.

Maps в языке JavaScript

Maps - необходимая структура данных в языке JavaScript. До ES6 мы создавали хэш-карты через объекты:

var map = new Object();
map[key1] = 'value1';
map[key2] = 'value2';

Однако это не защищает нас от случайных переопределений функций с конкретными именами свойств:

> getOwnProperty({ hasOwnProperty: 'Hah, overwritten'}, 'Pwned');
> TypeError: Property 'hasOwnProperty' is not a function

Maps позволяют нам устанавливать, получать и искать значения (и многое другое).

let map = new Map();
> map.set('name', 'david');
> map.get('name'); // david
> map.has('name'); // true

Самая удивительная часть Maps - это то, что мы больше не ограничиваемся использованием строк. Теперь мы можем использовать любой тип в качестве ключа, и он не будет привязан к типу для строки.

let map = new Map([
    ['name', 'david'],
    [true, 'false'],
    [1, 'one'],
    [{}, 'object'],
    [function () {}, 'function']
]);

for (let key of map.keys()) {
    console.log(typeof key);
    // > string, boolean, number, object, function
}

Примечание. Использование функций или объектов не будет работать при проверке равенства с использованием таких методов, как map.get(). Таким образом, придерживайтесь примитивных значений, таких как строки, булевы и числа.

Мы также можем перебирать Maps с использованием .entries():

for (let [key, value] of map.entries()) {
    console.log(key, value);
}

WeakMaps

Чтобы хранить частные версии данных у нас были разные способы сделать это:

class Person {
    constructor(age) {
        this._age = age;
    }

    _incrementAge() {
        this._age += 1;
    }
}

Но данный метод может вызвать путаницу и не всегда будет поддерживаться. Вместо этого мы можем использовать WeakMaps для хранения наших значений:

let _age = new WeakMap();
class Person {
    constructor(age) {
        _age.set(this, age);
    }

    incrementAge() {
        let age = _age.get(this) + 1;
        _age.set(this, age);
        if (age > 50) {
            console.log('Midlife crisis');
        }
    }
}

Самое интересное в использовании WeakMaps для хранения наших личных данных заключается в том, что их ключи не выдают имена свойств, которые можно увидеть с помощью Reflect.ownKeys():

> const person = new Person(50);
> person.incrementAge(); // 'Midlife crisis'
> Reflect.ownKeys(person); // []

Практичным примером использования WeakMaps является хранение данных, связанных с элементом DOM:

let map = new WeakMap();
let el  = document.getElementById('someElement');

// Храните ссылку на элемент с помощью ключа
map.set(el, 'reference');

// Доступ к значению элемента
let value = map.get(el); // 'reference'

// Удаление ссылки
el.parentNode.removeChild(el);
el = null;

Как показано выше, когда объект уничтожается сборщиком мусора, WeakMap автоматически удаляет пару ключ-значение, которая была идентифицирована этим объектом.

Для дополнительной иллюстрации полезности этого примера рассмотрим, как jQuery хранит кеш объектов, соответствующих элементам DOM, которые имеют ссылки. Используя WeakMaps, jQuery может автоматически освобождать любую память, связанную с определенным элементом DOM, после того, как он был удален. В общем, WeakMaps очень полезен для любой библиотеки.

Промисы (обещания) в языке JavaScript

Промисы позволяют нам превратить наш горизонтальный код:

func1(function (value1) {
    func2(value1, function (value2) {
        func3(value2, function (value3) {
            func4(value3, function (value4) {
                func5(value4, function (value5) {
                    // Do something with value 5
                });
            });
        });
    });
});

В вертикальный:

func1(value1)
    .then(func2)
    .then(func3)
    .then(func4)
    .then(func5, value5 => {
        // Do something with value 5
    });

До ES6 мы использовали bluebird или Q. Теперь у нас есть промисы:

new Promise((resolve, reject) =>
    reject(new Error('Failed to fulfill Promise')))
        .catch(reason => console.log(reason));

Вот практический пример использования промисов:

var request = require('request');

return new Promise((resolve, reject) => {
  request.get(url, (error, response, body) => {
    if (body) {
        resolve(JSON.parse(body));
      } else {
        resolve({});
      }
  });
});

Мы также можем распараллелить промисы для обработки массива асинхронных операций с помощью Promise.all ():

let urls = [
  '/api/commits',
  '/api/issues/opened',
  '/api/issues/assigned',
  '/api/issues/completed',
  '/api/issues/comments',
  '/api/pullrequests'
];

let promises = urls.map((url) => {
  return new Promise((resolve, reject) => {
    $.ajax({ url: url })
      .done((data) => {
        resolve(data);
      });
  });
});

Promise.all(promises)
  .then((results) => {
    // Сделайте что-нибудь с результатами наших промисов 
});

Генераторы в языке JavaScript

Подобно тому, как промисы позволяют нам избегать обратного вызова, генераторы позволяют придать нашему асинхронному коду синхронность. Генераторы – новый вид функций в современном языке JavaScript. Они отличаются от обычных тем, что могут приостанавливать своё выполнение, возвращать промежуточный результат и далее возобновлять его позже, в произвольный момент времени.

Ниже приведен простой пример использования генераторов:

function* sillyGenerator() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
}

var generator = sillyGenerator();
> console.log(generator.next()); // { value: 1, done: false }
> console.log(generator.next()); // { value: 2, done: false }
> console.log(generator.next()); // { value: 3, done: false }
> console.log(generator.next()); // { value: 4, done: false }

Мы можем использовать генераторы для записи асинхронного кода синхронно:

function request(url) {
    getJSON(url, function(response) {
        generator.next(response);
    });
}

Напишем функцию генератора, которая будет возвращать наши данные:

function* getData() {
    var entry1 = yield request('http://some_api/item1');
    var data1  = JSON.parse(entry1);
    var entry2 = yield request('http://some_api/item2');
    var data2  = JSON.parse(entry2);
}

Мы гарантируем, что entry1 будет иметь данные, которые необходимо проанализировать и сохранить в data1.

Хотя генераторы позволяют нам синхронно писать асинхронный код, нет четкого и простого пути для определения ошибок. Таким образом, мы можем улучшить наш генератор с помощью промисов:

function request(url) {
    return new Promise((resolve, reject) => {
        getJSON(url, resolve);
    });
}

Мы напишем функцию, которая будет использовать наш генератор, используя следующий, который, в свою очередь, будет использовать наш метод запроса выше, чтобы получить промисы:

function iterateGenerator(gen) {
    var generator = gen();
    (function iterate(val) {
        var ret = generator.next();
        if(!ret.done) {
            ret.value.then(iterate);
        }
    })();
}

Улучшая наш генератор промисами, получаем способ определения ошибок с помощью нашего промиса .catch и reject:

iterateGenerator(function* getData() {
    var entry1 = yield request('http://some_api/item1');
    var data1  = JSON.parse(entry1);
    var entry2 = yield request('http://some_api/item2');
    var data2  = JSON.parse(entry2);
});

Мы смогли повторно использовать нашу реализацию, чтобы применить наш генератор, как и прежде. Хотя генераторы и промисы позволяют нам синхронно писать асинхронный код, мы можем начать использовать более простую конструкцию, которая обеспечивает те же преимущества: async-wait.

Async Await в языке JavaScript

Async Await позволяет выполнять то же самое, что мы выполнили с помощью генераторов и промисов, но с меньшими усилиями:

var request = require('request');

function getJSON(url) {
  return new Promise(function(resolve, reject) {
    request(url, function(error, response, body) {
      resolve(body);
    });
  });
}

async function main() {
  var data = await getJSON();
  console.log(data); // NOT undefined!
}

main();

Геттеры и сеттеры в языке JavaScript

ES6 начал поддерживать функции getter и setter внутри классов. Посмотрим на следующий пример:

class Employee {

    constructor(name) {
        this._name = name;
    }

    get name() {
      if(this._name) {
        return 'Mr. ' + this._name.toUpperCase();  
      } else {
        return undefined;
      }  
    }

    set name(newName) {
      if (newName == this._name) {
        console.log('I already have this name.');
      } else if (newName) {
        this._name = newName;
      } else {
        return false;
      }
    }
}

var emp = new Employee("James Bond");

// используем метод get в фоновом режиме
if (emp.name) {
  console.log(emp.name);  // Mr. JAMES BOND
}

// используем setter в фоновом режиме
emp.name = "Bond 007";
console.log(emp.name);  // Mr. BOND 007

Последние браузеры также поддерживают функции getter / setter в объектах, и мы можем использовать их для вычисляемых свойств, добавляя предварительную обработку перед настройкой / получением:

var person = {
  firstName: 'James',
  lastName: 'Bond',
  get fullName() {
      console.log('Getting FullName');
      return this.firstName + ' ' + this.lastName;
  },
  set fullName (name) {
      console.log('Setting FullName');
      var words = name.toString().split(' ');
      this.firstName = words[0] || '';
      this.lastName = words[1] || '';
  }
}

person.fullName; // James Bond
person.fullName = 'Bond 007';
person.fullName; // Bond 007

Источник

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ