JavaScript Metaprogramming with Proxies

Eirik Vullum

- Independent Consultant

- JavaScript Training

eiriklv@github
eiriklv@twitter
http://vullum.io

Metaprogramming

"The ability to treat programs as data"

Wikipedia

"The ability to read, generate, analyse or transform other programs, and even modify itself while running"

Wikipedia

What?

Macros

Create your own..

// macro (sweet.js)
syntax hi = function (ctx) {
  return #`console.log('hello, world!')`;
};

Language Constructs / Syntax

// macro (sweet.js)
syntax hi = function (ctx) {
  return #`console.log('hello, world!')`;
};

// before compile/expansion
hi
// macro (sweet.js)
syntax hi = function (ctx) {
  return #`console.log('hello, world!')`;
};

// before compile/expansion
hi

// after compile/expansion
console.log('hello, world!');
// macro (sweet.js)
operator >>= left 1 = (left, right) => {
  return #`${left}.then(${right})`;
};

Operators

// macro (sweet.js)
operator >>= left 1 = (left, right) => {
  return #`${left}.then(${right})`;
};

// before compile/expansion
fetch('/foo.json') >>= resp => { return resp.json() }
                   >>= json => { return processJson(json) }
// macro (sweet.js)
operator >>= left 1 = (left, right) => {
  return #`${left}.then(${right})`;
};

// before compile/expansion
fetch('/foo.json') >>= resp => { return resp.json() }
                   >>= json => { return processJson(json) }

// after compile/expansion
fetch("/foo.json").then(resp => {
  return resp.json();
}).then(json => {
  return processJson(json);
});

Happens during
compile-time

(at least before the code runs)

Reflection

  • Introspection
  • Self-modification
  • Intercession

Introspection

const hero = {
  health: 100,
  backpack: ['carrots', 'beer'],
  weapon: 'sword'
};
const hero = {
  health: 100,
  backpack: ['carrots', 'beer'],
  weapon: 'sword'
};

const keys = Object.keys(hero);
const hero = {
  health: 100,
  backpack: ['carrots', 'beer'],
  weapon: 'sword'
};

const keys = Object.keys(hero);

console.log(keys);
// ['health', 'backpack', 'weapon']

Self-modification

function grumpySum(a, b) {
  if (a > 5) {
    grumpySum = () => 0;
  }

  return a + b;
}
function grumpySum(a, b) {




  return a + b;
}
function grumpySum(a, b) {
  if (a > 5) {
    grumpySum = () => 0;
  }

  return a + b;
}

console.log(grumpySum(1, 1))   // -> 2
console.log(grumpySum(10, 1))  // -> 11
function grumpySum(a, b) {
  if (a > 5) {
    grumpySum = () => 0;
  }

  return a + b;
}

console.log(grumpySum(1, 1))   // -> 2
console.log(grumpySum(10, 1))  // -> 11
console.log(grumpySum(2, 3))   // -> 0
function grumpySum(a, b) {
  if (a > 5) {
    grumpySum = () => 0;
  }

  return a + b;
}

console.log(grumpySum(1, 1))   // -> 2
console.log(grumpySum(10, 1))  // -> 11
console.log(grumpySum(2, 3))   // -> 0
console.log(grumpySum(2, 100)) // -> 0
function grumpySum(a, b) {
  if (a > 5) {
    grumpySum = () => 0;
  }

  return a + b;
}

console.log(grumpySum(1, 1))   // -> 2

Intercession

var hero = {
  health: 100,
};

Very limited / not very flexible

var hero = {
  health: 100,
};

Object.defineProperty(hero, 'status', {
  get: function() {





  },
});
var hero = {
  health: 100,
};

Object.defineProperty(hero, 'status', {
  get: function() {
    if (this.health > 50) {
      return 'fit like a champ'
    }


  },
});
var hero = {
  health: 100,
};

Object.defineProperty(hero, 'status', {
  get: function() {
    if (this.health > 50) {
      return 'fit like a champ'
    } else {
      return 'badly hurt';
    }
  },
});
var hero = {
  health: 100,
};

Object.defineProperty(hero, 'status', {
  get: function() {
    if (this.health > 50) {
      return 'fit like a champ'
    } else {
      return 'badly hurt';
    }
  },
});

hero.status // -> fit like a champ

Happens during
run-time

(while the code runs)

Why?

(is this useful)

"In some cases, this allows programmers to minimize the number of lines of code to express a solution, and thus reducing the development time"

Wikipedia

EXPRESSIVITY TO MODEL OUR PROBLEMS

we want to

DEVELOPER FRIENDLY INTERFACES

we strive for

Proxies

Proxy Server

Proxying Objects

BETTER INTERFACE

and hiding the dirty details behind abstractions

it's all about providing a








  new Proxy(target, handler)

Proxies in JavaScript

Fundamental operations

Closer look


  const proxy = new Proxy(target, handler);

Target

{}
{}
[]
{}
[]
function() {}
{}
[]
function() {}
new Proxy(...)
{}
[]
function() {}
new Proxy(...)

// any type of object

Transparent

const proxy = new Proxy({}, {})
const proxy = new Proxy({}, {})
typeof proxy === 'object'

Handler

var handler = {




};
var handler = {
  get: function(target, name) {
    return name in target ?
    target[name] : 37;
  }
};
var handler = {
  get: function(target, name) {
    return name in target ?
    target[name] : 37;
  }
};

var p = new Proxy({}, handler);
var handler = {
  get: function(target, name) {
    return name in target ?
    target[name] : 37;
  }
};

var p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;
var handler = {
  get: function(target, name) {
    return name in target ?
    target[name] : 37;
  }
};

var p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b); // 1, undefined
var handler = {
  get: function(target, name) {
    return name in target ?
    target[name] : 37;
  }
};

var p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b); // 1, undefined
console.log('c' in p, p.c); // false, 37

Traps

proxy = new Proxy({}, {
  get: ...,
  set: ...,
  has: ...,
  apply: ...,
  construct: ...,
  defineProperty: ...,
  getOwnPropertyDescriptor: ...,
  deleteProperty: ...,
  getPrototypeOf: ...,
  setPrototypeOf: ...,
  isExtensible: ...,
  preventExtensions: ...,
  ownKeys: ...,
});

Reflect API

const hero = { health: 100 };
const hero = { health: 100 };
Reflect.get(hero, 'health'); // 100

1 to 1

proxy = new Proxy({}, {
  get: ...,
  set: ...,
  has: ...,
  apply: ...,
  construct: ...,
  defineProperty: ...,
  getOwnPropertyDescriptor: ...,
  deleteProperty: ...,
  getPrototypeOf: ...,
  setPrototypeOf: ...,
  isExtensible: ...,
  preventExtensions: ...,
  ownKeys: ...,
});
proxy = new Proxy({}, {
  get: Reflect.get,
  set: Reflect.set,
  has: Reflect.has,
  apply: Reflect.apply,
  construct: Reflect.construct,
  defineProperty: Reflect.defineProperty,
  getOwnPropertyDescriptor: Reflect.getOwnPropertyDescriptor,
  deleteProperty: Reflect.deleteProperty,
  getPrototypeOf: Reflect.getPrototypeOf,
  setPrototypeOf: Reflect.setPrototypeOf,
  isExtensible: Reflect.isExtensible,
  preventExtensions: Reflect.preventExtensions,
  ownKeys: Reflect.ownKeys,
});

Revocable

const person = {
  name: 'Eirik',
  age: 31,
};
const person = {
  name: 'Eirik',
  age: 31,
};

const { proxy, revoke } = Proxy.revocable(person, {
  get(target, key) {
    return Reflect.get(target, key);
  }
});
const person = {
  name: 'Eirik',
  age: 31,
};

const { proxy, revoke } = Proxy.revocable(person, {
  get(target, key) {
    return Reflect.get(target, key);
  }
});

console.log(proxy.name);
// Eirik
const person = {
  name: 'Eirik',
  age: 31,
};

const { proxy, revoke } = Proxy.revocable(person, {
  get(target, key) {
    return Reflect.get(target, key);
  }
});

console.log(proxy.name);
// Eirik

revoke();
const person = {
  name: 'Eirik',
  age: 31,
};

const { proxy, revoke } = Proxy.revocable(person, {
  get(target, key) {
    return Reflect.get(target, key);
  }
});

console.log(proxy.name);
// Eirik

revoke();
console.log(proxy.name);
// TypeError: Cannot perform 'get' on a proxy that has been revoked  

You can overload fundamental operators like '.', '=' and '()'

Using them

Validation

