Giter Club home page Giter Club logo

blog's People

Contributors

nevenleung avatar

Watchers

 avatar

blog's Issues

记一道 JS 面试题的正确思考过程

实现 sum 函数,使得以下表达式的值正确

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

面试总结

  • 面试
    • 当时想一次性做出来,但发现自己不会处理,以至于很多地方都有错误,最后连最简单的例子都没有实现,就被迫放弃了
  • 反思
    • 不应该执着于一次性把完美的解决方案想出来
    • 应该先从简单的例子入手,之后冷静下来分析

正确的思路

第一个 test case

sum(1, 2, 3).sumOf(); //6

  • 通过观察这个 test case
    • 我们可以知道调用 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;
}

第二个 test case

sum(2, 3)(2).sumOf(); //7

  • 这个 test case 增加了一些新的特性
    • 调用 sum(),返回是一个函数(我们称为 fn
    • 这个 fn 函数被调用后,它的返回的是一个对象,它仍然拥有 sumOf 方法,来计算总和
    • 值得注意的一点是:sumfn调用,貌似是在做相同的事情
      • 接收数量不定的参数
      • 被调用后,返回的函数(对象) 里都有一个 sumOf 方法
        • sum 返回的是一个(可以被调用的)函数
          • 在第一个 test case 中,我们知道它返回的是一个对象,这个对象里有 sumOf 方法
          • 在第二个 test case 中,它返回的是一个函数,因为它可以被调用
        • 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;
}

第三、第四个 test case

sum(1)(2)(3)(4).sumOf(); //10
sum(2)(4, 1)(2).sumOf(); //9

  • 通过观察这两个 test case,基本印证了我们原来的假设
    • 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;
}

参考资料

谈谈 JS 的 callback、Promise 和 async/await

谈谈 JS 的 callback、Promise 和 async/await

callback

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 中进行异步编程的原始方式。

回调: 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

Promise 是 JS 中进行异步编程的一个重要的工具。Promise对象包含了异步操作是否完成(或失败),以及它的操作结果。在 Promise 被正式写入到标准文件之前,有很多的第三方库实现了 Promise 的功能。而在 ES2015 中,Javascript 正式引入了 Promise。

相比于 callback,Promise 具有更易读的代码组织形式(将有依赖的异步操作使用链式结构组织起来),更好的异常处理方式(无需为每个异步操作添加异常处理,只需在调用 Promise 的末尾添加上一个catch方法捕获异常即可),以及异步操作并行处理的能力(Promise.all())等优点。

使用 Promise 对代码进行改造

下面将前面使用 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(执行器函数),然后将原来的onloadonerror回调内部的callback分别替换为resolvereject,分别将这两处的结果使用resolvereject来调用(这是最简单粗暴的解决方式,有的时候可以有优化的空间)。

Promise:披了马甲的 callback?

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)

对比调用 fetchDatafetchDataWithPromise 的代码。到这里,我想你可能会发现,Promise 与 callback “长得很像”。这里不去讨论 Promise 的具体源码实现,可以联想到 Promise “继承了” callback 的大部分特性,Promise 是强化版的 callback,它拥有这一节开头提到的那些优点。

Promise 只是换了一种样子的 callback 吗?当然不是。如果仅仅是将老代码改造成为 Promise,这当然没有太大的作用,但当把一个个异步操作分别封装成合适的返回 Promise 的函数之后,我们可以借助 Promise 的特性来将一个一个异步操作函数组合起来,形成一个 Promise chain,这时才能发挥 Promise 的最大作用。 这个留在将 callback、Promise、async/await 三者进行比较时,再给出代码例子。

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 也不是没有缺点的。

下面是另一个例子,回调的嵌套使用,它通常被称为「回调地狱」(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,再用 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 给后面步骤的回调函数。

使用 async/await 的写法

这里复用了前面封装的返回 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 又有着更简洁的语法,更符合普通人阅读代码的习惯。

参考资料

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.