Pull to refresh

JavaScript F.A.Q: Часть 2

Reading time 14 min
Views 76K
image

Около 2-х месяцев назад я и TheShock собирали вопросы по JavaScript в теме FAQ по JavaScript: задавайте вопросы. Первая часть, те вопросы, которые достались мне, появилась буквально через несколько дней JavaScript F.A.Q: Часть 1, а вот вторая часть все не выходит и не выходит. TheShock сейчас переезжает в другую страну и поэтому ему не до ответов. Он попросил меня ответить на его часть. Итак вторая часть ответов — те вопросы, которые достались тоже мне.

В этой части:
  1. Почему eval = evil?
  2. Чем заменить eval?
  3. Как проиграть звук используя JS без flash? Какие форматы поддерживаются?
  4. На сколько использование глобальных переменных замедляет скрипты?
  5. Как запретить выделение текста на JS?
  6. (function() {})(); Для чего используется такая конструкция?
  7. Как сбросить события и восстановить состояние элемента?
  8. Как корректно передавать js-команды с параметрами в ajax приложениях?
  9. Для чего в jQuery передавать window если он и так глобальный и зачем undefined?
  10. Как узнать в какую сторону юзер крутит колесо мыши?
  11. Есть ли событие focus у элемента, если нет, то возможно ли его реализовать?
  12. Как сделать неизменяемый объект (freez)?
  13. Возможно ли сделать кастомную реализацию DomElement так чтобы при разборе для подмножества тегов использовалась она?
  14. Какой клиентский MVC-фреймворк посоветуете?
  15. Обход ограничений по Referer
  16. GOTO в Javascript
  17. Наследование в JavaScript. Как идеологически правильно заэкстэндить Man и Woman от Human
  18. Возможно ли устроиться javascript программистом без копания в верстке и server-side?
  19. Какие есть практики передачи данных с сервера в код, который исполняется из .js?
  20. Можно ли подцепить(эмулировать) поведение DOMInputElement'a с помощью обыных DOMElemen'ов(div)?
  21. Проблема приватных и публичным методов
  22. Как с помощью JS сделать одновременный выбор и загрузку нескольких файлов?
  23. При загрузке файлов с использованием File и FormData как определить не пытается ли пользователь загрузить директорию?
  24. Есть ли в JavaScript аналоги декларациям/аннотациям (термины из Python/Java соответственно)?

1. Почему eval = evil?


1. Eval нарушает привычную логику программы, порождая код.
2. Он выполняет код в текущем контексте и может его изменять — потенциальное место для атак. Контекст можно подменить, выполнив такой трюк:
(1,eval)(code);

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

3. Порожденный eval-ом код сложнее отлаживать.
4. Eval очень долго выполняется.

Почитать
Global eval. What are the options?
Куча вопросов на stackoverflow:
stackoverflow.com/questions/197769/when-is-javascripts-eval-not-evil
stackoverflow.com/questions/646597/eval-is-evil-so-what-should-i-use-instead
stackoverflow.com/questions/86513/why-is-using-javascript-eval-function-a-bad-idea


2. Чем заменить eval?


Лучшая замена eval — хорошая структура кода, которая не предполагает использование eval. Однако от eval не всегда легко отказаться (Парсинг JSON, Шаблоонизатор от Резига).
Хорошая замена eval — конструктор Function. Но это тот же eval, который выполняется в глобальном контексте.
new Function('return ' + code)();
// Это практически эквивалентно
(1,eval)(code);

3. Как проиграть звук используя JS без flash? Какие форматы поддерживаются?


Самое простое решение:
<audio src="http://developer.mozilla.org/@api/deki/files/2926/=AudioTest_(1).ogg" autoplay>
  Your browser does not support the <code>audio</code> element.
</audio>

Но этим HTML5 audio не ограничивается. Форматы: Ogg Vorbis, WAV PCM, MP3, AAC, Speex (зависит от конкретного браузера) О форматах: Audio format support

Почитать
HTML5 Audio и Game Development: баги браузеров, проблемы и их решения, идеи
WHATWG — The audio element
MDN — HTML audio
Пример: Creating a simple synth
Пример: Displaying a graphic with audio samples
Пример: Visualizing an audio spectrum
Пример: Creating a Web based tone generator