function validate(obj, validations) {
  







}
function validate(obj, validations) {
  return new Proxy(obj, {
    





  });
}
function validate(obj, validations) {
  return new Proxy(obj, {
    set(target, key, value) {
      



    }
  });
}
function validate(obj, validations) {
  return new Proxy(obj, {
    set(target, key, value) {
      const validate = validations[key] || (() => true);
      


    }
  });
}
function validate(obj, validations) {
  return new Proxy(obj, {
    set(target, key, value) {
      const validate = validations[key] || (() => true);
      validate(value);


    }
  });
}
function validate(obj, validations) {
  return new Proxy(obj, {
    set(target, key, value) {
      const validate = validations[key] || (() => true);
      validate(value);
      Reflect.set(target, key, value);
      return true;
    }
  });
}

Validation

const personValidations = {










}
const personValidations = {
  age(value) {



  },
  name(value) {



  }
}
const personValidations = {
  age(value) {
    if (typeof value !== 'number') {
      throw new Error('.age has to be a number!');
    }
  },
  name(value) {



  }
}
const personValidations = {
  age(value) {
    if (typeof value !== 'number') {
      throw new Error('.age has to be a number!');
    }
  },
  name(value) {
    if (typeof value !== 'string') {
      throw new Error('.name has to be a string!');
    }
  }
}

Validation

const person = validate({}, personValidations);
const person = validate({}, personValidations);
person.name = 'Eirik';
person.age = 5;
const person = validate({}, personValidations);
person.name = 'Eirik';
person.age = 5;

person.name = 10;
const person = validate({}, personValidations);
person.name = 'Eirik';
person.age = 5;

person.name = 10;
// Throws -> Error: .name has to be a string!

Debugging / Logging

function logAccessToProperties(obj, ref) {






}
function logAccessToProperties(obj, ref) {
  return new Proxy(obj, {




  })
}
function logAccessToProperties(obj, ref) {
  return new Proxy(obj, {
    get(target, key) {


    }
  })
}
function logAccessToProperties(obj, ref) {
  return new Proxy(obj, {
    get(target, key) {
      console.log('Accessed key', key, 'on', ref);

    }
  })
}
function logAccessToProperties(obj, ref) {
  return new Proxy(obj, {
    get(target, key) {
      console.log('Accessed key', key, 'on', ref);
      return Reflect.get(target, key);
    }
  })
}

Debugging / Logging

const person = {
  name: 'Eirik',
  age: 31,
};
const person = {
  name: 'Eirik',
  age: 31,
};

const personWithAccessLogging = logAccessToProperties(person, 'abc');
const person = {
  name: 'Eirik',
  age: 31,
};

const personWithAccessLogging = logAccessToProperties(person, 'abc');

personWithAccessLogging.age;
// Accessed age on abc
const person = {
  name: 'Eirik',
  age: 31,
};

const personWithAccessLogging = logAccessToProperties(person, 'abc');

personWithAccessLogging.age;
// Accessed age on abc

personWithAccessLogging.name;
// Accessed name on abc

Observable objects

function observable(obj, onChange) {










}
function observable(obj, onChange) {
  return new Proxy(obj, {








  });
}
function observable(obj, onChange) {
  return new Proxy(obj, {
    set(target, key, value) {


    },
    



  });
}
function observable(obj, onChange) {
  return new Proxy(obj, {
    set(target, key, value) {
      Reflect.set(target, key, value);
      
    },




  });
}
function observable(obj, onChange) {
  return new Proxy(obj, {
    set(target, key, value) {
      Reflect.set(target, key, value);
      onChange({ key, value });
    },




  });
}
function observable(obj, onChange) {
  return new Proxy(obj, {
    set(target, key, value) {
      Reflect.set(target, key, value);
      onChange({ key, value });
    },
    delete(target, key) {
      Reflect.delete(target, key);
      onChange({ key, value: undefined })
    },
  });
}

Observable objects

let person = {
  name: 'Eirik',
  age: 31,
};
let person = {
  name: 'Eirik',
  age: 31,
};

person = observable(person, ({ key, value }) => {
  console.log(`${key} changed to ${value}`);
});
let person = {
  name: 'Eirik',
  age: 31,
};

person = observable(person, ({ key, value }) => {
  console.log(`${key} changed to ${value}`);
});

person.name = 'Frank';
let person = {
  name: 'Eirik',
  age: 31,
};

person = observable(person, ({ key, value }) => {
  console.log(`${key} changed to ${value}`);
});

person.name = 'Frank';
// name changed to Frank
let person = {
  name: 'Eirik',
  age: 31,
};

person = observable(person, ({ key, value }) => {
  console.log(`${key} changed to ${value}`);
});

person.name = 'Frank';
// name changed to Frank
person.age = 40;
let person = {
  name: 'Eirik',
  age: 31,
};

person = observable(person, ({ key, value }) => {
  console.log(`${key} changed to ${value}`);
});

person.name = 'Frank';
// name changed to Frank
person.age = 40;
// age changed to 40

Construction / DSLs

function urlBuilder(domain) {
  let parts = [];

  const proxy = new Proxy(() => {
    const returnValue = domain + '/' + parts.join('/');
    parts = [];
    return returnValue;
  }, {
    has() {
      return true;
    },
    get(target, key) {
      parts.push(key);
      return proxy;
    },
  });

  return proxy;
}

Source: https://www.keithcirkel.co.uk/metaprogramming-in-es6-part-3-proxies/

Construction / DSLs


const google = urlBuilder('http://google.com');

const google = urlBuilder('http://google.com');

const url = google

const google = urlBuilder('http://google.com');

const url = google.search

const google = urlBuilder('http://google.com');

const url = google.search.products

const google = urlBuilder('http://google.com');

const url = google.search.products.bacon

const google = urlBuilder('http://google.com');

const url = google.search.products.bacon.and

const google = urlBuilder('http://google.com');

const url = google.search.products.bacon.and.eggs

const google = urlBuilder('http://google.com');

const url = google.search.products.bacon.and.eggs()

const google = urlBuilder('http://google.com');

const url = google.search.products.bacon.and.eggs();

console.log(url);
// http://google.com/search/products/bacon/and/eggs
const stories = [
  {
    id: 'story-1',
    name: 'Blabla',
    author: {
      person: 'person-1',
    },
    liked_by: {
      people: ['person-1', 'person-2'],
    },
    read_by: {
      people: ['person-1', 'person-2']
    },
  },
  {
    ...
  }
];
const people = [
  {
    id: 'person-1',
    name: 'Per',
    authored: {
      stories: ['story-1'],
    },
    read: {
      stories: ['story-1'],
    },
    liked: {
      stories: ['story-1'],
    },
  },
  {
    ...
  }
];
const stories = [
  {
    id: 'story-1',
    name: 'Blabla',
    author: {
      person: 'person-1',
    },
    liked_by: {
      people: ['person-1', 'person-2'],
    },
    read_by: {
      people: ['person-1', 'person-2']
    },
  },
  {
    ...
  }
];
const people = [
  {
    id: 'person-1',
    name: 'Per',
    authored: {
      stories: ['story-1'],
    },
    read: {
      stories: ['story-1'],
    },
    liked: {
      stories: ['story-1'],
    },
  },
  {
    ...
  }
];
const stories = [
  {
    id: 'story-1',
    name: 'Blabla',
    author: {
      person: 'person-1',
    },
    liked_by: {
      people: ['person-1', 'person-2'],
    },
    read_by: {
      people: ['person-1', 'person-2']
    },
  },
  {
    ...
  }
];
const people = [
  {
    id: 'person-1',
    name: 'Per',
    authored: {
      stories: ['story-1'],
    },
    read: {
      stories: ['story-1'],
    },
    liked: {
      stories: ['story-1'],
    },
  },
  {
    ...
  }
];
const graph = {
  stories: [...],
  people: [...]
}

Plain JSON

const nameOfFirstLiker =
graph.stories[0].liked_by.people[0].name

Deeply accessing a JSON "graph"

const nameOfFirstLiker =
graph.stories[0].liked_by.people[0].name

const nameOfFirstLike =
graph.people[0].author.person.likes[0].name
const nameOfFirstLiker = graph.stories[0].liked_by.people

The very explicit way

const nameOfFirstLiker = graph.stories[0].liked_by.people
.map(({ id }) => graph.people.find((p) => p.id === id))
const nameOfFirstLiker = graph.stories[0].liked_by.people
.map(({ id }) => graph.people.find((p) => p.id === id))
.find((person, index) => index === 0)
const nameOfFirstLiker = graph.stories[0].liked_by.people
.map(({ id }) => graph.people.find((p) => p.id === id))
.find((person, index) => index === 0)
.name;
const nameOfFirstLiker = graph.stories[0].liked_by.people
.map(({ id }) => graph.people.find((p) => p.id === id))
.find((person, index) => index === 0)
.name;

