[Перевод] Семь удивительных «возможностей» Javascript
За последние несколько месяцев я сделал несколько доработок для JSHint
, в основном с целью изучить ES6 (я особенно горжусь тем, как переделано обнаружение областей видимости для переменных). Во время этого процесса я наткнулся на несколько вещей, которые меня удивили — в основном, в ES6, однако есть и кое-что про ES3, что я до этого никогда не использовал.
Break из любого блока
Наверняка вы знаете, что в любом цикле можно использовать ключевые слова break
и continue
— это стандартная возможность в современных языках программирования. Однако не все знают, что циклам можно давать метки и с их помощью прерывать любой конкретный цикл:
outer: for(var i = 0; i < 4; i++) {
while(true) {
continue outer;
}
}
То же самое применимо и к break
. Вы наверняка видели, как он используется в выражении switch
:
switch(i) {
case 1:
break;
}
Вообще говоря, именно поэтому Крокфорд не советует добавлять отступы перед case
— выражение break
выкидывает из блока switch
, а не case
, но мне вариант с отступами кажется более читабельным. Выражения switch
также можно помечать меткой:
myswitch: switch(i) {
case 1:
break myswitch;
}
Также можно объявлять блоки просто так. Знаю, что это также доступно в C#, и наверняка в других языках тоже:
{
{
console.log("Я внутри произвольного блока");
}
}
Если сложить все это вместе, можно выйти из любого блока с помощью метки:
outer: {
inner: {
if (true) {
break outer;
}
}
console.log("Эта строчка никогда не выполнится");
}
Разумеется, это относится только к break
— оператор continue
допустим только внутри цикла. Я ни разу не видел метки в коде на Javascript — скорее всего, потому, что если вдруг понадобится экстренно выйти из более чем одного блока, это повод переписать код на функцию с return
.
Однако, если бы мне вдруг захотелось написать функцию с единственной точкой выхода (что, вообще-то говоря, не в моем вкусе) — можно было бы использовать этот подход. Вот, например, функция с несколькими точками выхода:
function(a, b, c) {
if (a) {
if (b) {
return true;
}
doSomething();
if (c) {
return c;
}
}
return b;
}
Добавляем метки, и получается вот что:
function(a, b, c) {
var returnValue = b;
myBlock: if (a) {
if (b) {
returnValue = true;
break myBlock;
}
doSomething();
if (c) {
returnValue = c;
}
}
return returnValue;
}
Или же, можно было бы использовать больше блоков:
function(a, b, c) {
var returnValue = b;
if (a) {
if (b) {
returnValue = true;
} else {
doSomething();
if (c) {
returnValue = c;
}
}
}
return returnValue;
}
Вообще, вариант с метками мне нравится меньше всех, но может только потому, что я к нему не привык?
Деструктуризация существующей переменной
Сперва — фишка, которую я не могу могу объяснить. В ES3, судя по всему, можно добавить скобки вокруг переменной при присваивании и это будет работать:
var a;
(a) = 1;
assertTrue(a === 1);
Если вы знаете, зачем кому-то может понадобиться так делать, то напишите, пожалуйста, в комментариях.
Деструктуризация — это процесс получения значения переменной из объекта или массива. Чаще всего можно видеть подобный пример:
function pullOutInParams({a}, [b]) {
console.log(a, b);
}
function pullOutInLet(obj, arr) {
let {a} = obj;
let [b] = arr;
console.log(a, b);
}
pullOutInParams({a: "Hello" }, ["World"]);
pullOutInLet({a: "Hello" }, ["World"]);
Но можно сделать то же самое и без let
, var
и const
. Для массива достаточно написать вот так:
var a;
[a] = array;
А вот с объектом не получится — его необходимо обернуть в круглые скобки:
var a;
({a} = array);
Причина в том, что это дает слишком большой простор для двусмысленного толкования и ошибок, связанных с анонимными блоками кода, потому как автоматическая расстановка точек с запятой превращает идентификаторы в вычисляемые выражения, а они могут иметь побочные эффекты:
var a = {
get b() {
console.log("Превед!");
}
};
with(a) {
{
b
}
}
Возвращаясь к изначальному примеру, где мы заключили присваивание в круглые скобки — вопреки предположениям, это не имеет никакого отношения к деструктуризации:
var a, b, c;
(a) = 1;
[b] = [2];
({c} = { c : 3 });
Деструктуризация с числами
Еще один аспект деструктуризации, о которой не все могут подозревать — это то, что названия свойств не обязательно должны быть незакавыченными строками. Это могут быть числа:
var {1 : a} = { 1: true };
Или строки в кавычках:
var {"1" : a} = { "1": true };
А еще можно вычислять имя свойства из выражения:
var myProp = "1";
var {[myProp] : a} = { [myProp]: true };
Это позволяет с легкостью написать очень запутанный код:
var a = "a";
var {[a] : [a]} = { a: [a] };
Объявления класса привязаны к блоку
Объявления функции поднимаются в самый верх блока, что позволяет использовать их до объявления:
func();
function func() {
console.log("Все в порядке");
}
А вот если функция объявляется в ходе присваивания переменной, то поднимается только объявление переменной, но не присваивание ей значения:
func(); // func объявлена, но не имеет значения, поэтому ошибка "func не является функцией"
var func = function func() {
console.log("Всё в порядке");
};
Классы — одна из наиболее популярных частей спецификации ES6, и всегда считались своего рода синтаксическим сахаром для функций. Однако если вы думаете, что этот код заработает, то вы ошибаетесь:
new func();
class func {
constructor() {
console.log("Все в порядке");
}
}
Несмотря на сходство с первым примером, оно не работает. На самом деле это эквивалент следующего кода:
new func();
let func = function func() {
console.log("Fine");
}
Тут мы пытаемся обратиться к func
внутри временной мертвой зоны, что является синтаксической ошибкой.
Параметры-тёзки
Я предполагал, что у функции не может быть двух параметров с одним и тем же именем —, а на самом деле может!
function func(a, a) {
console.log(a);
}
func("Привет", "Мир");
// выводит "Мир"
Однако в strict mode всё не так:
function func(a, a) {
"use strict";
console.log(a);
}
func("Привет", "Мир");
// в Chrome будет ошибка - SyntaxError: Strict mode function may not have duplicate parameter names
Оператор typeof
небезопасен
Ладно, ладно, я украл это наблюдение, но повторить все равно будет не лишним.
До ES6 было широко известно, что с помощью оператора typeof
можно безопасно узнать, объявлен ли идентификатор, даже если ему не присвоено значение:
if (typeof Symbol !== "undefined") {
// Symbol доступен
}
// Этот код выкинет исключение, если Symbol не объявлен
if (Symbol !== "undefined") {
}
Но теперь это работает только в том случае, если вы не объявили переменную с помощью let
или const
. Всему виной ВМЗ, из-за которой обращение к переменной до ее присваивания является синтаксической ошибкой, даже несмотря на то, что «под капотом» объявление переменной все равно поднимается в самый верх блока.
if (typeof Symbol !== "undefined") {
// Symbol доступен
}
let Symbol = true; // вызывает синтаксическую ошибку в условии выше!
Создание массива
Я всегда избегал создания массива с помощью ключевого слова new
. В основном потому, что аргументы могут быть либо длиной массива, либо его элементами:
new Array(1); // [undefined]
new Array(1, 2); // [1, 2]
Однако коллега недавно наткнулся на кое-что, что мне раньше не встречалось:
var arr = new Array(10);
for(var i = 0; i < arr.length; i++) {
arr[i] = i;
}
console.dir(arr);
Этот код выдает массив с числами от 0 до 9. А что будет, если отрефакторить его с использованием map
?
var arr = new Array(10);
arr = arr.map(function(item, index) { return index; });
console.dir(arr);
Массив остался неизменным. Судя по всему, конструктор, принимающий длину, создает массив и задает свойство length
, но не создает никаких элементов. Поэтому обратиться к свойству можно, а перечислить элементы нельзя. А если задать значение какому-нибудь элементу?
var arr = new Array(10);
arr[8] = undefined;
arr = arr.map(function(item, index) { return index; });
console.dir(arr);
Получаем массив, где восьмому элементу присвоено число 8, но все остальные значения не заданы. Если посмотреть на код полифилла для функции map
, она проверяет заданность свойства с помощью оператора in
. Такого же поведения можно достичь с помощью литералов массива:
var arr = [];
arr[9] = undefined;
// или же
var arr = [];
arr.length = 10;
Другие жемчужины
В блоге разработчиков Mozilla есть отличная статья про функции со стрелками, где говорится о том, что комментарии можно помечать символом <--
. Неплохо почитать и остальные посты в блоге.