CHAPTER 1

image

JavaScript Is Not the Language You Think It Is

Sean Bennett, Course Architect, Udacity

JavaScript is a deceptively familiar language. Its syntax is close enough to C/C++ that you may be tricked into thinking it behaves similarly.

However, JavaScrit has a number of gotchas that trip up developers coming from other languages. In this chapter, I’ll go over some of the more egregious offenders, teach you how to avoid them, and showcase the hidden power in the language.

This chapter is for game programmers who are coming from other languages and who are using JavaScript for the first time. It’s the tool I wish I’d had when I first started using JavaScript.

Variables and Scoping Rules

You wouldn’t think that declaring variables would be at all hard or error prone. After all, it’s a fundamental part of any language, right? The problem is that with JavaScript, it’s

  • very easy to accidentally declare variables after they’re used, which leads to accidentally accessing undefined variables
  • deceptively difficult to restrict access to variables, leading to naming collisions as well as memory allocation issues

I’ll discuss the issues with and limitations of JavaScript scoping and then present a well-known solution for modularizing your JavaScript code.

Declaration Scoping

The first thing you need to realize about JavaScript is that there are only two different scopes: global and function level. JavaScript does not have any further lexical or block scoping.

A variable is declared on the global scope like so:

zombiesKilled = 10;

A variable is declared on the function scope as follows:

var antiPokemonSpray = true;

That’s not entirely true, actually. Using the var keyword attaches the variable to the nearest closing scope, so using it outside any function will declare the variable on the global scope as well.

Note that the lack of any block-level scoping can cause bugs that are pretty hard to track down. The simplest example of this is the use of loop counters; for instance,

for (var i = 0; i < 10; i++) {
    console.log(i);
}
console.log(i);

That last logging statement won’t output null or undefined, as you might expect. Because of the lack of block scope, i is still defined and accessible. This can cause problems if you don’t explicitly define the value of your loop counter variables on every reuse.

Global scope is something to be avoided in JavaScript. Not only do you have all the usual reasons, such as code modularity and namespacing issues, but also JavaScript is a garbage-collected language. Putting everything in global scope means that nothing ever gets garbage collected. Eventually, you’ll run out of memory, and the memory manager will constantly have to switch things in and out of memory, a situation known as memory thrashing.

Declaration Hoisting

Another concern with variable declarations is that they’re automatically hoisted to the top of the current scope. What do I mean by that? Check this out:

var myHealth = 100;
 
var decrementHealth = function() {
    console.log(myHealth);
    
    var myHealth = myHealth - 1;
};
 
decrementHealth();

So, you would think that this would

  • output 100
  • declare a new, function-scoped variable, myHealth, shadowing the globally scoped variable myHealth
  • set the function-scoped myHealth to 99

And, it would be totally reasonable to think that. Unfortunately, what you actually output is undefined. JavaScript will automatically lift the declaration of myHealth to the top of the function, but not the assignment.

After the JavaScript engine gets done with that, here is the code you’re actually left with:

var myHealth = 100;
 
var decrementHealth = function() {
    var myHealth;
    console.log(myHealth);
    
    myHealth = myHealth-1;
 
};

Suddenly, that undefined output makes sense. Be careful! Declare all your variables up front so that this scenario doesn’t catch you unawares, and make sure they have sane default values.

As a further illustration, let’s take a look at the following example:

var myHealth = 100;
 
var decrementHealth = function(health) {
    var myHealth = health;
    myHealth--;
    console.log(myHealth);
};
 
decrementHealth(myHealth);
console.log(myHealth);

This will output 99 first, then 100, because you’re setting myHealth to the value of health inside the function rather than setting by reference.

JavaScript Typing and Equality

Now that you understand the basics of variables, let’s talk about what types of values those variables can take.

JavaScript is a loosely typed language, with a few base types and automatic coercion between types (for more information, see the section “Type Coercion”).

Base Types

JavaScript only has a few basic types for you to keep in mind:

  1. Numbers
  2. Strings
  3. Booleans
  4. Objects
  5. Arrays
  6. null
  7. undefined

Numbers

Numbers are fairly self-explanatory. They can be any number, with or without a decimal point or described using scientific notation, such as 12e-4.

Most languages treat at least integers and floating-point numbers differently. JavaScript, however, treats all numbers as floating point.

It would take too long to go into the potential problems with floating-point numbers here. Suffice it to say that if you’re not careful, you can easily run into floating-point errors. If you want to learn more about the pitfalls of floating-point arithmetic, I’d recommend checking out the Institute of Electrical and Electronics Engineers (IEEE) spec IEEE 754: Standard for Binary Floating-Point Arithmetic.

The two additional values numbers can take on are Infinity and NaN. That’s right, NaN is a number. (for more information, see the section “Equality Checking”).

Strings

Strings are quite a bit simpler. As in most languages, you can treat a string like an array of characters. However, strings are also objects, with numerous built-in properties and methods, such as length and slice:

> "example string"[0]
"e"
> "example string".length
14
> "example string".slice(7)
" string"

I should point out that what I’m doing in the previous example is particularly bad. The memory behavior of where hard-coded strings are allocated isn’t part of the language specification. Being allocated on the global heap is actually one of the better scenarios. Depending on the browser, each individual use could be allocated separately on the heap, further bloating your memory.

Booleans

Booleans, as in most languages, can take on the values true and false. Both are reserved keywords in JavaScript. The main difference here between JavaScript and many other languages lies in which values can be coerced to either true or false (for further details, see the section “Truthiness,” later in this chapter).

Objects

Objects are the bread and butter of JavaScript, but they behave a bit differently from those in several other languages. In many ways, objects are similar to dictionaries in modern interpreted languages:

x = {};

Curly braces indicate that you’re defining an object. Nothing inside suggests that this is the empty object. You can assign key-value pairs to an object like so:

player = { health: 10 };

Pretty simple, really. You can assign multiple key-value pairs to an object by separating them with a comma:

player = {
    health: 10,
    position: {
        x: 325,
        y: 210
    }
};

Note in this example that you’re assigning another object to the position property of player. This is entirely legal in JavaScript and incredibly simple to do.

To access the object, you can use either dot or bracket notation:

> player.health
10
> player['position'].x
325
> player['position']['y']
210

Note that when using bracket notation, you need to enclose the key in quotes. If you don’t do this, then the key will instead be treated like a variable:

> x = "unknownProperty";
"unknownProperty"
> player['position'][x]
undefined

This code returns undefined, because the interpreter can’t find player['position']['unknownProperty']. As a side note, you should minimize use of bracket notation whenever possible. Dot notation uses fewer bytes to represent the same thing and can be more effectively minified over the wire (for more information, see the section “Inheritance the JavaScript Way”).

Arrays

Arrays act similarly to other languages you may be familiar with:

> x = [1, 10, 14, "15"]
[1, 10, 14, "15"]
> x[0]
1
> x[3]
"15"
> x.length
4
> x.push(20, "I'm last!")
Undefined
> x
[1, 10, 14, "15", 20, "I'm last!"]

As you can see, arrays are heterogenous, meaning that you can have arbitrary types in a single array. Note that this is a bad idea; the internal representation for heterogenous arrays causes some serious performance headaches. Always try to keep a single type in a given array.

Arrays have a number of convenience functions as well, such as push, pop, and slice. I’m not going to go into much detail on these here. To learn more about them, check out the coverage of the Array object by the Mozilla Developer Network (MDN) (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array).

I do, however, want to sound a note of caution regarding the memory performance of these convenience functions. Most, if not all, act by allocating an entirely new array from the heap rather than modifying things in place.

In general, garbage collection and memory management are going to be huge performance concerns in JavaScript, so you want to avoid allocating new arrays and causing object churn as much as possible.