4. На сколько использование глобальных переменных замедляет скрипты?


Зависит от конкретного браузера. В современных браузерах этот момент оптимизирован и разницы нет. В старых доступ до глобальной переменной может быть медленнее в 2 раза.

Вот тест: jsperf.com/global-vs-local-valiable-access
Пожалуйста, учитывайте тот факт, что это тест, каждая операция прогоняется до 1 миллиона раз в секунду. Даже если прирост для теста 30%, то фактический прирост для 1й операции будет мизерный (микросекунды).

5. Как запретить выделение текста на JS?


Есть несколько вариантов.

1. Выполнить preventDefault для событий onselectstart и onmousedown
var element = document.getElementById('content');
element.onselectstart = function () { return false; }
element.onmousedown = function () { return false; }

2. Добавить атрибут unselectable
$(document.body).attr('unselectable', 'on')

3. Добавить стиль user-select: none
.g-unselectable {
  -moz-user-select: none;
  -khtml-user-select: none;
  -webkit-user-select: none;
  user-select: none;
}

Есть и безумные решения данной проблемы:
4. По таймеру чистить Range, TextRange, Selection
Не буду объяснять. Как работать с Selection можно посмотреть тут: Range, TextRange и Selection
5. Рендерить текст на канвасе
6. Использовать disabled textarea
<textarea disabled="disabled" style="border:none;color:#000;background:#fff; font-family:Arial;">
    Как запретить выделение текста на JS?
</textarea>

Вот микроплагин для jQuery, использующий методы 1-3
$.fn.disableSelection = function() {
    function preventDefault () {
        return false;
    }
    $(this).attr('unselectable', 'on')
           .css('-moz-user-select', 'none')
           .css('-khtml-user-select', 'none')
           .css('-o-user-select', 'none')
           .css('-msie-user-select', 'none')
           .css('-webkit-user-select', 'none')
           .css('user-select', 'none')
           .mousedown(preventDefault);
           .each(function() { 
               this.onselectstart = preventDefault;
           });
};

Этот плагин не спасет от Ctrl+A Ctrl+C в тех браузерах, которые не поддерживают user-select или unselectable. Помните: запрещать выделение — зло.

6. (function() {})(); Для чего используется такая конструкция?


Это называется Immediately-Invoked Function Expression (IIFE) — Функция-выражение, которая была вызвана сразу же после создания.

Вот несколько способов создать IIFE
(function(){ /* code */ })(); // Обычная IIFE
(function(){ /* code */ }()); // Крокфорд рекомендует

// Поставить функцию в состояние "значение"

true && function(){ /* code */ }();
0,function(){ /* code */ }();

// Можно создать, используя оператор в качестве префикса

!function(){ /* code */ }(); // Facebook style
~function(){ /* code */ }();
-function(){ /* code */ }();
+function(){ /* code */ }();

// Используя new

new function(){ /* code */ }
new function(){ /* code */ }() // Если необходимы аргументы, то ставим скобки

Где это используется:
1. Если вам нужно, чтобы какие-то переменные не попали во внешнее окружение (подобие let)
var APPLICATION_ID = function(){
    var keys = {
       'pew.ru': 15486,
       'pew.pew.ru': 15487,
       'pew.pew.pew.ru': 1548
    };
    
    return keys[window.location.host];
}();

2. Создание локального замыкания — основа для модулей, объектов, запоминающих состояние
var Module = (function(){
    function privateMethod () {
    
    };
    
    return {
       publicMethod: function () {
           return privateMethod();
       }
    };
}());

Почитать
Immediately-Invoked Function Expression (IIFE)
ECMA-262-3 in detail. Chapter 5. Functions
Functions and function scope
Named function expressions
JavaScript Module Pattern: In-Depth
Closures explained with JavaScript

7. Как сбросить события и восстановить состояние элемента?

