高质量javascript基本要点
可维护的代码意味着代码是:
- 可读的
- 一致的
- 可预测的
- 看起来像是同一个人写的
- 有文档的
1.减少全局变量
var声明中通过链式赋值的方法会创建全局变量,如下,b是全局变量,a是局部变量。
// antipattern, do not use
function foo() {
var a = b = 0;
// ...
}
因为这里的计算顺序是从右至左的。首先计算表达式b=0,这里的b是未声明的,这个表达式的值是0,然后通过var创建了局部变量a,并赋值为0。
2.隐式全局变量
隐式的全局变量和显式定义的全局变量之间有着细微的差别,差别在于通过delete来删除它们的时候表现不一致。
通过var创建的全局变量(在任何函数体之外创建的变量)不能被删除。
没有用var创建的隐式全局变量(不考虑函数内的情况)可以被删除。
也就是说,隐式全局变量并不算是真正的变量,但他们是全局对象的属性成员。属性是可以通过delete运算符删除的,而变量不可以被删除。
3.使用单var模式
防止因为声明提前导致了逻辑错误,而且可以使开发者更方便地看到已经声明的内容:
// antipattern
myname = "global"; // global variable
function func() {
alert(myname); // "undefined"
var myname = "local";
alert(myname); // "local"
}
func();
不过es5严格模式及es6中声明提前会直接报错。
4.为for循环缓存住要遍历的数组的长度
如下代码:
for (var i = 0, max = myarray.length; i < max; i++) {
// do something with myarray[i]
}
通过这种方法只需要访问DOM节点一次以获得length,在整个循环过程中就都可以使用它。在遍历HTMLCollection时缓存length都可以让程序执行的更快。
当然写成下面这样最好:
var i,max=myarray.length;
for (i = 0; i < max; i++) {
// do something with myarray[i]
}
5.switch
以default结束整个switch,以确保即便是在找不到匹配项时也会有正常的结果.
6.命名
- 对于构造函数,可以使用“大驼峰式”命名,比如MyConstructor()
- 对于函数和方法,可以采用“小驼峰式”命名,比如myFunction()
- 常量全部大写
- 全局变量全部大写
- 在私有成员或方法名之前加上下划线前缀
7.注释自动生成文档
直接量和构造函数
对象直接量
对象之间量相比于通过构造函数创建对象的优势在于:
- 它的代码更少
- 它可以强调对象就是一个简单的可变的散列表,而不必一定派生自某个类
- 对象直接量不需要“作用域解析”(scope resolution)。因为新创建的实例有可能包含了一个本地的构造函数,当你调用Object()的时候,解析器需要顺着作用域链从当前作用域开始查找,直到找到全局Object构造函数为止。
两种方式如下:
// one way -- using a literal
var car = {goes: "far"};
// another way -- using a built-in constructor
// warning: this is an antipattern
var car = new Object();
car.goes = "far";
尽可能的使用对象直接量来创建实例对象,当参数不确定的时候,Object()构造函数的这种特性会导致一些意想不到的结果。
获得对象的构造器:
// an empty object
var o = new Object();
console.log(o.constructor === Object); // true
// a number object
var o = new Object(1);
console.log(o.constructor === Number); // true
console.log(o.toFixed(2)); // "1.00"
// a string object
var o = new Object("I am a string");
console.log(o.constructor === String); // true
// normal objects don't have a substring()
// method but string objects do
console.log(typeof o.substring); // "function"
// a boolean object
var o = new Object(true);
console.log(o.constructor === Boolean); // true
自定义构造函数
强制使用new的模式
数组直接量
优先使用数组直接量而不是new Array(),主要new Array()这种存在陷阱,new Array(3)是一个长度为3的数组。
函数
函数表达式与函数声明
带有命名的函数表达式:
var myFun=function myFun (){};
不带命名的函数表达式:
var myFun=function(){};
函数声明:
function myFun (){}
需要注意函数提前
回调函数
返回函数
函数的重写
var scareMe = function () {
alert("Boo!");
scareMe = function () {
alert("Double boo!");
};
};
// using the self-defining function
scareMe(); // Boo!
scareMe(); // Double boo!
立即执行的函数
它为我们提供一个作用域的沙箱,可以在执行一些初始化代码的时候使用。
当有些操作我们只需要操作一次,后面都不会用到的时候就可以使用立即执行的函数。
立即初始化的对象:
({
// here you can define setting values
// a.k.a. configuration constants
maxwidth: 600,
maxheight: 400,
// you can also define utility methods
gimmeMax: function () {
return this.maxwidth + "x" + this.maxheight;
},
// initialize
init: function () {
console.log(this.gimmeMax());
// more init tasks...
}
}).init();
函数属性:length属性和cache属性
cache属性是一个对象(hash表),传给函数的参数会作为对象的key,函数执行结果会作为对象的值。
var myFunc = function (param) {
if (!myFunc.cache[param]) {
var result = {};
// ... expensive operation ...
myFunc.cache[param] = result;
}
return myFunc.cache[param];
};
// cache storage
myFunc.cache = {};
API模式
- 回调模式
- 配置对象
- 返回函数
- 柯里化
柯里化-函数的应用和部分应用
部分应用的例子:
// a curried add()
// accepts partial list of arguments
function add(x, y) {
var oldx = x, oldy = y;
if (typeof oldy === "undefined") { // partial
return function (newy) {
return oldx + newy;
};
}
// full application
return x + y;
}
// test
typeof add(5); // "function"
add(3)(4); // 7
// create and store a new function
var add2000 = add(2000);
add2000(10); // 2010
让函数理解并且处理部分应用的过程,叫柯里化。
通用的柯里化函数:
function schonfinkelize(fn) {
var slice = Array.prototype.slice,
stored_args = slice.call(arguments, 1);
return function () {
var new_args = slice.call(arguments),
args = stored_args.concat(new_args);
return fn.apply(null, args);
};
}
使用的例子:
// a normal function
function add(a, b, c, d, e) {
return a + b + c + d + e;
}
// works with any number of arguments
schonfinkelize(add, 1, 2, 3)(5, 5); // 16
// two-step currying
var addOne = schonfinkelize(add, 1);
addOne(10, 10, 10, 10); // 41
var addSix = schonfinkelize(addOne, 2, 3);
addSix(5, 5); // 16
当你发现自己在调用同样的函数并且传入的参数大部分都相同的时候,就是考虑柯里化的理想场景了。你可以通过传入一部分的参数动态地创建一个新的函数。这个新函数会存储那些重复的参数(所以你不需要再每次都传入),然后再在调用原始函数的时候将整个参数列表补全,正如原始函数期待的那样。
对象创建模式
命名空间模式
命名空间可以帮助减少全局变量的数量,与此同时,还能有效地避免命名冲突、名称前缀的滥用。
如下所示,将对象和方法都包在一个大的MYAPP里,这样就只是MYAPP一个全局变量了:
// AFTER: 1 global
// global object
var MYAPP = {};
// constructors
MYAPP.Parent = function () {};
MYAPP.Child = function () {};
// a variable
MYAPP.some_var = 1;
// an object container
MYAPP.modules = {};
// nested objects
MYAPP.modules.module1 = {};
MYAPP.modules.module1.data = {a: 1, b: 2};
MYAPP.modules.module2 = {};
但是要确定申请的命名空间是第一次申请的而不是已经存在的。下面是一个通用的namespace函数的实现:
var MYAPP = MYAPP || {};
MYAPP.namespace = function (ns_string) {
var parts = ns_string.split('.'),
parent = MYAPP,
i;
// strip redundant leading global
if (parts[0] === "MYAPP") {
parts = parts.slice(1);
}
for (i = 0; i < parts.length; i += 1) {
// create a property if it doesn't exist
if (typeof parent[parts[i]] === "undefined") {
parent[parts[i]] = {};
}
parent = parent[parts[i]];
}
return parent;
};
声明依赖具有很多优势:
- 明确的声明依赖是告知你代码的用户,需要保证指定的脚本文件被包含在页面中。
- 将声明放在函数顶部使得依赖很容易被查找和解析。
- 使用了依赖声明模式之后,全局变量的解析在函数中只会进行一次,在此之后将会使用更快的本地变量。因此会有更好的性能。
- 压缩的时候更精简。因为压缩的时候会重命名本地变量却不会重命名全局变量。
声明依赖即声明你需要用到的模块。
var myFunction = function () {
// dependencies
var event = YAHOO.util.Event,
dom = YAHOO.util.Dom;
// use event and dom variables
// for the rest of the function...
};
私有属性、私有方法
在javascript中,所以的对象成员都是公有的。如果需要私有成员,就需要使用到闭包。
在构造函数中创建一个闭包,任何在这个闭包中的部分都不会暴露到构造函数之外。但是,这些私有变量却可以被公有方法访问,也就是在构造函数中定义的并且作为返回对象一部分的那些方法。
function Gadget() {
// private member
var name = 'iPod';
// public function
this.getName = function () {
return name;
};
} var toy =
new Gadget();
// `name` is undefined, it's private
console.log(toy.name); // undefined
// public method has access to `name`
console.log(toy.getName()); // "iPod"
如上所示,我们无法直接获取到name,但是我们可以通过Gadget的getName方法来获取name。这个时候name就是一个私有成员了。而getName就是一个特权方法。
但是有时候特权方法返回的是一个对象或数组,然后外界利用特权方法获取数据后,对数据进行了改造,那么就会发现私有成员被修改了,如下所示:
function Gadget() {
// private member
var specs = {
screen_width: 320,
screen_height: 480,
color: "white"
};
// public function
this.getSpecs = function () {
return specs;
};
}
var toy = new Gadget(),
specs = toy.getSpecs();
specs.color = "black";
specs.price = "free";
console.dir(toy.getSpecs());
返回的结果是
Object
color:"black"
price:"free"
screen_height:480
screen_width:320
__proto__:Object
如果要解决这个问题,可以使用一下一些方法:
- 让getSpecs()返回一个新对象,这个新对象只包含对象的使用者感兴趣的数据。这也是众所周知的“最低授权原则”。
- 当你需要传递所有的数据时,使用通用的对象复制函数创建specs对象的一个副本。
使用构造函数创建私有成员的一个弊端是,每一次调用构造函数创建对象时这些私有成员都会被创建一次。因此为了避免重复劳动,节省内存,你可以将共用的属性和方法添加到构造函数的 prototype (原型)属性中。
function Gadget() {
// private member
var name = 'iPod';
// public function
this.getName = function () {
return name;
};
}
Gadget.prototype = (function () {
// private member
var browser = "Mobile Webkit";
// public prototype members
return {
getBrowser: function () {
return browser;
}
};
}());
var toy = new Gadget();
console.log(toy.getName()); // privileged "own" method console.log(toy.getBrowser()); //
将私有函数暴露为公有方法
“暴露模式”是指将已经有的私有函数暴露为公有方法。当对对象进行操作时,所有功能代码都对这些操作很敏感,而你想尽量保护这些代码的时候很有用。
var myarray;
(function () {
var astr = "[object Array]",
toString = Object.prototype.toString;
function isArray(a) {
return toString.call(a) === astr;
}
function indexOf(haystack, needle) {
var i = 0,
max = haystack.length;
for (; i < max; i += 1) {
if (haystack[i] === needle) {
return i;
}
}
return −1;
}
myarray = {
isArray: isArray,
indexOf: indexOf,
inArray: indexOf
};
}());
myarray.isArray([1,2]); // true
myarray.isArray({0: 1}); // false
myarray.indexOf(["a", "b", "z"], "z"); // 2
myarray.inArray(["a", "b", "z"], "z"); // 2
现在假如有一些意外的情况发生在暴露的 indexOf() 方法上,私有的 indexOf() 方法仍然是安全的,因此 inArray() 仍然可以正常工作:
myarray.indexOf = null;
myarray.inArray(["a", "b", "z"], "z"); // 2
模块模式
MYAPP.namespace('MYAPP.utilities.array');
MYAPP.utilities.array = (function () {
// dependencies
var uobj = MYAPP.utilities.object,
ulang = MYAPP.utilities.lang,
// private properties
array_string = "[object Array]",
ops = Object.prototype.toString;
// private methods
// ...
// end var
// optionally one-time init procedures
// ...
// public API
return {
inArray: function (needle, haystack) {
for (var i = 0, max = haystack.length; i < max; i += 1) {
if (haystack[i] === needle) {
return true;
}
}
},
isArray: function (a) {
return ops.call(a) === array_string;
}
// ... more methods and properties
};
}());
当然模块模式里也可以将所有的方法保持私有,只在最后暴露需要使用的方法来初始化API。
MYAPP.utilities.array = (function () {
// private properties
var array_string = "[object Array]",
ops = Object.prototype.toString,
// private methods
inArray = function (haystack, needle) {
for (var i = 0, max = haystack.length; i < max; i += 1) {
if (haystack[i] === needle) {
return i;
}
}
return −1;
},
isArray = function (a) {
return ops.call(a) === array_string;
};
// end var
// revealing public API
return {
isArray: isArray,
indexOf: inArray
};
}());
当然也可以直接用构造函数来创建对象。
MYAPP.namespace('MYAPP.utilities.Array');
MYAPP.utilities.Array = (function () {
// dependencies
var uobj = MYAPP.utilities.object,
ulang = MYAPP.utilities.lang,
// private properties and methods...
Constr;
// end var
// optionally one-time init procedures
// ...
// public API -- constructor
Constr = function (o) {
this.elements = this.toArray(o);
};
// public API -- prototype
Constr.prototype = {
constructor: MYAPP.utilities.Array,
version: "2.0",
toArray: function (obj) {
for (var i = 0, a = [], len = obj.length; i < len; i += 1) {
a[i] = obj[i];
}
return a;
}
};
// return the constructor
// to be assigned to the new namespace return Constr;
}());
var arr = new MYAPP.utilities.Array(obj);
沙箱模式
就是下面这个格式:
Sandbox(['ajax', 'event'], function (box) {
// console.log(box);
});
或者是:
Sandbox('ajax', 'dom', function (box) {
// console.log(box);
});
使用通配符“*”来表示“使用所有可用的模块”可用,假设没有任何模块传入时,沙箱使用“*”。
Sandbox('*', function (box) {
// console.log(box);
});
Sandbox(function (box) {
// console.log(box);
});
下面是这个沙箱的构造函数的实现:
function Sandbox() {
// turning arguments into an array
var args = Array.prototype.slice.call(arguments),
// the last argument is the callback
callback = args.pop(),
// modules can be passed as an array or as individual parameters
modules = (args[0] && typeof args[0] === "string") ? args : args[0], i;
// make sure the function is called
// as a constructor
if (!(this instanceof Sandbox)) {
return new Sandbox(modules, callback);
}
// add properties to `this` as needed:
this.a = 1;
this.b = 2;
// now add modules to the core `this` object
// no modules or "*" both mean "use all modules"
if (!modules || modules === '*') {
modules = [];
for (i in Sandbox.modules) {
if (Sandbox.modules.hasOwnProperty(i)) {
modules.push(i);
}
}
}
// initialize the required modules
for (i = 0; i < modules.length; i += 1) {
Sandbox.modules[modules[i]](this);
}
// call the callback
callback(this);
}
// any prototype properties as needed
Sandbox.prototype = {
name: "My Application",
version: "1.0",
getName: function () {
return this.name;
}
};
静态成员
公有的静态方法:
// constructor
var Gadget = function () {};
// a static method
Gadget.isShiny = function () {
return "you bet";
};
// calling a static method
Gadget.isShiny(); // "you bet"
// creating an instance and calling a method
var iphone = new Gadget();
iphone.setPrice(500);
//有时候需要实例上也能用这个静态方法
Gadget.prototype.isShiny = Gadget.isShiny;
iphone.isShiny(); // "you bet"
但这个时候需要考虑静态方法内部的this的指向。
// constructor
var Gadget = function (price) {
this.price = price;
};
// a static method
Gadget.isShiny = function () {
// this always works
var msg = "you bet";
if (this instanceof Gadget) {
// this only works if called non-statically
msg += ", it costs $" + this.price + '!';
}
return msg;
};
// a normal method added to the prototype
Gadget.prototype.isShiny = function () {
return Gadget.isShiny.call(this);
};
//测试一下静态方法
Gadget.isShiny(); // "you bet"
//测试一下实例中的非静态调用
var a = new Gadget('499.99');
a.isShiny(); // "you bet, it costs $499.99!"
私有的静态方法:
// constructor
var Gadget = (function () {
// static variable/property
var counter = 0,
NewGadget;
// this will become the
// new constructor implementation
NewGadget = function () {
counter += 1;
};
// a privileged method
NewGadget.prototype.getLastId = function () {
return counter;
};
// overwrite the constructor
return NewGadget;
}()); // execute immediately
静态属性(包括私有和公有)有时候会非常方便,它们可以包含和具体实例无关的方法和数
据,而不用在每次实例中再创建一次。
对象常量
var constant = (function () {
var constants = {},
ownProp = Object.prototype.hasOwnProperty,
allowed = {
string: 1,
number: 1,
boolean: 1
},
prefix = (Math.random() + "_").slice(2);
return {
set: function (name, value) {
if (this.isDefined(name)) {
return false;
}
if (!ownProp.call(allowed, typeof value)) {
return false;
}
constants[prefix + name] = value;
return true;
},
isDefined: function (name) {
return ownProp.call(constants, prefix + name);
},
get: function (name) {
if (this.isDefined(name)) {
return constants[prefix + name];
}
return null;
}
};
}());
// check if defined
constant.isDefined("maxwidth"); // false
// define
constant.set("maxwidth", 480); // true
// check again
constant.isDefined("maxwidth"); // true
// attempt to redefine
constant.set("maxwidth", 320); // false
// is the value still intact?
constant.get("maxwidth"); // 480
链式调用模式
var obj = {
value: 1,
increment: function () {
this.value += 1;
return this;
},
add: function (v) {
this.value += v;
return this;
},
shout: function () {
alert(this.value);
}
};
// chain method calls
obj.increment().add(3).shout(); // 5
// as opposed to calling them one by one
obj.increment();
obj.add(3);
obj.shout(); // 5
method()方法
if (typeof Function.prototype.method !== "function") {
Function.prototype.method = function (name, implementation) {
this.prototype[name] = implementation;
return this;
};
}
var Person = function (name) {
this.name = name;
}.
method('getName', function () {
return this.name;
}).
method('setName', function (name) {
this.name = name;
return this;
});
var a = new Person('Adam');
a.getName(); // 'Adam'
a.setName('Eve').getName(); // 'Eve'