Created
April 21, 2024 14:07
-
-
Save IwoHerka/578b09e94fa4be2974ae81eff89361df to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Objects ----------------------------------------------------------------------------- | |
// --- Creating --- | |
let user = new Object(); // "object constructor" syntax | |
let user = {}; // "object literal" syntax | |
// --- Adding and reading properties --- | |
user = { | |
name: "John", | |
age: 30 | |
}; | |
user.name; // -> "John" | |
user.age; // -> 30 | |
// --- Deleting attributes --- | |
delete user.age; | |
user.age; // -> undefined | |
// --- String keys --- | |
user = { | |
name: "John", | |
age: 30, | |
"likes birds": true | |
}; | |
user["likes birds"]; // - true | |
user["name"]; // -> "John" | |
user["age"]; // -> 30 | |
// - Reading property dynamically using [] notation | |
const key = "age"; | |
user[key]; // -> 30 | |
user.key; // -> undefined | |
// --- Computed properties --- | |
let fruit = "apple"; | |
let bag = { | |
[fruit]: 5 | |
}; | |
bag.apple; // -> 5 | |
bag.fruit; // -> undefined | |
bag = { | |
[fruit + "Computed"]: 10 | |
}; | |
bag.appleComputed; // -> 10 | |
// --- Property value shorthand --- | |
// Let's say we want to create user object: | |
// Ok, but verbose: | |
function makeUser(name, age) { | |
return { | |
name: name, | |
age: age | |
} | |
} | |
user = makeUser("John", 30); | |
user.name; // -> "John" | |
// Shorter: | |
function makeUser(name, age) { | |
return { | |
name, | |
age | |
} | |
} | |
user = makeUser("John", 30); | |
user.name; // -> "John" | |
// --- Invalid property names, numbers --- | |
let obj = { | |
// for: 1, | |
// let: 2, | |
// return: 3 | |
0: "test 0", | |
1: "test 1" | |
} | |
obj[0] // -> "test 0" | |
// --- Property existence test, "in" operator --- | |
obj.foo; // -> undefined | |
"foo" in obj; // -> false | |
// When value is set to 'undefined' by user | |
obj = { | |
test: undefined | |
} | |
obj.test // -> undefined | |
"test" in obj; // -> true | |
// --- The "for in" loop, iterating keys --- | |
user = { | |
name: "John", | |
age: 30, | |
isAdmin: true | |
}; | |
for (let k in user) { | |
console.log(k); // -> "name", "age", "isAdmin" | |
console.log(user[k]); // -> "John", 30, true | |
} | |
// --- Property order --- | |
// "Numeric" keys are first, the rest in creation order: | |
let codes = { | |
"49": "Germany", | |
"41": "Switzerland", | |
"44": "Great Britain", | |
"1": "USA", | |
"foo": "bar", | |
"bar": "spam" | |
}; | |
codes.spam = ''; | |
for (let code in codes) { | |
console.log(code); // 1, 41, 44, 49, 'foo, 'bar', 'spam' | |
} | |
// --- Objects are mutable --- | |
const foo = { bar: "spam" }; | |
foo.bar = "foo"; | |
foo.bar // -> "foo" | |
foo = {} // error | |
// --- Object references and comparison --- | |
let user = { name: "John" }; | |
let admin = user; | |
admin.name = 'Peter'; | |
user.name; // -> 'Peter' | |
user == admin // -> true | |
user === admin // -> true | |
{ foo: "bar" } == { foo: "bar" } // -> false | |
{ foo: "bar" } === { foo: "bar" } // -> false | |
// --- Naive cloning --- | |
let user = { | |
name: "John", | |
age: 30 | |
}; | |
let clone = {}; // the new empty object | |
// let's copy all user properties into it | |
for (let key in user) { | |
clone[key] = user[key]; | |
} | |
// now clone is a fully independent object with the same content | |
clone.name = "Pete"; // changed the data in it | |
user.name; // -> "John" still! | |
// --- Object.assign ---- | |
// First arg is destination object, | |
// The rest are sources | |
Object.assign(dest, ...sources) | |
// Example: | |
let user = { name: "John" }; | |
let permissions1 = { canView: true }; | |
let permissions2 = { canEdit: true }; | |
// copies all properties from permissions1 and permissions2 into user | |
Object.assign(user, permissions1, permissions2); | |
// now user = { name: "John", canView: true, canEdit: true } | |
user.name; // John | |
user.canView; // true | |
user.canEdit; // true | |
// --- Spread syntax --- | |
let user2 = {...user} | |
// --- Nested/deep cloning ---- | |
let user = { | |
name: "John", | |
sizes: { | |
height: 182, | |
width: 50 | |
} | |
}; | |
let clone = Object.assign({}, user); | |
user.sizes === clone.sizes; // true, same object | |
// user and clone share sizes | |
user.sizes.width = 60; // change a property from one place | |
clone.sizes.width; // 60, get the result from the other one | |
// structuredClone | |
let clone = structuredClone(user); | |
// Doesn't work with methods: | |
structuredClone({ | |
f: function() {} | |
}); | |
//////////////////////////////////////////////////////////////////////////////////////// | |
// Object methods, "this" -------------------------------------------------------------- | |
// Objects are assoc. data structures... | |
let user = { | |
name: "John", | |
age: 30 | |
}; | |
// ...and functions are first-class values... | |
let sayHi = function() { | |
console.log("Hello!"); | |
}; | |
// ..so we can assign functions to objects | |
user.sayHi = sayHi; | |
user.say(); // -> Hello! | |
// Functions defined using function declarations can be assigned too: | |
function sayHi() { | |
// ... | |
} | |
user.sayHi = sayHi; | |
user.say(); // -> Hello! | |
// --- Method shorthand --- | |
user = { | |
sayHi: function() { | |
console.log("Hello!"); | |
} | |
}; | |
// ...or even shorter: | |
user = { | |
sayHi() { | |
console.log("Hello!"); | |
} | |
}; | |
// --- 'this' keyword --- | |
let user = { | |
name: "John", | |
age: 30, | |
sayHi() { | |
console.log(this.name); | |
} | |
}; | |
// --- 'this' is unbound, ~dynamically scoped --- | |
function foo() { | |
console.log(this.bar); | |
}; | |
foo(); // Error, 'this' is undefined in strict mode, window otherwise | |
// 'this' depends on the object before the dot. | |
// Example: | |
let user = { name: "John" }; | |
let admin = { name: "Admin" }; | |
function sayHi() { | |
console.log(this.name); | |
} | |
// Use the same function in two objects | |
user.f = sayHi; | |
admin.f = sayHi; | |
// These calls have different this | |
// "this" inside the function is the object "before the dot" | |
user.f(); // John (this == user) | |
admin.f(); // Admin (this == admin) | |
// If function is callback, 'this' may be unpredictable: | |
// undefined in strict mode, window in non-strict mode | |
function foo() { | |
// In non-strict: | |
console.log(this); // Window {...} | |
} | |
// --- Arrow functions have no 'this' --- | |
let user = { | |
firstName: "Muad'Dib", | |
sayHi() { | |
let arrow = () => console.log(this.firstName); | |
arrow(); | |
} | |
}; | |
user.sayHi(); // Muad'Dib | |
// Summary: | |
// Regular functions = object before the dot, undefined or global object | |
// Arrow functions = undefiend or global object | |
// --- Constructors -------------------------------------------------------------------- | |
// Example constructor: | |
function User(name) { | |
this.name = name; | |
this.isAdmin = false; | |
}; | |
let user = new User("John"); | |
user.name; // -> John | |
user.isAdmin // false | |
// Above is evaluated to the following: | |
function User(name) { | |
this = {}; | |
this.name = name; | |
this.isAdmin = false; | |
return this; | |
}; | |
// --- Capitalized naming is a convention --- | |
// Following is a valid syntax: | |
let user = new function() { | |
this.name = "John"; | |
this.isAdmin = false; | |
// ...other code for user creation | |
// maybe complex logic and statements | |
// local variables etc | |
}; | |
// --- Return in constructors --- | |
// If there is a return statement, then the rule is simple: | |
// If return is called with an object, then the object is returned instead of this. | |
// If return is called with a primitive, it’s ignored. | |
function BigUser() { | |
this.name = "John"; | |
return { name: "Godzilla" }; // Returns literal object | |
} | |
console.log(new BigUser().name); // "Godzilla" | |
// --- Methods in constructors --- | |
function User(name) { | |
this.name = name; | |
this.sayHi = function() { | |
console.log("My name is: " + this.name); | |
}; | |
} | |
let john = new User("John"); | |
john.sayHi(); // My name is: John | |
//////////////////////////////////////////////////////////////////////////////////////// | |
// Symbols ----------------------------------------------------------------------------- | |
// ...only two primitive types may serve as object property keys: | |
// - symbols | |
// - strings | |
obj[1]; // -> obj["1"] | |
obj[true]; // -> obj["true"] | |
let id = Symbol(); | |
// or with descriptive name: | |
let id = Symbol("id"); | |
// However, they are not compared symbolically: | |
let id2 = Symbol("id"); | |
id == id2; // -> false | |
// 1. Symbols are "unique primitive values" | |
// 2. Symbols are not implicitly coerced into other types, such as strings: | |
console.log(Symbol("id")); // -> TypeError | |
// instead: | |
Symbol("id").toString(); | |
Symbol("id").description; | |
// --- Symbols can be used to create "hidden" properties: --- | |
let user = { | |
name: "John" | |
}; | |
let id = Symbol("id"); | |
let id2 = Symbol("id"); | |
user[id] = 1; | |
user[id2] = 2; | |
user['id'] = 3; | |
// We can access the data using the symbol as the key: | |
user[id]; // -> 1 | |
user[id2]; // -> 2 | |
user['id'; // -> 3 | |
// Symbols should not typically be used by the programmer! | |
// Basically, they solve the following problem: | |
user.id = 1; | |
// ...pass user by reference somewhere: | |
user.id = 2; | |
user.id != 1; // Value changed! | |
// To use symbols in literals: | |
let id = Symbol("id"); | |
let user = { | |
name: "John", | |
[id]: 123 // not "id": 123 | |
}; | |
// Global symbol registry -------------------------------------------------------------- | |
// read from the global registry | |
let id = Symbol.for("id"); // if the symbol did not exist, it is created | |
// read it again (maybe from another part of the code) | |
let idAgain = Symbol.for("id"); | |
// the same symbol | |
alert( id === idAgain ); // true | |
// get symbol by name | |
let sym = Symbol.for("name"); | |
let sym2 = Symbol.for("id"); | |
// get name by symbol | |
Symbol.keyFor(sym); // name | |
Symbol.keyFor(sym2); // id | |
// System symbols... | |
Symbol.hasInstance | |
Symbol.isConcatSpreadable | |
Symbol.iterator | |
Symbol.toPrimitive | |
// …and so on. | |
// Object -> primitive conversion ------------------------------------------------------ | |
// - Call obj[Symbol.toPrimitive](hint) – the method with the symbolic key | |
// Symbol.toPrimitive (system symbol), if such method exists, | |
// - Otherwise if hint is "string" | |
// try calling obj.toString() or obj.valueOf(), whatever exists. | |
// - Otherwise if hint is "number" or "default" | |
// try calling obj.valueOf() or obj.toString(), whatever exists. | |
obj[Symbol.toPrimitive] = function(hint) { | |
return // ...primitive value | |
}; | |
let user = { | |
name: "John", | |
money: 1000, | |
[Symbol.toPrimitive](hint) { | |
console.log(`hint: ${hint}`); | |
return hint == "string" ? `{name: "${this.name}"}` : this.money; | |
} | |
}; | |
console.log(user); // hint: string -> {name: "John"} | |
console.log(+user); // hint: number -> 1000 | |
console.log(user + 500); // hint: default -> 1500 | |
// We can override toString() and valueOf() | |
let user = { | |
name: "John", | |
money: 1000, | |
// for hint="string" | |
toString() { | |
return `{name: "${this.name}"}`; | |
}, | |
// for hint="number" or "default" | |
valueOf() { | |
return this.money; | |
} | |
}; | |
alert(user); // toString -> {name: "John"} | |
alert(+user); // valueOf -> 1000 | |
alert(user + 500); // valueOf -> 1500 | |
// --- Symbol.iterator --- | |
// Let's make custom "range" object: | |
let range = { | |
from: 1, | |
to: 5 | |
}; | |
// 1. call to for..of initially calls this | |
range[Symbol.iterator] = function() { | |
// ...it returns the iterator object: | |
// 2. Onward, for..of works only with the iterator object below, asking it for next values | |
return { | |
current: this.from, | |
last: this.to, | |
// 3. next() is called on each iteration by the for..of loop | |
next() { | |
// 4. it should return the value as an object {done:.., value :...} | |
if (this.current <= this.last) { | |
return { done: false, value: this.current++ }; | |
} else { | |
return { done: true }; | |
} | |
} | |
}; | |
}; | |
for (let num of range) { | |
console.log(num); // 1, 2, 3, 4, 5 | |
} | |
//////////////////////////////////////////////////////////////////////////////////////// | |
// Primitives and wrapper types -------------------------------------------------------- | |
// pl: Typy/obiekty opakowujace | |
// Here’s the paradox faced by the creator of JavaScript: | |
// - There are many things one would want to do with a primitive, like a string | |
// or a number. It would be great to access them using methods. | |
// - Primitives must be as fast and lightweight as possible. | |
let n = 777; | |
n.toFixed(2); | |
777.toFixed(2); // SyntaxError | |
7 === Number(7); // -> true | |
typeof 4; // -> 'number' | |
typeof Number(4); // -> 'number' | |
typeof new Number(4); // -> 'object; | |
// For more methods, check MDN documentation | |
// Notable modules: Math |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment