提升代码的可读性系列(一)--基础篇

作者:自成
原文地址:凹凸实验室(http://aotu.io/notes/2016/03/31/readable/)

提升代码的可读性系列(一)--基础篇_第1张图片
提升代码的可读性系列(一)--基础篇

编程是一门艺术活,好的代码应该就像住的房子一样,有整体的框架,有门,有窗户,相互独立又完美组合。你觉得门不够结实,就拆下来换个实心的;你觉得窗户不够明亮就换个全玻璃的,总之对房子的其他部位没有任何影响。所以说每一个程序员都应该有一颗设计师的心。本文主要从编码变量处理错误对象等基础方面进行简单的探讨,希望能对大家的工作有所帮助。

1 编码风格

老生常谈,我们先从最基础的编码说起吧!好的编码规范不仅仅能够提升代码的可读性与可维护性,提高团队的工作效率,也能够避开一些低级的错误,减少bug的隐患,提升程序员的自我修养。编码虽小,但却是万丈高楼的基础,对于编写清晰连贯的代码来说,每一个字符都是非常重要的。以下部分编码规范参考自凹凸实验室。

1.1 缩进

通常使用四个空格进行代码缩进,有些也用tab来缩进,这主要根据团队的风格跟个人喜好。

1.2 空格

  • 左括号与类名之间一个空格
  • 冒号与属性值之间一个空格
  • 操作符前后
  • 匿名函数表达式之后等

1.3 空行

这是一个容易被大家忽略的点,但它所带来的效果是毋庸置疑的!通常一段代码的语义和另一段代码不相关,就应该用空行隔开,避免一大段的代码揉在一起,比如:

  • 在方法之间;
  • 方法中的局部变量和第一条语句之间;
  • 注释之前;
  • 方法内的逻辑片段之间。

1.4 命名约定

有一位大师曾说过,计算机科学只存在两个难题:缓存命名。由此可见命名不仅是一门科学,也是一门技术。通常情况下,变量与函数一般使用驼峰大小写命名法,其中为了区分变量与函数,变量命名前缀应当是名词,函数前缀应当是动词,也就是说我们应当让命名承载一定的含义,因此要避免使用没有意义的命名。

1.4 注释

通常我们在编写完一段代码的短时间内,会清楚这段代码的工作原理。但是当过一段时间再次回到代码中,可能会花很长的时间才能读懂。这种情况下,编写注释就变得尤为重要了。

2 变量

首先说一说全局变量存在哪些的问题吧!命名冲突测试难度大深耦合等等。在创建变量的时候,我们应该注意以下几个方面:

2.1 避免隐性的创建全局变量

什么是隐性的全局变量呢?官方的回答是:任何变量,如果未经声明,就为全局对象所有。啥意思呢?其实就是没有加var声明的,请看下面的例子:

function obj() {
    name = "aotu";
    return name;
}

另外一种容易创建隐形全局变量的情况就是var声明的链式赋值,如下代码所示:

function person() {
    var a = b = 1;
}

以上这段代码的执行结果是:a是局部变量,b是全局变量,主要原因是从右至左的操作符优先级,它实际执行的结果等同于:

var a = ( b = 0 );

综上所述,隐式全局变量并不是我们平时用var声明的变量,而是全局对象的属性,既然是属性,那么它可以通过delete操作符删除,但变量不可以,且在ES5 strict以上会抛出错误。

2.2 在函数顶部声明变量

在javascript中,声明变量有一个“提升”的概念,即无论在函数哪里声明,效果都等同于在函数顶部进行声明。所以我们统一把变量在函数顶部声明,既有利于可读性与可维护行,也不易出错。

2.3 使用单一var模式

var a = 1,
    b = 1,
    c = 1;

这样声明的变量不仅可读性好,而且可以防止变量在定义前就被使用的逻辑错误,且编码更少。

2.4 单全局变量方式

虽然全局变量的容易污染命名空间,但有些功能的需要,难以避免使用,关键是我们应该做到避免全局变量超出我们的掌控,最佳的方法是依赖尽可能少的全局变量。我们可以使用单全局变量的方式来开启我们的项目,这种方式在许多的javascript类库中都有这样使用。如jQuery,它定义了两个全局变量$jQuery

3 UI松耦合

什么是松耦合?当修改一个组件的逻辑,而对另一个组件没有影响,就说这叫松耦合。通常一个大型的web应用,都是由多人共同开发维护,这时候松耦合显得至关重要,假如你修改了某一处的代码而影响了团队其他人的功能,这是非常不友好的。通常我们主要注意以下几点:

  • 将javascript从css中抽离,如:避免使用css表达式。
  • 将css从javascript中抽离,如:避免使用javascript直接修改css,最佳的方法是操作css的className;
  • 将javascript从HTML中抽离,如:避免将函数直接嵌入到html执行,我们应该尽量做到将所有的js代码都放入外置文件中,确保html中不会有内联的js代码。
  • 将html从javascript中抽离,如避免在js中拼接html结构,我们可以用模板引擎,也可以使用Vue、React等。

4 错误处理

4.1 为什么要抛出错误?

在javascript开发中,总是会悄无声息的出现一些超出我们预期的,携带的信息稀少的,隐晦含糊的bug,让我们措手不及,大大增加了我们调试错误、定位错误的难度,影响开发效率。假设错误中包含这样的信息:“由于某某情况,导致某某函数执行错误”,那么,是不是马上就可以开始调试,而不用花大量的时候去定位错误?

4.2 何时抛出错误?

主要是辨识代码中哪些部分,在特定的情况下最后可能导致错误。这里的错误,通常都是我们在思考的过程中的一些可预期的错误。

4.3 怎样抛出错误?

4.3.1 使用try-catch

将可能引发错误的代码放在try块中,处理错误的代码放在catch中,如:

try {
    someMethod();
} catch (ex) {
    catchError(ex);
}

也可以增加一个finally块,这里需注意的是:finally块中的代码块不管是否有错误发生,最后都会被执行。

4.3.2 throw

当我们能清晰的捕捉到错误的时候,最好的做法就是抛出这个错误,避免在不经意的时候又遇到它,让大家尴尬。这里需注意的是,当遇到throw操作符时,代码会立即停止执行:

throw new Error("method(): descdescdesc");

也可以自定义一个错误类型。总之,就是尽可能用最短的字符描述清楚:

throw { 
    name: "myErrorType",
    message: "arguments must be a DOM element",
    errorMethod: errorMethod
}

5 创建对象

5.1 对象字面量

所谓的对象字面量其实就是我们通常所说的键值对哈希表,这种方式不仅富有表现力,可读性好,且字符更短,没有作用域解析。它的语法规则如下:

  • 对象包装在大括号中
  • 逗号分隔属性和方法
  • 用冒号分隔属性名称和属性的值
var obj = {
    name: "aotu",
    job: "farmer",
    getName: function () {
        return this.name;
    }
}

//调用方式
obj.getName();

实现私有属性

以上例子的namejob属性都是可直接访问的。有些时候我们可能想实现一些私有的属性,然后提供一个公有的接口来对外访问。虽然javascript并没有特殊的语法来表示私有、公共属性和方法,但是可以通过匿名闭包来实现,内部的任意变量都不会暴露,来看以下代码:

var obj;
   
(function () {
    //这样就能实现私有成员
    var name = "aotu",
        job = "farmer";
       
    obj = {
        getName: function () {
            return name;
        }
    }
}())

更优雅的写法:

var obj = (function () {
    var name = "aotu",
        job = "farmer";

    return {
        getName: function () {
            return name;
        }
    }
}());

这种写法也是模块模式的基础框架,后续会有详细介绍。

熟悉了这种模式之后它还有很多种玩法,比如可以像jQuery这样链式调用:“$(‘#id’).siblings(‘ul’).find(“li”).addClass();

var obj = {
    num: 0,
    add: function (arg) {
        this.num += arg;
        return this;
    },
    red: function (arg) {
        this.num -= arg;
        return this;
    },
    setTotal: function () {
        console.log(this.num);
    }
};

//调用方式
obj.add(5).red(2).setTotal(); //3

5.2 构造函数

我们先来看看构造函数的基础框架:

function Obj() {
    //公有属性
    this.name = "aotu";
    this.job = "farmer";
       
    //公有方法
    this.getName = function () {
        console.log(this.name);
    } 
}

//调用方式
var obj = new Obj();
obj.getName();

在使用new方式实例化构造函数,通常会经历以下几个步骤:

  • 创建一个对象并且this变量引用了该对象,且继承了该对象的原型。
  • 属性和方法被加入到this引用的对象中。
  • 隐式的返回新对象。

忘记使用NEW的情况

当然,我们有时候会忘记使用new操作符实例化的情况,然而这并不会导致语法错误,但构造函数的this指向了全局对象,可能会发生逻辑错误或者意外,来看下面执行的结果:

var obj = Obj();
obj.getName(); //Cannot read property 'getInfo' of undefined

为了避免这种意外发生,我们也可以在构造函数中检查this是否为构造函数的一个实例,强制使用new操作符,继续看下面的例子:

function Obj() {
    if(!(this instanceof Obj)){
        return new Obj();
    }

    this.name = "aotu";
    this.age = 25;

    this.getName = function () {
        console.log(this.name);
    } 
}

再看执行的结果:

var obj = Obj();
obj.getName(); //"aotu"

静态成员

在javascript中,并没有特殊的语法来表示静态成员,但我们可以为构造函数添加属性这种方式来实现这种语法,请看下面的例子:

//构造函数
function Obj() {}

//添加静态方法
Obj.getAge = function () {
    console.log(25); 
}

//注意这里的调用方式
Obj.getAge(); //25

//如果使用实例对象调用
obj.getAge(); //Object # has no method 'getAge'

这里大家需要注意调用静态方法的方式,若以实例对象调用一个静态方法是无法正常运行的,反之同理。

私有属性与方法

在以上例子中,构造函数的属性与方法都属于公有方法,我们也可以给构造函数添加私有方法与私有属性:

function Obj() {
    this.name = "auto";
    this.age = 25;
     
    //私有属性
    var address = "sz",
        that = this;
      
    //私有方法
    function getAddress() {
        console.log(that.address);
    }
       
    this.getName = function () {
        console.log(this.name);
    } 
}

构造函数存在的问题

构造函数的主要问题就是当多次实例化这个构造函数的时候,每个方法都会重新创建一遍,这样就等于在内存中的拷贝。解决问题的第一种思路,就是将函数中的方法通过函数定义转移到函数外面,并将指针传递给构造函数,来看下面的例子:

function Obj() {
    this.name = "aotu";
    this.age = 25;

    //将指针赋给getName
    this.getName = getName;
}
  
function getName () {
    console.log(this.name);
}   

var obj1 = new Obj();
var obj2 = new Obj()

虽然也解决了以上的问题,但并没有达到封装的效果。接下来我们引入原型prototype的概念。

5.3 原型模式

每一个构造函数都有一个原型prototype,原型对象包含一个指向构造函数的指针,这个指针指向一个可以由特定类型的所有实例共享的属性和方法,所以使用原型对象可以让所有对象实例共享它的属性和方法,来看下面的例子:

function Obj() {}

Obj.prototype.name = "aotu";
Obj.prototype.age = 25;
Obj.prototype.getName = function () {
    console.log(this.name);
}

//调用方式

var obj1 = new Obj();
obj1.getName() //"aotu"

var obj2 = new Obj();
obj2.getName() //"aotu"

alert(obj1.getName == obj2.getName); //true

由此可见obj1obj2访问的是同一个getName函数。

更好的写法

我们可以将所有的原型都写在一个对象字面量里,这样整个代码看起来更加简洁清晰,继续往下看:

function Obj() {}

Obj.prototype = {
    name: "aotu",
    age: 25,
    getName: function () {
        return this.name;
    }
}

使用字面量的方式需注意的问题

在使用这种字面量的方式的时候,需注意以下两点:

1.将prototype设置为等于一个对象字面量形式创建的对象,它本质上已经完全重写了默认的prototype对象,最终结果虽然相同,但是其constructor属性不再指向该对象。

constructor是个什么鬼?在默认情况下,所有原型对象都会自动获得一个constructor,它指向prototype属性所在函数的指针,换句话说这个constructor就是指这个构造函数。以上代码执行结果如下所示:

var obj= new Obj();
alert(obj.cnstructor == Obj) //false;

我们可以在重写prototype的时候给constructor指定构造函数,接着往下看:

function Obj(){}

Obj.prototype = {
    constructor: Obj,
    name: "aotu",
    age: 25,
    getName: function () {
        return this.name;
    }
}

var obj= new Obj();
alert(obj.cnstructor == Obj) //true;

2.当我们重写整个原型的时候如果先创建了实例,就会切断构造函数与原型之间的联系,因为实例的指针仅仅指向原型,而不是构造函数,在实际的操作过程中,应该尽量避免这种错误。

function Obj() { }

var obj = new Obj();

Obj.prototype = {
    constructor: Obj,
    name: "aotu",
    age: 25,
    getName: function () {
        return this.name;
    }
}

obj.getName();  //error

组合使用二者

在我们的具体应用中,通常比较多的是组合使用构造函数模式与原型模式。构造函数用于定义实例属性,原型用于定于共享的属性和方法,这样能够最大限度的节省内存。以下是一个基本的组合使用构造函数与原型的例子:

function Obj(){
    if(!(this instanceof Obj)){
        return new Obj();
    }

    this.name = "aotu";
    this.age = 25;
}

Obj.prototype = {
    constructor: Obj,
    getName: function () {
        return this.name;
    }
}

var obj = Obj();
obj.getName();

5.4 模块模式

模块模式是一种非常通用的模式,也是使用频率比较高的模式,它具有以下几个特点:

  • 模块化
  • 可复用
  • 松耦合
  • 区分了私有方法与公共方法

我们先看看模块模式的基础框架:

var testModule = function () {
    //私有成员
    var testNode = document.getElementById("test");

    //也可在此定义私有方法
    function privateMethod() {
        console.log("this is Private method!");
    }

    return {
        //对外公开的方法
        setHtml: function (txt) {
            testNode.innerHTML = txt;
        }
    }
}

//调用方式
var testModule = new testModule();
testModule.setHtml("Hello");

这种方式看起来比较清晰、简洁。但就是每次调用的时候都需要用new来实例化。我们知道每个实例在内存里都是一份拷贝,如何解决这个问题呢?我们可以采用一个匿名闭包来完美的解决这个问题。

(function () {
   //将所有的变量和function放在这里声明,其作用域也只能在这个匿名闭包里面,既达到了封装的目的,也能防止命名冲突
}())

接下来我们将它应用到具体的实例中,以下就是一个基本的Module模式:

var testModule =(function () {
    var my = {},
        testNode = document.getElementById("test");
     
    my.setHtml = function(txt) {
        testNode.innerHTML = txt;
    }
    
    return my;
} ())

//调用方式
testModule.setHtml("Hello");

通常在一个大型的项目中,会有多人共同开发一个功能的情况,这个时候我们可以运用这种模式将全局变量当作参数传递,然后通过变量返回,从而达到多人协作的目的。

var testModule =(function (my) {
    var testNode = document.getElementById("test");
     
    my.setHtml = function(txt) {
        testNode.innerHTML = txt;
    }
    
    return my;
} (testModule || {}))

我们也可以通过这个模式将私有的对象或者属性保护起来,然后设置一些公共接口对外访问,继续来看下面的代码:

var testModule =(function () {
    var testNode = document.getElementById("test"),
     
        setHtml = function(txt) {
            testNode.innerHTML = txt;
        };

        //设置公共调用方法
        return {
            setHtml: setHtml
        }
    } ())

以上几种方式仅仅只是一些创建对象的基础,通过灵活运用这些基础,可以变换出传说中各种各样的模式,如迭代器模式、工厂模式、装饰者模式等。对于后续学习其他的技术也是极有帮助的。如React:

var MyTitle = React.createClass({
    getDefaultProps : function () {
        return {
            title : 'Hello World'
        };
    },

    render: function() {
        return 

{this.props.title}

; } });

Vue:

new Vue({
    el: '#app',
    data: {
        message: 'Hello Vue.js!'
    },
    methods: {
        reverseMessage: function () {
            this.message = this.message.split('').reverse().join('')
        }
    }
})

以上就是本期的所有内容,如有错漏,恳请指正,大家共同进步!在下一期中,会继续跟大家探讨更多好玩的东西,敬请期待。

6 参考资料

  • 《编写可维护的JavaScript》[美] Nicholas C. Zakas 著
  • 《JavaScript设计模式》[美] Addy Osmani 著
  • 《JavaScript高级程序设计(第3版)》
  • 博文:深入理解JavaScript系列

你可能感兴趣的:(提升代码的可读性系列(一)--基础篇)