我最喜欢的网站之一是BerkshireHathaway.com--它简单,有效,而且自1997年推出以来一直正常运行。更值得注意的是,在过去的20年中,这个网站颇有可能从未出现过错误。为何?由于它都是静态的。它自20多年前推出以来几乎同样。若是你预先拥有全部数据,那么构建网站很是简单。不幸的是,如今大多数网站都没有。为了弥补这一点,咱们发明了“模式”来处理为咱们的应用程序提取外部数据。像大多数事情同样,这些模式都随着时间的推移而发生变化。在这篇文章中,咱们将分析这三种最多见的模式的优缺点,模式分别是回调(Callbacks),Promises,和Async/Await 并从历史背景谈论它们的意义和进展。javascript
让咱们从这些数据获取的最初的模式开始,回调(Callbacks)java
我假设你彻底不知道什么是回调。若是我假设错了,只需向下滚动一下跳过。git
当我第一次学习编程时,它帮助我将函数理解为机器。这些机器能够作任何你想要的东西。他们甚至能够接受输入并返回一个值。每台机器上都有一个按钮,你能够在须要机器运行时按下该按钮,即()。github
function add (x, y) { return x + y } add(2,3) // 5 - 按下按钮,执行机器
不管我按下按钮,你按下按钮,或者别人按下按钮无所谓。不管什么时候按下按钮,机器都将运行。编程
function add (x, y) { return x + y } const me = add const you = add const someoneElse = add me(2,3) // 5 - Press the button, run the machine. you(2,3) // 5 - Press the button, run the machine. someoneElse(2,3) // 5 - Press the button, run the machine.
在上面的代码,咱们分配add函数,三个不一样的变量,me,you,和someoneElse。重要的是要注意add咱们建立的原始变量和每一个变量都指向内存中的相同位置。它们在不一样的名称下彻底相同。因此,当咱们调用me时you,或者someoneElse,就好像咱们正在调用同样add函数。
如今若是咱们把add机器送到另外一台机器怎么办?请记住,按下()按钮并不重要,若是按下它,它就会运行。json
function add (x, y) { return x + y } function addFive (x, addReference) { return addReference(x, 5) // 15 - Press the button, run the machine. } addFive(10, add) // 15
你的大脑可能在这一点上有点奇怪,但这里没有新的东西。咱们不是“按下按钮” add,而是add做为参数传递addFive,重命名它addReference,而后咱们“按下按钮”或调用它。api
这突出了JavaScript语言的一些重要概念。首先,正如你能够将字符串或数字做为参数传递给函数同样,你也能够将函数的引用做为参数传递。当执行此操做时,做为参数传递的函数称为回调函数,而且将回调函数传递给的函数称为高阶函数。数组
由于词汇很重要,因此这里的代码与从新命名的变量相同,以匹配他们演示的概念。promise
function add (x,y) { return x + y } function higherOrderFunction (x, callback) { return callback(x, 5) } higherOrderFunction(10, add)
这种模式应该看起来很熟悉,无处不在。若是你曾经使用过任何JavaScript Array方法,那么你已经使用了回调。若是你曾经使用过lodash,那么你已经使用过回调。若是你曾经使用过jQuery,那么你已经使用了回调。安全
[1,2,3].map((i) => i + 5) _.filter([1,2,3,4], (n) => n % 2 === 0 ); $('#btn').on('click', () => console.log('Callbacks are everywhere') )
一般,回调有两种常见的用例。第一,咱们看下.map和_.filter例子,是翻转一个值到另外一个很好的抽象。咱们说“嘿,这是一个数组和一个函数。来吧,根据我给你的函数给我一个新的值“。第二个,也就是咱们在jQuery示例中看到的,是将函数的执行延迟到特定时间。“嘿,这是这个函数。每当btn点击具备id的元素时,请继续调用它。“这是咱们将关注的第二个用例,”延迟执行函数直到特定时间“。
如今咱们只看了同步的例子。正如咱们在本文开头所讨论的那样,咱们构建的大多数应用程序都没有预先得到所需的全部数据。相反,他们须要在用户与应用程序交互时获取外部数据。咱们刚刚看到回调如何成为一个很好的用例,由于它们再次容许你“延迟执行函数直到特定时间”。看看咱们如何使该句子适应数据提取并不须要太多想象力。咱们能够延迟函数的执行,直到咱们得到所需的数据,而不是将函数的执行延迟到特定时间。这多是最流行的例子,jQuery的方法:getJSON。
// updateUI and showError are irrelevant. // Pretend they do what they sound like. const id = 'tylermcginnis' $.getJSON({ url: `https://api.github.com/users/${id}`, success: updateUI, error: showError, })
在得到用户数据以前,咱们没法更新应用的UI。那么咱们该怎么办?咱们说,“嘿,这是一个对象。若是请求成功,请继续调用success并传递用户的数据。若是没有,请继续调用error并传递错误对象。你不须要担忧每种方法的做用,只要确保在你应该的时候调用它们。这是使用异步请求回调的完美演示。
在这一点上,咱们已经了解了回调是什么以及它们如何在同步和异步代码中都有用处的。咱们尚未谈到的是回调的黑暗面。请看下面的代码。你能说出发生了什么吗?
// updateUI, showError, and getLocationURL are irrelevant. // Pretend they do what they sound like. const id = 'tylermcginnis' $("#btn").on("click", () => { $.getJSON({ url: `https://api.github.com/users/${id}`, success: (user) => { $.getJSON({ url: getLocationURL(user.location.split(',')), success (weather) { updateUI({ user, weather: weather.query.results }) }, error: showError, }) }, error: showError }) })
若是以为有帮助,你能够在这里玩实时版本。
请注意,咱们添加了一些回调层。首先,咱们说在btn点击具备id的元素以前不要运行初始的AJAX请求。单击按钮后,咱们会发出第一个请求。若是该请求成功,咱们会发出第二个请求。若是该请求成功,咱们将调用updateUI从两个请求得到的数据的方法。不管你是否乍一看是否理解了代码,客观地说它比之前的代码更难阅读。这将咱们带到“回调地狱”的主题。
做为人类,咱们很天然地会顺序思考。当你在嵌套回调中嵌套回调时,它会强迫你超出你天然的思惟方式。当你的软件阅读方式与天然思考方式之间存在脱节时,就会发生错误。
像大多数软件问题的解决方案同样,一种使“回调地狱”更容易消费的经常使用方法是模块化你的代码。
function getUser(id, onSuccess, onFailure) { $.getJSON({ url: `https://api.github.com/users/${id}`, success: onSuccess, error: onFailure }) } function getWeather(user, onSuccess, onFailure) { $.getJSON({ url: getLocationURL(user.location.split(',')), success: onSuccess, error: onFailure, }) } $("#btn").on("click", () => { getUser("tylermcginnis", (user) => { getWeather(user, (weather) => { updateUI({ user, weather: weather.query.results }) }, showError) }, showError) })
若是以为有帮助,你能够在这里玩实时版本。
好的,函数名称能够帮助咱们更加了解正在发生的事情,但客观上是“更好”吗?并非不少。咱们只是在回调地狱的可读性问题上加了一个创可贴。问题仍然存在,咱们天然地按顺序思考,即便有额外的功能,嵌套的回调也会使咱们摆脱顺序的思惟方式。
下一期回调与控制反转有关。当你编写一个回调时,假设你给回调的程序是负责的,而且会在它应该的时候(而且只有当它)时调用它。其实是将程序控制权转换为另外一个程序。当您处理jQuery,lodash甚至vanilla JavaScript等库时,能够安全地假设使用正确的参数在正确的时间调用回调函数。可是,对于许多第三方库,回调函数是您与它们交互方式的接口。第三方库不管是故意的仍是偶然的,均可以打破他们与你的回调互动的方式,这是彻底合情合理的。
function criticalFunction () { // It's critical that this function // gets called and with the correct // arguments. } thirdPartyLib(criticalFunction)
既然你不是那个调用者criticalFunction,你就能够控制调用它的时间和参数。大多数时候这不是问题,可是当它出现问题时,这是一个很大的问题。
你有没有预订去过一个繁忙的餐馆?当这种状况发生时,餐厅须要一种方法在桌子打开时与你联系。从历史上看,当你的桌子准备就绪时,他们只会取你的名字并大喊大叫。而后,天然而然地,他们决定开始变幻想。一个解决方案是,一旦桌子打开,他们就会取你的号码并给你发短信,而不是取你的名字。这使您能够超出大喊大叫的范围,但更重要的是,它容许他们随时根据须要定位你的手机广告。听起来有点熟?这应该!好吧,也许不该该。这是回调的隐喻!将你的号码提供给餐馆就像给第三方服务提供回拨功能同样。你但愿餐厅在桌子打开时给您发短信,就像你同样指望第三方服务在什么时候以及如何表达时调用你的功能。一旦你的号码或回叫功能掌握在他们手中,您就失去了全部控制权。
值得庆幸的是,存在另外一种解决方案。一个设计,容许您保持全部控制。你甚至可能之前都经历过 - 这是他们给你的小嗡嗡声。你知道,这个。
若是你以前从未使用过,那么这个想法很简单。他们没有取你的名字或号码,而是给你这个设备。当设备开始嗡嗡做响并发光时,你的桌子就准备好了。当你等待桌子打开时,你仍然能够作任何你想作的事,但如今你没必要放弃任何东西。事实上,偏偏相反。他们必须给你一些东西。没有控制倒置。
蜂鸣器始终处于三种不一样状态中的一种- pending,fulfilled或rejected。
pending是默认的初始状态。当他们给你蜂鸣器时,它处于这种状态。
fulfilled 当蜂鸣器闪烁而且你的桌子准备就绪时蜂鸣器所在的状态。
rejected当出现问题时,蜂鸣器处于状态。也许餐厅即将关闭,或者他们忘了有人在晚上出租餐厅。
一样,要记住的重要一点是,你,蜂鸣器的接收器,拥有全部的控制权。若是蜂鸣器进入fulfilled,你能够去你的桌子。若是它被放入fulfilled而且你想忽略它,那么很酷,你也能够这样作。若是它被放入rejected,那很糟糕,但你能够去别的地方吃。若是没有任何事情发生而且它留在pending,你永远不会吃,但你实际上并无任何东西。
如今你已成为餐厅蜂鸣器的主人,让咱们将这些知识应用到重要的事情上。
若是给餐厅你的号码就像给他们一个回调功能,接收这个小小的东西就像收到所谓的“Promise”。
一如既往,让咱们从为何开始吧。为何Promises存在?它们的存在使得使异步请求更易于管理的复杂性。彻底像蜂鸣器,一个 Promise能够处于三种状态之一pending,fulfilled或者rejected。与蜂鸣器不一样,它们表明表示餐馆桌子状态的这些状态,它们表明异步请求的状态。
若是异步请求仍在进行中,则Promise状态为pending。若是异步请求成功完成,则Promise状态将更改成fulfilled。若是异步请求失败,Promise则将更改成状态rejected。蜂鸣器比喻颇有意义,对吗?
既然你已经理解了Promise存在的缘由以及它们能够存在的不一样状态,那么咱们还须要回答三个问题。
一、如何创造一个Promise?
二、如何改变Prommise的状态?
三、当Promise的状态发生变化时,如何监听?
这个很直接。建立一个new实例Promise。
const promise = new Promise()
该Promise构造函数接受一个参数,一个(回调)函数。这个函数将传递两个参数,resolve和reject。
resolve - 一个容许你更改Promise状态的功能 fulfilled
reject- 一个容许你更改Promise状态的功能rejected。
在下面的代码中,咱们使用setTimeout等待2秒而后调用resolve。这将改变Promise的状态fulfilled。
const promise = new Promise((resolve, reject) => { setTimeout(() => { resolve() // Change status to 'fulfilled' }, 2000) })
咱们能够经过在建立它以后当即记录promise来看到这种变化,而后resolve在调用以后大约2秒后再次记录。
注意Promise从pending到resolved。
在我看来,这是最重要的问题。很酷咱们知道如何建立Promise并改变其状态,但若是咱们在状态发生变化后不知道如何作任何事情,那就毫无价值。
咱们尚未谈到的一件事是Promise其实是什么。当你建立一个时new Promise,你真的只是建立一个普通的旧JavaScript对象。该对象能够调用两个方法then,和catch。这是关键。当promise的状态更改fulfilled为时,.then将调用传递给的函数。当promise的状态更改rejected为时,.catch将调用传递给的函数。这意味着一旦你建立了一个promise,若是异步请求成功,你将传递你想要运行的函数.then。若是异步请求失败,你将传递要运行的功能.catch。
咱们来看一个例子吧。咱们将setTimeout再次使用fulfilled在两秒钟(2000毫秒)以后将Promise的状态更改成。
function onSuccess () { console.log('Success!') } function onError () { console.log('💩') } const promise = new Promise((resolve, reject) => { setTimeout(() => { resolve() }, 2000) }) promise.then(onSuccess) promise.catch(onError)
若是运行上面的代码,你会注意到大约2秒后,您将在控制台中看到“成功!”。这种状况再次发生的缘由是两件事。首先,当咱们建立了promise时,咱们resolve在〜2000毫秒以后调用- 这改变了promise的状态fulfilled。其次,咱们将onSuccess函数传递给promises的.then方法。经过这样作,咱们告诉Promise,onSuccess当Promise的状态改变为fulfilled〜2000毫秒后它所作的时,调用。
如今让咱们伪装发生了一些很差的事情,咱们想要改变Promise的状态rejected。resolve咱们打电话,而不是打电话reject。
function onSuccess () { console.log('Success!') } function onError () { console.log('💩') } const promise = new Promise((resolve, reject) => { setTimeout(() => { reject() }, 2000) }) promise.then(onSuccess) promise.catch(onError)
如今这一次而不是onSuccess被调用的onError函数,由于咱们调用了函数reject。
你已经了解了Promise API的方法,让咱们开始查看一些真实的代码。
还记得咱们以前看到的最后一个异步回调示例吗?
function getUser(id, onSuccess, onFailure) { $.getJSON({ url: `https://api.github.com/users/${id}`, success: onSuccess, error: onFailure }) } function getWeather(user, onSuccess, onFailure) { $.getJSON({ url: getLocationURL(user.location.split(',')), success: onSuccess, error: onFailure, }) } $("#btn").on("click", () => { getUser("tylermcginnis", (user) => { getWeather(user, (weather) => { updateUI({ user, weather: weather.query.results }) }, showError) }, showError) })
咱们能够在这里使用Promise API而不是使用回调吗?若是咱们将AJAX请求包含在promise中,该怎么办?而后咱们能够简单地resolve或reject取决于请求的方式。让咱们开始吧getUser。
function getUser(id) { return new Promise((resolve, reject) => { $.getJSON({ url: `https://api.github.com/users/${id}`, success: resolve, error: reject }) }) }
很好。请注意,参数getUser已更改。而不是接收id,onSuccess和onFailure,它只是接收id。再也不须要那些其余两个回调函数,由于咱们再也不反转控制。相反,咱们使用Promise resolve和reject函数。resolve若是请求成功reject将被调用,若是有错误将被调用。
接下来让咱们重构一下getWeather。咱们将遵循相同的策略。咱们将使用使用resolve和reject而不是接收onSuccess和onFailure回调函数。
function getWeather(user) { return new Promise((resolve, reject) => { $.getJSON({ url: getLocationURL(user.location.split(',')), success: resolve, error: reject, }) }) }
看起来不错。如今咱们须要更新的最后一件事是咱们的点击处理程序。请记住,这是咱们想要采起的流程。
从Github API获取用户的信息。
一、使用用户的位置从Yahoo Weather API获取他们的天气。
二、使用用户信息及其天气更新UI。
三、让咱们从#1开始 - 从Github API获取用户的信息。
$("#btn").on("click", () => { const userPromise = getUser('tylermcginnis') userPromise.then((user) => { }) userPromise.catch(showError) })
请注意,如今它不是getUser接受两个回调函数,而是返回一个咱们能够调用.then和.catch启用的Promise。若是.then被调用,将使用用户的信息调用它。若是.catch被调用,它将被调用错误。
接下来让咱们作#2 - 使用用户的位置来获取他们的天气。
$("#btn").on("click", () => { const userPromise = getUser('tylermcginnis') userPromise.then((user) => { const weatherPromise = getWeather(user) weatherPromise.then((weather) => { }) weatherPromise.catch(showError) }) userPromise.catch(showError) })
请注意,咱们遵循咱们在#1中彻底相同的模式,但如今咱们调用getWeather它传递给user咱们的对象userPromise。
最后,#3 - 使用用户信息及其天气更新UI。
$("#btn").on("click", () => { const userPromise = getUser('tylermcginnis') userPromise.then((user) => { const weatherPromise = getWeather(user) weatherPromise.then((weather) => { updateUI({ user, weather: weather.query.results }) }) weatherPromise.catch(showError) }) userPromise.catch(showError) })
这是你可使用的完整代码。
新代码看起来更好,但咱们仍然能够作出一些改进。在咱们作出这些改进以前,你须要知道Promise到两个功能:链式调用和将参数从resolve传递到then。
.then和.catch都会返回一个新的Promise。这彷佛是一个小细节,但它很重要,由于它意味着Promise能够被链式调用。
在下面的示例中,咱们调用getPromise它返回一个将在至少2000毫秒内解析的promise。从那里,由于.then将返回一个Promise,咱们能够继续将咱们的.thens连接在一块儿,直到咱们抛出一个new Error被该.catch方法捕获的东西。
function getPromise () { return new Promise((resolve) => { setTimeout(resolve, 2000) }) } function logA () { console.log('A') } function logB () { console.log('B') } function logCAndThrow () { console.log('C') throw new Error() } function catchError () { console.log('Error!') } getPromise() .then(logA) // A .then(logB) // B .then(logCAndThrow) // C .catch(catchError) // Error!
很酷,但为何这么重要?请记住,在回调部分,咱们谈到了回调的一个缺点,即它们会迫使你摆脱天然,顺序的思惟方式。当你将Promise连接在一块儿时,它并不会强迫你摆脱那种天然的思惟方式,由于链式调用Promise是连续的。getPromise runs then logA runs then logB runs then...。
这样你就能够看到另外一个示例,这是使用fetchAPI 时的常见用例。fetch将返回一个将经过HTTP响应解决的Promise。要得到实际的JSON,你须要调用.json。因为连接,咱们能够按顺序思考这个问题。
fetch('/api/user.json') .then((response) => response.json()) .then((user) => { // user is now ready to go. })
如今咱们知道有关链式调用,让咱们来重构咱们早期getUser/ getWeather代码而且使用它。
function getUser(id) { return new Promise((resolve, reject) => { $.getJSON({ url: `https://api.github.com/users/${id}`, success: resolve, error: reject }) }) } function getWeather(user) { return new Promise((resolve, reject) => { $.getJSON({ url: getLocationURL(user.location.split(',')), success: resolve, error: reject, }) }) } $("#btn").on("click", () => { getUser("tylermcginnis") .then(getWeather) .then((weather) => { // We need both the user and the weather here. // Right now we just have the weather updateUI() // ???? }) .catch(showError) })
它看起来好多了,但如今咱们遇到了一个问题。你能发现它吗?在第二个.then咱们要调用updateUI。问题是,咱们须要经过updateUI这两个user和weather。目前咱们如何设置,咱们只收到weather,而不是user。不知何故,咱们须要找出一种方法来使它成为一个Promise,即getWeather使用user和来解决回报weather。
这是关键。resolve只是一个功能。您传递给它的任何参数都将传递给给定的函数.then。这是什么意思是,里面getWeather,若是咱们调用resolve咱们本身,咱们能够经过它weather和user。而后,.then咱们链中的第二个方法将同时接收user和weather做为参数。
function getWeather(user) { return new Promise((resolve, reject) => { $.getJSON({ url: getLocationURL(user.location.split(',')), success(weather) { resolve({ user, weather: weather.query.results }) }, error: reject, }) }) } $("#btn").on("click", () => { getUser("tylermcginnis") .then(getWeather) .then((data) => { // Now, data is an object with a // "weather" property and a "user" property. updateUI(data) }) .catch(showError) })
你能够在这里玩最终的代码
它位于咱们的点击处理程序中,与回调相比,你真正看到了Promise的力量。
// Callbacks 🚫 getUser("tylermcginnis", (user) => { getWeather(user, (weather) => { updateUI({ user, weather: weather.query.results }) }, showError) }, showError) // Promises ✅ getUser("tylermcginnis") .then(getWeather) .then((data) => updateUI(data)) .catch(showError);
遵循这种逻辑感受很天然,由于它是咱们习惯于按顺序思考的方式。getUser then getWeather then update the UI with the data。
如今很明显,promises会大大提升异步代码的可读性,但有没有办法让它变得更好?假设你是TC39委员会成员,而且你有能力为JavaScript语言添加新功能。你将采起哪些步骤来改进此代码?
$("#btn").on("click", () => { getUser("tylermcginnis") .then(getWeather) .then((data) => updateUI(data)) .catch(showError) })
正如咱们所讨论的那样,代码读得很是好。正如咱们的大脑工做同样,它是按顺序排列的。咱们遇到的一个问题是咱们须要将data(users)从第一个异步请求一直到最后一个.then。这不是什么大不了的事,但它让咱们改变了咱们的getWeather功能,也传递了它users。若是咱们编写异步代码的方式与编写同步代码的方式相同怎么办?若是咱们这样作了,那么这个问题就会完全消失,并且它仍会按顺序读取。这是一个想法。
$("#btn").on("click", () => { const user = getUser('tylermcginnis') const weather = getWeather(user) updateUI({ user, weather, }) })
好吧,那会很好。咱们的异步代码看起来就像咱们的同步代码。咱们的大脑没有额外的步骤须要采起,由于咱们已经很是熟悉这种思惟方式。可悲的是,这显然是行不通的。如你所知,若是咱们要运行上面的代码,user而且weather二者都只是Promise,由于那是什么getUser并getWeather返回。但请记住,咱们正在使用TC39。咱们有能力为咱们想要的语言添加任何功能。按原样,这段代码在制做工做时会很是棘手。咱们必须以某种方式教JavaScript引擎,以便了解异步函数调用和常规的同步函数调用之间的区别。让咱们在代码中添加一些关键字,以便在引擎上更轻松。
首先,让咱们在主函数自己添加一个关键字。这可让引擎知道这个函数内部咱们将要进行一些异步函数调用。让咱们用async它。
$("#btn").on("click", async () => { const user = getUser('tylermcginnis') const weather = getWeather(user) updateUI({ user, weather, }) })
酷。这看似合理。接下来让咱们添加另外一个关键字,让引擎确切知道被调用的函数什么时候是异步的而且将返回一个promise。咱们来使用await。如同,“嘿发动机。此函数是异步的并返回一个promise。不要像往常同样继续,继续“等待”Promise的最终的值并在继续以前将其返回“。与这两个咱们的新的async和await在游戏中的关键字,咱们的新的代码看起来像这样。
$("#btn").on("click", async () => { const user = await getUser('tylermcginnis') const weather = await getWeather(user.location) updateUI({ user, weather, }) })
很是漂亮。咱们已经发明了一种合理的方法来使咱们的异步代码看起来和行为就像它是同步的同样。如今下一步是让TC39上的某我的相信这是一个好主意。幸运的是,正如你如今可能已经猜到的那样,咱们不须要作任何使人信服的事情,由于这个功能已经成为JavaScript的一部分并且它被称为Async/Await。
不相信我?这是咱们的实时代码,如今咱们已经添加了Async / Await。随意玩它。
既然您已经看到了Async / Await的好处,那么让咱们讨论一些重要的细节,这些细节很重要。首先,不管什么时候添加async到函数,该函数都将隐式返回一个promise。
async function getPromise(){} const promise = getPromise()
即便getPromise字面上是空的,它仍然会返回一个Promise,由于它是一个async函数。
若是async函数返回一个值,那么该值也将包含在一个promise中。这意味着你将不得不使用.then它来访问它。
async function add (x, y) { return x + y } add(2,3).then((result) => { console.log(result) // 5 })
若是你尝试await在非函数内部使用关键字async,则会出现错误。
$("#btn").on("click", () => { const user = await getUser('tylermcginnis') // SyntaxError: await is a reserved word const weather = await getWeather(user.location) // SyntaxError: await is a reserved word updateUI({ user, weather, }) })
如下是我对此的见解。添加async到函数时,它会执行两项操做。它使得函数自己返回(或包装返回的内容)一个Promise并使它能够await在其内部使用
你可能已经注意到咱们有点做弊。在咱们的原始代码中,咱们有一种方法能够捕获任何错误.catch。当咱们切换到Async / Await时,咱们删除了该代码。使用Async / Await,最多见的方法是将代码包装在一个try/catch块中以便可以捕获错误。
$("#btn").on("click", async () => { try { const user = await getUser('tylermcginnis') const weather = await getWeather(user.location) updateUI({ user, weather, }) } catch (e) { showError(e) } })
(完)
以上译文仅用于学习交流,水平有限,不免有错误之处,敬请指正。