У нас есть какой-нибудь DOM-объект с изначально заданными при помощи HTML и CSS параметрами «А». В процессе работы мы динамически меняем параметры на «В». Существует ли простой способ «сбросить» состояние объекта первоначальное состояние «А»? Аналогично с событиями. Как сделать откат на первоначальное состояние?
Откатить состояние объекта не получится. DOM не хранит состояние потому, что элементы могут быть получены различным образом: непосредственно из кода html и сгенерированные JavaScript.

С событиями все проще. В DOM есть хороший метод cloneNode. Мы просто клонируем элемент со всем его содержимым (он не будет иметь событий) и подменяем старый элемент новым.
// Для теста
$('input').click(function(){alert('')});

function resetEvents(element) {
    var newElement = element.cloneNode(true);
    element.parentNode.insertBefore(newElement, element); // fixed by @spmbt
    element.parentNode.removeChild(element);

    return newElement;
}

resetEvents($('input')[0]);

// Клик на инпут ничего не даст 

8. Как корректно передавать js-команды с параметрами в ajax приложениях?

Как корректно передавать js-команды с параметрами в ajax приложениях без возни с кавычками? Это относится к передаче данных в ajax-системе, описанной выше способом. При передаче сложных команд с несколькими уровнями вложенности параметров (например, «setTimeout('alert(»Бум!!!");', 200);" ) наблюдаются проблемы при использовании кавычек. Как их разрешить или есть ли какие-нибудь общие правила их оформления?
Пересылать код от сервера к клиенту — это плохо. Это аналогично проблеме с eval. Правильно, быстро и безопасно передавать не код, а данные, которые будет обрабатывать браузер.

Можно использовать RPC. Очень прозрачный RPC можно получить, используя NowJS. NowJS — фреймворк, позволяющий выполнять функции на сервере как будето вы выполняете их на клиенте.

Вот пример с сайта:
На сервере: Node.JS server
var nowjs = require("now");

// тут создаем http сервер

var everyone = nowjs.initialize(httpServer);

everyone.now.getServerInfo = function(callback){
  db.doQuery(callback);
}

На клиенте
now.getServerInfo(function(data){
  // Данные запроса
});

Просто, прозрачно и красиво!

Почитать
Getting Started with NowJS

9. Для чего в jQuery передавать window если он и так глобальный и зачем undefined?


jQuery создает замыкание и в него пробрасывает window и undefined.
1. undefined передается для уменьшение объема кода и обхода проблемы с перезаписью undefined. jQuery часто сравнивает с undefined и при сжатии undefined превратиться в однобуквенную переменную.
2. window передается для уменьшение объема кода. jQuery применяет хорошую политику, отделяя методы окна от своих, используя window.setInterval() и прочие. При сжатии window превратиться в однобуквенную переменную, в коде будет c.setInterval()

10. Как узнать в какую сторону юзер крутит колесо мыши?


Есть 2 события, реагирующие на скролл колесом 'DOMMouseScroll' (Firefox) и 'mousewheel' (остальные)
Как навесить событие, думаю всем понятно, посмотрим его обработчик
function wheel(event){
    var delta = 0;
    if (!event) event = window.event; // Событие IE.
    // Установим кроссбраузерную delta
    if (event.wheelDelta) { 
        // IE, Opera, safari, chrome - кратность дельта равна 120
        delta = event.wheelDelta/120;
    } else if (event.detail) { 
        // Mozilla, кратность дельта равна 3
        delta = -event.detail/3;
    }
    if (delta) {
        // Отменим текущее событие - событие поумолчанию (скролинг окна).
        if (event.preventDefault) {
            event.preventDefault();
        }
        event.returnValue = false; // для IE
        
        // если дельта больше 0, то колесо крутят вверх, иначе вниз
        var dir = delta > 0 ? 'Up' : 'Down',
    }
}

Для jQuery Есть Mouse Wheel Plugin jQuery Mouse Wheel Plugin Demos

Не все элементы могут порождать скролл-событие. Зависит от конкретного браузера. Подробнее: scroll and mousewheel

11. Есть ли событие focus у элемента, если нет, то возможно ли его реализовать?


Фокус — это логическое событие, показывающее то, что объект сейчас выделен.

Если почитать стандарт то там написано, что события focus и blur есть только у элементов формы: LABEL, INPUT, SELECT, TEXTAREA, и BUTTON. По стандарту никакие другие элементы не могут генерировать это событие. Для всех остальных элементов есть другие события DOMFocusIn и DOMFocusOut. IE, как всегда имеет свои события: focusin и focusout.
Firefox тоже не особо подчиняется стандарту у него некоторые элементы, кроме формы могут выбрасывать focus и blur.

Посмотрите тест событий focus, blur, focusin, focusout, DOMFocusIn, DOMFocusOut:
www.quirksmode.org/dom/events/tests/blurfocus.html
Если слушать все 3 события у элемента, то мы можем определить получил ли любой элемент фокус или нет.

Почитать
blur and focus

12. Как сделать неизменяемый объект (freeze)?


В ECMAScript 3 (стандарт который поддерживают все движки JavaScript) не было возможности замораживать объект, но в ECMAScript 5 появились сразу несколько функций, ограничивающих изменение объекта.

Object.preventExtensions — самое слабое ограничение. Объект не может получать дополнительные параметры
Object.sealpreventExtensions + любые параметры не могут удаляться
Object.freezepreventExtensions + seal + параметры становятся только на чтение
Эти новые методы поддерживаются далеко не во всех браузерах

Почитать
New in JavaScript 1.8.5
Object.preventExtensions/seal/freeze Function (JavaScript)
ECMAScript 5 compatibility table

13. Возможно ли сделать кастомную реализацию DomElement так чтобы при разборе для подмножества тегов использовалась она?


Напрямую нет. DOM отделен от JavaScript и поэтому есть некоторые ограничения.
1. Нет конструкторов конкретных элементов — есть только фабрика элементов — document.createElement('') и некоторые методы строки для создания элементов — String.prototype.anchor и тп. HTMLElement и HTMLDivElement — это не конструкторы.
2. Даже если мы эмулируем конструктор, то DOM «очистит» объект после вставки в дерево.
Пример
// Создаем наш какбы-конструктор
var HTMLZzzElement = function () {
   var self = document.createElement('zzz');
   self.__proto__ = HTMLZzzElement.prototype;
   return self;
};

// Наследуем от HTMLUnknownElement
function F(){}
F.prototype = HTMLUnknownElement.prototype;

HTMLZzzElement.prototype = new F();
HTMLZzzElement.prototype.constructor = HTMLZzzElement;
HTMLZzzElement.prototype.pewpewpew = function () {};

// Создаем
var zzz = new HTMLZzzElement();

// Да, работает
zzz.innerHTML = 'Yohoho!';

// Проверяем
console.log(zzz instanceof HTMLZzzElement); // true
console.log(zzz instanceof HTMLUnknownElement); // true
console.log(zzz instanceof HTMLElement); // true
console.log(typeof zzz.pewpewpew); // function
//Все хорошо - все есть

// Засовываем в DOM
document.body.appendChild(zzz);

// Получаем обратно
zzz = document.querySelector('zzz')

// Объект очистили
console.log(zzz instanceof HTMLZzzElement);  // false
console.log(zzz instanceof HTMLUnknownElement); // true
console.log(zzz instanceof HTMLElement); // true
console.log(typeof zzz.pewpewpew); // undefined

Однако, мы можем дописать к прототипу «класса» HTML элемента методы и свойства и потом использовать в будущем:
HTMLUnknownElement.prototype.pewpewpew = function () {};

console.log(typeof zzz.pewpewpew); // function

Цепочка наследования «классов» такая
Node -> Element -> HTMLElement -> HTMLDivElement/HTMLMetaElement ...
Вы можете дописать метод к прототипу элемента, однако помните, что пропатчивать чужие объекты — это плохо!

Этот способ будет работать только в современных браузерах. В ИЕ8 и ниже html элементы берутся как будето из воздуха, поэтому их прототипы нельзя переписать на низком уровне:
>>document.createElement('div').constructor
//undefined -- даже не null...

>>document.createElement('div') instanceof Object
//false -- забавно, правда? хотя магия с toString говорит обратное
>>Object.prototype.toString.call(document.createElement('div'));
//"[object Object]"

14. Какой клиентский MVC-фреймворк посоветуете?


Фреймворков много:
JavaScriptMVC
Backbone.js
SproutCore
TrimJunction

Посмотрите все и выберете лучше тот, который вам по душе. Я плотно работал только с Backbone.js

Почитать
Написание сложных интерфейсов с Backbone.js
И опять про MVC
MVC в JavaScript

15. Обход ограничений по Referer

На сервере blabla.ru лежит картинка, сервер не отдает её если передал Referer с другого сервера (pewpew.com). Как получить содержимое картинки и показать пользователю?
Напрямую из браузера никак. Можно сделать серверный прокси, который будет менять Referer.

16. GOTO в Javascript

Собственно вопрос как повторить поведение goto в JavaScript. (Про хороший стиль и goto рассказывать не нужно :) речь идёт про генерированный код. и восстановление блоков и циклов процесс весьма не тривиальный)
В JavaScript есть метки, но нет goto (есть только зарезервированное слово). Метка выглядит так же как и во всех других языках, но работают они иначе. Метка выделяет блок. Внутри блока могут быть другие помеченые блоки. На метку можно переходить используя break и continue, которые есть внутри этого блока.

