@tlhunter@mastodon.social

Property Descriptors,
Getters/Setters,
and Proxies, Oh My!

Thomas Hunter II

Check out Distributed Systems with Node.js:
bit.ly/34SHToF

What's an Object?

  • A complex key/value "Hash Map" structure
  • Don't use as a dynamic collection (use Map)

Basic Object Operations Syntax

const x = {
  prop1: true
}

x.prop2 = 42

x['prop3'] = 'quux'

delete x.prop1

console.log(x.prop2) // 42

Property Descriptors

Object.defineProperty(
  obj,
  propertyName,
  descriptors
)

Value and Enumerable

const obj = {}
Object.defineProperty(obj, 'foo', {
  value: 'hello', // the value of the property
  enumerable: false // will the property be visible
})
console.log(obj) // {}
console.log(obj.foo) // 'hello'
console.log(Object.keys(obj)) // []
console.log(Reflect.ownKeys(obj)) // [ 'foo' ]
console.log('foo' in obj) // true

Writable and Configurable

const obj = Object.defineProperty({}, 'foo', {
  value: 'hello',
  writable: false, // reassignable?
  configurable: false // deletable/redefinable?
})
obj.foo = 'bye'
console.log(obj.foo) // 'hello'
delete obj.foo
console.log(obj.foo) // 'hello'
Object.defineProperty(obj, 'foo', {
  value: 1
}) // TypeError: Cannot redefine property: foo

Getters and Setters

const obj = {realAge: null}

Object.defineProperty(obj, 'age', {
  get: function() {
    return Number(this.realAge)
  },
  set: function(value) {
    this.realAge = Number(value)
  }
})

console.log(obj.age) // 0
obj.age = '32'
console.log(obj.age) // 32

Object Literal Getters and Setters

const obj = {
  realAge: null,
  get age() {
    return Number(this.realAge)
  },
  set age(value) {
    this.realAge = Number(value)
  }
}

console.log(obj.age) // 0
obj.age = '32'
console.log(obj.age) // 32

Pop Quiz!






// What code could lead to this outcome?





if (typeof obj.type === 'number' && obj.type > 10) {
    console.log(obj.type) // 'not twelve'
}

Pop Quiz Explained

let accesses = 0
const obj = Object.defineProperty({}, 'type', {
  get: () => {
    if (accesses++ >= 2) {
      return 'not twelve'
    }
    return 12
  }
})

// And how can you fix it?
if (typeof obj.type === 'number' && obj.type > 10) {
    console.log(obj.type) // 'not twelve'
}

Pop Quiz Solved

let accesses = 0
const obj = Object.defineProperty({}, 'type', {
  get: () => {
    if (accesses++ >= 2) {
      return 'not twelve'
    }
    return 12
  }
})

const type = obj.type // read sensitive values once
if (typeof type === 'number' && type > 10) {
    console.log(type) // 12
}

All Object Properties have Descriptors

const obj1 = {
  a: 1
}

console.log(Object
  .getOwnPropertyDescriptor(obj1, 'a'))

{
  value: 1,
  writable: true,
  enumerable: true,
  configurable: true
}

All Object Properties have Descriptors

const obj2 = {
  get b() { }
}

console.log(Object
  .getOwnPropertyDescriptor(obj2, 'b'))

{
  get: Function,
  set: undefined,
  enumerable: true,
  configurable: true
}

All Object Properties have Descriptors

const obj3 = Object.defineProperty({}, 'c', {
  value: 'xyz'
})

console.log(Object
  .getOwnPropertyDescriptor(obj3, 'c'))

{
  value: 'xyz',
  writable: false,
  enumerable: false,
  configurable: false
}

Sealing, Extension, Freezing

Sealing Objects

const obj = { p: 'hi' }
Object.seal(obj)
obj.p = 'bye' // OK
delete obj.p // disallowed
obj.p2 = 'new' // disallowed
console.log(obj) // { p: "bye" }
console.log(Object.isSealed(obj)) // true
console.log(Object.getOwnPropertyDescriptor(obj, 'p'))
// { value: "bye", writable: true,
//   enumerable: true, configurable: false }

Preventing Object Extension

const obj = { p: 'bar' }
Object.preventExtensions(obj)

obj.x = true // fails silently
obj.p = 'bop' // OK
console.log(obj) // { p: "bar" }
console.log(Object.isExtensible(obj)) // false
console.log(Object.getOwnPropertyDescriptor(obj, 'p'))
// { value: "bop", writable: true,
//   enumerable: true, configurable: true }
Object.defineProperty(obj, 'new', { value: 1 })
// ^ TypeError

Freezing Objects

const obj = { p: 'orig' }
Object.freeze(obj)

console.log(Object.isFrozen(obj)) // true
obj.p = 'new' // fail silently, throw in strict
delete obj.p // fail silently, throw in strict
obj.x = 1 // fail silently, throw in strict

console.log(obj) // { p: 'orig' }
console.log(Object.getOwnPropertyDescriptor(obj, 'p'))
// { value: "orig", writable: false,
//   enumerable: true, configurable: false }

Proxies

A Proxy is an object which lets you provide callable "trap" methods which are called when interacting with the object in various ways.

const proxy = new Proxy(target, handler)

Get Proxy Trap

const orig = { p: 7 }
const handler = {
  get: (target, prop, receiver) => {
    return target[prop] ? target[prop] + 1 : Infinity
  }
}
const proxy = new Proxy(orig, handler)

console.log(orig.p) // 7
console.log(orig.r) // undefined
console.log(proxy.p) // 8
console.log(proxy.r) // Infinity

Has Proxy Trap

const orig = { p: 7 }
const handler = {
  has: (target, prop) => {
    return false
  }
}
const proxy = new Proxy(orig, handler)
console.log('p' in orig) // true
console.log('r' in orig) // false
console.log('p' in proxy) // false
console.log(Reflect.has(proxy, 'p')) // false
console.log(proxy.p) // 7

Set Proxy Trap

const orig = {}
const handler = {
  set: (target, prop, value, receiver) => {
    target[prop.toUpperCase()] = String(value)
  }
}
const proxy = new Proxy(orig, handler)

orig.p = 1
console.log(orig) // { p: 1 }
proxy.hello = 1
console.log(orig) // { p: 1, HELLO: '1' }

Delete Proxy Trap

const obj = { p: 1, r: 2 }
const handler = {
  deleteProperty: (target, prop) => {
    if (prop === 'r') delete target[prop]
    return true // falsey will throw in strict
  }
}
const proxy = new Proxy(obj, handler)
delete proxy.p
delete proxy.r
console.log(proxy) // { p: 2 }

Object Keys Proxy Trap

const sym = Symbol()
const orig = { p: 1, r: 2, [sym]: 3 }
const handler = {
  ownKeys: (target) => ["p", sym]
}
const proxy = new Proxy(orig, handler)
console.log(Object.keys(proxy)) // ["p"]
console.log(Reflect.ownKeys(proxy)) // ["p",Symbol()]
console.log(Object
  .getOwnPropertyNames(proxy)) // ["p"]
console.log(Object
  .getOwnPropertySymbols(proxy)) // [Symbol()]

Apply Proxy Trap

function orig(msg) {
  return `Hello, ${msg}!`
}

const handler = {
  apply: (target, self, args) => {
    return target(String(args[0]).toUpperCase())
  }
}

const proxy = new Proxy(orig, handler)
console.log(proxy('world')) // "Hello, WORLD!"

Construct Proxy Trap

class Original {
  constructor(arg) {
    this.value = `Hello, ${arg}!`
  }
}

const handler = {
  construct(target, args) {
    return new target(...args)
  }
}

const proxy = new Proxy(Original, handler)
console.log(new proxy('Tom').value) // 'Hello, Tom!'

Get/Set Prototype Proxy Traps

const orig = {}
const handler = {
  getPrototypeOf: (target) => null,
  setPrototypeOf: (target, proto) => {
    throw new Error('no way')
  }
}
const proxy = new Proxy(orig, handler)

console.log(orig.__proto__) // {}
console.log(proxy.__proto__) // null
console.log(Object.getPrototypeOf(proxy)) // null
console.log(Reflect.getPrototypeOf(proxy)) // null
proxy.__proto__ = {} // Error: no way

Extensibility Proxy Traps

const orig = {}
const handler = {
  preventExtensions: (target) =>
    Object.preventExtensions(target),
  isExtensible: (target) =>
    Reflect.isExtensible(target)
}
const proxy = new Proxy(orig, handler)
console.log(Object.isExtensible(proxy)) // true
Object.preventExtensions(proxy)
console.log(Object.isExtensible(proxy)) // false
// Note: Can't lie, otherwise will throw Error

Property Descriptors Proxy Traps

const proxy = new Proxy({}, {
  defineProperty: (target, prop, desc) => {
    if (desc.value === 42)
      Object.defineProperty(target, prop, desc)
    return true
  },
  getOwnPropertyDescriptor: (tgt, prp) => {
    return Object.getOwnPropertyDescriptor(tgt, prp)
  }
})
Object.defineProperty(proxy, 'p', { value: 42 })
Object.defineProperty(proxy, 'r', { value: 43 })
console.log(proxy.p, proxy.r) // 42, undefined

Proxies + Property Descriptors

Double Getter Fun

const orig = {}
Object.defineProperty(orig, 'name', {
  get: () => {
    console.log('2. prop desc get'); return 'Thomas'
  }
})
const proxy = new Proxy(orig, {
  get: (target, prop) => {
    console.log('1. proxy get'); return target[prop]
  }
})
console.log(`3. ${proxy.name}`) // 1. proxy get
                                // 2. prop desc get
                                // 3. Thomas

Fin