JavaScript Tips for Novices, Acolytes, and Gurus

JavaScript Tips for Novices, Acolytes, and Gurus
By Kris Kowal | Published: August 27, 2007 - 03:48PM CT

JavaScript, with its death grip on the Interwebs and every AJAX developer's language of only choice, is getting hot. With roll-overs, pop-ups, and menus either solved or relegated to CSS behaviors, XMLHttpRequests have made JavaScript once again a language of innovation. This article is meant to offer a tidbit of idiomatic advice for people who know JavaScript or want to know JavaScript coming from Java, C, Python, Perl, or PHP.

Novice: objects and associative arrays
Coming from Java, you might expect JavaScript to provide a base type, like Object. Coming from most languages, you might expect JavaScript to provide a dictionary, hash-table, or associative array type like Perl's %hash type, Python's dict type, PHP's array type, or Java's Map interface. JavaScript kills two birds with one stone, and I mean knocks them both dead. Object is the common, ancestral prototype of most instances. They are also used for String to Object mappings.

JavaScript provides a shorthand syntax for Object initialization, much like Perl and Python associative arrays.

var foo = new Object();
var foo = {};

Also, item indexing and member selection are equivalent.

foo.a == foo['a'];

Since you can only use Strings as keys, indexing an Object on another Object implies that JavaScript will convert the index to a string using toString.

foo[1] == foo[(1).toString()] == foo['1'];

If you need a real hash mapping like Python or Java, you should find a library that provides one. Since JavaScript implementations don't all provide a mechanism for overloading item indexing (nor an equivalent property system like Python's or Ruby's) and no JavaScript implementation provides a default hash algorithm for objects, a good library will provide an associative array type that includes functions like getItem, setItem, hasItem, and delItem, and a general hash algorithm that returns good default hashes for every built-in type and uses polymorphic hash member functions.

Novice: cute type conversions

JavaScript provides valueOf and toString member functions for all objects. However, literal numbers, booleans, strings, undefined, and null are distinct from their object counterparts. For example, typeof new Number(1) == 'object' whereas typeof 1 == 'number'. However, there are some cute ways to implicitly and reliably convert any object to a literal number, literal boolean, or literal string.

You can convert any object to a number by adding it to zero.

0 + foo

You can convert any object to a string by adding it to a null string.

'' + foo

However, strings and numbers are special. If you add a string and a number, the resultant type depends on whether the string is empty. An empty string gets coerced to the number zero. Otherwise, the zero gets coerced to a string and the addition performs concatenation.

0 + '' == 0
'' + 0 == 0
1 + '' == 1
'' + 1 == 1
0 + 'a' == '0a'
'a' + 0 == 'a0'

You can convert any object to a boolean, true or false value by negating it twice.

!!foo

Novice: iteration

Coming from Java or C, JavaScript has a fun variation on the for loop. Coming from Perl, PHP, or Python, the familiar for loop has a subtle quirk. You can use a for loop to run a block repeatedly, once through for each member (items.member) and item (items['item']) of an Object. The iterand will be the index of the item, not the item itself.

for (var index in items) {
  var item = items[index];
  ...
}

You should use a traditional, C-style for-loop for iterating Array objects. Bearing in mind that, in JavaScript, members and items are equivalent, the for loop will iterate over both numeric indices and named members, with the exception of built-in members like toString or exec. Some libraries controversially add member functions to the Array prototype. Sometimes this causes the for (var i in array) to iterate on these member functions in addition to the numbers. This is the pattern you should use to avoid getting member functions when you iterate on an Array:

for (var i = 0; i < items.length; i++) {
  var item = items[i];
  ...
}

Of course, these libraries and new browsers also provide alternate iteration mechanisms that are arguably more elegant. With recent versions of Firefox and libraries like Prototype, you get forEach iteration, which involves an anonymous function.

items.forEach(function (item) {
  ...
});

Acolyte: anonymous functions

The redeeming grace of JavaScript is its provision of anonymous functions with full closures. In JavaScript, functions are objects. Programmers can pass functions as arguments. Furthermore, the function's scope includes all of the names in the scope in which it was declared. Coupled with the facts that functions are declared at run time and that local variables can stick around long after a function returns, closures provide a wealth of options for programmers. An exhaustive compendium of all the fun uses for closures would be far beyond the scope of this article, so here's a canonical example for you to ponder: a range generator.

