Skip to content

Instantly share code, notes, and snippets.

@IwoHerka
Created April 21, 2024 14:07
Show Gist options
  • Save IwoHerka/578b09e94fa4be2974ae81eff89361df to your computer and use it in GitHub Desktop.
Save IwoHerka/578b09e94fa4be2974ae81eff89361df to your computer and use it in GitHub Desktop.
// 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