-
Notifications
You must be signed in to change notification settings - Fork 0
/
javascript_oop_notes.js
356 lines (303 loc) · 12.1 KB
/
javascript_oop_notes.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
// Constructors and new
// In ES5, we have no class keyword. Instead, we write functions that act as Object Constructors, or blueprints for creating particular objects
// An Object Constructor is a function that returns objects.
function personConstructor(name, age) {
// an object literal that will be returned
const person = {};
// attributes of a person
person.name = name;
person.age = age;
// when attached to an object or instance, functions are called 'methods'.
// this is our first method, greet
person.greet = function(){
console.log("Hello my name is " + person.name + " and I am " + person.age + " years old!");
}
// finally, this function must return an instance
return person;
}
// create the 'steve' instance, run greet
const steve = personConstructor("Steve", 27);
steve.greet();
// create the 'anika' instance, run greet. note that it is different.
const anika = personConstructor("Anika", 33);
anika.greet();
// finally note how we can refine the greet method for any particular instance
const emily = personConstructor("Emily", 22);
emily.greet = function() {
console.log("I am the greatest, ever!");
};
emily.greet();
// In the above example, we created an object literal at the top of the scope, and returned it at the bottom. There is nothing special about these objects, every instance is unique, and we can modify their methods at any time (like we did with the emily instance!)
// we can use the this keyword to store our attributes and methods, and the new keyword to create new instances.
function personConstructor(name, age) {
this.name = name;
this.age = age;
this.greet = function() {
console.log("Hello my name is " + this.name + " and I am " + this.age + " years old!");
}
}
// the 'new' keyword causes our constructor to return the object we expected
const anika = new personConstructor('Anika', 33);
anika.greet();
console.log(anika);
// using this & new, we can now refer to the 'name' attribute of our instance!
const emily = new personConstructor("Emily", 22);
emily.greet = function() {
console.log("My name is " + this.name + " and I'm the coolest ever!");
}
emily.greet();
// Private variables
// the naming convention for Classes and Object Constructors is that they're capitalized and singular
function Person(name, age) {
// create a private variable that stores a reference to the new object we create
const self = this;
const privateVariable = "This variable is private";
const privateMethod = function() {
console.log("this is a private method for " + self.name);
console.log(self);
}
this.name = name;
this.age = age;
this.greet = function() {
console.log("Hello my name is " + this.name + " and I am " + this.age + " years old!");
// we can access our attributes within the constructor!
console.log("Also my privateVariable says: " + privateVariable)
// we can access our methods within the constructor!
privateMethod();
}
}
const joe = new Person("Joe", 23);
joe.greet();
// .prototype
// In JavaScript, all objects have a prototype that they inherit methods and properties from. When working with OOP, it is important to be aware of what the prototype is and how we can access it.
const MyObjConstructor = function(name) {
const myPrivateVar = "Hello"; // just to show that it is hard to see this private var easily
this.name = name; // but you can see the name!
this.method = function() {
console.log( "I am a method");
};
}
const obj1 = new MyObjConstructor('object1');
const obj2 = new MyObjConstructor('object2');
console.log(obj1);
obj1.newProperty = "newProperty!";
obj1.__proto__.anotherProperty = "anotherProperty!";
console.log(obj1.anotherProperty); // anotherProperty!
console.log(obj1.newProperty); // newProperty!
// What about obj2?
console.log(obj2.newProperty); // undefined
console.log(obj2.anotherProperty); // anotherProperty! <= THIS IS THE COOL PART!
// While, expectedly, the line obj1.newProperty = 'newProperty!' gave obj1 a new property that obj2 couldn't access, the code obj1.__proto__.anotherProperty = 'anotherProperty!' can be accessed by both obj1 and obj2. That's because they both share a prototype object since they're both instances of MyObjConstructor.
// Major PROS of Prototype
//
// One memory space for all! If you are creating lots of the same object and use prototype, it can save you significant memory
// Great for general methods for objects
// We can access prototype methods with just using .method or .property.
// The interpreter will go through all prototypes in the prototype chain to check if any of them have the called method or property before giving up (it'll return/use the first match it finds).
// Major CONS of Prototype
//
// Methods generated in prototype cannot access the private variables inside the constructor function
// Lots of prototypes can be hard to read
// After we create our MyObjConstructor:
MyObjConstructor.prototype.methodName = function() {
//do stuff here!
}
function Cat(catName) {
const name = catName;
this.getName = function() {
return name;
};
}
//adding a method to the cat prototype
Cat.prototype.sayHi = function() {
console.log('meow');
};
//adding properties to the cat prototype
Cat.prototype.numLegs = 4;
const muffin = new Cat('muffin');
const biscuit = new Cat('biscuit');
console.log(muffin, biscuit);
//we access prototype properties the same way as we would access 'own' properties
muffin.sayHi();
biscuit.sayHi();
console.log(muffin.numLegs);
// poor mutant cats: muffin.__proto__.numLegs ++;
// doing this to muffin will mess up all the cats!
// Prototype methods make our code faster. If we were creating thousands of instances, adding methods to the shared prototype will improve the performance of our code significantly! However, if you are only going to have a small amount of instances, you should balance prototype methods with readability. We only get performance gains from prototype methods when using a large number of instances.
// Define the class
function Person(name, age) {
this.name = name;
this.age = age;
}
// Attach class methods using .prototype
Person.prototype.greet = function() {
console.log("Hello my name is " + this.name + " and I am " + this.age + " years old!");
return this;
};
// Create new instances with the new keyword
const amelia = new Person('Amelia', 36);
// Create instance methods by attaching the function directly to an instance
amelia.sing = function() {
console.log("Lalalala!");
};
// Soft Privacy & Method Chaining
// In order to keep data private in a particular instance, we just need to leverage JavaScripts scoping rules. By creating variables scoped to the Object Constructor, we keep them out of the global scope. To read and update private data, we'll need to write getter and setter methods.
//
// Additionally, we can chain methods together by returning this. Essentially, whenever we tell a public or prototype method to return this, we're asking for the entire object back. This lets us chain function calls together.
// Private variables are scoped to the constructor with the 'let' keyword
function Car(make, model) {
let odometer = 0;
this.make = make;
this.model = model;
// To make functions private, we scope them to the constructor
function updateOdometer(distance) {
odometer += distance;
};
// 'Getter' functions help us read private variables
this.readOdometer = function() {
return odometer;
}
// 'Setter' functions help us update private variables
this.drive = function(distance) {
updateOdometer(distance);
// return this will allow us to chain methods
return this;
}
}
const myCarInstance = new Car("Chevy", "Camaro");
// by returning this, we can chain drive()
myCarInstance.drive(50).drive(90);
// private variable is undefined
console.log(myCarInstance.odometer);
// but we can read it with our getter function
console.log(myCarInstance.readOdometer());
//
// Classes in ES6
// ES6's Classes are just syntactic wrappers around the Object Constructors we've already learned.
//
// All ES6 classes have a constructor, and the constructor always runs whenever we create a new instance.
// Classes are NOT hoisted. No matter what, the class keyword will stay where it was written and not move during interpretation.
//
// ES6 vs ES5:
// Old ES5 way
function Dot(x, y) {
this.x = x;
this.y = y;
}
Dot.prototype.showLocation = function() {
console.log("This Dot is at x " + this.x + " and y " + this.y);
}
const dot1 = new Dot(55, 20);
dot1.showLocation();
// New ES6 way
class Dot {
constructor(x, y) {
this.x = x;
this.y = y;
}
showLocation() {
// ES6 String Interpolation!
console.log(`This Dot is at x ${this.x} and y ${this.y}`);
}
}
const dot2 = new Dot(5, 13);
dot2.showLocation();
// class methods are called 'static methods`, while instance methods are called 'prototype methods'.
// Here we added a static method called getHelp(). This means that getHelp() is accessible from the Class, not the instance.
class Dot {
constructor(x, y) {
this.x = x;
this.y = y;
}
// prototype method
showLocation() {
console.log(`This Dot is at x ${this.x} and y ${this.y}`);
}
// static method
static getHelp() {
console.log("This is a Dot class, for created Dots! A Dot takes x and y coordinates, type 'new Dot' to create one!");
}
}
const dot3 = new Dot(4, 2);
// we can see showLocation from our instance...
console.log(dot3.showLocation);
// but we can't see getHelp
console.log(dot3.getHelp);
// however we can call getHelp this way:
Dot.getHelp();
// Inheritance is much easier with the ES6 class syntax. Using the extends keyword, we can define new classes that inherit from existing classes. Inheritance is a common aspect of OO programming, and it's important to see how JavaScript does it a little differently.
// Super is a special function that allows us to call the constructor of the parent class. Just like how Dot needs an x and y value, the super() of our Circle class requires that exact same thing.
// parent Dot class
class Dot {
constructor(x, y) {
this.x = x;
this.y = y;
}
showLocation() {
console.log(`This ${ this.constructor.name } is at x ${this.x} and y ${this.y}`);
}
}
// child Circle class
class Circle extends Dot {
constructor(x, y, radius) {
super(x, y);
this.radius = radius;
}
}
// Another important property of super is we can call Parent methods with it. Consider this example:
// Much like how we use super() to call the parent constructor, super can also be used to call other methods from the parent!
// parent Dot class
class Dot {
constructor(x, y) {
this.x = x;
this.y = y;
}
showLocation() {
console.log(`This ${ this.constructor.name } is at x ${ this.x } and y ${ this.y }`);
}
// simple method in the parent class
parentFunction(){
return "This is coming from the parent!";
}
}
// child Circle class
class Circle extends Dot {
constructor(x, y, radius) {
super(x, y);
this.radius = radius;
}
// simple function in the child class
childFunction() {
// by using super, we can call the parent method
const message = super.parentFunction();
console.log(message);
}
}
const circle = new Circle(1, 2, 3);
circle.childFunction();
// A common way to read and update attributes within our objects is to use Getters and Setters. While we can recreate this technique in many situations, JavaScript supports Getters and Setters syntactically.
class Pizza {
constructor(radius, slices) {
this.radius = radius;
this._slices = slices;
}
get slices () {
return this._slices;
}
set slices (slices) {
this._slices = slices;
}
};
// Using these same patterns, we can create custom Getters. Consider the following snippet:
class Circle{
constructor(x, y, radius) {
this.x = x;
this.y = y;
this.radius = radius;
}
get diameter() {
return this.radius * 2;
}
}
const circle1 = new Circle(1, 2, 5);
console.log(circle1.diameter);