打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
Frequently Misunderstood JavaScript Concepts

Frequently Misunderstood JavaScript Concepts

by , October 28, 2013

This is a complete "reprint" of Appendix B from my book,Closure: The Definitive Guide.Even though my book was designed to focus on Closure rather than JavaScript in general,there were a number of painpoints in the language that I did not think were covered well in other popular JavaScript books,such as JavaScript: The Good Parts orJavaScript: The Definitive Guide.So that the book did not lose its focus, I relegated this and my essay, "Inheritance Patterns in JavaScript,"to the back of the book among the appendices.However, many people have told me anecdotally that Appendix Bwas the most valuable part of the book for them, so it seemed like this was worth sharingmore broadly in hopes that it helps demystify a language that I have enjoyed so much.

This book is not designed to teach you JavaScript, but it does recognize thatyou are likely to have taught yourself JavaScript and that there are some keyconcepts that you may have missed along the way. This section is particularlyimportant if your primary language is Java as the syntactic similaritiesbetween Java and JavaScript belie the differences in their respectivedesigns.

JavaScript Objects are Associative Arrays whose Keys are Always Strings

Every object in JavaScript is an associative array whose keys are strings. Thisis an important difference from other programming languages, such as Java,where a type such as java.util.Map is an associative array whosekeys can be of any type. When an object other than a string is used as a key inJavaScript, no error occurs: JavaScript silently converts it to a string anduses that value as the key instead. This can have surprisingresults:

var foo = new Object();
var bar = new Object();
var map = new Object();

map[foo] = "foo";
map[bar] = "bar";

// Alerts "bar", not "foo".
alert(map[foo]);

In the above example, map does not map foo to"foo" and bar to"bar". When foo and bar areused as keys for map, they are converted into strings using theirrespective toString() methods. This results in mapping thetoString() of foo to "foo" andthe toString() of bar to"bar". Because both foo.toString() andbar.toString() are "[object Object]", theabove is equivalentto:

var map = new Object();
map["[object Object]"] = "foo";
map["[object Object]"] = "bar";

alert(map["[object Object]"]);

Therefore, map[bar] = "bar" replaces the mapping ofmap[foo] = "foo" on the previousline.

There are Several Ways to Look Up a Value in an Object

There are several ways to look up a value in an object, so if you learnedJavaScript by copy-and-pasting code from other web sites, it may not be clearthat the following code snippets areequivalent:

// (1) Look up value by name:
map.meaning_of_life;

// (2) Look up value by passing the key as a string:
map["meaning_of_life"];

// (3) Look up value by passing an object whose toString() method returns a
// string equivalent to the key:
var machine = new Object();
machine.toString = function() { return "meaning_of_life"; };
map[machine];

Note that the first approach, "Look up value by name," can only beused when the name is a valid JavaScript identifier. Consider the example fromthe previous section where the key was "[objectObject]":

alert(map.[object Object]); // throws a syntax error

This may lead you to believe that it is safer to always look up a value bypassing a key as a string rather than by name. In Closure, this turns out notto be the case because of how variable renaming works in the Compiler. Thiswill be explained in more detail in Chapter 13.

Single-quoted Strings and Double-quoted Strings are Equivalent

In some programming languages, such as Perl and PHP, double-quoted strings andsingle-quoted strings are interpreted differently. In JavaScript, both types ofstrings are interpreted in the same way; however, the convention in the ClosureLibrary is to use single-quoted strings. (By comparison, Closure Templatesmandate the use of single-quoted strings.) The consistent use of quotes makesit easier to perform searches over the codebase, but they make no difference tothe JavaScript interpreter or the ClosureCompiler.

The one caveat is that the JSON specification requires that strings bedouble-quoted, so data that is passed to a strict JSON parser (rather than theJavaScript eval() method) must use double-quotedstrings.

There are Several Ways to Define an Object Literal

In JavaScript, the following statements are equivalent methods for creating anew, emptyobject:

// This syntax is equivalent to the syntax used in Java (and other C-style
// languages) for creating a new object.
var obj1 = new Object();

