Статьи

ECMAScript.next: функции со стрелками и определения методов


В JavaScript один аспект создания функции внутри метода трудно понять правильно: как обрабатывать специальную переменную this.
ECMAScript.next упростит задачу, введя две конструкции: функции стрелок и определения методов. В этом блоге объясняется, что они собой представляют и как они помогают.

терминология

В этом сообщении мы выделяем два вида вызываемых объектов:

  • Подпрограмма существует сами по себе и вызывается непосредственно. Подпрограммы также часто называют «не-методными функциями» в JavaScript.
  • Метод является частью объекта о и называется через объект (который не является обязательно тот же объект, о).

В JavaScript и подпрограммы, и методы реализуются функциями. Например:

var obj = {
    myMethod: function () {
        setTimeout(function () { ... }, 0);
    }
}

myMethod — это метод, первый аргумент setTimeout () — подпрограмма. Оба реализуются функциями. Прежде чем мы сможем объяснить, как функции работают как методы, нам нужно сделать еще несколько определений.

Всякий раз, когда вызывается функция, она имеет два вида областей (или контекстов): ее лексические области — это синтаксические конструкции, которые ее окружают (признак исходного кода или лексикона ). Его динамические области действия — это функция, которая вызвала его, функция, которая вызвала эту функцию и т. Д. Обратите внимание на вложение, которое происходит в обоих случаях. Свободные переменныепеременные, которые не объявлены внутри функции. Если такие переменные читаются или записываются, JavaScript ищет их в окружающих лексических областях. Функции работают хорошо как реализации методов: у них есть специальная переменная, называемая this, которая ссылается на объект, через который был вызван метод. В отличие от других свободных переменных, это не ищется в окружающих лексических областях, оно передается функции через вызов. Поскольку функция получает это динамически, это называется динамическим this .

Функции не работают хорошо как реализации подпрограмм, потому что это все еще динамично. Вызов подпрограммы устанавливает значение undefined в строгом режиме [1] и глобальный объект в противном случае. Это означает, что он скрывает это от окружающего метода, даже несмотря на то, что функция-подпрограмма его вообще не использует. Например:

var jane = {
    name: "Jane",
    
    logHello: function (friends) {
        var that = this;  // (*)
        friends.forEach(function (friend) {
            console.log(that.name + " says hello to " + friend)
        });
    }
}

Аргументом forEach является подпрограмма. Вам нужно присвоение в (*), чтобы он мог получить доступ к logHello. Понятно, что подпрограммы должны иметь лексическое значение, что означает, что к ним следует относиться так же, как к другим свободным переменным, и искать их в прилагаемых лексических областях. это = это хороший обходной путь. Это имитирует лексическое это, если хотите. Другим обходным решением является использование связывания: 

var jane = {
    name: "Jane",
    
    logHello: function (friends) {
        friends.forEach(function (friend) {
            console.log(this.name + " says hello to " + friend)
        }.bind(this));
    }
}

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

  • Вы должны знать, как это работает в JavaScript (что вам не нужно и не нужно).
  • Вы должны постоянно следить за тем, когда имитировать лексическое это. Этот выбор должен быть автоматическим и не требовать дополнительных размышлений. Обратите внимание, что даже с довольно элегантными функциями стрелки CoffeeScript эта проблема не исчезает полностью.

Функции стрелок

Функции стрелок ECMAScript.next лучше подходят для определения подпрограмм, чем обычные функции, потому что они имеют лексическое значение this. Использование одного для forEach выглядит следующим образом.

let jane = {
    name: "Jane",
    
    logHello: function (friends) {
        friends.forEach(friend => {
            console.log(this.name + " says hello to " + friend)
        });
    }
}

«Толстая» стрелка => (в отличие от тонкой стрелки ->) была выбрана, чтобы быть совместимой с CoffeeScript, функции жирной стрелки которого очень похожи.

Указание аргументов:

        () => { ... } // no argument
         x => { ... } // one argument
    (x, y) => { ... } // several arguments

Указание тела:

    x => { return x * x }  // block
    x => x * x  // expression, equivalent to previous line

Блок операторов ведет себя как обычное тело функции. Например, вам нужно вернуть, чтобы вернуть значение. В теле выражения выражение всегда возвращается неявно. Обратите внимание, насколько функция стрелки с телом выражения может уменьшить многословность: приведенный выше пример избавляет вас от необходимости печатать

    function (x) { return x * x }

Реализуя лексическое это

Для лексических функций стрелок это реализовано следующим образом. Функция стрелки

    x => x + this.y

в основном синтаксический сахар для

    function (x) { return x + this.y }.bind(this)

Это выражение создает две функции: во-первых, исходную анонимную функцию с параметром x и динамическую this. Во-вторых,
связанная функция, которая является результатом привязки. Хотя функция со стрелкой ведет себя так, как если бы она была создана с помощью bind, она потребляет меньше памяти: создается только одна сущность, специализированная функция, которая напрямую связана с этой из окружающей функции.

Стрелка функции против нормальных функций

