Pull to refresh

Comments 28

А разве корректно добавлять свои методы в код во время тестирования?
В случае с JavaScript, имхо, самый правильный вариант — вообще не использовать «приватные» переменные, в таком виде, как к ним привыкли в «обычных» языках, только на уровне соглашений.
Я пришёл к JS с C++/C# и отсутствие контроля доступа полностью выбивает меня из колеи. Поэтому я использую единственный метод «восстановить справедливость» — closure. Я видел это использование во множестве мест и в том числе в известных фреймворках.
По поводу добавления своих методов: Я новичок в Unit Test, но видел разные варианты. В идеале тестируется только открытый API, но на практике часто нужно лезть в кишки. В «правильных» системах компилятором/средой предусмотрен режим для тестов, в котором private становится доступным для определенных програмистом модулей как protected и позволяет субклассирование и переопределение. К сожалению JS этого делать не умеет, или я не знаю способа добиться этого от него. Пришлось придумать костыль.
Ну все (лично мне) известные js-фреймворки далеки от идеологии, которая применятся в C++/C#, а приватные переменные в JS тут чуть не те, что в этих языках.

Имхо, в данном случае костыль совершенно не оправдан. Код для тестирования не должен мешаться среди кода, тем более, что eval — это отличная среда для трудноотлавливаемых ошибок.

ps. К приватным переменным можно обратиться интересным способом:
var foo = (function () {
  var y = 20;
  return function () {
    alert(y);
  };
})();
 
foo(); // 20
alert(foo.__parent__.y); // 20
foo.__parent__.y = 30;
foo(); // 30


Но вы таки подумайте о целесобразности использования приватных переменных =) Они не так нужны, как привыклось.
Вырванный из контекста, Ваш пример не работает. Насколько я знаю js, никакого __parent__ там нет :)

По сути вопроса, да, согласен: приватность переменных в js должна достигаться на уровне соглашений, а не с использованием замыканий.
Наверняка вы проверяли в FF4. __parent__ убрали начиная с FF4. Однако вы можете убедиться, что пример работает, поставив ФФ 3.6.
Ещё в ФФ каких-то древних версий был подобный баг — функции eval можно было передать контекст исполнения функции.
Проверил в хроме, ие, фф 4. Не работает. Я понимаю, у мозиллы своё видение javascript :) Но mozilla != javascript

В node такой фокус не пройдет.
Попробовал ваш код в 3-х браузерах: IE 9, Chrome 10, FF4. Ни один не видит __parent__ и бросает исключение Uncaught TypeError: Cannot read property 'y' of undefined.

Я за и совершенно не спорю, что тест-код должен жить отдельно, только не представляю как. Если бы ваш трюк сработал, я бы с удовольствием переписал бы свою систему под него — это намного чище и правильней.

Использование прав доступа всегда необходимо, если ты работаешь более чем с одним человеком в группе. Бегать и отлавливать зверей из за того, что кто-то влез куда не надо и решил изменить неизменяемое, себе дороже.
Мы передаем пустой объект в качестве контейнера для возвращаемого значения, поскольку return не может быть использован в функции eval().

  ...
  this.evalInContext = function(cmd){
    return eval(cmd);
  }
  ...

function getVar(obj, name){
    return obj.evalInContext(name);
}

Но вообще лучше подобное не практиковать, смирится с доступностью свойств и использовать closure, что Вы и делаете. Отсутствие контроля над видимостью выбивает из колеи лишь по началу, потом входишь в кураж и получаешь удовольствие ;)
Спасибо, о таком варианте я не подумал. А вот в самой cmd использование return выдает ошибку. В любом случае я использовал свой подход для задания списка переменных и их получения одним вызовом функции. Тогда пустой объект наполняется переменными из списка.

И все же я не очень понимаю в чем реальные преимущества использования декорированных членов перед использованием скрытых переменных? Ну кроме нашего случая. Насколько я понимаю скорость и использование памяти если и отличаются, то незначительно. Что же тогда?
Ну это холиварный вопрос, на самом деле. В первую очередь это спор между двумя разными подходами — объявление всего класса в конструкторе, или через прототипирование.
Тот подход, которым вы пользуетесь сейчас, представте на примере C#. У вас есть конструктор и в нём вы присваиваете анонимные функции переменным, делая таким образом методы. Подход имеет право на жизнь, но многими осуждается. Прототипирование — ближе к языку.

Более того, Google и Yandex в своих api используют именно прототипирование и они даже не используют подчёркивание для некоторых приватный свойств. Всё описано в апи и базируется на доверии между программистами. Пока оно сбоя не давало)

К примеру:

Яндекс-карты:



Гугл-карты:

Пока оно сбоя не давало
Увы у меня прямо противоположный опыт. Если чего не спрячешь — сразу влезут и испортят, даже если есть подробная документация (Кто её читает?)

А исходя из необходимости иметь права доступа в языке не предоставляющем этих возможностей приходится прибегать к кривому методу, обходя стороной правильный подход с прототипами. Я сам за него, но увы не всегда получается. Язык предоставляет одно одеяло. Потянеш в одну сторону — права доступа, но не ООП. Потянеш в другую — ООП, но анархия — мать порядка.

Очень хочется верить, что когда — нибудь введут наконец классы как ActionScript и все будет в шоколаде.
Увы у меня прямо противоположный опыт.