Пример:
loop1: for (var a = 0; a < 10; a++) {
   if (a == 4) {
       break loop1; // Только 4 попытки
   }
   alert('a = ' + a);
   loop2: for (var b = 0; b < 10; ++b) {
      if (b == 3) {
         continue loop2; // Пропускаем 3
      }
      if (b == 6) {
         continue loop1; // Продолжаем выполнят loop1 'finished' не будет показано
      }
      alert('b = ' + b);
   }
   alert('finished')
}

block1: {
    alert('hello'); // Отображает 'hello'
    break block1;
    alert('world'); // Никогда не будет отображено
}
goto block1; // Parse error - goto нет

17. Наследование в JavaScript. Как идеологически правильно заэкстэндить Man и Woman от Human


Идеологически правильно использовать прототипное наследование. Процесс наследования хорошо описан в статье "Основы и заблуждения насчет JavaScript"

18. Возможно ли устроиться javascript программистом без копания в верстке и server-side?


Зависит от проекта, размера компании и вашего опыта.
Если вы пойдете разработчиком браузерных-игр в крупную компанию, то шанс, что бы будете заниматься только JavaScript очень высок. Крупная компания может себе позволить отдельного интерфейсного программиста, а мелкая — нет. Вот повезет ли вам без опыта в крупной?!
Если вы только начинаете изучать веб, то вам стоит пойти туда где хотят всего.