Функция стрелки отличается от нормальной функции только двумя способами: во-первых, она всегда имеет ограничение this. Во-вторых, его нельзя использовать в качестве конструктора: нет внутреннего метода [[Construct]] (который позволяет вызывать нормальную функцию через new) и прототипа свойства. Поэтому new (() => {}) выдает ошибку.

Помимо этих ограничений, нет заметной разницы между функцией стрелки и нормальной функцией. Например, typeof и instanceof можно использовать как и раньше:

> typeof () => {}
'function'

> () => {} instanceof Function
true

Обсуждаемые синтаксические варианты

Следующие синтаксические варианты все еще обсуждаются и могут не быть добавлены в ECMAScript.next.

  • Опуская параметры:
        => { ... }
    

    С помощью автоматической вставки точек с запятой в JavaScript [2] существует риск того, что такое выражение будет ошибочно рассмотрено как продолжение предыдущей строки. Взять, к примеру, следующий код.

        var x = 3 + a
        => 5
    

    Эти две строки интерпретируются как

        var x = 3 + (a => 5);
    

    Однако функции стрелок обычно появляются в контексте выражения, вложенного в оператор. Следовательно, я не ожидал бы, что вставка точки с запятой будет большой проблемой. Если бы в JavaScript были значительные символы новой строки [3] (например, CoffeeScript), тогда проблема полностью исчезла бы.

  • Опуская тело:
        x =>
    

    Это функция с одним параметром, который всегда возвращает неопределенное значение. Это синоним оператора void [4] . Я не уверен, насколько это полезно.

  • Функции именованных стрелок: JavaScript уже имеет выражения именованных функций , где вы присваиваете функции имя, чтобы она могла вызывать себя. Это имя является локальным для этой функции, оно не проникает ни в какие окружающие области. Функции именованных стрелок будут работать одинаково. Например:
let fac = me(n) => {
    if (n <= 0) {
        return 1;
    } else {
        return n * me(n-1);
    }
}
console.log(me); // ReferenceError: me is not defined

Разбор функций стрелок

Большинство парсеров JavaScript имеют прогноз с двумя токенами. Как тогда такой синтаксический анализатор должен различать следующие два выражения?

    (x, y, z)
    (x, y, z) => {}

Первое выражение — оператор запятой, примененный к трем переменным в скобках. Второе выражение является функцией стрелки. Чтобы разобрать оба вышеперечисленных, используется трюк, называемый
покрывающей грамматикой : один создает правило грамматики, которое охватывает оба варианта использования, анализирует, а затем выполняет постобработку. Если стрелка следует за закрывающей скобкой, некоторые ранее проанализированные вещи вызовут ошибку, и проанализированная конструкция будет использоваться в качестве списка формальных параметров функции стрелки. Если стрелка не следует, другие ранее проанализированные вещи вызовут ошибку, и проанализированная конструкция является выражением в скобках. Некоторые вещи можно сделать только в выражении в скобках:

    (foo(123))

Другие вещи могут быть сделаны только в списке параметров. Например, объявив
параметр отдыха :

    (a, b, ...rest)

В обоих случаях можно сделать несколько вещей:

    (title = "no title")

Выше приведено присваивание в контексте выражения и объявление значения параметра по умолчанию в контексте функции стрелки.

Возможная функция стрелки: необязательный динамический

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

$(".someCssClass").each(function (i) { console.log(this) });

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

$(".someCssClass").each((this, i) => { console.log(this) });

Обычно ни один параметр не может иметь имя this, поэтому вышеприведенное является хорошим маркером для функции стрелки с динамическим this.

Два подхода, которые не работают

Разве не должно быть более простого решения для дополнительного динамического этого? Увы, два, казалось бы, более простых подхода не сработают.

Возможный подход # 1: переходите от динамического к лексическому этому, в зависимости от того, как вызывается функция. Если он вызывается как метод, используйте динамический this. Если он вызывается как подпрограмма, используйте лексический this. Проблема этого подхода заключается в том, что вы не можете контролировать, как написанная вами функция будет использоваться клиентами, открывая дверь непреднамеренно раскрываемым секретам и непреднамеренным побочным эффектам. Пример: давайте представим, что есть «функции тонких стрелок» (определенные через ->), которые на лету переключаются между динамическим и лексическим this.

let objectCreator = {
    create: function (secret) {
        return {
            secret: secret,
            getSecret: () -> {
                return this.secret;
            }
        };
    },
    secret: "abc"
}

Вот как обычно можно использовать obj: 

let obj = objectCreator.create("xyz");
// dynamic this:
console.log(obj.getSecret()); // xyz

Вот как злоумышленник может получить доступ к objectCreator.secret: 

let obj = objectCreator.create("xyz");
let func = obj.getSecret;
// lexical this:
console.log(func()); // abc

Возможный подход # 2: позвольте call или apply переопределить связанное значение этого. Это проблематично, потому что вызов или применение могут случайно сломать функцию стрелки, которая полагается на это, будучи лексическим. Следовательно, это слишком хрупкое решение.

Споря в пользу простоты

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