模式是解决或者避免一些问题的方案。

在JavaScript中,会用到一些常用的编码模式。下面就列出了一些常用的JavaScript编码模式,有的模式是为了解决特定的问题,有的则是帮助我们避免一些JavaScript中容易出现的错误。

单一var模式

所谓“单一var模式”(Single var pattern)就是指在函数顶部,只使用一个var进行变量声明的模式。例如:

  1. function func() {
  2. var a = 1,
  3. b = 2,
  4. sum = a + b,
  5. myObject = {},
  6. i,
  7. j;
  8. // other code
  9. }

使用这个模式的好处:

  • 在函数顶部展示了所有函数中使用的局部变量
  • 防止变量提升引起的问题

变量提升

JavaScript允许在函数的任意地方声明变量,但是效果都等同于在函数顶部进行声明,这个是所谓的变量提升(Hoisting)。

看一个例子:

  1. var num = 10;
  2. function func() {
  3. alert(num); // undefined
  4. var num = 1;
  5. alert(num); // 1
  6. }
  7. func();

从这个例子可以看到,第一次alert的值并不是10,而是undefined。所以,应该尽量使用“单一var模式”来避免类似的问题。
关于变量提升的细节,请参考我前面一篇JavaScript的执行上下文

for-in循环

在JavaScript中,for-in循环主要用来枚举对象的属性。

但是,由于JavaScript中原型链的存在,一般都会结合hasOwnProperty()来使用for-in循环,从而过滤原型链上的非该对象的属性。

  1. var wilber = {
  2. name: "Wilber",
  3. age: 28,
  4. gender: "male"
  5. };
  6. Object.prototype.printPersonalInfo = function() {
  7. console.log(this.name, "is", this.age, "years old");
  8. };
  9. for(var prop in wilber) {
  10. if(wilber.hasOwnProperty(prop)) {
  11. console.log(prop, ":", wilber[prop]);
  12. }
  13. }

开放的大括号位置

根据开发人员的习惯,开放大括号的位置会有不同的选择,可以和语句放在同一行,也可以放在新的一行:

  1. var total = 10;
  2. if(tatal > 5) {
  3. console.log("bigger than 5");
  4. }
  5. if(tatal > 5)
  6. {
  7. console.log("bigger than 5");
  8. }

两种形式的代码都能实现同样的逻辑,但是,JavaScript允许开发人员省略分号,JavaScript的分号插入机制(semicolon insertion mechanism)会负责加上省略的分号,这时开放大括号的位置不同就可能产生不同的结果。

看一个例子:

  1. function func() {
  2. return
  3. {
  4. name: "Wilber"
  5. };
  6. }
  7. alert(func());
  8. // undefined

之所以得到的结果是undefined就是因为JavaScript的分号插入机制,在return语句之后自动添加了分号。

调整一下开放的大括号的位置就可以避免这个问题:

  1. function func() {
  2. return {
  3. name: "Wilber"
  4. };
  5. }
  6. alert(func());
  7. // [object]

所以,关于开放的大括号位置,建议将开放的大括号放置在前面语句的同一行

强制new模式

JavaScript中,通过new关键字,可以用构造函数来创建对象,例如:

  1. function Person(name, city) {
  2. this.name = name;
  3. this.city = city;
  4. this.getInfo = function() {
  5. console.log(this.name, "lives at", this.city);
  6. }
  7. }
  8. var will = new Person("Will", "Shanghai");
  9. will.getInfo();
  10. // Will lives at Shanghai

但是,如果开发人员忘记了new关键字,那么构造函数中的this将代表全局对象(浏览器中就是window对象),所有的属性将会变成全局对象的属性。

  1. function Person(name, city) {
  2. this.name = name;
  3. this.city = city;
  4. this.getInfo = function() {
  5. console.log(this.name, "lives at", this.city);
  6. }
  7. }
  8. var will = Person("Will", "Shanghai");
  9. console.log(will.name);
  10. // Uncaught TypeError: Cannot read property 'name' of undefined
  11. console.log(window.name);
  12. // Will
  13. console.log(window.city);
  14. // Shanghai
  15. window.getInfo();
  16. // Will lives at Shanghai

所以,为了避免这类问题的方式,首先是从代码规范上下手。建议对于所有的JavaScript构造函数的命名方式都遵循,构造函数使用首字母大写的命名方式
这样当我们看到首字母大写的函数,就要考虑是不是漏掉了new关键字。

自调用构造函数

当然除了规范之外,还可以通过代码的方式来避免上面的问题。

具体的做法就是,在构造函数中检查this是否为构造函数的一个实例,如果不是,构造函数可以通过new关键字进行自调用。

下面就是使用自调用构造函数对上面的例子进行改进:

  1. function Person(name, city) {
  2. if(!(this instanceof Person)) {
  3. return new Person(name, city);
  4. }
  5. this.name = name;
  6. this.city = city;
  7. this.getInfo = function() {
  8. console.log(this.name, "lives at", this.city);
  9. }
  10. }
  11. var will = Person("Will", "Shanghai");
  12. console.log(will.name);
  13. // Will
  14. console.log(will.city);
  15. // Shanghai
  16. will.getInfo();
  17. // Will lives at Shanghai
  18. window.getInfo();
  19. // Uncaught TypeError: window.getInfo is not a function

结合构造函数的命名约定和自调用的构造函数,这下就不用担心漏掉new关键字的情况了。

数组性质检查

当在JavaScript中判断一个对象是不是数组的时候,不能直接使用typeof,因为我们会得到object

在ECMA5中提出了Array.isArray()这个函数,我们可以直接使用来判断一个对象是不是数组类型。

对于不支持ECMA5的环境,我们可以通过下面的方式自己实现Array.isArray()这个函数。

  1. if(typeof Array.isArray === "undefined") {
  2. Array.isArray = function(arg){
  3. return Object.prototype.toString.call(arg) === "[object Array]";
  4. };
  5. }
  6. var arr = [];
  7. console.log(Array.isArray(arr));
  8. // true

立即执行函数

立即执行函数是JavaScript中非常常用的一种模式,形式如下:

  1. (function() {
  2. // other code
  3. }());

通过这个模式可以提供一个局部的作用域,所以函数代码都会在局部作用域中执行,不会污染其他作用域。

现在的很多JavaScript库都直接使用了这种模式,例如JQuery、underscore等等。

立即执行函数的参数

关于立即执行函数另外一点需要注意的地方就是立即执行函数的参数。

我们可以像正常的函数调用一样进行参数传递:

  1. (function(name, city) {
  2. console.log(name, "lives at", city);
  3. }("Wilber", "Shanghai"));
  4. // Wilber lives at Shanghai

在立即执行函数中,是可以访问外部作用域的(当然包括全局对象),例如:

  1. var name = "Wilber";
  2. var city = "Shanghai";
  3. (function() {
  4. console.log(name, "lives at", city);
  5. }());
  6. // Wilber lives at Shanghai

但是,如果立即执行函数需要访问全局对象,常用的模式就是将全局对象以参数的方式传递给立即执行函数

  1. var name = "Wilber";
  2. var city = "Shanghai";
  3. (function(global) {
  4. console.log(global.name, "lives at", global.city);
  5. }(this));
  6. // Wilber lives at Shanghai

这样做的好处就是,在立即执行函数中访问全局变量的属性的时候就不用进行作用域链查找了,关于更多JavaScript作用域链的内容,可以参考理解JavaScript的作用域链

初始化时分支

初始化时分支(Init-time Branching)是一种常用的优化模式,就是说当某个条件在整个程序声明周期内都不会发生改变的时候,不用每次都对条件进行判断,仅仅一次判断就足够了。

这里最常见的例子就是对浏览器的检测,在下面的例子中,每次使用utils.addListener1属性的时候都要进行浏览器判断,效率比较低下:

  1. var utils = {
  2.     addListener: function(el, type, fn) {
  3.         if (typeof window.addEventListener === 'function') {
  4.             el.addEventListener(type, fn, false);
  5.         } else if (typeof document.attachEvent === 'function') { // IE
  6.             el.attachEvent('on' + type, fn);
  7.         } else { // older browsers
  8.             el['on' + type] = fn;
  9.         }
  10.     },
  11.     removeListener: function(el, type, fn) {
  12.         // pretty much the same...
  13.     }
  14. };

所以,根据初始化时分支模式,可以在脚本初始化的时候进行一次浏览器检测,这样在以后使用utils的时候就不必进行浏览器检测了:

  1. // the interface
  2. var utils = {
  3.     addListener: null,
  4.     removeListener: null
  5. };
  6. // the implementation
  7. if (typeof window.addEventListener === 'function') {
  8.     utils.addListener = function(el, type, fn) {
  9.         el.addEventListener(type, fn, false);
  10.     };
  11.     utils.removeListener = function(el, type, fn) {
  12.         el.removeEventListener(type, fn, false);
  13.     };
  14. } else if (typeof document.attachEvent === 'function') { // IE
  15.     utils.addListener = function(el, type, fn) {
  16.         el.attachEvent('on' + type, fn);
  17.     };
  18.     utils.removeListener = function(el, type, fn) {
  19.         el.detachEvent('on' + type, fn);
  20.     };
  21. } else { // older browsers
  22.     utils.addListener = function(el, type, fn) {
  23.         el['on' + type] = fn;
  24.     };
  25.     utils.removeListener = function(el, type, fn) {
  26.         el['on' + type] = null;
  27.     };
  28. }

命名空间模式

JavaScript代码中,过多的全局变量经常会引发一些问题,比如命名冲突。

结合命名空间模式就可以一定程度上减少代码中全局变量的个数。

下面就看一个通用命名空间函数的实现:

  1. var MYAPP = MYAPP || {};
  2. MYAPP.namespace = function (ns_string) {
  3. var parts = ns_string.split('.'),
  4. parent = MYAPP,
  5. i;
  6. // strip redundant leading global
  7. if (parts[0] === "MYAPP") {
  8. parts = parts.slice(1);
  9. }
  10. for (i = 0; i < parts.length; i += 1) {
  11. // create a property if it doesn't exist
  12. if (typeof parent[parts[i]] === "undefined") {
  13. parent[parts[i]] = {};
  14. }
  15. parent = parent[parts[i]];
  16. }
  17. return parent;
  18. };

结合这个通用命名空间函数的,就可以实现代码的模块化:

  1. // assign returned value to a local var
  2. var module2 = MYAPP.namespace('MYAPP.modules.module2');
  3. module2 === MYAPP.modules.module2; // true
  4. // skip initial `MYAPP`
  5. MYAPP.namespace('modules.module51');
  6. // long namespace
  7. MYAPP.namespace('once.upon.a.time.there.was.this.long.nested.property');

声明依赖关系

JavaScirpt库通常是通过命名空间来进行模块化,当我们在代码中使用第三方的库的时候,可以只引入我们代码依赖的模块。

所谓声明依赖关系,就是指在函数或者模块的顶部是声明代码需要依赖哪些模块,这个声明包括创建一个局部变量,并将它们指向你需要的模块:

  1. var myFunction = function () {
  2. // dependencies
  3. var event = YAHOO.util.Event,
  4. dom = YAHOO.util.Dom;
  5. // use event and dom variables
  6. // for the rest of the function...
  7. };

