From e5d5b7468e08915d1d170fcb1cd8a8c1eaeb0b21 Mon Sep 17 00:00:00 2001 From: Marcus Noble Date: Fri, 26 Jan 2018 20:27:34 +0000 Subject: [PATCH] Added getters/setters draft --- .../getters-and-setters-in-javascript.md | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 src/drafts/getters-and-setters-in-javascript.md diff --git a/src/drafts/getters-and-setters-in-javascript.md b/src/drafts/getters-and-setters-in-javascript.md new file mode 100644 index 0000000..38a6330 --- /dev/null +++ b/src/drafts/getters-and-setters-in-javascript.md @@ -0,0 +1,175 @@ +--- +layout: post.html +title: "Getters and Setters in JavaScript" +date: 2018-01-27 +tags: JavaScript +summary: "I was recently asked by [Ruth](https://twitter.com/rumyra) to explain what the purpose of getters and setters are in JavaScript and how to use them. This led to me somewhat ramble about all I know on the subject in the [JSOxford](https://jsoxford.com) channel of the [Digital Oxford Slack](http://slack.digitaloxford.com/). People seemed to find what I said useful so I thought it best to write a more coherent version." +--- + +I was recently asked by [Ruth](https://twitter.com/rumyra) to explain what the purpose of getters and setters are in JavaScript and how to use them. This led to me somewhat ramble about all I know on the subject in the [JSOxford](https://jsoxford.com) channel of the [Digital Oxford Slack](http://slack.digitaloxford.com/). People seemed to find what I said useful so I thought it best to write a more coherent version. + +## What are they? + +The simplest way I like to think about them is as a way to create dynamic values that look like normal properties to those using them. I think the best way to explain them is with an example. + +```js +class Person { + constructor(firstName, lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + get fullName() { + return this.firstName + ' ' + this.lastName; + } +} + +const ruth = new Person('Ruth', 'John'); +console.log(ruth.fullName); // "Ruth John" +``` + +Here we have a Person class that takes in a first and last name when instantiating and we have a getter that dynamically returns the full name when called. Notice we're not calling `fullName` as a function, it just looks like any other property to those interacting with the object. + +As we only have a getter specified for `fullName` we can't set anything to it. + +```js +console.log(ruth.fullName); // "Ruth John" +ruth.fullName = "Someone Else"; +console.log(ruth.fullName); // Still "Ruth John" +``` + +Setters work in a very similar way. They take in a single value and do something with it. + +```js +set fullName(val) { + const names = val.split(' '); + this.firstName = names[0]; + this.lastName = names[1]; +} + +... + +ruth.fullName = "Someone Else"; +console.log(ruth.firstName); // "Someone" +console.log(ruth.lastName); // "Else" +``` + +## But why? + +So you might be looking at this and wondering "why not just use a function"? + +There's a couple of reasons I think they are useful. First they give the consumer of the object a single way to interact with data, it doesn't need to know the different between static values and calculated values. The second reason is the ability to set a getter as being enumerable (more on this below) which would mean it would be included when doing things like `JSON.stringify` so you can have a string representation of a calculated value that you can then persist or send over the wire. + +> Note: Using the `get` and `set` keywords are just syntactical sugar that provides some default properties. See below about how to have more control over the properties you define. + +One very useful usecase for setters is when you have an existing object with a well used interface, let's say something that takes a width, and it's decided at a later date that some validation is wanted on this field. Rather than having to introduce a breaking change you can use a setter and implement the validation there, rather than having to have all clients update the way they interact with your object. + + +```js +class Shape { + constructor() { + this.width = 0; + } +} + +const box = new Shape(); +box.width = 100; // OK +box.width = 'Cake'; // Also OK + +// Now we want to add validation + +class Shape { + constructor() { + this.width = 0; + } + + set width(val) { + if (typeof val !== 'number') { + throw new Error('Must be a number'); + } + this._width = val; + } +} + +const box = new Shape(); +box.width = 100; // OK +box.width = 'Cake'; // Throws +``` + +> Note: I am using `this._width` here to indicate a "private" property. In reality JavaScript doesn't have this ability so is more just a convention. + +## Having more control + +You can get more control when using `Object.defineProperty` rather than the `get` or `set` keyword. This allows you to specify things like whether the property is enumerable or not (useful for `JSON.stringify` as mentioned above). + +```js +class Person { + constructor(firstName, lastName) { + this.firstName = firstName; + this.lastName = lastName; + + Object.defineProperty(this, 'fullName', { + enumerable: true, + get: function() { + return this.firstName + ' ' + this.lastName; + } + }); + } +} + +const ruth = new Person('Ruth', 'John'); +console.log(JSON.stringify(ruth)); // "{\"firstName\":\"Ruth\",\"lastName\":\"John\",\"fullName\":\"Ruth John\"}" +``` + +Note that `fullName` is now stringified thanks to it being marked as `enumerable`. + +> One thing to be careful of here is if you have an object with an enumerable getter but no setter (as we have here) and you try and do `Object.assign(new Person, JSON.parse(stringifiedRuthObject))` you will get an error: +> +> TypeError: Cannot set property fullName of #\ which has only a getter + +`Object.defineProperty` can do more than just getters and setters too. It also has the ability to create read only properties. + +```js +class Person { + constructor(firstName, lastName) { + this.firstName = firstName; + this.lastName = lastName; + + Object.defineProperty(this, 'isAwesome', { + value: true // note we don't specify `writable` which defaults to false + }); + } +} + +const ruth = new Person('Ruth', 'John'); +console.log(ruth.isAwesome); // true +ruth.isAwesome = false; +console.log(ruth.isAwesome); // Still true! +``` + +## Can I use it? + +Throughout this post I've only used classes to demonstrate the usage of getters and setters but it's not needed, you can use `Object.defineProperty` on normal objects too and is supported all the way back to **IE9**. + +``` +function Person(firstName, lastName) { + this.firstName = firstName; + this.lastName = lastName; + + Object.defineProperty(this, 'fullName', { + get: function() { + return this.firstName + ' ' + this.lastName; + } + }); +} + +var ruth = new Person('Ruth', 'John'); +console.log(ruth.fullName); // "Ruth John" +``` + +## Related + +It's also been possible to do similar in other ways. The now deprecated `Object.observe` could do similar to an instantiated object and the more recent introduction of `Proxy` that allows you to wrap an instantiated object and intercept actions performed on it. But those are best left for another day. + +It's worth checkout out the MDN documentation for [Object.defineProperty](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) to see all the configuration available. +