var range = function (start, stop, step) {
  return function () {
    var at = start;
    start += step;
    if (at < stop) return at;
    else throw new Error();
  };
};
var next = range(0, 10, 2); /* range returns a function */

The first call of next returns 0. The following times you call next it returns 2, 4, 6, and 8. Thereafter, calling next will throw an Error. This is because the local variables of range, the one time you've called it, remain in memory as long as next exists. The anonymous function that range returns is a closure: both the code of the function and a reference to the scope in which the function was declared. The returned function object effectively holds the range "function allocation record" in memory. Thus, the value of start persists across calls to next.

Acolyte: enclosure

In JavaScript's top scope, variables you declare with var are members of the window. To get a bit more privacy with your variables, you should use a closure by declaring an anonymous function and immediately calling it. Since calling functions can have unfortunate side-effect of switching your context object to the global context, use call to send this into your closure.

(function () {
  ...
}).call(this);

Another handy use for enclosures is to make using with and var behave deterministically and intuitively. The with block pushes an Object onto the scope chain. That is, members of a given object become local variables in the given block.

with ({'a': 10}) {
  a == 10;
}

However, when you declare variables with var inside a with block, the variables should be declared in the function block scope and a variable by the same name should be initialized in the with block scope. However, all bets are off depending on what browser runs the script. Using an enclosure inside all with blocks will force var declarations to operate in the innermost scope.

with ({'a': 10}) {
function () {
  var a = 20;
}).call(this);
}

Acolyte: context object manipulation

JavaScript provides a context object, this. In the global context, this is the window. Also, by default, this is the window when you call a function.

var bar = function () {return this};
bar() == window;

There are two ways to send other objects as the context object into a function. The first is to call a function as a member of an object.

var foo = {'bar': bar};
foo.bar() == foo;

The second way to send an object to a function as its context object is explicit. You can use the Function.call method's first argument to send in an arbitrary object. call accepts the context object followed by any arguments you want to pass in normally.

bar.call(foo, 0, 1, 2, ...);

Acolyte: variadic arguments

Coming from C, variadic arguments are the va_arg and ... syntax trick left over from ANSI C that allow functions like printf to accept an arbitrary number of arguments. In Python, you get variadic arguments and keywords using * and ** in your declarations and invocations. In JavaScript, like Perl, functions implicitly accept any number of arguments. You can access these variadic argument lists using the arguments variable inside your functions.

Consider a trivial max function that returns the largest of two numbers.

this.max = function (a, b) {
  if (a > b) return a;
  else return b;
};

max can use the arguments to read any number of arguments.

this.max = function () {
  var items = arguments;
  var basis;
  for (var member in items) {
    var item = items[member];
    if (basis === undefined || item > basis) {
      basis = item;
    }
  };
  return basis;
};

If you want to pass variadic arguments to a function from an arbitrary number of items in an Array, you can use the Function.apply function. Like call, apply accepts the context object as its first argument, followed by a single Array containing whatever arguments to you want to pass.

max.apply(this, [10, 30, 20]) == 30;

Guru: binding

You've likely noticed that the context object a function receives depends on the invocation syntax. Binding a function to an object assures that you receive a particular object. Here's a sample bind function.

var bind = function (context, functor) {
  return function () {
    return functor.apply(context, arguments);
  };
};

Guru: lazy function definition

Peter Michaux recently documented and named the "Lazy Function Definition Pattern" wherein you determine the behavior of a function the first time the user calls it. This is handy for choosing among cross-platform code paths. Consider this function that gets the value of PI by calculating it once, then simply returning it after each successive call.

var getPi = function () {
  var pi = calculatePi();
  getPi = function () {
    return pi;
  }
  return pi;
};

Guru: polymorphic callable objects

Supposing you were writing a type system, you might want objects of all your types to have the option of being callable, and for the call behavior to be redefinable in sub-types. One fun way to do this is to always use Function instead of Object as your base type.

var callableType = function (constructor) {
  return function () {
    var callableInstance = function () {
      return callableInstance.callOverload.apply(callableInstance, arguments);
    };
    constructor.apply(callableInstance, arguments);
    return callableInstance;
  };
};

Conclusion

JavaScript is a simple, powerful language made complex by a lack of guarantees, subtle variations of its implementation, and a propensity for silently ignoring errors. However, for the time being, JavaScript is the only language we have for cross-browser web development. By subscribing to some simple patterns, we can avoid some unfortunate surprises and build power-tools using power-tools in our collective JavaScript programming adventure.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值