Как избежать ошибок типа "не удается прочитать свойство неопределенного"?

119

В моем коде я имею дело с массивом, в котором есть записи со множеством объектов, вложенных друг в друга, а некоторых нет. Это выглядит примерно так:

// where this array is hundreds of entries long, with a mix
// of the two examples given
var test = [{'a':{'b':{'c':"foo"}}}, {'a': "bar"}];

Это вызывает у меня проблемы, потому что мне нужно время от времени перебирать массив, а несогласованность вызывает такие ошибки:

for (i=0; i<test.length; i++) {
    // ok on i==0, but 'cannot read property of undefined' on i==1
    console.log(a.b.c);
}

Я знаю, что могу сказать if(a.b){ console.log(a.b.c)}, но это чрезвычайно утомительно в тех случаях, когда имеется до 5 или 6 объектов, вложенных друг в друга. Есть ли другой (более простой) способ сделать так, чтобы он ТОЛЬКО делал console.log, если он существует, но без выдачи ошибки?

Ari
источник
3
Ошибка, вероятно, является обычным исключением Javascript, поэтому попробуйте выполнить try..catchинструкцию. Тем не менее, массив, содержащий крайне разнородные элементы, мне кажется проблемой дизайна.
millimoose
3
Если ваша структура не согласована между элементами, то что плохого в проверке существования? На самом деле, я бы использовал if ("b" in a && "c" in a.b). Это может быть «утомительно», но это то, что вы получите за несогласованность ... нормальную логику.
Ян
2
Зачем вам обращаться к несуществующим свойствам, почему вы не знаете, как выглядят объекты?
Берги
9
Я могу понять, почему кто-то не хочет, чтобы ошибка приводила к сбою всего. Вы не всегда можете полагаться на то, что свойства объекта существуют или не существуют. Если у вас есть что-то, что может обработать событие неправильной формы объекта, ваш код будет намного более эффективным и менее хрупким.
SSH это
3
Вы будете удивлены,
узнав,

Ответы:

122

Обновление :

  • Если вы используете JavaScript в соответствии с ECMAScript 2020 или новее, см. Необязательную цепочку .
  • TypeScript добавил поддержку необязательного связывания в версии 3.7 .
// use it like this
obj?.a?.lot?.of?.properties

Решение для JavaScript до ECMASCript 2020 или TypeScript до версии 3.7 :

Быстрый обходной путь - использование вспомогательной функции try / catch с функцией стрелки ES6 :

function getSafe(fn, defaultVal) {
    try {
        return fn();
    } catch (e) {
        return defaultVal;
    }
}

// use it like this
getSafe(() => obj.a.lot.of.properties);

// or add an optional default value
getSafe(() => obj.a.lot.of.properties, 'nothing');

Рабочий фрагмент:

Подробнее см. В этой статье .

ул
источник
2
Я люблю это! единственное, что я бы добавил, - это console.warn внутри улова, чтобы вы знали об ошибке, но она продолжается.
Рабби Шуки Гур
Поймать все исключения без повторной генерации - это плохо , и, как правило, использование исключений как части ожидаемого потока выполнения также не очень хорошо, хотя в этом случае оно довольно хорошо сдерживается.
hugo
50

То, что вы делаете, вызывает исключение (и это справедливо).

Вы всегда можете сделать

try{
   window.a.b.c
}catch(e){
   console.log("YO",e)
}

Но я бы не стал думать о вашем варианте использования.

Почему вы получаете доступ к данным, 6 уровням вложенности, которые вам незнакомы? Какой вариант использования оправдывает это?

Обычно вы действительно хотите проверить, с каким объектом вы имеете дело.

Кроме того, на стороне примечания вы не должны использовать такие утверждения, как, if(a.b)потому что он вернет false, если ab равно 0 или даже если оно равно «0». Вместо этого проверьте, еслиa.b !== undefined