Увы, но опыт Google и Yandex — ценнее и авторитетнее вашего, имхо)
Конечно опыт этих фирм наше всё, но к сожалению я у них не работаю, а работаю у нас, где этот опыт опровергается на практике каждый день. И весь его авторитет остается где-то там за стенами нашей фирмы. Я бы и рад им воспользоваться, но не условия дают. (
Все зависит от того какая задача. Если у Вас простой класс, с двумя-тремя полями и эти поля обязательно задаются уникальными значениями при создании экземпляра — разницы в скорости не ощутите.
А если у Вас сложный класс, с двумя-тремя _десятками_ свойств и методов (или больше), имеющие цепочку наследования, и при этом большинство из свойств не инициализируются при создании (имеют значение по умолчанию) — то прототипный подход значительно выигрывает (если у вас конечно не синглтон). При прототипном подходе большинство экземпляров разделяют одни и те же данные, и уж точно разделяют один и тот же код «методов», то есть под функции-замыкания не выделяется память, не тратится время на парсинг и обработку кода.
Разделение данных, если они mutable объекты, вообще самоубийственный вариант, а вот с разделением функций согласен, более правильный подход, хотя доступ по цепочке прототипов, говорят, очень тяжелая по времени операция.
По поводу парсинга кода замыканий я читал, что собственно код не парсится повторно, просто данные объектов вызова функций сохраняются и используются как данные замыкания. При этом действительно используется лишняя память, но вот (опять же по слухам) доступ к переменным замыкания быстрее доступа к переменным цепочки прототипов, включая обращение к функциям.
Тесты не опровергли слухи о том, что «доступ к переменным замыкания быстрее доступа к переменным цепочки прототипов, включая обращение к функциям» и, при этом, подтвердили слухи, что «используется лишняя память». Между прочим, что цепочку прототипов проходить, что цепочку контекстов — какая разница? В одном случае речь поиск по "__proto__", в другом — по "[[scope]]".
Не совсем так… Будет проще объяснить на примере
var Point = function(){};
Point.prototype = {
  x: 0,
  y: 0,
  kind: 'point',
  setCoords: function(x, y){
     this.x = Number(x);
     this.y = Number(x);
  }
}
var Appointment = function(date){
  if (date)
    this.date = date;
}
Appointment.prototype = new Point();
Appointment.prototype.kind = 'appointment';
Appointment.prototype.date = null;
Appointment.prototype.setDate = function(){ .. };
...

Здесь два класса, Appointment унаследован от Point.
Смысл в том, что все экземпляры Point и Appointment создаются с дефолтовыми значениями x, y и kind, а Appointment еще может задать при создании date. Так вот значения Вы задаете уже после создания объектов (хотя конечно это можно и в конструкторе сделать). Пока вы не зададите экземпляру значение для свойства, он будет брать это значение из прототипа.
var a = new Point();
var b = new Point();
a.x === b.x;  // указывают на одно значение 0
Point.prototype.x = 10;
alert(a.x); // будет 10
a.x = 1; 
b.x = 1;
// после этого у экземпляров будет собственные значения
delete a.x; // а после этого будет опять браться из прототипа

Конечно пример может показаться не очень удачным, так как для Point скорей всего Вы захотите сразу задавать x и y, но у этого класса может быть много других свойств; для примера я добавил kind который, например, может определять тип маркера на карте — так вот вам не нужно для всех экземпляров задавать значение этого свойства.
По поводу цепочки прототипов, да, по ней делается проход. Но только если свойства (ключа) нет в экземпляре. Сначала поиск делается в прототипе, потом в следующем прототипе и т.п.
Но! Когда Вы делаете что-то вроде Appointment.prototype = new Point(), то тем самым копируете все свойства (ключи) супер класса. Таким образом, если свойства нет в прототипе, то оно скорей всего не будет найдено и в последующей цепочке. Так что стоит определить все возможные свойства (к которым возможно обращение) в прототипе (с дефолтовыми значениями, пусть и null/undefined), и обращение к ключам не будет «тяжелым» (разве что к ключам, которых вообще нет, что можно считать за ошибку).

ЗЫ Если Вы используете eval то никаких оптимизаций не делается, и код парсится регулярно. Для замыканий, да — меняется только контекст, при этом все равно создается дополнительная структура и выделяется память. Если у вас будут тысячи объектов — это накладно.

ЗЫ2 Одно дело слухи, другое дело собственный опыт. Все сильно зависит от сочетания подходов.
Спасибо, конечно, но азбуку прототипирования я прочитал, а заодно и Кроукфорда и javascript.ru. Там очень много полезного. И в частности то, что Appointment.prototype = new Point(); есть зло. А делать надо так:

var Tmp = function() { }
Tmp.prototype = Point.prototype
Appointment.prototype = new Tmp()
Appointment.prototype.constructor = Appointment;


Ну и конечно в конструкторе наследника нужно вызвать конструктор базового класса. Подробности и объяснение смотри на javascript.ru/tutorial/object/inheritance#nasledovanie-na-klassah-funkciya-extend

Но дело даже не в этом. Я говорил о mutable объектах. Пример:

function Rect(){}
Rect.prototype.pt1 = new Point();
Rect.prototype.pt2 = new Point();

var r1 = new Rect();
var r2 = new Rect();

r1.pt1.x = 100;
r2.pt1.x = 200;

alert(r1.pt1.x); // 200


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

Re: ЗЫ: eval я использую не в рабочем коде, а как вспомогательную функцию для тестов и только по той причине, что язык/система не предоставляет других средств. Поэтому постоянный парсинг кода меня не волнует. Объекты, требующие создания большого количества экземпляров, я пишу в упрощенном варианте или использую прямое создание объекта только с данными (типа структуры в С#)
Re: ЗЫ2: согласен на 100 %.
Не знаю, как в Java и C#, но в php в объекте можно создавать только примитивные значения, то есть, скажем, такое не прокатит:
<?php
class Rect {
    private $from = new Point();
}
Я рад за PHP, но мы вроде бы о JavaScript говорим. А в С# действительно в объекте можно создавать все, включая другие объекты и самое приятное, что они обрабатываются корректно.
А если в конструкторе объекта будет выполнятся какой-то код, например запись лога в базу данных? Или конструктор при этом не вызывается?
Вы про С#? Ну ладно, давайте пару слов о нем.
Если вы вызываете конструктор объекта, то соответственно он исполнится вместе со всеми действиями внутри. При создании объекта, содержащего другие объекты (обычная распространенная практика в С#, как в ОО языке) можно инициализировать (создать) эти объекты в момент конструирования папы или другими средствами, например lazy конструированием по мере надобности. Поскольку любой объект сохраняется по ссылке на него, то можно иметь неинициализированную ссылку до тех пор, пока он вам не понадобится.
Если слишком простые вещи написал — уж извини, дезориентировало ваше лирическое отступление «Я новичек в JavaScript...» ;)
И в частности то, что Appointment.prototype = new Point(); есть зло. А делать надо так:

Так делать необязательно, все зависит от того как вы создаете классы. В нашем фреймворке мы используем что-то похожее на то, что есть у Дмитрия Котерова dklab.ru/chicken/nablas/40.html, или John Resig'а ejohn.org/blog/simple-javascript-inheritance/ и других (к сожалению, больше с ходу ссылок не дам).

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

Re: Re: ЗЫ Ваши цели и задачи я понял, прочитав ваши комментарии ниже, после того как запостил свой. Возражения снимаются :)
Я действительно считаю себя новичком, поскольку пишу на JS всего 4-ый месяц. Я хорошо (надеюсь) знаю теорию, но ещё слаб в практике. И ещё я три месяца писал проект на ActionScript 2, но там были классы, хотя под ними скрывался тот же стандарт.

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

Из первой ссылки я понял, что метод там описанный, очень далек от совершенства, хотя и делает то же, что и мой, только вывернутым образом. Он не работает корректно, если вы в качестве базового используете созданный не им же объект (например из библиотеки). Тогда вызов clazz.prototype = new parent; зовет реальный, а не специальный конструктор и тут уж как повезет. Он так же работет некорректно, если в цепочке более двух классов. Вызов базового класса всегда идет к самому базовому, а не к тому от кого наследовался (хотя конструкторы работают корректно). Я люблю более надежные решения.
Проброс кода в контекст исполнения нарушает всю структуру и логику программы. Если вы делаете подобный проброс, то вы явно делаете что-то не так — поверьте есть пути решения ваших проблем. Если вам необходимо сокрыть перменные — скрывайте их в глобальном замыкании, если вы желаете ограничить область видимости и при этом сохранить красивую структуру, то вам необходимо экспортировать так называемую «песочницу».

Код все объясняет:
(function (global, document, jQuery, undefined) {
         
    var localVar,

    YourClass = {
        a: function () {},
        b: function () {},
        c: function (a, b, c) {}
    };

    // Экспортируем часть объекта во все платформы (node.js + браузер)
    global.YourClass = {
        // Вариант экспорта 1 - элегантный
        a: YourClass.a.bind(YourClass),
        // Вариант экспорта 2
        c: function (a, b, c) {
            return YourClass.c(a, b, c);
        }
    };

}(typeof exports === 'undefined' ? this : exports, document, jQuery));
Пока я не вижу возможности доступа к локальным переменным при необходимости протестировать часть этого кода, например неэкспортируемую функцию.
Кроме того особый доступ в скрытый контекст необходим только, если вам необходимо протестировать что-то вырезанное из этого контекста. Mocking собственно и создает эту искусственную обрезку.
> Пока я не вижу возможности доступа к локальным переменным при необходимости протестировать часть этого кода
Зачем тестировать локальные переменные и функции отдельно? Т.е. зачем тестировать приватные методы и свойства?

Я вас уверяю, что юнит тестами можно покрыть 100% такого кода.
Не всегда. К примеру мне дали задание (собственно из него и родилась идея) протестировать touchScroll плагин к jQuery. Внешняя у него только одна функция, которая подключает к элементу функционал плагина. А внутри добрый десяток сложных и ещё десяток простых функций, которые мне и надо было протестить. В основном тесты писались для тестирования, поскольку плагин сильно дорабатывался.
Sign up to leave a comment.

Articles