// Parentheses are technically optional if no arguments are passed to a
// function used with the 'new' operator, though this is generally avoided.
var obj2 = new Object;

// This syntax is the most succinct and is used exclusively in Closure.
var obj3 = {};

The third syntax is called an "object literal" because the propertiesof the object can be declared when the object iscreated:

// obj4 is a new object with three properties.
var obj4 = {
  'one': 'uno',
  'two': 'dos',
  'three': 'tres'
};

// Alternatively, each property could be added in its own statement:
var obj5 = {};
obj5['one'] = 'uno';
obj5['two'] = 'dos';
obj5['three'] = 'tres';

// Or some combination could be used:
var obj6 = { 'two': 'dos' };
obj6['one'] = 'uno';
obj6['three'] = 'tres';

Note that when using the object literal syntax, each property is followed by acomma except for the last one. Care must be taken to keep track of commas, asit is often forgotten when later editing code to add a newproperty:

// Suppose the declaration of obj4 were changed to include a fourth property.
var obj4 = {
  'one': 'uno',
  'two': 'dos',
  'three': 'tres'   // Programmer forgot to add a comma to this line...
  'four': 'cuatro'  // ...when this line was added.
};

The above will result in an error from the JavaScript interpreter because itcannot parse the object literal due to the missing comma. Currently, allbrowsers other than Internet Explorer allow a trailing comma in object literalsto eliminate this issue (support for the trailing comma is mandated in ES5, soit should appear in IEsoon):

var obj4 = {
  'one': 'uno',
  'two': 'dos',
  'three': 'tres', // This extra comma is allowed on Firefox, Chrome, and Safari.
};

Unfortunately, the trailing comma produces a syntax error in Internet Explorer,so the Closure Compiler will issue an error when it encounters the trailingcomma.

Because of the popularity of JSON, it is frequent to see the keys of objectliterals as double quoted strings. The quotes are required in order to be validJSON, but they are not required in order to be valid JavaScript. Keys in objectliterals can be expressed in any of the following threeways:

var obj7 = {
  one: 'uno',      // No quotes at all
  'two': 'dos',    // Single-quoted string
  "three": 'tres'  // Double-quoted string
};

Using no quotes at all may seem odd at first, particularly if there is avariable in scope with the same name. Try to predict what happens in thefollowingcase:

var one = 'ONE';
var obj8 = { one: one };

The above creates a new object, obj8, with one property whose nameis one and whose value is 'ONE'. Whenone is used on the left of the colon, it is simply a name, butwhen it is used on the right of the colon, it is a variable. This is perhapsmore obvious if obj8 were defined in the followingway:

var obj8 = {};
obj8.one = one;

Here it is clearer that obj8.one identifies the property onobj8 named one which is distinct from the variableone to the right of the equalssign.

The only time that quotes must be used with a key in an object literal is whenthe key is a JavaScript keyword (note this is no longer a restriction inES5):

var countryCodeMap = {
  fr: 'France',
  in: 'India',  // Throws a syntax error because 'in' is a JavaScript keyword
  ru: 'Russia'
};

Despite this edge case, keys in object literals are rarely quoted in Closure.This has to do with variable renaming, which is explained in more detail inChapter 13 on the Compiler. As a rule of thumb,only quote keys that would sacrifice the correctness of the code if they wererenamed. For example, if the codewere:

var translations = {
  one: 'uno',
  two: 'dos',
  three: 'tres'
};

var englishToSpanish = function(englishWord) {
  return translations[englishWord];
};

englishToSpanish('two'); // should return 'dos'

Then the Compiler might rewrite this codeas:

var a = {
  a: 'uno',
  b: 'dos',
  c: 'tres'
};

var d = function(e) {
  return a[e];
};

d('two'); // should return 'dos' but now returns undefined

In this case, the behavior of the compiled code is different from that of theoriginal code, which is a problem. This is because the keys oftranslations do not represent properties that can be renamed, butstrings whose values are significant. Because the Compiler cannot reduce stringliterals, defining translations as follows would result in thecompiled code having the correctbehavior:

var translations = {
  'one': 'uno',
  'two': 'dos',
  'three': 'tres'
};

