本文所有准则均来自DavidHerman的《编写高质量JavaScript代码的68分有效方法》
1、了解你使用的JavaScript版本
1)不要将进行严格模式检查的文件和不进行严格模式检查的文件连接起来。
2)通过将其自身包裹在立即调用的函数表达式中的方式连接多个文件。
// file1.js
(function(){
"use strict";
// ....
})();
// file2.js
(function(){
"use strict";
// ....
})();
2、当心隐式的强制转换
3 + true; // 4
"2" + 3; // "23"
1)结果为null的变量在算术运算中不会导致失败,而是被隐式地转换为0;
2)一个未定义的变量将被转换为特殊的浮点数值NaN;
3)NaN是JavaScript中唯一一个不等于其自身的值;
// 如何判断是否是NaN
function isReallyNaN(x) {
return x !== x;
}
4)JavaScript中有7个假值
false、0、-0、“”“、NaN、null、undefined
3、避免对混合类型使用==运算符
1)5个原始值类型:布尔值、数字、字符串、null、undefined
2)==运算符的强制转换规则
参数类型1 | 参数类型2 | 强制转换 |
---|---|---|
null | undefined | 不转换,返回true |
null或undefined | 其他任何非null或undefined | 不转换,返回false |
string、number、boolean | Date对象 | 将原始类型转换为数字;将Date类型对象转换为原始类型(先toString,在尝试valueOf) |
string、number、boolean | 非Date对象 | 将原始类型转换为数字;将非Date类型对象转换为原始类型(先toString,在尝试valueOf) |
string、number、boolean | string、number、boolean | 将原始类型转换为数字 |
4、避免声明全局变量,尽量声明局部变量
5、始终声明局部变量
1)如果忘记将变量声明为局部变量,那么该变量将会被隐式转变为全局变量
6、避免使用with
7、熟练掌握闭包
1)函数可以引用定义再起外部作用域的变量
function outerFn() {
var outerVal = 'outer';
function innerFn() {
console.log("I'am innerFn, I can use", + outerVal);
}
}
2)闭包比创建它们的函数有更长的生命周期
3)闭包在内部存储其外部变量的引用,并能读写这些变量
function box() {
var val = null;
return {
set: function (newVal) { val = newVal; }
get: function () { return val; }
}
}
var b = box();
b.get(); // null
b.set('change');
b.get(); // change
8、理解变量声明提升
1)JS不支持块级作用域,即变量定义的作用域并不是离其最近的封闭语句或代码块,而是包含它们的函数。
function test(a) {
for (var i = 0; i < 3; i++) {
var a = i;
}
return a;
}
var b = test(9); // 打印 2,在for循环声明的a被提升了,所以循环内的a替换了传入的a
2)把变量声明和赋值分为两部分理解。JS隐式提升声明部分到封闭函数的顶部,而将赋值留在原地。
3)考虑手动提升局部变量的声明,避免混淆
9、使用立即调用的函数表达式创建局部作用域
function wrapElements(a) {
var result = [];
for (var i = 0, len = a.length; i < len; i++) {
result[i] = function () {
return a[i];
}
}
return result;
}
var list = wrapElements([10, 20, 30, 40, 50]);
var f = list[0];
f(); // undefined
function wrapElements(a) {
var result = [];
for (var i = 0, len = a.length; i < len; i++) {
(function (j) {
result[j] = function () {
return a[j];
}
})(i);
}
return result;
}
var list = wrapElements([10, 20, 30, 40, 50]);
var f = list[0];
f(); // 10
1)闭包通过引用而不是值捕获它们的外部变量
2)使用立即调用的函数表达式来创建局部作用域
10、理解函数调用、方法调用以及构造函数调用之间的不同
1)方法调用将被查找方法属性的对象作为调用接收者
function hello () {
return 'hello, ' + this.username;
}
var obj1 = {
hello: hello,
username: "obj1"
}
obj1.hello(); // hello, obj1
var obj2 = {
hello: hello,
username: "obj2"
}
obj2.hello(); // hello, obj2
2)函数调用将全局对象作为其接收者
3)构造函数需要通过new运算符调用,并产生一个新的对象作为其接收者
11、熟练掌握高阶函数
1)高阶函数是那些将函数作为参数或返回值的函数,例如回调函数
12、使用call发放自定义接收者来调用方法
1)使用call方法可以调用在给定的对象中不存在的方法
var hasOwnProperty = {}.hasOwnProperty;
var dict = {
foo: 1
};
delete dict.hasOwnProperty;
hasOwnProperty.call(dict, 'foo'); // true
hasOwnProperty.call(dict, 'hasOwnProperty'); // false
2)使用call方法定义高阶函数允许使用者给回调函数指定接收者
var obj = {
name: 'obj'
}
function test(callback) {
var tip = 'hello, ';
callback.call(obj, tip);
}
test(function (tip) {
console.log(tip + this.name); // hello, obj
})
13、永远不要修改arguments对象
14、使用bind方法提取具有确定接收者的方法
const module = {
x: 42,
getX: function() {
return this.x;
}
};
const unboundGetX = module.getX;
console.log(unboundGetX()); // undefined
const boundGetX = unboundGetX.bind(module);
console.log(boundGetX()); // 42
15、理解prototype、getPrototypeOf和__proto__之间的不同
1)C.prototype用于建立由new C()创建的对象的原型
2)Object.getPrototypeOf(obj)是ES5用来获取obj对象的原型对象的标准方法
3)obj.__proto__是获取obj对象的原型对象的非标准方法
16、使构造函数与new操作符无关
function User(name, age) {
var self = this instanceof User ? this : Object.create(User.prototype);
self.name = name;
self.age = age;
return self;
}
var user1 = new User('one', 18);
var user2 = User('two', 20);
console.log(user1.name + ' : ' + user1.age); // one : 18
console.log(user2.name + ' : ' + user2.age); // two : 20
17、只将实例状态存储在实例对象中
function Tree (x) {
this.name = x;
this.children = []; // 实例状态
}
Tree.prototype = {
addChildren: function (x) {
this.children.push(x);
}
}
18、认识到this变量的隐式绑定问题
// 错误写法
CSVReader.prototype.read = function (str) {
var lines = str.trim().split(/\n/);
return lines.map(function (line) {
return line.split(this.regexp); // 这里的this是错误的,绑定的this是lines本身,而不是CSVReader
})
}
var reader = new CSVReader();
reader.read("a,b,c\nd,e,f\n");
// 正确写法1
CSVReader.prototype.read = function (str) {
var lines = str.trim().split(/\n/);
return lines.map(function (line) {
return line.split(this.regexp);
}, this);
}
// 正确写法2
CSVReader.prototype.read = function (str) {
var lines = str.trim().split(/\n/);
var self = this;
return lines.map(function (line) {
return line.split(self.regexp);
});
}
// 正确写法3
CSVReader.prototype.read = function (str) {
var lines = str.trim().split(/\n/);
return lines.map(function (line) {
return line.split(this.regexp);
}.bind(this));
}
1)this变量的作用域总是由其最近的封闭函数所确定
19、使用null原型以防止原型污染
1)创建一个没有原型的对象
// 方法一
var x = Object.create(null);
Object.getPrototypeOf(x) === null; // true
// 方法二
var x = { __proto__: null };
x instanceof Object; // false
20、使用数组而不要使用字典来存储有序集合
21、数组迭代要优先使用for循环而不是for..in循环
1)考虑在循环之前将数组的长度储存在一个局部变量中以避免重新计算数组长度
22、在类数组对象上复用通用的数组方法
1)对于类数组对象,通过提取方法对象并使用其call方法来复用通用的Array方法
function test() {
arguments.forEach(function (arg) {
console.log(arg) // Uncaught TypeError: arguments.forEach is not a function
})
}
test(1, 'test', 'argument', 2);
function test() {
[].forEach.call(arguments, function (arg) {
console.log(arg)
})
}
test(1, 'test', 'argument', 2); // 正常打印
2)任意一个具有索引属性和恰当length属性的对象都可以使用通用的Array方法
// 例子1
var arrayLike = {
0: 'a',
1: 'b',
2: 'c',
length: 3
}
var result = Array.prototype.map.call(arrayLike, function (s) {
return s.toUpperCase();
});
console.log(result); // ["A", "B", "C"]
// 例子2
var result = Array.prototype.map.call("abc", function (s) {
return s.toUpperCase();
});
console.log(result); // ["A", "B", "C"]
23、将undefined看做“没有值”
1)避免使用undefined表示任何非特定值
2)不要使用undefined或null来代表特定应用标识
3)0,NaN或空字符串为有效参数的地方,绝不要通过真值测试来实现参数默认值
24、接收关键字参数的选项对象
1)使用选项对象使得API更具可读性、更容易记忆
function test(opts) {
// ...
}
test({
width: 100,
height: 100,
bgColor: 'red',
borderColor: 'black'
})