-This document describes what you should put in the DOM (display) and what you shouldn't (data). It is adapted from an e-mail to javascript@lists.
-
-=The DOM is Not a Datastore. Don't Store Application State in the DOM.=
-
-I don't know why this is so compelling, but it happens pretty often and everyone who is doing it really needs to stop doing it. What I mean by "application state" is that the DOM elements representing the page you're producing should be a rendered view of some state which you're storing internally in a Javascript object or datastructure. They should NOT themselves be the datastructure. Suppose you have a WidgetCounter, which counts widgets. Here's a reasonable implementation:
-
-<code> function /* class */ WidgetCounter(display_field) {
-</code>Even ignoring internationalization concerns, I hope this example is so egregiously bad that it speaks for itself. I don't think anyone would actually implement this, but we get many more subtle flavors of it. For example, the photo tagging code limits the number of tags to 30; it does this by counting the number of childNodes:
-
-<code> COUNTEREXAMPLE
- if (tagsDiv.childNodes.length < 30 && ge(tagsID+'_error')) {
- $(tagsID+'_error').style.display = 'none';
- }
-
-</code>This practice is pervasive. A recent bug (circa July 2008) came down to a system storing its state not only in the DOM but in the "className" field. Someone changed some CSS on another page, which necessitated a followup CSS fix to deal with long field names, which cascaded into a minor CSS fix, which broke a portion of the underlying system. If a third-degree cascade of CSS-only changes can break your feature -- not by changing the display, but by changing actual execution paths -- you're doing it wrong.
-
-Two tangents here:
-
-First, it's also bad to rely on DOM context, like this (paraphrased but not really exaggerated) line that used to exist somewhere in poke:
-</code>"If there are no pokes left, hide the whole poke panel." (Someone removed a couple divs and this started hiding the BODY tag.)
-
-You should generally acquire references to nodes either by using $() or by using DOM.scry() from some parent container you've used $() on. The advantage of using DOM.scry() over DOM-walking chains is that the nodes can be moved around in a lot of ways without breaking you code. You should generally avoid using and particularly avoid chaining parentNode, childNodes, firstChild, nextSibling, etc. This isn't completely hard-and-fast, but almost all cases of "element.childNodes[3].firstChild" are bad and all of them are fragile.
-
-Second, it's way way less bad to use non-DOM properties of DOM objects to hold appropriate state. For instance, code like this (related to the earlier bug) is inherently fragile:
-
-<code> COUNTEREXAMPLE
- if (container.childNodes[i].className == 'friends_'+entity)
-
-</code>It relies on unchanging DOM relationships, it relies on nothing and no one ever touching className, and it constructs a classname programmatically which makes it more difficult to debug when it breaks or find when it you're changing things. A far less bad implementation might use :DOMStorage:
-
-<code> if (DOMStorage.getData(container.childNodes[i], 'privacyType') == entity)
-
-</code>This is still kind of nasty and I'd argue that a better design would see the code iterating over objects instead of over DOM nodes, using getters, and calling rendering methods to make changes reflect in the DOM. But, at the least, this is much less fragile and usually a practical alternative which doesn't require all that time-consuming "software engineering" associated with using classes and objects.
-This article describes how scope resolution works in Javascript. <i>Note that Firebug creates a fake global scope, so if you try some of these examples in Firebug they may behave differently than if you put them in a .js file.</i>
-
-=<tt>window</tt>=
-
-Javascript's global scope is a variable named window. At global scope, these statements are all equivalent:
-
-<code> u = 1;
- var u = 1;
- window.u = 1;
-
-</code>Everything you define at global scope exists at global scope, so you can iterate over window to get a list of all the functions and classes, plus a whole pile of browser builtins. For instance, to test if a function exists at global scope:
-
-<code> if (window['function']) { ... }
-
-</code>
-=Scope=
-
-There are two ways to create a new scope: <tt>function</tt> and <tt>with</tt>. Never use <tt>with</tt> -- it's a cute idea, but some of the browser implementations suck and it's more trouble than it's worth (see [[Articles/Javascript Design Flaws]]). Other statements do not introduce scope in Javascript.
-
-So there's one way to create a new scope that you are actually allowed to use: creating a function. A function definition creates a new scope:
-
-<code> function f(param) {
- var local;
- param = 'param';
- local = 'local';
- global = 'global';
- }
-
-</code>Local variables and parameter variables have resolution precedence over global variables, so <tt>param</tt> is resolved to the parameter and <tt>local</tt> is resolved to the function-local. However, <tt>global</tt> is not declared anywhere in this scope, so it will resolve to the global <tt>global</tt>.
-
-Equivalently, you could write <tt>window.global</tt>. In general, you can force global resolution with <tt>window</tt>.
-
-Two notes: first, <b>don't ever give a local and a parameter the same name</b>. You will be decidedly unhappy about the result. Second, the <tt>var</tt> keyword applies regardless of where in the function scope it appears, so this block also creates a local called <tt>local</tt>, even though the line <tt>var local;</tt> will never be executed. That is, this function will always return <tt>undefined</tt> regardless of the value of <tt>window.local</tt>.
-
-<code> function f() {
- return local;
- if (false) {
- var local;
- }
- }
-
-</code>It may be helpful to think of this as: one pass to find all the <tt>var</tt> keywords, then a second pass to actually execute the code.
-
-=Advanced Scope=
-
-Of course, you can define functions inside functions. In this case, identifiers which are not locals or parameters will be resolved at the parent scope, and so on until <tt>window</tt> is reached.
-
-<code> function f() {
- var l = 35;
- function g() {
- return l;
- }
- g(); // 35
- }
-
-</code>Here, <tt>l</tt> is resolved at <tt>f</tt>'s scope. Note that <tt>g</tt> is a ``closure'' -- it encloses a variable from its containing scope. This reference does not dissolve when the scope dissolves:
-
-<code> function buildBeancounterFunctions() {
- var bean_count = 0;
- return {
- inc : function() { bean_count++; },
- get : function() { return bean_count; }
- };
- }
-
- var b = buildBeancounterFunctions();
- b.inc();
- b.inc();
- b.get(); // 2
-
-</code>This works as expected; the reference to the <tt>bean_count</tt> local is preserved by the closures even though the scope created by <tt>buildBeancounterFunctions</tt> has dissolved. If you call <tt>buildBeancounterFunctions</tt> again, you'll get a different set of functions enclosing a different variable. (But don't build classes like this, see [[Articles/Object-Oriented Javascript]] instead.)
-
-=Really Advanced Scope=
-
-One caveat is that this goes the other way, too:
-
-<code> function assignButtonHandlers(buttons) {
- for (var ii = 0; ii < buttons.length; ii++) {
- buttons[ii].onclick = function() {
- alert(ii);
- }
- }
- }
-
-</code>Suppose you pass in an array of three buttons. Since all of the closures are referencing the same variable, the buttons will not alert 0, 1, and 2, but 3, 3, and 3.
-
-The solution to this is to reference different variables; to do this, you need to introduce more scope.
-
-<code> function assignButtonHandlers(buttons) {
- for (var ii = 0; ii < buttons.length; ii++) {
- buttons[ii].onclick = (function(n) {
- return function() {
- alert(n);
- })(ii);
- }
- }
- }
-
-</code>This creates and calls a function which takes the current value of the iterator and returns a function which encloses that value. This is difficult to understand. But generally you can just learn how bind() works and use that instead, far more simply:
-When you create a function scope, two <i>magic</i> variables are automatically injected: <tt>this</tt> and <tt>arguments</tt>. <tt>arguments</tt> is an Array-like object with the function's arguments (so you can write functions which take a variable number of arguments, like sprintf()). <tt>arguments</tt> also has some other properties, like <tt>callee</tt>, which are occasionally useful. Look it up on the internet if you really care so very much.
-
-<code> function howManyArguments() {
- return arguments.length;
- }
-
- howManyArguments(1, 2, 3, 4, 5); // 5
-
-</code><tt>this</tt> is a variable that contains the function's calling context. What this means is that if a function is invoked as a property of an object, <tt>this</tt> will be the object which it is a property of. Basically, what you would expect from other object-oriented languages.
-
-<code> o.f(); // inside the scope of f(), `this' is `o'
- o['f'](); // inside the scope of f(), `this' is `o'
- a.b.c.f(); // inside the scope of f(), `this' is `c'
-
-</code><b>But! If a function has no calling context, <tt>this</tt> will be defined but set to <tt>window</tt>.</b> So, <tt>this</tt> <b>always exists</b> inside a function scope, but sometimes it is <tt>window</tt> which is 100% Bad News Bears and almost certainly not what you want.
-
-<code> f(); // `this' is window! That is terrible!
-
-</code>This is particularly tricky because it is the <b>immediate calling context</b> that becomes <tt>this</tt>.
-
-<code> o.f(); // inside the scope of f(), `this' is `o', but...
- var g = o.f;
- g(); // ...now it's window, sucker.
-
-</code>Fortunately, you can inject a specific calling context using Function.call() or Function.apply().
-
-=<tt>call</tt> and <tt>apply</tt>=
-
-Function.call() takes one or more arguments; the first is the object to inject as the function's calling context and the rest (if any) are arguments to pass.
-
-<code> function add(n) { return this + n; }
- add.call(3, 5); // 8
-
-</code>Function.apply() works the same way, but it takes two arguments: the object to inject as <tt>this</tt> and an array of arguments to pass. So these are equivalent:
-
-<code> add.call(3, 5);
- add.apply(3, [5]);
-
-</code>But, there's generally an easier way than <tt>call</tt> or <tt>apply</tt>: bind() (which you should probably read next).
-<b>Use Exceptions</b>: Error conditions in our PHP stack should be communicated through exceptions, which provide a simple, robust mechanism for handling errors.
-
-In some cases, extenuating circumstances (like legacy code which is difficult to untangle or needs to be phased in) may prevent immediate enactment, but exception-based error handling should be strongly preferred wherever reasonable, and particularly in new features.
-
-= Overview =
-
-Traditionally error handling, including much of the error handling in our codebase, revolves around returning error codes. Return codes are straightforward, but they need to be manually returned, and then manually passed up the stack. They need to be handled at every level. If they aren't, they silently vanish.
-
-The behavior of error codes is pathological: they resist doing the right thing. Unless they are handled at every level they will leap headlong into the void, never to be heard from again. Exceptions are a more powerful error handling mechanism than error codes. They have the right default behaviors: they can effect function return, and they make their way up the stack unaided by default.
-
-As a top-level caller using an API that throws exceptions, you <i>must</i> handle error conditions, because ignoring them means program termination. This relationship between callers (which are in a position to handle errors) and lower level APIs (which are not), which is explored in greater detail below, is both correct and desirable. Only callers can be responsible for error handling, and exceptions ensure they are.
-
-Using exceptions will make your life easier and your code better, because:
-
-* exceptions <b>simplify error handling</b> because they can effect function return and unwind the stack without coddling
-* exceptions <b>make your code robust</b> by preventing errors from silently vanishing
-* exceptions carry <b>more information</b> about error conditions, so you can better react to them
-* exceptions <b>make error conditions explicit</b> by providing a single, unambiguous way to communicate errors
-* exceptions <b>simplify APIs</b> by allowing you to use return values for returning data only
-
-However, handling error conditions with exceptions instead of error codes requires you to change your approach somewhat in order to reap the benefits:
-
-* you should catch exceptions <b>at the highest level</b> that can deal with them, often the top level
-* you should use exceptions to indicate <b>exceptional conditions</b>, not to affect control flow
-* you should not catch the base class <b>Exception</b> (except in very special circumstances)
-
-The remainder of this document explains these points in greater detail.
-
-=Simplified Error Handling=
-
-Exceptions simplify error handling by reducing the number of checks you need to make against return codes. Generally, exceptions allow you to omit many checks against return values and all the boilerplate code responsible for pushing error codes up the stack. Exceptions are explicitly able to effect function return and make their way up the stack without help.
-
-=Robustness=
-
-Exceptions make your code more robust because they prevent error codes from being silently dropped. With traditional error codes, any caller can forget to check them. To counteract this, we've developed the "debug_rlog()-and-return" idiom:
-
-<code> COUNTEREXAMPLE
- $ok = some_function();
- if (!$ok) {
- debug_rlog('Something didn't work!');
- // Now I'm covered if my caller is lazy! Also, if I'm the caller, I don't
- // have to handle this error because it has already been "handled"! Great!
- return false;
- }
-
-</code>This idiom arose as a compromise between two concerns: you can't handle errors properly from deep in the stack, but you need to make sure they get handled. This idiom mitigates both but solves neither: callers feel okay about ignoring errors because they've already been ``handled'' through logging, and it's easy to ignore them because there are no real consequences.
-
-The right way to resolve this is to <b>throw an exception</b>.
-
-Throwing sends an exception scurrying toward top level where it can be dispatched properly, and forces it to be handled or the program will terminate. This means no log spew and a guarantee that callers aren't ignoring error conditions.
-
-A concern sometimes raised about exceptions is that an uncaught exception that escapes the stack causes program termination. But, this is an extremely desirable behavior. Stated another way, exceptions mean that when your program is wrong it stops. Error codes mean that when your program is wrong, it keeps going, it just does the wrong thing. Doing the wrong thing is a <b>much</b> worse behavior.
-
-Unexpected program termination is not particularly bad. It's obvious, it is less likely to make it through testing in the first place, and even if it does it will show up in the logs and get fixed quickly. Most importantly, its badness is bounded: even in the worst case, it can't do more than bring the site down for a few minutes. This is already a condition we have to deal with even without exceptions, because program termination can be caused in a number of other ways (like calling a function which is not defined).
-
-Conversely, silent failure is very difficult to detect so it is more likely to make it to production in the first place. It may not show up in the logs once it's live. Most importantly, continuing execution when the program is wrong is unboundedly bad. Data corruption and privacy violations are much, much worse than bringing the site down. They can take weeks to clean up, or be impossible to completely revert. It takes only minutes to revert a bad push that caused program termination via uncaught exception.
-
-Although the risk of these severe outcomes is small, the risk of taking the site down is also small and these severe outcomes are much worse than the worst possible case of uncaught exceptions. Using exceptions and coding defensively is essentially insurance: you are accepting an increased risk of program termination and other well-defined failures that are easy to deal with because you want to lower the risk of nebulous failures that are difficult or impossible to deal with. This is a highly desirable tradeoff which you should be eager to make.
-
-We already have a number of libraries that communicate error conditions through exceptions or react gracefully in the presence of exceptions: for examples, see :AsyncResponse, :CodedException, :queryfx() ([[Articles/Using queryfx()]]), :Filesystem, Hypershell ([[Articles/Hypershell Architecture]]), Hyperpush ([[Articles/Hyperpush Architecture]]), Sitevars ([[Articles/Sitevar Architecture]]), and :execx() ([[Articles/System Commands]]). Far from being harbingers of uncaught-exception doom, these systems have resulted in simplified code and increased reliability.
-
-=Catch At Top Level=
-
-When you catch exceptions, you should generally catch them at the highest level where you can correctly handle the error. Often, this is at or very near the top level. It is usually wrong to convert an :Exception to a return code:
-
-<code> COUNTEREXAMPLE
- try {
- $this->flip();
- } catch (PancakeException $ex) {
- return false;
- }
-
-</code>Converting an exception to a return code throws away much of the power of exceptions (e.g., automatic navigation up the stack, guaranteed handling, additional information). However, you may sometimes need to do this as an interim step in the process of converting a legacy API into an exception-oriented API.
-
-It is almost certainly wrong to convert an Exception into a debug_rlog():
-
-<code> COUNTEREXAMPLE
- try {
- $this->flip();
- } catch (PancakeException $ex) {
- debug_rlog('Couldn't flip pancake.');
- }
-
-</code>This is basically equivalent to:
-
-<code> COUNTEREXAMPLE
- $err = pancake_flip();
- if ($err) {
- debug_rlog('Hurf durf I am absolving myself of responsibility.');
- $err = false; // No more error! Magic!
- }
-
-</code>Instead, you should catch and handle exceptions at the top level, where they can be meaningfully acted upon.
-
-<code> try {
- $pancake = new Pancake();
- $pancake->cook();
-
- $breakfast = $pancake; // Yum!
- } catch (PancakeException $ex) {
- $breakfast = $cereal; // Ick. :(
- }
-
-</code>
-=Don't Catch "Exception"=
-
-You should usually avoid catching :Exception unless you are implementing a very general, top-level exception handling mechanism like the one in :AsyncResponse. Instead, catch a specific exception or exception subtree, like :CodedException, :QueryException, :CommandException, or :FilesystemException.
-
-A corollary to this is that you should avoid throwing :Exception unless you do not expect any caller to handle the exception. Essentially, throwing Exception is guaranteeing program termination (albeit via a graceful stack unwind and sensible top-level behavior rather than abrupt exit). The major use for this is checking invariants to detect that an API is being misused so you can communicate to the caller that they have strictly and unambiguously abused your interface.
-
-Because PHP has no <tt>finally</tt> clause, it is acceptable to catch :Exception if you are cleaning up resources and then re-throwing, although most kinds of resources that need cleanup (like database transactions and temporary files) already have exception-aware APIs that will handle this for you.
-This document describes how to use queryfx(), an extended form of queryf().
-
-= What queryfx() Does =
-
-queryfx() stands for something like "query, formatted + extensions", (or "exceptions", depending on who you ask) and is a method for easily and correctly executing queries against a MySQL database. queryfx() is similar to queryf(), but provides more conversions and better error handling.
-
-The API is similar to the sprintf() family of functions.
-</code>queryfx() will properly escape parameters so they are safe in SQL and protect you from SQL injection holes.
-
-= queryfx_one() and queryfx_all() =
-
-Many queries either expect to select exactly one row, or want to select all result rows as a list of dictionaries. In these cases, you may use queryfx_one() or queryfx_all(), respectively.
-
-queryfx_one() will return either null if zero rows match, or a dictionary if exactly one row matches. If more than one row matches, a <tt>QueryCountException</tt> will be thrown.
-
-<code> $pie = queryfx_one($conn_r, 'SELECT * FROM pie WHERE id = %d', $id);
- if ($pie) {
- echo "The pie's flavor is {$pie['flavor']}.";
- } else {
- echo 'No such pie exists. This is sad times.';
- }
-
-</code>queryfx_all() will always return a list of dictionaries, although it may be empty.
-
-<code> $list = queryfx_all(
- $conn_r,
- 'SELECT * FROM pie WHERE baked > %d',
- time() - (60 * 60));
-
- if (count($list)) {
- echo 'Pies baked in the last hour: '.count($list);
- } else {
- echo 'No pies were baked in the last hour. This is sad times indeed.';
- }
-
-</code>These convenience wrappers don't cover every case, but can simplify your code in many cases.
-
-= Supported Conversions =
-
-queryfx() supports three simple conversions, <b>%d</b> (integer), <b>%s</b> (string), and <b>%f</b> (float). These work exactly like sprintf(), except that they will be properly escaped for a MySQL context.
-
-<code> $res = queryfx(
- $conn_w,
- 'INSERT INTO pie (flavor, size) values (%s, %d)',
- $pie_flavor,
- $pie_size);
-
-</code>Note that <b>%s</b> is binary-safe, so it is safe to convert UTF-8 strings or raw byte sequences using <b>%s</b>.
-
-In addition to these simple conversions, a wide array of additional conversions is supported.
-
-<b>Nullable conversions</b> work like the simple conversions but handle <tt>NULL</tt> properly. The nullable conversions are <b>%nd</b>, <b>%ns</b>, and <b>%ns</b>. These conversions behave exactly like the corresponding normal conversions, except when the value they are passed is a strict null. In this case, they will print <tt>NULL</tt>.
-
-<code> // INSERT INTO t (u, v) VALUES (3, NULL)
- queryfx($conn_w, 'INSERT INTO t (u, v) VALUES (%nd, %nd)', 3, null);
-
-</code><b>Nullable test conversions</b> work like the simple conversions but handle equivalence with NULL properly by printing either an <tt>=</tt> or an <tt>IS NULL</tt> clause. The nullable test conversions are <b>%=d</b>, <b>%=s</b>, and <b>%=f</b>.
-
-<code> // SELECT * FROM t WHERE u = 3 AND v IS NULL
- queryfx($conn_r, 'SELECT * FROM t WHERE u %=d AND v %=d, 3, null);
-
-</code><b>List conversions</b> accept a list of values and produce a comma-separated list, appropriate for insertion into an <tt>IN</tt> clause. The list conversions are <b>%Ld</b>, <b>%Ls</b>, and <b>%Lf</b>. Note: these conversions treat their arguments as nullable, so <tt>null</tt> will be converted to NULL in the query, not 0 or empty string.
-
-<code> // SELECT * FROM t WHERE u IN ('a', 'b', 'c')
- queryfx($conn_r, 'SELECT * FROM t WHERE u IN (%Ls)', array('a', 'b', 'c'));
-
-</code><b>Identifier conversions</b> escape SQL identifiers like table or column names. The identifier conversions are <b>%T</b> (table), <b>%C</b> (column) and <b>%LC</b> (list of columns).
-
-<code> // SELECT `select` FROM `from` WHERE `where` = 4
- queryfx(
- $conn_r,
- 'SELECT %C FROM %T WHERE %C = %d',
- 'select', 'from', 'where', 4);
-
-</code><b>Dictionary conversions</b> escape a dictionary of key-value pairs into column-value pairs. The dictionary conversions are <b>%U</b> (update clause), <b>%LA</b> (list of predicates joined by AND), and <b>%LO</b> (list of predicates joined by OR). <b>%LA</b> and <b>%LO</b> also support array values for generating "IN" clauses.
-
-<code> // UPDATE t SET a = 1, b = 2, c = 3 WHERE u = 5 AND d = 6 AND e IN (1, 2, 3)
-</code><b>Like conversions</b> escape a string for a LIKE (or NOT LIKE) clause. The like conversions are <b>%~</b> (substring match), <b>%></b> (prefix match), and <b>%<</b> (suffix match).
-
-<code> // SELECT * FROM t WHERE u LIKE '%example%' OR v LIKE 'prefix%'
- queryfx(
- $conn_w,
- 'SELECT * FROM t WHERE u LIKE %~ OR v LIKE %>',
- 'example',
- 'prefix');
-
-</code><b>Miscellaneous conversions</b> escape other random junk. The miscellaneous conversions are <b>%K</b> (comment) and <b>%Q</b> (raw subquery). You must be <b>extremely careful</b> with <b>%Q</b> -- unlike other conversions, it does <i>no</i> escaping. If you do not use it properly, you will open a SQL injection hole.
-
-<code> // UPDATE /* hey guys what is up */ t SET u = "v"
- queryfx(
- $conn_w,
- 'UPDATE %K t SET %Q',
- 'hey guys what is up', 'u = "v"');
-
-</code>Be careful with <b>%Q</b> because it's extremely dangerous. It should be rarely (if ever) used. Often, vqueryfx() is a better approach.
-
-= Handling Exceptions =
-
-queryfx() throws exceptions on failure, which means that <b>you need to catch and handle them</b>. All exceptions descend from <tt>QueryException</tt>. Generally, you should catch <tt>QueryExceptions</tt> at or near the top level -- that is, catching exceptions high in the callstack is often better than catching them low in the callstack.
-
-<code> try {
- $pies = pies_get_for_user($uid);
- } catch (QueryException $ex) {
- $errmsg = 'Pies are not available at this time, try again later.';
- }
-
-</code>You should <b>not</b> catch exceptions in your database/generator function. You can't do anything useful with them here.
-
-<code> COUNTEREXAMPLE
- try {
- $ret = queryfx($conn_r, 'SELECT * FROM pie WHERE owner = %d', $uid);
- } catch (QueryException $ex) {
- return null;
- }
-
-</code>This means that writing generators is much easier:
-
-<code> function pies_for_users_dbget($user_ids) {
-</code>This is a complete, correct database/generator function under queryfx(). Notably, you do not need to test either the connection or the return for `null', because exceptions will be thrown in either case.
-
-Note that the cache_get_scb() layer will properly catch and handle exceptions thrown by queryfx().
- You issued an insert or update statement which would have caused a duplicate
- key (on the primary key or some unique key) collision. Attempting the insert
- and catching this exception is often the correct way to ensure uniqueness.
-
-</code>In most cases, it is sufficient to catch <tt>QueryException</tt> and consider it an unrecoverable error at the top level. However, you may find the fine-grained exceptions useful when building abstractions, debugging, or under unusual use cases.
-
-One caveat is that memcache_dispatch() is exception-aware but can not currently expose exceptions at the top level. Instead, it will convert <tt>QueryExceptions</tt> into an implicity null return value from your database/generator function. This may be fixed in the future but requires improving some abstractions.