模式是解决或者避免一些问题的方案。
在JavaScript中,会用到一些常用的编码模式。下面就列出了一些常用的JavaScript编码模式,有的模式是为了解决特定的问题,有的则是帮助我们避免一些JavaScript中容易出现的错误。
所谓“单一var模式”(Single var pattern)就是指在函数顶部,只使用一个var进行变量声明的模式。例如:
function func() { var a = 1, b = 2, sum = a + b, myObject = {}, i, j; // other code }
使用这个模式的好处:
JavaScript允许在函数的任意地方声明变量,但是效果都等同于在函数顶部进行声明,这个是所谓的变量提升(Hoisting)。
看一个例子:
var num = 10; function func() { alert(num); // undefined var num = 1; alert(num); // 1 } func();
从这个例子可以看到,第一次alert
的值并不是10
,而是undefined
。所以,应该尽量使用“单一var模式”来避免类似的问题。
关于变量提升的细节,请参考我前面一篇JavaScript的执行上下文。
在JavaScript中,for-in循环主要用来枚举对象的属性。
但是,由于JavaScript中原型链的存在,一般都会结合hasOwnProperty()来使用for-in循环,从而过滤原型链上的非该对象的属性。
var wilber = { name: "Wilber", age: 28, gender: "male" }; Object.prototype.printPersonalInfo = function() { console.log(this.name, "is", this.age, "years old"); }; for(var prop in wilber) { if(wilber.hasOwnProperty(prop)) { console.log(prop, ":", wilber[prop]); } }
根据开发人员的习惯,开放大括号的位置会有不同的选择,可以和语句放在同一行,也可以放在新的一行:
var total = 10; if(tatal > 5) { console.log("bigger than 5"); } if(tatal > 5) { console.log("bigger than 5"); }
两种形式的代码都能实现同样的逻辑,但是,JavaScript允许开发人员省略分号,JavaScript的分号插入机制(semicolon insertion mechanism)会负责加上省略的分号,这时开放大括号的位置不同就可能产生不同的结果。
看一个例子:
function func() { return { name: "Wilber" }; } alert(func()); // undefined
之所以得到的结果是undefined
就是因为JavaScript的分号插入机制,在return
语句之后自动添加了分号。
调整一下开放的大括号的位置就可以避免这个问题:
function func() { return { name: "Wilber" }; } alert(func()); // [object]
所以,关于开放的大括号位置,建议将开放的大括号放置在前面语句的同一行。
JavaScript中,通过new
关键字,可以用构造函数来创建对象,例如:
function Person(name, city) { this.name = name; this.city = city; this.getInfo = function() { console.log(this.name, "lives at", this.city); } } var will = new Person("Will", "Shanghai"); will.getInfo(); // Will lives at Shanghai
但是,如果开发人员忘记了new
关键字,那么构造函数中的this
将代表全局对象(浏览器中就是window
对象),所有的属性将会变成全局对象的属性。
function Person(name, city) { this.name = name; this.city = city; this.getInfo = function() { console.log(this.name, "lives at", this.city); } } var will = Person("Will", "Shanghai"); console.log(will.name); // Uncaught TypeError: Cannot read property 'name' of undefined console.log(window.name); // Will console.log(window.city); // Shanghai window.getInfo(); // Will lives at Shanghai
所以,为了避免这类问题的方式,首先是从代码规范上下手。建议对于所有的JavaScript构造函数的命名方式都遵循,构造函数使用首字母大写的命名方式。
这样当我们看到首字母大写的函数,就要考虑是不是漏掉了new
关键字。
当然除了规范之外,还可以通过代码的方式来避免上面的问题。
具体的做法就是,在构造函数中检查this是否为构造函数的一个实例,如果不是,构造函数可以通过new
关键字进行自调用。
下面就是使用自调用构造函数对上面的例子进行改进:
function Person(name, city) { if(!(this instanceof Person)) { return new Person(name, city); } this.name = name; this.city = city; this.getInfo = function() { console.log(this.name, "lives at", this.city); } } var will = Person("Will", "Shanghai"); console.log(will.name); // Will console.log(will.city); // Shanghai will.getInfo(); // Will lives at Shanghai window.getInfo(); // Uncaught TypeError: window.getInfo is not a function
结合构造函数的命名约定和自调用的构造函数,这下就不用担心漏掉new
关键字的情况了。
当在JavaScript中判断一个对象是不是数组的时候,不能直接使用typeof
,因为我们会得到object
。
在ECMA5中提出了Array.isArray()这个函数,我们可以直接使用来判断一个对象是不是数组类型。
对于不支持ECMA5的环境,我们可以通过下面的方式自己实现Array.isArray()这个函数。
if(typeof Array.isArray === "undefined") { Array.isArray = function(arg){ return Object.prototype.toString.call(arg) === "[object Array]"; }; } var arr = []; console.log(Array.isArray(arr)); // true
立即执行函数是JavaScript中非常常用的一种模式,形式如下:
(function() { // other code }());
通过这个模式可以提供一个局部的作用域,所以函数代码都会在局部作用域中执行,不会污染其他作用域。
现在的很多JavaScript库都直接使用了这种模式,例如JQuery、underscore等等。
关于立即执行函数另外一点需要注意的地方就是立即执行函数的参数。
我们可以像正常的函数调用一样进行参数传递:
(function(name, city) { console.log(name, "lives at", city); }("Wilber", "Shanghai")); // Wilber lives at Shanghai
在立即执行函数中,是可以访问外部作用域的(当然包括全局对象),例如:
var name = "Wilber"; var city = "Shanghai"; (function() { console.log(name, "lives at", city); }()); // Wilber lives at Shanghai
但是,如果立即执行函数需要访问全局对象,常用的模式就是将全局对象以参数的方式传递给立即执行函数。
var name = "Wilber"; var city = "Shanghai"; (function(global) { console.log(global.name, "lives at", global.city); }(this)); // Wilber lives at Shanghai
这样做的好处就是,在立即执行函数中访问全局变量的属性的时候就不用进行作用域链查找了,关于更多JavaScript作用域链的内容,可以参考理解JavaScript的作用域链。
初始化时分支(Init-time Branching)是一种常用的优化模式,就是说当某个条件在整个程序声明周期内都不会发生改变的时候,不用每次都对条件进行判断,仅仅一次判断就足够了。
这里最常见的例子就是对浏览器的检测,在下面的例子中,每次使用utils.addListener1
属性的时候都要进行浏览器判断,效率比较低下:
var utils = { addListener: function(el, type, fn) { if (typeof window.addEventListener === 'function') { el.addEventListener(type, fn, false); } else if (typeof document.attachEvent === 'function') { // IE el.attachEvent('on' + type, fn); } else { // older browsers el['on' + type] = fn; } }, removeListener: function(el, type, fn) { // pretty much the same... } };
所以,根据初始化时分支模式,可以在脚本初始化的时候进行一次浏览器检测,这样在以后使用utils
的时候就不必进行浏览器检测了:
// the interface var utils = { addListener: null, removeListener: null }; // the implementation if (typeof window.addEventListener === 'function') { utils.addListener = function(el, type, fn) { el.addEventListener(type, fn, false); }; utils.removeListener = function(el, type, fn) { el.removeEventListener(type, fn, false); }; } else if (typeof document.attachEvent === 'function') { // IE utils.addListener = function(el, type, fn) { el.attachEvent('on' + type, fn); }; utils.removeListener = function(el, type, fn) { el.detachEvent('on' + type, fn); }; } else { // older browsers utils.addListener = function(el, type, fn) { el['on' + type] = fn; }; utils.removeListener = function(el, type, fn) { el['on' + type] = null; }; }
JavaScript代码中,过多的全局变量经常会引发一些问题,比如命名冲突。
结合命名空间模式就可以一定程度上减少代码中全局变量的个数。
下面就看一个通用命名空间函数的实现:
var MYAPP = MYAPP || {}; MYAPP.namespace = function (ns_string) { var parts = ns_string.split('.'), parent = MYAPP, i; // strip redundant leading global if (parts[0] === "MYAPP") { parts = parts.slice(1); } for (i = 0; i < parts.length; i += 1) { // create a property if it doesn't exist if (typeof parent[parts[i]] === "undefined") { parent[parts[i]] = {}; } parent = parent[parts[i]]; } return parent; };
结合这个通用命名空间函数的,就可以实现代码的模块化:
// assign returned value to a local var var module2 = MYAPP.namespace('MYAPP.modules.module2'); module2 === MYAPP.modules.module2; // true // skip initial `MYAPP` MYAPP.namespace('modules.module51'); // long namespace MYAPP.namespace('once.upon.a.time.there.was.this.long.nested.property');
JavaScirpt库通常是通过命名空间来进行模块化,当我们在代码中使用第三方的库的时候,可以只引入我们代码依赖的模块。
所谓声明依赖关系,就是指在函数或者模块的顶部是声明代码需要依赖哪些模块,这个声明包括创建一个局部变量,并将它们指向你需要的模块:
var myFunction = function () { // dependencies var event = YAHOO.util.Event, dom = YAHOO.util.Dom; // use event and dom variables // for the rest of the function... };
通过声明依赖关系这种模式,会给我们带来很多好处:
下面就看看JavaScript中的代码复用模式。一般来说,通常使用下面的方式来实现代码的复用:
在JavaScript中可以很方便的通过原型来实现继承。
关于原型式继承,ECMA5通过新增Object.create()
方法规范化了原型式继承。这个方法接收两个参数:
看一个使用Object.create()
的例子:
utilsLibC = Object.create(utilsLibA, {
sub: { value: function(){ console.log("sub method from utilsLibC"); } }, mult: { value: function(){ console.log("mult method from utilsLibC"); } }, }) utilsLibC.add(); // add method from utilsLibA utilsLibC.sub(); // sub method from utilsLibC utilsLibC.mult(); // mult method from utilsLibC console.log(utilsLibC.__proto__); // Object {add: (), sub: (), __proto__: Object} console.log(utilsLibC.__proto__.constructor); // function Object() { [native code] }
关于JavaScript继承的更多信息,可以参考关于JavaScript继承的那些事。
有时候可能只需要一个已经存在的对象的一个或两个方法,但是又不想通过继承,来建立额外的父子(parent-child)关系。
这时就可以考虑使用借用方法模式完成一些函数的复用。借用方法模式得益于function的方法call()和apply()。
这种模式一个常见用法就是借用数组方法。
数组拥有有用的方法,那些类数组对象(array-like objects)比如arguments类数组对象(array-like objects)比如arguments没有的方法。所以arguments可以借用数组的方法,比如slice()方法,看一个例子:
function f() { var args = [].slice.call(arguments, 1, 3); return args; } // example f(1, 2, 3, 4, 5, 6); // returns [2,3]
本文主要介绍了JavaScript中常用的编码模式,通过这些模式可以使代码健壮、可读。
主要参考《JavaScript patterns》。
此文出处:
作者:田小计划