前面我们使用HDFS进行了相关的操作,也了解了HDFS的原理和机制,有了分布式文件系统我们如何去处理文件呢,这就的提到hadoop的第二个组成部分-MapReduce。

MapReduce充分借鉴了分而治之的思想,将一个数据的处理过程分为Map(映射)和Reduce(处理)两步。那么用户只需要将数据以需要的格式交给reduce函数处理就能轻松实现分布式的计算,很多的工作都由mapReduce框架为我们封装好,大大简化了操作流程。

1 MapReduce的编程思想

MapReduce的设计思路来源于LISP和其他的函数式编程语言中的映射和化简操作。数据操作的最小单位是一个键值对。用户在使用MapReduce编程模型的时候第一步就是要将数据转化为键值对的形式,map函数会以键值对作为输入,经过map函数的处理,产生新的键值对作为中间结果,然后MapReduce计算框架会自动将这些中间结果数据做聚合处理,然后将键相同的数据分发给reduce函数处理。reduce函数以键值对的形式对处理结果进行输出。要用表达式的形式表达的话,大致如下:

{key1,value1}----->{key2,List<value2>}----->{key3,value3}

2 MapReduce的运行环境

与HDFS相同的是,MapReduce计算框也是主从架构。支撑MapReduce计算框架的是JobTracker和TaskTracker两类后台进程。

2.1 JobTracker

Job Tracker 在集群中扮演了主的角色,它主要负责任务调度和集群资源监控这两个功能,但并不参与具体的计算。一个Hadoop 集群只有一个JobTracker ,存在单点故障的可能,所以必须运行在相对可靠的节点上,一JobTracker 出错,将导致集群所有正在运行的任务全部失败。

与HDFS 的NameNode 和DataNode 相似, TaskTracker 也会通过周期性的心跳向JobTracker汇报当前的健康状况和状态,心跳信息里面包括了自身计算资源的信息、被占用的计算资源的信息和正在运行中的任务的状态信息。JobTracker 则会根据各个TaskTracker 周期性发送过来的心跳信息综合考虑TaskTracker 的资源剩余量、作业优先级、作业提交时间等因素,为TaskTracker分配合适的任务。

2.2 TaskTracker

TaskTracker 在集群中扮模了从的角色,它主要负责汇报心跳和执行JobTracker 的命令这两个功能。一个集群可以有多个TaskTracker ,但一个节点只会有一个TaskTracker , 并且TaskTracker和DataNode 运行在同一个节点之中,这样, 一个节点既是计算节点又是存储节点。TaskTracker会周期性地将各种信息汇报给JobTracker ,而JobTracker 收到心跳信息, 会根据心跳信息和当前作业运行情况为该TaskTracker 下达命令, 主要包括启动任务、提交任务、杀死任务、杀死作业和重新初始化5 种命令。

2.3 客户端

用户通过客户端提交编写的MapReduce程序给JobTracker。

3 MapReduce 作业和任务

Map Reduce 作业( job )是用户提交的最小单位, 而Map/Reduce 任务( task ) 是MapReduce

计算的最小单位。当用户向Hadoop 提交一个MapReduce 作业时, JobTracker 的作业分解模块会将其分拆为

任务交由各个TaskTracker 执行, 在MapReduce 计算框架中,任务分为两种-Map 任务和Reduce 任务。

4 MapReduce 的计算资源划分

一个MapReduce 作业的计算工作都由TaskTracker 完成。用户向Hadoop 提交作业,Job Tracker 会将该作业拆分为多个任务, 并根据心跳信息交由空闲的TaskTracker 启动。一个Task Tracker 能够启动的任务数量是由TaskTracker 配置的任务槽( slot ) 决定。槽是Hadoop 的计算资源的表示模型, Hadoop 将各个节点上的多维度资源( CPU 、内存等)抽象成一维度的槽,这样就将多维度资源分配问题转换成一维度的槽分配的问题。在实际情况中,Map 任务和Reduce任务需要的计算资源不尽相同, Hadoop 又将槽分成Map 槽和Reduce 槽, 并且Map 任务只能使用Map槽, Reduce 任务只能使用Reduce槽。

Hadoop 的资源管理采用了静态资源设置方案,即每个节点配置好Map 槽和Reduce 槽的数量(配置项为mapred-site.xml 的mapred.task:tracker.map.tasks.maximum 和mapred.taskTracker.reduce.tasks.maximum),一旦Hadoop启动后将无法动态更改。

5 MapReduce 的局限性

  1. 从MapReduce 的特点可以看出MapReduce 的优点非常明显,但是MapReduce 也有其局限性,井不是处理海量数据的普适方法。它的局限性主要体现在以下几点:

    MapReduce 的执行速度慢。一个普通的MapReduce作业一般在分钟级别完成,复杂的作业或者数据量更大的情况下,也可能花费一小时或者更多,好在离线计算对于时间远没有OLTP那么敏感。所以MapReduce 现在不是,以后也不会是关系型数据库的终结者。MapReduce的慢主要是由于磁盘1/0 , MapReduce 作业通常都是数据密集型作业,大量的中间结果需要写到磁盘上并通过网络进行传输,这耗去了大量的时间。

  2. MapReduce过于底层。与SQL相比,MapReduce显得过于底层。对于普通的查询,一般人是不会希望写一个map 函数和reduce函数的。对于习惯于关系型数据库的用户,或者数据分析师来说,编写map 函数和reduce 函数无疑是一件头疼的事情。好在Hive的出现,大大改善了这种状况。

  3. 不是所有算法都能用MapReduce 实现。这意味着,不是所有算法都能实现并行。例如机器学习的模型训练, 这些算法需要状态共享或者参数间有依赖,且需要集中维护和更新。

6 来一个Hello world

本次hello world为word count程序,通过这个小程序来了解一下MapReduce的用法。

Mapper类:

import java.io.IOException;
import java.util.StringTokenizer; import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper; public class TokenizerMapper extends Mapper<Object, Text, Text, IntWritable> {
//声时一个IntWritable变量,作计数用,每出现一个key,给其一个value=1的值
IntWritable one = new IntWritable(1);
Text word = new Text(); public void map(Object key, Text value, Context context) throws IOException,InterruptedException {
//Hadoop读入的value是以行为单位的,其key为该行所对应的行号,因为我们要计算每个单词的数目,
//默认以空格作为间隔,故用StringTokenizer辅助做字符串的拆分,也可以用string.split("")来作。
StringTokenizer itr = new StringTokenizer(value.toString());
//遍历每一个单词
while(itr.hasMoreTokens()) {
word.set(itr.nextToken());
context.write(word, one);
}
}
}

以上就是Map打散过程。

上面我们看到有IntWritable这个数据类型,这里的Text相当于jdk中的String IntWritable相当于jdk的int类型,hadoop有8个基本数据类型,它们均实现了WritableComparable接口:

描述
BooleanWritable 标准布尔型数值
ByteWritable 单字节数值
DoubleWritable 双字节数
FloatWritable 浮点数
IntWritable 整型数
LongWritable 长整型数
Text 使用UTF8格式存储的文本
NullWritable 当《key,value》中的key或value为空时使用

下面接着写reduce类:

import java.io.IOException;

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer; public class MyReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
IntWritable result = new IntWritable(); public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException,InterruptedException {
int sum = 0;
//因为map已经将文本处理为键值对的形式,所以在这里只用取出map中保存的键值
for(IntWritable val:values) {
sum += val.get();
}
result.set(sum);
context.write(key,result);
}
}

下面是主函数:

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser; public class WordCount {
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
//判断一下命令行输入路径/输出路径是否齐全,即是否为两个参数
if(otherArgs.length != 2) {
System.err.println("Usage: wordcount <in> <out>");
System.exit(2);
} //此程序的执行,在hadoop看来是一个Job,故进行初始化job操作
Job job = new Job(conf, "wordcount");
//可以认为成,此程序要执行WordCount.class这个字节码文件
job.setJarByClass(WordCount.class);
//在这个job中,我用TokenizerMapper这个类的map函数
job.setMapperClass(TokenizerMapper.class);
//在这个job中,我用MyReducer这个类的reduce函数
job.setCombinerClass(MyReducer.class);
job.setReducerClass(MyReducer.class);
//在reduce的输出时,key的输出类型为Text
job.setOutputKeyClass(Text.class);
//在reduce的输出时,value的输出类型为IntWritable
job.setOutputValueClass(IntWritable.class);
//初始化要计算word的文件的路径
FileInputFormat.addInputPath(job, new Path(otherArgs[0]));
//初始化要计算word的文件的之后的结果的输出路径
FileOutputFormat.setOutputPath(job, new Path(otherArgs[1]));
//提交job到hadoop上去执行了,意思是指如果这个job真正的执行完了则主函数退出了,若没有真正的执行完就退出了。
System.exit(job.waitForCompletion(true)?0:1);
}
}

工程目录结构如下:

然后就是打jar包,我用的是idea,打包的过程就不表,自己百度一下。我打的jar包名为:hadoop.jar。打完之后将jar包上传至服务器。

在你的hadoop服务开启的状态下,输入如下命令:

#hadoop jar hadoop.jar cn.edu.hust.demo1.WordCount /user/input /user/output

中间的是主程序的路径,后面第一个路径是你的单词文档的路径,我将单词文档上传到/user/input下,/user/output的路径为输出路径。这个路径在执行上面这个命令之前不能存在,不然会报错。

执行完等一会之后,如果出现上面的输出表示运行成功。

下面是输出路径的内容:

我上传的txt 单词文件内容为:

hello world big
hello world xiaoming
the world is beautiful
xiaoming is my friend

运行完程序之后的单词统计结果为:

由此我们的单词统计就完成了。

下面我们借着词频统计来说一下MapReduce的运行过程。

7 MapReduce的运行过程深入了解

从前面的WordCount可以看出, 一个MapReduce作业经过了input,map,combine,reduce,output 五个阶段,其中combine阶段并不一定发生, map输出的中间结果被分发到reducer的过程被称为shuffle (数据混洗)。

7.1 从输入到输出的状态

在shuffle阶段还会发生copy(复制)和sort(排序)。

在MapReduce 的过程中,一个作业被分成Map和Reduce计算两个阶段,它们分别由两个或者多个Map任务和Reduce任务组成,这个在前面已经说过了。Reduce任务默认会在Map任务数量完成5%后才开始启动。

Map任务的执行过程可概括为:

  1. 首先通过用户指定的Inputformat类(如WordCount中的FilelnputFormat 类)中的getSplits方法和next方法将输入文件切片并解析成键值对作为map函数的输入。

  2. 然后map函数经过处理之后输出并将中间结果交给指定的Partitioner处理,确保中间结果分发到指定的Reduce任务处理,此时如果用户指定了Combiner,将执行combine操作。

  3. 最后map函数将中间结果保存到本地。

Reduce 任务的执行过程可概括为:

首先需要将已经完成的Map任务的中问结果复制到Reduce任务所在的节点,待数据复制完成后,再以key进行排序,通过排序,将所有key相同的数据交给reduce函数处理,处理完成后,结果直接输出到HDFS上。

7.2 input过程

如果使用HDFS上的文件作为MapReduce的输入(由于用户的数据大部分数据是以文件的

形式存储在HDFS上,所以这是最常见的情况)MapReduce计算框架首先会用

org.apache.hadoop.mapreduce.InputFormat类的子类FilelnputFormat类将作为输入的HDFS 上的文件切分形成输入分片(InputSplit),每个InputSplit将作为一个Map任务的输入,再将InputSplit解析为键值对。InputSplit的大小和数量对于MapReduce作业的性能有非常大的影响,因此有必要深入了解InputSplit 。

在List getSplits(JobContext job)方法中会对JobTracker进行拆分,确定需要多少个taskTracker。

该方法中调用了computeSplitSize(blockSize, minSize, maxSize)方法。用来计算拆分多少个taskTracker。我们看到三个参数:blockSize, minSize, maxSize,

  1. 其中minSize由mapred-site.xml文件中的配置项mapred.min.split.size决定,默认为l;
  2. maxSize由mapred-site.xml文件中的配置项mapred.max.split.size决定,默认为9223372036854775807;
  3. 而blockSize也是由hdfs-site.xml文件中的配置项dfs.block.size决定,默认为67108864字节(64MB)。

一般来说, dfs.block.size的大小是确定不变的,所以得到目标InputSplit大小,只需改变mapred.min.split.size和mapred.max.split.size的大小即可。

对于Map任务来说,处理的单位为一个InputSplit。而InputSplit是一个逻辑概念, InputSplit所包含的数据是仍然是存储在HDFS 的块里面,它们之间的关系如下图所示:

InputSplit 可以不和块对齐,根据前面的公式也可以看出,一个InputSplit的大小可以大于一个块的大小亦可以小于一个块的大小。Hadoop在进行任务调度的时候,会优先考虑本节点的数据,如果本节点没有可处理的数据或者是还需要其他节点的数据, Map 任务所在的节点会从其他节点将数据通过网络传输给自己。当InputSplit的容量大于块的容量,Map任务就必须从其他节点读取一部分数据,这样就不能实现完全数据本地性,所以当使用FileIputFormat实现InputFormat时,应尽量使InputSplit的大小和块的大小相同以提高Map任务计算的数据本地性。

7.3 Map及中间结果的输出

InputSplit将解析好的键值对交给用户编写的map函数处理,处理后的中间结果会写到本地磁盘上,在刷写磁盘的过程中,还做了partition(分区)和sort(排序)的操作。map 函数产生输出时,并不是简单地刷写磁盘。为了保证I/0 效率,采取了先写到内存的环形缓冲区,并作一次预排序。

每个Map任务都有一个环形内存缓冲区,用于存储map函数的输出。默认情况下,缓冲

区的大小是100 MB ,该值可以通过mapred-site.xml文件的io.sort.mb的配置项配置。一旦缓冲区内容达到阀值(由mapred-site.xml文件的io.sort.spill.percent 的值决定,默认为0.80或80%),一个后台线程便会将缓冲区的内容溢写(spill)到磁盘中。在写磁盘的过程中,map函数的输出继续被写到缓冲区,但如果在此期间缓冲区被填满,map会阻塞直到写磁盘过程完成。写磁盘会以轮询的方式写到mapred.local.dir(mapred-site.xml文件的配置项)配置的作业特定目录下。

在写磁盘之前,线程会根据数据最终要传送到的Reducer把缓冲区的数据划分成(默认是按照键)相应的分区。在每个分区中,后台线程技键进行内排序,此时如果有一个Combiner,它会在排序后的输出上运行。

如果己经指定Combiner且溢出写次数至少为3时,Combiner就会在输出文件写到磁盘之前运行。如前文所述,Combiner可以多次运行,并不影响输出结果。运行Combiner 的意义在于使map 输出的中间结果更紧凑,使得写到本地磁盘和传给Reducer的数据更少。

为了提高磁盘I/O 性能,可以考虑压缩map的输出,这样会让写磁盘的速度更快,节约磁盘空间,从而使传送给Reducer的数据量减少。默认情况下,map的输出是不压缩的,但只要将mapred-site.xml文件的配置项mapred.compress.map.output设为true 即可开启压缩功能。使用的压缩库由mapred-site.xml文件的配置项mapred.map.output.compression.codec指定。

map输出的中间结果存储的格式为IFile,IFile是一种支持行压缩的存储格式。Reducer通过HTTP方式得到输出文件的分区。将map输出的中间结果(map 输出)发送

到Reducer的工作线程的数量由mapred-site.xml文件的tasktracker.http.threads 配置项决定,此配置针对每个节点,即每个TaskTracker,而不是每个Map任务,默认是40,可以根据作业大小,集群规模以及节点的计算能力而增大。

7.4 shuffle

shuffle,也叫数据混洗。在某些语境中,代表map函数产生输出到reduce的消化输入的整个过程,上面我们也有一个流程图中看到shuffle是在combine和reduce之间的过程,前面map已经处理完了数据那为什么还要shuffle呢,这就是shuffle的功能:完成数据和合并和排序。

前面我们说了,Map任务输出的结果位于运行Map任务的TaskTracker所在的节点的本地磁盘上。TaskTracker需要为这些分区文件(map输出)运行Reduce任务。但是 Reduce任务可能需要多个Map 任务的输出作为其特殊的分区文件。每个Map 任务的完成时间可能不同,当只要有一个任务完成,Reduce 任务就开始复制其输出。这就是shuffle中的copy阶段。Reduce任务有少量复制线程,可以井行取得Map 任务的输出,默认值是5个线程,该值可以通过设置mapred-site.xml的mapred.reduce.parallel.copies配置项来改变。

如果map输出相当小,则会被复制到Reducer所在的TaskTracker的内存的缓冲区中,缓冲区的大小由mapred-site.xml文件中的mapred.job.shuffie.input.buffer.percent配置项指定。否则,

map输出将会被复制到磁盘。一旦内存缓冲区达到阀值大小(由mapred-site.xml 文件的

mapred.job.shuffle.merge.percent 配置项决定)或缓冲区的文件数达到阀值大小(由

mapred-site.xml文件的mapred.inmem.merge.threshold配置项决定),则合并后溢写到磁盘中。

随着溢写到磁盘的文件增多,后台线程会将它们合并为更大的、有序的文件,这会为后面的合并节省时间为了合并,压缩的中间结果都将在内存中解压缩。

复制完所有的map输出,shuffle进入sort阶段。这个阶段将合并map的输出文件,并维持其顺序排序,其实做的是归并排序。排序的过程是循环进行,如果有50个map的输出文件,而合并因子(由mapred-site.xml文件的io.sort.factor配置项决定,默认为10)为10,合并操作将进行5次,每次将10个文件合,并成一个文件,最后会有5个文件,这5个文件由于不满足合并条件(文件数小于合并因子),则不会进行合井,将会直接把这5 个文件交给reduce函数处理。至此,shuffle 阶段完成。

从shuffle的过程可以看出,Map任务处理的是一个InputSplit,而Reduce任务处理的是所有Map任务同一个分区的中间结果。

reduce阶段操作的实质就是对经过shuffle处理后的文件调用reduce函数处理。由于经过了shuffle的处理,文件都是按键分区且有序,对相同分区的文件调用一次reduce函数处理。与map的中间结果不同的是,reduce的输出一般为HDFS。

7.5 sort

上面我们讲到shuffle的过程在合并Map的时候顺带做了排序。其实排序贯穿Map和Reduce的所有任务,在MapReduce任务中一共发生了三次排序:

  1. 当map函数产生输出时,会首先写入内存的环形缓冲区,当达到设定的阀值,在刷写磁盘之前,后台线程会将缓冲区的数据划分戚相应的分区。在每个分区中,后台线程技键进行内排序。
  2. 在Map任务完成之前,磁盘上存在多个己经分好区并排好序的、大小和缓冲区一样的溢写文件,这时溢写文件将被合并成一个己分区且己排序的输出文件。由于溢写文件己经经过第一次排序,所以合并文件时只需再做一次排序就可使输出文件整体有序。
  3. 在shuffle 阶段,需要将多个Map任务的输出文件合并,由于经过第二次排序,所以合并文件时只需再做一次排序就可使输出文件整体有序。

在这3次排序中第一次是在内存缓冲区做的内排序,使用的算法是快速排序,第二次排序和第三次排序都是在文件合并阶段发生的,使用的是归并排序。

至此mapReduce的工作过程我们就说完啦,多看几次好好消化消化吧!

hadoop学习(七)----mapReduce原理以及操作过程的更多相关文章

  1. hadoop笔记之MapReduce原理

    MapReduce原理 MapReduce原理 简单来说就是,一个大任务分成多个小的子任务(map),并行执行后,合并结果(reduce). 例子: 100GB的网站访问日志文件,找出访问次数最多的I ...

  2. [Hadoop]浅谈MapReduce原理及执行流程

    MapReduce MapReduce原理非常重要,hive与spark都是基于MR原理 MapReduce采用多进程,方便对每个任务资源控制和调配,但是进程消耗更多的启动时间,因此MR时效性不高.适 ...

  3. Hadoop学习笔记—MapReduce的理解

    我不喜欢照搬书上的东西,我觉得那样写个blog没多大意义,不如直接把那本书那一页告诉大家,来得省事.我喜欢将我自己的理解.所以我会说说我对于Hadoop对大量数据进行处理的理解.如果有理解不对欢迎批评 ...

  4. Hadoop学习之Mapreduce执行过程详解

    一.MapReduce执行过程 MapReduce运行时,首先通过Map读取HDFS中的数据,然后经过拆分,将每个文件中的每行数据分拆成键值对,最后输出作为Reduce的输入,大体执行流程如下图所示: ...

  5. 【尚学堂·Hadoop学习】MapReduce案例2--好友推荐

    案例描述 根据好友列表,推荐好友的好友 数据集 tom hello hadoop cat world hadoop hello hive cat tom hive mr hive hello hive ...

  6. 【尚学堂·Hadoop学习】MapReduce案例1--天气

    案例描述 找出每个月气温最高的2天 数据集 -- :: 34c -- :: 38c -- :: 36c -- :: 32c -- :: 37c -- :: 23c -- :: 41c -- :: 27 ...

  7. hadoop学习day3 mapreduce笔记

    1.对于要处理的文件集合会根据设定大小将文件分块,每个文件分成多块,不是把所有文件合并再根据大小分块,每个文件的最后一块都可能比设定的大小要小 块大小128m a.txt 120m 1个块 b.txt ...

  8. Hadoop学习(3)-mapreduce快速入门加yarn的安装

    mapreduce是一个运算框架,让多台机器进行并行进行运算, 他把所有的计算都分为两个阶段,一个是map阶段,一个是reduce阶段 map阶段:读取hdfs中的文件,分给多个机器上的maptask ...

  9. hadoop学习之HDFS原理

    HDFS原理 HDFS包括三个组件: NameNode.DataNode.SecondaryNameNode NameNode的作用是存储元数据(文件名.创建时间.大小.权限.与block块映射关系等 ...

随机推荐

  1. [AI开发]目标跟踪之计数

    基于视频结构化的应用中,目标在经过跟踪算法后,会得到一个唯一标识和它对应的运动轨迹,利用这两个数据我们可以做一些后续工作:测速(交通类应用场景).计数(交通类应用场景.安防类应用场景)以及行为检测(交 ...

  2. Django中使用JS通过DataTable实现表格前端分页,每页显示页数,搜索等功能

    Django架构中自带了后端分页的技术,通过Paginator进行分页,前端点击按钮提交后台进行页面切换. 优缺点:后端分页对于数据量大的场景有其优势,但页面切换比较慢. 后端分页python3代码如 ...

  3. MySql突然连接不上,报Can't connect to MySQL server on 'localhost' (10061),并且没有mysqld时解决方案

    今天连接数据库时突然连接不上,前一天还是好好的.打开数据库就报 Can't connect to MySQL server on 'localhost' (10061) 一直也知道是MySQL服务没有 ...

  4. json字符串转换成java对象

  5. WinForm控件之【LinkLabel】

    基本介绍 超链接标签控件,随处可见应用极为广泛,一般用作触发指定链接跳转指定页面等操作. 常设置属性.事件 ActiveLinkColor:用户单击超链接时超链接显示的颜色: LinkColor:超链 ...

  6. MyBatis从入门到精通:第一章配置文件log4j.properties

    配置文件: #全局配置 log4j.rootLogger=ERROR,stdout #MyBatis日志配置 log4j.logger.tk.mybatis.simple.mapper=TRACE # ...

  7. MFC在一个工程中启动其他工程的exe文件

    说明:有的时候把两个工程合并,但是偷懒不想在工程中添加代码,所以想到了这个办法,仅限偷懒哈哈哈哈 方法:新建一个主程序,在主程序的界面中添加按钮,在按钮的程序代码中添加以下语句: void CMain ...

  8. python菜鸟入门知识

    第二章 入门 2.1 输出 2.1.1print() 输出 print(12+21) print((12+21)*9) print(a) # 注意a不可以加引号 2.2变量 1.变量由字母,数字,下划 ...

  9. 通过代数,数字,欧几里得平面和分形讨论JavaScript中的函数式编程

    本文是对函数式编程范式的系列文章从而拉开了与以下延续一个. 介绍 在JavaScript中,函数只是对象.因此,可以构造函数,作为参数传递,从函数返回或分配给变量.因此,JavaScript具有一流的 ...

  10. 个人永久性免费-Excel催化剂功能第69波-打造最专业易用的商务图表库,即点即用的高级Excel图表

    Excel很大一块细分领域是图表,数据分析的末端,数据展示环节,精美恰当的图表,能够为数据分析数据结论带来画龙点睛的一笔.Excel催化剂简单内置了图表库,利用已经做好的模板式的图表示例,可快速复制使 ...