《编写可维护的JavaScript》笔记

由于JavaScript语法比较松散,会出现很多坑,代码规范化有助于较少BUG,也方便维护。

一、编程风格

1、for-in: for-in 循环用于遍历对象属性的,它有一个坑:它不仅遍历实例属性,还会遍历从原型继承来的属性,在遍历自定义熟悉时往往会因为意外结果而终止,所以最好(一般编码规范要求必须)使用hasOwnProperty()方法来为for-in循环过滤出实例属性。

2、相等比较: 由于JavaScript具有强制类型转换机制,所以判断相等操作是很容易出错。

a:如果将数字与字符串进行比较,字符串首先会转换为数字,然后进行比较:


console.log(5 == "5"); //true

b:如果布尔值和数字比较,布尔值首先会转换为数字然后进行比较,false->0,true->1。

console.log(1 == true);//true
console.log(0 == false);//true
console.log(2 == true);//false

c.如果其中一个值是对象,另一个不是,则会首先调用对象的valueOf()方法,得到原始类型值再进行比较,如果没有定义valueOf,则调用toString(),之后再进行比较:

var object = {
    toString: function() {
        return "0x19";
    }
}
console.log(object == 25);//true

d.null与undefined这个值判断的时候是相等的:

console.log(null == undefined);//true

由于强制类型转换的原因,所以一般不推荐使用== 与!=,而是使用===,!==,因为后者不会涉及强制转换,2个值类型不一样则肯定不相等.

console.log(5 == "5");//true
console.log(5 === "5");//false

console.log(1 == true);//true
console.log(1 === true);//false

var object = {
    toString: function() {
        return "0x19";
    }
}
console.log(object == 25);//true
console.log(object === 25);//false

console.log(null == undefined);//true
console.log(null === undefined);//false

3、严格模式

//错误的做法,不要全局使用严格模式

"use strick";
function doSomething() {
    //
}

//正确的做法,应该仅限于函数内部使用
function doSomething() {
    "use strick";
    //
}

//多个函数需要严格模式
(function)() {
    "use strick";
    function doSomething() {
        //
    }
    function doSomethingElse() {
        //
    }
}());

二、全局变量

1、单全局变量方式 由于全局变量是直接挂在window对象下面的,全局变量会带来很多问题,比如命名冲突、代码脆弱、难以测试等很多问题,所以一般都建议不要直接使用全局变量。而采用单全局变量等方式,比如JQuery有$,YUI的Y等。

但全局变量的意思是所创建的这个唯一全局对象是独一无二的,不会与内置API冲突,将所有的功能代码都挂在这个全局对象上,从而原来的众多全局变量就变成了这个唯一全局变量的属性了。

function Book(title) {
    this.titie = title;
    this.page = 1;
}

Book.prototype.turnPage = function(direction) {
    this.page  += direction;
};

var Chapter1 = new Book("Introducation to Style Guidelines");
var Chapter2 = new Book("Basic Formatting");
var Chapter3 = new Book("Comments");

以上代码创建了4个全局对象:Book、Chapter1、Chapter2、Chapter3共计4个全局变量。
但全局变量的方式则只会创建一个全局变量:

var MaintainableJs {};

MaintainableJs.Book = function(title) {
    this.title = title;
    this.page = 1;
};

MaintainableJs.Book.prototype.turnpage = function (direction) {
    this.page += direction;
};

MaintainableJs.Chapter1 = new Book("Introducation to Style Guidelines");
MaintainableJs.Chapter2 = new Book("Basic Formatting");
MaintainableJs.Chapter3 = new Book("Comments");

2、命名空间:
使用单全局变量已经大大减少了全局变量的问题,但还是有可能会出现全局污染的问题,大部分第三方的知名库在使用单全局变量的情况下也会使用命名空间的概念,即简单的通过全局对象的单一属性表示的功能性分组,比如G.DB、G.Utils,按功能将命名空间进行分组,可以让但全局变量井然有序。

a.简单粗暴式:

var G = {};
G.DB = {};
G.Utils = {};

b.上面的简单粗暴式有一个问题就是会对单一的全局变量造成侵入式修改,所以改进版如下:

var G = {
    namespace: function(ns) {
        var parts = ns.split("."),
        object = this,
        i,len;

        for(i = 0, len = parts.length; i < len; i++) {
            if(!object[parts[i]]) {
                object[parts[i]] = {};
            }
            object = object[parts[i]];
        }
        return object;
    }
};

