JavaScript scoping makes my head hurt

Piers Cawley

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 »

These are archived comments. To respond to this post, use a webmention

http://www.gravatar.com/avatar.php?gravatar_id=40f438095ffe6ce6a6ccffc4837e80d2&size=48&url=http://threebrothers.org/brendan/ 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.

http://www.gravatar.com/avatar.php?gravatar_id=d41d8cd98f00b204e9800998ecf8427e&size=48&url= 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.

http://www.gravatar.com/avatar.php?gravatar_id=0196ff65610046d2f8ba58bc4a45f144&size=48&url=http://www.bofh.org.uk/ 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?

http://www.gravatar.com/avatar.php?gravatar_id=0196ff65610046d2f8ba58bc4a45f144&size=48&url=http://www.bofh.org.uk/ By Piers Cawley Thu, 20 Mar 2008 17:17:07 GMT

@anon: Ah, one more reason to maintain my dislike for Python.

http://www.gravatar.com/avatar.php?gravatar_id=d41d8cd98f00b204e9800998ecf8427e&size=48&url=http://whoot.org 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.

http://www.gravatar.com/avatar.php?gravatar_id=e17949267bbfe21a0fadf1bbf00592b4&size=48&url=http://plasmasturm.org/ 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()!

http://www.gravatar.com/avatar.php?gravatar_id=0196ff65610046d2f8ba58bc4a45f144&size=48&url=http://www.bofh.org.uk/ 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.

http://www.gravatar.com/avatar.php?gravatar_id=0196ff65610046d2f8ba58bc4a45f144&size=48&url=http://www.bofh.org.uk/ 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.

http://www.gravatar.com/avatar.php?gravatar_id=e17949267bbfe21a0fadf1bbf00592b4&size=48&url=http://plasmasturm.org/ By Aristotle Pagaltzis Fri, 21 Mar 2008 18:53:53 GMT

Ah, then you’ll enjoy lambda. :-)

http://www.gravatar.com/avatar.php?gravatar_id=d41d8cd98f00b204e9800998ecf8427e&size=48&url= 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

http://www.gravatar.com/avatar.php?gravatar_id=0196ff65610046d2f8ba58bc4a45f144&size=48&url=http://bofh.org.uk 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.

http://www.gravatar.com/avatar.php?gravatar_id=d41d8cd98f00b204e9800998ecf8427e&size=48&url= 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?

http://www.gravatar.com/avatar.php?gravatar_id=0196ff65610046d2f8ba58bc4a45f144&size=48&url=http://www.bofh.org.uk/ 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 { ... }

  • 0 likes
  • 0 reposts
  • 0 replies
  • 0 mentions