0,0
рейтинг
20 ноября 2012 в 12:08

Разработка → Подводные камни JavaScript

Мне очень нравится JavaScript и я считаю его мощным и удобным. Но для большинства начинающих JS-программистов, много проблем создаёт недопонимание аспектов языка. Часто конструкции языка ведут себя «нелогично». В данной статье я хочу привести примеры «граблей», на которые я наступил; объяснить поведение языка и дать пару советов.



Типы


Как написано в спецификации ECMAScript, всего существует 6 типов:
  1. Undefined
  2. Null
  3. Boolean
  4. String
  5. Number
  6. Object

Все значения должны принадлежать к ним. В JS есть оператор typeof, который, как казалось бы, должен возвращать тип объекта. Казалось бы, один из перечисленных. Что получается на самом деле:

  typeof 5;             //"number",        ок, похоже на правду
  typeof "hello";       //"string" 
  typeof true;          //"boolean" 
  typeof undefined;     //"undefined"
  typeof {};            //"object".        Пока 5 из 5
  typeof null;          //"object".        WTF?
  typeof function(){};  //"function".      Разве у нас есть тип function?

Проблема: несмотря на то, что тип у null — Null, оператор возвращает 'object'; а тип у функции — Object, оператор возвращает 'function', а такого типа нет.
Объяснение: typeof возвращает не тип, а строку, которая зависит от аргумента и не является именем типа.
Совет: забудьте про типы. Серьезно, я считаю что знание 6 типов JS не даст вам пользы, а оператор typeof используется довольно часто, поэтому лучше запомнить результаты его работы:
Тип аргумента Результат
Undefined undefined
Null object
Boolean boolean
Number number
String string
Object (результаты оператора new, inline-объекты ({key: value})) object
Object (функции) function


Магические значения: undefined, null, NaN


В спецификации описаны так:

  • undefined value — primitive value used when a variable has not been assigned a value
  • Undefined type — type whose sole value is the undefined value
  • null value — primitive value that represents the intentional absence of any object value
  • Null type — type whose sole value is the null value
  • NaN — number value that is a IEEE 754 “Not-a-Number” value


У себя в голове я держу следующее:
  • undefined — значение переменной, которая не была инициализирована. Единственное значение типа Undefined.
  • null — умышленно созданный «пустой» объект. Единственное значение типа Null.
  • NaN — специальное значение типа Number, для выражения «не чисел», «неопределенности». Может быть получено, например, как результат деления 0 на 0 (из курса матанализа помним, что это неопределенность, а деление других чисел на 0 — это бесконечность, для которой в JS есть значения Infinity).

С этими значениями я обнаружил много «магии». Для начала, булевы операции с ними:
  !!undefined; //false
  !!NaN; //false
  !!null; //false
  //как видим, все 3 значения при приведении к boolean дают false

  null == undefined; //true

  undefined === undefined; //true
  null === null; //true

  NaN == undefined; //false
  NaN == null; //false

  NaN === NaN; //false!
  NaN == NaN; //false!

Проблема: с чем бы мы ни сравнивали NaN, результатом сравнения всегда будет false.
Объяснение: NaN может возникать в результате множества операций: 0/0, parseInt('неприводимая к числу строка'), Math.sqrt(-1) и было бы странно, если корень из -1 равнялся 0/0. Именно поэтому NaN !== NaN.
Совет: не использовать булевы операторы с NaN. Для проверки нужно использовать функцию isNaN.



  typeof a; //'undefined'
  a; //ReferenceError: a is not defined

Проблема: оператор typeof говорит нам, что тип необъявленной переменной — undefined, но при обращении к ней происходит ошибка.
Объяснение: на самом деле, есть 2 понятия — Undefined и Undeclared. Так вот, необъявленная переменная является Undeclared-переменной и обращение к ней вызывает ошибку. Объявленная, но не инициализированная переменная принимает значение undefined и при обращении к ней ошибок не возникает.
Совет: перед обращением к переменной, вы должны быть уверенны, что она объявлена. Если вы обратитесь к Undeclared-переменной, то код, следующий за обращением, не будет выполнен.



  var a; //вновь объявленная переменная, для которой не указано значение, принимает значение undefined
  console.log(undefined); //undefined
  console.log(a); // undefined
  a === undefined; //true
  undefined = 1;
  console.log(undefined); //1
  a === undefined; //false

Проблема: в любой момент мы можем прочитать и записать значение undefined, следовательно, кто-то может перезаписать его за нас и сравнение с undefined будет некорректным.
Объяснение: undefined — это не только значение undefined типа Undefined, но и глобальная переменная, а значит, любой может её переопределить.
Совет: просто сравнивать переменные с undefined — плохой тон. Есть 3 варианта решения данной проблемы, для создания «пуленепробиваемого» кода.
  • Вы можете сравнивать не значение переменной, а её тип: «typeof a === 'undefined'».
  • Использовать паттерн immediately-invoked function:

  (function(window, undefined){
    //т.к. второй аргумент не был передан, значение переменной undefined будет «правильным».
  }(this));

  • Для получения реального «undefined»-значения можно использовать оператор void (кстати, я не знаю другого применения этому оператору):

  typeof void(0) === 'undefined' // true




Теперь попробуем совершить аналогичные действия с null:
  console.log(null); //null
  null = 1; //ReferenceError: Invalid left-hand side in assignment

Проблема: несмотря на некоторые сходства между null и undefined, null мы перезаписать не можем. На самом деле проблема не в этом, а в том, что язык ведёт себя нелогично: даёт перезаписать undefined, но не даёт перезаписать null.
Объяснение: null — это не глобальная переменная и вы не можете её создать, т. к. null — зарезервированное слово.
Совет: в JavaScript не так много зарезервированных слов, проще их запомнить и не использовать как имена переменных, чем вникать, в чём проблема, когда она возникнет.



