在 JavaScript 中设计 API 方法
探索命名参数、方法链和函数多态性
探索命名参数、方法链和函数多态性
学习 JavaScript API 方法设计是一项非常有用的技能,它能让你从不同的角度看待编程。这种视角让你不再是为用户构建项目,而是为开发者构建项目。如果你之前没有构建过库或 SDK,本文或许能帮助你了解如何着手进行方法设计。
JavaScript 是一种弱类型语言,我们作为开发人员可以利用这一点来创建强大、多用途的 API。
本文将介绍在 JavaScript 中设计 API 方法时需要牢记的几个规则。
1. 命名参数
我们首先要讨论的是命名参数。在 ES6 引入之前,声明函数的唯一方法是使用`function`function语法。为了确定执行流程,你需要获取函数的参数个数(即函数期望的参数数量),将其转换为数组,然后根据参数的格式应用后续逻辑。
在这个例子中,`a` animal、options`b` 和callback`c` 是函数的参数,参数个数为 3。该函数用于创建一个新账户,如果调用者没有提供账户的默认设置,则每个账户都会有一些默认设置:
function createAccount(
username = '',
password = '',
nickname = '',
email = '',
gender = 'Male',
bio = '',
subscription = 'Basic',
callback,
) {
if (!username || !password || !email) {
throw new Error(
'You are missing one or all of the following fields: "username", "password", "email"',
)
}
return api
.createAccount({
username,
password,
nickname,
email,
gender,
bio,
subscription,
})
.then((result) => {
if (callback) callback(null, result)
})
.catch((error) => {
console.error(error)
if (callback) callback(error)
})
}
createAccount(
'lucas',
'applebee123x123',
'',
'applebee1233@gmail.com',
'',
'My bio',
'Basic',
function cb(err, data) {
if (err) {
console.error(err)
}
// do something with data
},
)
问题在于,即使一个或多个参数并非必需,调用者也必须知道传递给函数的参数的确切顺序才能使函数正常工作。记住这些参数的顺序可能很困难,而且如果不小心,很容易弄错顺序。此外,如果某个参数并非函数正常运行所必需,那么将其设为必需参数就毫无意义。
将来维护起来也会很困难,因为当你或你的老板需要删除它username并将其设置email为新用户名时,你就必须更改逻辑。
更好的做法是直接使用对象:
function createAccount({
username = '',
password = '',
nickname = '',
email = '',
gender = 'Male',
bio = '',
subscription = 'Basic',
callback,
}) {
if (!username || !password || !email) {
throw new Error(
'You are missing one or all of the following fields: "username", "password", "email"',
)
}
return api
.createAccount({
username,
password,
nickname,
email,
gender,
bio,
subscription,
})
.then((result) => {
if (callback) callback(null, result)
})
.catch((error) => {
console.error(error)
if (callback) callback(error)
})
}
我们受益于代码的可读性以及对可维护性的更高控制,因为您只需从代码中删除用户名即可:
function createAccount({
password = '',
nickname = '',
email = '',
gender = 'Male',
bio = '',
subscription = 'Basic',
callback,
}) {
if (!password || !email) {
throw new Error(
'You are missing one or all of the following fields: "email", "password"',
)
}
return api
.createAccount({
password,
nickname,
email,
gender,
bio,
subscription,
})
.then((result) => {
if (callback) callback(null, result)
})
.catch((error) => {
console.error(error)
if (callback) callback(error)
})
}
通话内容也变得更加简洁易懂:
createAccount({
password: 'applebee123x123',
email: 'applebee1233@gmail.com',
bio: 'My bio',
callback: function cb(err, data) {
if (err) {
console.error(err)
}
// do something with data
},
})
2. 方法链/流畅 API
我最喜欢的编写 API 的方式是使用方法链式流畅 API 。
方法链式调用是指将多个方法依次调用。其主要目的是编写更易读、更流畅的代码,从而提高代码的可理解性。这些方法通常是动词(例如 rotate)。
例如:
getPhoto('../nemo_the_fish.jpg')
.applyFilter('grayscale', '100%')
.rotate(100)
.scale(1.5)
这意味着:“获取图像nemo_the_fish.jpg,应用灰度滤镜,灰度值为 100%,将图像旋转 100 度,并将缩放比例增加 1.5 倍。”
这种做法的好处在于,它可以让你快速上手编写自己的流畅 API 接口。你只需在方法调用中返回上下文的引用,以便可以链式调用:
const createWarrior = function createWarrior(name) {
let hp = 100
let battleCryInterval = 0
return {
bash: function(target) {
target -= 10
return this
},
// Increase the wrarior's health by 60, decrementing it by 1 every second for 60 seconds
battleCry: function battleCry() {
hp += 60
battleCryInterval = setInterval(() => {
hp -= 1
}, 1000)
setTimeout(() => {
if (battleCryInterval) {
clearInterval(battleCryInterval)
}
}, 60000)
return this
},
getHp: function getHp() {
return hp
},
}
}
const warrior = createWarrior('chris')
const otherWarrior = createWarrior('bob')
warrior
.battleCry()
.bash(otherWarrior)
.bash(otherWarrior)
.bash(otherWarrior)
.bash(otherWarrior)
.bash(otherWarrior)
const otherWarriorsHp = otherWarrior.getHp()
console.log(otherWarriorsHp) // result: 100
jQuery是流畅 API 的最佳示例之一,而且由于该库的流畅性,它可以说是最容易学习和使用的 JavaScript 库之一:
$(window).resize(function() {
$('#logbox').append('<div>The window resized</div>')
})
然而,方法链式流畅 API 也存在一些缺点。
最大的缺点在于很难在链式调用中间设置断点,导致错误难以调试。此外,它还会鼓励编写过多的过程式代码。不过,可以通过在链式调用的任何步骤插入日志记录器,并利用日志记录器调用后续方法来规避调试问题。
另一个缺点是,你可能会陷入使用密集点号输入法编写长句的陷阱。这会妨碍你保持代码的简洁性,所以请记住要保持代码的简洁。
3. 函数多态性
你可能在其他语言中听说过“多态性”这个术语,它通常指的是事物根据上下文表现出不同的行为。
同样的道理也适用于 JavaScript 中的函数多态性。这些函数会根据传入的参数(也就是我们的上下文)而表现出相应的行为。
API 通常会将参数收集到数组或类似数组的结构中,以便更好地控制它们。将参数放入数组结构中,可以实现诸如在同一作用域内将参数传递给其他函数,反之亦然等操作。
在箭头函数出现之前,多态函数内部通常使用类数组arguments对象来收集参数。有时,在将参数赋值给数组之后,你可能需要对参数进行更多操作。尽管 `arguments` 是一个类数组对象,但它并不像真正的数组那样工作,因为它缺少一些必要的数组函数——这非常局限。
开发者解决这个问题的方法是使用 `slice()` 创建一个单独的浅拷贝Array.prototype.slice.call()。这被称为 ` slice() method delegation`。换句话说,你将`slice()` 调用委托给了该Array.prototype对象。
例如:
const args = Array.prototype.slice.call(arguments, 0)
这将复制从索引 0 开始的项目,并返回之后的所有内容。
Arguments没有像.pushor 这样的真正方法.shift,所以我们用 将其转换为数组,Array.prototype.slice以便我们可以访问所有数组方法。
在 ES6 中,我们可以使用扩展运算符轻松地将其转换为数组,如下所示:
const someFunction = function(...args) {
console.log(args)
console.log(args.shift())
}
someFunction(1, 'hello', 'bob')
// result:
// [1, "hello", "bob"]
// 1
当您将参数放入数组或类似数组的结构中时,您可以根据参数的形式来确定执行逻辑的执行位置。这使得它非常灵活,可以用于多种用途,而无需编写过多的代码。
不扩散:
const applyFilter = function(filter, value, options) => {
const args = [].slice.call(arguments, 0)
console.log(args.length) // result: 2
}
applyFilter('grayscale', '100%')
随着传播:
const applyFilter = (...args) => {
console.log(args.length) // result: 1
}
applyFilter('grayscale', '100%')
基于以上考虑,我们现在可以根据这些参数确定如何处理执行:
const applyFilterToImage = (image) => {
return function applyFilter(...args) => {
// we can also grab args with [].prototype.slice.call(arguments, 0)
let options
let filters = {}
let callback
const arg1 = args[0]
// The caller wants to apply multiple filters
if (args.length === 1) {
if (arg1 && typeof arg1 === 'object') {
filters = { ...arg1 }
// Find out of the caller wants the new image with applied filters back by checking if a callback was passed in
const arg2 = args[1]
if (arg2 && typeof arg2 === 'function') {
callback = arg2
}
} else {
throw new Error(
'You must supply an object if you are only providing the first argument',
)
}
} else {
if (args.length > 2) {
// The caller passed in options as the third argument
if (typeof args[3] === 'object') {
options = args[3]
}
// The caller provided a callback function and wants the image with applied filters passed back
else if (typeof args[3] === 'function') {
callback = args[3]
}
}
// The caller wants to apply one filter
if (typeof arg1 === 'string') {
const filter = arg1
const value = args[1]
filters[filter] = value // or filters = { [filter]: value }
} else {
if (callback) {
callback(new Error('Filter is not a string'))
}
}
}
const newImg = api.filterImage(filters, options)
if (callback) {
return callback(null, newImg)
}
}
}
const img = '../bob_the_builder.jpg'
const applyFilter = applyFilterToImage(img)
const callback = (newImg) => {
console.log(newImg)
}
applyFilter({
grayscale: '100%',
rotate: 100,
scale: 1.5,
}, callback)
这个简单的函数允许开发者以多种方式使用它:
- 通过提供字符串和值作为第一个参数和第二个参数,应用单个过滤器。
- 或者,可以通过提供键/值对对象作为第一个参数来应用多个过滤器。
- 或者,可以通过提供包含一个筛选键/值的对象作为第一个参数来应用单个筛选器。
- (可选)通过回调函数接收新过滤后的图像,作为第二个或第三个参数(位置取决于他们传入的过滤器)。
结论
本文到此结束。敬请期待我未来的更多文章!
文章来源:https://dev.to/jsmanifest/designing-api-methods-in-javascript-1n30
