Title: 从underscore源码看如何实现map函数(一) · Issue #13 · webproblem/Blog · GitHub
Open Graph Title: 从underscore源码看如何实现map函数(一) · Issue #13 · webproblem/Blog
X Title: 从underscore源码看如何实现map函数(一) · Issue #13 · webproblem/Blog
Description: 前言 经常会看到这样的面试题,让面试者手动实现一个 map 函数之类的,嗯,貌似并没有什么实际意义。但是对于知识探索的步伐不能停止,现在就来分析下如何实现 map 函数。 PS: 关于 underscore 源码解读注释,详见:underscore 源码解读。 Array.prototype.map 先来了解下原生 map 函数。 map 函数用于对数组元素进行迭代遍历,返回一个新函数并不影响原函数的值。map 函数接受一个 callback 函数以及执行上下文参数,c...
Open Graph Description: 前言 经常会看到这样的面试题,让面试者手动实现一个 map 函数之类的,嗯,貌似并没有什么实际意义。但是对于知识探索的步伐不能停止,现在就来分析下如何实现 map 函数。 PS: 关于 underscore 源码解读注释,详见:underscore 源码解读。 Array.prototype.map 先来了解下原生 map 函数。 map 函数用于对数组元素进行迭代遍历,返回一个新函数并不影...
X Description: 前言 经常会看到这样的面试题,让面试者手动实现一个 map 函数之类的,嗯,貌似并没有什么实际意义。但是对于知识探索的步伐不能停止,现在就来分析下如何实现 map 函数。 PS: 关于 underscore 源码解读注释,详见:underscore 源码解读。 Array.prototype.map 先来了解下原生 map 函数。 map 函数用于对数组元素进行迭代遍历,返回一个新函数并不影...
Opengraph URL: https://github.com/webproblem/Blog/issues/13
X: @github
Domain: patch-diff.githubusercontent.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"从underscore源码看如何实现map函数(一)","articleBody":"### 前言\r\n\r\n经常会看到这样的面试题,让面试者手动实现一个 map 函数之类的,嗯,貌似并没有什么实际意义。但是对于知识探索的步伐不能停止,现在就来分析下如何实现 map 函数。\r\n\r\nPS: 关于 underscore 源码解读注释,详见:[underscore 源码解读](https://github.com/webproblem/Blog/blob/master/源码解读/underscore/Underscore.1.8.3.analysis.js)。\r\n\r\n### Array.prototype.map\r\n\r\n先来了解下原生 map 函数。\r\n\r\nmap 函数用于对数组元素进行迭代遍历,返回一个新函数并不影响原函数的值。map 函数接受一个 callback 函数以及执行上下文参数,callback 函数带有三个参数,分别是迭代的当前值,迭代当前值的索引下标以及迭代数组自身。map 函数会给数组中的每一个元素按照顺序执行一次 callback 函数。\r\n\r\n```javascript\r\nvar arr = [1,2,3];\r\nvar newArr = arr.map(function(item, index){\r\n if(index == 1) return item * 3;\r\n return item;\r\n})\r\nconsole.log(newArr); // [1, 6, 3]\r\n```\r\n\r\n### 实现\r\n\r\n#### for 循环\r\n\r\n实现思路其实挺简单,使用 for 循环对原数组进行遍历,每个元素都执行一遍回调函数,同时将值赋值给一个新数组,遍历结束将新数组返回。\r\n\r\n将自定义的 _map 函数依附在 Array 的原型上,省去了对迭代数组类型的检查等步骤。\r\n\r\n```javascript\r\nArray.prototype._map = function(iteratee, context) {\r\n var arr = this;\r\n var newArr = [];\r\n for(var i=0; i\u003carr.length; i++) {\r\n newArr[i] = iteratee.call(context, arr[i], i, arr);\r\n }\r\n return newArr;\r\n}\r\n```\r\n\r\n测试如下:\r\n\r\n```javascript\r\nvar arr = [1,2,3];\r\nvar newArr = arr._map(function(item, index){\r\n if(index == 1) return item * 3;\r\n return item;\r\n})\r\nconsole.log(newArr); // [1, 6, 3]\r\n```\r\n\r\n好吧,其实重点不在于自己如何实现 map 函数,而是解读 underscore 中是如何实现 map 函数的。\r\n\r\n### underscore 中的 map 函数\r\n\r\n_.map 相对于 Array.prototype.map 来说,功能更加完善和健壮。 _.map 源码:\r\n\r\n```javascript\r\n /**\r\n * @param obj 对象\r\n * @param iteratee 迭代回调\r\n * @param context 执行上下文\r\n * _.map 的强大之处在于 iteratee 迭代回调的参数可以是函数,对象,字符串,甚至不传参\r\n * _.map 会根据不同类型的 iteratee 参数进行不同的处理\r\n * _.map([1,2,3], function(num){ return num * 3; }); // [3, 6, 9]\r\n * _.map([{name: 'Kevin'}, {name: 'Daisy'}], 'name'); // [\"Kevin\", \"Daisy\"]\r\n */\r\n _.map = _.collect = function(obj, iteratee, context) {\r\n // 针对不同类型的 iteratee 进行处理\r\n iteratee = cb(iteratee, context);\r\n var keys = !isArrayLike(obj) \u0026\u0026 _.keys(obj),\r\n length = (keys || obj).length,\r\n results = Array(length);\r\n for (var index = 0; index \u003c length; index++) {\r\n var currentKey = keys ? keys[index] : index;\r\n results[index] = iteratee(obj[currentKey], currentKey, obj);\r\n }\r\n return results;\r\n };\r\n```\r\n\r\n可以看到,_.map 接受 3 个参数,分别是迭代对象,迭代回调和执行上下文。iteratee 迭代回调在函数内部进行了特殊处理,为什么要这么做,原因是因为iteratee 迭代回调的参数可以是函数,对象,字符串,甚至不传参。\r\n\r\n```javascript\r\n// 传入一个函数\r\n_.map([1,2,3], function(num){ return num * 3; }); // [3, 6, 9]\r\n\r\n// 什么也不传\r\n_.map([1,2,3]); // [1, 2, 3]\r\n\r\n// 传入一个对象\r\n_.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], {name: 'Daisy'}); // [false, true]\r\n\r\n// 传入一个字符串\r\n_.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], 'name'); // [\"Kevin\", \"Daisy\"]\r\n```\r\n\r\n先来分析下 _.map 函数内部是如何针对不同类型的 iteratee 进行处理的。\r\n\r\n### cb\r\n\r\ncb 函数源码如下(PS: 所有的注释都是个人见解):\r\n\r\n```javascript\r\nvar cb = function(value, context, argCount) {\r\n // 是否使用自定义的 iteratee 迭代器,外部可以自定义 iteratee 迭代器\r\n if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);\r\n // 处理不传入 iteratee 迭代器的情况,直接返回迭代集合\r\n // _.map([1,2,3]); // [1,2,3]\r\n if (value == null) return _.identity;\r\n // 优化 iteratee 迭代器是函数的情况\r\n if (_.isFunction(value)) return optimizeCb(value, context, argCount);\r\n // 处理 iteratee 迭代器是对象的情况\r\n if (_.isObject(value) \u0026\u0026 !_.isArray(value)) return _.matcher(value);\r\n // 其他情况的处理,数组或者基本数据类型的情况\r\n return _.property(value);\r\n};\r\n```\r\n\r\ncb 函数内部针对 value 类型(也就是 iteratee 迭代器)的不同做了相应的处理。\r\n\r\nunderscore 中允许我们自定义 _.iteratee 函数的,也就是可以自定义迭代回调。\r\n\r\n```javascript\r\nif (_.iteratee !== builtinIteratee) return _.iteratee(value, context);\r\n```\r\n\r\n正常情况下,这个判断语句应该为 false,因为在 underscore 内部中已经定义了 _.iteratee 就是与 builtinIteratee 相等。\r\n\r\n```javascript\r\n_.iteratee = builtinIteratee = function(value, context) {\r\n return cb(value, context, Infinity);\r\n};\r\n```\r\n\r\n这样做的目的是为了区分是否有自定义 _.iteratee 函数,如果有重写了 _.iteratee 函数,就使用自定义的函数。\r\n\r\n那么为什么会允许我们去修改 _.iteratee 函数呢?试想如果场景中只是需要 _.map 函数的 iteratee 参数是函数的话,就用该函数处理数组元素,如果不是函数,就直接返回当前元素,而不是将 iteratee 进行针对性处理。\r\n\r\n```javascript\r\n_.iteratee = function(value, context) {\r\n if(typeof value === 'function') {\r\n return function(...rest) {\r\n return value.call(context, ...rest)\r\n };\r\n }\r\n return function(value) {\r\n return value;\r\n }\r\n}\r\n```\r\n\r\n测试如下:\r\n\r\n```javascript\r\n_.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], 'name');\r\n```\r\n\r\n\r\n\r\n需要注意的是,很多迭代函数都依赖于 _.iteratee 函数,所以要谨慎使用自定义 _.iteratee。\r\n\r\n当然了,如果没有 iteratee 迭代器的情况下,也是直接返回迭代集合。\r\n\r\n正常使用情况下,传入的 iteratee 迭代器应该都会是函数的,为了提升性能,在 cb 函数内部针对 iteratee 迭代器是函数的情况做了性能处理,也就是 optimizeCb 函数。\r\n\r\n### optimizeCb \r\n\r\noptimizeCb 函数源码如下:\r\n\r\n```javascript\r\n /**\r\n * 优化迭代器回调\r\n * @param func 迭代器回调 \r\n * @param context 执行上下文\r\n * @param argCount 指定迭代器回调接受参数个数\r\n */\r\n var optimizeCb = function(func, context, argCount) {\r\n // 如果没有传入上下文,直接返回\r\n if (context === void 0) return func;\r\n // 根据指定接受参数进行处理\r\n switch (argCount) {\r\n case 1: return function(value) {\r\n // value: 当前迭代元素\r\n return func.call(context, value);\r\n };\r\n // The 2-parameter case has been omitted only because no current consumers\r\n // made use of it.\r\n case null:\r\n case 3: return function(value, index, collection) {\r\n // value: 当前迭代元素,index: 迭代元素索引,collection: 迭代集合\r\n return func.call(context, value, index, collection);\r\n };\r\n case 4: return function(accumulator, value, index, collection) {\r\n // accumulator: 累加器,value: 当前迭代元素,index: 迭代元素索引,collection: 迭代集合\r\n return func.call(context, accumulator, value, index, collection);\r\n };\r\n }\r\n // 当指定迭代器回调接受参数的个数超过4个,就用 arguments 代替\r\n // 为什么不直接使用这段代码而是在上面根据 argCount 处理接受的参数\r\n // 1. arguments 存在性能问题\r\n // 2. call 比 apply 速度更快\r\n return function() {\r\n return func.apply(context, arguments);\r\n };\r\n };\r\n```\r\n\r\noptimizeCb 函数内部主要是针对 iteratee 迭代器接受的参数进行性能优化。当指定迭代器回调接受参数的个数超过4个,就用 arguments 代替。为什么要这样处理?原因是因为 arguments 存在性能问题,且 call 比 apply 速度更快。具体分析会在下一篇给出解释,这里不做过多的分析。\r\n\r\n### _.matcher \r\n\r\n回到前面对 iteratee 迭代器类型做处理的话题,如果 iteratee 迭代器是对象的情况,又该如何处理?也就是这样:\r\n\r\n```javascript\r\n_.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], {name: 'Daisy'}); // [false, true]\r\n```\r\n\r\n在 cb 函数内部使用了 _.matcher 函数处理这种情况,来分析下 _.matcher 函数都做了哪些事情。 _.matcher 源码如下:\r\n\r\n```javascript\r\n /**\r\n * 传入一个属性对象,返回一个属性检测函数,检测对象是否具有指定属性\r\n * var matcher = _.matcher({name: '白展堂'});\r\n var obj = {name: '白展堂', age: 25};\r\n matcher(obj); // true\r\n */\r\n _.matcher = _.matches = function(attrs) {\r\n // 合并复制对象,attrs 必须是 Objdect 类型\r\n // arrts 的值为空或者其他数据类型,都能保证 attrs 是 Object 类型\r\n attrs = _.extendOwn({}, attrs);\r\n // 返回属性检测函数\r\n return function(obj) {\r\n // 检测 obj 对象是否具有指定属性 attrs\r\n return _.isMatch(obj, attrs);\r\n };\r\n };\r\n```\r\n\r\n_.matcher 的主要作用就是检测 obj 对象是否具有指定属性 attrs,例如:\r\n\r\n```javascript\r\nvar matcher = _.matcher({name: '白展堂'});\r\nvar obj = {name: '白展堂', age: 25};\r\nvar obj2 = {name: '吕秀才', age: 25};\r\n\r\nmatcher(obj); // true\r\nmatcher(obj2); // false\r\n```\r\n\r\n具体的检测是使用了 _.isMatch 函数, _.isMatch 源码如下:\r\n\r\n```javascript\r\n /**\r\n * 检测对象中是否包含指定属性\r\n * var obj = {name: '白展堂', age: 25}; \r\n * var attrs = {name: '白展堂'}; \r\n * _.isMatch(obj, attrs); // true\r\n */\r\n _.isMatch = function(object, attrs) {\r\n var keys = _.keys(attrs), length = keys.length;\r\n if (object == null) return !length;\r\n var obj = Object(object);\r\n for (var i = 0; i \u003c length; i++) {\r\n var key = keys[i];\r\n if (attrs[key] !== obj[key] || !(key in obj)) return false;\r\n }\r\n return true;\r\n };\r\n```\r\n\r\n核心部分就梳理清楚了,回到 _.map 函数,可以看到,也是使用了 for 循环来实现 map 功能,和我们自己实现了思路一致,有一点不同的是, _.map 函数的第一个参数,不仅限于数组,还可以是对象和字符串。\r\n\r\n```javascript\r\n_.map('name'); // [\"n\", \"a\", \"m\", \"e\"]\r\n\r\n_.map({name: '白展堂', age: 25}); // [\"白展堂\", 25]\r\n```\r\n\r\n在 _.map 函数内部,对类数组的对象也进行了处理。\r\n\r\n### 遗留问题\r\n\r\n到这里就梳理清楚了在 underscore 中是如何实现 map 函数的,以及优化性能方案。可以说在 underscore 中每行代码都很精炼,值得反复揣摩。\r\n\r\n同时在梳理过程中,遗留了两个问题:\r\n\r\n* arguments 存在性能问题\r\n* call 比 apply 速度更快\r\n\r\n这两个问题将会在下一篇中进行详细的分析。\r\n\r\n### 参考\r\n\r\n* https://blog.fundebug.com/2017/07/26/master_map_filter_by_hand_written/\r\n\r\n* https://github.com/mqyqingfeng/Blog/issues/58","author":{"url":"https://github.com/webproblem","@type":"Person","name":"webproblem"},"datePublished":"2018-11-27T13:16:55.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":0},"url":"https://github.com/13/Blog/issues/13"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:59a7c042-a0f7-466a-c794-9f153eb2ffb5 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | C2E8:18A62A:8788113:AF6D9F7:6975DBCF |
| html-safe-nonce | f6b6a372888aff6299b7afb7ab4f4015e9296ef1b2cb11231e48b5dd2593ed16 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJDMkU4OjE4QTYyQTo4Nzg4MTEzOkFGNkQ5Rjc6Njk3NURCQ0YiLCJ2aXNpdG9yX2lkIjoiNTk2NjcxNTQ1NzI1NDA1NDg2MyIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9 |
| visitor-hmac | 4872706e600a424e0288a6a541998ebe748b3e64d8ea0a7e41cfa96096c1a468 |
| hovercard-subject-tag | issue:384790168 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/webproblem/Blog/13/issue_layout |
| twitter:image | https://opengraph.githubassets.com/57b1da2c730c75a05a29b3e035d8b0d375834b1cd5e66eeaafaaef119e8c00c6/webproblem/Blog/issues/13 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/57b1da2c730c75a05a29b3e035d8b0d375834b1cd5e66eeaafaaef119e8c00c6/webproblem/Blog/issues/13 |
| og:image:alt | 前言 经常会看到这样的面试题,让面试者手动实现一个 map 函数之类的,嗯,貌似并没有什么实际意义。但是对于知识探索的步伐不能停止,现在就来分析下如何实现 map 函数。 PS: 关于 underscore 源码解读注释,详见:underscore 源码解读。 Array.prototype.map 先来了解下原生 map 函数。 map 函数用于对数组元素进行迭代遍历,返回一个新函数并不影... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | webproblem |
| hostname | github.com |
| expected-hostname | github.com |
| None | 2bce766e7450b03e00b2fc5badd417927ce33a860e78cda3e4ecb9bbd1374cc6 |
| turbo-cache-control | no-preview |
| go-import | github.com/webproblem/Blog git https://github.com/webproblem/Blog.git |
| octolytics-dimension-user_id | 20440496 |
| octolytics-dimension-user_login | webproblem |
| octolytics-dimension-repository_id | 115346710 |
| octolytics-dimension-repository_nwo | webproblem/Blog |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 115346710 |
| octolytics-dimension-repository_network_root_nwo | webproblem/Blog |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | fcca2b8ef702b5f7f91427a6e920fa44446fe312 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width