JavaScript scoping makes my head hurt
In which I grumble about JavaScript’s dodgy scoping (more or less solved by let
in modern JavaScript.)
Who came up with the JavaScript scoping rules? What were they smoking. Here’s some Noddy Perl that demonstrates what I’m on about:
my @subs;
for my $i (0..4) {
push @subs, sub { $i }
}
print $subs[0]->(); # => 0;
Here’s the equivalent (or what I thought should be the equivalent) in JavaScript:
var subs [];
for (var i in [0,1,2,3,4]) {
subs[i] = function () {
return i;
}
}
alert subs[0]() // => 4
What’s going on? In Perl, $i
is scoped to the for
block.
Essentially, each time through the loop, a new variable is created, so
each generated closure refers to a different $i
. In Javascript, i
is scoped to the for
loop’s containing function. Each of the generated
closures refer to the same i
. Which means that, to get the same effect
as the Perl code, you must write:
var subs = [];
for (var shared_i in [0,1,2,3,4]) {
(function (i) {
subs[i] = function () {
return i;
};
})(shared_i);
}
subs[0]() // => 0
Dodgy Ruby scoping
I had initially planned to write the example “How it should work” code
in Ruby, but it turns out that Ruby’s for
has the same problem:
subs = [];
for i in 0..4
subs << lambda { i }
end
subs[0].call # => 4
Which is one reason why sensible Ruby programmers don’t use for
. If I
were writing the snippet in ‘real’ Ruby, I’d write:
subs = (0..4).collect { |i|
lambda { i }
}
subs[0].call # => 0
My conclusion
JavaScript is weird. Okay, so you already know this. In so many ways it’s a lovely language, but it does have some annoyingly odd corners.
13 historic comments »
By Brendan Thu, 20 Mar 2008 13:31:29 GMT
Here’s how I would have done this sort of thing in JavaScript:
var subs = [];
for (var i in [0,1,2,3,4]) {
subs[i] = {
execute: function() {return this.i;}
};
subs[i].i = i;
}
alert(subs[0].execute());
You’re right, though: JavaScript’s scoping is very often contrary to expectations.
By anon Thu, 20 Mar 2008 16:02:40 GMT
I think it’s Perl that’s being dodgy here, for a ‘scripting’ language. The equivalent in python behaves the same as js and ruby:
[lambda: i for i in range(5)]0() # 4
Note though, if you use the value of i
in the list comprehension
scope rather than keeping the reference alive with the lambda, it acts
as you want it to:
[lambda n=i: n for i in range(5)]0() # 0
A bit of a cheat, but a short way to spell it.
By Piers Cawley Thu, 20 Mar 2008 17:12:28 GMT
@Brendan: I’ve always disliked that style – what’s the point of manually unpicking a perfectly serviceable closure?
By Piers Cawley Thu, 20 Mar 2008 17:17:07 GMT
@anon: Ah, one more reason to maintain my dislike for Python.
By James Duncan Thu, 20 Mar 2008 19:19:10 GMT
In JavaScript 1.7 you get “let” which does what you’re looking for. Of course that doesn’t help you in the majority of browsers, but it is a recognised problem.
By Aristotle Pagaltzis Thu, 20 Mar 2008 19:59:50 GMT
Yeah, scoping is insufficiently thought out in JavaScript and downright braindead in Python. At least in JavaScript, you can fix this particular example by reformulating it in the same way as the reformulated Ruby example:
var subs = [0,1,2,3,4].map( function(i) {
return function() { i };
} );
Of course I’d write this in Perl just like you did in “real Ruby”:
my @subs = map { my $i = $_; sub { $i } } 0 .. 4;
And gosh, here I was annoyed by the verbosity of having to write
sub
in front of every block snippet. Welcome to JavaScript
which makes you write function()
!
By Piers Cawley Fri, 21 Mar 2008 14:56:20 GMT
Heh. JavaScript continues in its mission to turn people onto Lisp (but with extra syntax) eh? Are any of the various JavaScript implementations doing tail call optimisation yet? It’d certainly make continuation passing style a good deal easier to manage.
By Piers Cawley Fri, 21 Mar 2008 15:06:59 GMT
@Aristotle: And no way to alias function
either. Giles Bowkett came
up with a neat ruby hack:
alias :L :lambda
Which I find rather delightful.
By Aristotle Pagaltzis Fri, 21 Mar 2008 18:53:53 GMT
Ah, then you’ll enjoy
lambda
.
:-)
By Daniel Berger Mon, 24 Mar 2008 11:05:01 GMT
Pssh, Ruby doesn’t require a third party library:
http://www.oreillynet.com/ruby/blog/2007/10/fun_with_unicode_1.html
By Piers Cawley Mon, 24 Mar 2008 14:13:37 GMT
@Daniel: You obviously haven’t looked at lambda.pm
very closely. It’s
not substantially longer than the ruby implementation, but it has some
very neat tricks for lexically scoping the λ.
Perl’s conventions for module inclusion and pragma-like modules knock Ruby’s into a cocked hat, frankly.
By David Cantrell Tue, 25 Mar 2008 04:00:57 GMT
The loop counter is declared outside the for{}
block, so to me it
makes sense that it could be scoped to the containing block. Perl’s
the odd one out here. I like the way it’s odd, but it’s still odd.
And, digging into the dusty recesses of my memory, isn’t that the same as how a for-loop counter would be scoped in C?
By Piers Cawley Tue, 25 Mar 2008 05:26:33 GMT
Who cares how it’s scoped in C? Frankly, the difference in scope only really becomes important/noticeable when your language gets closures. The slight weirdness of the Perl 5 scoping’s been addressed in Perl 6: It’s now:
foreach @array -> $each {
...
}
Where -> $whatever { ... }
is the new way of declaring parameterised
anonymous blocks. I’m sure it won’t be long before someone writes a
library to let them spell that as → $foo { ... }