The "prototype" Property is Not the Prototype You are Looking For

For all the praise for its support of prototype-based programming, manipulatingan object's prototype is not straightforward inJavaScript.

Recall that every object in JavaScript has a link to another object called itsprototype. Cycles are not allowed in a chain of prototypelinks, so a collection of JavaScript objects and prototype relationships can berepresented as a rooted tree where nodes are objects and edges are prototyperelationships. Many modern browsers (though not all) expose an object'sprototype via its __proto__ property. (This causes a great deal ofconfusion because an object's __proto__ and prototypeproperties rarely refer to the same object.) The root of such a tree will bethe object referenced by Object.prototype in JavaScript. Considerthe following JavaScriptcode:

// Rectangle is an ordinary function.
var Rectangle = function() {};

// Every function has a property named 'prototype' whose value is an object
// with a property named 'constructor' that points back to the original
// function. It is possible to add more properties to this object.
Rectangle.prototype.width = 3;
Rectangle.prototype.height = 4;

// Creates an instance of a Rectangle, which is an object whose
// __proto__ property points to Rectangle.prototype. This is discussed
// in more detail in Chapter 5 on Classes and Inheritance.
var rect = new Rectangle();

Figure B-1 contains the corresponding objectmodel:

In the diagram, each box represents a JavaScript object and each circlerepresents a JavaScript primitive. Recall that JavaScript objects areassociative arrays whose keys are always strings, so each arrow exiting a boxrepresents a property of that object, the target being the property's value.For simplicity, the closed, shaded arrows represent a __proto__property while closed, white arrows represent a prototypeproperty. Open arrows have their own label indicating the name of theproperty.

The prototype chain for an object can be found by following the__proto__ arrows until the root object is reached. Note that eventhough Object.prototype is the root of the graph when only__proto__ edges are considered, Object.prototype alsohas its own values, such as the built-in function mapped tohasOwnProperty.

When resolving the value associated with a key on a JavaScript object, eachobject in the prototype chain is examined until one is found with a propertywhose name matches the specified key. If no such property exists, the valuereturned is undefined. This is effectively equivalent to thefollowing:

var lookupProperty = function(obj, key) {
  while (obj) {
    if (obj.hasOwnProperty(key)) {
      return obj[key];
    }
    obj = obj.__proto__;
  }
  return undefined;
};

For example, to evaluate the expression rect.width, the first stepis to check whether a property named width is defined onrect. From the diagram, it is clear that rect has noproperties of its own because it has no outbound arrows besides__proto__. The next step is to follow the __proto__property to Rectangle.prototype which does have an outboundwidth arrow. Following that arrow leads to the primitive value3, which is what rect.width evaluatesto.

Because the prototype chain always leads to Object.prototype, anyvalue that is declared as a property on Object.prototype will beavailable to all objects, by default. For example, every object has a propertynamed hasOwnProperty that points to a native function. That is,unless hasOwnProperty is reassigned to some other value on anobject, or some object in its prototype chain. For example, ifRectangle.prototype.hasOwnProperty were assigned toalert, then rect.hasOwnProperty would refer toalert because Rectangle.prototype appears earlier inrect's prototype chain than Object.prototype.Although this makes it possible to grant additional functionality to allobjects by modifying Object.prototype, this practice isdiscouraged and error-prone as explained in Chapter 4.

Understanding the prototype chain is also important when considering the effectof removing properties from an object. JavaScript provides thedelete keyword for removing a property from an object: usingdelete can only affect the object itself, but not any of theobjects in its prototype chain. This may sometimes yield surprisingresults:

rect.width = 13;
alert(rect.width); // alerts 13
delete rect.width;
alert(rect.width); // alerts 3 even though delete was used
delete rect.width;
alert(rect.width); // still alerts 3

When rect.width = 13 is evaluated, it creates a new binding onrect with the key width and the value13. When alert(rect.width) is called on the followingline, rect now has its own property named width, soit displays its associated value, 13. When deleterect.width is called, the width property defined onrect is removed, but the width property onRectangle.prototype still exists. This is why the second call toalert yields 3 rather than undefined. Toremove the width property from every instance ofRectangle, delete must be applied toRectangle.prototype:

delete Rectangle.prototype.width;
alert(rect.width); // now this alerts undefined

It is possible to modify rect so that it behaves as if it did nothave a width property without modifyingRectangle.prototype by setting rect.width toundefined. It can be determined whether the property wasoverridden or deleted by using the built-in hasOwnPropertymethod:

var obj = {};
rect.width = undefined;

// Now both rect.width and obj.width evaluate to undefined even though obj
// never had a width property defined on it or on any object in its prototype
// chain.

rect.hasOwnProperty('width'); // evaluates to true
obj.hasOwnProperty('width');  // evaluates to false

Note that the results would be different if Rectangle wereimplemented asfollows:

var Rectangle2 = function() {
  // This adds bindings to each new instance of Rectangle2 rather than adding
  // them once to Rectangle2.prototype.
  this.width = 3;
  this.height = 4;
};

var rect1 = new Rectangle();
var rect2 = new Rectangle2();

rect1.hasOwnProperty('width'); // evaluates to false
rect2.hasOwnProperty('width'); // evaluates to true

delete rect1.width;
delete rect2.width;

rect1.width; // evaluates to 3
rect2.width; // evaluates to undefined

Finally, note that the __proto__ properties in the diagram are notset explicitly in the sample code. These relationships are managed behind thescenes by the JavaScriptruntime.

The Syntax for Defining a Function is Significant

There are two common ways to define a function inJavaScript:

// Function Statement
function FUNCTION_NAME() {
  /* FUNCTION_BODY */
}

// Function Expression
var FUNCTION_NAME = function() {
  /* FUNCTION_BODY */
};

Although the function statement is less to type and is commonly used by thosenew to JavaScript, the behavior of the function expression is morestraightforward. (Despite this, the Google style guide advocates using thefunction , so Closure uses it in almost all cases.) The behavior ofthe two types of function definitions is not the same, as illustrated in thefollowingexamples:

function hereOrThere() {
  return 'here';
}

alert(hereOrThere()); // alerts 'there'

function hereOrThere() {
  return 'there';
}

It may be surprising that the second version of hereOrThere is usedbefore it is defined. This is due to a special behavior of function statementscalled hoisting which allows a function to be used beforeit is defined. In this case, the last definition of hereOrThere()wins, so it is hoisted and used in the call toalert().

By comparison, a function expression associates a value with variable, just likeany other assignment statement. Because of this, calling a function defined inthis manner uses the function value most recently assigned to thevariable:

var hereOrThere = function() {
  return 'here';
};

alert(hereOrThere()); // alerts 'here'

hereOrThere = function() {
  return 'there';
};

For a more complete argument of why function expressions should be favored overfunction statements, see Appendix B of Douglas Crockford'sJavaScript: The Good Parts(O'Reilly).

What "this" Refers to When a Function is Called

When calling a function of the form foo.bar.baz(), the objectfoo.bar is referred to as the receiver. Whenthe function is called, it is the receiver that is used as the value forthis:

var obj = {};

obj.value = 10;

/** @param {...number} additionalValues */
obj.addValues = function(additionalValues) {
  for (var i = 0; i < arguments.length; i++) {
    this.value += arguments[i];
  }
  return this.value;
};

// Evaluates to 30 because obj is used as the value for 'this' when
// obj.addValues() is called, so obj.value becomes 10 + 20.
obj.addValues(20);

If there is no explicit receiver when a function is called, then the globalobject becomes the receiver. As explained in "goog.global",window is the global object when JavaScript is executed in a webbrowser. This leads to some surprisingbehavior:

var f = obj.addValues;

// Evaluates to NaN because window is used as the value for 'this' when
// f() is called. Because and window.value is undefined, adding a number to
// it results in NaN.
f(20);

// This also has the unintentional side-effect of adding a value to window:
alert(window.value); // Alerts NaN

Even though obj.addValues and f refer to the samefunction, they behave differently when called because the value of the receiveris different in each call. For this reason, when calling a function that refersto this, it is important to ensure that this willhave the correct value when it is called. To be clear, if thiswere not referenced in the function body, then the behavior off(20) and obj.addValues(20) would be thesame.

