`
wenbois2000
  • 浏览: 44655 次
  • 性别: Icon_minigender_1
  • 来自: 湖南
社区版块
存档分类
最新评论

JavaScript Patterns 读书笔记(四)

    博客分类:
  • Web
阅读更多

4.Function Pattern

  • Namespace Pattern
       Namespaces help reduce the number of globals required by our programs and at the same time also help avoid naming collisions or excessive name prefixing. JavaScript doesn’t have namespaces built into the language syntax, but this is a feature that is quite easy to achieve. Instead of polluting the global scope with a lot of functions, objects, and other variables, you can create one (and ideally only one) global object for your application or library. Then you can add all the functionality to that object. Consider the following example:
    // BEFORE: 5 globals
    // Warning: antipattern
    // constructors
    function Parent() {}
    function Child() {}
    
    // a variable
    var some_var = 1;
    
    // some objects
    var module1 = {};
    module1.data = {a: 1, b: 2};
    var module2 = {};
    
        You can refactor this type of code by creating a single global object for your application, called, for example, MYAPP, and change all your functions and variables to become properties of your global object:
    // AFTER: 1 global
    // global object
    var MYAPP = {};
    
    // constructors
    MYAPP.Parent = function () {};
    MYAPP.Child = function () {};
    
    // a variable
    MYAPP.some_var = 1;
    
    // an object container
    MYAPP.modules = {};
    
    // nested objects
    MYAPP.modules.module1 = {};
    MYAPP.modules.module1.data = {a: 1, b: 2};
    MYAPP.modules.module2 = {};
    
       For the name of the global namespace object, you can pick, for example, the name of your application or library, your domain name, or your company name. Often developers use the convention of making the global variable ALL CAPS, so it stands out to the readers of the code. (But keep in mind that all caps are also often used for constants.)
      This pattern is a good way to namespace your code and to avoid naming collisions in your own code, and collisions between your code and third-party code on the same page, such as JavaScript libraries or widgets. This pattern is highly recommended and perfectly applicable for many tasks, but it does have some drawbacks:

          • A bit more to type; prefixing every variable and function does add up in the total
        amount of code that needs to be downloaded

         • Only one global instance means that any part of the code can modify the global
        instance and the rest of the functionality gets the updated state

         • Long nested names mean longer (slower) property resolution lookups

    The sandbox pattern discussed later in the chapter addresses these drawbacks.
  • Declaring Dependencies
       JavaScript libraries are often modular and namespaced, which enables you to include only the modules you require. For example, in YUI2 there’s a global variable YAHOO, which serves as a namespace, and then modules that are properties of the global variable, such as YAHOO.util.Dom (the DOM module) and YAHOO.util.Event (Events module).
       It’s a good idea to declare the modules your code relies on at the top of your function or module. The declaration involves creating only a local variable and pointing to the desired module:
    var myFunction = function () {
    	// dependencies
    	var event = YAHOO.util.Event,
    	dom = YAHOO.util.Dom;
    	// use event and dom variables
    	// for the rest of the function...
    };
    
     This is an extremely simple pattern, but at the same time it has numerous benefits:


        • Explicit declaration of dependencies signals to the users of your code the specific script files that they need to make sure are included in the page.

         • Upfront declaration at the top of the function makes it easy to find and resolve dependencies.

         • Working with a local variable (such as dom) is always faster than working with a global (such as YAHOO) and even faster than working with nested properties of a global variable (such as YAHOO.util.Dom), resulting in better performance. When following this dependency declaration pattern, the global symbol resolution is performed only once in the function. After that the local variable is used, which is much faster.

         • Advanced minification tools such as YUICompressor and Google Closure compiler will rename local variables (so event will likely become just one character such as A), resulting in smaller code, but never global variables, because it’s not safe to do so.

     
  • Private Properties and Methods
      JavaScript has no special syntax to denote private, protected, or public properties and methods, unlike Java or other languages. All object members are public:
    var myobj = {
    	myprop: 1,
    	getProp: function () {
    		return this.myprop;
    	}
    };
    console.log(myobj.myprop); // `myprop` is publicly accessible
    console.log(myobj.getProp()); // getProp() is public too
    
       The same is true when you use constructor functions to create objects; all members are still public:
    function Gadget() {
    	this.name = 'iPod';
    	this.stretch = function () {
    		return 'iPad';
    	};
    }
    var toy = new Gadget();
    console.log(toy.name); // `name` is public
    console.log(toy.stretch()); // stretch() is public
    
        Although the language doesn’t have special syntax for private members, you can implement them using a closure. Your constructor functions create a closure and any variables that are part of the closure scope are not exposed outside the constructor. However, these private variables are available to the public methods: methods defined inside the constructor and exposed as part of the returned objects. Let’s see an example where name is a private member, not accessible outside the constructor:
    function Gadget() {
    	// private member
    	var name = 'iPod';
    	// public function
    	this.getName = function () {
    		return name;
    	};
    }
    
    var toy = new Gadget();
    // `name` is undefined, it's private
    console.log(toy.name); // undefined
    // public method has access to `name`
    console.log(toy.getName()); // "iPod"
    
       As you can see, it’s easy to achieve privacy in JavaScript. All you need to do is wrap the data you want to keep private in a function and make sure it’s local to the function, which means not making it available outside the function.

  • Privileged Methods
       The notion of privileged methods doesn’t involve any specific syntax; it’s just a name given to the public methods that have access to the private members (and hence have more privileges).
      In the previous example, getName() is a privileged method because it has “special” access to the private property name.
     
  • Privacy Failures
      There are some edge cases when you’re concerned about privacy:
            • Some earlier versions of Firefox enable a second parameter to be passed to eval() that is a context object enabling you to sneak into the private scope of the function. Similarly the __parent__ property in Mozilla Rhino would enable you access to private Scope. These edge cases don’t apply to widely used browsers today.

            • When you’re directly returning a private variable from a privileged method and this variable happens to be an object or array, then outside code can modify the private variable because it’s passed by reference.

        Let’s examine the second case a little more closely. The following implementation of  Gadget looks innocent:
    function Gadget() {
    	// private member
    	var specs = {
    		screen_width: 320,
    		screen_height: 480,
    		color: "white"
    	};
    
    	// public function
    	this.getSpecs = function () {
    		return specs;
    	};
    }
    
     
       The problem here is that getSpecs() returns a reference to the specs object. This enables the user of Gadget to modify the seemingly hidden and private specs; The solution to this unexpected behavior is to be careful not to pass references to objects and arrays you want to keep private. One way to achieve this is to have getSpecs() return a new object containing only some of the data that could be interesting to the consumer of the object. This is also known as Principle of Least Authority (POLA), which states that you should never give more than needed. In this case, if the consumer of Gadget is interested whether the gadget fits a certain box, it needs only the dimensions.
       So instead of giving out everything, you can create getDimensions(), which returns a new object containing only width and height. You may not need to implement getSpecs() at all.
       Another approach, when you need to pass all the data, is to create a copy of the specs object, Using a general-purpose object-cloning function.

  • Revealing Private Functions As Public Methods
       The revelation pattern is about having private methods, which you also expose as public methods. This could be useful when all the functionality in an object is critical for the workings of the object and you want to protect it as much as possible. But at the same time you want to provide public access to some of this functionality because that could be useful, too. When you expose methods publicly, you make them vulnerable; some of the users of your public API may modify it, even involuntarily. In ECMAScript 5 you have the option to freeze an object, but not in the previous versions of the language. Enter the revelation pattern (the term coined by Christian Heilmann originally was “revealing module pattern”).
       Let’s take an example, building on top of one of the privacy patterns—the private members in object literals:
    var myarray;
    (function () {
    	var astr = "[object Array]",
    	toString = Object.prototype.toString;
    
    	function isArray(a) {
    		return toString.call(a) === astr;
    	}
    
    	function indexOf(haystack, needle) {
    		var i = 0,
    		max = haystack.length;
    		for (; i < max; i += 1) {
    			if (haystack[i] === needle) {
    				return i;
    			}
    		}
    		return −1;
    	}
    
    	myarray = {
    		isArray: isArray,
    		indexOf: indexOf,
    		inArray: indexOf
    	};
    }());
    
     
       Here you have two private variables and two private functions—isArray() and indexOf(). Toward the end of the immediate function, the object myarray is populated with the functionality you decide is appropriate to make publicly available. In this case the same private indexOf() is exposed as both ECMAScript 5–style indexOf and PHPinspired inArray. Testing the new myarray object:
    myarray.isArray([1,2]); // true
    myarray.isArray({0: 1}); // false
    myarray.indexOf(["a", "b", "z"], "z"); // 2
    myarray.inArray(["a", "b", "z"], "z"); // 2
    
     
  • Module Pattern
       The module pattern is widely used because it provides structure and helps organize your code as it grows. Unlike other languages, JavaScript doesn’t have special syntax for packages, but the module pattern provides the tools to create self-contained decoupled pieces of code, which can be treated as black boxes of functionality and added, replaced, or removed according to the (ever-changing) requirements of the software you’re writing.
       The module pattern is a combination of several patterns described so far in the book,namely:
    • Namespaces
    • Immediate functions
    • Private and privileged members
    • Declaring dependencies
    
       The first step is setting up a namespace. Let’s use the namespace() function from earlier in this chapter and start an example utility module that provides useful array methods:
    MYAPP.namespace('MYAPP.utilities.array');
        The next step is defining the module. The pattern uses an immediate function that will provide private scope if privacy is needed. The immediate function returns an object—the actual module with its public interface, which will be available to the consumers of the module:
    MYAPP.utilities.array = (function () {
    	return {
    		// todo...
    	};
    }());
    
     Next, let’s add some methods to the public interface:
    MYAPP.utilities.array = (function () {
    	return {
    		inArray: function (needle, haystack) {
    			// ...
    		},
    		isArray: function (a) {
    			// ...
    		}
    	};
    }());
    
        Using the private scope provided by the immediate function, you can declare some private properties and methods as needed. Right at the top of the immediate function will also be the place to declare any dependencies your module might have. Following the variable declarations, you can optionally place any one-off initialization code that helps set up the module. The final result is an object returned by the immediate function that contains the public API of your module:
    MYAPP.namespace('MYAPP.utilities.array');
    MYAPP.utilities.array = (function () {
    
    	// dependencies
    	var uobj = MYAPP.utilities.object,
    	ulang = MYAPP.utilities.lang,
    
    	// private properties
    	array_string = "[object Array]",
    	ops = Object.prototype.toString;
    
    	// private methods
    	// ...
    	// end var
    
    	// optionally one-time init procedures
    	// ...
    
    	// public API
    	return {
    		inArray: function (needle, haystack) {
    			for (var i = 0, max = haystack.length; i < max; i += 1) {
    				if (haystack[i] === needle) {
    						return true;
    				}
    			}
    		},
    		isArray: function (a) {
    			return ops.call(a) === array_string;
    		}
    		// ... more methods and properties
    	};
    }());
    
     
       The module pattern is a widely used and highly recommended way to organize your code, especially as it grows.

  • Sandbox Pattern
       The sandbox pattern addresses the drawbacks of the namespacing pattern, namely :

         • Reliance on a single global variable to be the application’s global. In the namespacing pattern, there is no way to have two versions of the same application or library run on the same page, because they both need the same global symbol name, for example, MYAPP.
         • Long, dotted names to type and resolve at runtime. for example,  MYAPP.utilities.array.

       As the name suggests, the sandbox pattern provides an environment for the modules to “play” without affecting other modules and their personal sandboxes. The pattern is heavily used in YUI version 3, for example, but keep in mind that the following discussion is a sample reference implementation and does not attempt to describe how YUI3’s sandbox implementation works.

    A Global Constructor
         In the namespacing pattern you have one global object; in the sandbox pattern the single global is a constructor: let’s call it Sandbox(). You create objects using this constructor, and you also pass a callback function, which becomes the isolated sandboxed environment for your code. Using the sandbox will look like this:
    new Sandbox(function (box) {
    	// your code here...
    });
    

        The object box will be like MYAPP in the namespacing example—it will have all the library functionality you need to make your code work. Let’s add two more things to the pattern: 

         • With some magic (enforcing new pattern from Chapter 3), you can assume New and not require it when creating the object.    
                          
         • The Sandbox() constructor can accept an additional configuration argument (or arguments) specifying names of modules required for this object instance. We want the code to be modular, so most of the functionality Sandbox() provides will be contained in modules.

       With these two additional features, let’s see some examples of what the code for instantiating objects will look like. You can omit new and create an object that uses some fictional “ajax” and “event” modules like so:
    Sandbox(['ajax', 'event'], function (box) {
    	// console.log(box);
    });
    
        This example is similar to the preceding one, but this time module names are passed as individual arguments:
    Sandbox('ajax', 'dom', function (box) {
    	// console.log(box);
    });
    
       And how about using a wildcard “*” argument to mean “use all available modules”? For convenience, let’s also say that when no modules are passed, the sandbox will assume '*'. So two ways to use all available modules will be like:
    Sandbox('*', function (box) {
    	// console.log(box);
    });
    Sandbox(function (box) {
    	// console.log(box);
    });
    
     
       And one more example of using the pattern illustrates how you can instantiate sandbox objects multiple times—and you can even nest them one within the other without the two interfering:
    Sandbox('dom', 'event', function (box) {
    	// work with dom and event
    	Sandbox('ajax', function (box) {
    		// another sandboxed "box" object
    		// this "box" is not the same as
    		// the "box" outside this function
    		//...
    		// done with Ajax
    	});
    	// no trace of Ajax module here
    });
    
     
       As you can see from these examples, when using the sandbox pattern, you can protect the global namespace by having your code wrapped into callback functions. If you need, you can also use the fact that functions are objects and store some data as “static” properties of the Sandbox() constructor. And, finally, you can have different instances depending on the type of modules you need and those instances work independently of each other.
       Now let’s see how you can approach implementing the Sandbox() constructor and its modules to support all this functionality.

    Adding Modules
         Before implementing the actual constructor, let’s see how we can approach adding modules.
         The Sandbox() constructor function is also an object, so you can add a static property called modules to it. This property will be another object containing key-value pairs where the keys are the names of the modules and the values are the functions that implement each module:
    Sandbox.modules = {};
    
    Sandbox.modules.dom = function (box) {
    	box.getElement = function () {};
    	box.getStyle = function () {};
    	box.foo = "bar";
    };
    Sandbox.modules.event = function (box) {
    	// access to the Sandbox prototype if needed:
    	// box.constructor.prototype.m = "mmm";
    	box.attachEvent = function () {};
    	box.dettachEvent = function () {};
    };
    Sandbox.modules.ajax = function (box) {
    	box.makeRequest = function () {};
    	box.getResponse = function () {};
    };
    
        In this example we added modules dom, event, and ajax, which are common pieces of functionality in every library or complex web application.The functions that implement each module accept the current instance box as a parameter and may add additional properties and methods to that instance.

    Implementing the Constructor
       Finally, let’s implement the Sandbox() constructor (naturally, you would want to rename this type of constructor to something that makes sense for your library or application):
    function Sandbox() {
    	// turning arguments into an array
    	var args = Array.prototype.slice.call(arguments),
    	// the last argument is the callback
    	callback = args.pop(),
    	// modules can be passed as an array or as individual parameters
    	modules = (args[0] && typeof args[0] === "string") ? args : args[0],
    	i;
    
    	// make sure the function is called
    	// as a constructor
    	if (!(this instanceof Sandbox)) {
    		return new Sandbox(modules, callback);
    	}
    
    	// add properties to `this` as needed:
    	this.a = 1;
    	this.b = 2;
    
    	// now add modules to the core `this` object
    	// no modules or "*" both mean "use all modules"
    	if (!modules || modules === '*') {
    		modules = [];
    		for (i in Sandbox.modules) {
    			if (Sandbox.modules.hasOwnProperty(i)) {
    				modules.push(i);
    			}
    		}
    	}
    
    	// initialize the required modules
    	for (i = 0; i < modules.length; i += 1) {
    		Sandbox.modules[modules[i]](this);
    	}
    
    	// call the callback
    	callback(this);
    }
    
    // any prototype properties as needed
    Sandbox.prototype = {
    	name: "My Application",
    	version: "1.0",
    	getName: function () {
    		return this.name;
    	}
    };
    
     
    The key items in the implementation are:
        • There’s a check whether this is an instance of Sandbox and if not (meaning Sandbox() was called without new), we call the function again as a constructor.

        • You can add properties to this inside the constructor. You can also add properties to the prototype of the constructor.

        • The required modules can be passed as an array of module names, or as individual arguments, or with the * wildcard (or omitted), which means we should load all available modules. Notice that in this example implementation we don’t worry about loading required functionality from additional files, but that’s definitely an option. This is something supported by YUI3 for example. You can load only the most basic module (also known as a “seed”) and whatever modules you require will be loaded from external files using a naming convention where the filenames correspond to module names.

       • When we know the required modules, we initialize them, which means we call the function that implements each module.

       • The last argument to the constructor is the callback. The callback will be invoked at the end using the newly created instance. This callback is actually the user’s sandbox, and it gets a box object populated with all the requested functionality.

  • Static Members
       Static properties and methods are those that don’t change from one instance to another.In class-based languages, static members are created using special syntax and then used as if they were members of the class itself. For example, a static method max() of some MathUtils class would be invoked like MathUtils.max(3, 5). This is an example of a public static member, which can be used without having to create an instance of the class. There can also be private static members—not visible to the consumer of the class but still shared among all the instances of the class. Let’s see how to implement both private and public static members in JavaScript.

    Public Static Members
        In JavaScript there’s no special syntax to denote static members. But you can have the same syntax as in a “classy” language by using a constructor function and adding properties to it. This works because constructors, like all other functions, are objects and they can have properties. The memoization pattern discussed in the previous chapter employed the same idea—adding properties to a function.
       The following example defines a constructor Gadget with a static method isShiny() and a regular instance method setPrice(). The method isShiny() is a static method because it doesn’t need a specific gadget object to work (just like you don’t need a particular gadget to figure out that all gadgets are shiny). setPrice(), on the other hand,needs an object, because gadgets can be priced differently:
    // constructor
    var Gadget = function () {};
    
    // a static method
    Gadget.isShiny = function () {
    	return "you bet!"; // 没错
    };
    
    // a normal method added to the prototype
    Gadget.prototype.setPrice = function (price) {
    	this.price = price;
    };
    
    /*
    Now let’s call these methods. The static isShiny() is invoked directly on the constructor,
    whereas the regular method needs an instance:
    */
    // calling a static method
    Gadget.isShiny(); // "you bet"
    // creating an instance and calling a method
    var iphone = new Gadget();
    iphone.setPrice(500);
    
    typeof Gadget.setPrice; // "undefined"
    typeof iphone.isShiny; // "undefined"
    
       Attempting to call an instance method statically won’t work; same for calling a static method using the instance iphone object
       Sometimes it could be convenient to have the static methods working with an instance too. This is easy to achieve by simply adding a new method to the prototype, which serves as a façade pointing to the original static method:

    Gadget.prototype.isShiny = Gadget.isShiny;
    window.alert(iphone.isShiny()); // "you bet"
        In such cases you need to be careful if you use this inside the static method. When you do Gadget.isShiny() then this inside isShiny() will refer to the Gadget constructor function. If you do iphone.isShiny() then this will point to iphone(!).
       One last example shows how you can have the same method being called statically and nonstatically and behave slightly different, depending on the invocation pattern. Here instanceof helps determine how the method was called:
    // constructor
    var Gadget = function (price) {
    	this.price = price;
    };
    
    // a static method
    Gadget.isShiny = function () {
    	// this always works
    	var msg = "you bet";
    
    	if (this instanceof Gadget) {
    		// this only works if called non-statically
    		msg += ", it costs $" + this.price + '!';
    	}
    	return msg;
    };
    
    // a normal method added to the prototype
    Gadget.prototype.isShiny = function () {
    	return Gadget.isShiny.call(this);
    };
    
    // Testing a static method call:
    Gadget.isShiny(); // "you bet"
    
    //Testing an instance, nonstatic call:
    var a = new Gadget('499.99');
    a.isShiny(); // "you bet, it costs $499.99!"
    
     
    Private Static Members
       The discussion so far was on public static methods; now let’s take a look at how you can implement private static members. By private static members, we mean members that are:

        • Shared by all the objects created with the same constructor function
        • Not accessible outside the constructor

        Let’s look at an example where counter is a private static property in the constructor Gadget. In this chapter there was already a discussion on private properties, so this part is still the same—you need a function to act as a closure and wrap around the private members. Then let’s have the same wrapper function execute immediately and return a new function. The returned function value is assigned to the variable Gadget and becomes the new constructor:

    var Gadget = (function () {
    	// static variable/property
    	var counter = 0;
    
    	// returning the new implementation
    	// of the constructor
    	return function () {
    		console.log(counter += 1);
    	};
    }()); // execute immediately
     
         The new Gadget constructor simply increments and logs the private counter. Testing with several instances you can see that counter is indeed shared among all instances; Because we’re incrementing the counter with one for every object, this static property becomes an ID that uniquely identifies each object created with the Gadget constructor. The unique identifier could be useful, so why not expose it via a privileged method? Below is an example that builds upon the previous and adds a privileged method getLastId() to access the static private property:
    // constructor
    var Gadget = (function () {
    	// static variable/property
    	var counter = 0,
    	NewGadget;
    
    	// this will become the
    	// new constructor implementation
    	NewGadget = function () {
    		counter += 1;
    	};
    	// a privileged method
    	NewGadget.prototype.getLastId = function () {
    		return counter;
    	};
    
    	// overwrite the constructor
    	return NewGadget;
    }()); // execute immediately
    
    // Testing the new implementation:
    var iphone = new Gadget();
    iphone.getLastId(); // 1
    var ipod = new Gadget();
    ipod.getLastId(); // 2
    var ipad = new Gadget();
    ipad.getLastId(); // 3
    
     
       Static properties (both private and public) can be quite handy. They can contain methods and data that are not instance-specific and don’t get re-created with every instance.
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics