nevenleung / blog Goto Github PK
View Code? Open in Web Editor NEW记录一些自己思考成果的地方
记录一些自己思考成果的地方
sum(1, 2, 3).sumOf(); //6
sum(2, 3)(2).sumOf(); //7
sum(1)(2)(3)(4).sumOf(); //10
sum(2)(4, 1)(2).sumOf(); //9
sum(1, 2, 3).sumOf(); //6
sum(...arr).sumOf()
, 具有计算所有输入参数的总和的功能.sumOf()
获取到参数的信息呢?// version 1
function sum(...args) {
let totalArgs = [...args];
// 由于 `sumOf()` 跟在 `sum()` 这个函数调用后
// 可以推断,执行 `sum()` 后返回的是一个对象
// 那不如把 `sumOf` 方法绑定到 `this` 对象上吧
this.sumOf = function() {
// 原理: sumOf 是一个闭包,它拥有 `totalArgs` 的访问权
return totalArgs.reduce((a, b) => a + b, 0);
}
// 为了能让 `sumOf` 被链式调用,`sum` 就需要返回 `this`
return this;
}
sum(2, 3)(2).sumOf(); //7
sum()
,返回是一个函数(我们称为 fn
)fn
函数被调用后,它的返回的是一个对象,它仍然拥有 sumOf
方法,来计算总和sum
和 fn
调用,貌似是在做相同的事情
sumOf
方法
sum
返回的是一个(可以被调用的)函数
sumOf
方法fn
返回的是一个对象// version 2, 对第一个版本的代码,进行修改
function sum(...args) {
let totalArgs = [...args];
// 在 `sum` 内部声明了一个函数 innerFn
function innerFn(...innerArgs){
// code
}
// 为 `innerFn`对象,添加一个 `sumOf` 方法
innerFn.sumOf = function() {
return totalArgs.reduce((a, b) => a + b, 0);
}
return innerFn;
}
sum(1)(2)(3)(4).sumOf(); //10
sum(2)(4, 1)(2).sumOf(); //9
sum
,或者是前面假定的 fn
返回的函数对象,确定它可以做两件事
sumOf
方法sum
,或者的 fn
返回的函数对象,可以被无限连续地调用// version 3,补充 `innerFn`的函数体内容,得到最后的答案
function sum(...args) {
let totalArgs = [...args];
function innerFn(...innerArgs) {
// 将 `innerFn` 接收到的参数值,汇总到 `totalArgs` 中
totalArgs = [...totalArgs, ...innerArgs];
// 通过推断 `sum` 和 `innerFn` 返回同一个函数对象,而且它可以被无限调用(想到递归了吗?)
// 这里确定代码,调用 `innerFn`,返回它自己
// 我想,这就是这道题最难的地方(哭笑不得.jpg)
return innerFn;
}
innerFn.sumOf = function() {
return totalArgs.reduce((a, b) => a + b, 0);
}
return innerFn;
}
function sum(...args) {
let totalArgs = [...args];
function innerFn(...innerArgs) {
totalArgs = [...totalArgs, ...innerArgs];
// the key to the problem
return innerFn;
}
innerFn.sumOf = function() {
return totalArgs.reduce((a, b) => a + b, 0);
};
return innerFn;
}
sumOf
方法)callback(回调)在 javascript 的程序设计中被大量使用,比如,作为事件监听的响应函数,定时器的回调函数,异步请求结果的处理函数,第三方库中的钩子函数等等。使用回调进行的程序设计,通常被称为「回调模式」。
下面是一个常见的 callback 使用例子:
function fetchData(url, callback) {
let xhr = new XMLHttpRequest();
xhr.onerror = function() {
callback(new Error(xhr.statusText));
};
xhr.onload = function() {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
callback(null, xhr.responseText);
} else {
console.log("Request was failed: " + xhr.status);
}
};
xhr.open("get", url, true);
xhr.send(null);
}
fetchData("example.com", function(err, result) {
if (err) {
console.error(err);
} else {
console.log(result);
}
});
上面例子中的xhr.onload
实际上是一个 DOM 0 级事件处理函数,跟经常看到的btn.onclick
之间并没有太多的区别。对于xhr
的请求结果,我们无法像同步编程那样,在相应的操作执行完毕后,通过 return 关键字将操作结果返回。这是因为fetchData()
内部的xhr.onload
是异步执行的,它可能马上就会执行,也可能因为网速不佳,会在之后的某个时间点才执行,总之,我们无法保证它何时会执行。不管是将请求结果赋给一个局部变量或者全局变量,再将这个值 return,还是 return 一个可以访问到请求结果的闭包函数,都没有办法保证在请求结果从服务器返回后,第一时间读取到请求结果,更别说其他可能依赖于请求结果的操作了。
这也就是为什么在 JS 中的事件处理函数,往往是以回调函数的形式出现。我们需要在事件被触发的时候,才去执行相应的处理逻辑。因此,传入一个回调函数的指针,让 javascript 引擎在相应事件被触发时,才去调用我们先前传入的回调函数,以达到异步调用代码的目的。这就是 JS 中进行异步编程的原始方式。
callback 除了在异步编程中方面的应用,还是 JS 中的一种程序设计技巧。比如在第三方的库中,往往可以看到库函数的 api 中,支持传入一个 callback,函数只提出了基本的参数要求,剩余的部分交给 callback 来定制想要实现的功能。这里使用 JS 数组的 map、filter 方法来举例:
let arr = [1, 2, 3];
let result;
result = arr.map(function(item, index, array) {
return item + 1;
});
console.log(result); // [2, 3, 4]
result = arr.map(function(item, index, array) {
return item * 10;
});
console.log(result); // [10, 20, 30]
result = arr.filter(function(item, index, array) {
return item % 2 === 0;
});
console.log(result); // [2]
result = arr.filter(function(item, index, array) {
return item >= 2;
});
console.log(result); // [2, 3]
通过给相同的函数或者方法传入不同的 callback,来完成不同的任务。「回调模式」,它在带来代码使用灵活性的同时,变现提高了代码的复用率。
function fn(param, callback) {
// code
}
fn(param, callback);
fn(param, anotherCallback);
此外,使用回调函数,有一个点是值得注意的。通常,传入一个匿名函数作为回调,就可以解决问题,但使用匿名函数作为回调,尤其是回调的代码逻辑相对复杂时,代码的可读性会大大地降低,其他人必须读懂你的匿名函数,才能理解你传入的回调函数的作用。使用一个合适的声明函数的函数名,作为回调函数的指针,即可在某种程度上,缓解这样的问题。我们需要有意识的去管理好回调函数。
function fetchData(url, callback) {
let xhr = new XMLHttpRequest();
xhr.onerror = function() {
callback(new Error(xhr.statusText));
};
xhr.onload = function() {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
callback(null, xhr.responseText);
} else {
console.log("Request was failed: " + xhr.status);
}
};
xhr.open("get", url, true);
xhr.send(null);
}
// 新封装的方法
function showResponse(err, result) {
if (err) {
console.err(err);
} else {
console.log(result);
}
}
fetchData("example.com", showResponse); // 改动的地方
以上的例子,将原来的的匿名函数单独封装成名为showResponse
的声明函数,在调用fetchData
时,可以感受到使用声明函数作为回调,带来的代码可读性的提高。
Promise 是 JS 中进行异步编程的一个重要的工具。Promise
对象包含了异步操作是否完成(或失败),以及它的操作结果。在 Promise 被正式写入到标准文件之前,有很多的第三方库实现了 Promise 的功能。而在 ES2015 中,Javascript 正式引入了 Promise。
相比于 callback,Promise 具有更易读的代码组织形式(将有依赖的异步操作使用链式结构组织起来),更好的异常处理方式(无需为每个异步操作添加异常处理,只需在调用 Promise 的末尾添加上一个catch
方法捕获异常即可),以及异步操作并行处理的能力(Promise.all()
)等优点。
下面将前面使用 callback 的fetchData
函数改造成一个返回 Promise 的函数:
function fetchDataWithPromise(url) {
// fetchDataWithPromise返回一个Promise
return new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest();
xhr.onerror = function() {
reject(new Error(xhr.statusText)); // reject
};
xhr.onload = function() {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
resolve(xhr.responseText); // resolve
} else {
console.log("Request was failed: " + xhr.status);
}
};
xhr.open("get", url, true);
xhr.send(null);
});
}
fetchDataWithPromise("example.com") // 使用 Promise 的 then() 和 catch() 来设置相应的处理函数
.then(function(res) {
console.log(res);
})
.catch(function(err) {
console.error(err);
});
这里,我将原来的fetchData
函数直接作为 Promise 的 executor function(执行器函数),然后将原来的onload
和onerror
回调内部的callback
分别替换为resolve
和reject
,分别将这两处的结果使用resolve
和reject
来调用(这是最简单粗暴的解决方式,有的时候可以有优化的空间)。
Promise 给 JS 带来了不一样的异步编程方式,但它本质上还是 callback。为什么这么说呢?假如我将前面的 callback 例子修改一下,再与 Promise 的例子进行对比。
function fetchData(url, successHandler, errorHandler) {
// code
}
fetchData('example.com', successHandler, errorHandler);
function fetchDataWithPromise(url) {
return new Promise(function(resolve, reject) {
// code
}
}
fetchDataWithPromise('example.com')
.then(fulfillHandler, rejectHandler); // 作用等同于.then(fulfillHandler).catch(rejectHandler)
对比调用 fetchData
和 fetchDataWithPromise
的代码。到这里,我想你可能会发现,Promise 与 callback “长得很像”。这里不去讨论 Promise 的具体源码实现,可以联想到 Promise “继承了” callback 的大部分特性,Promise 是强化版的 callback,它拥有这一节开头提到的那些优点。
Promise 只是换了一种样子的 callback 吗?当然不是。如果仅仅是将老代码改造成为 Promise,这当然没有太大的作用,但当把一个个异步操作分别封装成合适的返回 Promise 的函数之后,我们可以借助 Promise 的特性来将一个一个异步操作函数组合起来,形成一个 Promise chain,这时才能发挥 Promise 的最大作用。 这个留在将 callback、Promise、async/await 三者进行比较时,再给出代码例子。
在 ES2017 中,JS 中新增了一种异步编程的语法,async/await。它的作用是,改进 JS 中异步操作串行执行的代码组织方式,可以有效减少 callback 的嵌套,但 async/await 本身不提供并行执行的能力,因此,在 JS 中进行异步操作并行执行还是要使用Promise.all()
或者将多个Promise.then()
单独使用。 使用 async/await,可以让异步编程以更为直观的方式来呈现(近似于同步编程的写法,但它执行的却是异步代码),它仍然是基于 Promise 实现的一种语法。 在《understand ES6》的 Promise 章节中,作者给出了一种将 Promise 和 Generator 结合使用,达到类似于 async/await 效果的代码。
与 Promise 是基于 callback 进行的改进不同的是,async/await 是对 Promise 的链式结构上进行的改进,使用 async/await 不能离开 Promise。 在await
操作符之后,通常紧跟着一个 Promise,await
对后面的表达式进行计算,将返回 Promise 的 resolve 结果,如果后面的表达式不是 Promise,将自动将它转化为Promise.resolve(value)
。此外 async 函数隐式地返回一个 Promise,这个可以是你代码中明确返回的一个 Promise,或者将你指定的返回值转化为Promise.resolve(value)
,将这个 Promise 返回。如果没有指定任何返回值,则会返回Promise.resolve(undefined)。
在 async 函数中,遇到 await 操作符,内部的代码就会暂停执行,当 await 操作符的运算完成(即得到 Promise 的 resolve 值),后续代码会恢复执行,只有依赖的异步操作成功执行,后续代码才会继续执行。如果 await 操作符得到的是 reject 的值,或 await 后的 Promise 在执行中抛出错误,async 函数就会抛出异常,后续代码就不会被执行了。由于 async/await 语法中并没有添加额外的异常处理方法,所以应当使用传统的try-catch
结构对异常进行捕获。此外,使用浏览器开发者工具对 async 函数进行 debug,要比在 Promise chain 中容易得多。
如果只是将一个原本传入 callback 的异步操作函数改造成一个 async 函数没有意义的,同样将一个 Promise 用 async 函数封装起来也没有任何意义,需要在await
关键字的下一行中放置依赖于异步操作完成结果的代码才有意义。也就是说,将依赖于异步操作的代码,通过 async/await 组织起来才是有意义。
async function fetchDataWithAsync(url) {
let result;
// 使用 try-catch 来处理 fetchDataWithPromise 中出现的异常和错误信息
try {
result = await fetchDataWithPromise(url);
console.log(result);
} catch (err) {
// 处理上面可能返回的 Promise.reject(value)
// 通过 try-catch,我们就可以根据所执行的异步操作,给出更为合适的 rejectedHandler。
console.err(err);
result = await fallbackFetch(url);
// 而不是让 fetchDataWithAsync() 被其他异步代码调用后,
// 由最外层的 Promise.then().catch() 的 .catch() 捕获这里出现的错误信息
}
// 另一种不使用 try-catch,进行 error handling 的方式,
// result = await fetchDataWithPromise(url).catch(rejectedHandler);
return result;
}
fetchDataWithAsync("example.com")
.then(function(result) {
console.log(result);
})
// 由于在 async function 中,已经通过 try-catch 处理异常和错误信息,
// 这里的 catch() 并不会捕获到 fetchDataWithAsync() 内部出现的异常与错误信息
.catch(function(err) {
console.error(err);
});
虽然 callback 很强大,但 callback 也不是没有缺点的。
下面是另一个例子,回调的嵌套使用,它通常被称为「回调地狱」(callback hell or the pyramid of doom)。
function getData(dbName, tableName, query, callback) {
SomeDB.connectDB(dbName, function(err, db) {
if (err) {
console.error(err);
} else {
db.useTable(tableName, function(err, table) {
if (err) {
console.error(err);
} else {
table.findById(query, function(err, result) {
if (err) {
console.error(err);
} else {
callback(result);
}
});
}
});
}
});
}
getData("FinalExam", "Math", "002", function(result) {
console.log(result);
});
以上的例子是数据库的一个数据获取函数,我需要在找到相应的数据后,根据找到的结果进行一定的处理。它做了下面几件事,首先,它要根据dbName
去连接相应的数据库,之后再根据tableName
选中相应的数据表,然后它根据传入query
去数据库中查找相应的数据,在找到这个数据后,执行传入的 callback
。仔细观察,在getData
的内部有一个 callback 的嵌套,table.findById
的依赖于db.useTable
的执行结果,db.useTable
又依赖于SomeDB.connectDB
的执行结果,它们之间很自然地形成了一个回调的嵌套。
即使有了代码不同层级的缩进,但这些一级又一级的,像“楼梯”一样的代码还是不利于人们去阅读理解代码的逻辑层次。这是「回调地狱」的代码非常不受欢迎的一个重要原因。
这里,我尝试将前面例子所有的异步操作分别改写成单独的 promise,再用 Promise chain 把它们组合起来。
// 分别将原来的每一个异步操作分别改写成 promise
function connectDB(dbName) {
return new Promise(function(resolve, reject) {
someDB.connectDB(dbName, function(err, db) {
if (err) {
reject(err);
} else {
resolve(db);
}
});
});
}
function useTable(db, tableName) {
return new Promise(function(resolve, reject) {
db.useTable(tableName, function(err, table) {
if (err) {
reject(err);
} else {
resolve(table);
}
});
});
}
function findById(table, query) {
return new Promise(function(resolve, reject) {
table.findById(query, function(err, findResult) {
if (err) {
reject(err);
} else {
resolve(findResult);
}
});
});
}
// 利用Promise的返回值,将异步操作串联起来,形成链式结构
function getDataWithPromise(dbName, tableName, query) {
// 返回一个 Promise
return connectDB(dbName)
.then(function(db) {
return useTable(db, tableName); // onFulfill 函数中返回另一个 Promise
})
.then(function(table) {
return findById(table, query); // 最终,返回 findById 这个 Promise
});
}
getDataWithPromise("FinalExam", "Math", "002")
.then(function(result) {
console.log(result);
})
.catch(function(err) {
console.error(err);
});
注意getDataWithPromise
函数内部的变动,这里利用 Promise 的返回值,在一个 onFulfill 函数中返回useTable(db, tableName)
这个 Promise,这个内部函数返回 Promise 的.then()
,它的运算结果就是这个返回的 Promise,所以才会有后一个.then()
的调用,在后一个.then()
的 onFulfill 函数中可以访问到useTable(db, tableName)
resolved 的table
值。改写成 Promise chain 后,没有了嵌套的代码,每一个对前面有依赖的异步操作都写在了.then()
中,
Promise 还有更为简洁的用法,假如除了开头的 Promise 以外,其他 Promise 都不依赖任何的外部参数(可以依赖前一个 Promise 的 resolve 的值),甚至可以写成这样。
function shoppingCartSettlement(cartList) {
return checkTheStock(cartList)
.then(getTheTotalPrice)
.then(waitForPayment)
.then(showTheOrderInfo);
}
这是一个购物车结算的例子,步骤分别为检测库存,计算商品总额,等待付款,显示订单信息。
这里只是对.then()
中的回调函数作了一层封装(回忆一下前文中讲到的管理回调的例子),它们都是接收前一个 Promise 返回的结果作为参数,之后又返回另一个 Promise 给后面步骤的回调函数。
这里复用了前面封装的返回 Promise 的函数,再使用 async/await 改写getDataWithPromise
函数。
// 继续沿用前面封装的 Promise
async function getDataWithAsync(dbName, tableName, query) {
// 省略了对于每一个步骤的 error handling
const db = await connectDB(dbName);
const table = await useTable(db, tableName);
return findById(table, query);
}
getDataWithAsync("FinalExam", "Math", "002")
.then(function(result) {
console.log(result);
})
.catch(function(err) {
console.error(err);
});
从getDataWithAsync
可以看出,这个 async 函数内部的代码逻辑就像是同步代码(但要注意await
操作符运算的是一个异步操作),前后逻辑很清晰,而且没有回调的嵌套,也没有.then()
的链式调用,也不需要在.then()
中不断的return
另一个 Promise。在这里可以很明显地体现出了 async/await 的作用以及价值,但它仍然离不开 Promise。
对于简单的异步代码,callback 仍然是可以胜任的。如果只是将一个使用 callback 的异步操作作为 Promise 的 executor 封装起来,作用并不大,仅仅是在调用这些单个的异步操作时,代码风格更为统一,Promise.then().catch()
。而当遇到需要将多个异步操作的串行或者并行执行时,则需要考虑使用 Promise 跟 async/await 了。
对于异步操作嵌套的情况,使用 Promise 和使用 async/await 可以极大程度的减少 callback 的嵌套,提高代码的可读性。比起 Promise,async/await 又有着更简洁的语法,更符合普通人阅读代码的习惯。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.