Eirik Vullum

- Independent Consultant

- JavaScript Training

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

Macros

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

Make your own 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})`;
};

Make your own 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

(before the code runs)

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)

Proxies








  new Proxy(target, handler)

Proxies in JavaScript

More powerful intercession

Fundamental operations


  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 37;
  }
};
var handler = {
  get: function(target, name) {
    return 37;
  }
};

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

var p = new Proxy({}, handler);

console.log(p.a); // 37
var handler = {
  get: function(target, name) {
    return 37;
  }
};

var p = new Proxy({}, handler);

console.log(p.a); // 37
console.log(p.b); // 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,
});

Overload fundamental operators like . = and ()

Instrumentation

function logAccessToProperties(obj) {










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








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


    },




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

    },




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




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


    }
  });
}
function logAccessToProperties(obj) {
  return new Proxy(obj, {
    get(target, key) {
      console.log('Accessed key', key);
      return Reflect.get(target, key);
    },
    set(target, key, value) {
      console.log('Updated key', key, 'to', value);

    }
  });
}
function logAccessToProperties(obj) {
  return new Proxy(obj, {
    get(target, key) {
      console.log('Accessed key', key);
      return Reflect.get(target, key);
    },
    set(target, key, value) {
      console.log('Updated key', key, 'to', value);
      Reflect.set(target, key, value);
    }
  });
}

Instrumentation

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

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

const personWithAccessLogging =
logAccessToProperties(person);

personWithAccessLogging.age;
const person = {
  name: 'Eirik',
  age: 30,
};

const personWithAccessLogging =
logAccessToProperties(person);

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

const personWithAccessLogging =
logAccessToProperties(person);

personWithAccessLogging.age;
// Accessed age

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

const personWithAccessLogging =
logAccessToProperties(person);

personWithAccessLogging.age;
// Accessed age

personWithAccessLogging.age = 31;
// Updated key age to 31

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 });
    }
  };
}

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
const stories = [
  {
    id: 'story-1',
    title: 'The Last Question',
    author: {
      $ref: 'people',
      id: 'person-1'
    },
    liked_by: [
      { $ref: 'people', id: 'person-1' },
      { $ref: 'people', id: 'person-2' }
    ],
    read_by: [
      { $ref: 'people', id: 'person-1' },
      { $ref: 'people', id: 'person-2' }
    ],
  },
  {
    ...
  }
];
const people = [
  {
    id: 'person-1',
    name: 'Isaac Asimov',
    authored: [
      { $ref: 'stories', id: 'story-1' },
    ],
    read: [
      { $ref: 'stories', id: 'story-1' },
    ],
    liked: [
      { $ref: 'stories', id: 'story-1' },
    ],
  },
  {
    ...
  }
];
const stories = [
  {
    id: 'story-1',
    title: 'The Last Question',
    author: {
      $ref: 'people',
      id: 'person-1'
    },
    liked_by: [
      { $ref: 'people', id: 'person-1' },
      { $ref: 'people', id: 'person-2' }
    ],
    read_by: [
      { $ref: 'people', id: 'person-1' },
      { $ref: 'people', id: 'person-2' }
    ],
  },
  {
    ...
  }
];
const people = [
  {
    id: 'person-1',
    name: 'Isaac Asimov',
    authored: [
      { $ref: 'stories', id: 'story-1' },
    ],
    read: [
      { $ref: 'stories', id: 'story-1' },
    ],
    liked: [
      { $ref: 'stories', id: 'story-1' },
    ],
  },
  {
    ...
  }
];
const stories = [
  {
    id: 'story-1',
    title: 'The Last Question',
    author: {
      $ref: 'people',
      id: 'person-1'
    },
    liked_by: [
      { $ref: 'people', id: 'person-1' },
      { $ref: 'people', id: 'person-2' }
    ],
    read_by: [
      { $ref: 'people', id: 'person-1' },
      { $ref: 'people', id: 'person-2' }
    ],
  },
  {
    ...
  }
];
const people = [
  {
    id: 'person-1',
    name: 'Isaac Asimov',
    authored: [
      { $ref: 'stories', id: 'story-1' },
    ],
    read: [
      { $ref: 'stories', id: 'story-1' },
    ],
    liked: [
      { $ref: 'stories', id: 'story-1' },
    ],
  },
  {
    ...
  }
];
const graph = {
  stories: [...],
  people: [...]
}

Plain JSON

Deeply traversing a

JSON "graph"

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

const titleOfFirstLikedStoryByAuthor =
const nameOfFirstLiker =
graph.stories[0].liked_by
.map(({ $ref, id }) => graph[$ref].find((p) => p.id === id))[0]
.name;

const titleOfFirstLikedStoryByAuthor =
graph[graph.stories[0].author.$ref]
const nameOfFirstLiker =
graph.stories[0].liked_by
.map(({ $ref, id }) => graph[$ref].find((p) => p.id === id))[0]
.name;

const titleOfFirstLikedStoryByAuthor =
graph[graph.stories[0].author.$ref]
.find((p) => p.id === graph.stories[0].author.id))
const nameOfFirstLiker =
graph.stories[0].liked_by
.map(({ $ref, id }) => graph[$ref].find((p) => p.id === id))[0]
.name;

const titleOfFirstLikedStoryByAuthor =
graph[graph.stories[0].author.$ref]
.find((p) => p.id === graph.stories[0].author.id))
.title;
const nameOfFirstLiker =
graph.stories[0].liked_by[0].name

Deeply traversing a

JSON "graph"

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

const titleOfFirstLikedStoryByAuthor =
graph.stories[0].author.likes[0].title

Populate

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

const populatedNode = populate(          )

const populatedNode = populate(          )

const populatedNode = populate(graph,    )

const populatedNode = populate(graph, node)

const populatedNode = populate(graph, node)
const populatedStory = populate(graph, graph.stories[0])

Populate

Naive Approach

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


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

Proxy Approach

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

Proxy Approach

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

Proxy Approach

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

Proxy Approach

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

Proxy Approach

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

Proxy Approach


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

const titleOfFirstLikedStory =
graph.people[0].author.likes[0].title

Proxy Approach

github.com/eiriklv/json-populate


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 for JavaScript

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 == undefined ?
        Undefined :
        accessedProperty;
      }
    }
  });
}
const iNeverFail = seatbelt({





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

iNeverFail

iNeverFail.notHere

iNeverFail.notHere.notAfunction

iNeverFail.notHere.notAfunction()

iNeverFail.notHere.notAfunction().nope

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

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

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

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

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

Winning at JavaScript

Why can't I do this?? 🤔




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

Conclusion

Good

  • Powerful abstractions
  • Add functionality transparently

Bad

  • "Magic"

Another tool in your toolbox to build your
DREAM INTERFACES

@eiriklv

JSBP - JavaScript Metaprogramming with Proxies

By Eirik Langholm Vullum

JSBP - JavaScript Metaprogramming with Proxies

  • 3,215