[Effective JavaScript 笔记]第6章:库和API设计--个人总结

时间:2023-01-10 20:38:39

前言

又到了一章的总结,这章里的内容。是把我从一个代码的使用者,如何换位成一个代码的编写者。如何让别人用自己的代码更容易,不用去注意太多的无用细节,不用记住冗长的函数名。在使用API时怎样避免使用者会出现理解的偏差。如何处理一些特殊敏感的值,参数如何设置可以更好地自说明,如何减少API对状态的依赖,如何使API更加灵活,更利于用户的编写。下面一一展开介绍,对应的也会说明每条对应希望给到的是哪方面的建议!

第53条:保持一致的约定

个人总结

不要去创造一个独特的API,要使用一些大家惯用的词汇,参数。比如使用统一的命名规范,相似属性设定相同的属性顺序。

命名规范

有好多种,比如:

  • 构造函数首字母大写。如MyClass

  • 普通函数,原型方法,变量,小写字母开始,其他单词首字母大写。如compareFn,a.setValue(),goodName。

  • 常量及全局变量,为全部字母大写。如PI=3.1415

  • 私有原型方法及属性,为_开头。如 _good, _getName()
    像以上的内容都是在js编写过程中的惯用法,当看到上面的命名时,自然而然会知道表达的是什么意思。

参数的设定

比如我们在使用jquery里的一个函数时,一些函数在传入一个参数,是取值操作。传入二个参数时是付值操作。

$('.a').css('width');
$('.a').css('width',100); $('a').attr('href');
$('a').attr('href','http://wengxuesong.cnblogs.com');

从上面也可以看出,这些函数在第一个参数传入的都是一个可以设定和获取的属性值。而第二个参数都是一个要使用的值。当我们遇到类似的函数时,我们在使用的时候就可以很快使用了。比如

$('.b').data('name');
$('.b').data('name','wxs');

很快就知道上面这个的意思啦~
而且使用的函数都是很容易记忆并符合函数意义的简单词汇。
可以看出,上面这样的一些设定,可以让我们解决前言中提到的一些问题。
如何让别人用自己的代码更容易,不用去注意太多的无用细节,不用记住冗长的函数名

提示

  • 在变量命名和函数签名中使用一致的约定

  • 不要偏离用户在他们的开发平台中很可能遇到的约定

第54条:将undefined看做“没有值”

个人总结

由于产生undefined的情况很多,所以在对待undefined的时候,想很好地区分它代表的真正的意义并不太容易。

产生undefined值的几种情况
  • 变量声明后没有赋值

  • 函数参数没有传入真正的实参数,形参的值

  • 函数运行没有返回值或直接return时,函数的运行结果的值

  • 对象不存在的属性的值

不能把undefined做为函数判断的特殊值进行处理,因为函数无法判定,这个参数是真正传递的为undefined,还是表达式运行的结果。要undefined只做为一个“没有值”来对待。如果要特殊处理一种情况,可以使用选项对象做为参数。

可选参数
  • 可选参数在未输入值时是undefined。这时的arguments.length不计入这个参数。

  • 当可选参数传入一个值为undefined的值时。这时的arguments.length会计入这个参数。

function Server(port,hostname){
if(arguments.length < 2){
hostname='localhost'
}
hostname=''+hostname;
return 'http://'+hostname+':'+port;
}
var s1=new Server(80,'cnblogs.com');//"http://cnblogs.com:80"var s2=new Server(80);//"http://localhost:80"var s3=new Server(80,undefined);//"http://undefined:80"

使用arguments.length来对函数可选参数进行判断会导致错误。应该对可选参数与undefined来比较来实现功能。

function Server(port,hostname){
if(hostname===undefined){
hostname='localhost'
}
hostname=''+hostname;
return 'http://'+hostname+':'+port;
}
var s1=new Server(80,'cnblogs.com');//"http://cnblogs.com:80"var s2=new Server(80);//"http://localhost:80"var s3=new Server(80,undefined);//"http://localhost:80"
隐式类型转换

在隐式类型转换为真时,undefined会转换为false。但除undefined外还有很多值也可以转换为false,如:null,""(空字符串),0和NaN。
在函数参数中使用真值测试时,设定默认值时。如果上面的0或空字符串为合法值时,就不能用真值来进行判断。

function W(w){
this.w=w||100;
}
var sw=new W(0);
sw.w;//100

上面如果传入0,则得到一个期望宽度为0的对象。
这种情况下必须显示地判断传入的值是否为undefined。

function W(w){
this.w=w===undefined?100:w;
}
var sw=new W(0);
sw.w;//0

这条解决了如何去处理一些特殊敏感的值。

提示

  • 避免使用undefined表示任何值

  • 使用描述性的字符串值或命名布尔属性的对象,而不要使用undefined或null来代表特定应用标志

  • 提供参数默认值应当采用测试undefined的方式,而不是检查arguments.length

  • 在允许0、NaN或空字符串为有效参数的地方,绝不要通过真值测试来实现参数默认值

第55条:接收关键字参数的选项对象

个人总结

总结起来就是,当函数接收很多参数时,参数的顺序无法改变(这里也称为位置参数),导致有些不需要的值也得传入,没法处理多个参数可选的情况。比如

function aaa(a,b,c){
//b,c都是可选的
}
aaa('a',,'c')

如上,如果我b不想传递只想传入a,c也必须传入把b传递进去。要不里面的代码就会出错。
上面的代码也记忆也是一种挑战,里面的每个参数应该对应什么样的值。
面对这种情况,可以选择使用选项对象。

选项对象

使用选项对象,选项对象里的属性名可以很好说明参数的作用。选项对象参数,只处理非必选的参数。对于非必选参数,可以提供一些默认值。

  • 如果仅包括可选参数。可能会省略掉所有的参数,选项对象包括所有参数。

  • 如果有一个或两个必选参数,使它们独立于选项对象。

使用每一项值的对于选项对象的默认值的设定。需要检测所有的非必选的参数项值。这个工作并不好做,需要去处理真值测试,或排除合理值的情况等问题。这样做起来比使用位置参数的方法更加烦琐。可以使用对象的覆盖处理方法,把对象覆盖的处理抽象出来,然后调用,这样就可以使代码的逻辑更加清晰。比如

function A(parent,message,opts){
opts=extend({
width:320,
height:240
});
opts=extend({
x:10,
y:10,
title:'alert',
modal:false
},opts);
extend(this,opts);
}

抽象出来的扩展函数为

function extend(target,source){
if(source){
for(var key in source){
var val=source[key];
if(typeof val !== 'undefined'){
target[key]=val;
}
}
}
return target;
}

一致性是库设计的目标,可以给API的使用者更好地预测它的功能及使用方法。
这条解决了使用API时怎样避免使用者会出现理解的偏差。如何处理一些特殊敏感的值,参数如何设置可以更好地自说明。

提示

  • 使用选项对象使得API更具有可读性、更容易记忆

  • 所有通过选项对象提供的参数应当被视为可选的

  • 使用extend函数抽象出从选项对象中提取值的逻辑

第56条:避免不必要的状态

个人总结

API分为两类:有状态的和无状态的。无状态的API相当于纯函数,行为只取决于输入,与程序所处的环境状态无关。有状态的方法,对于同一个方法可能返回不同的结果。主要取决于方法所处的状态。
无状态的API更容易学习和使用,因为所有的行为都是函数本身决定的,只要查询相关函数的代码就能知道输出是否正确。
有状态的API处于不同的状态有可能会产生不同的结果。导致在记忆和使用时都要记住额外的信息。状态的改变也常常会带来代码的耦合度更高。无状态的API可以使代码更加模块化,避免与其它代码产生耦合。
在设计API时,无状态的API更好。

提示

  • 尽可能地使用无状态的API

  • 如果API是有状态的,标示出每个操作与哪些状态有关联

第57条:使用结构类型设计灵活的接口

个人总结

结构类型(鸭子类型):任何对象只要具有预期的结构就属于该类型。这个类似于强类型面向对象语言里说的接口,也就是面向接口编程。
结构类型可以有利于单元测试,可以很容易去实现一个测试的数据结构。
结构类型也可以使代码各部分解耦,代码的依赖只是通过结构类型。
在实现代码时,不用去管结构类型最终的实现细节,只要提供对应的方法及属性,那么程序就可以正常运行。

提示

  • 使用结构类型来设计灵活的对象接口

  • 结构接口更灵活、更轻量,所以应该避免使用继承

  • 针对单元测试,使用mock对象即接口的替代实现来提供可复验的行为

第58条:区分数组对象和类数组对象

个人总结

分离数组对象

数组对象和类数组对象,使用类型判断可以直接区分。

var a=[];
var b={0:1,length:1};
var toString=({}).toString;
toString.call(a);//"[object Array]"
toString.call(b);//"[object Object]"

这样一目了然。但这与js的灵活的类数组对象的概念有争执,因为任何对象都可被视为数组,只要它遵循正确的接口。上一条使用结构类型设计灵活的接口。灵活的结构只要一个数据符合相应的接口,就可以把它视为正确的数据。

重载

重载两种类型意味着必须有一种方法来区分两种不同情况。如果出现了两种情况的重叠区域,则无法对API进行重载。
API绝不应该重载与其他类型有重叠的类型。

Array.isArray函数

这个函数测试一个值是否是数组,而不管原型继承。

var a={};
var b=[];
var c=10;
Array.isArray(a);//falseArray.isArray(b);//trueArray.isArray(c);//false

可以直接区分,数组与类数组。

类数组转化为数组
var slice=[].slice;
var b={0:10,1:'good',length:2};
slice.call(b);//[10,'good']

如果对API的传入参数有特殊指定要求的,需要在文档中注明,API的重载也必须要注明,不同情况的参数要求。

提示

  • 绝不重载与其他类型有重叠的结构类型

  • 当重载一个结构类型与其他类型时,先测试其他类型

  • 当重载其他对象类型时,接收真数组而不是类数组对象

  • 文档标注你的API是否接收真数组或类数组值

  • 使用ES5提供的Array.isArray方法测试真数组

第59条:避免过度的强制转换

个人总结

重载和强制转换

重载基于类型的判断,而强制转换使参数类型信息丢失,导致结果和预期不同。
在使用参数类型来作为重载依据时,应该避免强制转换。
重载时通过对参数的类型进行强制要求,来实现API的设计,这样代码更谨慎。

防御性编程

以额外的检查来抵御潜在的错误。抵御所有的错误是不可能的。除js中提供的基本检查工具外,可以通过编写一些简洁的检查工具函数来辅助开发。
额外的代码会影响程序的性能,也可以更早地捕获错误。看具体情况来使用防御性编程。

提示

  • 避免强制转换和重载的混用

  • 考虑防御性地监视非预期的输入

第60条:支持方法链

个人总结

js里自带的方法链式调用,如字符串的replace方法

function escapeBasicHTML(str){
return str.replace(/&/g,"&amp;")
.replace(/< /g,"&lt;")
.replace(/>/g,"&gt;")
.replace(/"/g,"&quot;")
.replace(/'/g,"&apos;");
}

数组方法

var users=records.map(function(record){
return record.username;
})
.filter(function(username){
return !!username;
})
.map(function(username){
return username.toLowerCase();
});

这些都可以写成传统的方式,把每次的返回值保存到中间变量。
实现方法链的关键是,每次都返回下一个方法的对象。
无状态的API中,可以返回一个新对象,则链式得到了自然的结果。像上面的replace方法,返回的是一个新的字符串对象。
有状态的API也值得使用,这时方法链被称为流畅式。这个好举例子,如jQuery里的方法操作。

$('body').html('good').addClass('y');

方法链的书写风格,需要代码进行一定的处理才能支持(返回对象自身)。方法链的代码都可以改写成传统的风格。

提示

  • 使用方法链来连接无状态的操作

  • 通过在无状态的方法中返回新对象来支持方法链

  • 通过在有状态的方法中返回this来支持方法链

总结

纵观这一章,从比写具体代码更高的一个层次给了指导。前前后后,每条都提出了,文档很重要。有特殊要求和惯用法不同的都要文档说明。

  • 在处理API接口命名时,保持命名约定一致,不偏离用户习惯。

  • 处理undefined时要特别对象,不能简单处理。

  • 参数使用选项参数,说明性更强,更容易记忆和使用。

  • 设计无状态的API接口,更容易模块化和使用。

  • 面向结构类型编程,API更灵活。

  • 函数重载要注意不同情况,不能有重叠区域。

  • 强制转换会破坏依赖类型判断的重载。

  • 支持方法链,使代码书写起来更流畅。

倒数第二章了,还有一章就要完事了。这一章的内容虽然硬知识点没有多少,但我看起来是这几章里就吃力的,因为找不到着力点。编码的多少直接就决定了对这章各条的理解程度,这章可以在写一些代码后回来再反复看一下,掌握后就内化为自己的能力啦。