一个map函数引起的血案

前言


本文写做目的在于,对上次面试中未手写出来的map函数作一个收尾工做。其内容以map函数做为线,将其涉及到的众多知识点穿针引线梳理一下,并赋予本人学习及写做时的所感所想。既是所感所想,想必不免存在一些我的拙见,望各位大佬不吝指正,还望轻喷!!!node

map函数是个黑盒


还记得初识JS的map函数时webpack

[1,2,3].map((e, i, arr) => {
    return 2*e;  //[2,4,6]
})
复制代码

大学简单学过C语言后,第一次看到这个用法就感受特别神奇,彻底不知道它怎么运做,仅对它造成了一个大体的轮廓:你想基于原数组生成一个怎样的新数组,只要把逻辑写在回调函数里就行了。对我而言它彻底就像一个黑盒。但不觉间潜意识里却模糊了一些概念(已然与未然/主动与被动的关系)。web

对不起,map函数实现不来


  • 上次面试时终于被这个黑盒给安排了。面试官让我手写map函数,我懵逼!尽管感受好像能怼出来,却总差了点什么。被摁在地板摩擦以后,发现本身没作出来确实是有缘由。
  • 一方面,写这篇文章的时候发现,实现map所须要的知识点我都有涉猎,但却仅把它看成理论指导,没有把它与实践联系起来。今天就让我来学以至用一下。
  • 另外一方面,对于一些概念有些许误区,尽管这些误区看似无关紧要微乎其微,但却真真切切的影响我不少。那就让这篇文章欢送这些不速之客。

map黑盒背后的利益集团


以我我的入门JS的心路历程来看,假若对如下知识有所涉猎了解,就算不是十分熟练也能轻松实现map函数。知识点以下: (仅以实现map而作简单讲解,详情内容请自阅其余文献)面试

1. 数据类型与存储

JS中基本数据类型和引用数据类型是不一样的。当咱们把一个存储基本数据类型值的变量A赋值给另外一个变量B时,本质是值传递,两个变量存储两个独立的值。但如果引用数据类型,本质是地址传递,那么此时两个变量存储的是同一个数据的地址,所以A、B会互相影响。数据库

  • 而函数是引用数据类型,其存储在堆内存中,将其赋值给一变量,则该变量存储的是函数在堆内存中的起始地址,以此来引用函数。
  • 在这里还想多扯一下,关于赋值,深、浅拷贝的问题,对比研究一下能很快掌握。另外说到存储不得不提提垃圾回收机制,均可以偷偷学一下,串一下。

2. 函数是一等公民

咱们知道JS是一门多范式语言,其中就包括函数式编程。所以在JS中函数就像任何其余引用数据类型同样能够把它们存在数组里,看成函数参数传递,赋值给变量,做为对象的属性值等。编程

  • 做为对象的属性值
    咱们定义了一个对象f,并将一个函数myFn赋值给了f中的属性fn,则此时咱们的f.fn属性就已经指向了该函数,并能够经过f.fn()完成对函数的调用。
  • 看成函数参数传递
    这里咱们声明了一个fn函数,其接受一个函数做为参数并执行。咱们又声明了一个callback函数。接着将callback做为参数传入fn中,并执行fn函数,其结果就是callback在fn函数中执行了。 若是你真的会意该部分,那么对你而言,map函数的金钟罩将会变成最后一块遮羞布了。

3. 原型与原型链 / new构造函数调用的过程

在JS中,当咱们用 var arr = [1,2,3] 建立一个数组并将其赋值给变量arr时,该方式本质上与var arr = new Array(1,2,3) 是没有区别的。(这里忽然意识到,还涉及到new构造函数调用的知识,优秀的你应该是知道该知识点的!!) 那么此时arr表示的数组就能够称为Array的一个实例,该实例的_proto_属性是指向构造函数Array的原型对象(也就是Array.prototype所表示的一个对象)json

  • 如今就让咱们一步步揭开map的神秘面纱