G.namespace("G.DB");
G.DB.User.name = "Grey";


G.namespace("G.Utils");
G.Utils.Search = {};

3、模块:
两外一种对单全局变量的扩充是模块,ECMAScript6之前并不支持模块,需要使用YUI,AMD等第三方库,但ECMAScript6添加命名空间与模块的支持.

三、事件处理

看一个例子:

function handleClick(event) {
    var popup = document.getElementById("popup");
    popup.style.left = event.clientX + "px";
    popup.style.top = event.clientY + "px";
    popup.style.className = "reveal";
}

addListenr(element, "click", handleClick);

把握几种规则:

1、隔离应用逻辑,事件处理中调用应用逻辑即可,因为应用逻辑也有可能由其它触发执行。

var MyApp = {
    handleClick: function(event) {
        this.showPopup(event);
    },
    showPopup: function(event) {
        var popup = document.getElementById("popup");
        popup.style.left = event.clientX + "px";
        popup.style.top = event.clientY + "px";
        popup.style.className = "reveal";
    }
};

addListenr(element, "click", function(event) {
    MyApp.handleClick(event)
});

2、不要分发事件对象,事件不要无休止的往下传递,直接传递要使用的参数值即可,让应用处理逻辑与事件无关,仅与参数值有关,也方便了测试。

var MyApp = {
    handleClick: function(event) {
        this.showPopup(event.clientX, event.clientY);
    },
    showPopup: function(x, y) {
        var popup = document.getElementById("popup");
        popup.style.left = x + "px";
        popup.style.top = y + "px";
        popup.style.className = "reveal";
    }
};

addListenr(element, "click", function(event) {
    MyApp.handleClick(event)
});

MyApp.showPopup(10,10)//很方便的测试

四、避免空比较,检测值的正确方式

1、检测原始值

JavaScript有5种原始类型:字符串、数字、布尔值、null、undefined,使用typeof检测值类型:

typeof 字符串 返回"string"
typeof 数字 返回"number"
typeof 布尔值 返回"boolean"
typeof undefined 返回"undefined"

typeof null 返回object,一般不用,而是直接使用=== or !==。
typeof function 返回"function"

2、检测引用值
由于typeof任何对象都是返回object,所以使用instanceof。
eg:

if (data instanceof Date) {
    console.log(date.getFullYear());
}

if (str instanceof RegExp) {
    if (str.test(anotherStr)) {
        console.log("matches");
    }
}

if (value instanceof Error) {
    throw value;
}

instanceof不仅检测构造这个对象的构造器,还会向上检测原型链:

var now = new Date();
console.log(now instanceof Object);//true
console.log(now instanceof Date);//true

跨frame的问题:
由于每个frame都有对象的构造函数一个拷贝,虽然二者定义都一样,但却是2个对象:
将aPersonInFrameA传入到Frame B中,A、B,2个frame都定义了Person

aPersonInFrameA instanceof PersonInFrameA//true
aPersonInFrameA instanceof PersonInFrameB//false

3、检测函数
函数也是引用类型,所以使用instanceof也可以:

function myFun() {}
console.log(myFun instanceof Function)//true

但这个方法不能跨frame使用,因为每个Function都有各自的构造函数,所以使用typeof比较好,返回"function"

console.log(typeof myFun === "function")//true

4、检测数组
ECMAScript5都已经引入了专门的isArray方法。

a: in

使用in,使用null和undefined都会有其特殊性,都容易产生错误,而in仅仅会简单判断属性是否存在,并不会去读取属性值,如果属性存在,或者继承的对象存在该属性,都会返回true。

var object = {
    count: 0,
    related: null,
};

if ("count" in object) {
    //
}

b: hasOwnProperty()

如果不想检测继承链的属性,则可以使用hasOwnProperty()方法

if(object.hasOwnPropery("related")) {
    //非Dom对象
}

if ("hasOwnProperty" in object && object.hasOwnProperty("related")) {
    //Dom对象
}

五、保存配置数据独立

六、抛出自定义错误
有些代码必须依赖正确的输入值,如果不抛出异常,将很难发现错误去调试,所以任何语言中抛出异常都是一个必要的内容,

ECMA-262定义了7种错误:

Error:错误的基本类型,JavaScript不会直接抛出该错误。
EvalError:通过evel函数执行代码发生的错误。
RangeError: 一个数字超过边界,eg:创建一个长度为负数的数组。
ReferenceError:很常见的一种错误,期望的对象不存在,在null上调用一个函数。
SyntaxError:evel函数传递的代码中有语法错误。
TypeError:变量不是期望的类型,如new 10。
URIError: 给encodeURI()、encodeURIComponent()、decodeURI()、decodeURIComponent()等跟URI相关的函数传递非法URI时。

自定义Error:

function MyError(message) {
    this.message = message;
}
MyError.prototype = new Error();//从Error继承

throw new MyError("My Custom Error");

try {
    
} catch(ex) {
    if (ex instanceof MyError) {
        //get my error
    } else {
        //其它错误
    }
}

七、不是自己的对象不要动

如果不是自己创建的对象,不要去修改:

原生对象:Object、Array等。
DOM对象:eg: document
浏览器对象模型(BOM)对象:eg:window
类库的对象
这些都是项目执行环境的一部分,它们已经存在,不要修改。

对已经存在的这些对象,做到

不覆盖方法
不新增方法
不删除方法

1、不覆盖不是自己的对象的方法

//错误的做法,不好的示例
document._originalGetElementById = document.getElementById;
document.getElementById = function(id) {
    if (id == "window") {
        return window;
    } else {
        return document._originalGetElementById(id);
    }
};

2、不为非自己的对象新增方法

//错误的做法,不好的示例 - 在DOM对象上新增了方法
document.sayImAwesome = function() {
    alert("You're awesome.");
};

//错误的做法,不好的示例 - 在原生对象上新增了方法
Array.prototype.reverseSort = function() {
    return this.sort().reverse();
}

//错误的做法,不好的示例 - 在库对象上新增了方法
$.doSomething = funuction() {
    //
}

一般类库都会提供一种插件机制,允许为代码库安全的新增一些功能。

3、不删除方法

//错误的做法
document.getElementById = null;

//如果方法是在对象的实例上定义的,也可以使用delete删除
var person = {
    name: "Grey";
};
delete person.name;
console.log(person.name);//undefined

//如果方法定义在prototype属性上,delete是不起作用的.
delete document.getElementById;
console.log(document.getElementById("myElement"));//work 

总之,不要去删除已存在对象的方法,这是机器糟糕的实践。

那如果现有对象的方法并不能满足要求的时候,如何做呢?请使用继承、扩展来实现。
但是由于JavaScript的一些限制 ,所以不能从DOM或者DOM对象继承,由于数组索引和length属性的复杂关系,继承自己Array也不能正常工作。

八、继承的实现

1、基于对象的继承

var person = {
    name: "Grey",
    sayName: function() {
        alert(this.name);
    }
};
var myPerson = Object.create(person);
myPerson.sayName();//弹出"Grey"

myPerson.sayName = function() {
    alert("Anonymous");
}
myPerson.sayName();//弹出"Anonymous"
person.sayName();//弹出"Grey"

Object.create方法还可以指定第二个参数,该参数对象中的属性和方法将添加到新的对象中。

var myPerson = Object.creat(person, {
    name: {
        value: "Jack";
    }
})
myPerson.sayName;//弹出"Jack"
person.sayName;//弹出Grey

使用这种方法创建的一个新对象,该新对象完全可以随意修改,因为作为新对象的拥有者,可以任意新增、覆盖、删除方法。

缺点:

实例是父类的实例,不是子类的实例
不支持多继承

2、基于类型的继承
a.原型链继承:

function MyError(message) {
    this.message = message;
}
MyError.prototype = new Error();//MyError将继承Error所有的属性与方法,instanceof也能工作。

var error = new MyError("Something bad happened.");
console.log(error instanceof Error);//true
console.log(error instanceof MyError);//true

特点:

实例是子类的实例,也是父类的实例

缺点:

无法实现多继承
来自原型对象的引用属性是所有实例共享的
创建子类时无法向父类传参

b.构造继承
在开发者定义了构造函数的情况下,基于类型的继承最后合适,同时,基于类型的继承一般需要2步:

(1)原型继承
(2)构造器继承,调用超类的构造函数时传入新建的对象作为其this的值:

function Person(name) {
    this.name = name;
}

function Author(name) {
    Person.call(this, name);//继承构造器
}

Author.prototype = new Person();
console.log(Author("grey"));

特点:

解决了原型链继承共享父类引用属性的问题
创建子类时,可以向父类传参
可以多继承(call多个父类对象)

缺点:

实例并不是父类的实例,中是子类的实例
只能继承父类的实例属性与方法, 不能继承原型属性与方法

c.组合继承

function Person(name) {
    this.name = name;   
    eat: function {
        console.log(name + "is eating ... ");
    }
}

function Author(name) {
    Person.call(this);
    this.name = name;
}

Author.prototype = new Person();
var author = new Author();
console.log(author.name);
author.eat()
console.log(author instanceof Author);//true
console.log(author instanceof Person);//true

特点:
由于组合继承,是组合了原型链继承与构造继承,所以二者的优点都有了:

可以继承实例属性,方法,也可以继承原型属性与方法
即是子类的实例,也是父类实例
没有引用属性共享问题
可以向父类传参

缺点:调用了2次父类的构造函数,生成了2份实例,会初始化2次实例方法/属性。

d.寄生组合继承

function Author(name) {
    Person.call(this);
    this.name = name
}
(function(){
    var Super = function() {};//避免组合继承的2次初始化实例方法/属性。
    Super.prototype = Person.prototype;
    Author.prototype = new Super();
})();

var author = new Author();
console.log(author.name);
console.log(author instanceof Author);//true
console.log(author instanceof Person);//true

九、门面模式

由于无法继承DOM元素,所以如果要对DOM增加一些操作,可以使用门面模式,就是使用另一个对象将DOM元素包装起来,在对象中操作这个DOM元素,而不是直接给DOM元素增添方法。

function DOMWrapper(element) {
    this.element = element;
}

DOMWrapper.prototype.addClass = function(className) {
    element.className += "" + className;
}

DOMWrapper.Prototype.remove = function() {
    this.element.parentNode.removeChild(this.element);
}

var wrapper = new DOMWrapper(document.getElementById("my-div"));
wrapper.addClass("selected");
wrapper.remove()

门面模式与适配器模式有一点不同就是门面模式创建新的接口,而适配器是实现已存在的接口。

十、阻止修改
在其它强面向对象的语言中,一般都会有final防止继承,在javaScript中没有,但是它有其自身的一些防止修改的方式:

防止扩展: 禁止为对象『添加』属性与方法,但已存在的属性与方法可以『修改』、『删除』。
密封:在防止扩展的基础上,禁止『删除』已存在的属性与方法。
冻结:在密封的基础上,禁止对已存在的属性与方法进行『修改』、『删除』(只读)。

所有锁定类型都提供了2个方法,一个实施禁止操作,一个用于检测是否实施了某种锁定操作。
1、防止扩展:

var person = {
    name: "Grey"
};
Object.preventExtension(person);//锁定
console.log(Object.isExtensible(person));//false
person.age = 25;//!!!wrong

2、密封:

Object.seal(person);
console.log(Object.isExtensibe(person));//false,//是否允许扩展
console.log(Object.isSealed(person));//true,被密封
delete person.name; //!!!wrong
person.age = 25;//!!!wrong

3、冻结

Object.freeze(person);
console.log(Object.isExtensibe(person));//false,//是否允许扩展
console.log(Object.isSealed(person));//true,被密封
console.log(Object.isFrozen(person));//true,被冻结
person.name = "Grey";//!!!wrong
person.age = 25;//!!!wrong
delete person.name;//!!!wrong

十一、项目文件和目录结构

一个文件只包含一个对象,方便维护,减少冲突
相关的文件用目录分组
保持第三方代码的独立
确定创建位置,不要将JavaScript源文件提交到项目工程中,网站应该是可配置的,使用编译后发布的目录结构,而非源码目录。
保持测试代码的完整性与独立性,以方便维护者检测测试用例的覆盖。

目录结构:build,src,test(s) 这3个目录是比较常见的目录结构。

十二、使用Ant构建项目

十三、使用JSLint、JSHint检测代码规范

十四、压缩JavaScript代码,eg: YUI Compressor、Closure Compiler、UglifyJS

十五、使用自动化文档生成工具生成文档:JSDoc Toolkit、YUI Doc

十六、自动化测试:YUI Test Selenium、Yeti、PhantomJS、JSTestDriver

参考:
本文主要用于一个知识的归纳总结,过程中可能会引用到其它地方的文字或代码,如有侵权请及时联系我,在此对写作过程中参考了的文章作者表示感谢!

  • JS实现继承的几种方式
  • 《编写可维护的JavaScript》

你可能感兴趣的:(《编写可维护的JavaScript》笔记)