Есть ли в javascript оператор с нулевым слиянием (Элвис) или оператор безопасной навигации?
Я объясню на примере:
Элвис Оператор (?:)
"Оператор Элвиса" является сокращением троичного оператора Java. Один из примеров того, где это удобно, - это возвращение значения "разумного значения по умолчанию", если выражение принимает значение false или ноль. Простой пример может выглядеть так:
def gender = user.male ? "male" : "female" //traditional ternary operator usage
def displayName = user.name ?: "Anonymous" //more compact Elvis operator
Оператор безопасной навигации (?.)
Оператор безопасной навигации используется, чтобы избежать исключения NullPointerException. Обычно, когда у вас есть ссылка на объект, вам может потребоваться проверить, что он не является нулевым, прежде чем получить доступ к методам или свойствам объекта. Чтобы избежать этого, оператор безопасной навигации просто возвратит null вместо того, чтобы выдавать исключение, например так:
def user = User.find( "admin" ) //this might be null if 'admin' does not exist
def streetName = user?.address?.street //streetName will be null if user or user.address is null - no NPE thrown
22 ответа
Вы можете использовать логический оператор "ИЛИ" вместо оператора Элвиса:
Например displayname = user.name || "Anonymous"
,
Но у Javascript в настоящее время нет другой функциональности. Я бы порекомендовал посмотреть на CoffeeScript, если вы хотите альтернативный синтаксис. Он имеет некоторую стенографию, которая похожа на то, что вы ищете.
Например, экзистенциальный оператор
zip = lottery.drawWinner?().address?.zipcode
Функциональные ярлыки
()-> // equivalent to function(){}
Сексуальная функция вызова
func 'arg1','arg2' // equivalent to func('arg1','arg2')
Также есть многострочные комментарии и классы. Очевидно, вы должны скомпилировать это в JavaScript или вставить на страницу как <script type='text/coffeescript>'
но это добавляет много функциональности:) . С помощью <script type='text/coffeescript'>
действительно предназначен только для разработки, а не производства.
Обновление 2020
В JavaScript теперь есть эквиваленты как для оператора Элвиса, так и для оператора безопасной навигации.
Безопасный доступ к собственности
По желанию оператора цепочки (?.
) в настоящее время является предложением ECMAScript на этапе 4. Вы можете использовать его сегодня с Babel.
// `undefined` if either `a` or `b` are `null`/`undefined`. `a.b.c` otherwise.
const myVariable = a?.b?.c;
Логический оператор (&&
) - это "старый", более подробный способ справиться с этим сценарием.
const myVariable = a && a.b && a.b.c;
Предоставление значения по умолчанию
Оператор объединения с нулевым значением (??
) в настоящее время является предложением ECMAScript на этапе 4. Вы можете использовать его сегодня с Babel. Он позволяет вам установить значение по умолчанию, если левая часть оператора является нулевым значением ( null
/undefined
).
const myVariable = a?.b?.c ?? 'Some other value';
// Evaluates to 'Some other value'
const myVariable2 = null ?? 'Some other value';
// Evaluates to ''
const myVariable3 = '' ?? 'Some other value';
Логический оператор ИЛИ (||
) - альтернативное решение с немного другим поведением. Он позволяет вам установить значение по умолчанию, если левая часть оператора неверна. Обратите внимание, что результатmyVariable3
ниже отличается от myVariable3
над.
const myVariable = a?.b?.c || 'Some other value';
// Evaluates to 'Some other value'
const myVariable2 = null || 'Some other value';
// Evaluates to 'Some other value'
const myVariable3 = '' || 'Some other value';
Я думаю, что следующее эквивалентно оператору безопасной навигации, хотя и немного дольше:
var streetName = user && user.address && user.address.street;
streetName
тогда будет либо значение user.address.street
или же undefined
,
Если вы хотите, чтобы по умолчанию было что-то еще, вы можете комбинировать с вышеупомянутым ярлыком или дать:
var streetName = (user && user.address && user.address.street) || "Unknown Street";
Логический оператор ИЛИ Javascript имеет короткое замыкание и может заменить ваш оператор "Элвис":
var displayName = user.name || "Anonymous";
Однако, насколько мне известно, нет эквивалента ?.
оператор.
Иногда я находил следующую идиому полезной:
a?.b.?c
можно переписать как:
((a||{}).b||{}).c
Это использует тот факт, что получение неизвестных атрибутов объекта возвращает неопределенное значение, а не генерирует исключение, как это происходит на null
или же undefined
, поэтому мы заменяем null и undefined пустым объектом перед навигацией.
Я думаю, что Лодаш _.get()
может помочь здесь, как в _.get(user, 'name')
и более сложные задачи, такие как _.get(o, 'a[0].b.c', 'default-value')
Еще нет. Может быть, скоро. В настоящее время существует проект спецификации:
https://github.com/tc39/proposal-optional-chaining
https://tc39.github.io/proposal-optional-chaining/
Пока что мне нравится использовать lodashget(object, path, [defaultValue])
или ДЛВdelve(obj, keypath)
Для первого вы можете использовать ||
, Оператор Javascript "логический или", а не просто возвращает постоянные истинные и ложные значения, следует правилу возврата левого аргумента, если он истинен, и в противном случае оценивает и возвращает свой правый аргумент. Когда вас интересует только значение истины, оно работает так же, но это также означает, что foo || bar || baz
возвращает самый левый из foo, bar или baz, который содержит истинное значение.
Вы не найдете такой, которая может отличить ложь от нуля, а 0 и пустая строка являются ложными значениями, поэтому избегайте использования value || default
построить где value
может законно быть 0 или ""
,
Вот моя функция "Элвис". Передайте корневой объект и цепочку как строку. Он всегда возвращает первый неопределенный элемент цепочки. Работает как с объектами, так и с массивами, методами и примитивами.
elvis(myObject, 'categories.shirts[0].getPrice().currency');
Рабочий пример:
const elvis = (obj, keychain) => {
const handleArray = (parent, key) => {
if (key.indexOf('[') > -1) {
const arrayName = key.split('[')[0];
const arrayIndex = +key.split('[')[1].slice(0, -1);
return parent[arrayName] && parent[arrayName][arrayIndex];
}
if (key.indexOf('(') > -1) {
const methodName = key.split('(')[0];
return parent[methodName] && parent[methodName]();
}
return parent[key];
}
const keys = keychain.split('.');
let base = obj;
for (let i = 0; i < keys.length; i += 1) {
base = handleArray(base, keys[i]);
if (typeof base === 'undefined') return base;
}
return base;
}
//--------
const myObject = {
categories: {
getFoo: () => 'foo',
shirts: [
{ color: 'red' },
{ color: 'blue' }
]
}
}
console.log(elvis(myObject, 'categories.shirts[0].color'));
console.log(elvis(myObject, 'categories.getFoo()'));
console.log(elvis(myObject, 'categories.getBar()'));
console.log(elvis(myObject, 'categories.shirts[0].length'));
console.log(elvis(myObject, 'categories.pans[2].color'));
Вот простой эквивалент оператора Элвиса:
function elvis(object, path) {
return path ? path.split('.').reduce(function (nestedObject, key) {
return nestedObject && nestedObject[key];
}, object) : object;
}
> var o = { a: { b: 2 }, c: 3 };
> elvis(o)
{ a: { b: 2 }, c: 3 }
> elvis(o, 'a');
{ b: 2 }
> elvis(o, 'a.b');
2
> elvis(o, 'x');
undefined
ОБНОВЛЕНИЕ СЕНТЯБРЬ 2019
Да, JS теперь поддерживает это. Дополнительная цепочка скоро появится в версии 8. Подробнее
Вы можете достичь примерно того же эффекта, сказав:
var displayName = user.name || "Anonymous";
Это чаще всего называют оператором слияния нулей. У Javascript его нет.
У меня есть решение для этого, адаптировать его к вашим собственным потребностям, выдержка из одной из моих библиотек:
elvisStructureSeparator: '.',
// An Elvis operator replacement. See:
// http://coffeescript.org/ --> The Existential Operator
// http://fantom.org/doc/docLang/Expressions.html#safeInvoke
//
// The fn parameter has a SPECIAL SYNTAX. E.g.
// some.structure['with a selector like this'].value transforms to
// 'some.structure.with a selector like this.value' as an fn parameter.
//
// Configurable with tulebox.elvisStructureSeparator.
//
// Usage examples:
// tulebox.elvis(scope, 'arbitrary.path.to.a.function', fnParamA, fnParamB, fnParamC);
// tulebox.elvis(this, 'currentNode.favicon.filename');
elvis: function (scope, fn) {
tulebox.dbg('tulebox.elvis(' + scope + ', ' + fn + ', args...)');
var implicitMsg = '....implicit value: undefined ';
if (arguments.length < 2) {
tulebox.dbg(implicitMsg + '(1)');
return undefined;
}
// prepare args
var args = [].slice.call(arguments, 2);
if (scope === null || fn === null || scope === undefined || fn === undefined
|| typeof fn !== 'string') {
tulebox.dbg(implicitMsg + '(2)');
return undefined;
}
// check levels
var levels = fn.split(tulebox.elvisStructureSeparator);
if (levels.length < 1) {
tulebox.dbg(implicitMsg + '(3)');
return undefined;
}
var lastLevel = scope;
for (var i = 0; i < levels.length; i++) {
if (lastLevel[levels[i]] === undefined) {
tulebox.dbg(implicitMsg + '(4)');
return undefined;
}
lastLevel = lastLevel[levels[i]];
}
// real return value
if (typeof lastLevel === 'function') {
var ret = lastLevel.apply(scope, args);
tulebox.dbg('....function value: ' + ret);
return ret;
} else {
tulebox.dbg('....direct value: ' + lastLevel);
return lastLevel;
}
},
работает как шарм. Наслаждайся меньшей болью!
Вы можете свернуть свой собственный:
function resolve(objectToGetValueFrom, stringOfDotSeparatedParameters) {
var returnObject = objectToGetValueFrom,
parameters = stringOfDotSeparatedParameters.split('.'),
i,
parameter;
for (i = 0; i < parameters.length; i++) {
parameter = parameters[i];
returnObject = returnObject[parameter];
if (returnObject === undefined) {
return undefined;
}
}
return returnObject;
};
И использовать это так:
var result = resolve(obj, 'a.b.c.d');
* результат не определен, любой из a, b, c или d не определен
Я прочитал эту статью ( https://www.beyondjava.net/elvis-operator-aka-safe-navigation-javascript-typescript) и изменил решение с помощью прокси.
function safe(obj) {
return new Proxy(obj, {
get: function(target, name) {
const result = target[name];
if (!!result) {
return (result instanceof Object)? safe(result) : result;
}
return safe.nullObj;
},
});
}
safe.nullObj = safe({});
safe.safeGet= function(obj, expression) {
let safeObj = safe(obj);
let safeResult = expression(safeObj);
if (safeResult === safe.nullObj) {
return undefined;
}
return safeResult;
}
Вы называете это так:
safe.safeGet(example, (x) => x.foo.woo)
Результат будет неопределенным для выражения, которое встречается с нулевым или неопределенным на своем пути. Вы можете сойти с ума и изменить прототип Object!
Object.prototype.getSafe = function (expression) {
return safe.safeGet(this, expression);
};
example.getSafe((x) => x.foo.woo);
Очень поздно, есть предложение [1] об опциональной цепочке в настоящее время на стадии 2, с доступным плагином babel [2]. В настоящее время его нет ни в одном браузере, о котором я знаю.
Это было проблемой для меня долгое время. Мне нужно было придумать решение, которое можно легко перенести, как только мы получим оператора Элвиса или что-то в этом роде.
Это то, что я использую; работает как для массивов, так и для объектов
поместите это в файл tools.js или что-то в этом роде
// this will create the object/array if null
Object.prototype.__ = function (prop) {
if (this[prop] === undefined)
this[prop] = typeof prop == 'number' ? [] : {}
return this[prop]
};
// this will just check if object/array is null
Object.prototype._ = function (prop) {
return this[prop] === undefined ? {} : this[prop]
};
пример использования:
let student = {
classes: [
'math',
'whatev'
],
scores: {
math: 9,
whatev: 20
},
loans: [
200,
{ 'hey': 'sup' },
500,
300,
8000,
3000000
]
}
// use one underscore to test
console.log(student._('classes')._(0)) // math
console.log(student._('classes')._(3)) // {}
console.log(student._('sports')._(3)._('injuries')) // {}
console.log(student._('scores')._('whatev')) // 20
console.log(student._('blabla')._('whatev')) // {}
console.log(student._('loans')._(2)) // 500
console.log(student._('loans')._(1)._('hey')) // sup
console.log(student._('loans')._(6)._('hey')) // {}
// use two underscores to create if null
student.__('loans').__(6)['test'] = 'whatev'
console.log(student.__('loans').__(6).__('test')) // whatev
хорошо, я знаю, что это делает код немного нечитаемым, но это простое однострочное решение и отлично работает. Надеюсь, это кому-то поможет:)
Я создал пакет, который значительно упрощает его использование.
Вы можете справиться с простыми вещами, такими как и объект:
const world = {
locations: {
europe: 'Munich',
usa: 'Indianapolis'
}
};
world.dig('locations', 'usa');
// => 'Indianapolis'
world.dig('locations', 'asia', 'japan');
// => 'null'
или чуть посложнее:
const germany = () => 'germany';
const world = [0, 1, { location: { europe: germany } }, 3];
world.dig(2, 'location', 'europe') === germany;
world.dig(2, 'location', 'europe')() === 'germany';
Это было интересное решение для оператора безопасной навигации с использованием некоторого миксина.
http://jsfiddle.net/avernet/npcmv/
// Assume you have the following data structure
var companies = {
orbeon: {
cfo: "Erik",
cto: "Alex"
}
};
// Extend Underscore.js
_.mixin({
// Safe navigation
attr: function(obj, name) { return obj == null ? obj : obj[name]; },
// So we can chain console.log
log: function(obj) { console.log(obj); }
});
// Shortcut, 'cause I'm lazy
var C = _(companies).chain();
// Simple case: returns Erik
C.attr("orbeon").attr("cfo").log();
// Simple case too, no CEO in Orbeon, returns undefined
C.attr("orbeon").attr("ceo").log();
// IBM unknown, but doesn't lead to an error, returns undefined
C.attr("ibm").attr("ceo").log();
Лично я использую
function e(e,expr){try{return eval(expr);}catch(e){return null;}};
и например безопасно получить:
var a = e(obj,'e.x.y.z.searchedField');