想一想我第一次看到上图的时候非常懵逼,明明我只声明了一个数组,怎么它竟然拥有map函数这样的属性?还有_proto_这个属性是什么?不行我晕了。(相信聪明的你必定没问题!!!)后来学习了原型链后才明白。

  • 当访问属性的时候,会先在本实例对象(arr)中搜索属性,若未找到则会经过原型链继续搜索其指针指向的原型对象(Array.prototype)是否有该属性,OK找到了。没错咱们日常用的map函数通常都是经过原型链查找到的Array.prototype.map所指向的函数。
  • 这里再多扯一些,咱们建立一个数组并将其起始地址赋值给h,同理得g。可是h却不等于g。由于对于引用数据类型,g、h变量存储的是堆内存中该数据的起始地址,而内存中同时开辟了两个数据地址,所以不等。那么也就是说,这里arr._proto_指向的对象与Array.prototype指向的对象是堆内存中同一个引用数据类型。而arr.map经过原型链查找到的便是Array.prototype.map指向的函数所以也必然是相等的。
  • 再多扯一点,关于对象中属性的读取与修改,与做用域变量的读取与修改仍是有很大不一样的。感兴趣的话能够研究一下,对比记忆很快就掌握了。

4. this的指向性问题

关于JS函数里this的指向问题就再也不概述了,大体分为四个规则加一个特殊的箭头函数。如今对于 [1,2,3].map(callback) 咱们大概明白了,经过[1,2,3].map以原型链查询的方式找到了在Array.prototype.map里的函数,而后将callback函数做为参数传入map函数中以达到后期调用并执行相关逻辑的目的,可是咱们怎么在调用的函数中找到原数组?没错经过函数中的this。数组

  • 由this指向规则中的隐含转换知(this本质是函数执行时建立的执行上下文里的一个对象,所以this的指向由调用点决定),当咱们经过a.fn()调用a.fn指向的函数时,函数中的this就指向对象a。同理,当实现map函数时也可经过此原理来找到原数组,即实现的map函数里的this就指向实例数组自己。[1,2,3].map()则函数里的this指向该[1,2,3]数组。

5. 回调函数

学习JS时才第一次接触回调函数,一度以为本身挺懂回调,后来发现本身真的是根本不懂,还觉得本身很懂!如今让咱们看看map中回调的真容吧。promise

  • 一直以来我都把回调函数理解成主动性,但事实上传入的回调函数是被动性的。想一下平时咱们为了实现某个功能定义了一个函数,而后传参执行该函数。但回调函数本质只是一个函数声明,之因此会执行相关的逻辑是由于以后会给该回调函数传入参数并调用该回调函数,它是被调用的。bash

  • 那么这里又涉及到已然性和未然性。原生的map函数是被定义过的,当调用map函数实现相关逻辑时,它内部执行流程就会将数组每一个元素的(item/元素值, index/元素索引, arr/原数组)传入回调函数callback并以callback(item, index, arr)的形式调用。所以,咱们知道该回调会被传入指定的参数并调用。因此,咱们在仅须要作的声明传入的回调函数时,能够把此时回调函数的参数当作对应的数组中元素的值,在此基础上实现相关逻辑。实际上,就是把声明回调里的参数当作map执行时内部调用回调时传入的参数(item, index, arr)进行操做

  • 其实以上两点总结来讲就是以往咱们都是先声明函数,再传参调用。而如今咱们在理解map函数时遇到的事实倒是,已经肯定了将回调函数传入map中调用时,将会在map函数内调用该回调函数,且该回调函数是被传入了固定参数的状态下调用的。所以能够说咱们已经肯定了内部会自动执行该回调,就差声明回调并传入map中执行了。因此如今对回调函数的理解是,会(hui)被调用的函数

  • 如上图,咱们定义了一个sumTwoItemFlag函数,它接受一个回调函数并将对象a,b传入该回调执行,因此当咱们执行sumTwoItemFlag函数时要传入一个声明的回调函数,并在回调里完成相应的逻辑。这就是以前赘述的,已经肯定好回调函数调用时传入的参数,咱们已经知道此时回调里的参数就是sumTwoItemFlag中的a,b对象。在此基础上,咱们只须要传入回调的时候把回调里的参数当作是a、b,并执行咱们想要的逻辑就能够了。
  • 其本质上就是反其道而行之。但却能达到咱们思惟里的先声明再调用的正常逻辑,且其更加灵活。由于虽然回调函数执行时传入的参数是固定的,可是对于map函数来讲传入的回调函数倒是灵活多变的,因此能够根据我的传入回调的不一样,达到灵活实现数组操做的目的,真正是一本万利呀!回调牛逼!!!
  • 再啰嗦一局,该部分知识点配合上篇文章推荐知识清单中的用promise实现jsonp更丝滑哦。(该部分好像很啰嗦很重复,但仍是选择了啰嗦重复,那你就把它当作强调吧)

手写代码


相信看完内容的你已经对map这个有了很清晰的认识了吧。其实我以为若是以上能掌握,那么之后绝大多数手写方法的题应该都不成问题了。那么接下来就让我贴出手写map的代码吧。(写文章真是个累人的活啊,贴出来把,写不动了!)

map实现

Array.prototype.myMap = function(callback, context) {
    var arr = this;
    var res = [];
    context = context ? context : window;
    for(let i = 0; i < arr.length; i++) {
        let tem = callback.call(context, arr[i], i, arr);
        res.push(tem); 
    }
    return res;
}
复制代码

reduce的实现

相信只要你稍微动动灵活的小脑壳确定也能实现一个reduce函数吧。

Array.prototype.myReduce = function(callback) {
    var arr = this;
    var res; <!--用arguments捕获第二个参数由于其值多是null,NaN之类-->
    if(typeof(callback) !== "function")  throw new Error("not a function");
    if(arguments.length < 2 && arr.length === 0) throw new Error("empty array with no initial value");
    if(arguments.length < 2 && arr.length === 1) return arr[0];
    if(arguments.length > 1 && arr.length === 0) return arguments[1];
    res = arguments.length > 1? arguments[1] : arr.shift();
    for(let i = 0; i < arr.length; i++) {
        res = callback(res, arr[i], i, arr);
    }
    return res;
}
复制代码

map的reduce实现

Array.prototype._myMap = function(callback, context) {
    context = context ? context : window;
    return this.reduce((accum, item, index, arr) => 
        [...accum, callback.call(context, item, index, arr)]
    , []);
}
复制代码

总结


  • 千万别钻进牛角尖。相信你也看出来了,上述只是map函数等的简易版实现,关于该方法实现map函数,其边界状况真的是太多了。而我就有幸((┬_┬))钻入了牛角尖,意图实现一个理想的map。其结果就是花费了太多时间却收效甚微。看了源码以后顿感本身真是傻!
  • 学习的时候方向不能搞错啊! 为何说本身傻呢?我在实现的时候仍是在用原生map手动测试边界状况,花费来大量时间以后,终于以为搞不定了,要去看看源码。看了源码以后就开始怀疑人生了。其实仔细想一想也能明白,不必实现一个完美的map啊,你就算仿了一个完美的map又能说明什么?想搞明白就去看源码啊,还在那跟个××同样意图从表面探测真相,仍是手动的。另外一方面,面试官出这个题也是想考察你的基本功,也不可能真是让你完美实现啊。因此学习的时候千万不能搞错方向,更不要钻进不错误的牛角尖
  • 想探究一个技术的真容,若是不懂,真的搞不定的话,就去学习源码。 这能够说是惟一欣慰的一点收获吧。之前学习webpack的时候也想弄明白这个黑盒的真容,当时也是手动由表入里的探索,结果最后实在是进行不下去了,结果就收工了,好在当时仍是有所收获的。这篇文章后将更加坚决了我以研究源码做为往后学习各类黑盒的决心。

展望


  • 感受对箭头函数仍是有一点不熟练,准备再研究一哈。
  • 接下来想再探探webpack的真容,目前想的是从源码方面入手,若是太难的话就再补充补充涉及到的前置知识,再继续攻略源码。
  • 昨天在看node开发实战里的爬虫实战时,惊觉之前似懂非懂的模块调用并掺杂着一些回调的逻辑业务,竟然能看懂,再也不半遮半掩了。感受写文章真的是对之前纯输入的一种很好的输出方式。不只能认识新朋友,更能对多所学知识进行一个梳理,总结。如今已经从纯输入过渡到想输出的阶段了,之后会继续坚持下去。可是也要深深的明白,根据能量守恒,这些输出是创建在之前输入的基础上,因此将来的日子也不能忘记充电呀!!!
  • 本觉得QQ截图会保留在本地,但在写这篇文章的时候竟然惊奇地发现用QQ截图拖入该编辑页面竟然会有该图片的地址,且在非本地状况下输入网址后真的有该图片,感受本身真的对网络一无所知。粗略的想了一下(瞎猜的),大概是截图成功就会将该图片放入存储我QQ对应数据的数据库中吧,而后能够经过该url访问此图片。以后会想要了解清楚。原来习觉得常的QQ截图背后竟有如此鲜为人知的操做,真是该对平常理所应当的事更上上心了呀
相关文章
相关标签/搜索