Pull to refresh

Семь удивительных «возможностей» Javascript

Reading time 5 min
Views 66K
Original author: Luke Page
За последние несколько месяцев я сделал несколько доработок для 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 есть отличная статья про функции со стрелками, где говорится о том, что комментарии можно помечать символом <--. Неплохо почитать и остальные посты в блоге.
Tags:
Hubs:
+52
Comments 71
Comments Comments 71

Articles