Существуют ли законные варианты использования оператора "with" в JavaScript?
Комментарии Алана Шторма в ответ на мой ответ относительно with
заявление заставило меня задуматься. Я редко находил причину использовать эту особенность языка и никогда не задумывался над тем, как это может вызвать проблемы. Теперь мне интересно, как я мог бы эффективно использовать with
, избегая при этом своих подводных камней.
Где вы нашли with
Скажите полезно?
33 ответа
Сегодня мне пришло в голову другое использование, поэтому я с энтузиазмом искал в Интернете и нашел упоминание об этом: определение переменных внутри области действия.
Фон
JavaScript, несмотря на его внешнее сходство с C и C++, не включает переменные в блок, в котором они определены:
var name = "Joe";
if ( true )
{
var name = "Jack";
}
// name now contains "Jack"
Объявление замыкания в цикле является обычной задачей, где это может привести к ошибкам:
for (var i=0; i<3; ++i)
{
var num = i;
setTimeout(function() { alert(num); }, 10);
}
Поскольку цикл for не вводит новую область видимости, то же самое num
- со значением 2
- будут разделены всеми тремя функциями.
Новая сфера: let
а также with
С введением let
Заявление в ES6 позволяет легко вводить новую область видимости, когда необходимо избежать этих проблем:
// variables introduced in this statement
// are scoped to each iteration of the loop
for (let i=0; i<3; ++i)
{
setTimeout(function() { alert(i); }, 10);
}
Или даже:
for (var i=0; i<3; ++i)
{
// variables introduced in this statement
// are scoped to the block containing it.
let num = i;
setTimeout(function() { alert(num); }, 10);
}
Пока ES6 не станет универсально доступным, это использование будет ограничено новейшими браузерами и разработчиками, желающими использовать транспортеры. Тем не менее, мы можем легко смоделировать это поведение, используя with
:
for (var i=0; i<3; ++i)
{
// object members introduced in this statement
// are scoped to the block following it.
with ({num: i})
{
setTimeout(function() { alert(num); }, 10);
}
}
Цикл теперь работает так, как задумано, создавая три отдельные переменные со значениями от 0 до 2. Обратите внимание, что переменные, объявленные в блоке, не ограничены им, в отличие от поведения блоков в C++ (в C переменные должны быть объявлены в начале блок, так что в некотором роде это похоже). Это поведение на самом деле очень похоже на let
блочный синтаксис, представленный в более ранних версиях браузеров Mozilla, но не получивший широкого распространения в других местах.
Я использовал оператор with как простую форму импорта по областям. Допустим, у вас есть какой-то компоновщик разметки. Вместо того чтобы писать:
markupbuilder.div(
markupbuilder.p('Hi! I am a paragraph!',
markupbuilder.span('I am a span inside a paragraph')
)
)
Вы могли бы вместо этого написать:
with(markupbuilder){
div(
p('Hi! I am a paragraph!',
span('I am a span inside a paragraph')
)
)
}
Для этого варианта использования я не делаю никаких заданий, поэтому у меня нет проблем с неоднозначностью, связанных с этим.
Как указано в моих предыдущих комментариях, я не думаю, что вы можете использовать with
безопасно, независимо от того, насколько заманчиво это может быть в любой конкретной ситуации. Поскольку проблема здесь не рассматривается напрямую, я повторю это. Рассмотрим следующий код
user = {};
someFunctionThatDoesStuffToUser(user);
someOtherFunction(user);
with(user){
name = 'Bob';
age = 20;
}
Без тщательного изучения этих вызовов функций невозможно определить состояние вашей программы после выполнения этого кода. Если user.name
был уже установлен, теперь будет Bob
, Если он не был установлен, глобальный name
будет инициализирован или изменен на Bob
и user
объект останется без name
имущество.
Баги случаются. Если вы используете с вами, в конечном итоге это будет сделано и увеличит шансы вашей программы не удастся. Хуже того, вы можете столкнуться с рабочим кодом, который устанавливает глобал в блоке with, либо намеренно, либо через автора, не знающего об этой особенности конструкции. Это очень похоже на столкновение с провалом на переключателе, вы не представляете, задумал ли автор это, и нет способа узнать, приведет ли "исправление" кода к регрессии.
Современные языки программирования переполнены функциями. Некоторые функции, после многих лет использования, оказались плохими, и их следует избегать. в JavaScript with
это один из них.
Я на самом деле нашел with
заявление, чтобы быть невероятно полезным в последнее время. Эта техника никогда не приходила мне в голову, пока я не начал свой текущий проект - консоль командной строки, написанную на JavaScript. Я пытался эмулировать API-интерфейсы консоли Firebug / WebKit, где в консоль можно вводить специальные команды, но они не переопределяют никакие переменные в глобальной области видимости. Я думал об этом, пытаясь преодолеть проблему, о которой упоминал в комментариях к превосходному ответу Shog9.
Чтобы достичь этого эффекта, я использовал два оператора with для "наложения" области действия за глобальной областью действия:
with (consoleCommands) {
with (window) {
eval(expression);
}
}
Самое замечательное в этом методе заключается в том, что, помимо недостатков производительности, он не страдает от обычных страхов with
утверждение, потому что мы все равно оцениваем в глобальной области видимости - нет опасности того, что переменные вне нашей псевдоскопа будут изменены.
Я был вдохновлен опубликовать этот ответ, когда, к моему удивлению, мне удалось найти ту же технику, что и в других местах - исходный код Chromium!
InjectedScript._evaluateOn = function(evalFunction, object, expression) {
InjectedScript._ensureCommandLineAPIInstalled();
// Surround the expression in with statements to inject our command line API so that
// the window object properties still take more precedent than our API functions.
expression = "with (window._inspectorCommandLineAPI) { with (window) { " + expression + " } }";
return evalFunction.call(object, expression);
}
РЕДАКТИРОВАТЬ: Только что проверил источник Firebug, они соединяют 4 с заявлениями вместе для еще большего количества слоев. Псих!
const evalScript = "with (__win__.__scope__.vars) { with (__win__.__scope__.api) { with (__win__.__scope__.userVars) { with (__win__) {" +
"try {" +
"__win__.__scope__.callback(eval(__win__.__scope__.expr));" +
"} catch (exc) {" +
"__win__.__scope__.callback(exc, true);" +
"}" +
"}}}}";
Да, да и да. Существует очень законное использование. Часы:
with (document.getElementById("blah").style) {
background = "black";
color = "blue";
border = "1px solid green";
}
В основном любые другие DOM или CSS-хуки - фантастическое применение с. Это не значит, что "CloneNode" будет неопределенным и вернется к глобальной области видимости, если вы не пошли своим путем и решили сделать это возможным.
Скорость жалобы Крокфорда состоит в том, что новый контекст создается с помощью. Контексты, как правило, дороги. Согласен. Но если вы только что создали div и у вас нет какой-либо платформы для настройки вашего css и вам нужно вручную установить около 15 свойств CSS, тогда создание контекста, вероятно, будет дешевле, чем создание переменных и 15 разыменований:
var element = document.createElement("div"),
elementStyle = element.style;
elementStyle.fontWeight = "bold";
elementStyle.fontSize = "1.5em";
elementStyle.color = "#55d";
elementStyle.marginLeft = "2px";
так далее...
Вы можете определить небольшую вспомогательную функцию, чтобы обеспечить преимущества with
без двусмысленности
var with_ = function (obj, func) { func (obj); };
with_ (object_name_here, function (_)
{
_.a = "foo";
_.b = "bar";
});
Вряд ли это того стоит, так как вы можете сделать следующее:
var o = incrediblyLongObjectNameThatNoOneWouldUse;
o.name = "Bob";
o.age = "50";
Я никогда не использую, не вижу причин и не рекомендую это.
Проблема с with
заключается в том, что он предотвращает многочисленные лексические оптимизации, которые может выполнять реализация ECMAScript. Учитывая появление быстрых двигателей на базе JIT, эта проблема, вероятно, станет еще более важной в ближайшем будущем.
Это может выглядеть как with
допускает более чистые конструкции (когда, скажем, вводится новая область вместо обычной обертки анонимной функции или замена многословного псевдонима), но это действительно не стоит. Помимо снижения производительности, всегда существует опасность присвоения свойству неправильного объекта (когда свойство не найдено в объекте во введенной области видимости) и, возможно, ошибочно вводить глобальные переменные. IIRC - последний вопрос, который побудил Крокфорда рекомендовать избегать with
,
Visual Basic.NET имеет аналогичный With
заявление. Один из наиболее распространенных способов, которыми я пользуюсь, - это быстро установить ряд свойств. Вместо:
someObject.Foo = ''
someObject.Bar = ''
someObject.Baz = ''
, Я могу написать:
With someObject
.Foo = ''
.Bar = ''
.Baz = ''
End With
Это не просто вопрос лени. Это также делает для намного более читаемого кода. И в отличие от JavaScript, он не страдает от двусмысленности, так как вы должны поставить перед всем, на что влияет оператор, префикс .
(Точка). Итак, следующие два четко различимы:
With someObject
.Foo = ''
End With
против
With someObject
Foo = ''
End With
Бывший someObject.Foo
; последний Foo
в объеме за пределами someObject
,
Я считаю, что отсутствие различий в JavaScript делает его гораздо менее полезным, чем вариант Visual Basic, так как риск неоднозначности слишком высок. Кроме этого, with
по-прежнему мощная идея, которая может сделать для лучшей читаемости.
Ты можешь использовать with
представить содержимое объекта как локальные переменные для блока, как это делается с помощью этого небольшого шаблонизатора.
Я думаю, что очевидное использование в качестве ярлыка. Если вы, например, инициализируете объект, вы просто сохраняете набор "ObjectName". Вроде как "с-слоты" Лиспа, который позволяет вам писать
(with-slots (foo bar) objectname
"some code that accesses foo and bar"
что то же самое, что писать
"some code that accesses (slot-value objectname 'foo) and (slot-value objectname 'bar)""
Более понятно, почему это ярлык, чем когда ваш язык разрешает "Objectname.foo", но все же.
Использование "с" может сделать ваш код более сухим.
Рассмотрим следующий код:
var photo = document.getElementById('photo');
photo.style.position = 'absolute';
photo.style.left = '10px';
photo.style.top = '10px';
Вы можете высушить его до следующего:
with(document.getElementById('photo').style) {
position = 'absolute';
left = '10px';
top = '10px';
}
Я думаю, это зависит от того, предпочитаете ли вы разборчивость или выразительность.
Первый пример более разборчив и, вероятно, рекомендуется для большей части кода. Но большая часть кода в любом случае довольно ручная. Второй вариант немного более неясен, но использует выразительную природу языка для сокращения размера кода и лишних переменных.
Я предполагаю, что люди, которые любят Java или C#, выберут первый путь (object.member), а те, кто предпочитает Ruby или Python, выберут второй.
Имея опыт работы с Delphi, я бы сказал, что использование with должно быть последней мерой оптимизации размера, возможно, выполняемой неким алгоритмом минимизатора JavaScript, имеющим доступ к статическому анализу кода для проверки его безопасности.
Проблемы с областями видимости, с которыми вы можете столкнуться при свободном использовании оператора with, могут быть непростой задачей в a**, и я бы не хотел, чтобы кто-либо испытывал сеанс отладки, чтобы выяснить, что он... происходит в вашем коде только для того, чтобы узнать, что он захватил член объекта или неправильную локальную переменную вместо вашей глобальной или внешней переменной области действия, которую вы намеревались.
VB с утверждением лучше, так как ему нужны точки для устранения неоднозначности, но Delphi с утверждением - это заряженное ружье с hairtrigger, и мне кажется, что javascript достаточно похож, чтобы оправдать то же предупреждение.
Использование с не рекомендуется, и запрещено в строгом режиме ECMAScript 5. Рекомендуемая альтернатива - назначить объект, свойства которого вы хотите получить доступ к временной переменной.
Оператор with может использоваться для уменьшения размера кода или для закрытых членов класса, например:
// demo class framework
var Class= function(name, o) {
var c=function(){};
if( o.hasOwnProperty("constructor") ) {
c= o.constructor;
}
delete o["constructor"];
delete o["prototype"];
c.prototype= {};
for( var k in o ) c.prototype[k]= o[k];
c.scope= Class.scope;
c.scope.Class= c;
c.Name= name;
return c;
}
Class.newScope= function() {
Class.scope= {};
Class.scope.Scope= Class.scope;
return Class.scope;
}
// create a new class
with( Class.newScope() ) {
window.Foo= Class("Foo",{
test: function() {
alert( Class.Name );
}
});
}
(new Foo()).test();
Оператор with очень полезен, если вы хотите изменить область, что необходимо для того, чтобы иметь собственную глобальную область, которой вы можете манипулировать во время выполнения. Вы можете добавить константы или некоторые часто используемые вспомогательные функции, такие как, например, "toUpper", "toLower" или "isNumber", "clipNumber" aso..
О плохой производительности, которую я часто читал: определение области действия не влияет на производительность, фактически в моем FF функция с областью видимости работает быстрее, чем с незаданной областью:
var o={x: 5},r, fnRAW= function(a,b){ return a*b; }, fnScoped, s, e, i;
with( o ) {
fnScoped= function(a,b){ return a*b; };
}
s= Date.now();
r= 0;
for( i=0; i < 1000000; i++ ) {
r+= fnRAW(i,i);
}
e= Date.now();
console.log( (e-s)+"ms" );
s= Date.now();
r= 0;
for( i=0; i < 1000000; i++ ) {
r+= fnScoped(i,i);
}
e= Date.now();
console.log( (e-s)+"ms" );
Таким образом, в вышеупомянутом способе оператор with не оказывает отрицательного влияния на производительность, но хорош, поскольку уменьшает размер кода, что влияет на использование памяти на мобильных устройствах.
Я думаю, что использование литерала объекта интересно, как замена вставки для использования замыкания
for(var i = nodes.length; i--;)
{
// info is namespaced in a closure the click handler can access!
(function(info)
{
nodes[i].onclick = function(){ showStuff(info) };
})(data[i]);
}
или с оператором, эквивалентным замыканию
for(var i = nodes.length; i--;)
{
// info is namespaced in a closure the click handler can access!
with({info: data[i]})
{
nodes[i].onclick = function(){ showStuff(info) };
}
}
Я думаю, что реальный риск заключается в случайном минимизации переменных, которые не являются частью оператора with, и именно поэтому мне нравится объектный литерал, передаваемый в, вы можете точно видеть, что это будет в добавленном контексте в коде.
Использование with также делает ваш код более медленным во многих реализациях, так как теперь все упаковано в дополнительную область для поиска. Там нет законных оснований для использования с в JavaScript.
Я создал функцию "слияния", которая устраняет эту двусмысленность with
заявление:
if (typeof Object.merge !== 'function') {
Object.merge = function (o1, o2) { // Function to merge all of the properties from one object into another
for(var i in o2) { o1[i] = o2[i]; }
return o1;
};
}
Я могу использовать его аналогично with
, но я могу знать, что это не повлияет на область, на которую я не собираюсь влиять.
Использование:
var eDiv = document.createElement("div");
var eHeader = Object.merge(eDiv.cloneNode(false), {className: "header", onclick: function(){ alert("Click!"); }});
function NewObj() {
Object.merge(this, {size: 4096, initDate: new Date()});
}
Для некоторых коротких фрагментов кода я хотел бы использовать тригонометрические функции, такие как sin
, cos
и т.д. в градусном режиме, а не в лучистом режиме Для этого я использую AngularDegree
объект:
AngularDegree = new function() {
this.CONV = Math.PI / 180;
this.sin = function(x) { return Math.sin( x * this.CONV ) };
this.cos = function(x) { return Math.cos( x * this.CONV ) };
this.tan = function(x) { return Math.tan( x * this.CONV ) };
this.asin = function(x) { return Math.asin( x ) / this.CONV };
this.acos = function(x) { return Math.acos( x ) / this.CONV };
this.atan = function(x) { return Math.atan( x ) / this.CONV };
this.atan2 = function(x,y) { return Math.atan2(x,y) / this.CONV };
};
Тогда я могу использовать тригонометрические функции в режиме степени без дальнейшего языкового шума в with
блок:
function getAzimut(pol,pos) {
...
var d = pos.lon - pol.lon;
with(AngularDegree) {
var z = atan2( sin(d), cos(pol.lat)*tan(pos.lat) - sin(pol.lat)*cos(d) );
return z;
}
}
Это означает: я использую объект как набор функций, которые я включаю в ограниченной области кода для прямого доступа. Я считаю это полезным.
Это хорошо для помещения кода, который выполняется в относительно сложной среде, в контейнер: я использую его для создания локальной привязки для "окна" и для запуска кода, предназначенного для веб-браузера.
Я просто не понимаю, как использование with более читабельно, чем просто набирать object.member. Я не думаю, что это менее читабельно, но я не думаю, что это также более читабельно.
Как сказал lassevk, я определенно вижу, что использование with будет более подвержено ошибкам, чем просто использование очень явного синтаксиса "object.member".
Я думаю, что полезность with
может зависеть от того, насколько хорошо написан ваш код. Например, если вы пишете код, который выглядит так:
var sHeader = object.data.header.toString();
var sContent = object.data.content.toString();
var sFooter = object.data.footer.toString();
тогда вы могли бы утверждать, что with
улучшит читабельность кода, сделав это:
var sHeader = null, sContent = null, sFooter = null;
with(object.data) {
sHeader = header.toString();
sContent = content.toString();
sFooter = content.toString();
}
И наоборот, можно утверждать, что вы нарушаете Закон Деметры, но, опять же, возможно, нет. Я отвлекся =).
Прежде всего, знайте, что Дуглас Крокфорд рекомендует не использовать with
, Я призываю вас проверить его пост в блоге относительно with
и его альтернативы здесь.
Мой
switch(e.type) {
case gapi.drive.realtime.ErrorType.TOKEN_REFRESH_REQUIRED: blah
case gapi.drive.realtime.ErrorType.CLIENT_ERROR: blah
case gapi.drive.realtime.ErrorType.NOT_FOUND: blah
}
сводится к
with(gapi.drive.realtime.ErrorType) {switch(e.type) {
case TOKEN_REFRESH_REQUIRED: blah
case CLIENT_ERROR: blah
case NOT_FOUND: blah
}}
Можно ли доверять такому некачественному коду? Нет, мы видим, что это было сделано абсолютно нечитаемым. Этот пример неопровержимо доказывает, что нет необходимости в выражении with, если я правильно понимаю читаемость;)
Коко- вилка CoffeeScript имеет with
ключевое слово, но оно просто устанавливает this
(также доступно для записи как @
в CoffeeScript/Coco) к целевому объекту в блоке. Это устраняет неоднозначность и обеспечивает строгое соблюдение режима ES5:
with long.object.reference
@a = 'foo'
bar = @b
Вы должны увидеть подтверждение формы в javascript в W3schools http://www.w3schools.com/js/js_form_validation.asp где форма объекта "сканируется", чтобы найти вход с именем "email"
Но я изменил его, чтобы получить из ЛЮБОЙ формы все поля проверяются как не пустые, независимо от имени или количества поля в форме. Ну, я тестировал только текстовые поля.
Но с помощью () все стало проще. Вот код:
function validate_required(field)
{
with (field)
{
if (value==null||value=="")
{
alert('All fields are mandtory');return false;
}
else
{
return true;
}
}
}
function validate_form(thisform)
{
with (thisform)
{
for(fiie in elements){
if (validate_required(elements[fiie])==false){
elements[fiie].focus();
elements[fiie].style.border='1px solid red';
return false;
} else {elements[fiie].style.border='1px solid #7F9DB9';}
}
}
return false;
}
использование оператора with с прокси-объектами
Недавно я хочу написать плагин для babel, который включает макросы. Я хотел бы иметь отдельное пространство имен переменных, в котором хранятся мои макропеременные, и я могу запускать свои макрокоды в этом пространстве. Кроме того, я хочу обнаруживать новые переменные, которые определены в кодах макросов (потому что это новые макросы).
Сначала я выбираю модуль vm, но я обнаружил, что глобальные переменные в модуле vm, такие как Array, Object и т. Д., Отличаются от основной программы, и я не могу реализовать
module
и
require
которые будут полностью совместимы с этими глобальными объектами (потому что я не могу восстановить основные модули). В конце концов, я нахожу утверждение "с".
const runInContext = function(code, context) {
context.global = context;
const proxyOfContext = new Proxy(context, { has: () => true });
let run = new Function(
"proxyOfContext",
`
with(proxyOfContext){
with(global){
${code}
}
}
`
);
return run(proxyOfContext);
};
Этот прокси-объект перехватывает поиск всех переменных и говорит: "Да, у меня есть эта переменная". и если прокси-объект действительно не имеет этой переменной, покажите ее значение как
undefined
.
Таким образом, если в макросе определена какая-либо переменная
code
с
var
оператор, я могу найти его в объекте контекста (например, в модуле vm). Но переменные, которые определены с помощью
let
или же
const
доступны только в это время и не будут сохранены в объекте контекста (модуль vm сохраняет их, но не раскрывает).
Производительность: производительность этого метода лучше, чем
vm.runInContext
.
безопасность: если вы хотите запустить код в песочнице, это ни в коем случае не безопасно, и вы должны использовать модуль vm. Он предоставляет только новое пространство имен.
Как отметил Энди Э. в комментариях к ответу Shog9, это потенциально неожиданное поведение возникает при использовании with
с литералом объекта:
for (var i = 0; i < 3; i++) {
function toString() {
return 'a';
}
with ({num: i}) {
setTimeout(function() { console.log(num); }, 10);
console.log(toString()); // prints "[object Object]"
}
}
Не то чтобы неожиданное поведение уже не было отличительной чертой with
,
Если вы все еще хотите использовать эту технику, по крайней мере, используйте объект с нулевым прототипом.
function scope(o) {
var ret = Object.create(null);
if (typeof o !== 'object') return ret;
Object.keys(o).forEach(function (key) {
ret[key] = o[key];
});
return ret;
}
for (var i = 0; i < 3; i++) {
function toString() {
return 'a';
}
with (scope({num: i})) {
setTimeout(function() { console.log(num); }, 10);
console.log(toString()); // prints "a"
}
}
Но это будет работать только в ES5+. Также не используйте with
,
Я работаю над проектом, который позволит пользователям загружать код для изменения поведения частей приложения. В этом сценарии я использовал with
предложение, чтобы их код не модифицировал что-либо вне области, с которой я хочу, чтобы они возились. (Упрощенная) часть кода, которую я использую для этого:
// this code is only executed once
var localScope = {
build: undefined,
// this is where all of the values I want to hide go; the list is rather long
window: undefined,
console: undefined,
...
};
with(localScope) {
build = function(userCode) {
eval('var builtFunction = function(options) {' + userCode + '}');
return builtFunction;
}
}
var build = localScope.build;
delete localScope.build;
// this is how I use the build method
var userCode = 'return "Hello, World!";';
var userFunction = build(userCode);
Этот код (в некоторой степени) гарантирует, что пользовательский код не имеет доступа ни к каким объектам глобальной области, таким как window
ни к одной из моих локальных переменных через замыкание.
Как слово для мудрых, мне все еще нужно выполнить статические проверки кода для кода, представленного пользователем, чтобы убедиться, что они не используют другие хитрые способы доступа к глобальной области видимости. Например, следующий определенный пользователем код получает прямой доступ к window
:
test = function() {
return this.window
};
return test();
Вот хорошее использование для with
: добавление новых элементов в Object Literal на основе значений, хранящихся в этом Object. Вот пример, который я только что использовал сегодня:
У меня был набор возможных плиток (с отверстиями вверху, внизу, влево или вправо), которые можно было использовать, и я хотел быстро добавить список плиток, которые всегда будут размещаться и блокироваться в начале игры., Я не хотел продолжать печатать types.tbr
для каждого типа в списке, поэтому я просто использовал with
,
Tile.types = (function(t,l,b,r) {
function j(a) { return a.join(' '); }
// all possible types
var types = {
br: j( [b,r]),
lbr: j([l,b,r]),
lb: j([l,b] ),
tbr: j([t,b,r]),
tbl: j([t,b,l]),
tlr: j([t,l,r]),
tr: j([t,r] ),
tl: j([t,l] ),
locked: []
};
// store starting (base/locked) tiles in types.locked
with( types ) { locked = [
br, lbr, lbr, lb,
tbr, tbr, lbr, tbl,
tbr, tlr, tbl, tbl,
tr, tlr, tlr, tl
] }
return types;
})("top","left","bottom","right");