通过声明依赖关系这种模式,会给我们带来很多好处:

  • 明确的依赖声明可以向你的代码的使用者表明这些特殊的脚本文件需要被确保包含进页面
  • 数头部的声明解,让发现和处理依赖关系更加简单
  • 局部变量(比如:dom)通常比使用全局变量(比如:YAHOO)快,比访问全局对象的属性(比如:YAHOO.util.Do)更快,可以得到更好的性能,全局符号只会在函数中出现一次,然后就可以使用局部变量,后者速度更快。
  • 压缩工具比如YUICompressor 和 Google Closure compiler会重命名局部变量,产生更小的体积的代码,但从来不会重命名全局变量,因为那样是不安全的

代码复用模式

下面就看看JavaScript中的代码复用模式。一般来说,通常使用下面的方式来实现代码的复用:

  • 继承
  • 借用方法

继承

在JavaScript中可以很方便的通过原型来实现继承。

关于原型式继承,ECMA5通过新增Object.create()方法规范化了原型式继承。这个方法接收两个参数:

  • 一个用作新对象原型的对象
  • 一个为新对象定义额外属性的对象(可选的)

看一个使用Object.create()的例子:

  1. utilsLibC = Object.create(utilsLibA, {
  2. sub: {
  3. value: function(){
  4. console.log("sub method from utilsLibC");
  5. }
  6. },
  7. mult: {
  8. value: function(){
  9. console.log("mult method from utilsLibC");
  10. }
  11. },
  12. })
  13. utilsLibC.add();
  14. // add method from utilsLibA
  15. utilsLibC.sub();
  16. // sub method from utilsLibC
  17. utilsLibC.mult();
  18. // mult method from utilsLibC
  19. console.log(utilsLibC.__proto__);
  20. // Object {add: (), sub: (), __proto__: Object}
  21. console.log(utilsLibC.__proto__.constructor);
  22. // function Object() { [native code] }

关于JavaScript继承的更多信息,可以参考关于JavaScript继承的那些事

借用方法

有时候可能只需要一个已经存在的对象的一个或两个方法,但是又不想通过继承,来建立额外的父子(parent-child)关系。

这时就可以考虑使用借用方法模式完成一些函数的复用。借用方法模式得益于function的方法call()和apply()。

这种模式一个常见用法就是借用数组方法。
数组拥有有用的方法,那些类数组对象(array-like objects)比如arguments类数组对象(array-like objects)比如arguments没有的方法。所以arguments可以借用数组的方法,比如slice()方法,看一个例子:

  1. function f() {
  2. var args = [].slice.call(arguments, 1, 3);
  3. return args;
  4. }
  5. // example
  6. f(1, 2, 3, 4, 5, 6); // returns [2,3]

总结

本文主要介绍了JavaScript中常用的编码模式,通过这些模式可以使代码健壮、可读。

主要参考《JavaScript patterns》。

此文出处:

作者:田小计划

本文版权归作者和博客园共有。
 

(转)常用的js设计模式的更多相关文章

  1. 常用的Javascript设计模式

    <parctical common lisp>的作者曾说,如果你需要一种模式,那一定是哪里出了问题.他所说的问题是指因为语言的天生缺陷,不得不去寻求和总结一种通用的解决方案. 不管是弱类型 ...

  2. 前端笔记之JavaScript面向对象(三)初识ES6&underscore.js&EChart.js&设计模式&贪吃蛇开发

    一.ES6语法 ES6中对数组新增了几个函数:map().filter().reduce() ES5新增的forEach(). 都是一些语法糖. 1.1 forEach()遍历数组 forEach() ...

  3. js设计模式总结1

    js设计模式有很多种,知道不代表会用,更不代表理解,为了更好的理解每个设计模式,对每个设计模式进行总结,以后只要看到总结,就能知道该设计模式的作用,以及模式存在的优缺点,使用范围. 本文主要参考张容铭 ...

  4. JS设计模式(一)

    刚入职时,看过一段时间的设计模式,似懂非懂.不知不觉过去七个月了,对JS的理解更深刻了,数据结构与算法的基础也基本上算是过了一遍了,接下来要把设计模式搞定,然后不再深层次研究JS了,而是学习前端自动化 ...

  5. JS表单验证-12个常用的JS表单验证

    JS表单验证-12个常用的JS表单验证 最近有个项目用到了表单验证,小编在项目完结后的这段时间把常用的JS表单验证demo整理了一下,和大家一起分享~~~ 1. 长度限制 <p>1. 长度 ...

  6. 几种常用的JS类定义方法

    几种常用的JS类定义方法   // 方法1 对象直接量var obj1 = {    v1 : "",    get_v1 : function() {        return ...

  7. 封装常用的js(Base.js)——【01】理解库,获取节点,连缀,

    封装常用的js(Base.js)——[01]理解库,获取节点,连缀,  youjobit07 2014-10-10 15:32:59 前言:       现如今有太多优秀的开源javascript库, ...

  8. js设计模式(12)---职责链模式

    0.前言 老实讲,看设计模式真得很痛苦,一则阅读过的代码太少:二则从来或者从没意识到使用过这些东西.所以我采用了看书(<js设计模式>)和阅读博客(大叔.alloyteam.聂微东)相结合 ...

  9. JS设计模式——5.单体模式

    JS设计模式——5.单体模式 http://www.cnblogs.com/JChen666/p/3610585.html   单体模式的优势 用了这么久的单体模式,竟全然不知!用它具体有哪些好处呢? ...

随机推荐

  1. Github排行榜

    http://githubranking.com/ 中国区开发者排行榜: http://githubrank.com/ 也可以在官网查询: https://github.com/search?q=st ...

  2. sqlserver2008使用设置sa用户登录步骤

    1.打开sql server 2008,使用windows身份验证. 2.成功登录后,点击安全性->登录名,“sa”右键选择属性,设置密码,勾选“强制密码实施策略”. 3.然后选择属性页下的“状 ...

  3. 构造 hihocoder 1257 Snake Carpet (15北京I)

    题目传送门 题意:贪吃蛇,要求长度奇数的蛇转弯次数为正奇数,长度偶数转弯次数为正偶数,且组成矩形.(北大出的题咋都和矩形相关!!!) 分析:构造找规律,想到就简单了.可以构造 宽:(n + 1) / ...

  4. WCF 超时情形

    在做WCF开发时,会经常碰到超时的情况,总结了一下,主要是由一下原因引起: 1.客户端没有正确地Close. 确保每次客户端调用完毕之后,就要调用Close,保证连接数. 另外,服务端配置最大连接数: ...

  5. HDU2824 The Euler function(欧拉函数)

    题目求φ(a)+φ(a+1)+...+φ(b-1)+φ(b). 用欧拉筛选法O(n)计算出n以内的φ值,存个前缀和即可. φ(p)=p-1(p是质数),小于这个质数且与其互质的个数就是p-1: φ(p ...

  6. ural 1434. Buses in Vasyuki

    1434. Buses in Vasyuki Time limit: 3.0 secondMemory limit: 64 MB The Vasyuki University is holding a ...

  7. quick 关于触摸的问题

    以前遇到一个问题就是,如果触摸层不在最后,会导致触摸失效.这是由于下面添加的层挡住了触摸层,而后添加的层会位于上面,默认是不可点击,点击不可穿透的.所以我们必须将触摸层放置到最上面. Logic.lu ...

  8. TYVJ P1067 合唱队形 Label:上升子序列?

    背景 NOIP2004 提高组 第三道 描述     N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学排成合唱队形. 合唱队形是指这样的一种队形:设K位同学从左到右依次编号 ...

  9. Android环境搭建要点

    1.JDK环境变量配置 在环境变量的path变量中加入jdk安装目录的bin路径字符串(C:\Program Files\Java\jdk1.8.0\bin). 配置好后,在命令提示符界面输入&quo ...

  10. redis_查找命令

    1:文件名查找 find -name "filename" filename可以用通配符 2:文件内行数查找 在vi模式下 ,刚进入vi 输入:128(linenum)   即能跳 ...