Js Drip #7 Опасность при использовании "this" в конструкторах

Опубликовано Вт. 03 Февраль 2015 в Translations

Вольный перевод Dealing with the Dangers of this in Constructors

В прошлой статье мы говорили, что опасно вызова конструктор без new. А теперь расскажем почему.

function Color(r, g, b) {
    this.r = r;
    this.g = g;
    this.b = b;
}

// Безопасный вызов
var red = new Color(255, 0, 0);

// Опасный вызов
var blue = Color(0, 0, 255);

Когда конструктор вызывается с ключевым словом new, значение this внутри конструктора указывает на новый объект, который создается. Но когда конструктор вызывается как обычная функция значение this конструктора соответствует значению this любой другой функции. И хорошо, если это будет глобальный объект (window в браузере).

Вот иллюстрация проблемы:

// Глобальная переменная
r = "Rodent Of Unusual Size";

function Color(r, g, b) {
    this.r = r;
    this.g = g;
    this.b = b;
}

// Опасный вызов
// `this` указывает на глобальный объект
var blue = Color(0, 0, 255);

// выведет: 0
console.log(r);

// Выведет: undefined
console.log(blue);

В примере вышел есть глобальная переменная r. Или, если посмотреть под другим углом, то глобальный объект содержит свойство r. Когда конструктор Color вызывается без new, this конструктора указывает на глобальный объект (в большинстве случаев). Таким образом функция-конструктор просто переписывает глобальную переменную r, когда задает свойство объекта blue.

Кроме того, т.к. конструктор Color был вызван как обычная функция, то он не вернул автоматически новый объект, поэтому значение blue равно undefined.

Думаю, ты представляешь, как может быть "весело" отлаживать такой баг. Как можно избежать этой проблемы? На самом деле решение крайне простое:

// Глобальная перменная
r = "Rodent Of Unusual Size";

function Color(r, g, b) {
    // Проверяем, что `this` это
    // объект `Color`.
    if (this instanceof Color) {
        this.r = r;
        this.g = g;
        this.b = b;
    } else {
        // Если это не так, то принудительно вызываем
        // конструктор правильно.
        return new Color(r, g, b);
    }
}

// Опасный вызов
// Сейчас `this` указывает на глобальный объект.
var blue = Color(0, 0, 255);

// Выведет: "Rodent Of Unusual Size"
console.log(r);

// Выведет: Color {r: 0, g: 0, b: 255}
console.log(blue);

В обновленном конструкторе Color, мы сначала проверяем, что this указывает на образец Color. Это корректно, потому что ключевое слово new всегда создает новый объект-образец конструктор прежде, чем функция-конструктор начнет работать.

Если this указывает не на образец Color, то конструктор быз вызван не правильно, а значит пропускаем логику конструктора и вернем в качестве результат корректно вызванный конструктор.

Вот такой способ избежать опасных вызовов функции-конструктора и защититься от перезаписи свойств глобальный объектов.

Конечно, используя такой способ разработчики могут привыкнуть к вызову конструктора без new, что плохо. Если ты хочешь просто заставить их использовать new, то можно вместо корректно вызова конструктора вызывать ошибку:

function Color(r, g, b) {
    // Проверяем, что `this`указывает
    // на образец `Color`.
    if (this instanceof Color) {
        this.r = r;
        this.g = g;
        this.b = b;
    } else {
        // Если это не так, то вызываем ошибку.
        throw new Error("`Color` invoked without `new`");
    }
}

Теперь ты знаешь как обезопасить свои конструкторы от вызова без ключевого слова new.

Яндекс.Метрика