const nameOfFirstLike = graph.people[0].author.person.likes.stories
.map(({ id }) => graph.stories.find((s) => s.id === id))
.find((story, index) => index === 0)
.name;

MongoDB Populate?

var mongoose = require('mongoose')
  , Schema = mongoose.Schema
  
var personSchema = Schema({
  _id     : Number,
  name    : String,
  age     : Number,
  stories : [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});

var storySchema = Schema({
  _creator : { type: Number, ref: 'Person' },
  title    : String,
  fans     : [{ type: Number, ref: 'Person' }]
});

var Story  = mongoose.model('Story', storySchema);
var Person = mongoose.model('Person', personSchema);

Schemaless

{
  id: 'person-1',
  name: 'Per',
  authored: {
    stories: ['story-1'],
  },
  read: {
    stories: ['story-1'],
  }
}
{
  id: 'person-1',
  name: 'Per',
  authored: {
    stories: ['story-1'],
  },
  read: {
    stories: ['story-1'],
  },
  liked: {
    stories: ['story-1'],
  }
}
{
  id: 'person-1',
  name: 'Per',
  authored: {
    stories: ['story-1'],
  },
  lists: [{
    name: 'Summer reading',
    stories: [
      'story-1',
      'story-2',
      'story-3',
    ]
  }]
}

Naming conventions

{
  id: 'story-1',
  name: 'Story of Your Life',
  author: {
    person: 'ted-chiang-1'
  }
}
{
  id: 'story-1',
  name: 'Story of Your Life',
  author: {
    person: {
      id: 'ted-chiang-1',
      name: 'Ted Chiang',
      likes: {
        stories: ['story-1'],
      }
    }
  }
}
{
  id: 'story-1',
  name: 'Story of Your Life',
  author: {
    person: {
      id: 'ted-chiang-1',
      name: 'Ted Chiang',
      likes: {
        stories: [{
          id: 'story-1',
          name: 'Story of Your Life',
          ...
        }],
      }
    }
  }
}

Object.assign (eager)

function populateByAssign(depth = 0, collections = {}, object) {
  if (!object) { return object;}
  const collectionKeys = Object.keys(collections);

  function populateRecursively(depthLeft, subItem) {
    if (!subItem) {
      return subItem;
    }

    const objectKeys = Object.keys(subItem);

    return objectKeys.reduce((result, key) => {
      if (
        collectionKeys.includes(key) &&
        Array.isArray(subItem[key])
      ) {
        return {
          ...result,
          [key]: !depthLeft ? (
            subItem[key]
          ) : (
            subItem[key]
            .map(getByIdFromCollection(collections[key]))
            .map((item) => populateRecursively(depthLeft - 1, item))
          )
        };
      } else if (
        collectionKeys.includes(key) &&
        subItem[key] &&
        typeof subItem[key] === 'object'
      ) {
        return {
          ...result,
          [key]: !depthLeft ? (
            subItem[key]
          ) : (
            Object.keys(subItem[key]).reduce((result, id) => {
              return {
                ...result,
                [id]: populateRecursively(depthLeft - 1, getByIdFromCollection(collections[key])(id))
              }
            }, { ...subItem[key] })
          )
        };
      } else if (collectionKeys.includes(plural(key))) {
        return {
          ...result,
          [key]: !depthLeft ? (
            subItem[key]
          ) : (
            populateRecursively(depthLeft - 1, getByIdFromCollection(collections[plural(key)])(subItem[key]))
          )
        };
      } else if (Array.isArray(subItem[key])) {
        return {
          ...result,
          [key]: !depthLeft ? (
            subItem[key]
          ) : (
            subItem[key]
            .map((item) => populateRecursively(depthLeft, item))
          )
        };
      } else if (subItem[key] && typeof subItem[key] === 'object') {
        return {
          ...result,
          [key]: !depthLeft ? (
            subItem[key]
          ) : (
            populateRecursively(depthLeft, subItem[key])
          )
        }
      } else {
        return result;
      }
    }, { ...subItem });
  }

  return Array.isArray(object) ? (
    object.map((item) => populateRecursively(depth, item))
  ) : (
    populateRecursively(depth, object)
  );
}

Object.assign (eager)


const populatedNode = populate(                 )

const populatedNode = populate(depth,           )

const populatedNode = populate(depth, graph,    )

const populatedNode = populate(depth, graph, obj)

const populatedNode = populate(depth, graph, obj)
const populatedPerson = populate(3, graph, graph.people[0])

Object.assign (eager)

{
  id: 'story-1',
  name: 'Story of Your Life',
  author: {
    person: 'ted-chiang-1'
  }
}
{
  id: 'story-1',
  name: 'Story of Your Life',
  author: {
    person: {
      id: 'ted-chiang-1',
      name: 'Ted Chiang',
      likes: {
        stories: ['story-1'],
      }
    }
  }
}
{
  id: 'story-1',
  name: 'Story of Your Life',
  author: {
    person: {
      id: 'ted-chiang-1',
      name: 'Ted Chiang',
      likes: {
        stories: [{
          id: 'story-1',
          name: 'Story of Your Life',
          ...
        }],
      }
    }
  }
}


// Boom

Proxy (lazy)

function populateByProxy(depth = 0, collections = {}, object) {
  if (!object || typeof object !== 'object') {
    return object;
  }

  const collectionKeys = Reflect.ownKeys(collections);

  const handler = {
    get(target, key, receiver) {
      const value = Reflect.get(target, key, receiver);

      if (typeof key === 'symbol' || depth <= 0) {
        return value;
      }
      if (Array.isArray(value) && collectionKeys.includes(key)) {
        return value
        .map(getByIdFromCollection(collections[key]))
        .map(populateByProxy.bind(null, depth - 1, collections));
      }
      if (typeof value === 'object' && collectionKeys.includes(key)) {
        return Reflect.ownKeys(value)
        .map(getByIdFromCollection(collections[key]))
        .filter(x => x)
        .map(populateByProxy.bind(null, depth - 1, collections))
        .reduce((res, item) => Object.assign({}, res, { [item.id]: item }), {});
      }
      if (collectionKeys.includes(plural(key))) {
        return populateByProxy(
          depth - 1,
          collections,
          getByIdFromCollection(collections[plural(key)])(value)
        );
      }
      if (value && typeof value === 'object') {
        return populateByProxy(depth, collections, value);
      }

      return value;
    },
  };

  return Array.isArray(object) ? (
    object.map((item) => populateByProxy(depth, collections, item))
  ) : (
    new Proxy(object, handler)
  );
}

Proxy (lazy)


const populatedNode = populate(depth, graph, obj)
const populatedPerson = populate(3, graph, graph.people[0])

Proxy (lazy)

{
  id: 'story-1',
  name: 'Story of Your Life',
  author: {
    person: 'ted-chiang-1'
  }
}
{
  id: 'story-1',
  name: 'Story of Your Life',
  author: {
    person: {
      id: 'ted-chiang-1',
      name: 'Ted Chiang',
      likes: {
        stories: ['story-1'],
      }
    }
  }
}
{
  id: 'story-1',
  name: 'Story of Your Life',
  author: {
    person: {
      id: 'ted-chiang-1',
      name: 'Ted Chiang',
      likes: {
        stories: [{
          id: 'story-1',
          name: 'Story of Your Life',
          ...
        }],
      }
    }
  }
}

Proxy (lazy)

const stories = [
  {
    id: 'story-1',
    name: 'Story of Your Life',
    author: {
      person: 'ted-chiang-1',
    }
  },
  {
    ...
  }
];
const people = [
  {
    id: 'ted-chiang-1',
    name: 'Ted Chiang',
    authored: {
      stories: ['story-1'],
    },
  },
  {
    ...
  }
];

Proxy (lazy)

const stories = [
  {
    id: 'story-1',
    name: 'Story of Your Life',
    author: {
      person: 'ted-chiang-1',
    }
  },
  {
    ...
  }
];
const people = [
  {
    id: 'ted-chiang-1',
    name: 'Ted Chiang',
    authored: {
      stories: ['story-1'],
    },
  },
  {
    ...
  }
];

Proxy (lazy)

const stories = [
  {
    id: 'story-1',
    name: 'Story of Your Life',
    author: {
      person: 'ted-chiang-1',
    }
  },
  {
    ...
  }
];
const people = [
  {
    id: 'ted-chiang-1',
    name: 'Ted Chiang',
    authored: {
      stories: ['story-1'],
    },
  },
  {
    ...
  }
];

Proxy (lazy)

const stories = [
  {
    id: 'story-1',
    name: 'Story of Your Life',
    author: {
      person: 'ted-chiang-1',
    }
  },
  {
    ...
  }
];
const people = [
  {
    id: 'ted-chiang-1',
    name: 'Ted Chiang',
    authored: {
      stories: ['story-1'],
    },
  },
  {
    ...
  }
];

Proxy (lazy)

const stories = [
  {
    id: 'story-1',
    name: 'Story of Your Life',
    author: {
      person: 'ted-chiang-1',
    }
  },
  {
    ...
  }
];
const people = [
  {
    id: 'ted-chiang-1',
    name: 'Ted Chiang',
    authored: {
      stories: ['story-1'],
    },
  },
  {
    ...
  }
];

Proxy (lazy)


const nameOfFirstLiker =
graph.stories[0].liked_by.people[0].name

const nameOfFirstLike =
graph.people[0].author.person.likes[0].name

Seatbelts for JavaScript


TypeError: undefined is not a function

TypeError: Cannot read property 'undefined' of undefined

Redefining Undefined

const Undefined = new Proxy(function() {}, {










});
const Undefined = new Proxy(function() {}, {
  get(target, key, receiver) {





  },



});
const Undefined = new Proxy(function() {}, {
  get(target, key, receiver) {
    if (key === 'name') {
      return 'Undefined';
    }

    return Undefined;
  },



});
const Undefined = new Proxy(function() {}, {
  get(target, key, receiver) {
    if (key === 'name') {
      return 'Undefined';
    }

    return Undefined;
  },
  apply() {

  },
});
const Undefined = new Proxy(function() {}, {
  get(target, key, receiver) {
    if (key === 'name') {
      return 'Undefined';
    }

    return Undefined;
  },
  apply() {
    return Undefined;
  },
});
function seatbelt(obj) {













}

Seatbelts

function seatbelt(obj) {
  return new Proxy(obj, {











  });
}
function seatbelt(obj) {
  return new Proxy(obj, {
    get(target, key) {









    }
  });
}
function seatbelt(obj) {
  return new Proxy(obj, {
    get(target, key) {
      const accessedProperty = Reflect.get(target, key);








    }
  });
}
function seatbelt(obj) {
  return new Proxy(obj, {
    get(target, key) {
      const accessedProperty = Reflect.get(target, key);

      if (typeof accessedProperty === 'object') {
        return seatbelt(accessedProperty);
      }




    }
  });
}
function seatbelt(obj) {
  return new Proxy(obj, {
    get(target, key) {
      const accessedProperty = Reflect.get(target, key);

      if (typeof accessedProperty === 'object') {
        return seatbelt(accessedProperty);
      } else {
        return accessedProperty ?
        accessedProperty :
        Undefined;
      }
    }
  });
}
const neverUndefined = seatbelt({





});
const neverUndefined = seatbelt({
  foo: 'bar',
  baz: {
    qux: 10,
    name: 'yoyo',
  },
});
neverUndefined.foo
neverUndefined.foo
// bar
neverUndefined.foo
// bar
neverUndefined.baz.qux
neverUndefined.foo
// bar
neverUndefined.baz.qux
// 10
neverUndefined.foo
// bar
neverUndefined.baz.qux
// 10
neverUndefined.baz.name
neverUndefined.foo
// bar
neverUndefined.baz.qux
// 10
neverUndefined.baz.name
// yoyo

neverUndefined

neverUndefined.notHere

neverUndefined.notHere.notAfunction

neverUndefined.notHere.notAfunction()

neverUndefined.notHere.notAfunction().nope

neverUndefined.notHere.notAfunction().nope
// [Function: Undefined]

neverUndefined.notHere.notAfunction().nope
// [Function: Undefined]

neverUndefined.foo.b.a.g.e.d().sf().d

neverUndefined.notHere.notAfunction().nope
// [Function: Undefined]

neverUndefined.foo.b.a.g.e.d().sf().d
// [Function: Undefined]

Winning at JavaScript

Why can't I do this?? 🤔




Object.prototype = seatbelt({});
/**
 * Nobel prize material
 */
Object.prototype = seatbelt({});

Conclusion

Good

  • Powerful abstractions
  • Possibility for lazyness
  • Adding functionality transparently

Bad

  • "Magic"
  • *Performance

Build your
DREAMS INTERFACES

JavaScript Metaprogramming with Proxies

By Eirik Langholm Vullum

JavaScript Metaprogramming with Proxies

  • 4,224