Распространенная ошибка №1: неправильные ссылки на "this'".

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

Рассмотрим этот пример фрагмента кода:

Game.prototype.restart = function () {
  this.clearLocalStorage();
  this.timer = setTimeout(function() {
    this.clearBoard();    // what is "this"?
  }, 0);
};

Выполнение вышеуказанного кода приводит к следующей ошибке:

Uncaught TypeError: undefined is not a function

Почему?

Все дело в контексте. Причина, по которой вы получаете указанную выше ошибку, заключается в том, что, когда вы вызываете setTimeout(), вы фактически вызываете window.setTimeout(). В результате анонимная функция, передаваемая в setTimeout(), определяется в контексте объекта window, у которого нет метода clearBoard().

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

Game.prototype.restart = function () {
  this.clearLocalStorage();
  var self = this;   // save reference to 'this'
  this.timer = setTimeout(function(){
    self.clearBoard();    // oh OK, I do know who 'self' is!
  }, 0);
};

В качестве альтернативы в новых браузерах вы можете использовать метод .bind() для передачи правильной ссылки:

Game.prototype.restart = function () {
  this.clearLocalStorage();
  this.timer = setTimeout(this.reset.bind(this), 0); // bind to 'this'
};
Game.prototype.reset = function(){
    this.clearBoard();    //back in the context of the right 'this'!
};

Распространенная ошибка №2: Думать, что есть область видимости на уровне блоков.

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

for (var i = 0; i < 10; i++) {
  /* ... */
}
console.log(i);  // what will this output?

Если вы предполагаете, что вызов console.log() либо выведет undefined, либо выдаст ошибку, вы угадали неправильно. Вы не поверите, но на выходе будет 10. Почему?

В большинстве других языков приведенный выше код приведет к ошибке, поскольку «срок действия» (т. Е. Область действия) переменной i будет ограничен блоком for.

Однако в JavaScript это не так, и переменная i остается в области видимости даже после завершения цикла for, сохраняя свое последнее значение после выхода из цикла. (это поведение известно, кстати, как подъем переменной).

Распространенная ошибка № 3: создание утечек памяти

Утечки памяти - это почти неизбежные проблемы JavaScript, если вы сознательно не кодируете их, чтобы избежать их. Есть множество способов их возникновения, поэтому мы просто выделим несколько наиболее частых случаев.

Пример утечки памяти 1. Висячие ссылки на несуществующие объекты

Рассмотрим следующий код:

var theThing = null;
var replaceThing = function () {
  var priorThing = theThing;  // hold on to the prior thing
  var unused = function () {
    // 'unused' is the only place where 'priorThing' is referenced,
    // but 'unused' never gets invoked
    if (priorThing) {
      console.log("hi");
    }
  };
  theThing = {
    longStr: new Array(1000000).join('*'),  // create a 1MB object
    someMethod: function () {
      console.log(someMessage);
    }
  };
};
setInterval(replaceThing, 1000);    // invoke `replaceThing' once every second

Если вы запустите приведенный выше код и проследите за использованием памяти, вы обнаружите, что произошла огромная утечка памяти, утечка которой составляет полный мегабайт в секунду! И даже ручная сборка мусора не помогает. Похоже, что утечка longStr происходит каждый раз, когда вызывается replaceThing. Но почему?

Давайте рассмотрим ситуацию более подробно:

Каждый theThing объект содержит свой собственный longStr объект размером 1 МБ. Каждую секунду, когда мы вызываем replaceThing, он сохраняет ссылку на предыдущий объект theThing в priorThing. Но мы по-прежнему не думаем, что это будет проблемой, поскольку каждый раз ранее упоминавшийся priorThing разыменовывался (когда priorThing сбрасывается с помощью priorThing = theThing;). Более того, на него ссылаются только в основном теле replaceThing и в функции unused, которая фактически никогда не используется.

Итак, мы снова задаемся вопросом, почему здесь утечка памяти !?