Yet, there isn’t a good way to modify arrays in JavaScript without creating newly allocated objects on the heap. You can do some things to mitigate this, and, to that end, a great resource is Static MemoryJavaScript with Object Pools (www.html5rocks.com/en/tutorials/speed/static-mem-pools/). Unfortunately, it won’t completely solve your problems, but keeping these performance considerations in mind will go a long way toward mitigating your biggest memory performance issues.

null

The null type is a special value similar to None in Python. null signifies when a value has been emptied or specifically set to nothing. Note that this is distinct from the value that unknown variables are equal to or that declared but unassigned variables are set to. For that, we have undefined.

undefined

Variables are initially set to undefined when declared. Remember from declaration hoisting that declarations are automatically hoisted to the top of the function but that any accompanying assignments are not. This means that any variables will be set to undefined between where they’re declared at the top of a function and where they’re assigned to.

Let’s take a look at the difference between undefined and null:

var player = {
    health: 100,
    damage: 5,
    hit: function() {
        console.log('poke'),
    }
};
console.log(enemy);
 
var enemy = {
    health: 100,
    damage: 50,
    hit: function() {
        console.log('SMASH'),
    }
};
console.log(enemy.health);
console.log(player.shields);

This code will output as follows:

> undefined
 
> 100
> undefined

The typeof Operator

JavaScript has a handy operator, typeof, which can tell you, as you’d guess, the type of its operator. Let’s examine a few of these:

> typeof 2
"number"
> typeof 2.14
"number"
> typeof Infinity
"number"
> typeof NaN
"number"

So far so good; as you might expect, all of these return the string “number”.

> typeof ""
"string"
> typeof "coconuts"
"string"
> typeof '2.4'
"string"
> typeof true
"boolean"
> typeof false
"boolean"

Strings and booleans also behave as expected. The challenge comes when looking at objects, undefined, and null.

> typeof {}
"object"
> typeof { key: "value" }
"object"
> typeof undefined
"undefined"
> typeof null
"object"

The issue is that null is treated as an object rather than its own type. This can be a problem when using the typeof operator, so make sure only to use null in situations in which this isn’t a concern.

Note as well that typeof makes no distinction between different kinds of objects; it just tells you whether a value is an object. To distinguish between different types of objects, we have the instanceof operator.

The instanceof Operator

instanceof compares two objects and returns a boolean indicating whether the first inherits from the second. Let’s look at a few examples:

> String instanceof Object
true
> Object instanceof String
False
> var a = {};
> a instanceof Object
true
> a instanceof String
false

In JavaScript all objects inherit from the base Object, so the first result, comparing String, makes sense, as does the third, comparing a, the empty object. In contrast, neither Object nor a inherits from String, so this should return false (for details on how to structure inheritance and object-oriented (OO) code in JavaScript, see the section “Inheritance the JavaScript Way”).

Type Coercion

JavaScript is a dynamically typed language, with automatic type conversion, meaning that types are converted as needed, based on the operations being performed. Now, this type conversion is a little . . . misbehaved. Let’s take a look at the following example:

> x = "37" + 3
"373"
> x = 3 + "7"
"37"
> x = 10 - "3"
7

Wait, what?

JavaScript will automatically convert between numbers and strings, but the way it does so depends on the operators involved. Basically, if the + operator is involved, it will convert any numbers to strings and assume that + means “concatenate.”

However, any other operators will instead convert strings to numbers and assume that the operators involved are arithmetic.

What about an expression with more operators?

> x = "10" + 3 / 4 - 2
98.75

Can you tell what steps JavaScript took to get the result 98.75? Personally, it took me a few seconds to step through and figure it out.

In general, you should avoid automatic coercion between types, and instead be explicit. JavaScript has a couple of handy built-in functions to convert from strings to numbers, parseInt and parseFloat:

> parseInt("654", 10)
654
> parseInt("654.54", 10)
654
> parseInt("654", 8)
428
> parseFloat("654")
654
> parseFloat("654.54")
654.54

The first parameter for both functions is the string you want to convert to a number. Note that parseInt automatically truncates anything after the decimal point rather than rounding.

parseInt also takes an optional second parameter, which is the radix, or base of the number system being converted to. The default is the standard base-10 number system.

It’s worth pointing out the reverse process, converting a number to a string. The primary way to do this is to call String(value), where value is what you want converted to a string.

Equality Checking

One of the greatest challenges for new JavaScript developers is, without a doubt. equality checking. Thankfully, the key to avoiding getting tripped up can be summed up very easily:

Always use === and !== to do equality checking rather than == and !=.

But, why must you do that? What’s the deal with this === nonsense, and why are there two different equality operators?

The answer has to do with our friend automatic type coercion. == and != will automatically convert values to different types before comparing them for equality. The === and !== operators do not and will return false for different types.

However, what are the rules for how == converts types?

  • Comparing numbers and strings will always convert the strings to numbers.
  • null and undefined will always equal each other.
  • Comparing booleans to any other type will always cause the booleans to be converted to numbers.
  • Comparing numbers or strings to objects will always cause the numbers or strings to be converted to objects.
  • Any other type comparisons are automatically false.

Once the types have been converted, the comparison continues the same as with ===. Numbers and booleans are compared by value, and strings, by identical characters. null and undefined equal each other and nothing else, and objects must reference the same object.

Now that you know how == works, the earlier advice never to use it can be relaxed, at least a little bit. == can be useful, but you must be absolutely sure you know what you’re doing.

Truthiness

Using various types in conditional statements is similarly problematic. Because of type coercion, you can use any type in a conditional, and that type is converted to a boolean.

The rules for converting other types to booleans are actually relatively straightforward:

  • undefined and null are always false.
  • Booleans are just treated as booleans (obviously).
  • Numbers are false if they equal 0 or NaN; otherwise, they’re true.
  • Strings are true, except for the empty string "", which is false.
  • Objects are always true.

The biggest thing to watch out for is that, whereas the empty string is false, the empty object is true. Be aware of this when using objects in comparisons, and you’ll have solved 90 percent of your problems with truthiness.

Inheritance the JavaScript Way

If you’re coming from traditional game development, you’re probably very familiar with object-oriented programming (OOP) and specifically, class-based OOP, the model that C++ and Java use.

JavaScript uses a different OOP model, prototypical inheritance, which is derived from self’s object model.

I’ll close out this chapter by discussing what prototypical inheritance is and how to use it instead of the more classical inheritance you may be used to.

Prototypical Inheritance

Prototypical inheritance, at its core, is concerned with only two things:

  1. How do you create a new object?
  2. How do you extend a new object from an existing one?

Creating a bare new object is simple, using object literal notation. Let’s say you wanted to create the following ship:

var myAwesomeShip = {
    health: 100,
    shields: 50,
    guns: [{
        damage: 20,
        speed: 5
    },{
        damage: 5,
        speed: 9000
    }],
    fire: function() {
        console.log('PEW PEW'),
    }
};

Simple enough. But, what if you wanted to create a new ship, using myAwesomeShip as a template, but with better shields? Obviously, you could just copy and paste things, but that’s no good. Instead, you can create a clone of myAwesomeShip, using prototypical inheritance, like so:

var myMoreAwesomeShip = Object.create(myAwesomeShip);
myMoreAwesomeShip.shields = 100;

And, you’re done. Now, if you wanted, you could roll this into a ship template object, as follows:

var ship = {
    manufacture: function(shields) {
        var newShip = Object.create(this);
        newShip.shields = shields;
        return newShip;
    },
    health: 100,
    shields: 50,
    guns: [{
        damage: 20,
        speed: 5
    },{
        damage: 5,
        speed: 9000
    }],
    fire: function() {
        console.log(PEW PEW'),
    }
};
 
var myWayMoreAwesomeShip = ship.manufacture(150);

Voilà: you have a ship template that you can build off of, using any given ship as the template.

Of course, there is still one very important question that must be answered: Can you somehow combine these steps in order to extend the base ship with arbitrary properties?

It turns out that you can do this by writing an extend function and attaching it to all objects. The code for this is short, but dense:

Object.prototype.extend = function(extendPrototype) {
    var hasOwnProperty = Object.hasOwnProperty;
    var object = Object.create(this);
    
    for (var property in extendPrototype) {
        if(hasOwnProperty.call(extendPrototype, property) || typeof object[property] === 'undefined') {
            object[property] = extendPrototype[property];
        }
    }
    return object;
};

Whew! There’s a lot going on there. Here’s what’s happening:

  1. You create a function, extend, attached to the base Object.
  2. The function hasOwnProperty checks whether the object has the passed-in property or whether it’s inherited from somewhere else, for example, the base Object.
  3. You create a clone of this; in the previous example, this would be ship.
  4. Now, you loop through all the properties in the extension; for each property, you perform these tasks:
    1. You check whether the base Object does not have the given property or whether the extension has the property directly.
    2. You then you assign the value from the extension to the cloned object.
  5. Once you’re done, you return the completed object.

Reread that a few times if you need to; it’s a lot to digest. Now that you know the steps, however, you can create a new ship template, with any additional property changes you want, as shown:

var newShipModel = ship.extend({
    health: 200,
    shields: 100,
    fire: function() {
        console.log('TRIPLE PEW!!!'),
    }
});

You can think of this as a new model of ship that you’re going to have your shipyards build:

var oldShip = ship.manufacture(100);
var newShip = newShipModel.manufacture(150);

this

You may be a little confused by the use of the this keyword in the prior extend function. this behaves somewhat differently in JavaScript than it does in many other languages, primarily because JavaScript is not a class-based language.

this can behave differently, depending on where it’s called, and can even change from function call to function call in the same function.

Let’s walk through the different values this can take on:

  • If you call this globally, then it refers to the global object, which is the window object, if running inside a browser.
  • If you call this inside a function that is not attached to an object, this refers to the global object, because, by default, functions are attached to the global object.
  • If you call this inside a function that is attached to an object, such as in the fire method of ship, then it refers to the object the function is attached to (in this case, ship).
  • If you call this inside a constructor function, then a new object is created when you call it with new, and this refers to that object.
  • If you call this in a function that is then called from an event handler, then this refers either to the document object model (DOM) element in the page that triggered the event or to the global object, if there is no such DOM element.

Note that this last value is where you’re most likely to run into trouble, because the behavior concerning event handlers specifically overrides the behavior you would otherwise expect.

Fortunately, you can get around some of this behavior with the call and apply functions. These are attached to every function object and are used to explicitly set what this refers to. For instance, if you called myAwesomeShip.fire.call(myMoreAwesomeShip), then this would refer to myMoreAwesomeShip.

In general, it’s useful to explicitly declare what you expect this to be whenever you call a function that uses it.

image Note  Often, developers coming from a class-based OOP language rail against the lack of proper OOP in JavaScript. The truth is that JavaScript has a very flexible OOP model that can be used in much the same way as a more classical language if required. If you don’t need it, then the flexibility of JavaScript’s prototypical inheritance can actually be a huge boon, making it far simpler to build up the complex inheritances necessary for a game.

Conclusion

This has been a whirlwind tour of JavaScript, detailing all the pieces you need to start building a basic game architecture and pointing out some of the pitfalls along the way.

JavaScript gets a bad rap for some of its quirks and idiosyncrasies. I’ve detailed a few of the more nuanced issues here, and awareness of these should keep you from making some of the more painful mistakes I made when first starting out.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.188.121