这是我刷完的第一本书。万事开头难,总算是在2017年02月09日开了一个好头。
这篇总结是为了记录在读这本书的过程中所遇到的好的知识点和思想,以及我在实际工作中结合作者的想法所做的一些实践和读书的收获。
这本书从两个个方面(风格和实践)来讲述如何去写维护性高、可读性高和高效的JavaScript代码。
“编程风格”是指在长期编写代码的过程中,养成关于编写方式、代码结构和代码可读性等等方面的习惯。
统一编程风格的好处是:
- 由于编程风格一致,团队合作过程中,看其他人的代码就像在看自己写的代码,提高了可读性;
- 由于编程风格一致,当遇到自己感到奇怪的代码会立刻发现其可能出现的问题。
格式化是编程风格指南的核心规则,这些规则最直接的体现在于代码的水准。
目前主要有两种主张缩进的方式:
1.使用制表符进行缩进:
每一个缩进都是用单独的 `Tab(制表符)` 来表示。
2.使用空格符进行缩进:
即每一个缩进都是用单独的 `space(空格)` 来表示
二者各自有各自的缺点:
个人非常倾向于使用 “Tab” 来表示行内缩进的。
对于 JavaScript 代码来说,在语句的结尾可以写分号(;),亦可以不写分号。
这是有赖于分析器的自动分号插入(Automatic Semicolon Insertion, ASI),ASI会自动的寻找代码中应当使用分号但实际没有分号的位置,并插入分号。
个人推荐的是在每句的结尾如果可以加上分号,那么一定加上分号,不止防止了低级错误的发生,也增加了代码的可读性,因为会告诉正在看代码的人“这一句到这就结束了”。
目前关于代码中每行的长度是由争议的,前段时间看过阮一峰老师的一篇关于CPL为什么会有在72和80这两个选项,很受启发。
对于我来说,尽可能让一行代码的字符数少于72,如果72个字符满足不了的话,就尽可能的少于80个字符,若这样还是满足不了,那么就尽量的减少吧,这种情况虽然极少数(可能和我水平有关),如果出现了也实在是没有办法。
当一行的代码长度达到了单行最大字符数的限制时,就需要手动将一行拆成两行(这种情况居多)。通常需要在运算符后换行,下一行增加两个层级的缩进。
请记住,一定要将运算符置于行的结尾,这样ASI就不会再该行的结尾插入分号。
在编码规范中,空行是常常被忽略的一个方面。在书中,介绍了关于一段代码应该是一系列可读的段落(像文章一样),而不是一大堆揉在一起的连续的文本。
因此书中建议在以下地方插入空行(即回车):
- 方法之间;
- 在方法中的局部变量和第一条语句之间;
- 在多行或单行注释之前;
- 在方法内的逻辑片段之间插入空行,提高可读性;
计算机科学只存在两个难题: 缓存失效和命名 —— Phil Karlton
我在写代码的过程中,其实是十分重视变量名的,我看过一句话大概的意思是:再好的注释,也比不过天然自带注释的命名。但是由于自身的英语词汇量少的可怜,因此变量的名称翻来覆去也想不出多少个花样。。。
目前存在两种主要的命名方式:驼峰式 和 下划线,其中驼峰式又包括小驼峰(Camel Case)和大驼峰(Pascal Case),二者主要的区别在于大驼峰要求首字母大写,而小驼峰要求首字母小写。下划线的命名方式是每个单词之间用“_”来链接,同时所有的字母均小写。
个人比较习惯的命名方式是:对于变量,若是局部的变量,使用小驼峰的方式;若是全局变量和静态变量,使用所有字母大写加上下划线的方式。函数的命名习惯是:构造函数使用首字母大写的小驼峰式,普通函数使用小驼峰的方式命名
什么是直接量?JavaScript 中包含一些类型的原型值: 字符串、数字、布尔值、null
和undefined
。同样的,也包含数组直接量[]
和对象直接量{}
。
字符串可以用单括号''
或者是双括号""
括起来,在这里作者强调的是,不论是单括号还是双括号,在你的代码中,一定要保持相同的括号风格,即在代码中,尽量在以一种方式的括号表示字符串。
字符串还需要关注的另一个问题就是多行字符串。作者建议,如果字符串如果太长,那么使用多行字符串代替过长的字符串。例如:
var longString = 'Here`s the story, of a man ' +
'named Brady';
在 JavaScript 中只需要关心数字的一个问题,即如果一个数字是浮点型,一定要写全小数点后面几位。万万不可只有小数点,省略了小数点后面的小数部分,如10.
。
null 在 JavaScript 中是特殊值,但是它很容易和undefined
搞混。作者在书中介绍说,在以下几种场景中可以使用null
:
还有一些场景中不应当使用null
:
null
来检测是否传入了某个参数;null
来检测一个未初始化的变量。undefined 也是一个特殊值。其中最让人感到困惑的是 null === undefined
的结果是 true
。
那些没有被初始化的变量都有一个初始值,即 undefined
,表示这个变量等待被赋值。
在我的实践中,可以通过判断一个参数是否是 undefined
来判断这个参数是否在函数被调用是被传入。同时,undefined
通常来说,只与 typeof
运算符一起使用,用以判断一个变量是否被赋值或者存在。
{}
& []
)创建对象或者数组的最流行也是最常见的一种方式是使用对象或数组的直接量,在直接量中直接写出所有的属性。例如:
// 使用对象直接量
var book = {
title: 'Maintaintable JavaScript',
author: 'Nicholas C. Zakas'
}
// 使用数组直接量
var colors = [ 'red', 'blue', 'green' ];
var numbers = [1, 2, 3 ,4, 5];
注释共分为两种方式:单行注释和多行注释;
对于注释,我的习惯是:
单行注释:双斜杠
//
后面插入一个空格,独占一行并且在此注释之前插入一个空行,缩进和要注释的代码的缩进一致。
多行注释:总是会出现在将要描述的代码段之前,缩进与要描述的代码段一致,同时注释之前插入一个或两个空行。
所有语句都应当使用花括号,包括:
if/for/while/do...while.../try...catch...finally
有两种风格:
// 第一种
if (condition) {
doSomeThings();
}
// 第二种
if (condition)
{
doSomeThings();
}
共三种方式:
// 第一种
if(condition){
doSomeThings();
}
// 第二种
if (condition) {
doSomeThings();
}
// 第三种
if ( condition ) {
doSomeThings();
}
我个人比较倾向于第二种方式。
for - in
循环是用来遍历对象属性的。
但是,for - in
循环有一个问题,就是它不仅遍历对象的实例属性,同样的还遍历从原型继承来的属性。因此,对于这个问题,最好使用 hasOwnProperty()
方法来为 for - in
循环过滤出实例属性。
var props;
for (var item in props) {
if (props.hasOwnProperty(item)) {
doSomeThings(props[item]);
}
}
我在编码过程中,如果使用 for-in
循环都会使用 hasOwnProperty()
方法。
需要注意的是: for-in
循环是用来遍历实例对象,不能用它来遍历数组,这会造成一些潜在的问题。
既然如此,就可以用这个特性来判断一个对象是否为空:
const isEmptyForObject = (obj) => {
for(let item in obj) {
return false;
}
return true;
}
说到变量,首先对于 JavaScript 的变量要明白一个理论:变量提升。
当创建变量时,需要用到 var
、let
、const
关键字,这些关键字意义和作用并不相同:
var
:是 JavaScript 最经典的创建变量的关键字,它定义的变量可以修改并且它并不存在“块”的作用域,也就是说在ES6之前,函数内部定义的变量,在其外部依旧可以调用:function change () {
var demo = 4;
console.log(demo);
}
change(); // 4
console.log(demo) // demo = 4
let
:是ES6新添的创建变量的关键字,它定义的变量也可以修改,但是它强调“块作用域”的概念,即在函数内部使用let并修改,对函数外部没有影响:function change () {
let demo = 4;
console.log(demo);
}
change(); // 4
console.log(demo) // demo is not defined
const
:同样是ES6特有的创建变量的关键字,不过它定义的变量除了初始化以外,是不可以修改赋值的。const demo = 4;
demo = 5; // Assignment to constant variable.
请注意:
当我在创建变量的时候,习惯于将所有的变量放到局部代码块的首行,并且很喜欢将所有的 var/let/const
语句合并成一句:
var a = 5,
b = 6;
let c = 7,
d = 8;
const e = '123',
f = 'afdasdf';
在 JavaScript 中,函数及变量的声明都将被提升到函数的最顶部。
在 JavaScript 中,变量可以在使用后声明,也就是变量可以先使用再声明,即:在函数内部任意地方定义变量和在函数定义变量是完全啊一样的。例如:
// 提升前(源代码)
function doSomething () {
var result = 10 + value;
var value = 10;
return result;
}
// 变量提升
function doSomething () {
var result, value;
result = 10 + value;
valut = 10;
return result;
}
doSomething() // NaN (not a number)
需要注意的是,只有变量的声明才会被提升,至于变量的初始化,并不会被提升。
我的习惯是,总是将局部变量的定义作为函数内第一条语句。
和变量声明一样的,函数的声明也会被变量提升机制提升到当前作用域下的顶部。
// 函数声明提升之后:
function doSomethingWithItems (items) {
var i, len,
value = 10,
result = value + 10;
function doSomething (item) {
// todo...
}
for (i = 0; len = item.length; i < len; i++) {
doSomething(items[i])
}
}
同样需要注意的是,请看下面的栗子:
if (condition) {
function doSomething (item) {
alert("HI!");
}
} else {
function doSomething (item) {
alert("YO!");
}
}
这段代码的实际效果是 alert 弹窗里面的内容是 “YO!”。这也是由于函数声明提升造成的,上面这段代码转换为下面这段代码:
// 被第二个 doSomething 覆盖
function doSomething () {
alert("HI!");
};
function doSomething () {
alert("YO!");
}
if (condition) {
doSomething();
} else {
doSomething();
}
这也是大多数浏览器都会自动运行第二个声明的原因。
函数调用的风格是:在函数名和左括号之间没有空格。
在 JavaScript 中允许声明匿名函数,并将匿名函数赋值给变量或者是对象的属性。
var doSomething = function () {
// todo...
}
这种匿名函数在函数的最后加上一对括号可以立即执行并返回一个值,然后将这个值赋值给变量或者对象的属性。
// 这种写法并不推荐,只是为了展示作用
var value = funciton () {
return {
message: 'hi'
};
}();
这种模式的问题在于,会让人误认为将一个匿名函数赋值给了这个变量。除非读完了完整的代码。更好的做法是,为了能让立即执行的函数能够被一眼看出来,用一对括号将立即执行的函数包裹起来:
var value = (funciton () {
return {
message: 'hi'
};
}());
由于 JavaScript 具有强制类型转换机制,因此在 JavaScript 中判断相等的操作是很微妙的。
发生强制类型转换最常见的场景,就是使用了判断相等运算符 ==
和 !=
。如果发生了强制类型转换,那么对于判断变量的类型或者是否相等就变得尤为的困难。
在使用 ==
或 !=
的情况下:
- 在判断数字和字符串时,字符串会首先转换为数字,然后进行比较;
- 在判断数字与布尔值时,布尔值会首先转换为数字,然后进行比较;
- 若其中一个值是对象,则会先调用对象的
valueOf()
方法,得到原始类型。若没有定义valueOf()
, 则调用toString()
。- 需要注意的是,如果
null
和undefined
相比较,这个两个特殊值是相等的。
// 比较数字 5 和字符串 5
console.log(5 == '5'); // true
// 比较数字 25 和十六进制的字符串 25
console.log(25 == '0x19'); // true
// 数字 1 和 true
console.log(1 == true); // true
// 对象
var object = {
toString: function () {
return '0x19';
}
}
console.log(object == 25); // true
// null & undefined
console.log(null == undefined);
前面说了,如果使用 ==
和 !=
会造成变量之间的类型互相转换,但是有些时候我们想要严格检查,即两个变量的类型不同,我们就认为二者不同,这就需要使用 ===
和 !==
,这两个运算符并不会触发强制类型转换。
我的习惯是总是使用 ===
和 !==
进行严格检查。
JavaScript 中一个不易被理解且常常被误解的方面是,这门语言对原始包装类型的依赖。
JavaScript 有3种原始包装类型: String
、Boolean
和 Number
。每种类型代表全局作用域中的一个构造函数,并分别表示各自对应的原始值的对象。 原始包装类型的主要作用是让原始值具有对象般的行为
var name = 'Nicholas';
console.log(name.toUpperCase()); // NICAHOLAS
尽管 name
是一个字符串,是原始类型不是对象,但是仍然可以使用如 toUpperCase()
、toLowerCase()
等方法,把字符串当成对象来用。
原因是:
在创建一个字符串变量时,JavaScript 引擎创建了 String 类型的新实例,紧跟着就被销毁了,当在此需要的时候再重新创建另一个实例。
var name = 'Nicholas';
name.author = true;
console.log(name.author); // undefined
现如今的前端工程师几乎人人都在谈模块化,如:AMD、CMD和UMD等等,就是在解决某一个复杂问题或者一系列的杂糅问题时,依照一种分类的思维把问题进行系统性的分解以之处理。
那么 AMD / CMD / UMD 的区别是什么呢?参考资料 Javascript模块化
首先来解释下什么是模块化,模块化是指将复杂的系统分解为多个代码结构合理、可维护性更高和可管理的模块的方式。解耦软件系统的复杂性,使得不管多么大的系统,也可以将管理,开发,维护变得“有理可循”。
作为模块化的系统必须具有如下三个特点:
因此基于模块化,目前在 JavaScript 中出现了一些非传统开发方式的模式: CommonJS模式、AMD模式、CMD模式和UMD模式。
CommonJS 是用于服务器端的模块规范,NodeJs 主要采用这种模式。
根据 CommonJs 模式的规范,一个单独的文件即为一个模块。加载模块使用 require
方法,该方法读取一个文件并执行,最后返回文件内部的 exports
对象。
// foobar.js
//私有变量
var test = 123;
//公有方法
function foobar () {
this.foo = function () {
// do someing ...
}
this.bar = function () {
//do someing ...
}
}
//exports对象上的方法和变量是公有的
var foobar = new foobar();
exports.foobar = foobar;
//require方法默认读取js文件,所以可以省略js后缀
var test = require('./boobar').foobar;
test.bar();
从上面可以看出,CommonJS 是同步加载的,只有将其他的依赖都加载完才可以执行文件本身的内容。如 NodeJs 编写的服务器代码,文件一般都是存放在本地硬盘上,加载起来比较快,因此适用于 CommonJS 模式。但是对于浏览器来说,下载资源必须要异步的方式,所以就有了 AMD
/ CMD
解决方案。
AMD 模式是异步模块定义模式,它设计出一个简洁的写模块 API:
define(id?, dependencies?, factory);
第一个参数 id
为字符串类型,表示了模块标识,为可选参数。若不存在则模块标识应该默认定义为在加载器中被请求脚本的标识。如果存在,那么模块标识必须为顶层的或者一个绝对的标识。
第二个参数 dependencies
是一个当前模块依赖的,已被模块定义的模块标识的数组字面量(直接量)。
第三个参数 factory
是一个需要进行实例化的函数或者一个对象。
define("alpha", [ "require", "exports", "beta" ], function( require, exports, beta ){
export.verb = function(){
return beta.verb();
// or:
return require("beta").verb();
}
});
在不考虑多了一层函数外,格式和 NodeJs 是一样的:使用 require
获取依赖模块,使用 exports
导出 API。
除了define之外,AMD 还保留了一个关键字 require
。require
作为规范保留的全局标识符,可以实现为 module loader
,也可以不实现。
require([module], callback)
第一个参数 [module]
是一个数组,里面的成员就是要加载的模块;
第二个参数 callback
是加载完成这些依赖的模块之后执行的回调函数。
require(['math'], function(math) {
math.add(2, 3);
});
CMD 是 SeaJS 在推广过程中对模块定义的规范化产出:
// AMD
define(['./a','./b'], function (a, b) {
//依赖一开始就写好
a.test();
b.test();
});
// CMD
define(function (requie, exports, module) {
//依赖可以就近书写
var a = require('./a');
a.test();
...
//软依赖
if (status) {
var b = requie('./b');
b.test();
}
});
虽然 AMD也支持CMD写法,但依赖前置是官方文档的默认模块定义写法。
UMD 是 AMD 和 CommonJS 的结合。
AMD 模块以浏览器第一的原则发展,异步加载模块。
CommonJS 模块以服务器第一原则发展,选择同步加载,它的模块无需包装。
这迫使人们又想出另一个更通用的模式 UMD(Universal Module Definition)
。
希望解决跨平台的解决方案。
UMD 先判断是否支持 NodeJs 的模块(exports
)是否存在,存在则使用 NodeJs 模块模式。在判断是否支持 AMD(define
是否存在),存在则使用 AMD 方式加载模块。
(function (window, factory) {
if (typeof exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define(factory);
} else {
window.eventUtil = factory();
}
})(this, function () {
// this is factory
// module ...
});
写出一个具有兼容性的事件绑定/挂载的方法(原生JS):
function addListener (target, type, handler) {
if (target.addEventListener) {
// 判断 addEventListener 是否存在
target.addEventListener(type, handler, false);
} else if (target.attachEvent) {
// 判断 attachEvent 是否存在
target.attachEvent('on' + type, handler);
} else {
target['on' + type] = handler;
}
}
全局 name
实际上是 window
的一个默认属性,window.name
属性经常用于框架(frame
)和 iframe
的场景中。
浏览器处理事件的过程: 事件捕获 –> 事件目标 –> 事件冒泡;
事件捕获:事件(click/mouseenter/mouseleave…)首先发生在 document -> body -> … -> 节点(事件目标);
事件冒泡:事件到达事件目标之后不会结束,会逐层向上冒泡,直至document对象,跟事件捕获相反;
事件委托即利用这2个特性,将事件绑定在父节点中,通过事件捕获获得节点,通过冒泡执行事件。避免了对每个节点都进行事件的绑定,节省了劳动力。
事件触发代码要与应用逻辑代码分开,这么做的好处是应用逻辑代码可以复用,同时在测试时直接功能测试,而不需要模拟对元素触发事件来测试,同时不要无限地分发事件对象。
var myApplication = {
handleClick: function (event) {
// 阻止 事件默认行为 和 事件冒泡
event.preventDefault();
event.stopPropagation();
// 传入应用逻辑
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.className = 'reveal';
}
};
addListener(element, "click", function(event) {
myApplication.handleClick(event);
})
typeof
是运算符;
instanceof
也是运算符;检测一个对象是否是 Array
?
function isArray (arg) {
if ('function' === typeof Array.isArray) {
return Array.isArray(arg);
} else {
return Object.prototype.toString.call(arg) === '[object Array]';
}
}
in
与 hasOwnProperty
的区别
1. 使用 in 的方式来检测某实例是否具有某属性时,in 会在其实例属性中、原型链中遍历查找该属性;
2. 使用 hasOwnProperty 的方式检测时,只会检测实例属性,不会检测其原型链;
若不确定是否存在 hasOwnProperty
可以这么写:
if ('hasOwnProperty' in object && object.hasOwnProperty('xxxx')) {
// todo...
}
配置数据需要从代码中分离开,这么做的目的防止在修改源代码的时候引入bug,尤其是对修改一些数据的值从而带来一些不必要的bug风险,那么什么是配置数据呢?配置数据是可发生变更的,而且你不希望因为有人突然想修改页面中的展示信息,导致去修改 JavaScript 源码。
JavaScript 的常见可用文件格式有三种:JSON/JSONP/JS
格式。
1. JSON:这是一种很常见的数据格式,使用一组 JavaScript 的数组转换为字符串作为表示格式。
2. JSONP:是将 JSON 结构用一个函数(调用)包装起来。
3. JavaScript:将 JSON 对象赋值给一个变量,这个变量会被程序用到。
这里,我习惯的方式是第三种,使用 JavaScript 的格式,同样的会将所有的配置数据全部存入全局对象中,防止过多的全局变量导致的变量混乱问题。
JavaScript 错误:
抛出错误:
使用 throw 的操作符,将提供的一个对象作为错误抛出,任何类型的对象都可以作为错误抛出,一般的,Error 对象是最常用的。
throw new Error('something is happened.');
捕获错误:
JavaScript 提供了 try-catch
的语句,使得能在浏览器处理抛出的错误之前捕获它,可能引发错误的代码块放到 try
快中,错误的代码放在 catch
块中。
但是,请注意 try-catch
还有一种写法 try-catch-finally
的格式,这种格式中 finally
块中的代码是在try-catch
中不论是否发生错误,均会执行。但是,如果 try
块中包含了一个 return
语句,那么它必须等到 finally
中的代码执行完毕之后才能返回。
错误类型:
所有的错误类型继承了 Error
,因此用 instanceof Error
运算符检查其类型得不到任何的有用的信息。但是可以通过检查“特定”的错误类型来处理:
try {
if (ex instanceof TypeError) {
// 处理 TypeError 错误
} else if (ex instanceof ReferenceError) {
// 处理 ReferenceError 错误
} else {
// 处理其他类型的错误
}
}
因为错误类型比较多,在判断错误类型时并不好区分,因此一个比较好的解决方案是创建自己的错误类型,让它继承 Error
。这种做法的好处是自定义错误类型可以检测自己的错误。
function MyError (msg) {
this.message = msg;
}
MyError.prototype = new Error();
Object 的锁:
Object 的锁主要有三种:prevent extension
、seal
和 freeze
;
preventExtensions & isExtensible
preventExtensions: Object.prevenExtensions() 方法是阻止对象扩展;
isExtensible: Object.isExtensible() 方式是检查对象是否已经阻止了扩展。
var person = {
name: 'testName'
};
Object.preventExtensions(person);
Object.isExtensible(person); // true
person.age = 25 // 正常情况下会悄悄地失败,严格模式下会报错
seal & isSealed
seal: Object.seal() 方法是将对象密封,即不允许添加/删除属性或者方法;
isSealed: Object.isSealed() 方式是检查对象是否已经设置了密封状态。
var person = {
name: 'testName'
};
Object.seal(person);
Object.isSealed(person); // true
// 添加或删除对象的属性或者方法
person.age = 25 // 正常情况下会悄悄地失败,严格模式下会报错
delete person.name // 正常情况下会悄悄地失败,严格模式下会报错
// 修改对象的属性
person.name = 'Nicholas'; // 正常发生
freeze & isFrozen
freeze: Object.freeze() 方法是冻结对象,即不能添加/删除/更改对象的属性和方法;
isFrozen: Object.isFrozen() 方式是检查对象是否已经被冻结。
var person = {
name: 'testName'
};
Object.freeze(person);
Object.isFrozen(person); // true
// 添加/删除/修改对象的属性或者方法
person.age = 25 // 正常情况下会悄悄地失败,严格模式下会报错
delete person.name // 正常情况下会悄悄地失败,严格模式下会报错
person.name = 'Nicholas'; // 正常情况下会悄悄地失败,严格模式下会报错
由以上可知,对象的权限的大小级关系是:preventExtensions
< seal
< freeze
。
浏览器的兼容问题:
书中所说的大概的意思是,禁止使用特性推断,建议使用特性检测,不建议使用浏览器嗅探(user-agent)。
// 特性检测
function setAnimation (cb) {
// 标准
if (window.requestAnimationFram) {
return window.requestAnimationFram(cb);
// Firefox
} else if (window.mozRequestAnimationFram) {
return window.requestAnimationFram(cb);
// webkit
} else if (window.webkitRequestAnimationFram) {
return window.requestAnimationFram(cb);
// Opera
} else if (window.oRequestAnimationFram) {
return window.requestAnimationFram(cb);
// IE
} else if (window.msRequestAnimationFram) {
return window.requestAnimationFram(cb);
// 浏览器都不支持的时候,使用 setTimeout 方法
} else {
setTimeout(cb, 0);
}
}
关于这本书,其实我并不知道这本书会给我带来多大的价值,但是却坚定我的信心。通过通读和细读,加之写了这篇回顾和总结,我收获了在写代码过程需要注意的和需要改进的地方,同时也得到了一些我意想不到的处理问题的方法。然而每个人都有自己的风格和理解,因此未来的 coding 之路,我会基于这本书的思想来做,但是多少会有些出入。总之,就是希望自己越来越好,努力成为牛逼闪闪的攻城狮之类的就不说了,至少要做到让自己身后的家人幸福快乐。
2017-02-13 完!