Чтобы понять, что происходит, нам нужно лучше понять, как все работает в JavaScript изнутри. Типичный способ реализации замыканий состоит в том, что каждый функциональный объект имеет ссылку на объект словарного стиля, представляющий его лексическую область видимости. Если обе функции, определенные внутри replaceThing, на самом деле использовали priorThing, было бы важно, чтобы они обе получали один и тот же объект, даже если priorThing назначается снова и снова, чтобы обе функции использовали одну и ту же лексическую среду. Но как только переменная используется каким-либо замыканием, она попадает в лексическую среду, разделяемую всеми замыканиями в этой области видимости. И этот маленький нюанс и приводит к этой ужасной утечке памяти. (Подробнее об этом можно прочитать здесь.)

Пример утечки памяти 2. Циклические ссылки

Рассмотрим этот фрагмент кода:

function addClickHandler(element) {
    element.click = function onClick(e) {
        alert("Clicked the " + element.nodeName)
    }
}

Здесь onClick имеет закрытие, которое сохраняет ссылку на element (через element.nodeName). Также путем присвоения onClick element.click создается круговая ссылка; то есть: element - ›onClick -› element - ›onClick -› element

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

Как избежать утечек памяти: что вам нужно знать

Управление памятью в JavaScript (и, в частности, сборка мусора) в значительной степени основано на понятии достижимости объекта.

Предполагается, что следующие объекты достижимы и известны как «корни»:

  • Объекты, на которые есть ссылки из любого места в текущем стеке вызовов (то есть все локальные переменные и параметры в функциях, которые в настоящее время вызываются, и все переменные в области закрытия)
  • Все глобальные переменные

Объекты хранятся в памяти, по крайней мере, до тех пор, пока они доступны из любого корня через ссылку или цепочку ссылок.

В браузере есть сборщик мусора (GC), который очищает память, занятую недоступными объектами; т.е. объекты будут удалены из памяти тогда и только тогда, когда GC считает, что они недоступны. К сожалению, довольно легко получить несуществующие «зомби» объекты, которые фактически больше не используются, но которые GC все еще считает «достижимыми».

По теме: Рекомендации и советы по JavaScript от разработчиков Toptal

Распространенная ошибка №4: заблуждение относительно равенства

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

// All of these evaluate to 'true'!
console.log(false == '0');
console.log(null == undefined);
console.log(" \t\r\n" == 0);
console.log('' == 0);
// And these do too!
if ({}) // ...
if ([]) // ...

Что касается последних двух, несмотря на то, что они пусты (что может привести к предположению, что они будут оцениваться как false), оба {} и [] на самом деле являются объектами, и любой объект будет приведен к логическому значению true в JavaScript, в соответствии со спецификацией ECMA-262.

Как показывают эти примеры, правила приведения типов иногда могут быть ясны как грязь. Соответственно, если явно не требуется приведение типов, обычно лучше использовать === и !== (а не == и !=), чтобы избежать любых непреднамеренных побочных эффектов приведения типов. (== и != автоматически выполняют преобразование типов при сравнении двух вещей, тогда как === и !== выполняют то же сравнение без преобразования типов.)

И полностью как побочный момент - но поскольку мы говорим о приведении типов и сравнении - стоит упомянуть, что сравнение NaN с чем угодно (даже NaN!) всегда возвращает false. Следовательно, вы не можете использовать операторы равенства (==, ===, !=, !==), чтобы определить, является ли значение NaN или нет. Вместо этого используйте встроенную глобальную функцию isNaN():

console.log(NaN == NaN);    // false
console.log(NaN === NaN);   // false
console.log(isNaN(NaN));    // true

Распространенная ошибка # 5: неэффективное манипулирование DOM

JavaScript позволяет относительно легко манипулировать DOM (то есть добавлять, изменять и удалять элементы), но ничего не делает для повышения эффективности этого.

Типичный пример - это код, который добавляет серию элементов DOM по одному. Добавление элемента DOM - дорогостоящая операция. Код, который последовательно добавляет несколько элементов DOM, неэффективен и, скорее всего, не будет работать должным образом.

Одна эффективная альтернатива, когда необходимо добавить несколько элементов DOM, - это использовать вместо них фрагменты документа, тем самым повышая как эффективность, так и производительность.

Например:

var div = document.getElementsByTagName("my_div");
var fragment = document.createDocumentFragment();
for (var e = 0; e < elems.length; e++) {  
   // elems previously set to list of elements
    fragment.appendChild(elems[e]);
}
div.appendChild(fragment.cloneNode(true));

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

Распространенная ошибка №6: неправильное использование определений функций внутри for циклов

Рассмотрим этот код:

var elements = document.getElementsByTagName('input');
var n = elements.length;    // assume we have 10 elements for this example
for (var i = 0; i < n; i++) {
    elements[i].onclick = function() {
        console.log("This is element #" + i);
    };
}

Основываясь на приведенном выше коде, если бы было 10 элементов ввода, нажатие любого из них отобразило бы «Это элемент №10»! Это потому, что к моменту вызова onclick для любого из элементов вышеупомянутый цикл for будет завершен, и значение i уже будет равно 10 (для всех из их).

Однако вот как мы можем исправить указанные выше проблемы с кодом для достижения желаемого поведения (с помощью так называемого ЗАКРЫТИЯ):

var elements = document.getElementsByTagName('input');
var n = elements.length;    // assume we have 10 elements for this example
var makeHandler = function(num) {  // outer function
     return function() {   // inner function
         console.log("This is element #" + num);
     };
};
for (var i = 0; i < n; i++) {
    elements[i].onclick = makeHandler(i+1);
}

В этой пересмотренной версии кода makeHandler немедленно выполняется каждый раз, когда мы проходим через цикл, каждый раз получая текущее значение i+1 и привязывая его к переменной num с ограниченной областью видимости. Внешняя функция возвращает внутреннюю функцию (которая также использует эту num переменную с ограниченной областью видимости), а onclick элемента устанавливается на эту внутреннюю функцию. Это гарантирует, что каждый onclick получит и использует правильное значение i (через переменную num с заданной областью).

Распространенная ошибка № 7: неспособность правильно использовать прототипное наследование

Удивительно высокий процент разработчиков JavaScript не в состоянии полностью понять и, следовательно, полностью использовать возможности прототипного наследования.

Вот простой пример. Рассмотрим этот код:

BaseObject = function(name) {
    if(typeof name !== "undefined") {
        this.name = name;
    } else {
        this.name = 'default'
    }
};

Кажется довольно простым. Если вы указываете имя, используйте его, в противном случае установите имя «по умолчанию»; например.:

var firstObj = new BaseObject();
var secondObj = new BaseObject('unique');
console.log(firstObj.name);  // -> Results in 'default'
console.log(secondObj.name); // -> Results in 'unique'

Но что, если бы мы сделали это:

delete secondObj.name;

Тогда мы получили бы:

console.log(secondObj.name); // -> Results in 'undefined'

Но не лучше ли было бы вернуться к «дефолту»? Это легко сделать, если мы изменим исходный код, чтобы использовать прототипное наследование, следующим образом:

BaseObject = function (name) {
    if(typeof name !== "undefined") {
        this.name = name;
    }
};
BaseObject.prototype.name = 'default';

В этой версии BaseObject наследует свойство name от своего объекта prototype, где для него установлено (по умолчанию) значение 'default'. Таким образом, если конструктор вызывается без имени, имя по умолчанию будет default. Аналогичным образом, если свойство name удалено из экземпляра BaseObject, тогда будет произведен поиск в цепочке прототипов, и свойство name будет извлечено из объекта prototype, где его значение по-прежнему равно 'default'. Итак, теперь мы получаем:

var thirdObj = new BaseObject('unique');
console.log(thirdObj.name);  // -> Results in 'unique'
delete thirdObj.name;
console.log(thirdObj.name);  // -> Results in 'default'

Распространенная ошибка № 8: создание неправильных ссылок на методы экземпляра

Давайте определим простой объект и создадим его экземпляр следующим образом:

var MyObject = function() {}
MyObject.prototype.whoAmI = function() {
    console.log(this === window ? "window" : "MyObj");
};
var obj = new MyObject();

Теперь, для удобства, давайте создадим ссылку на метод whoAmI, предположительно, чтобы мы могли получить к нему доступ просто по whoAmI(), а не по более длинному obj.whoAmI():

var whoAmI = obj.whoAmI;

И чтобы убедиться, что все выглядит чисто, давайте распечатаем значение нашей новой переменной whoAmI:

console.log(whoAmI);

Выходы:

function () {
    console.log(this === window ? "window" : "MyObj");
}

Окей круто. Выглядит хорошо.

Но теперь посмотрите на разницу, когда мы вызываем obj.whoAmI() и нашу справочную информацию whoAmI():

