作者:自成
原文地址:凹凸实验室(http://aotu.io/notes/2016/03/31/readable/)
编程是一门艺术活,好的代码应该就像住的房子一样,有整体的框架,有门,有窗户,相互独立又完美组合。你觉得门不够结实,就拆下来换个实心的;你觉得窗户不够明亮就换个全玻璃的,总之对房子的其他部位没有任何影响。所以说每一个程序员都应该有一颗设计师的心。本文主要从编码、变量、处理错误、对象等基础方面进行简单的探讨,希望能对大家的工作有所帮助。
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();
实现私有属性
以上例子的name
、job
属性都是可直接访问的。有些时候我们可能想实现一些私有的属性,然后提供一个公有的接口来对外访问。虽然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 #<Obj> 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
由此可见obj1
和obj2
访问的是同一个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 <h1> {this.props.title} </h1>;
}
});
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系列