Hello World & Running
Hello, World
use v5.38;
say "Hello, World!"; console.log("Hello, World!"); JavaScript needs no
use v5.38; preamble to opt into modern behavior — there is no equivalent "legacy mode" to escape from, since JavaScript engines simply keep adding syntax without ever changing the meaning of existing code. console.log() is the direct equivalent of Perl's say: both append a trailing newline (in the console) and both accept any value directly with no explicit stringification required.Running a program
# Run directly (no compile step):
# perl hello.pl
#
# Check syntax only:
# perl -c hello.pl
#
# One-liner:
# perl -e 'print "Hello!\n"' // Run directly with Node.js (no compile step):
// node hello.js
//
// One-liner:
// node -e 'console.log("Hello!")'
//
// Interactive REPL:
// node
//
// Or skip a local runtime entirely and run it in any browser's console. Both languages are interpreted with no separate compile step, so
node hello.js and perl hello.pl feel identical to run from a terminal. JavaScript has a genuine advantage in reach, though: the exact same code (minus any Node-specific globals) also runs directly inside every web browser's developer console with no installation of any kind, something Perl has no analog for at all.Comments
use v5.38;
# Single-line comment — the only inline comment syntax
# Perl has no multi-line comment syntax.
# Block documentation uses Pod: =begin comment ... =end comment
my $name = "Alice"; # inline comment
say $name; // Single-line comment
/* Multi-line
block comment */
const name = "Alice"; // inline comment
console.log(name); JavaScript uses the familiar C-style comment syntax —
// for a single line and /* ... */ for a genuine multi-line block comment. This is a small but real improvement over Perl, which has no true multi-line comment construct at all; Perl programmers either stack consecutive # lines or reach for the Pod =begin/=end markers borrowed from its documentation tooling.print/say vs console.log
use v5.38;
print "no newline";
print "\n";
say "with newline"; # say appends a newline automatically
say 42; # works with numbers too process.stdout.write("no newline");
process.stdout.write("\n");
console.log("with newline"); // console.log appends a newline automatically
console.log(42); // works with numbers too, no coercion needed JavaScript's
console.log() is the direct equivalent of Perl's say — both append a trailing newline automatically and both accept a bare number with no explicit stringification. Suppressing the newline in Node.js requires dropping to the lower-level process.stdout.write(), the mirror image of Perl's print (no newline) versus say (with newline) being two separate functions; in the browser console, console.log is effectively the only option and this distinction rarely comes up.Variables & Sigils
No sigils — one kind of name
use v5.38;
my $name = "Alice";
my @colors = ("red", "green", "blue");
my %person = (name => "Alice", age => 30);
say $name;
say "@colors";
say $person{name}; const name = "Alice";
const colors = ["red", "green", "blue"];
const person = { name: "Alice", age: 30 };
console.log(name);
console.log(colors.join(", "));
console.log(person.name); JavaScript has no sigils at all: a string, an array, and an object are all referenced with a bare, unpunctuated name — the type is determined by what the variable holds, not by a leading
$, @, or %. This eliminates an entire category of Perl syntax rules (element access uses $ even on an @array, interpolation context changes the sigil, and so on): in JavaScript the name never changes no matter how the value is used.my vs let/const/var
use v5.38;
my $count = 0;
{
my $count = 10; # shadows the outer $count in this block
say $count;
}
say $count; let count = 0;
{
let count = 10; // shadows the outer count in this block — block-scoped
console.log(count);
}
console.log(count);
// Never use var: it is function-scoped and hoisted, ignoring block boundaries
// entirely, which is a common source of bugs for anyone reading Perl's
// block-scoped "my" and expecting the same behavior. Modern JavaScript's
let and const are genuinely block-scoped, just like Perl's my: an inner let count shadows an outer one exactly the way an inner my $count does. The historical var keyword, kept only for backward compatibility, is function-scoped and hoisted instead — it ignores block boundaries the way Perl's our ignores lexical scoping, and should never be used in new code. Prefer const by default and reach for let only when the binding needs to be reassigned.Dynamic typing in both languages
use v5.38;
my $value = 42;
say ref(\$value) ? "reference" : "plain scalar";
$value = "hello"; # scalars hold any single value freely
$value = [1, 2, 3];
say ref($value); # "ARRAY" — now it's an array reference let value = 42;
console.log(typeof value); // "number"
value = "hello";
console.log(typeof value); // "string"
value = [1, 2, 3];
console.log(Array.isArray(value) ? "array" : typeof value); Both languages let a variable hold a value of any type at any time — neither requires a type declaration. JavaScript's
typeof operator plays a similar diagnostic role to Perl's ref(), though typeof reports "object" for both plain objects and arrays, so checking specifically for an array needs the dedicated Array.isArray() function rather than a single universal type-inspection call.Constants — enforced vs convention
use v5.38;
use constant MAX_RETRIES => 3;
use constant API_URL => "https://example.com";
say MAX_RETRIES;
say API_URL; const MAX_RETRIES = 3;
const API_URL = "https://example.com";
console.log(MAX_RETRIES);
console.log(API_URL);
// MAX_RETRIES = 5; // TypeError: Assignment to constant variable. Perl's
use constant pragma enforces immutability at compile time — reassigning a Perl constant is a compile error. JavaScript's const matches that guarantee directly for the binding itself: reassigning a const name throws a TypeError at runtime. The one caveat is that const only locks the binding, not the value — const list = [1, 2, 3]; list.push(4); is perfectly legal, since the array's contents are still mutable even though list itself can never be pointed at a different array.Truthiness — a different set of exceptions
use v5.38;
# false: undef, 0, "", "0"
# true: everything else — including "00" and " "
for my $value (undef, 0, "", "0", "00", " ") {
my $label = defined($value) ? "'$value'" : "undef";
say "$label is " . ($value ? "truthy" : "falsy");
} // JavaScript falsy values: false, 0, -0, 0n, "", null, undefined, NaN.
// Unlike Perl, the string "0" is truthy in JavaScript — it is simply non-empty.
// Unlike Python, an empty array or object is ALSO truthy in JavaScript.
const values = [undefined, 0, "", "0", "00", " ", []];
for (const value of values) {
const label = value === undefined ? "undefined" : JSON.stringify(value);
console.log(`${label} is ` + (value ? "truthy" : "falsy"));
} JavaScript's truthiness diverges from Perl in the same sharp way Python's does: the exact string
"0" is falsy in Perl but truthy in JavaScript, since JavaScript treats any non-empty string as truthy. JavaScript then goes a step further than Python by keeping empty collections truthy as well — an empty array [] and an empty object {} are both truthy in JavaScript, which trips up not only Perl programmers but Python programmers making the jump too. A Perl if ($value) check that relies on "0" being falsy needs an explicit rewrite when ported to JavaScript.Multiple assignment / destructuring
use v5.38;
my ($first, $second, $third) = (1, 2, 3);
say "$first $second $third";
# Swap without a temp variable
($first, $second) = ($second, $first);
say "$first $second";
# Destructure a list
my ($head, @tail) = (10, 20, 30, 40);
say $head;
say "@tail"; let [first, second, third] = [1, 2, 3];
console.log(`${first} ${second} ${third}`);
// Swap without a temporary variable
[first, second] = [second, first];
console.log(`${first} ${second}`);
// Destructure an array — the rest element soaks up what remains
const [head, ...tail] = [10, 20, 30, 40];
console.log(head);
console.log(tail); Both languages support list-style destructuring assignment and the classic temp-free swap idiom. JavaScript's rest element
...tail plays exactly the same role as Perl's @tail soaking up the remaining elements — one of the closer syntactic matches between the two languages — though JavaScript spells the destructuring target with square brackets ([first, second, third]) rather than Perl's parenthesized list, and needs no sigils to distinguish which name captures the rest.Strings
String interpolation vs template literals
use v5.38;
my $name = "Alice";
my $age = 30;
say "Hello, $name! You are $age years old.";
say "Next year: @{[ $age + 1 ]}"; # expression interpolation trick const name = "Alice";
const age = 30;
console.log(`Hello, ${name}! You are ${age} years old.`);
// Any expression works inside ${}, no special-casing needed
console.log(`Next year: ${age + 1}`); Perl interpolates a bare
$name directly inside a double-quoted string, but embedding an arbitrary expression like age + 1 requires the awkward @{[ expression ]} trick. JavaScript's template literals (backtick-delimited strings, `...`) require the backtick punctuation instead of ordinary quotes — plain "..." and '...' strings never interpolate at all — but once inside backticks, ${expression} accepts any expression directly, with no special-casing needed.String methods as method calls
use v5.38;
my $text = " Hello, World! ";
$text =~ s/^\s+|\s+$//g; # trim: no built-in method, use regex
say $text;
say length($text);
say uc($text);
say lc($text);
say scalar reverse($text);
say index($text, "World") >= 0 ? "found" : "not found"; const text = " Hello, World! ";
const trimmed = text.trim();
console.log(trimmed);
console.log(trimmed.length);
console.log(trimmed.toUpperCase());
console.log(trimmed.toLowerCase());
console.log([...trimmed].reverse().join(""));
console.log(text.includes("World") ? "found" : "not found"); Perl's string operations are mostly standalone functions —
length($text), uc($text), index($text, ...) — that take the string as an argument, and there is no built-in trim function at all (trimming needs a manual regex substitution). JavaScript's String type has built-in methods called directly on the string object: text.trim(), text.toUpperCase(). Reversing has no dedicated string method in JavaScript either — the idiomatic approach spreads the string into an array of characters, reverses the array, and joins it back — and substring membership uses the .includes() method rather than a function call.Heredocs vs template literals
use v5.38;
my $name = "Alice";
my $message = <<~LETTER;
Dear $name,
Welcome to the team!
Regards,
HR
LETTER
print $message; const name = "Alice";
const message = `Dear ${name},
Welcome to the team!
Regards,
HR
`;
console.log(message); JavaScript has no dedicated heredoc syntax; a template literal spanning multiple lines is the closest equivalent, and interpolation is always available inside it with no extra opt-in required. Unlike Perl's squiggly heredoc (
<<~LABEL), a JavaScript template literal does not automatically strip common leading indentation — every line is taken completely literally, so the source usually cannot be indented to match the surrounding code without that indentation leaking into the string.sprintf-style formatting
use v5.38;
printf("%-10s: %6.2f\n", "Price", 9.99);
say sprintf("Hex: 0x%08X", 255);
say sprintf("Count: %05d", 42); // JavaScript has no built-in sprintf/printf at all — pad and format manually.
console.log("Price".padEnd(10) + ": " + (9.99).toFixed(2).padStart(6));
console.log("Hex: 0x" + (255).toString(16).toUpperCase().padStart(8, "0"));
console.log("Count: " + String(42).padStart(5, "0")); This is a genuine gap in JavaScript: unlike Perl's
sprintf/printf (inherited from C) and Python's % operator, JavaScript has no built-in format-string function whatsoever. Formatting falls to a combination of number methods — .toFixed() for decimal places, .toString(16) for a different base — and general-purpose string padding methods, .padStart() and .padEnd(), introduced in ES2017. Node.js does ship a util.format() helper with limited %s/%d support, but it is not available in the browser and covers far fewer format specifiers than Perl's sprintf.Splitting and joining strings
use v5.38;
my $csv = "apple,banana,cherry";
my @fruits = split(/,/, $csv);
say "@fruits";
say join(" | ", @fruits);
my @words = split ' ', " the quick fox "; # special-cased whitespace split
say scalar(@words); const csv = "apple,banana,cherry";
const fruits = csv.split(",");
console.log(fruits.join(" "));
console.log(fruits.join(" | "));
// A trimmed string split on a whitespace regex collapses runs of whitespace,
// matching Perl's special-cased split ' ', $string form
const words = " the quick fox ".trim().split(/\s+/);
console.log(words.length); JavaScript's
string.split(separator) and array.join(separator) map directly onto Perl's split/join, with the call target and separator swapped: Perl calls join($separator, @list) as a standalone function, while JavaScript calls list.join(separator) as a method on the array itself. Getting Perl's special-cased split ' ', $string whitespace-collapsing behavior in JavaScript needs an explicit .trim().split(/\s+/) — there is no single built-in call that both trims and collapses runs of whitespace the way Perl's bare-space form does.Both languages' strings: mutable Perl vs immutable JavaScript
use v5.38;
# Perl scalars holding strings are always mutable
my $greeting = "hello";
$greeting .= ", world"; # append in place, no error
say $greeting; // JavaScript strings are always immutable — every "modification"
// builds a brand-new string and rebinds the name to it.
let greeting = "hello";
greeting += ", world"; // creates a new string; does not mutate the original
console.log(greeting);
greeting += "!";
console.log(greeting); Perl string scalars are always mutable —
.= appends in place with no restriction. JavaScript strings are immutable by design and always have been — every operation that looks like mutation, including +=, actually creates a new string and rebinds the variable to it. This has real performance implications for a loop that builds up a string incrementally in a hot path; the idiomatic fix is collecting pieces in an array and calling .join("") once at the end, rather than repeated concatenation.Numbers
One number type, in both languages
use v5.38;
my $integer = 42;
my $float = 3.14;
say ref(\$integer); # "SCALAR" — Perl doesn't distinguish int from float at the type level
say $integer + $float;
say 2**64; # Perl promotes to floating point beyond native integer range const integerValue = 42;
const floatValue = 3.14;
console.log(typeof integerValue); // "number" — no separate int/float type
console.log(integerValue + floatValue);
console.log(2 ** 64); // imprecise once past Number.MAX_SAFE_INTEGER, like Perl Both languages have exactly one general-purpose numeric type at the source level: Perl's scalar draws no distinction between integer and floating-point values, and JavaScript's
number is always a 64-bit IEEE 754 double, whether it holds 42 or 3.14. Both also lose precision on very large integers in the same way — 2 ** 64 is not computed exactly in either language, since neither has arbitrary-precision integers built into its default numeric type. JavaScript does offer the separate BigInt type (42n) for exact large-integer arithmetic when it is explicitly needed, which Perl has no equivalent for.Division and modulo with negative numbers
use v5.38;
say 10 / 3; # 3.33333... — Perl's / is always floating-point division
say int(10 / 3); # 3 — truncate toward zero for integer division
say 10 % 3; # 1
say -7 % 3; # 2 in Perl — result takes the sign of the right operand console.log(10 / 3); // 3.3333333333333335 — JavaScript's / is always float division
console.log(Math.trunc(10 / 3)); // 3 — truncate toward zero for integer division
console.log(10 % 3); // 1
console.log(-7 % 3); // -1 in JavaScript — result takes the sign of the LEFT operand Both languages'
/ always performs floating-point division regardless of operand types — getting a truncated integer result needs an explicit call, int(...) in Perl and Math.trunc(...) in JavaScript. The two languages disagree on the sign of % with a negative operand, though: Perl's modulo takes the sign of the right operand, so -7 % 3 is 2, while JavaScript's % is a true C-style remainder operator that takes the sign of the left operand, so -7 % 3 is -1. This is a genuine trap for a Perl programmer porting modulo-based logic (like wraparound indexing) without adjustment.Built-in functions vs Math namespace
use v5.38;
my $number = 42;
say $number % 2 == 0 ? "even" : "odd";
say $number > 0 ? "positive" : "not positive";
say abs(-42);
for my $count (0..2) {
say "tick";
} const numberValue = 42;
console.log(numberValue % 2 === 0 ? "even" : "odd");
console.log(numberValue > 0 ? "positive" : "not positive");
console.log(Math.abs(-42));
for (let count = 0; count < 3; count++) {
console.log("tick");
} Both languages check parity and sign with inline comparison expressions — there is no dedicated
even?-style predicate method in either. Perl's abs is a standalone built-in function, while JavaScript groups nearly all of its numeric functions under the Math namespace object — Math.abs(), Math.floor(), Math.max() — rather than exposing them as free-floating global functions. JavaScript has no native integer range syntax matching Perl's 0..2; the classic C-style three-part for loop is the idiomatic replacement for a fixed count of iterations.Numeric conversions
use v5.38;
my $parsed = "42" + 0; # Perl coerces strings to numbers automatically
say $parsed;
say "$parsed"; # back to string via interpolation
say "3.14" + 0.0;
say hex("FF"); # 255
say sprintf("%x", 255); # "ff" const parsedInteger = Number("42");
console.log(parsedInteger);
console.log(String(parsedInteger));
console.log(Number("3.14"));
console.log(parseInt("FF", 16)); // hex parsing: 255
console.log((255).toString(16)); // to hex string: "ff" Perl coerces strings to numbers implicitly in numeric context —
"42" + 0 just works, with warnings if the string does not look numeric. JavaScript also coerces implicitly with +, but in the opposite direction from Perl for mixed operands: since + is overloaded for both addition and string concatenation, "42" + 0 in JavaScript produces the string "420", not the number 42 — the explicit Number("42") conversion is the safe, unambiguous choice. parseInt(string, radix) takes an explicit base argument, just like Perl has no dedicated hex()-equivalent single function; going the other direction, (number).toString(16) replaces Perl's sprintf("%x", ...).Arrays
Arrays — a genuine similarity
use v5.38;
my @numbers = (1, 2, 3, 4, 5);
my @fruits = ("apple", "banana", "cherry");
say $numbers[0]; # element access uses $, not @
say scalar(@fruits); # count
say "@fruits"; # interpolates space-joined const numbers = [1, 2, 3, 4, 5];
const fruits = ["apple", "banana", "cherry"];
console.log(numbers[0]);
console.log(fruits.length);
console.log(fruits.join(" ")); This is one of the closer conceptual matches in the whole comparison. Perl arrays and JavaScript arrays are both heap-allocated, dynamically sized, and freely heterogeneous — a single JavaScript array can mix a number, a string, and another array with no wrapper needed, exactly like a Perl
@array can mix a number, a string, and an array reference. The mechanical difference is sigil consistency: Perl declares an array with @numbers but reads a single element with $numbers[0], while JavaScript uses the same bare name numbers everywhere regardless of whether you are addressing the whole array or one element.Negative indexing and slicing
use v5.38;
my @numbers = (10, 20, 30, 40, 50);
say $numbers[0]; # first: 10
say $numbers[-1]; # last: 50
say $numbers[-2]; # second-to-last: 40
my @middle = @numbers[1..3];
say "@middle"; const numbers = [10, 20, 30, 40, 50];
console.log(numbers[0]); // first: 10
console.log(numbers.at(-1)); // last: 50 — .at() supports negative indices
console.log(numbers.at(-2)); // second-to-last: 40
console.log(numbers.slice(1, 4)); // [20, 30, 40] — stop index is EXCLUSIVE Perl supports negative indexing directly in ordinary bracket notation (
$numbers[-1]), but bare bracket access in JavaScript does not: numbers[-1] silently returns undefined, since JavaScript treats -1 as an object property key rather than an offset from the end. The modern .at() method, added in ES2022, is the correct way to negatively index a JavaScript array. Slicing is where the two diverge as well: Perl's @numbers[1..3] range slice is inclusive on both ends, while JavaScript's .slice(1, 4) is half-open — the stop index is exclusive — so getting the same three elements (indices 1 through 3) requires writing 4, not 3, as the stop argument.push, pop, shift, unshift
use v5.38;
my @items = (2, 3);
push @items, 4; # add to end
unshift @items, 1; # add to front
say "@items"; # 1 2 3 4
my $last = pop @items; # remove from end
my $first = shift @items; # remove from front
say $last;
say $first;
say "@items"; const items = [2, 3];
items.push(4); // add to end — same name as Perl
items.unshift(1); // add to front — same name as Perl too
console.log(items); // [1, 2, 3, 4]
const lastItem = items.pop(); // remove from end
const firstItem = items.shift(); // remove from front
console.log(lastItem);
console.log(firstItem);
console.log(items); This is a rare case where the vocabulary matches exactly: JavaScript's array methods are named
push, pop, shift, and unshift, the identical names Perl uses for the identical operations — a Perl programmer can guess these method names correctly without ever having seen JavaScript before. The only mechanical difference is that JavaScript calls them as methods on the array object (items.push(4)) rather than as standalone functions taking the array as an argument (push @items, 4).sort, reverse, and de-duplication
use v5.38;
use List::Util qw(uniq);
my @numbers = (3, 1, 4, 1, 5, 9);
my @sorted = sort { $a <=> $b } @numbers;
say "@sorted";
say join(" ", reverse @sorted);
say join(" ", uniq(@numbers)); const numbers = [3, 1, 4, 1, 5, 9];
const sortedNumbers = [...numbers].sort((first, second) => first - second);
console.log(sortedNumbers);
console.log([...sortedNumbers].reverse());
console.log([...new Set(numbers)]); // de-duplicate while preserving first-seen order Perl's
sort defaults to string comparison — sorting numbers correctly requires the explicit { $a <=> $b } comparator block, a common beginner trap. JavaScript's .sort() has the exact same trap: with no comparator it also sorts as strings ([10, 9].sort() yields [10, 9] unchanged, since the string "10" sorts before "9"), so a numeric comparator function is just as necessary as Perl's. Both .sort() and .reverse() mutate the array in place in JavaScript, unlike Perl's sort/reverse which return new lists — spreading into a fresh array first ([...numbers]) avoids mutating the original. JavaScript has no built-in uniq either, but the Set constructor, which discards duplicates while preserving insertion order, is the idiomatic order-preserving de-duplication trick — no import needed, unlike Perl's CPAN-distributed List::Util.List::Util reduce/sum0 vs Array methods
use v5.38;
use List::Util qw(reduce sum0);
my @numbers = (1, 2, 3, 4, 5);
my $total = sum0(@numbers);
my $product = reduce { $a * $b } @numbers;
say $total;
say $product; const numbers = [1, 2, 3, 4, 5];
const total = numbers.reduce((accumulator, number) => accumulator + number, 0);
const product = numbers.reduce((accumulator, number) => accumulator * number, 1);
console.log(total); // 15
console.log(product); // 120 Perl needs the CPAN-distributed core module
List::Util to get reduce and sum0 — these are not built into arrays themselves. JavaScript's .reduce() is a genuine built-in Array method requiring no import at all, and it covers both use cases at once (there is no separate sum method — summing is simply .reduce() with an addition callback and an initial value of 0). Always pass an explicit initial value as the second argument to .reduce(); omitting it uses the array's first element as the seed instead, which silently changes behavior on an empty array.map/grep chains vs map/filter methods
use v5.38;
my @numbers = (1, 2, 3, 4, 5, 6);
my @doubled = map { $_ * 2 } @numbers;
my @evens = grep { $_ % 2 == 0 } @numbers;
my @even_doubled = map { $_ * 2 } grep { $_ % 2 == 0 } @numbers;
say "@doubled";
say "@evens";
say "@even_doubled"; const numbers = [1, 2, 3, 4, 5, 6];
const doubled = numbers.map((number) => number * 2);
const evens = numbers.filter((number) => number % 2 === 0);
const evenDoubled = numbers
.filter((number) => number % 2 === 0)
.map((number) => number * 2);
console.log(doubled);
console.log(evens);
console.log(evenDoubled); Perl's
map and JavaScript's .map() share both a name and a purpose, and Perl's grep is JavaScript's .filter() under a different name — the exact same two operations, just spelled differently for the filtering half. Perl's chained form reads right-to-left (the grep filter runs before the outer map transform, even though map is written first), while JavaScript's method-chaining syntax reads left-to-right in call order: .filter(...).map(...) filters first, then transforms, in the same order the code is written.Hashes vs Objects & Maps
Hashes vs plain objects
use v5.38;
my %scores = (
alice => 95,
bob => 87,
carol => 91,
);
say $scores{alice}; # element access uses $, not %
say scalar(keys %scores); # count const scores = {
alice: 95,
bob: 87,
carol: 91,
};
console.log(scores.alice);
console.log(Object.keys(scores).length); Just as with arrays, Perl declares a hash with the
% sigil but reads an individual value with $scores{alice}, while JavaScript uses one bare name (scores) for the whole structure and either dot notation or bracket notation to reach inside it. JavaScript object literals use : between key and value where Perl's fat comma => plays the same visual role, and — like Perl since 5.18 randomized hash ordering for security — a plain JavaScript object's string-keyed properties preserve insertion order in modern engines, though counting keys always needs the explicit Object.keys(...).length rather than a direct property on the object.Accessing, setting, and checking existence
use v5.38;
my %config = (timeout => 30, retries => 3);
$config{timeout} = 60; # set / update
say $config{timeout};
say exists $config{missing} ? "yes" : "no"; # existence check
say $config{missing} // "default"; # defined-or with a default const config = { timeout: 30, retries: 3 };
config.timeout = 60; // set / update
console.log(config.timeout);
console.log("missing" in config ? "yes" : "no"); // existence check via 'in'
console.log(config.missing ?? "default"); // nullish coalescing default Perl's
exists checks for key presence without triggering autovivification. JavaScript's in operator plays the same role, checking a property name's existence on the object — including inherited properties from the prototype chain, which Object.hasOwn(config, "missing") excludes if that distinction matters. JavaScript's ?? nullish-coalescing operator (ES2020) mirrors Perl's // defined-or default pattern closely, though ?? only falls through on null/undefined rather than Perl's broader "undefined" check; accessing a missing key directly (config.missing) returns undefined rather than throwing, similar to how Perl silently returns undef.Iterating over a hash
use v5.38;
my %scores = (alice => 95, bob => 87, carol => 91);
for my $name (sort keys %scores) {
say "$name: $scores{$name}";
}
say join(", ", keys %scores);
say join(", ", values %scores); const scores = { alice: 95, bob: 87, carol: 91 };
for (const [name, score] of Object.entries(scores)) {
console.log(`${name}: ${score}`);
}
console.log(Object.keys(scores).join(", "));
console.log(Object.values(scores).join(", ")); Perl's
keys and values functions have direct JavaScript equivalents (Object.keys(), Object.values()), though JavaScript calls them as static methods on the Object constructor rather than as free-floating functions. Where a Perl programmer loops with for my $key (keys %hash) and then looks up $hash{$key} inside the loop, Object.entries() yields both the key and value together as a two-element array in one step, unpacked directly via destructuring into [name, score] — no separate lookup needed.The Map class — a genuine JavaScript advantage
use v5.38;
# Perl hash keys are always coerced to strings — there is no way
# to use a number, an array reference, or another hash as a key directly.
my %counts;
$counts{"apple"}++;
$counts{"apple"}++;
say $counts{"apple"}; // A plain object also coerces every key to a string in JavaScript.
// The Map class is the fix: any value at all can be a key, including
// objects, arrays, and numbers, with no coercion and no collisions.
const wordCount = new Map();
const word = "apple";
wordCount.set(word, (wordCount.get(word) ?? 0) + 1);
wordCount.set(word, (wordCount.get(word) ?? 0) + 1);
console.log(wordCount.get(word));
const objectAsKey = {};
const arrayAsKey = [1, 2, 3];
const mixedKeyMap = new Map();
mixedKeyMap.set(objectAsKey, "an object key");
mixedKeyMap.set(arrayAsKey, "an array key");
console.log(mixedKeyMap.get(objectAsKey)); Both Perl hashes and plain JavaScript objects share the same limitation: every key is silently coerced to a string, so a Perl programmer already knows to be careful using a reference as a hash key (it stringifies to its memory address, not something useful to compare later). JavaScript's dedicated
Map class, introduced in ES2015, removes that limitation entirely — any value, including an object or an array, can be a key with no coercion and no collision, something Perl has no built-in equivalent for at all without reaching for a CPAN module like Hash::Util::FieldHash.Default values and merging
use v5.38;
my %word_count;
my @words = ("apple", "banana", "apple", "cherry", "apple");
$word_count{$_}++ for @words; # autovivifies each key, starting at undef+1
for my $word (sort keys %word_count) {
say "$word: $word_count{$word}";
}
my %defaults = (timeout => 30, retries => 3, debug => 0);
my %overrides = (timeout => 60);
my %merged = (%defaults, %overrides); # later keys win
say $merged{timeout}; const wordCount = {};
const words = ["apple", "banana", "apple", "cherry", "apple"];
for (const word of words) {
wordCount[word] = (wordCount[word] ?? 0) + 1; // no autovivification — be explicit
}
for (const word of Object.keys(wordCount).sort()) {
console.log(`${word}: ${wordCount[word]}`);
}
const defaults = { timeout: 30, retries: 3, debug: false };
const overrides = { timeout: 60 };
const merged = { ...defaults, ...overrides }; // spread merge — later keys win
console.log(merged.timeout); Perl's autovivification lets
$word_count{$_}++ spring a missing key into existence and treat undef as 0 for the increment — implicit and occasionally surprising. JavaScript has no autovivification at all; incrementing a missing property needs the explicit ?? 0 nullish-coalescing default shown here, since undefined + 1 would otherwise produce NaN. The object spread operator { ...defaults, ...overrides } (ES2018) is the modern equivalent of Perl's (%defaults, %overrides) list-flattening merge, with later keys winning in both.Context Sensitivity
Scalar vs list context — a Perl-only concept
use v5.38;
my @animals = ("cat", "dog", "bird");
my $count = @animals; # scalar context: array evaluates to its length
say $count; # 3
my @copy = @animals; # list context: array evaluates to all its elements
say "@copy";
my $last_result = (1, 2, 3); # scalar context on a list: LAST element, not count!
say $last_result; # 3 — a classic Perl surprise const animals = ["cat", "dog", "bird"];
const count = animals.length; // explicit property access — no ambiguity
console.log(count);
const copy = animals; // always the array itself, no context switch
console.log(copy);
// There is no equivalent surprise: an array is always an array
const lastElement = [1, 2, 3].at(-1);
console.log(lastElement); This is the single biggest conceptual difference between the two languages, and JavaScript has no trace of it whatsoever — the same is true when comparing Perl to almost every other mainstream language. In Perl, the same expression (
@animals, or a parenthesized list) evaluates differently depending on whether it appears in a context that wants one value (scalar context) or many (list context) — assigning an array to a scalar silently gives you its length, but assigning a literal list (1, 2, 3) to a scalar gives you its last element, not its count. JavaScript expressions always evaluate to exactly one thing, and getting a count always means reading the explicit .length property. This eliminates an entire category of Perl surprises, but it also means JavaScript cannot express Perl's occasionally-useful "give me count here, list there" idiom without writing it out explicitly.wantarray — no JavaScript equivalent
use v5.38;
sub context_aware {
if (wantarray()) {
return (1, 2, 3); # caller wants a list
} else {
return "scalar result"; # caller wants a scalar
}
}
my @list_result = context_aware();
my $scalar_result = context_aware();
say "@list_result";
say $scalar_result; // JavaScript functions always return the same value regardless of call site.
// There is no equivalent to wantarray — a function cannot inspect how
// its return value will be used.
function contextAware() {
return [1, 2, 3];
}
const listResult = contextAware();
const firstOnly = contextAware()[0];
console.log(listResult);
console.log(firstOnly); Perl's
wantarray lets a subroutine inspect the context it was called in and return a different value (or even a different shape of value) depending on whether the caller wants a list, a scalar, or nothing (void context). JavaScript has no equivalent mechanism whatsoever — a function always returns the same value, and it is entirely up to the caller to decide what to do with it (index into it, read .length, or use the whole thing). This is simpler to reason about but means the same expressive trick Perl uses for dual-purpose subroutines must be written as two differently named functions in JavaScript.Forcing a count — always explicit
use v5.38;
my @items = (10, 20, 30, 40);
say scalar(@items); # explicit count via the scalar() function
say 0 + @items; # numeric context also forces scalar
say "There are " . @items . " items"; # string concatenation, too — forces scalar const items = [10, 20, 30, 40];
console.log(items.length); // always read .length explicitly
console.log(`There are ${items.length} items`); // no implicit context conversion Perl offers several implicit ways to coerce an array into its count — numeric context, string concatenation, or the explicit
scalar() function all work. JavaScript offers none of these shortcuts: attempting to interpolate an array directly into a template literal prints its comma-joined contents (10,20,30,40), not its length, so items.length must always be read out explicitly, whether inside a template literal or anywhere else.Control Flow
if / elsif / else vs if / else if / else
use v5.38;
my $temperature = 22;
if ($temperature > 30) {
say "hot";
} elsif ($temperature > 20) {
say "warm";
} elsif ($temperature > 10) {
say "cool";
} else {
say "cold";
} const temperature = 22;
if (temperature > 30) {
console.log("hot");
} else if (temperature > 20) {
console.log("warm");
} else if (temperature > 10) {
console.log("cool");
} else {
console.log("cold");
} JavaScript spells the middle branch as two full words,
else if, rather than Perl's single contracted keyword elsif — technically JavaScript has no elseif keyword at all; it is simply an if statement nested as the body of an else, which happens to read the same way. Braces and their placement work identically in both languages, and — unlike Python — indentation carries no syntactic meaning in either Perl or JavaScript, so this is one of the more comfortable, low-friction sections for a Perl programmer.unless and postfix modifiers — a genuine gap
use v5.38;
my $authenticated = 0;
unless ($authenticated) {
say "Please log in";
}
# Postfix form
say "Please log in" unless $authenticated;
say "Welcome!" if $authenticated; const authenticated = false;
if (!authenticated) {
console.log("Please log in");
}
// JavaScript has no postfix "if"/"unless" statement modifier and no unless
// keyword at all — every guard needs a full if statement:
if (!authenticated) {
console.log("Please log in");
}
if (authenticated) {
console.log("Welcome!");
}
// The closest JavaScript gets is the conditional (ternary) EXPRESSION
// (not a statement modifier) — useful only when producing a value:
const message = authenticated ? "Welcome!" : "Please log in";
console.log(message); This is a real loss for a Perl programmer: JavaScript has neither an
unless keyword nor a postfix statement-modifier form for if at all — Ruby borrowed both of these directly from Perl, but JavaScript never adopted either. Every conditional guard must be written as a full if (...) { } block, even for the simplest one-line guard clause that Perl would express as statement unless condition;. JavaScript does have a conditional (ternary) expression (condition ? valueIfTrue : valueIfFalse), but it produces a value for assignment rather than gating a statement, so it is not a substitute for the postfix-modifier idiom.for/foreach vs for...of
use v5.38;
for my $number (1..5) {
say $number * $number;
}
foreach my $letter ("a".."e") {
print "$letter ";
}
print "\n"; for (const number of Array.from({ length: 5 }, (_, index) => index + 1)) {
console.log(number * number);
}
// JavaScript has no native character range — build one from charCodeAt/fromCharCode
for (let code = "a".charCodeAt(0); code <= "e".charCodeAt(0); code++) {
process.stdout.write(String.fromCharCode(code) + " ");
}
console.log(); Perl's numeric range
1..5 has no single built-in JavaScript equivalent — Array.from({ length: 5 }, (_, index) => index + 1) is the standard workaround for generating a counting sequence, considerably more verbose than Perl's range syntax. Perl's character range "a".."e" has no JavaScript equivalent either: a character sequence must be built manually via .charCodeAt() (character to code point) and String.fromCharCode() (code point to character) — the same genuine ergonomic gap Python has, since neither language's range machinery understands characters the way Perl and Ruby's do.while loops — no until in JavaScript
use v5.38;
my $counter = 0;
while ($counter < 5) {
say $counter;
$counter++;
}
$counter = 5;
until ($counter == 0) {
$counter--;
}
say $counter; let counter = 0;
while (counter < 5) {
console.log(counter);
counter++;
}
// JavaScript has no "until" keyword — negate the condition instead
counter = 5;
while (counter !== 0) {
counter--;
}
console.log(counter); Both languages have a
while loop with the same semantics, and both keep C-style ++/-- increment and decrement operators (Ruby dropped these, but Perl and JavaScript both kept them). JavaScript has no until keyword at all (Ruby has one, borrowed from Perl, but JavaScript never adopted it) — "loop while some condition is false" must always be written as a negated while.given/when workaround vs switch
use v5.38;
my $direction = "north";
for ($direction) { # topicalize $_ — the "poor man's given/when" since Perl deprecated given/when
if (/^north$/) { say "Going up"; last; }
if (/^south$/) { say "Going down"; last; }
if (/^east$/ || /^west$/) { say "Going sideways"; last; }
say "Unknown";
} const direction = "north";
switch (direction) {
case "north":
console.log("Going up");
break;
case "south":
console.log("Going down");
break;
case "east":
case "west":
console.log("Going sideways");
break;
default:
console.log("Unknown");
} Perl once had an experimental
given/when construct but removed it due to design problems with smart matching, leaving Perl programmers to fake it with a for ($topic) { if (...) { ...; last } } loop. JavaScript's switch statement is a direct, permanent equivalent, though it comes with one sharp trap of its own: cases fall through to the next case by default unless each one ends with an explicit break — stacking bare case labels (as with "east"/"west" here) is the idiomatic way to group several values under one branch, but forgetting a break anywhere else silently runs the following case's code too.Subroutines & Functions
sub vs function — named parameters vs @_
use v5.38;
sub square {
my ($number) = @_; # unpack the args array manually
return $number * $number;
}
sub greet {
my ($name) = @_;
return "Hello, $name!";
}
say square(7);
say greet("Alice"); function square(number) {
return number * number;
}
function greet(name) {
return `Hello, ${name}!`;
}
console.log(square(7));
console.log(greet("Alice")); Perl subroutines receive all arguments bundled into the single array
@_ and must manually unpack them with my ($number) = @_; — there is no way to name parameters in the signature itself in the widely-used, portable syntax. JavaScript functions declare named parameters directly (function square(number)), so there is no unpacking step — a large ergonomic win for a Perl programmer, though JavaScript still requires an explicit return statement (unlike Ruby, which implicitly returns the last expression) since JavaScript has no implicit-return convention for ordinary function declarations.Anonymous subs vs arrow functions
use v5.38;
my $square = sub {
my ($number) = @_;
return $number * $number;
};
say $square->(7);
my @numbers = (1, 2, 3);
my @doubled = map { $_ * 2 } @numbers;
say "@doubled"; const square = (number) => number * number;
console.log(square(7));
const numbers = [1, 2, 3];
const doubled = numbers.map((number) => number * 2);
console.log(doubled); Perl's anonymous subroutine (
sub { ... }) needs the explicit -> arrow to be called ($square->(7)), since a code reference is just another kind of reference requiring dereferencing. JavaScript's arrow function syntax (ES2015) is considerably terser for the common case of a single-expression callback — (number) => number * number needs no return keyword, no braces, and no explicit call-through-a-reference syntax; it is called exactly like an ordinary function, square(7). Arrow functions are also the idiomatic way to write the transform passed to .map(), playing the same role as Perl's { $_ * 2 } block passed to map.Default parameter values
use v5.38;
sub greet {
my ($name, $greeting) = @_;
$greeting //= "Hello"; # defined-or assigns the default
return "$greeting, $name!";
}
say greet("Alice");
say greet("Bob", "Hi"); function greet(name, greeting = "Hello") {
return `${greeting}, ${name}!`;
}
console.log(greet("Alice"));
console.log(greet("Bob", "Hi")); Perl has no default-parameter syntax in the subroutine signature — the idiomatic approach is unpacking positionally from
@_ and then using //= (defined-or assignment) to fill in a default for any argument left undef. JavaScript supports default values directly in the function signature (greeting = "Hello"), evaluated once per call whenever the argument is undefined — a genuinely simpler model than Python's equivalent feature, since JavaScript re-evaluates the default expression fresh on every call rather than once at function-definition time, avoiding Python's well-known mutable-default-argument trap.Simulated keyword arguments — both languages
use v5.38;
sub create_user {
my (%args) = @_; # unpack into a hash to simulate keyword args
my $name = $args{name};
my $age = $args{age};
my $city = $args{city} // "Unknown";
say "$name, age $age, from $city";
}
create_user(name => "Alice", age => 30, city => "Paris");
create_user(age => 25, name => "Bob"); # order doesn't matter function createUser({ name, age, city = "Unknown" }) {
console.log(`${name}, age ${age}, from ${city}`);
}
createUser({ name: "Alice", age: 30, city: "Paris" });
createUser({ age: 25, name: "Bob" }); // order doesn't matter inside the object literal Neither language has true keyword arguments as a core language feature the way Python does — both simulate them by accepting a single collection of key-value pairs and unpacking it. Perl unpacks a flat list into a hash (
my (%args) = @_;); JavaScript instead accepts a single object parameter and destructures it directly in the function signature ({ name, age, city = "Unknown" }), which additionally allows a default value to be declared right at the destructuring site — a slightly more compact idiom than Perl's separate // default-fallback line for each key.Variadic arguments — @_ slicing vs rest parameters
use v5.38;
sub print_all {
my (@items) = @_;
say $_ for @items;
}
sub describe {
my (%attributes) = @_;
for my $key (sort keys %attributes) {
say "$key: $attributes{$key}";
}
}
print_all(10, 20, 30);
describe(name => "Alice", age => 30); function printAll(...items) {
for (const item of items) {
console.log(item);
}
}
function describe(attributes) {
for (const key of Object.keys(attributes).sort()) {
console.log(`${key}: ${attributes[key]}`);
}
}
printAll(10, 20, 30);
describe({ name: "Alice", age: 30 }); Since Perl subroutines already receive every argument in the flat list
@_, "variadic" just means unpacking all of it into an array (or a hash, for key-value pairs) rather than a fixed number of named scalars. JavaScript's rest parameter syntax, ...items, makes the intent explicit in the signature itself and reuses the exact same triple-dot punctuation as the spread operator seen earlier — collecting every remaining positional argument into a genuine array. There is no dedicated keyword-argument-collecting rest syntax analogous to Perl's hash-unpacking idiom; passing a single object parameter, as in describe here, is the idiomatic JavaScript substitute.Destructuring & Templates
Hash slices vs object destructuring
use v5.38;
my %person = (name => "Alice", age => 30, city => "Paris");
my ($name, $city) = @person{qw(name city)}; # hash slice
say "$name lives in $city"; const person = { name: "Alice", age: 30, city: "Paris" };
const { name, city } = person; // object destructuring
console.log(`${name} lives in ${city}`);
// Renaming while destructuring, with no equivalent Perl shortcut:
const { name: personName, city: personCity } = person;
console.log(`${personName} lives in ${personCity}`); Perl's hash slice,
@person{qw(name city)}, pulls several values out of a hash in one expression, playing a similar role to JavaScript's object destructuring, { name, city } = person. JavaScript goes further by supporting renaming directly at the destructuring site ({ name: personName } binds the value of the name property to a differently-named local variable), something Perl's hash-slice syntax has no equivalent shortcut for — a Perl programmer would need a separate assignment line per renamed variable.Nested destructuring — no Perl equivalent
use v5.38;
my %response = (
status => "ok",
data => { user => { name => "Alice", age => 30 } },
);
# Perl has no destructuring syntax that reaches into nested structures —
# every level of nesting needs its own explicit dereference:
my $name = $response{data}{user}{name};
say $name; const response = {
status: "ok",
data: { user: { name: "Alice", age: 30 } },
};
// Destructuring reaches directly through nested levels in one expression:
const { data: { user: { name } } } = response;
console.log(name); Perl has no destructuring syntax that reaches into nested data structures at all — pulling a deeply nested value out always means chaining explicit dereferences one level at a time (
$response{data}{user}{name}), which is at least straightforward even if verbose. JavaScript's destructuring pattern can mirror the shape of a nested object directly, reaching straight through multiple levels in a single binding — genuinely convenient once the shape is memorized, though the deeply nested pattern syntax itself takes some getting used to and can become difficult to read past two or three levels.Tagged templates — no Perl equivalent
use v5.38;
# Perl has no equivalent to a tagged template literal — string
# interpolation is always evaluated the same fixed way by the
# interpreter itself, with no way to intercept and transform it.
my $name = "O'Brien";
say "Hello, $name!"; # no automatic escaping of any kind // A tag function intercepts the literal pieces and interpolated
// values BEFORE the string is assembled, letting you transform them —
// here, escaping HTML-unsafe characters automatically.
function escapeHtml(stringParts, ...values) {
return stringParts.reduce((result, part, index) => {
const value = values[index - 1];
const escaped = typeof value === "string"
? value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")
: value;
return result + (index > 0 ? escaped : "") + part;
});
}
const name = "<script>alert(1)</script>";
console.log(escapeHtml`Hello, ${name}!`); This is a feature with no Perl equivalent whatsoever: Perl's string interpolation is always evaluated the same fixed way by the interpreter itself, with no hook to intercept or transform the pieces before they are joined. JavaScript's tagged templates let a function placed immediately before a template literal (
escapeHtml`Hello, ${name}!`) receive the literal string segments and the interpolated values separately, before they are concatenated, and decide how to combine them — the mechanism behind libraries that auto-escape HTML or SQL injection risks directly in ordinary-looking template literal syntax.References vs Objects
Explicit \/-> vs implicit object references
use v5.38;
my @numbers = (1, 2, 3);
my $array_ref = \@numbers; # explicit reference operator
say $array_ref->[0]; # explicit arrow dereference
push @$array_ref, 4; # explicit sigil-based dereference
say "@numbers"; const numbers = [1, 2, 3];
const arrayReference = numbers; // arrays are ALWAYS references — no operator needed
console.log(arrayReference[0]); // ordinary bracket access, no special dereference syntax
arrayReference.push(4);
console.log(numbers); This is the most consequential structural difference between the two languages, after context sensitivity. Perl distinguishes plain values from references explicitly: creating one needs the
\ operator, and reading through one needs either the -> arrow or a sigil-based dereference like @$array_ref — the reference-ness of a variable is always visible in the syntax used to touch it. JavaScript has no separate reference type or reference-creation operator at all: every array and every object is a reference by nature, assignment never copies one, and reading or writing through it uses the exact same bracket or dot syntax as any other value — there is nothing that visually marks a variable as "a reference" the way Perl's sigils and arrows do.Value copy vs reference copy
use v5.38;
my @original = (1, 2, 3);
my @copy = @original; # list assignment COPIES the elements
push @copy, 4;
say "@original"; # unaffected: 1 2 3
say "@copy"; # 1 2 3 4
my $ref_a = \@original;
my $ref_b = $ref_a; # reference assignment SHARES the same array
push @$ref_b, 99;
say "@original"; # affected: 1 2 3 99 const original = [1, 2, 3];
const assigned = original; // assignment SHARES the same array — no copy happens
assigned.push(4);
console.log(original); // affected: [1, 2, 3, 4]
console.log(assigned); // affected too — same underlying array
// A genuine copy needs an explicit spread or Array.from:
const realCopy = [...original];
realCopy.push(99);
console.log(original); // unaffected: [1, 2, 3, 4] This is where Perl's explicitness becomes a real safety feature: assigning one Perl array to another with
@copy = @original always copies every element, and only an explicit reference assignment shares the underlying storage. JavaScript has no plain-value assignment mode for arrays or objects at all — const assigned = original always shares the same underlying array, full stop, since there is no non-reference way to hold one in the first place. A genuine copy requires an explicit operation, most commonly the spread operator ([...original]), and forgetting this is one of the most common sources of action-at-a-distance bugs for programmers arriving in JavaScript from a language with an explicit copy-by-default semantics like Perl's arrays.Building nested structures
use v5.38;
my %company = (
name => "Acme",
employees => [
{ name => "Alice", roles => ["engineer", "lead"] },
{ name => "Bob", roles => ["designer"] },
],
);
say $company{employees}[0]{name};
say $company{employees}[0]{roles}[1]; const company = {
name: "Acme",
employees: [
{ name: "Alice", roles: ["engineer", "lead"] },
{ name: "Bob", roles: ["designer"] },
],
};
console.log(company.employees[0].name);
console.log(company.employees[0].roles[1]); Both languages can freely nest arrays inside hashes/objects and vice versa to arbitrary depth, and the reading syntax ends up looking almost identical (
$company{employees}[0]{name} versus company.employees[0].name). The underlying mechanism is different, though: Perl's hash and array values are implicitly references the moment they are nested this way (an @array cannot literally live inside a %hash value slot — only a reference to one can), while every JavaScript array and object nests directly, since arrays and objects are always reference types in JavaScript with no separate reference-taking step ever required.Regular Expressions
Regex literals in both languages
use v5.38;
my $text = "The quick brown fox";
if ($text =~ /quick (\w+) fox/) {
say "Matched: $1";
}
say "matches" if $text =~ /^The/; const text = "The quick brown fox";
const match = text.match(/quick (\w+) fox/);
if (match) {
console.log(`Matched: ${match[1]}`);
}
console.log(/^The/.test(text) ? "matches" : "no match"); Both languages treat a regular expression as a genuine literal syntax, delimited by forward slashes, rather than a string that must first be compiled — a real similarity, since JavaScript inherited its regex literal syntax directly from Perl through the shared lineage of early web scripting languages. The mechanical difference is in how a match result is obtained: Perl's
=~ binding operator matches in place and populates $1, $2, and so on with capture groups, while JavaScript's .match() method returns an array-like result object where the whole match is index 0 and captures follow at index 1, 2, and so on — no special variables involved.s/// substitution vs .replace()
use v5.38;
my $text = "The rain in Spain";
(my $replaced = $text) =~ s/ain/AIN/g;
say $replaced;
my $count = () = $text =~ /ain/g; # count matches via list-context trick
say $count; const text = "The rain in Spain";
const replaced = text.replaceAll("ain", "AIN");
console.log(replaced);
const count = (text.match(/ain/g) ?? []).length;
console.log(count); Perl's
s/pattern/replacement/g operator performs the substitution in place on a copy (the (my $replaced = $text) =~ s/.../ idiom copies first, then substitutes destructively). JavaScript's .replaceAll() method (ES2021) is a pure function that always returns a new string, leaving the original untouched — there is no destructive substitution operator in JavaScript at all, matching the language's general preference for immutable-feeling string operations. Counting matches has no dedicated syntax in JavaScript comparable to Perl's clever my $count = () = $text =~ /pattern/g; list-context assignment trick; the idiomatic JavaScript approach falls back to .match() with the global flag and reading the resulting array's .length.Named captures — nearly identical syntax
use v5.38;
my $date = "2026-07-03";
if ($date =~ /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/) {
say "Year: $+{year}, Month: $+{month}, Day: $+{day}";
} const date = "2026-07-03";
const match = date.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
if (match) {
console.log(`Year: ${match.groups.year}, Month: ${match.groups.month}, Day: ${match.groups.day}`);
} Named capture groups are spelled identically in both languages —
(?<name>...) — since JavaScript's regex engine (added in ES2018) directly copied Perl's named-capture syntax rather than inventing its own. The only real difference is where the captured values end up: Perl populates the special hash %+, accessed as $+{year}, while JavaScript attaches a .groups object directly onto the match result, accessed as match.groups.year — no separate global variable to remember.Splitting on a regex — both languages
use v5.38;
my $text = "one1two22three333four";
my @parts = split(/\d+/, $text);
say "@parts"; const text = "one1two22three333four";
const parts = text.split(/\d+/);
console.log(parts); Both
split functions accept a regular expression directly as the separator, and the behavior is identical here — a genuine, low-friction similarity. Perl's split is a standalone function taking the string as its second argument, while JavaScript's .split() is a method called on the string itself, but otherwise the mental model transfers with no adjustment needed.OOP
package + bless vs native class
use v5.38;
package Animal;
sub new {
my ($class, %args) = @_;
return bless {
name => $args{name},
sound => $args{sound} // "...",
}, $class;
}
sub speak {
my $self = shift;
say "$self->{name} says $self->{sound}";
}
package main;
my $dog = Animal->new(name => "Rex", sound => "Woof");
$dog->speak; class Animal {
constructor(name, sound = "...") {
this.name = name;
this.sound = sound;
}
speak() {
console.log(`${this.name} says ${this.sound}`);
}
}
const dog = new Animal("Rex", "Woof");
dog.speak(); This is where a Perl programmer will feel real envy. Perl OOP is built manually from two separate primitives: a
package to hold the namespace, and bless to associate a plain hash reference with that package so it is recognizable as an object — every constructor is hand-written. JavaScript has a genuine native class keyword (ES2015), which is syntax sugar over the same prototype-based object model underneath, but the sugar is thick enough that it reads almost like a class-based language: constructor() replaces Perl's hand-written new subroutine, and this.name = name replaces the manual bless { name => ... }, $class pattern.Hand-written accessors vs public/private fields
use v5.38;
package Person;
sub new {
my ($class, %args) = @_;
return bless { name => $args{name}, age => $args{age} }, $class;
}
sub name { $_[0]->{name} } # read-only getter
sub age { # combined getter/setter
my $self = shift;
$self->{age} = shift if @_;
return $self->{age};
}
package main;
my $person = Person->new(name => "Alice", age => 30);
say $person->name;
$person->age(31);
say $person->age; class Person {
#name; // # prefix marks a genuinely private field (ES2022)
constructor(name, age) {
this.#name = name;
this.age = age; // public — direct property access, no accessor needed
}
get name() {
return this.#name;
}
}
const person = new Person("Alice", 30);
console.log(person.name);
person.age = 31;
console.log(person.age);
// person.#name; // SyntaxError outside the class body — truly private, not convention Perl has no built-in accessor generation at all — every getter and setter is hand-written, often using the combined pattern shown here where the same method acts as both getter and setter depending on whether an argument was passed, and Perl has no true privacy either (a leading underscore is purely a naming convention that nothing enforces). JavaScript's public fields need no accessor methods whatsoever — direct property assignment, as with
person.age = 31, works with no method call — while the #-prefixed private field syntax (ES2022) is a genuine language-enforced privacy mechanism, unlike Perl's underscore convention: accessing person.#name from outside the class is a hard SyntaxError, not merely bad manners.Inheritance — @ISA vs extends
use v5.38;
package Animal;
sub new { bless { name => $_[1] }, $_[0] }
sub name { $_[0]->{name} }
sub speak { say $_[0]->{name} . " makes a sound" }
package Dog;
our @ISA = ('Animal'); # declare inheritance
sub speak {
my $self = shift;
say $self->name . " barks";
}
package main;
my $dog = Dog->new("Rex");
$dog->speak;
say $dog->isa("Animal") ? "is an Animal" : "not an Animal"; class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
}
class Dog extends Animal {
speak() {
return `${this.name} barks`;
}
}
const dog = new Dog("Rex");
console.log(dog.speak());
console.log(dog instanceof Animal ? "is an Animal" : "not an Animal"); Perl declares inheritance by populating the special array
@ISA with parent package names (or, in modern Perl, the more readable use parent 'Animal'; pragma). JavaScript uses the extends keyword directly in the class definition — a single-inheritance declaration built into the syntax, closer to Ruby's < than to Perl's array-based mechanism. JavaScript's instanceof operator replaces Perl's isa method call for checking ancestry, and both languages support calling the parent's overridden method — Perl's SUPER::method, JavaScript's super.method().use overload vs no operator overloading at all
use v5.38;
package Money;
use overload
'+' => \&add,
'""' => \&stringify;
sub new { bless { cents => $_[1] }, $_[0] }
sub add { Money->new($_[0]->{cents} + $_[1]->{cents}) }
sub stringify { sprintf('$%.2f', $_[0]->{cents} / 100) }
package main;
my $total = Money->new(150) + Money->new(250);
say "$total"; class Money {
#cents;
constructor(cents) {
this.#cents = cents;
}
add(other) {
return new Money(this.#cents + other.#cents);
}
toString() {
return `$${(this.#cents / 100).toFixed(2)}`;
}
}
const total = new Money(150).add(new Money(250));
console.log(`${total}`); // toString() is called implicitly here JavaScript has no true operator-overloading protocol for a custom class at all — unlike Perl's
use overload pragma, there is no way to make JavaScript's built-in + operator invoke a method on a custom object; addition between two Money instances must be an explicit method call like .add() rather than total + other. Where the languages do align is stringification: JavaScript's toString() method plays exactly the role Perl's '""' overload entry plays, and JavaScript needs no separate pragma to opt in — any class simply defining toString() is automatically called wherever the object is coerced to a string, such as inside a template literal.Error Handling
die/eval vs try/catch/throw
use v5.38;
eval {
die "Something went wrong\n";
say "This won't print";
};
if ($@) {
say "Caught: $@";
} try {
throw new Error("Something went wrong");
console.log("This won't print");
} catch (error) {
console.log(`Caught: ${error.message}`);
} die is Perl's throw; a bare eval { } block is the try-block equivalent of JavaScript's try/catch. JavaScript's error-handling ergonomics are a real improvement over Perl's global-variable-based approach: the caught error object is bound directly to a local variable at the point of the catch clause (catch (error)), with no equivalent of Perl's easy-to-forget or easy-to-accidentally-clear $@ global to check manually. JavaScript also permits throw-ing any value at all, not only an Error object, though throwing a proper Error (or a subclass of it) is always the idiomatic choice since it carries a stack trace.Blessed error objects vs Error subclasses
use v5.38;
package ValidationError;
sub new {
my ($class, $message, $field) = @_;
return bless { message => $message, field => $field }, $class;
}
sub message { $_[0]->{message} }
sub field { $_[0]->{field} }
package main;
eval {
die ValidationError->new("must not be empty", "username");
};
if (my $error = $@) {
if (ref($error) && $error->isa("ValidationError")) {
say "Validation failed on '" . $error->field . "': " . $error->message;
}
} class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
try {
throw new ValidationError("must not be empty", "username");
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Validation failed on '${error.field}': ${error.message}`);
}
} Perl can
die with a blessed object instead of a plain string, letting the catch-equivalent branch on ref($error) && $error->isa("ValidationError") to distinguish error types — a manual pattern with no dedicated exception-class hierarchy backing it. JavaScript's built-in Error class is designed to be subclassed directly: extends Error plus a super(message) call in the constructor produces a genuine exception type complete with a stack trace, and instanceof checks the class hierarchy natively rather than relying on Perl's isa method convention.Cleanup — local $@ save/restore vs finally
use v5.38;
my $file_opened = 0;
eval {
$file_opened = 1;
say "Working...";
die "failure during work\n";
};
my $error = $@; # save before anything can clobber it
if ($file_opened) {
say "Cleaning up (would close the file handle here)";
}
say "Caught: $error" if $error; let fileOpened = false;
try {
fileOpened = true;
console.log("Working...");
throw new Error("failure during work");
} catch (error) {
console.log(`Caught: ${error.message}`);
} finally {
if (fileOpened) {
console.log("Cleaning up (would close the file handle here)");
}
} Perl has no dedicated cleanup block syntax — guaranteeing cleanup code runs whether or not
die was called requires either careful manual bookkeeping (saving $@ immediately, since a later operation can silently clear it) or a module like Try::Tiny or core Feature::Compat::Try's finally block. JavaScript's try/catch/finally has a dedicated finally clause built directly into core syntax: code inside it always runs, whether the try block completed successfully, threw an exception that was caught, or even if the catch block itself returned or re-threw — no manual state-saving required.Async & Promises
Promises — a genuine JavaScript advantage
use v5.38;
# Perl has no built-in Promise or async/await concept at all.
# Asynchronous work in Perl means callbacks (AnyEvent, IO::Async)
# or blocking calls — there is no language-level primitive for
# "a value that will be available later."
say "Perl runs this line...";
say "...then this line, strictly in order, with no async primitive."; // A Promise represents a value that will be available LATER —
// a genuine language-level concept Perl has no equivalent for at all.
const delayedGreeting = new Promise((resolve) => {
setTimeout(() => resolve("Hello from the future!"), 10);
});
delayedGreeting.then((message) => console.log(message));
console.log("This logs first — the Promise resolves asynchronously."); This is a genuine, structural JavaScript advantage over Perl: JavaScript's
Promise is a language-level primitive representing "a value that will be available later," with the entire runtime built around a single-threaded event loop that resolves promises as their underlying work completes. Perl has nothing comparable in the core language — asynchronous work in Perl means either blocking calls that stall the whole process, or reaching for a CPAN event-loop module like AnyEvent or IO::Async and structuring code around explicit callbacks, a strictly more manual and error-prone style than a built-in Promise.async/await — no Perl equivalent
use v5.38;
# Again, no equivalent: Perl subroutines are always synchronous.
# Simulating "asynchronous-looking" code requires an external
# event-loop module and callback-based or coroutine-based style,
# never a built-in keyword pair like async/await.
sub fetch_greeting {
return "Hello, synchronously, since Perl has no core async keyword";
}
say fetch_greeting(); async function fetchGreeting() {
// await pauses this function (not the whole program) until the
// Promise settles, while other code keeps running on the event loop.
const response = await new Promise((resolve) => {
setTimeout(() => resolve("Hello, asynchronously!"), 10);
});
return response;
}
(async () => {
const greeting = await fetchGreeting();
console.log(greeting);
})(); JavaScript's
async/await keyword pair (ES2017) lets asynchronous code read almost exactly like synchronous code, while still yielding control back to the event loop at every await point — Perl has no core-language equivalent whatsoever, and even CPAN's best event-loop modules require either nested callbacks or coroutine-style tricks to approximate the same readability. The immediately-invoked async arrow function wrapping the final call here is a common idiom for using await at the top level of a plain script, since node -e and older module systems do not permit a bare top-level await outside an async function.Concurrent work — Promise.all
use v5.38;
# Perl has no built-in equivalent: running several independent
# operations "at once" and collecting all their results requires
# an external event-loop module (AnyEvent, IO::Async, Mojo::IOLoop),
# each with its own API for this pattern.
for my $name (qw(alice bob carol)) {
say "Fetched profile for $name (run sequentially in this example)";
} function fetchProfile(name) {
return new Promise((resolve) => {
setTimeout(() => resolve(`Fetched profile for ${name}`), 10);
});
}
(async () => {
// All three requests run concurrently; Promise.all waits for every one
const profiles = await Promise.all([
fetchProfile("alice"),
fetchProfile("bob"),
fetchProfile("carol"),
]);
for (const profile of profiles) {
console.log(profile);
}
})(); Promise.all() runs every promise in the array concurrently and resolves once all of them have settled, returning their results in the same order they were passed in — a built-in primitive for "fan out several independent operations, then wait for all of them," with genuinely no Perl core-language equivalent. A Perl programmer would need a CPAN event-loop module such as Mojo::IOLoop or AnyEvent, each with its own bespoke API for the same pattern, whereas every JavaScript environment (browser or Node.js) ships Promise.all() as a language-level built-in with no import needed.File & Console I/O
Reading a file
use v5.38;
open(my $file_handle, '<', 'data.txt') or die "Cannot open: $!";
while (my $line = <$file_handle>) {
chomp $line;
say $line;
}
close $file_handle; import { readFile } from "node:fs/promises";
const contents = await readFile("data.txt", "utf8");
for (const line of contents.split("\n")) {
console.log(line);
} Perl opens a file handle with a three-argument
open call and reads it line by line with the diamond operator <$file_handle> inside a while loop, closing it explicitly afterward. JavaScript has no built-in filesystem access in the browser at all — file I/O is only available in a server-side runtime like Node.js, via the node:fs module — and the modern idiomatic approach reads the whole file at once with a promise-based readFile() rather than streaming it line by line, then splits the result into lines afterward. Both sides assume a data.txt file already exists on disk, so neither can run in the sandboxed backends used by this page.Writing a file
use v5.38;
open(my $file_handle, '>', 'output.txt') or die "Cannot open: $!";
print $file_handle "Hello, World!\n";
close $file_handle; import { writeFile } from "node:fs/promises";
await writeFile("output.txt", "Hello, World!\n", "utf8"); Perl opens the file handle in write mode with the
> mode string, prints directly to the handle, and closes it explicitly. Node.js's fs/promises module collapses the open-write-close sequence into a single writeFile() call, which handles opening and closing the file descriptor internally — there is no separate handle object for the caller to manage or remember to close, at the cost of always writing the entire content in one call rather than streaming it incrementally.@ARGV vs process.argv
use v5.38;
# Run as: perl script.pl foo bar
say "Number of arguments: " . scalar(@ARGV);
say "First argument: $ARGV[0]" if @ARGV;
say "All arguments: @ARGV"; // Run as: node script.js foo bar
// process.argv[0] is the node executable path, [1] is the script path —
// actual user arguments start at index 2, unlike Perl's @ARGV which
// contains ONLY the user-supplied arguments from index 0.
const userArguments = process.argv.slice(2);
console.log(`Number of arguments: ${userArguments.length}`);
if (userArguments.length > 0) {
console.log(`First argument: ${userArguments[0]}`);
}
console.log(`All arguments: ${userArguments.join(" ")}`); Perl's
@ARGV contains exactly and only the user-supplied command-line arguments, indexed from 0. Node.js's process.argv includes two extra leading entries — the path to the Node executable itself and the path to the script being run — so the equivalent of Perl's @ARGV is process.argv.slice(2), not the bare array; forgetting this offset is a common early mistake for a Perl programmer's first Node.js script.%ENV vs process.env
use v5.38;
my $home = $ENV{HOME} // "unset";
say "HOME: $home";
$ENV{MY_VAR} = "custom value"; # setting also works
say $ENV{MY_VAR}; const homeDirectory = process.env.HOME ?? "unset";
console.log(`HOME: ${homeDirectory}`);
process.env.MY_VAR = "custom value"; // setting also works, for this process only
console.log(process.env.MY_VAR); Perl's
%ENV hash and Node.js's process.env object play an identical role: both expose environment variables as a simple key-value collection, both support reading with a fallback default (Perl's //, JavaScript's ??), and both allow writing a new entry that is visible to child processes spawned afterward but never propagates back to the parent shell. There is no browser equivalent for process.env at all — environment variables are a Node.js-specific, server-side concept, since a web page has no access to the host operating system's environment.CPAN vs npm
CPAN/cpanm vs npm
# Installing a dependency:
# cpanm JSON::PP
#
# Declaring dependencies (cpanfile):
# requires 'JSON::PP', '4.16';
# requires 'HTTP::Tiny', '0.088';
#
# Installing everything a cpanfile lists:
# cpanm --installdeps . // Installing a dependency:
// npm install lodash
//
// Declaring dependencies happens automatically in package.json:
// "dependencies": {
// "lodash": "^4.17.21"
// }
//
// Installing everything package.json lists:
// npm install CPAN (the Comprehensive Perl Archive Network) predates npm by roughly a decade and pioneered the idea of a central package repository with a dependency-declaration file — Perl's
cpanfile and cpanm --installdeps plays the same role as npm's package.json and npm install. The biggest practical difference is scope: cpanm installs modules globally (or into a local::lib-managed directory) by default, while npm installs into a project-local node_modules directory by default, keeping each project's dependencies completely isolated from every other project on the same machine with no extra configuration needed.cpanfile.snapshot vs package-lock.json
# Perl's reproducible-install story is comparatively new and less
# universal: Carton (a Bundler-inspired tool) generates a
# cpanfile.snapshot pinning every dependency's exact resolved version.
# carton install
# carton exec perl script.pl // npm generates package-lock.json automatically, with NO extra
// tool needed — it pins the exact resolved version (and integrity
// hash) of every direct and transitive dependency:
// npm install
// npm ci // installs EXACTLY what the lockfile specifies, no updates Reproducible installs are a more recent, optional addition to the Perl ecosystem:
Carton, modeled explicitly on Ruby's Bundler, generates a cpanfile.snapshot pinning exact resolved versions, but it is a separate tool a project must opt into. npm's package-lock.json is generated automatically by npm install with no separate tool required, and npm ci gives a genuinely stricter guarantee than Carton's workflow: it installs precisely what the lockfile specifies and fails outright if package.json and the lockfile have drifted apart, rather than silently resolving a new set of compatible versions.JSON::PP vs native JSON
use v5.38;
use JSON::PP;
my $data = { name => "Alice", age => 30, tags => ["admin", "verified"] };
my $json_text = encode_json($data);
say $json_text;
my $decoded = decode_json($json_text);
say $decoded->{name}; const data = { name: "Alice", age: 30, tags: ["admin", "verified"] };
const jsonText = JSON.stringify(data);
console.log(jsonText);
const decoded = JSON.parse(jsonText);
console.log(decoded.name); Perl needs an explicit module —
JSON::PP is bundled with core Perl since 5.14, but still requires an explicit use statement and function calls, encode_json/decode_json. JavaScript has JSON support built directly into the language with zero imports required at all: JSON.stringify() and JSON.parse() are global objects available everywhere JavaScript runs, a natural consequence of JSON's syntax having been derived directly from JavaScript object literal syntax in the first place (the "JS" in "JSON" is not a coincidence).