19. Какие есть практики передачи данных с сервера в код, который исполняется из .js?


Способов передачи данных очень много: XHR, SSE, WebSockets, JSONP:
Реализация HTTP server push с помощью Server-Sent Events
Новые возможности XMLHttpRequest2
Создание приложений реального времени с помощью Server-Sent Events
Много всего про ajax

Какой транспорт и формат выбрать вам — зависит от требований к приложению. Например, если это чат, то лучше SSE или WebSockets (время доставки сообщения критично). А если этот чат должен работать кроссдоменно и в ИЕ6, то придется добавить магии с iframe или flash.

20. Можно ли подцепить(эмулировать) поведение DOMInputElement'a с помощью обыных DOMElemen'ов(div)?


На низком уровне нельзя. Причину я объяснил в 13-м примере. Можно сделать обертку, которая будет эмулировать Input, в основе которой будет DOMDivElement.
Я набросал простенький эмулятор jsfiddle.net/azproduction/Kz4d9 — получился очень кривой инпут (не умеет сабмититься, не умеет терять фокус и т.п.).
Эту же проблему можно решить более элегантно(спасибо Finom, что напомнил), добавив всего 1 атрибут: contenteditable, который поддерживают только десктопные браузеры — получился тот же инпут, но гораздо проше и лучше jsfiddle.net/azproduction/ZTqRe

21. Проблема приватных и публичным методов

Если возникает желание использовать частные переменные и методы объектов, в JavaScript есть проблема доступа к частным методам из публичных. А привилегированные методы, определенные внутри конструктора, если я правильно понимаю, дублируются для каждого экземпляра, что не очень хорошо с точки зрения производительности. Как насчет такого подхода?
TheShock:
Неплохой подход. По крайней мере лучше, чем объявлять все методы в конструкторе.
// our constructor
function Person(name, age){
    this.name = name;
    this.age  = age;
};

// prototype assignment
Person.prototype = new function(){

    // we have a scope for private stuff
    // created once and not for every instance
    function toString(){
        return this.name + " is " + this.age;
    };

    // create the prototype and return them
    this.constructor = Person;

    this._protectedMethod = function (){
        // act here
    };

    this.publicMethod = function() {
        this._protectedMethod();
    };

    // "magic" toString method
    this.toString = function(){
         // call private toString method
         return toString.call(this);
    }
};

Модульный подход:
var Module = (function (window) {
    function privateFunction() {
        console.log('pass');
    };

    var privateClass = function () {};

    var publicClass = function () {};
    publicClass.prototype.publicMethod = function () {
        privateFunction(); // ...
    };
    
    return { // Экспортируем
        publicClass: publicClass
    };
}(this));

// Используем
(new Module.publicClass()).publicMethod();

22. Как с помощью JS сделать одновременный выбор и загрузку нескольких файлов?


Все уже давно и не по разу написано:
1. HTML5 File API: множественная загрузка файлов на сервер
2. Загрузка файлов с помощью HTML5 и сколько раз мы сказали нехорошие слова
3. Загрузка браузером нескольких файлов
4. ajax загрузка нескольких файлов с php формой
5. Загрузка файлов с помощью html5 File API, с преферансом и танцовщицами

23. При загрузке файлов с использованием File и FormData как определить не пытается ли пользователь загрузить директорию?

При загрузке файлов с использованием File и FormData — как определить не пытается ли пользователь загрузить директорию. Свойство File.type к сожалению не помогает (для некоторых типов, как и для дир — пустая строка), свойство size — тоже (ff и сафари по разному показывают)?
Насчет File.type говорится, что это mime-тип и если он не определен, то он становится пустой строкой. mime-тип, как водится получается из расширения файла/папки. Если мы подсунем настоящий файл с именем «pewpew», то mime будет "", а если директорию с именем «pewpew.html», то mime будет "text/html". Нам это не подходит.
Размер папки всегда 0, но могут быть и файлы размером 0 байт. Для 100% уверенности нам это не подходит.
В стандарте www.w3.org/TR/FileAPI не говорится о директориях отдельно, но там отмечено, что браузеры могут выкидывать SECURITY_ERR если пользователь пытается работать с не корректным файлов (защищенным, не доступным на чтение и т.п.) Будем исходить из этого.

В основу я взял пример Drag and Drop
Будем использовать FileReader для определения типа ресурса. В фаерфоксе если читать папку как файл, то метод reader.readAsDataURL выбрасывает исключение. В хроме это исключение не выбрасывается, но вызывается событие onerror у reader. Скрестим и получим функцию для определения файл это или нет.
function isReadableFile(file, callback) {
    var reader = new FileReader();
    reader.onload = function () {
        callback && callback(true);
        callback = null;
    }
            
    reader.onerror = function () {
        callback && callback(false);
        callback = null; 
    }
        
    try {
        reader.readAsDataURL(file); 
    } catch (e) {
        callback && callback(false);
        callback = null; 
    }
}

В функции я для подстраховки удаляю callback, чтобы он не вызвался повторно (в будущем поведение FileReader может измениться).
Пример использования jsfiddle.net/azproduction/3sV23/8

Можно, было ограничится размером файла. Если 0, то считать что это не файл (ибо странно загружать файл размером 0 байт).

24. Есть ли в JavaScript аналоги декларациям/аннотациям (термины из Python/Java соответственно)?


Прямых аналогов нет.

PS Если я забыл ответить на какой-то вопрос — задайте его ещё раз. Если у вас есть вопросы, на которых нет ответов ни в первой ни во второй части — задавайте — они будут в третей ;)
Tags:
Hubs:
+175
Comments 100
Comments Comments 100

Articles