obj.whoAmI();  // outputs "MyObj" (as expected)
whoAmI();      // outputs "window" (uh-oh!)

Что пошло не так?

Подделка здесь заключается в том, что, когда мы выполняли присвоение var whoAmI = obj.whoAmI;, новая переменная whoAmI определялась в пространстве имен global. В результате его значение this равно window, не obj экземпляр MyObject!

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

var MyObject = function() {}
MyObject.prototype.whoAmI = function() {
    console.log(this === window ? "window" : "MyObj");
};
var obj = new MyObject();
obj.w = obj.whoAmI;   // still in the obj namespace
obj.whoAmI();  // outputs "MyObj" (as expected)
obj.w();       // outputs "MyObj" (as expected)

Распространенная ошибка № 9: предоставление строки в качестве первого аргумента для setTimeout или setInterval

Для начала давайте кое-что проясним: предоставление строки в качестве первого аргумента для setTimeout или setInterval не само по себе является ошибкой. Это совершенно законный код JavaScript. Проблема здесь больше в производительности и эффективности. То, что редко объясняется, заключается в том, что если вы передадите строку в качестве первого аргумента в setTimeout или setInterval, она будет передана в конструктор функции для преобразования в новую функцию. Этот процесс может быть медленным и неэффективным, и в нем редко бывает необходимость.

Альтернативой передаче строки в качестве первого аргумента этим методам является передача функции. Давайте посмотрим на пример.

Таким образом, здесь будет довольно типичное использование setInterval и setTimeout с передачей строки в качестве первого параметра:

setInterval("logTime()", 1000);
setTimeout("logMessage('" + msgValue + "')", 1000);

Лучшим выбором было бы передать функцию в качестве начального аргумента; например.:

setInterval(logTime, 1000); // passing the logTime function to setInterval

setTimeout(function() {       // passing an anonymous function to setTimeout
    logMessage(msgValue);     // (msgValue is still accessible in this scope)
  }, 1000);

Распространенная ошибка № 10: отказ от использования «строгого режима».

Как объясняется в нашем Руководстве по найму JavaScript, строгий режим (т.е. включение 'use strict'; в начале ваших исходных файлов JavaScript) - это способ добровольно принудительно применить более строгий синтаксический анализ и обработку ошибок в вашем коде JavaScript во время выполнения, а также сделать это более безопасно.

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

Вот некоторые ключевые преимущества строгого режима:

  • Облегчает отладку. Ошибки кода, которые в противном случае были бы проигнорированы или не были бы незаметными, теперь будут генерировать ошибки или вызывать исключения, предупреждая вас раньше о проблемах в вашем коде и быстрее направляя вас к их источнику.
  • Предотвращает случайные глобальные переменные. Без строгого режима присвоение значения необъявленной переменной автоматически создает глобальную переменную с этим именем. Это одна из самых распространенных ошибок в JavaScript. В строгом режиме попытка сделать это приводит к ошибке.
  • Устраняет this принуждение. Без строгого режима ссылка на this значение null или undefined автоматически приводится к глобальному. Это может вызвать множество ошибок, связанных с выдергиванием волос. В строгом режиме ссылка на this значение null или undefined вызывает ошибку.
  • Запрещает повторяющиеся имена свойств или значений параметров. Строгий режим вызывает ошибку, когда обнаруживает повторяющееся именованное свойство в объекте (например, var object = {foo: "bar", foo: "baz"};) или повторяющийся именованный аргумент для функции (например, function foo(val1, val2, val1){}), тем самым выявление того, что почти наверняка является ошибкой в ​​вашем коде, на отслеживание которой в противном случае вы могли бы потратить много времени.
  • Делает eval () более безопасным. Есть некоторые различия в том, как eval() ведет себя в строгом и нестрогом режимах. Наиболее важно то, что в строгом режиме переменные и функции, объявленные внутри оператора eval(), не создаются в содержащей области (они создаются в содержащей области в нестрогом режиме , что также может быть частым источником проблем).
  • Выдает ошибку при недопустимом использовании delete . Оператор delete (используемый для удаления свойств из объектов) не может использоваться для неконфигурируемых свойств объекта. Нестрогий код автоматически завершится ошибкой при попытке удалить ненастраиваемое свойство, тогда как в строгом режиме в этом случае будет выдана ошибка.

Заворачивать

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

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