Ruby.CodeCompared.To/JavaScript

An interactive executable cheatsheet for Rubyists learning JavaScript

Ruby 4.0 JavaScript (ES2025)
Syntax Basics
Blocks / Indentation
def greet(name) "Hello, #{name}" end puts greet("Alice")
function greet(name) { return `Hello, ${name}`; } console.log(greet("Alice"));
JavaScript uses {} blocks, not end. The return keyword is explicit β€” there is no implicit last-value return except in single-expression arrow functions. Indentation is convention only; the parser ignores it.
Semicolons
x = 1 y = 2 puts x + y
const x = 1; const y = 2; console.log(x + y);
Semicolons are technically optional in JavaScript (ASI β€” Automatic Semicolon Insertion), but most style guides require them. The edge cases where ASI surprises you are subtle and painful. Use semicolons.
Comments
# single line =begin multi-line =end puts "done"
// single line /* multi-line block */ console.log("done");
JavaScript uses C-style comments. There is no =begin/=end equivalent. JSDoc uses /** */ for documentation comments.
String Interpolation
name = "Alice" puts "Hello, #{name}!" puts "2 + 2 = #{2 + 2}"
const name = "Alice"; console.log(`Hello, ${name}!`); console.log(`2 + 2 = ${2 + 2}`);
Only backtick strings (template literals) support interpolation. Single and double-quoted strings are inert β€” "Hello, ${name}" prints literally as Hello, ${name}.
Variables & Scope
var / let / const
x = 10 # local variable LIMIT = 100 # constant (warning on reassign) puts x puts LIMIT
var old = 1; // function-scoped, hoisted β€” avoid let count = 0; // block-scoped, reassignable const MAX = 100; // block-scoped, no reassign count = 5; console.log(old, count, MAX);
Always prefer const, use let when you need to reassign. Never use var β€” it is function-scoped and hoisted in ways that cause bugs. const on an object or array only prevents rebinding the variable, not mutating the contents.
Hoisting
# Ruby 4.0: accessing an unassigned local is a NameError # (no hoisting β€” variables must be assigned before use) x = 10 puts x # => 10 # Conditional assignment is safe: y ||= 42 puts y # => 42
console.log(hoisted); // undefined (not ReferenceError!) var hoisted = 10; // let/const are in a Temporal Dead Zone until declaration: try { console.log(blocked); } catch (err) { console.log("TDZ error:", err.message); } let blocked = 20;
var declarations are hoisted to the top of their function scope and initialized to undefined. let and const are in a "temporal dead zone" β€” accessing them before their declaration throws a ReferenceError. Ruby pre-creates locals as nil at parse time, so reading an unassigned local returns nil, not an error.
Block Scope
if true message = "hello" end puts message # "hello" β€” if doesn't create a new scope
if (true) { let message = "hello"; var leaked = "oops"; } console.log(typeof message); // "undefined" β€” block-scoped console.log(leaked); // "oops" β€” var leaks out!
Ruby's if, unless, and while do not create new variable scopes. JavaScript blocks {} create new scopes for let/const but not for var.
Types
Primitive Types
puts 42.class # => Integer puts 3.14.class # => Float puts "hi".class # => String puts :name.class # => Symbol puts true.class # => TrueClass puts nil.class # => NilClass
console.log(typeof 42); // "number" console.log(typeof 3.14); // "number" console.log(typeof "hi"); // "string" console.log(typeof Symbol()); // "symbol" console.log(typeof true); // "boolean" console.log(typeof undefined); // "undefined" console.log(typeof null); // "object" β€” famous bug console.log(typeof {}); // "object" console.log(typeof function(){}); // "function"
JS has one number type (64-bit float) β€” no separate Integer/Float. NaN is of type number. null and undefined are distinct: undefined means "never assigned", null means "intentionally empty". typeof null === "object" is a decades-old bug.
null vs undefined
x = nil puts x.nil? # => true puts x.inspect # => "nil"
let notSet; console.log(notSet); // undefined console.log(notSet === undefined); // true let empty = null; console.log(empty === null); // true // == checks both null and undefined together: console.log(notSet == null); // true console.log(notSet === null); // false
Use === null to check for null, === undefined for undefined, or == null to catch both (the only safe use of ==). Ruby has only nil; JavaScript has two "empty" values.
Truthiness
# Only nil and false are falsy puts !!0 # => true puts !!"" # => true puts !![] # => true
// Falsy: false, 0, -0, 0n, "", null, undefined, NaN console.log(Boolean(0)); // false β€” GOTCHA for Rubyists console.log(Boolean("")); // false β€” GOTCHA console.log(Boolean([])); // true (empty array is truthy!) console.log(Boolean({})); // true (empty object is truthy!) console.log(Boolean(null)); // false console.log(Boolean(undefined)); // false
Major gotcha. 0 and "" are falsy in JavaScript β€” the opposite of Ruby. Empty arrays and empty objects are truthy. This bites Rubyists constantly.
== vs ===
puts 1 == 1.0 # => true puts 1.eql?(1.0) # => false (strict type check) puts 1.equal?(1) # => true (object identity)
console.log(1 == "1"); // true β€” type coercion (avoid!) console.log(1 === "1"); // false β€” strict equality console.log(0 == false); // true β€” coercion console.log(0 === false); // false console.log(null == undefined); // true console.log(null === undefined); // false console.log(NaN === NaN); // false β€” NaN is never equal to itself console.log(Number.isNaN(NaN)); // true β€” correct check
Never use == in JavaScript except == null (to check null/undefined together). Always use ===. Ruby's == is already strict by class convention; JavaScript's == does type coercion and produces surprising results.
Numbers & NaN
puts 10 / 3 # => 3 (integer division) puts 10.0 / 3 # => 3.333... puts 1_000_000 # => 1000000 puts Float::NAN == Float::NAN # => false (same as JavaScript!)
console.log(10 / 3); // 3.333... (always float) console.log(Math.floor(10 / 3)); // 3 console.log(1_000_000); // 1000000 (ES2021 separators) console.log(1 / 0); // Infinity console.log(-1 / 0); // -Infinity console.log(0 / 0); // NaN console.log(parseInt("abc")); // NaN console.log(NaN === NaN); // false β€” NaN is never equal to itself
JS has no integer division operator. All numbers are 64-bit floats. Division never raises β€” it returns Infinity or NaN. Ruby's Float::NAN == Float::NAN is also false β€” IEEE 754 applies in both languages.
Strings
Common Methods
puts "hello".upcase # => "HELLO" puts "hello".length # => 5 puts "hello".include?("ell") # => true puts "hello" * 3 # => "hellohellohello" puts " hi ".strip # => "hi" puts "a,b,c".split(",").inspect puts [1, 2, 3].join(", ")
console.log("hello".toUpperCase()); // "HELLO" console.log("hello".length); // 5 (property, not method) console.log("hello".includes("ell")); // true console.log("hello".repeat(3)); // "hellohellohello" console.log(" hi ".trim()); // "hi" console.log("a,b,c".split(",")); // ["a","b","c"] console.log([1, 2, 3].join(", ")); // "1, 2, 3" console.log("hello".startsWith("he")); // true console.log("hello".endsWith("lo")); // true console.log("hello".slice(1, 3)); // "el" console.log("hello".replace("l", "r")); // "herlo" (first only) console.log("hello".replaceAll("l", "r")); // "herro"
.length is a property in JavaScript, not a method β€” no (). String methods that "look" mutating (replace, toUpperCase) return new strings; JavaScript strings are immutable. .replace() without the g flag or .replaceAll() only replaces the first match.
Collections
Arrays
numbers = [1, 2, 3, 4, 5] numbers.push(6) puts numbers.first # => 1 puts numbers.last # => 6 puts numbers.length # => 6 puts numbers.map { |n| n * 2 }.inspect puts numbers.select { |n| n.even? }.inspect puts numbers.reduce(0) { |sum, n| sum + n } puts numbers.any? { |n| n > 4 } puts numbers.all? { |n| n > 0 } puts numbers.find { |n| n > 3 } puts numbers.include?(3)
const numbers = [1, 2, 3, 4, 5]; numbers.push(6); console.log(numbers[0]); // 1 (no .first) console.log(numbers.at(-1)); // 6 (modern .last) console.log(numbers.length); // property, not method console.log(numbers.map(n => n * 2)); console.log(numbers.filter(n => n % 2 === 0)); console.log(numbers.reduce((sum, n) => sum + n, 0)); console.log(numbers.some(n => n > 4)); // .any? console.log(numbers.every(n => n > 0)); // .all? console.log(numbers.find(n => n > 3)); console.log(numbers.includes(3)); console.log([...new Set(numbers)]); // .uniq
.sort() in JavaScript sorts in-place and mutates the array β€” opposite of Ruby. Default sort is lexicographic: [10, 2, 1].sort() gives [1, 10, 2]. Always pass a comparator: .sort((a, b) => a - b). Use .slice().sort() to avoid mutating the original. .at(-1) is the modern way to get the last element.
Objects (β‰ˆ Ruby Hashes)
person = { name: "Alice", age: 30 } puts person[:name] person[:age] = 31 puts person.key?(:name) puts person.keys.inspect puts person.values.inspect person.each { |k, v| puts "#{k}: #{v}" }
const person = { name: "Alice", age: 30 }; console.log(person.name); // dot notation console.log(person["name"]); // bracket notation person.age = 31; // set (even on const!) console.log("name" in person); // true console.log(Object.keys(person)); console.log(Object.values(person)); console.log(Object.entries(person)); Object.entries(person).forEach(([k, v]) => console.log(`${k}: ${v}`)); const merged = { ...person, role: "admin" }; // spread merge console.log(merged);
JS objects use string (or Symbol) keys. Dot notation and bracket notation are interchangeable. The in operator checks the prototype chain too; use Object.hasOwn(obj, key) for own-only checks. Use Map when you need non-string keys or care about insertion order.
Destructuring
first, *rest = [1, 2, 3, 4] puts first # => 1 puts rest.inspect # => [2, 3, 4] # Hash destructuring in method params: def greet(name:, age:) puts "#{name} is #{age}" end greet(name: "Alice", age: 30)
const [first, ...rest] = [1, 2, 3, 4]; console.log(first); // 1 console.log(rest); // [2, 3, 4] // Object destructuring: const person = { name: "Alice", age: 30 }; const { name, age } = person; const { name: fullName, age: years = 0 } = person; // rename + default console.log(name, age, fullName, years); // In function params: function greet({ name, age }) { return `${name} is ${age}`; } console.log(greet({ name: "Alice", age: 30 })); // Nested: const { address: { city } } = { address: { city: "NYC" } }; console.log(city);
JS destructuring is more flexible than Ruby's parallel assignment β€” you can rename keys, provide defaults, and nest patterns. The ...rest operator in array destructuring is equivalent to Ruby's splat. You cannot do a middle splat like Ruby's first, *middle, last.
Spread / Rest
def sum(*numbers) puts numbers.sum end sum(1, 2, 3) first, *middle, last = [1, 2, 3, 4, 5] puts "#{first} | #{middle.inspect} | #{last}" combined = [*[1, 2], *[3, 4]] puts combined.inspect
function sum(...numbers) { // rest params return numbers.reduce((a, b) => a + b, 0); } console.log(sum(1, 2, 3)); const [first, ...rest] = [1, 2, 3, 4, 5]; // no middle splat in JavaScript console.log(first, rest); const combined = [...[1, 2], ...[3, 4]]; // [1, 2, 3, 4] console.log(combined); const objA = { a: 1 }; const objB = { b: 2 }; const merged = { ...objA, ...objB }; console.log(merged);
JavaScript's ... operator does double duty as both rest (in function params and destructuring) and spread (in calls and literals). You can only have a trailing rest β€” no middle splat like Ruby's first, *middle, last.
Map and Set
# Hash is the equivalent of Map counts = Hash.new(0) counts[:a] += 1 puts counts[:a] require 'set' unique = Set.new([1, 2, 2, 3]) puts unique.size puts unique.include?(2)
// Map β€” any key type, preserves insertion order const counts = new Map(); counts.set("a", (counts.get("a") || 0) + 1); console.log(counts.has("a")); // true console.log(counts.get("a")); // 1 console.log(counts.size); // property (not method) // Set β€” unique values, any type const unique = new Set([1, 2, 2, 3]); console.log(unique.size); // 3 console.log(unique.has(2)); // true unique.add(4); console.log([...unique]); // [1, 2, 3, 4]
Use Map instead of plain objects when keys are non-strings, when you need .size, or when insertion order matters. Use Set for unique collections. Both are iterable with for...of.
Functions
Declarations vs Expressions vs Arrows
# Method definition def add(a, b) a + b end puts add(1, 2) # Lambda multiply = ->(a, b) { a * b } puts multiply.call(3, 4) # Proc double = Proc.new { |x| x * 2 } puts double.call(5)
// Function declaration (hoisted) function add(a, b) { return a + b; } console.log(add(1, 2)); // Function expression (not hoisted) const multiply = function(a, b) { return a * b; }; console.log(multiply(3, 4)); // Arrow function β€” implicit return for single expression const double = x => x * 2; console.log(double(5)); // Arrow with no params needs parens const greet = () => "Hello"; console.log(greet()); // Arrow returning an object literal needs outer parens const makePoint = (x, y) => ({ x, y }); console.log(makePoint(1, 2));
Arrow functions do not have their own this β€” they inherit this from the enclosing scope. This is usually what you want in callbacks. Regular functions get their own this based on how they are called.
Default Parameters
def greet(name, greeting = "Hello") puts "#{greeting}, #{name}!" end greet("Alice") greet("Bob", "Hi")
function greet(name, greeting = "Hello") { return `${greeting}, ${name}!`; } console.log(greet("Alice")); console.log(greet("Bob", "Hi")); // Defaults trigger on undefined, not null: console.log(greet("Carol", null)); // "null, Carol!" β€” no default console.log(greet("Dave", undefined)); // "Hello, Dave!" β€” uses default
JS default params trigger on undefined only β€” passing null does not use the default. This differs from Ruby where omitting an argument triggers the default; JavaScript distinguishes between "argument absent" (undefined) and "explicitly null".
Closures
def make_counter count = 0 increment = -> { count += 1; count } reset = -> { count = 0 } [increment, reset] end inc, rst = make_counter puts inc.call # => 1 puts inc.call # => 2 rst.call puts inc.call # => 1
function makeCounter() { let count = 0; const increment = () => { count += 1; return count; }; const reset = () => { count = 0; }; return { increment, reset }; } const { increment, reset } = makeCounter(); console.log(increment()); // 1 console.log(increment()); // 2 reset(); console.log(increment()); // 1
Closures work conceptually the same way β€” the inner function closes over the outer variable. The key difference is in loops: let creates a new binding per iteration, but var does not, leading to the classic closure-in-loop bug (see Gotchas).
this β€” The Big Gotcha
class Counter def initialize @count = 0 end def increment @count += 1 end end counter = Counter.new method_ref = counter.method(:increment) puts method_ref.call # still bound to counter puts method_ref.call # => 2
class Counter { constructor() { this.count = 0; } increment() { this.count += 1; return this.count; } } const counter = new Counter(); console.log(counter.increment()); // 1 β€” works fine // Extracting a method loses its binding: const fn = counter.increment; try { fn(); // TypeError: Cannot read properties of undefined } catch (err) { console.log("Lost this:", err.message); } // Fix 1: .bind() const bound = counter.increment.bind(counter); console.log(bound()); // 3 // Fix 2: arrow class field β€” this is always bound class Counter2 { count = 0; increment = () => { this.count += 1; return this.count; }; } const counter2 = new Counter2(); const fn2 = counter2.increment; console.log(fn2()); // 1 β€” works even when detached
this in JavaScript is determined by how a function is called, not where it is defined (except for arrow functions, which have no own this). Extracting a method loses its binding. Use .bind(), arrow class fields, or arrow functions in callbacks to preserve this.
Classes
Class Definition
class Animal attr_reader :name def initialize(name) @name = name end def speak "..." end def to_s "Animal(#{@name})" end end class Dog < Animal def speak "Woof!" end end dog = Dog.new("Rex") puts dog.speak # => "Woof!" puts dog.name # => "Rex"
class Animal { #name; // private field (ES2022) constructor(name) { this.#name = name; } get name() { return this.#name; } // getter speak() { return "..."; } toString() { return `Animal(${this.#name})`; } } class Dog extends Animal { speak() { return "Woof!"; } } const dog = new Dog("Rex"); console.log(dog.speak()); // "Woof!" console.log(dog.name); // "Rex" console.log(dog instanceof Dog); // true console.log(dog instanceof Animal); // true
Private fields use #prefix β€” they are truly private (not a naming convention like Ruby's private). Use super() in the child constructor before accessing this. JavaScript classes are syntactic sugar over prototypes β€” typeof Dog === "function".
Static Methods
class MathUtils def self.square(n) n * n end end puts MathUtils.square(4) # => 16
class MathUtils { static square(n) { return n * n; } static PI = 3.14159; // static field } console.log(MathUtils.square(4)); // 16 console.log(MathUtils.PI); // 3.14159
The static keyword in JavaScript is equivalent to Ruby's def self.method_name. Static fields (class-level constants) are also supported with the same static keyword.
Mixins
module Serializable def to_json instance_variables.map { |v| "\"#{v.to_s[1..]}\": #{instance_variable_get(v).inspect}" }.join(", ").then { |body| "{ #{body} }" } end end class Person include Serializable def initialize(name); @name = name; end end person = Person.new("Alice") puts person.to_json
// No built-in mixin syntax; use higher-order class functions const Serializable = (Base) => class extends Base { serialize() { return JSON.stringify(Object.assign({}, this)); } }; class Person extends Serializable(class {}) { constructor(name) { super(); this.name = name; } } const person = new Person("Alice"); console.log(person.serialize()); // Simpler pattern: standalone utility functions function serialize(obj) { return JSON.stringify(obj); } console.log(serialize({ name: "Bob", age: 25 }));
JavaScript has no include/extend mixin system. Patterns include: higher-order classes (mixin functions that wrap a base class), plain object composition, or standalone utility functions. The community generally favors composition over inheritance in JavaScript.
Modules
ES Modules (ESM)
# lib/math_utils.rb module MathUtils def self.square(n) = n * n end # main.rb require_relative 'lib/math_utils' puts MathUtils.square(4)
// math_utils.js export function square(n) { return n * n; } export const PI = 3.14159; export default class Calculator { /* ... */ } // main.js import Calculator, { square, PI } from './math_utils.js'; import * as MathUtils from './math_utils.js'; console.log(square(4)); // 16
ES Modules are static β€” imports are resolved at parse time, not runtime. Each file is its own module scope (no global leakage). In Node.js, use .mjs extension or "type": "module" in package.json. CommonJS (require()/module.exports) is the older Node.js system and has messy ESM interoperability.
Error Handling
try / catch / finally
begin result = 10 / 0 rescue ZeroDivisionError => err puts "Error: #{err.message}" rescue StandardError => err puts "Other: #{err.message}" ensure puts "always runs" end class AppError < StandardError; end begin raise AppError, "something went wrong" rescue AppError => err puts err.message end
try { JSON.parse("invalid json"); } catch (error) { if (error instanceof SyntaxError) { console.log("JSON parse error:", error.message); } else { throw error; // re-raise } } finally { console.log("always runs"); } // Custom error class AppError extends Error { constructor(message, code) { super(message); this.name = "AppError"; this.code = code; } } try { throw new AppError("something went wrong", 404); } catch (err) { console.log(`${err.name}: ${err.message} (code: ${err.code})`); }
JS catch catches everything β€” there is no type-dispatching like Ruby's multi-rescue. Use instanceof inside the catch block to distinguish error types. Always set this.name in custom errors. JavaScript has no retry equivalent.
Async / Await & Promises
async / await
# Ruby uses threads; no async/await keyword thread = Thread.new { "data from server" } result = thread.value # blocks until thread completes puts "Got: #{result}"
// async functions always return a Promise async function fetchData(url) { // Using Promise.resolve() to simulate a resolved fetch response // (fetch() is not available in this runner) const data = await Promise.resolve({ status: 200, body: `data from ${url}` }); if (data.status !== 200) throw new Error(`HTTP ${data.status}`); return data.body; } // Top-level await works inside AsyncFunction const result = await fetchData("/api/users"); console.log(result); // Parallel requests with Promise.all const [users, posts] = await Promise.all([ Promise.resolve(["Alice", "Bob"]), Promise.resolve(["Post 1", "Post 2"]), ]); console.log("users:", users); console.log("posts:", posts);
async functions always return a Promise. await only works inside async functions (or at top level in ESM). Use Promise.all() for parallel async operations β€” sequential await serializes them unnecessarily. Note: fetch() is not available in this inline runner; examples use Promise.resolve() to simulate responses.
Iterators & Generators
Generators
def fibonacci Enumerator.new do |yielder| a, b = 0, 1 loop do yielder << a a, b = b, a + b end end end puts fibonacci.take(8).inspect # => [0, 1, 1, 2, 3, 5, 8, 13]
function* fibonacci() { let [a, b] = [0, 1]; while (true) { yield a; [a, b] = [b, a + b]; } } const gen = fibonacci(); const first8 = Array.from({ length: 8 }, () => gen.next().value); console.log(first8); // [0, 1, 1, 2, 3, 5, 8, 13] // for...of works on generators let output = []; for (const n of fibonacci()) { if (n > 20) break; output.push(n); } console.log(output);
function* defines a generator; yield pauses it and returns a value. The generator object implements the iterator protocol (.next() returning { value, done }). Any object can be iterable by implementing [Symbol.iterator](). for...of works on arrays, strings, Maps, Sets, generators, and any iterable.
Prototypes
Prototype Chain
# Ruby's object model: every object has a class, # classes have a method lookup chain puts String.ancestors.inspect # => [String, Comparable, Object, Kernel, BasicObject]
class Animal { constructor(name) { this.name = name; } speak() { return "..."; } } class Dog extends Animal { speak() { return "Woof!"; } } const dog = new Dog("Rex"); console.log(Object.getPrototypeOf(dog) === Dog.prototype); // true console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true // Own vs inherited properties: console.log(Object.hasOwn(dog, "name")); // true β€” own property console.log(Object.hasOwn(dog, "speak")); // false β€” on prototype // Extending built-ins (don't do this in shared code): Array.prototype.second = function() { return this[1]; }; console.log([10, 20, 30].second()); // 20 β€” but avoid this!
JS's class syntax is sugar over prototype-based inheritance. Understanding prototypes helps debug instanceof surprises. Extending built-in prototypes (Array.prototype, Object.prototype) is strongly discouraged in shared code β€” it causes prototype pollution.
Symbols
Symbols
:name # interned, compared by identity puts :name == :name # => true (same object) sym = :status obj = { sym => "active" } puts obj[:status] # => "active" # Symbols are unique by name in Ruby puts :foo.equal?(:foo) # => true
const id = Symbol("id"); // always unique const id2 = Symbol("id"); console.log(id === id2); // false! every Symbol() call is unique // Global symbol registry (shared across modules): const shared = Symbol.for("app.id"); console.log(Symbol.for("app.id") === shared); // true // Unique object keys that don't conflict with string keys: const KEY = Symbol("privateKey"); const obj = { [KEY]: "secret", name: "Alice" }; console.log(obj[KEY]); // "secret" console.log(Object.keys(obj)); // ["name"] β€” symbols excluded // Well-known symbols customize language behavior: console.log(typeof Symbol.iterator); // "symbol"
Unlike Ruby Symbols, JavaScript Symbols created with Symbol() are always unique β€” they are not interned by description. Use Symbol.for() for shared/global symbols. Well-known symbols like Symbol.iterator let you hook into language protocols (iteration, coercion, etc.).
Regular Expressions
Regular Expressions
"hello world" =~ /(\w+)\s(\w+)/ puts $1 # => "hello" puts $2 # => "world" match = "hello".match(/e(l+)o/) puts match&.captures&.inspect # => ["ll"] puts "hello world".scan(/\w+/).inspect puts "hello".gsub(/l/, "r") # => "herro"
const match = "hello world".match(/(\w+)\s(\w+)/); console.log(match[1]); // "hello" console.log(match[2]); // "world" // Named capture groups (ES2018): const { groups: { first, second } } = "hello world".match(/(?<first>\w+)\s(?<second>\w+)/); console.log(first, second); // Global flag β€” find all matches: console.log("hello world".match(/\w+/g)); // ["hello", "world"] // Replace with /g flag: console.log("hello".replace(/l/g, "r")); // "herro" // Test (like Ruby's =~): console.log(/^\d+$/.test("123")); // true
JS regex flags go after the closing /: g (global), i (case-insensitive), m (multiline), s (dotAll), u (unicode). Without g, .match() returns only the first match with capture groups. With g, it returns all matches but no capture groups. Use .matchAll() for all matches with captures.
Package Management
npm / yarn
# Gemfile gem 'rails', '~> 7.1' gem 'puma' $ bundle install $ bundle exec rails server
$ npm init -y $ npm install express $ npm install --save-dev jest # or yarn $ yarn add express $ yarn add --dev jest # Run a binary without global install (like bundle exec): $ npx jest
package.json β‰ˆ Gemfile + gemspec. package-lock.json / yarn.lock β‰ˆ Gemfile.lock. node_modules/ β‰ˆ the vendor bundle β€” always add to .gitignore. npx runs a package binary without installing globally, like bundle exec. Global installs (npm install -g) are generally avoided in favor of project-local installs.
⚠ Gotchas for Rubyists
Array sort() mutates
nums = [3, 1, 2] sorted = nums.sort # new array, nums unchanged puts sorted.inspect # => [1, 2, 3] puts nums.inspect # => [3, 1, 2]
const nums = [3, 1, 2]; nums.sort((a, b) => a - b); // mutates nums! console.log(nums); // [1, 2, 3] β€” original changed // Safe non-mutating sort: const original = [3, 1, 2]; const sorted = [...original].sort((a, b) => a - b); console.log(original); // [3, 1, 2] β€” unchanged console.log(sorted); // [1, 2, 3]
Ruby's .sort returns a new array and leaves the receiver unchanged. JavaScript's .sort() mutates the array in-place. Use [...arr].sort() or arr.slice().sort() to get non-mutating behavior. .reverse() is also mutating.
Default sort is lexicographic
puts [10, 9, 2, 1].sort.inspect # => [1, 2, 9, 10] β€” numeric sort
console.log([10, 9, 2, 1].sort()); // [1, 10, 2, 9] β€” WRONG! string comparison console.log([10, 9, 2, 1].sort((a, b) => a - b)); // [1, 2, 9, 10] β€” correct numeric sort
JavaScript's default .sort() converts elements to strings and compares them lexicographically, so 10 sorts before 2. Always pass a comparator function: .sort((a, b) => a - b) for ascending numeric sort.
0 and "" are falsy
puts 0 ? "truthy" : "falsy" # truthy puts "" ? "truthy" : "falsy" # truthy # Only nil and false are falsy in Ruby
console.log(0 ? "truthy" : "falsy"); // falsy β€” opposite of Ruby! console.log("" ? "truthy" : "falsy"); // falsy β€” opposite of Ruby! console.log([] ? "truthy" : "falsy"); // truthy (empty array is truthy) console.log({} ? "truthy" : "falsy"); // truthy (empty object is truthy) // Common bug β€” checking array length: const items = []; if (items) console.log("array is truthy even when empty!");
In Ruby, only nil and false are falsy β€” everything else (including 0, "", and []) is truthy. JavaScript's falsy values include false, 0, -0, 0n, "", null, undefined, and NaN.
this is dynamic
class Foo def bar puts self.class # always the instance end end foo = Foo.new method_ref = foo.method(:bar) method_ref.call # still prints "Foo"
class Foo { bar() { return this ? this.constructor.name : "undefined this"; } } const foo = new Foo(); console.log(foo.bar()); // "Foo" β€” works as method call const fn = foo.bar; try { fn(); // TypeError in strict mode } catch (err) { console.log("Detached call:", err.message); } // Fix: bind or use arrow class field const bound = foo.bar.bind(foo); console.log(bound()); // "Foo"
In Ruby, self inside an instance method always refers to the receiver. In JavaScript, this is determined at call time, not definition time. Extracting a method and calling it as a plain function loses this. Use .bind() or arrow class fields to fix it.
Closure over var in loops
funcs = (0...3).map { |i| -> { i } } puts funcs.map(&:call).inspect # => [0, 1, 2] β€” each closure gets its own i
// var: all closures share ONE i (the final value after loop) const funcsVar = []; for (var i = 0; i < 3; i++) { funcsVar.push(() => i); } console.log("var:", funcsVar.map(f => f()).join(", ")); // 3, 3, 3 // let: new binding per iteration β€” each closure gets its own j const funcsLet = []; for (let j = 0; j < 3; j++) { funcsLet.push(() => j); } console.log("let:", funcsLet.map(f => f()).join(", ")); // 0, 1, 2
var is function-scoped β€” the loop variable is shared across all iterations, so all closures capture the same binding (its final value). let is block-scoped β€” each loop iteration gets its own binding. Always use let (or const) in loops.
typeof null === "object"
puts nil.class # => NilClass puts nil.nil? # => true # No confusion β€” nil is its own type
console.log(typeof null); // "object" β€” a 25-year-old bug console.log(null instanceof Object); // false β€” the correct answer console.log(null === null); // true β€” how to check for null console.log(typeof null === "object" && null !== null); // false // The right way to check for null: const value = null; console.log(value === null); // true
typeof null === "object" is a famous JavaScript bug that cannot be fixed without breaking backward compatibility. Always check for null with value === null, not typeof.
NaN is not equal to itself
puts Float::NAN == Float::NAN # => false (same IEEE 754 rule) puts Float::NAN.nan? # => true (correct check) puts (0.0/0.0).nan? # => true
console.log(NaN === NaN); // false β€” NaN is never equal to itself console.log(Number.isNaN(NaN)); // true β€” correct check console.log(isNaN("hello")); // true β€” WRONG! isNaN coerces first console.log(Number.isNaN("hello")); // false β€” correct (not a NaN value) console.log(parseInt("abc")); // NaN console.log(Number.isNaN(parseInt("abc"))); // true
Both Ruby and JavaScript follow IEEE 754: NaN !== NaN. Always use Number.isNaN() to check for NaN β€” never the global isNaN(), which coerces its argument to a number first and produces surprising results for strings.
parseInt is greedy
puts Integer("42") # => 42 begin Integer("42abc") # raises ArgumentError rescue ArgumentError => err puts err.message end
console.log(parseInt("42abc")); // 42 β€” stops at first non-numeric char console.log(parseInt("0x10")); // 16 β€” interprets hex console.log(parseInt("010")); // 10 β€” modern JavaScript (was 8 in old JavaScript) // Always pass the radix to be safe: console.log(parseInt("10", 10)); // 10 // Strict parsing β€” use Number() instead: console.log(Number("42abc")); // NaN β€” fails on invalid input console.log(Number("42")); // 42 β€” only pure numeric strings
Ruby's Integer() raises on invalid input. JavaScript's parseInt() silently stops at the first non-numeric character. Always pass a radix (e.g., 10) to parseInt(). Prefer Number() when you need strict validation.
== type coercion
puts 1 == "1" # => false (no coercion) puts 0 == false # => false (no coercion) puts nil == false # => false # Ruby == is predictable
console.log([] == false); // true β€” coercion madness console.log([] == 0); // true console.log("" == 0); // true console.log(null == 0); // false β€” null only == undefined console.log(null == undefined); // true // The fix: use === always console.log([] === false); // false console.log("" === 0); // false
JavaScript's == applies a complex set of type coercion rules that produce counter-intuitive results. Use === (strict equality) everywhere β€” no exceptions. The only acceptable use of == is value == null to check for both null and undefined.
arguments vs rest params
def old_style(*args) puts args.class # => Array puts args.inspect end old_style(1, 2, 3)
function oldStyle() { console.log(Array.isArray(arguments)); // false β€” array-LIKE, not Array console.log(arguments[0], arguments[1]); // arguments is NOT available in arrow functions! } oldStyle(1, 2, 3); // Modern: use rest params β€” it's a real Array function modernStyle(...args) { console.log(Array.isArray(args)); // true console.log(args); } modernStyle(1, 2, 3);
The legacy arguments object is array-like (has numeric indices and .length) but is not a real Array β€” it has no .map(), .filter(), etc. It is also unavailable in arrow functions. Always use rest params (...args) in new code.
Prototype pollution
obj = {} puts obj.respond_to?(:to_s) # => true (inherited) puts obj.instance_variables.inspect # => [] (no inherited ivars)
const obj = {}; console.log("toString" in obj); // true β€” inherited from Object.prototype! console.log(obj.hasOwnProperty("toString")); // false β€” it's inherited console.log(Object.hasOwn(obj, "toString")); // false β€” preferred (ES2022) // Danger: plain objects as maps can collide with prototype methods const userInput = "constructor"; console.log(userInput in obj); // true β€” that's Object.prototype.constructor! // Safe alternative: use Map for dynamic string keys const safeMap = new Map(); safeMap.set("constructor", "my value"); console.log(safeMap.get("constructor")); // "my value" β€” no collision
Every plain JavaScript object inherits from Object.prototype, so built-in method names like toString, constructor, and hasOwnProperty exist on every object. The in operator includes inherited properties. Use Object.hasOwn() for own-only checks, and use Map when storing user-controlled string keys.