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

1 编码风格

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

1.1 缩进

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

1.2 空格

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

1.3 空行

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

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

1.4 命名约定

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

1.4 注释

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

2 变量

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

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

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

1
2
3
4
function obj() {
name = "aotu";
return name;
}

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

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

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

1
var a = ( b = 0 );

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

2.2 在函数顶部声明变量

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

2.3 使用单一var模式

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

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

2.4 单全局变量方式

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

3 UI松耦合

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

  • 将javascript从css中抽离,如避免使用css表达式
  • 将csst从javascrip中抽离,如避免使用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中,如

1
2
3
4
5
try {
someMethod();
} catch (ex) {
catchError(ex);
}

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

4.3.2 throw

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

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

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

1
2
3
4
5
throw {
name: "myErrorType",
message: "arguments must be a DOM element",
errorMethod: errorMethod
}

5 创建对象

5.1 对象字面量

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

  • 对象包装在大括号中
  • 逗号分隔属性和方法
  • 用冒号分隔属性名称和属性的值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var obj = {
    name: "aotu",
    job: "farmer",
    getName: function () {
    return this.name;
    }
    }
    //调用方式
    obj.getName();
实现私有属性

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

1
2
3
4
5
6
7
8
9
10
11
12
13
var obj;
(function () {
//这样就能实现私有成员
var name = "aotu",
job = "farmer";
obj = {
getName: function () {
return name;
}
}
}())

更优雅的写法

1
2
3
4
5
6
7
8
9
10
var obj = (function () {
var name = "aotu",
job = "farmer";
return {
getName: function () {
return name;
}
}
}());

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 构造函数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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指向了全局对象,可能会发生逻辑错误或者意外,来看下面执行的结果

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

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

1
2
3
4
5
6
7
8
9
10
11
12
function Obj() {
if(!(this instanceof Obj)){
return new Obj();
}
this.name = "aotu";
this.age = 25;
this.getName = function () {
console.log(this.name);
}
}

再看执行的结果

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

静态成员

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

1
2
3
4
5
6
7
8
9
10
11
12
13
//构造函数
function Obj() {}
//添加静态方法
Obj.getAge = function () {
console.log(25);
}
//注意这里的调用方式
Obj.getAge(); //25
//如果使用实例对象调用
obj.getAge(); //Object #<Obj> has no method 'getAge'

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

私有属性与方法

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
}
}

构造函数存在的问题

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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,原型对象包含一个指向构造函数的指针,这个指针指向一个可以由特定类型的所有实例共享的属性和方法,所以使用原型对象可以让所有对象实例共享它的属性和方法,来看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

由此可见obj1 和 obj2 访问的是同一个getName函数

更好的写法

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

1
2
3
4
5
6
7
8
9
function Obj() {}
Obj.prototype = {
name: "aotu",
age: 25,
getName: function () {
return this.name;
}
}

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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.当我们重写整个原型的时候如果先创建了实例,就会切断构造函数与原型之间的联系,因为实例的指针仅仅指向原型,而不是构造函数,在实际的操作过程中,应该尽量避免这种错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Obj() { }
var obj = new Obj();
Obj.prototype = {
constructor: Obj,
name: "aotu",
age: 25,
getName: function () {
return this.name;
}
}
obj.getName(); //error

组合使用二者

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 模块模式

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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来实例化,我们知道每个实例在内存里都是一份拷贝。如何解决这个问题呢?…我们可以采用一个匿名闭包来完美的解决这个问题。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
var testModule =(function () {
var my = {},
testNode = document.getElementById("test");
my.setHtml = function(txt) {
testNode.innerHTML = txt;
}
return my;
} ())
//调用方式
testModule.setHtml("Hello");

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

1
2
3
4
5
6
7
8
9
var testModule =(function (my) {
var testNode = document.getElementById("test");
my.setHtml = function(txt) {
testNode.innerHTML = txt;
}
return my;
} (testModule || {}))

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

1
2
3
4
5
6
7
8
9
10
11
12
var testModule =(function () {
var testNode = document.getElementById("test"),
setHtml = function(txt) {
testNode.innerHTML = txt;
};
//设置公共调用方法
return {
setHtml: setHtml
}
} ())

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

1
2
3
4
5
6
7
8
9
10
11
var MyTitle = React.createClass({
getDefaultProps : function () {
return {
title : 'Hello World'
};
},
render: function() {
return <h1> {this.props.title} </h1>;
}
});

Vue:

1
2
3
4
5
6
7
8
9
10
11
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系列

感谢您的阅读,本文由 凹凸实验室 版权所有。如若转载,请注明出处:凹凸实验室(https://aotu.io/notes/2016/03/31/readable/