Страницы

воскресенье, 28 сентября 2014 г.

Объекты в JavaScript. Вариации на тему

В последнее время я все чаще стал использовать геттеры, сеттеры, создавать объекты и определять их свойства с помощью соответствующих методов конструктора Object, а также предотвращать возможность их последующего изменения. Не потому что "так круче", просто в моем случае для этого есть причины. Сегодня хочу осветить несколько "продвинутых", на самом деле уже давно существующих, приемов работы с объектами в JavaScript на простых примерах.


1. Геттеры и сеттеры.

1.1. Геттеры и сеттеры в литерале:
var cat = {
  type_: 'cat', 
  name_: '',
  get name() {
    return this.type_ + ': ' + this.name_;
  },
  set name(name) {
    this.name_ = name;
  }
};
cat.name = 'Tom';
console.log(cat.name);
console.log(cat.name = 'Max'); // здесь Max! не cat: Max
console.log(cat.name);

1.2. Геттеры и сеттеры в прототипе:
function Animal(type) {
  this.type_ = type;
  this.name_ = '';
}

Animal.prototype = {
  get name () {
    return this.type_ + ': ' + this.name_;
  },
  set name (name) {
    this.name_ = name;    
  }
};

1.3. Геттеры и сеттеры в дескрипторе (дескриптору посвящен следующий раздел поста):
function Animal(type) {
  this.type_ = type;
  this.name_ = '';
}

Object.defineProperty(Animal.prototype, 'name', {
  get: function() {
    return this.type_ + ': ' + this.name_;
  },
  set: function(name) {
    this.name_ = name;
  }
});

Тестируем листинги 1.2 и 1.3:
var cat = new Animal('cat');
cat.name = 'Tom';
console.log(cat.name);
console.log(cat.name = 'Max'); // здесь Max! не cat: Max
console.log(cat.name);


Обращаю внимание на комментарий в коде - пытаться возвращать свойство непосредственно из сеттера бесполезно, например следующим образом:
set name (name) {
  this.name_ = name;
  return this.type_ + ': ' + this.name_; // не помогает
}


2. Дескриптор.

Дескриптор - это объект, который содержит описание свойства объекта (именно его мы отправляли в качестве третьего аргумента метода Object.defineProperty чуть выше - листинг 1.3).

2.1. Свойства дескриптора по умолчанию:
var cat1 = {name: 'Tom'};
var cat2 = Object.defineProperty({}, 'name', {
  value: 'Max'
});
var cat3 = {
  get name() {
    return this.name_;
  },
  set name(name) {
    this.name_ = name;
  }
};
console.log(Object.getOwnPropertyDescriptor(cat1, 'name'));
console.log(Object.getOwnPropertyDescriptor(cat2, 'name'));
console.log(Object.getOwnPropertyDescriptor(cat3, 'name'));


Найдите отличия.

2.2. Object.defineProperty, не объявляем свойства дескриптора (оставляем по умолчанию):
var cat = {name: 'Tom'};
console.log(cat.name);
delete cat.name; // если не удалить, то свойство останется configurable и writable
Object.defineProperty(cat, 'name', {
  value: 'Max'
});
console.log(cat.name);
cat.name = 'Sam'; // в строгом режиме ошибка будет уже здесь
console.log(cat.name); // здесь Max
delete cat.name;
console.log(cat.name); // все равно Max
Object.defineProperty(cat, 'name', { // здесь Cannot redefine property: name
  value: 'Sam',
});


По умолчанию свойства configurable и writable равны false.

2.3. Object.defineProperty + configurable + writable:
var cat = {};
Object.defineProperty(cat, 'name', {
  value: 'Max',
  configurable: true,
  writable: true
});
console.log(cat.name);
cat.name = 'Sam';
console.log(cat.name);
delete cat.name;
console.log(cat.name);
Object.defineProperty(cat, 'name', {
  value: 'Tom',
  configurable: true,
  writable: true
});
console.log(cat.name);
for (var key in cat) { // здесь никого нет
  console.log('%s: %s',key, cat[key]);
}


По умолчанию свойство enumerable тоже равно false.

2.4. Object.defineProperty + enumerable:
var cat = {};
Object.defineProperty(cat, 'name', {
  value: 'Tom',
  enumerable: true
});
for (var key in cat) {
  console.log('%s: %s', key, cat[key]);
  console.log('%s is enumerable:', key, cat.propertyIsEnumerable(key));
}


2.5. Пытаемся миксовать value или writable с get/set:
var cat = {};
// здесь TypeError: Invalid property. A property cannot both have accessors and be writable or have a value
Object.defineProperty(cat, 'name', {
  value: 'Tom',
  writable: true,
  get: function () {}
});

2.6. Object.defineProperties, объявляем несколько свойств сразу:
var cat = Object.defineProperties({}, {
  name: {
    value: 'Tom',
    writable: true,
  },
  greet: {
    value: function() {
      console.log('Hi, my name is', this.name);
    }
  }
});
cat.greet();
cat.name = 'Sam';
cat.greet();


2.7. Object.keys vs. Object.getOwnPropertyNames:
var cat = Object.defineProperties({}, {
  name: {
    value: 'Tom',
    enumerable: true,
  },
  greet: {
    value: function() {
      console.log('Hi, my name is', this.name);
    }
  }
});
console.log(Object.keys(cat));
console.log(Object.getOwnPropertyNames(cat));


Найдите отличия.

3. Наследование.

3.1. Object.create, создаем новый объект с указанным прототипом и свойствами:
var animal = {
  roars: 'r-r-r',
  roar: function () {
    console.log(this.roars);
  }
};
var props = {
  meows: {
    value: 'meow'
  },
  meow: {
    value: function () {
      console.log(this.meows);
    }
  }
};
var cat = Object.create(animal, props);
cat.roar();
cat.meow();


3.2. Cвойства, объявленные в объекте перекрывают свойства прототипа ("стандартное" наследование):
var animal = {
  greeting: 'r-r-r',
  roar: function () {
    console.log(this.greeting);
  }
};
var props = {
  greeting: {
    value: 'meow'
  },
  meow: {
    value: function () {
      console.log(this.greeting);
    }
  }
};
var cat = Object.create(animal, props);
cat.roar();
cat.meow();


3.3. Установка свойства непримитивного типа при создании объекта с помощью Object.create:
var animal = {
  roars: ['r-r-r'],
  roar: function () {
    console.log(this.roars.join('...'));
  }
};
var props = {
  meows: {
    value: ['meow']
  },
  meow: {
    value: function () {
      console.log(this.meows.join(', '));
    }
  }
};
var cat1 = Object.create(animal, props);
var cat2 = Object.create(animal, props);
cat1.roars.push('meow'); // изменили свойство прототипа
cat2.roar(); // cat2 рычит иначе
cat1.meows.push('meow'); // изменили свойство cat1
cat2.meow(); // cat2 мяукает иначе!


Обращаю внимание на комментарии в коде - использование одного объекта props, содержащего дескрипторы (или чьи свойства указывают на дескрипторы - не силен в терминах, думаю понятно о чем речь), для создания нескольких объектов в этом случае является ошибкой. Угадайте почему.

3.4. Создавая объекты с помощью Object.create мы не используем конструктор, поэтому для идентификации объекта вместо instanceof следует применять isPrototypeOf:
var animal = {
  greeting: 'r-r-r',
  roar: function () {
    console.log(this.greeting);
  }
};
var tiger = Object.create(animal);
var cat = Object.create(tiger, {greeting: {value: 'meow'}});
tiger.roar();
cat.roar();
console.log('tiger is prototype of cat:', tiger.isPrototypeOf(cat));
console.log('animal is prototype of cat:', animal.isPrototypeOf(cat));


Обращаю внимание на то, что метод isPrototypeOf отрабатывает по всей цепочке прототипов.

4. Запрет изменения объекта.

4.1. Object.preventExtensions, запрет добавления свойств:
var cat = {name: 'Tom', greeting: 'r-r-r'};
console.log(cat);
console.log('cat is extensible:', Object.isExtensible(cat));
Object.preventExtensions(cat);
console.log('cat is extensible:', Object.isExtensible(cat));
delete cat.greeting;
console.log(cat);
cat.greeting = 'meow'; // в строгом режиме здесь будет ошибка
console.log(cat);
Object.defineProperty(cat, 'name', { // здесь без ошибок
  value: 'Max',
  configurable: true
});
console.log(cat);


4.2. Object.seal, запрет добавления, удаления и конфигурирования свойств:
var cat = {name: 'Tom', greeting: 'r-r-r'};
console.log(cat);
console.log('cat is sealed:', Object.isSealed(cat));
Object.seal(cat);
console.log('cat is sealed:', Object.isSealed(cat));
delete cat.greeting; // в строгом режиме ошибка будет уже здесь
console.log(cat);
cat.greeting = 'meow';
console.log(cat);
Object.defineProperty(cat, 'name', { // здесь Cannot redefine property: name
  value: 'Max',
  configurable: true
});
console.log(cat);


4.3. Object.freeze, запрет добавления, удаления, конфигурирования и изменения значения свойств:
var cat = {name: 'Tom', greeting: 'r-r-r'};
console.log(cat);
console.log('cat is frozen:', Object.isFrozen(cat));
Object.freeze(cat);
console.log('cat is frozen:', Object.isFrozen(cat));
cat.name = 'Max'; // в строгом режиме здесь будет ошибка
console.log(cat);


В ECMAScript 6 есть еще более вкусные фишки, но об этом как-нибудь в другой раз...