Because functions are first-class objects in JavaScript, they can have their ownmethods. All functions have the methods call() andapply() which make it possible to redefine the receiver (i.e., theobject that this refers to) when calling the function. The methodsignatures are asfollows:

/**
 * @param {*=} receiver to substitute for 'this'
 * @param {...} parameters to use as arguments to the function
 */
Function.prototype.call;

/**
 * @param {*=} receiver to substitute for 'this'
 * @param {Array} parameters to use as arguments to the function
 */
Function.prototype.apply;

Note that the only difference between call() andapply() is that call() receives the functionparameters as a individual arguments whereas apply() receives themas a singlearray:

// When f is called with obj as its receiver, it behaves the same as calling
// obj.addValues(). Both of the following increase obj.value by 60:
f.call(obj, 10, 20, 30);
f.apply(obj, [10, 20, 30]);

The following calls are equivalent, as f andobj.addValues refer to the samefunction:

obj.addValues.call(obj, 10, 20, 30);
obj.addValues.apply(obj, [10, 20, 30]);

However, the following will not work: neither call() norapply() uses the value of its own receiver to substitute for thereceiver argument when it isunspecified.

// Both statements evaluate to NaN
obj.addValues.call(undefined, 10, 20, 30);
obj.addValues.apply(undefined, [10, 20, 30]);

The value of this can never be null orundefined when a function is called. When null orundefined is supplied as the receiver tocall() or apply(), the global object is used as thevalue for receiver instead. Therefore, the above has the sameundesirable side-effect of adding a property named value to theglobalobject.

It may he helpful to think of a function as having no knowledge of the variableto which it is assigned. This helps reinforce the idea that the value ofthis will be bound when the function is called rather than when itisdefined.

The "var" Keyword is Significant

Many self-taught JavaScript programmers believe that the varkeyword is optional because they do not observe different behavior when theyomit it. On the contrary, omitting the var keyword can lead tosome very subtlebugs.

The var keyword is significant because it introduces a new variablein local scope. When a variable is referenced without the varkeyword, it uses the variable by that name in the closest scope. If no suchvariable is defined, a new binding for that variable is declared on the globalobject. Consider the followingexample:

var foo = 0;

var f = function() {
  // This defines a new variable foo in the scope of f.
  // This is said to "shadow" the global variable foo, whose value is 0.
  // The global value of foo could be referenced via window.foo, if desired.
  var foo = 42;

  var g = function() {
    // This defines a new variable bar in the scope of g.
    // It uses the closest declaration of foo, which is in f.
    var bar = foo + 100;
    return bar;
  };

  // There is no variable bar declared in the current scope, f, so this
  // introduces a new variable, bar, on the global object. Code in g has
  // access to f's scope, but code in f does not have access to g's scope.
  bar = 'DO NOT DO THIS!';

  // Returns a function that adds 100 to the local variable foo.
  return g;
};

// This alerts 'undefined' because bar has not been added to the global scope yet.
alert(typeof bar);

// Calling f() has the side-effect of introducing the global variable bar.
var h = f();
alert(bar);  // Alerts 'DO NOT DO THIS!'

// Even though h() is called outside of f(), it still has access to scope of
// f and g, so h() returns (foo + 100), or 142.
alert(h());  // Alerts 142

This gets even trickier when var is omitted from loop variables.Consider the following function, f(), which uses a loop to callg() three times. Calling g() uses a loop to callalert() three times, so you may expect nine alert boxes to appearwhen f() iscalled:

var f = function() {
  for (i = 0; i < 3; i++) {
    g(i);
  }
};

var g = function() {
  for (i = 0; i < 3; i++) {
    alert(i);
  }
}

// This alerts 0, 1, 2, and then stops.
f();

Instead, alert() only appears three times because bothf() and g() fail to declare the loop variablei with the var keyword. When g() iscalled for the first time, it uses the global variable i which hasbeen initialized to 0 by f(). When g()exits, it has increased the value of i to 3. On thenext iteration of the loop in f(), i is now3, so the test for the conditional i < 3 fails,and f() terminates. This problem is easily solved by appropriatelyusing the var keyword to declare each loopvariable:

for (var i = 0; i < 3; i++)

Understanding var is important in avoiding subtle bugs related tovariable scope. Enabling Verbose warnings from the Closure Compiler will helpcatch theseissues.

Block Scope is Meaningless

Unlike most C-style languages, variables in JavaScript functions are accessiblethroughout the entire function rather than the block (delimited by curlybraces) in which the variable is declared. This can lead to the followingprogrammingerror:

/**
 * Recursively traverses map and returns an array of all keys that are found.
 * @param {Object} map
 * @return {Array.<string>}
 */
var getAllKeys = function(map) {
  var keys = [];
  for (var key in map) {
    keys.push(key);
    var value = map[key];
    if (typeof value == 'object') {
      // Here, "var map" does not introduce a new local variable named map
      // because such a variable already exists in function scope.
      var map = value;
      keys = keys.concat(getAllKeys(map));
    }
  }
  return keys;
};

var mappings = {
  'derivatives': { 'sin x': 'cos x', 'cos x': '-sin x'},
  'integrals': { '2x': 'x^2', '3x^2': 'x^3'}
};

// Evaluates to: ['derivatives', 'sin x', 'cos x', 'integrals']
getAllKeys(mappings);

The array returned by getAllKeys() is missing the values'2x' and '3x^2'. This is because of a subtle errorwhere map is reused as a variable inside the ifblock. In languages that support block scoping, this would introduce a newvariable named map that would be assigned to valuefor the duration of the if block, and upon exiting theif block, the recent binding for map would bediscarded and the previous binding for map would be restored. InJavaScript, there is already a variable named map in scope becauseone of the arguments to getAllKeys() is named map.Even though declaring var map within getAllKeys() islikely a signal that the programmer is trying to introduce a new variable, thevar is silently ignored by the JavaScript interpreter andexecution proceeds withoutinterruption.

When the Verbose warning level is used, the Closure Compiler issues a warningwhen it encounters code such as this. To appease the Compiler, either thevar must be dropped (to indicate the existing variable is meant tobe reused) or a new variable name must be introduced (to indicate that aseparate variable is meant to be used). The getAllKeys() examplefalls into the latter case, so the if block should be rewrittenas:

if (typeof value == 'object') {
  var newMap = value;
  keys = keys.concat(getAllKeys(newMap));
}

Interestingly, because the scope of a variable includes the entire function, thedeclaration of the variable can occur anywhere in the function, even after itsfirst"use":

var strangeLoop = function(someArray) {
  // Will alert 'undefined' because i is in scope, but no value has been
  // assigned to it at this point.
  alert(i);

  // Assign 0 to i and use it as a loop counter.
  for (i = 0; i < someArray.length; i++) {
    alert('Element ' + i + ' is: ' + someArray[i]);
  }

  // Declaration of i which puts it in function scope.
  // The value 42 is never used.
  var i = 42;
};

Like the case where redeclaring a variable goes unnoticed by the interpreter butis flagged by the Compiler, the Compiler will also issue a warning (again, withthe Verbose warnings enabled) if a variable declaration appears after its firstuse within thefunction.

It should be noted that even though blocks do not introduce new scopes,functions can be used in place of blocks for that purpose. The ifblock in getAllKeys() could be rewritten asfollows:

if (typeof value == 'object') {
  var functionWithNewScope = function() {
    // This is a new function, and therefore a new scope, so within this
    // function, map is a new variable because it is prefixed with 'var'.
    var map = value;

    // Because keys is not prefixed with 'var', the existing value of keys
    // from the enclosing function is used.
    keys = keys.concat(getAllKeys(map));
  };
  // Calling this function will have the desired effect of updating keys.
  functionWithNewScope();
}

Although the above will work, it is less efficient than replacing varmap with var newMap as describedearlier. 

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
spiderMonkey JSAPI User Guide
Lua Generic Call
jquery的对象数组的添加元素,删除元素
Performance Tips and Tricks in .NET Applications
Chapter 4: Defining Functions and Using Built
OO C is passable
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服