@tlhunter
intrinsic.com

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

Thomas Hunter II

Roadmap

  1. Intro to Objects
  2. Property Descriptors
  3. Sealing, Extension, Freezing
  4. Proxies
  5. Proxies + Property Descriptors

Intro to Objects

  • A complex key/value “Hash Table” structure
  • Don't use as a dynamic collection (use Map)
  • Map is also more secure (e.g. .__proto__)

Basic Object Operations Syntax

const x = { // create object
  prop1: true
}

x.prop2 = 42 // add property

x['prop2'] = 'quux' // reassign property

delete x.prop1 // remove

console.log(x.prop2) // read property, 'quux'

Property Descriptors

  • They let you lie!
Object.defineProperty(
  obj,
  propertyName,
  descriptors
)

Value and Enumerable

const obj = {}
Object.defineProperty(obj, 'foo', {
  value: 'hello', // the property value
  enumerable: false // property will not be listed
})
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

for ... in requiring .hasOwnProperty()

const proto = {}
const obj = { ok: 1 }
obj.__proto__ = proto
for (let key in obj) console.log(key) // [ok]

proto.bad = () => 42

for (let key in obj) console.log(key) // [ok,bad]
for (let key in obj) {
  if (obj.hasOwnProperty(key)) {
    console.log(key) // [ok]
  }
}

for ... in without .hasOwnProperty()

const proto = {}
const obj = { ok: 1 }
obj.__proto__ = proto
for (let key in obj) console.log(key) // [ok]

Object.defineProperty(proto, 'good', {
  value: () => 42,
  enumerable: false
})

for (let key in obj) console.log(key) // [ok]

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: 0 }

Object.defineProperty(obj, 'age', {
  get: function() {
    return 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: 0,
  get age() {
    return this.realAge
  },
  set age(value) {
    this.realAge = Number(value)
  }
}

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

All Object Properties have Descriptors

const obj1 = {
  a: 1
}

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

{
  value: 1,
  writable: true,
  enumerable: true,
  configurable: true
} // (e.g. Data Property)

All Object Properties have Descriptors

const obj2 = {
  get b() { }
}

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

{
  get: Function,
  set: undefined,
  enumerable: true,
  configurable: true
} // (e.g. Accessor Property)

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
} // (e.g. Data Property)

Pop Quiz!






// What code could lead to this outcome?





if (typeof obj.p === 'number' && obj.p > 10) {
    console.log(obj.p) // 'lies!'
}

Pop Quiz Explained

let accesses = 0
const obj = Object.defineProperty({}, 'p', {
  get: () => {
    if (accesses++ >= 2) {
      return 'lies!'
    }
    return 12
  }
})

// How can we prevent this craziness?!
if (typeof obj.p === 'number' && obj.p > 10) {
    console.log(obj.p) // 'lies!'
}

Pop Quiz Solved

let accesses = 0
const obj = Object.defineProperty({}, 'p', {
  get: () => {
    if (accesses++ >= 2) {
      return 'lies!'
    }
    return 12
  }
})

const p = obj.p // read object properties once
if (typeof p === 'number' && p > 10) {
    console.log(p) // 12
}

Sealing, Extension, Freezing

Sealing Objects

const obj = { p: 'first' }
Object.seal(obj)

obj.p = 'second' // OK
delete obj.p // fail silently, throw in strict
obj.p2 = 'new val' // fail silently, throw in strict

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

Preventing Object Extension

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

obj.p = 'second' // OK
obj.p2 = 'new val' // fail silently, throw in strict

console.log(obj) // { p: 'second' }
console.log(Object.isExtensible(obj)) // false
console.log(Object.getOwnPropertyDescriptor(obj, 'p'))
// { value: 'second', writable: true,
//   enumerable: true, configurable: true }
delete obj.p // OK

Freezing Objects

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

obj.p = 'second' // fail silently, throw in strict
delete obj.p // fail silently, throw in strict
obj.p2 = 'new val' // fail silently, throw in strict

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

Summary

isFrozenisSealedisExtadddelreassign
Seal✔️✔️
PrevExt✔️✔️
Freeze✔️✔️

Proxies

  • A Proxy is an object which lets you provide callable “trap” methods which are called when interacting with the object in various ways.
  • They let you lie in fun and imaginitive ways!
  • If a trap is missing, fallback to default bevavior.
  • Very useful for writing test code.
  • Traps have corresponding Reflect. methods.
const proxy = new Proxy(target, handler)

Get Proxy Trap

const orig = { p: 7 }
const handler = {
  get: (target, prop, receiver) => {
    // target === orig
    // receiver === proxy || receiver === child
    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', sym]
console.log(Object
  .getOwnPropertyNames(proxy)) // ['p']
console.log(Object
  .getOwnPropertySymbols(proxy)) // [sym]

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!'
// Also, Function.prototype.apply(), .call()

Construct Proxy Trap

class Original {
  constructor(arg) {
    console.log(`Hello, ${arg}!`)
  }
}

const handler = {
  construct(target, args) {
    return new target(String(args[0]).toUpperCase())
  }
}

const OriginalProxy = new Proxy(Original, handler)
new OriginalProxy('Tom') // '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
Reflect.setPrototypeOf(proxy, {}) // 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