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.