И теперь сделаем тоже самое с NaN:
  console.log(NaN); //NaN
  NaN = 1;
  console.log(NaN); //NaN
  isNaN(NaN); //true

Проблема: при переопределении undefined всё прошло успешно, при переопределении null возникла ошибка, а при переопределении NaN операция не вызвала ошибки, но свойство не было переопределено.
Объяснение: нужно понимать, что NaN — переменная глобального контекста (объекта window). Помимо этого, к NaN можно «достучаться» через Number.NaN. Но это неважно, ниодно из этих свойств вы не сможете переопределить, т. к. NaN — not writable property:
  Object.getOwnPropertyDescriptor(window, NaN).writable; //false
  Object.getOwnPropertyDescriptor(Number, NaN).writable; //false

Совет: как JS-программисту, вам нужно знать об атрибутах свойств:
Атрибут Тип Смысл
enumerable Boolean Если true, то данное свойство будет участвовать в циклах for-in
writable Boolean Если false, то значение этого свойства нельзя будет изменить
configurable Boolean Если false, то значение этого свойства нельзя изменить, удалить и изменить атрибуты свойства тоже нельзя
value Любой Значение свойства при его чтении
get Object (или Undefined) функция-геттер
set Object (или Undefined) функция-сеттер

Вы можете объявлять неудаляемые или read-only свойства и для созданных вами объектов, используя метод Object.defineProperty:
  var obj = {};
  Object.defineProperty(obj, 'a', {writable: true,  configurable: true,  value: 'a'});
  Object.defineProperty(obj, 'b', {writable: false, configurable: true,  value: 'b'});
  Object.defineProperty(obj, 'c', {writable: false, configurable: false, value: 'c'});

  console.log(obj.a); //a
  obj.a = 'b';
  console.log(obj.a); //b
  delete obj.a; //true

  console.log(obj.b); //b
  obj.b = 'a';
  console.log(obj.b); //b
  delete obj.b; //true

  console.log(obj.c); //c
  obj.b = 'a';
  console.log(obj.c); //c
  delete obj.b; //false


Работа с дробными числами


Давайте вспомним 3-й класс и сложим несколько десятичных дробей. Результаты сложения в уме проверим в консоли JS:
  0.5 + 0.5; //1
  0.5 + 0.7; //1.2
  0.1 + 0.2; //0.30000000000000004;
  0.1 + 0.7; //0.7999999999999999;
  0.1 + 0.2 - 0.2; //0.10000000000000003

Проблема: при сложении некоторых дробных чисел, выдаётся арифметически неверный результат.
Объяснение: такие результаты получаются из-за особенностей работы c числами с плавающей точкой. Это не является особенностью JavaScript, другие языки работают также (я проверил в PHP, Python и Ruby).
Совет: во-первых, вы, как программист, обязаны знать об особенностях работы компьютера с числами с плавающей точкой. Во-вторых, в большинстве случаев достаточно просто округлять результаты. Но, если вдруг необходимо выдавать пользователю точный результат, например, при работе с данными о деньгах, вы можете просто умножать все аргументы на 10 и результат делить обратно на 10, например так:
  function sum() {
    var result = 0;
    for (var i = 0, max = arguments.length; i< max; i++ ) {
      result += arguments[i]*10;
    }
    return result / 10;
  }
  sum(0.5, 0.5); //1
  sum(0.5, 0.7); //1.2
  sum(0.1, 0.2); //0.3
  sum(0.1, 0.7); //0.8
  sum(0.1, 0.2, -0.2); //0.1


Вывод


Это только несколько необычных примеров с непредсказуемым результатом. Если помнить о них, то получится не наступить на те же грабли и быстро понять, в чём проблема. Если найдёте новый «нелогичный» кусок кода, то попробуйте осознать, что происходит с точки зрения языка, почитав спецификацию или MDN.
Александр Субботин @KELiON
карма
29,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (148)

  • +11
    Для получения того самого типа — внутреннего свойства [[Class]] можно использовать особенность работы Object.prototype.toString и написать вот такой код
    var allowedTypes = ["Object", "Undefined", "Null", "Boolean", "Number", "String"];
    /**
     * @param {Mixed} x
     *
     * @return {String}
     */
    function Type(x) {
        var type = Object.prototype.toString.call(x).replace(/^\[object\s|\]$/g, '');
        return ~allowedTypes.indexOf(type) && type || "Object";
    }
    

    Еще есть интересная особенность оператора new — он не может вызвать функцию у которой нет свойства prototype — все нативные функции и функции () => из ES6
    new Array.prototype.slice
    // TypeError: Array.prototype.slice is not a constructor
    
    // VS
    
    var A = function () {};
    
    Object.getOwnPropertyDescriptor(A, "prototype");
    // {"configurable":false,"enumerable":false,"value":{},"writable":true}
    
    // Убить prototype у пользовательской функции нельзя
    delete A.prototype; // false - "configurable":false
    
    new A(); // ok
    
    • 0
      Спасибо за пример, таким способом действительно можно получить тип объекта, но, как я и говорил вышел, не вижу этому применения
    • +3
      Это и многое другое можно найти в JS Garden:
      shamansir.github.com/JavaScript-Garden/#types.typeof
    • 0
      с typeof и [[Class]] вообще интересного много: blgo.ru/t/jstypes/table/
    • +2
      По второму уточню: не в отсутствии prototype дело, а в отсутствии внутреннего метода [[Construct]]:
      Array.prototype.slice.prototype={}
      new Array.prototype.slice // TypeError: function slice() { [native code] } is not a constructor
      
  • +5
    По поводу работы с дробными числами. Вместо писать велосипед с «function sum...», я бы рекомендовал Вам воспользоваться toFixed
    Вот Вам явный пример:
    console.log((0.1+0.22 -0.2+ 0.3 + 0.7).toFixed(2));
    
    • 0
      В функции автора в каких-то случаях даже есть смысл, ибо toFixed: String
      Math.PI.toFixed(2); // "3.14"
      1e+22.toFixed(2) // "1e+22"
      
  • –2
    Какая хорошая статья, спасибо!
    • +20
      И правда, в этом месяце на Хабре ещё не было описания подводных камней JS.
      • +5
        Я с нетерпением жду ремейк «наследование в js» или «подробное описание prototype».
        • +1
          Я с нетерпением жду ремейк «наследование в js» или «подробное описание prototype».

          Та да, уже недели две не было. Я уж и забывать начал.
        • +1
          И ни слова про неугомонный this…
  • +1
    Использовать паттерн immediately-invoked function:

    По поводу этого паттерна я недавно делал замечание.
  • +16
    > в любой момент мы можем прочитать и записать значение undefined, следовательно, кто-то может перезаписать его за нас и сравнение с undefined будет некорректным.

    С таким же успехом кто-то может перезаписать все свойства документа, jQuery, и методы массивов. После этого он может облить ваш компьютер бензином и поджечь его.
    • 0
      Нелогично то, что кто-то может это сделать с undefined, но не может, например, с NaN. undefined так же могло быть not-writable свойством — это было бы логично.

      А jQuery пока не является нативным объектом JS, так что не будем о нём в этой статье ;)
      • +1
        void 0 и будет вам счастье
      • +5
        Дался вам этот undefined! Сравнивайте с null. Они дают равенство только друг другу и ничему больше.
      • +6
        Нужно помнить что при использовании 'use strict'; — undefined станет immutable

        //TypeError: undefined is read-only
        (function () {
        'use strict';
        undefined = 'aaa';
        }());
        
    • 0
      Именно поэтому через замыкание дополнительным параметром передается undefined
    • 0
      Вот Вы иронизируете, а у меня когда-то получилось сделать это непреднамеренно.
    • 0
      хорошо бы узнать как автор сделает это в глобальном контексте. в локальном это возможно — но это тогда будет Ваша проблема, а не глобальная.
  • –1
    В js чем меньше знаешь, тем лучше спишь:
    только что сделал
    var x = 1000/0;
    console.log(typeof x); // number
    console.log(typeof 1000/0) // NaN
    

    • +20
      var x = 1000/0;// — это бесконечность, Infinity, тип Number.
      console.log(typeof x); // number, всё верно
      console.log(typeof 1000/0) // NaN
      //Теперь поставим скобки правильно:
      console.log((typeof 1000)/0); //NaN
      console.log(typeof(1000/0)); //Number

      Всё логично:)
      • 0
        О спасибо. Как-то казалось оператор деления будет иметь приоритет перед typeof. Тогда да логично.
        • +2
          typeof унарный оператор, он выполняется раньше.
          • 0
            Непонятно, в чём смысл минусов. Если вы спецификацию не читаете, это ваше дело.
      • +2
        А когда 1000/0 стал безконечностью?
        1000/0 как раз таки неопределенность, поскольку не существует числа, которое при умножении на 0 даст 1000. И бесконечность при умножении на 0 не даст 1000.
        • –1
          Не вижу противоречия. Бесконечность разве число?
          • 0
            В комментарии KELiON «это бесконечность, Infinity, тип Number.»
            Кроме того, даже если бесконечность не число — с чего вдруг бесконечность умножить на 0 будет 1000?
            • 0
              NaN — тоже тип Number. Произведение Infinity на что-угодно даёт NaN. Не вижу проблемы. Почему вы считаете, что операции, дающие не числа как Infinity, должны быть обратимы?
              • 0
                Потому что математическая операция деления — обратима. И она не должна возвращать Infinity.
                • +1
                  Только когда речь идёт о числах. Не забывайте о границах применимости.
                  • 0
                    Я ведь делю 1000 на 0. Это числа?
                    • 0
                      Продолжайте размышлять, вы недалеко от ответа.
                      • 0
                        Продолжайте строить из себя умника, не уважающего других. Вы уже ОЧЕНЬ близко к цели.
                        • –1
                          1000/0 — это бесконечность, если исходить из математики. Т.к. если делить 1000 на бесконечно малое число, то получим бесконечность. Грубо говоря 0 это вообще ничего не существует, всегда хоть что то да есть, даже что то бесконечно малое. Вот и выходит, что 1000 / (1/10000000000000) получается бесконечно большое число.

                          P.S. во я завернул)
                          • 0
                            Да не правда это. Ноль — это строго ноль. 1/10000000000000 — не ноль, хоть тысячу раз в квадрат возведите. Нужно понимать, что есть разница между нулем, и бесконечно малым.
                          • +1
                            Вы немного путаете. Бесконечности будет равен предел выражения 1000/x, где x приближается к нулю. В математике результат деления на 0 же неопределен.
                  • 0
                    По хорошему, при выходе за границы применимости следовало бы бросать исключение.
                • –2
                  Она-то обратима, вот только делить на ноль нельзя. Разработчики ECMAScript (и не одни они) это решили весьма интересным (и часто очень удобным) способом.
                  • +4
                    На 0 делить нельзя только в элементарной алгебре. Я с первого курса матнализа помню, что:
                    • 0/0 — неопределенность,
                    • любое положительное число делить на 0 = +∞
                    • любое отрицательное число число делить на 0 = -∞
                    • ∞/∞ — неопределенность
                    • ∞*∞=∞


                    В решении JS нет ничего интересного, если заменить неопределенность на NaN, а бесконечность на Infinity. Операции с ними не противоречат матанализу. Единственное замечание — в JS нет разницы между +0 и -0, но есть Infinity и -Infinity.
                    • +1
                      любое положительное число делить на положительное число СТРЕМЯЩЕЕСЯ К НУЛЮ равно числу, СТРЕМЯЩЕМУСЯ К БЕСКОНЕЧНОСТИ.
                      • 0
                        Не вижу противоречия: 0 стремится к 0, а ∞ стремится к ∞
                        • +1
                          Мы говорим о разных вещах. НА НОЛЬ ДЕЛИТЬ НЕЛЬЗЯ, никак.

                          На курсе матана вы проходили ПЕРЕМЕННЫЕ стремящиеся к нулю/бесконечности.

                          5 / 0 — бред, это только для упрощения пишут, например, 0/0. На деле подразумевается x/y при x, y -> 0.

                          — Единственное замечание — в JS нет разницы между +0 и -0

                          Глупости:
                          console.log( 1/(-0) )
                          • 0
                            В ECMAScript в The Abstract Equality Comparison Algorithm отдельным пунктом прописывается -0 == +0
                            Деление же на 0 со знаком Applying the / Operator опирается на IEEE 754
                            Division of a non-zero finite value by a zero results in a signed infinity. The sign is determined by the rule already stated above.

                            PS Я прекрасно понимаю ваши чувства и то, что для получения бесконечности надо поделить на бесконечно малую величину, а не ноль. Вы так же «поймите и простите» ECMAScript :)
                          • 0
                            5/0 — это не бред. это элемент кольца замыкание®, а именно бесконечность.
                            • –1
                              Можно подробнее? Хотя бы, куда копать?
                            • 0
                              вообще 5/0 = 5*(0)^(-1) = 5*Infinity = Infinity (при этом обратного к Infiniti не существует)
                              / — это ведь просто значёк для упрощения записи умножения на обратный эллемент
                              А ещё это алгебра и теория чисел, а не матан
                              • 0
                                Просто меня смущает то, что вы выше написали… Разве можно говорить, что бесконечность есть обратный элемент относительно операции умножения для нуля? Вроде в определении поля как раз у него нет обратного.

                                Или я путаю понятия?
                                • 0
                                  Если вы смотрите на это как на поле, то вы правы, но можно смотреть как на кольцо. Тогда можно к R добавить два элемента +-бесконечность и доопределить нужные операции. При этом довольно формально можно определить операцию деления и обратные для некоторых элементов (кольцо не требует их существования).

                                  А в поле R всё верно R* = R\{0} — множество обратимых элементов.
                                  • –2
                                    Давайте решим, что на ноль делить нельзя, т.к. это абсурдно с логической точки зрения? :) Чтобы не думать о вариантах математических объектов.
                                    • 0
                                      А давайте решим, что деление на ноль даёт бесконечность, чтобы не продолжать этот бессмысленный спор.
                                      • +2
                                        В питоне 1/0 — ошибка; В JS 1/0 — infinity; Каждый язык хорош по своему — давайте расходиться, а то тред уже плющит :)
                                      • –1
                                        Ну так я о том же.
                                    • +1
                                      можно согласиться, что 5/0 — это формальность. делить — это тоже формальность =)
                                      можно только умножать и брать обратный. вы можете работать с кольцом R U {+-Infinity, NaN} и определить все правила умножения — это просто. А потом формально доопределить обратные элементы для того, чтобы «делить». В общем-то так и делают все ЯП.

                                      И ничего тут абсурдного нет, так ведь можно сказать что комплексные числа тоже абсурд с логической точки зрения =)
                        • 0
                          0 не стремится к 0, 0 достиг нуля.
            • +1
              1000/0 - Infinity, 0/0 - NaN, NaN !== NaN Это все IEEE 754
              • +1
                Ну наконец-то. Читал эту ветку и ужасался :) Действительно, достаточно просто сказать, что реализация Number соответствует стандарту IEEE 754 для чисел удвоенной точности. Откуда следует, в частности, что:
                console.log((1e+16 + 1)  == 1e+16); //true
                console.log((1e+16 + 100)  == 1e+16); //false
                

                и прочие «забавные» вещи.
    • +3
      И что смущает?
      Читаем Operator precedence на mdn
      image
  • НЛО прилетело и опубликовало эту надпись здесь
    • +1
      нет — это проблема при оптимизации арифметеки. Я с такой проблемой столкнулся в mongoDB — там тоже пришлось делать враппер над представлениями чисел.
  • +2
    Для получения реального «undefined»-значения можно использовать оператор void (кстати, я не знаю другого применения этому оператору):

    Помимо
    void 0 == undefined
    void можно использовать для красивого замыкания без лишних скобок:
    void function(global) {
        //closure
    } (this);
    
    • +1
      Я часто замыкаю
      ~function() {

      }

      Но тут реально дело вкуса, как нравится.
    • +2
      Да, тут опущен момент, что void — это тоже оператор, так же как и typeof. Это не функция, а скобки просто вычисляют выражение, то есть void(0) == void 0.
  • +3
    А еще из-за особенностей внутреннего представления JavaScript объектов V8 и Оперы в хэше сперва идут числа в порядке убывания, а потом стоковые элементы в порядке добавления. Притом, что при использовании ключа большего, чем 1e+9 ключи в них воспринимаются как строки и идут в порядке добавления.

    var a = {};
    
    a['pewpew'] = 1;
    a['ololo'] = 2;
    a[1] = 3;
    a[0] = 4;
    a[1e+9] = 5;
    a[1e+10] = 6;
    
    JSON.stringify(a);
    
    JSON.stringify(a);
    // V8 (Chrome, Node,js) и Опера
    // {"0":4,"1":3,"1000000000":5,"pewpew":1,"ololo":2,"10000000000":6}
    
    // FireFox и Safari
    // {"pewpew":1,"ololo":2,"1":3,"0":4,"1000000000":5,"10000000000":6}
    

    Мораль: не мешайте стоковые ключи с числовыми. Не опирайтесь на порядок добавления элементов в «Хэш».
    • +1
      Не знал о таких тонкостях, но почти во всех языках, включая js, хеши (в данном случае объекты) не гарантируют определенного порядка прохода по ключам, работая с ключами, как с неупорядоченным множеством.
      • +1
        В настоящее время во всех браузерах (и в Node.js также) ключи хэша выводятся в том порядке, в котором они были вставлены в объект, за исключением поведения Хрома и Оперы с числовыми ключами. Поэтому для получения ключей в порядке их поступления достаточно следовать двум простым правилам:

        • Имя ключа не должно начинаться с цифры (или со знака «+» или «−» с последующею цифрою).
           
        • Один и тот же ключ не должен добавляться в один и тот же объект дважды.

        (Правила взяты из README-файла от библиотеки jParser.)
        • +1
          С Ваших слов очень интересно получается. И Chrome и Node.js используют V8 в качестве js-движка, но при этом работают с хэшами по-разному.
          • 0
            Я тоже сначала так прочёл, но с третьего раза таки понял товарища Mithgol. Они оба работают одинаково. И надо следовать двум правилам. Но написано странно, согласен.
          • 0
            1) не забывайте про версии
            2) не забывайте про оптмизации
            а еще запустите node --v8-options ;)
  • +9
    Сижу и тихо млею. Неужели кто-то до сих пор использует числа с плавающей точкой для работы с деньгами?

    Единственно правильная реализация денежных операций — работать с самой маленькой денежной единицей, можно даже в абстрактной. Для отображения используются таблицы трансформаций в более крупную валюту.

    В странах Европы принято деление 1:100, т.е. рубль состоит из 100 копеек, гривна = 100копеек, евро = 100 центов. Значит операции производятся в копейках, рубль получается путем деления на 100 нужной суммы.

    В Англии до 1971 использовалась более сложная схема 1 фунт = 20 шиллингам, шиллинг = 12 пенсов. Соответственно должна составляться таблица последовательных преобразований из пенсов в фунты, так как цена могла быть отображена в виде 1 фунт, 2 шиллинга, 4 пенса.

    Некоторые страны имеют соотношение 1 к 1000, например ливийский динар состоит из 1000 дирхамов, а на Мадагаскаре и в Мавритании соотношение 1 к 5. В Японии нет деления на более мелкие валюты, там все операции производятся в иенах.

    Вы все еще используете вещественные числа для рассчета валют? Тогда рефакторинг ждет вас! :)
    • +4
      ну давайте положим 1 рубль на счет и начнем ежедневно добавлять сотые доли процента %)
      • 0
        Поэтому в банках добавляют еще две цифры запаса (то есть считают всё в сотых долях копеек), а выводят с округлением до целых копеек.
        • +6
          в банках используют особый денежный формат 21:5 — т.е. 21 цифра для целых частей, и 5 для дробных, как правило более 4 знаков после запятой при делении валюты или начисления % не используется, а последний 5 разряд после запятой используйте как корректирующий для случаев проблем округления, также дополнительно используется расширенные правила округления (чаще по IEEE, но иногда и по локальным стандартам — для РФ — стандарт ЦБ РФ и МинФин РФ)
          • 0
            Во!
            А то ходють тут любители рефакторинга и махают своим единственно правильным!
            • 0
              Я не понял к чему Ваш комментарий :) но обычно используют так, конечно Я не могу гарантировать что ВСЕ системы работают таким образом — но по стандартам обычно принято таким образом, а фин. секторе не принято их нарушать.
          • 0
            Есть один нюанс, для всех операций используется decimal floating point арифметика, в которой число хранится в целочисленке.
        • 0
          ну вот есть у меня на счете 99 рублей и 99.98 копеек, выводить будет 100 рубликов, а снять их не даст, ибо с нужной точностью их нету
          • 0
            вообще-то нет — обычно запрещено округлять подобным образом. в случае «грязного» округления дробная часть отбрасывается вовсе и Вам тогда отобразят 99, ОДНАКО «грязное» округление уже нигде не используется, если только за исключением каких либо аггрегированных показателей в регистрах накопителях для целей внутреннего учета.
    • 0
      Приходится :)
      Например, пользователь хочет перевести 36.3 USD на счет. Конечно, мы переводим эту сумму в центы, но в итоге получится float :)
      36.3 * 100 = 3629.9999999999995
      • 0
        36.3 * 10000 / 100 = 3630
        • 0
          Предпочитаю Math.round в таких случаях
  • 0
    Еще, на всякий случай напомню одну особенность которую данный программист не учел:
    Хотя умножение/деление на 10 решает проблему
    >>> (0.7*10 + 0.1*10)/10
    0.8
    но вот незадача
    >>> (0.07*10 + 0.02*10)/10
    0.09000000000000001

    Не нужно всегда думать что у вас числа будут только с десятками, даже одна копейка при таких арифметических действиях может выйти в серьезную проблему.
    • –1
      Это проблема не яваскрипта, а особенность работы компьютера с float. floating-point-gui.de/
      • –1
        точнее реализации в JS движках.
      • +1
        я про javascript ничего не говорил, я говорил про умножение на 10, это вообще не выход из положения, округлять надо а не страдать фигней
  • 0
    Чем-то напомнило старый добрый боян "...enough making fun of programming languages, let's talk about Javascript."
  • 0
    Дополню:

    • При арифметических null приводится к нулю. null+1===1 Это не то поведение, которого ждёшь от null после СУБД. Зачем нужен такой null, непонятно — вместо него кажется правильным использовать undefined.
    • Начиная с JavaScript 1.8.5 (Firefox 4), undefined неперезаписываемое, согласно спецификации ECMAScript 5.
  • +1
    Кто может объяснить это? Один раз увидел, и с тех пор меня мучают ночные кошмары.

    [] + [] = ""
    [] + {} = "[object Object]"
    {} + [] = 0
    {} + {} = NaN
    
    • +1
      • 0
        в статье не указан тот факт что движок V8 использует свой парсер и правила обработки операций над объектами.
        • 0
          Можно автору написать, чтобы дополнил.
          • 0
            автора оригинала Я не знаю к сожалению, а т.к. статья перевод — то думаю не нужно вносить правки (от себя).
    • +6
      //(1) [] + [] = ""
      [].toString() // -> "" - Array не является нативным типом - поэтому сериализуется черезе .toString()
      "" + "" // -> "" - сложение двух пустых строк равняется пустой строке
      
      //(2) [] + {} = "[object Object]"
      {}.toString() // -> "[object Object]" - {} является упрощенной формой new Object() 
      [].toString() // -> "" - Array не является нативным типом - поэтому сериализуется через .toString()
      "" + {} // -> "[object Object]" - сложение строки с объектом невозможно, результат Object
      
      //(3) {} + [] = 0 и {} + {} = NaN
      // тут есть одна особенность - результат зависит от движка
      // Mozilla Rhino, Opera Presto/2.12.388, Microsoft Trident
      {} + [] = 0.0 // действует правило конвертации пустых объектов (нативный тип + ненативный тип), массивы конвертируются в "", объект в 0. 
      // тоже самое будет если
      0 /*{}*/ + "" /*[]*/ = 0.0
      {} + {} = NaN // попытка приведения объекта к типу "number" приводит к NaN - что по сумме дает NaN т.к. оба аргумента неприводимы в тип доступный для складывания
      // Google V8 
      {} + [] = "[object Object]"
      {} + {} = "[object Object][object Object]"
      
      
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Посмотрите внимательно — ЗАВИСИТ от движка, разные движки по разному собирают внутреннее представление.
          Я поэтому и описал 2 вариант — (1) V8 (2) Все остальные движке (Rhino, Presto, Trident)
          • НЛО прилетело и опубликовало эту надпись здесь
            • 0
              вообще-то Вы своим комментов наоборот это подтвердили :)
              значение пустой строки как раз "" а не 0
              // Rhino
              {} + 1 = 1.0 // {} = 0.0
              [] + 1 = 1 // [] = 0
              
              • НЛО прилетело и опубликовало эту надпись здесь
                • 0
                  1) установите JDK
                  2) запустите там jrunscript
                  3) ???
                  4) PROFIT!!!
              • НЛО прилетело и опубликовало эту надпись здесь
                • 0
                  // Rhino
                  +[] = 0
                  +{} = NaN
                  +"" = 0.0
                  
                  • НЛО прилетело и опубликовало эту надпись здесь
                    • 0
                      Нужно будет проверить в багтрекере Mozilla
              • НЛО прилетело и опубликовало эту надпись здесь
                • 0
                  Я тестирую в node.js 0.8.9 (Linux) + node.js 0.8.7 (Windows) если что :) результаты именно такие.
        • 0
          в этой-же статье не учтено что движок V8 по другому парсит и обрабатывает код — конкретный пример Я привел выше.
          P.S. а т.к. V8 относится и к Chrome и node.js — то автоматически к этому применимо. А вот у Safari используется модифицированный WebKit с другим движком.
      • +1
        {}+{}
        В этом примере нет операции сложения. Это пустой блок и «плюс пустой объект» (как +2). Приведение пустого объекта к числу дает NaN.
        • 0
          ОК. А теперь упакуйте этот код в функцию и удивитесь ;)
          • 0
            Как именно?
            • 0
              (function() { return ( {} + [] ); })(); // [object Object]
              (function() { return ( {} + {} ); })(); // [object Object][object Object]
              
              • +1
                return требует вычисление выражения. А в вышеописанном случае это просто token, который парсер принимает как есть.
                • 0
                  ОК, Вы часто используете выражения описанные выше в Global Scope?
                  • 0
                    Я вообще такие выражения не использую. Но они носят чисто академический интерес.
    • +2
      Углубльсю немного в один из примеров.
      Завист все от того места, куда этот экспрешн будет вставлен:
      {} + ""; // 0
      "" + {}; // "[object Object]"
      

      Дело в том, что парсер воспринимает в 1-м примере скобки как пустой блок
      вот так:
      {;} 
      +""; // 0
      
      Во втором случае скобки — литерал объекта т.к. стейтмент плюс еще не закрыт.

      То как воспринял данный код браузер можно понять, используя вот такой хак Function.prototype.toString();
      (function(){{}+""})+"";
      /*FF распарсил эту функцию вот так:
      "function () {
          + "";
      }"*/
      
      /*Опера
      "function() {
      	{}
      	+""
      }"
      */
      
      /*Хром
      "function (){{}+""}"
      */
      
      /*Сафари
      "function () {{}+"";}
      */
      

      А вот в этом случае оба блоки
      {2+1;} + ""; // 0 - аналогично первому примеру
      "" + {2+1;}; // Syntax error - пытается распарсить блок {2+1;} как литерал объекта
      

      Если поставить наше самое первое выражение в стейтмент (реальное условие), то парсер будет думать, что фигурные скобки это литерал объекта из-за того, что стейтмет var еще не закрыт.
      var a = {} + ""; // "[object Object]"
      

      Проще говоря, эти случаи предельно синтетические и в реальности их словить нельзя.
      • 0
        Я ловил в реальном коде, после обфускации и минификации ;)
        • 0
          Пример кода в студию. (желательно на Gist)
  • 0
    Пара дополнений.

    1.
    оператор typeof используется довольно часто, поэтому лучше запомнить

    Так как в JS, всё — объекты, лучше использовать более универсальный и логичный оператор instanceof. Им можно сверять типы любых объектов, например:
    [] instanceof Array // true
    

    Для проверки на нулевые значения, лучше использовать прямые сравнения, вроде
    var === undefined
    


    2.
    Значение Infinity также имеет свои особенности:
    Infinity + 1 == Infinity // true
    

    Существует как +Infinity, так и -Infinity.

    3.
    Если запрашивать не объявленную переменную, то будет ошибка.
    someVar; // error
    

    Но если запрашивать туже переменную, через свойство объекта, то ошибки не будет.
    window.someVar; // undefined
    

    В JS, всё — свойство какого либо объекта.
    • 0
      Из-за особенностей работы оператора instanceof такая операция не пройдет, если вы будете проверять инстанс у массива во фрейме.
      frame.contentWindow.var instanceof Array;
      

      В этом случае используют особенность алгоритма Object.prototype.toString и получают «Тип» из [[Class]]

      var === undefined
      

      Это требование зависит как от кодстайла так и от алгоритма. Однозначано так говорить не хорошо.

      В JS, всё — свойство какого либо объекта.
      Вы про какие объекты сейчас говорите? Даю код исключительно для уточнения ваших слов.
      function a() {
          var a; // свойство какого объекта переменная а?
      }
      

      Если запрашивать / Но если запрашивать
      тут важно понимание операций Identifier Resolution и Property Accessors, а не их последствий :)
      • 0
        Вообще с введением Harmony разницы особой не будет, можно будет поставить прокси на GLOBAL область и ловить исключения несуществующей переменной в рантайме.
        В JS действительно — ВСЕ свойства какого либо объекта, просто не ко всем контекстам есть доступ — например к локальным переменным + без сохранения контекста видимости код с функцией a действительно будет «волшебными грибами».
        От себя добавлю — сделал библиотеку typeis(«typename»)(object) — это позволяет ВСЕГДА гарантированно проверять тип объекта без лишних проблем, в т.ч. и проблема несуществующих переменных решает :)
        • 0
          ловить исключения несуществующей переменной в рантайме
          Ошибку лучше предотвратить, чем обработать исключение.

          можно будет поставить прокси на GLOBAL область
          Вы уверены в понимании идеи Proxy? :) Пример, пожалуйста (Proxy сейчас есть в Firefox).

          В JS действительно ВСЕ свойства какого либо объекта
          Это я прекрасно понимаю. Хотел уточнить знание автора комента выше, а тут вы :)

          сделал библиотеку typeis(«typename»)(object)
          Из многолетнего опыта — мне всегда хватало typeof и Array.isArray. Зря вы так GC мучаете своими «красивыми» функциональными интерфейсами :)
          typeis("typename")(object);
          
          type(object) === "typename"; // faster - true, better IMO
          
          • 0
            1) ОК, если Вы такой маг и все пишите сразу отлично — то спорить не буду, мне лично проще вести трассировку подобным образом
            2) идея Proxy это расширенный полиморфизм и метапрограммирование
            3) сорри, не распознал сарказма
            4) GC не не жалко, можно и вручную собрать — в моем вариант просто функционал шире, т.к. он позволяет верифицировать ОЧЕНЬ много custom типов — например Country (проверка страны по ISO), время, составные типы по паттернам и т.д. — также это позволяет ввести НЕнативный тип и использовать его 1 строкой с расширенной валидацией, теоретически можно и перегрузить typeof/type — но как мне кажется лучше не стоит. (например перегрузки и расширения свойств Object плохо совместимы с ранними версиями express.js)
            P.S. Я например проверяю размеры typeis("<10KB")(number) или например так typeis("<MAX_FILE_SIZE")(number) — так весьма удобно ИМХО.
            • 0
              Я вашу позицию понял, спасибо за дополнение :)
          • 0
            Вообще то подумал про window, если говорить про «всё — свойство какого либо объекта». Хотя window и является сам свойством window, но не считаю что такой трюк равняет window с другими объектами.

            По сути, должен быть один корневой элемент, и от этого не уйти. Даже строя кольцо из указателей, нужно помнить что начинается его строительство с одного элемента. Первый же созданный указатель на этот элемент из вне JS среды и является переменной не являющейся свойством какого либо объекта.
            • 0
              Хотя window может и неудачный пример, но тот или иной базовый элемент всё равно должен быть.
        • 0
          Код о Harmony Proxy и Identifier Resolution.
          var windowDescriptor = Object.getOwnPropertyDescriptor(this, 'window');
          // {"configurable":false,"enumerable":true,"value":window,"writable":false}
          
          this.window = Proxy.create({}); // (А) "writable":false - ничего не получится
          
          window.__noSuchMethod__ = function () {
              return function () {};
          }
          
          window.b(); // OK __noSuchMethod__ тут сработает
          
          b(); // (B)  ReferenceError: b is not defined
          
          • 0
            Я работаю с node.js и тут можно использовать циклическую переменную GLOBAL — например переопределив ее так
            (function(){ GLOBAL = Proxy.create({}); })();
            
            • 0
              Попробовать, к сожалению не могу.
              » node -v
              v0.8.8
              » node --harmony_proxies
              > GLOBAL = Proxy.create({get: console.log.bind(console)});
              
              util.js:120
                var ctx = {
                ^
              RangeError: Maximum call stack size exceeded
              


              Но даже если мы перепишем GLOBAL на проксю, то от Identifier Resolution это не поможет.
              » node
              > GLOBAL = {a: 1};
              { a: 1 }
              > a;
              ReferenceError: a is not defined
                  at repl:1:1
                  at REPLServer.self.eval (repl.js:111:21)
                  at rli.on.e (repl.js:260:20)
                  at REPLServer.self.eval (repl.js:118:5)
                  at Interface.<anonymous> (repl.js:250:12)
                  at Interface.EventEmitter.emit (events.js:88:17)
                  at Interface._onLine (readline.js:199:10)
                  at Interface._line (readline.js:517:8)
                  at Interface._ttyWrite (readline.js:735:14)
                  at ReadStream.onkeypress (readline.js:98:10)
              > 
              

              Вобщем проксю не подмешать без патчинга ноды.
              • 0
                1) ЕСТЕСТВЕННО
                console.log.toString() // 'function () {\n  process.stdout.write(util.format.apply(this, arguments) + \'\\n\');\n}' где process ссылается на GLOBAL
                

                Для корректной перегрузки GLOBAL Вам нужно сделать 3 действия:
                1) скопировать оригинальный инстанц GLOBAL в замыкание
                2) создать Harmony Proxy Handler с реализацией ВСЕХ перегружаемых методов
                3) передать в качестве опционального аргумента оригинальный инстанц GLOBAL из замыкания
                только после этого у Вас не будет циклического переполнения стэка, и вообще Раэйн сказал что хочет все что можно переписать на JS из ядра node.js поэтому все функции ядра viсe-versa являются высокоуровневыми абстракциями. (например тот-же process.nextTick) ;)

                Вообще-то нет, и если уже на то пошло — то не ноды, а движка V8, т.к. политика Joyent — не вносить патчи в V8, даже если кто-то вносит такой коммит — то его не принимают, и отправляют его на googlegroup чтобы был патч для основной ветки V8 на офф. сайте — при условии что это критический баг.
                • 0
                  Проблему то мы не решили. У нас до сих пор `ReferenceError: a is not defined`
              • 0
                кстати по поводу
                GLOBAL = {a: 1} 
                

                было глупо т.к.
                typeof(GLOBAL) // "[object global]" 
                

                это особый тип и его нельзя заменять — можно только оборачивать через Proxy

                например:
                this.GLOBAL["a"] = 1 // 1
                a // 1
                
                • 0
                  Ну понятное же дело, что global :) Мы же сейчас говорим об подмене GLOBAL проксей — те попытке подменить/пропатчить объект на который ссылается GLOBAL.

                  По старой спеке первый параметр прокси target — это просто хранилище полей для прокси. Прокся его не пропатчивает и при работе с ним напрямую ничего не меняется. В новой все вроде бы так и осталось.

                  Show me the code ;-) пока я не верю в возможность влияния прокси на Identifier Resolution
          • 0
            в этом случае суперглобальные объекты будут обрабатыватся через Harmony Proxy handler
      • 0
        Интересно. instanceof только с кроссфреймовыми запросами себя так ведёт, или ещё в каких то ситуациях? Это баг реализации или это следует из стандарта?

        Необходимость кроссфреймовых JS запросов очень редка, так что знать полезно, но бояться из за этого пользоваться instanceof не стоит.

        Это требование зависит как от кодстайла так и от алгоритма. Однозначано так говорить не хорошо.
        Не настаиваю, на том что это лучший вариант, это лишь дополнение к instanceof для проверки пустых типов. Хотя в конечном итоге всё сводится к кодстаилу и конкретной ситуации.

        Вы про какие объекты сейчас говорите?
        Да, похоже преувеличил, имея в виду общий случай, в которой дело имеется либо со свойствами объекта «this.var», либо с локальными переменными, объявленными и используемыми в пределах одного экрана, когда нет необходимости помнить была ли переменная объявлена. Реальный случай, когда действительно может остро встать проблема ошибок из-за не объявленных переменных — это десяти экранные функции со множеством переменных, или глобальная область определения, в которой можно воспользоваться «window.var».

        тут важно понимание операций Identifier Resolution и Property Accessors, а не их последствий :)
        Теория и практика — две части целого.
        • 0
          Это баг реализации или это следует из стандарта?
          Это фича. instanceof использует цепочку прототипов, которая в разых фреймах разная. «Адреса» объектов разные, а функционал одинаковый.

          Необходимость кроссфреймовых JS запросов очень редка
          Не спорю. Лучше вообще использовать Array.isArray ;-)
          • –1
            Array.isArray в ИЕ с 9-й версии

            если уж совсем кроссбраузерно, то наверное как-то так будет лучше:
            toString.call(a) == "[object Array]"
            
        • 0
          Интересно. instanceof только с кроссфреймовыми запросами себя так ведёт, или ещё в каких то ситуациях?

          Если писать UserJS плагины, то тоже такая фигня. В хроме есть window.Array и unsafeWindow.Array. И внутри приложения используется именно второе. То есть если вы хотите написать плагин — вам придётся использовать unsafeWindow.Array(1,2,3) вместо [1,2,3].
  • –1
    www.destroyallsoftware.com/talks/wat
    Советую посмотреть, в этой короткометражке вы узнаете чему равно
    {} + {}
    {} + []
    [] + {}
    [] + []
    

    и прочие радости яваскрипта :)
    • 0
      Сударь, вы уж очень невовремя.
      • 0
        Увы, не осилил все комменты, так что пусть это просто полежит здесь.
    • 0
      А зачем плюсовать объекты?
      • +1
        Этот вопрос я каждый раз задаю себе, когда вижу очередную ссылку на видео про «WAT» :)
  • +1
    (function(){
            var uscope = window, //window['utils'] = (window['utils']?window['utils'] : {}) ,//
                types = "Boolean Number String Function Array Date RegExp Object".split(" "),
                is = function (type, obj) {
                    var clas = Object.prototype.toString.call(obj).slice(8, -1);
                    return obj !== undefined && obj !== null && clas === type;
                };
            for(var i=0;i<types.length;i++)
                uscope['is'+types[i]] = (function( type ){
                    return function( obj ){return is( type, obj )};
                }( types[i] ));
        }());
    

    и получаем методы:
    isBoolean
    isNumber
    isString
    isFunction
    isArray
    isDate
    isRegExp
    isObject

    если желаем — то в window.utils…
    одна беда — при разработке использовать не удобно, среда их «не знает», т.к. они генерятся в runtime
  • 0
    За разъяснениями по таким вещам ->хорошо ходить на MDN. И стандарты упоминают, и совместимость, не только со своими продуктавми

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.