Бенджамин Грюнбаум
источник
1
Что касается вашего первого редактирования: это оправдано; Я имею дело со структурированными записями базы данных JSON, так что объекты будут масштабировать несколько уровней полей (например, entry.users.messages.date и т. Д., Где не во всех случаях введены данные)
Ари
"он вернет истину, если ab равно 0" - нет. typeof a.b === "undefined" && a.b!=null- ненужно делать вторую часть после первой, и имеет смысл просто сделатьif ("b" in a)
Ян
@Ian да, я, очевидно, имел в виду наоборот, он вернет false, даже если ab равно "0". Хороший улов
Бенджамин Грюнбаум
@BenjaminGruenbaum Звучит хорошо, не был уверен, что вы это имели в виду. Кроме того, я думаю, вы хотите typeof a.b !== "undefined" && ab! = Null` - обратите внимание на!==
Ян
3
Если вы не хотите утомлять ab && abc && console.log (abc), то это единственный способ постоянно регистрировать неизвестные.
Брайан Крей
14

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

Самый простой способ - использовать inоператор .

window.a = "aString";
//window should have 'a' property
//lets test if it exists
if ("a" in window){
    //true
 }

if ("b" in window){
     //false
 }

Конечно, вы можете вложить это так глубоко, как хотите

if ("a" in window.b.c) { }

Не уверен, что это поможет.

Мэтт Вайс
источник
9
Вы не можете безопасно вложить это так глубоко, как хотите. Что делать, если window.bне определено? Вы получите ошибку типа:Cannot use 'in' operator to search for 'c' in undefined
Trevor
13

Если вы используете lodash , вы можете использовать их функцию has. Он похож на родной "in", но допускает пути.

var testObject = {a: {b: {c: 'walrus'}}};
if(_.has(testObject, 'a.b.c')) {
  //Safely access your walrus here
}
Tehwalris
источник
2
Лучше всего, мы можем использовать по _.get()умолчанию для удобства чтения:_.get(object, 'a.b.c', 'default');
Ifnot
12

Попробуй это. Если a.bне определено, ifоператор оставит оператор без исключения.

if (a.b && a.b.c) {
  console.log(a.b.c);
}
Шон Чен
источник
5

Это обычная проблема при работе с глубокими или сложными объектами json, поэтому я стараюсь избегать попыток / перехвата или встраивания нескольких проверок, которые могут сделать код нечитаемым. Обычно я использую этот небольшой фрагмент кода во всех своих процедурах для выполнения работы.

/* ex: getProperty(myObj,'aze.xyz',0) // return myObj.aze.xyz safely
 * accepts array for property names: 
 *     getProperty(myObj,['aze','xyz'],{value: null}) 
 */
function getProperty(obj, props, defaultValue) {
    var res, isvoid = function(x){return typeof x === "undefined" || x === null;}
    if(!isvoid(obj)){
        if(isvoid(props)) props = [];
        if(typeof props  === "string") props = props.trim().split(".");
        if(props.constructor === Array){
            res = props.length>1 ? getProperty(obj[props.shift()],props,defaultValue) : obj[props[0]];
        }
    }
    return typeof res === "undefined" ? defaultValue: res;
}
Maelkhor
источник
5

Если у вас есть lodash, вы можете использовать его .getметод

_.get(a, 'b.c.d.e')

или укажите значение по умолчанию

_.get(a, 'b.c.d.e', default)
Брэндон Дайер
источник
4

Я неукоснительно использую undefsafe . Он проверяет каждый уровень вашего объекта до тех пор, пока он либо не получит запрошенное вами значение, либо не вернет «undefined». Но ни разу ошибок.

martinedwards
источник
2
это похоже на lodash_.get
Filype
Хороший крик! По-прежнему полезно, если вам не нужны другие функции lodash.
martinedwards
3

Мне нравится ответ Цао Шоугуана, но я не люблю передавать функцию в качестве параметра в функцию getSafe каждый раз, когда я выполняю вызов. Я модифицировал функцию getSafe, чтобы она принимала простые параметры и чистый ES5.

/**
* Safely get object properties.    
* @param {*} prop The property of the object to retrieve
* @param {*} defaultVal The value returned if the property value does not exist
* @returns If property of object exists it is returned, 
*          else the default value is returned.
* @example
* var myObj = {a : {b : 'c'} };
* var value;
* 
* value = getSafe(myObj.a.b,'No Value'); //returns c 
* value = getSafe(myObj.a.x,'No Value'); //returns 'No Value'
* 
* if (getSafe(myObj.a.x, false)){ 
*   console.log('Found')
* } else {
*  console.log('Not Found') 
* }; //logs 'Not Found'
* 
* if(value = getSafe(myObj.a.b, false)){
*  console.log('New Value is', value); //logs 'New Value is c'
* }
*/
function getSafe(prop, defaultVal) {
  return function(fn, defaultVal) {
    try {
      if (fn() === undefined) {
        return defaultVal;
      } else {
        return fn();
      }
    } catch (e) {
      return defaultVal;
    }
  }(function() {return prop}, defaultVal);
}
Харди Ле Ру
источник
Это действительно не работает getSafe(myObj.x.c). Пробовал последние версии Chrome и Firefox.
Rickard Elimää
2

В ответе str будет возвращено значение undefined вместо установленного значения по умолчанию, если свойство не определено. Иногда это может вызвать ошибки. Следующее гарантирует, что defaultVal всегда будет возвращаться, если свойство или объект не определены.

const temp = {};
console.log(getSafe(()=>temp.prop, '0'));

function getSafe(fn, defaultVal) {
    try {
        if (fn() === undefined) {
            return defaultVal
        } else {
            return fn();
        }

    } catch (e) {
        return defaultVal;
    }
}
Цао Шоугуан
источник
Улучшенная версия моего кода Харди Ле Ру не работает с let myObj = {} getSafe (() => myObj.ab, "nice"), а моя работает. Кто-нибудь объясняет почему?
Цао Шоугуан
2

В Lodash есть getметод, который позволяет использовать значение по умолчанию в качестве необязательного третьего параметра, как показано ниже:

const myObject = {
  has: 'some',
  missing: {
    vars: true
  }
}
const path = 'missing.const.value';
const myValue = _.get(myObject, path, 'default');
console.log(myValue) // prints out default, which is specified above
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"></script>

Pureferret
источник
2

Представьте, что мы хотим применить ряд функций к тому xи только тогда, когда не xравно нулю:

if (x !== null) x = a(x);
if (x !== null) x = b(x);
if (x !== null) x = c(x);

Теперь предположим, что нам нужно сделать то же самое для y:

if (y !== null) y = a(y);
if (y !== null) y = b(y);
if (y !== null) y = c(y);

И то же самое z:

if (z !== null) z = a(z);
if (z !== null) z = b(z);
if (z !== null) z = c(z);

Как видите, без надлежащей абстракции мы в конечном итоге будем дублировать код снова и снова. Такая абстракция уже существует: Возможно монада .

Может быть , монада имеет как значение и вычислительный контекст:

  1. Монада сохраняет значение в безопасности и применяет к нему функции.
  2. Вычислительный контекст - это проверка нуля перед применением функции.

Наивная реализация выглядела бы так:

⚠️ Эта реализация предназначена только для иллюстрации! Так делать не следует, и это неправильно на многих уровнях. Однако это должно дать вам лучшее представление о том, о чем я говорю.

Как видите, ничего не сломается:

  1. Мы применяем ряд функций к нашему значению
  2. Если в какой-то момент значение становится нулевым (или неопределенным), мы больше не применяем никаких функций.

const abc = obj =>
  Maybe
    .of(obj)
    .map(o => o.a)
    .map(o => o.b)
    .map(o => o.c)
    .value;

const values = [
  {},
  {a: {}},
  {a: {b: {}}},
  {a: {b: {c: 42}}}
];

console.log(

  values.map(abc)

);
<script>
function Maybe(x) {
  this.value = x; //-> container for our value
}

Maybe.of = x => new Maybe(x);

Maybe.prototype.map = function (fn) {
  if (this.value == null) { //-> computational context
    return this;
  }
  return Maybe.of(fn(this.value));
};
</script>


Приложение 1

Я не могу объяснить, что такое монады, поскольку это не является целью этого поста, и есть люди, которые справляются с этим лучше, чем я. Однако, как сказал Эрик Эллиот в своем блоге, JavaScript Monads Made Simple :

Независимо от вашего уровня навыков или понимания теории категорий, использование монад упрощает работу с кодом. Неспособность воспользоваться преимуществами монад может затруднить работу с вашим кодом (например, ад обратных вызовов, вложенные условные ветки, большая многословность).


Приложение 2

Вот как я бы решил вашу проблему с помощью Maybe монаду из

const prop = key => obj => Maybe.fromNull(obj[key]);

const abc = obj =>
  Maybe
    .fromNull(obj)
    .flatMap(prop('a'))
    .flatMap(prop('b'))
    .flatMap(prop('c'))
    .orSome('🌯')
    
const values = [
  {},
  {a: {}},
  {a: {b: {}}},
  {a: {b: {c: 42}}}
];

console.log(

  values.map(abc)

);
<script src="https://www.unpkg.com/monet@0.9.0/dist/monet.js"></script>
<script>const {Maybe} = Monet;</script>

командир таможни
источник
0

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

function containsProperty(instance, propertyName) {
    // make an array of properties to walk through because propertyName can be nested
    // ex "test.test2.test.test"
    let walkArr = propertyName.indexOf('.') > 0 ? propertyName.split('.') : [propertyName];

    // walk the tree - if any property does not exist then return false
    for (let treeDepth = 0, maxDepth = walkArr.length; treeDepth < maxDepth; treeDepth++) {

        // property does not exist
        if (!Object.prototype.hasOwnProperty.call(instance, walkArr[treeDepth])) {
            return false;
        }

        // does it exist - reassign the leaf
        instance = instance[walkArr[treeDepth]];

    }

    // default
    return true;

}

В своем вопросе вы могли бы сделать что-то вроде:

let test = [{'a':{'b':{'c':"foo"}}}, {'a': "bar"}];
containsProperty(test[0], 'a.b.c');
Мэтт Вайс
источник
0

Я обычно использую вот так:

 var x = object.any ? object.any.a : 'def';
Вансуита младший
источник
0

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

var test = [{'a':{'b':{'c':"foo"}}}, {'a': "bar"}];

for (i=0; i<test.length; i++) {
    const obj = test[i]
    // No error, just undefined, which is ok
    console.log(((obj.a || {}).b || {}).c);
}

Это отлично работает и с массивами:

const entries = [{id: 1, name: 'Scarllet'}]
// Giving a default name when is empty
const name = (entries.find(v => v.id === 100) || []).name || 'no-name'
console.log(name)

Игорь Парра
источник