函数式编程

概念

问题:JavaScript中,一等公民是什么?

函数是一等公民(First-Class Function第一级函数),纯面向对象里,函数能做什么?

  • 函数声明
  • 函数调用
  • 简单封装
  • 赋值
  • 传参
  • 返回值
  • 构造函数
  • 类实例
  • 立即执行

高阶函数

JavaScript函数实际上都是会指向某一个变量,变量可以指向一个函数,函数的参数能接收变量,一个函数就可以接受另一个函数作为变量

问题:什么是高阶函数?

只要函数的参数是函数,或者返回值是函数的形式时,都成为高阶函数

问题:高阶函数具体有什么应用场景?

  1. 能够将代码划分为若干片段,然后按顺序执行
  2. 将功能或复用的业务能够抽离成一个函数,如参数是函数的形式(axios的封装)
  3. 一个函数只能做一件事情(单独的业务功能),如具有高度相似业务逻辑的工具函数的再封装(返回值是函数的形式)

问题:函数的短板?

函数作为另外函数的参数,函数赋值遍历,函数作为返回值,操作比较繁琐,需要通过指针,代理的方式实现。

问题:函数式编程是什么?

一个固定的功能或者程序段被封装的过程,实现一个固定的功能或者程序,在这个封装体中需要一个入口和一个出口

  • 入口就是参数
  • 出口就是返回值
  1. function test(str1, str2, str3) {
  2. return str1 + str2 + str3;
  3. }
  4. console.log(test('哈','哈哈','哈哈哈'));

函数式编程,设计模式,面向对象规范程序写法和架构,构成程序开发体系

问题:JavaScript编程特点是什么?

  1. 函数式编程和面向对象编程的混编语言
  2. 可拓展性强(函数参数个数和数据类型不确定)

面向对象与函数式编程的关系

  • 面向对象:适用于高度复用场景,有复杂this指向问题
  • 函数式编程的优点:易读易维护
  • 函数式编程是第一类对象,不依赖任何其他对象独立存在(概念)

纯函数

纯函数是相同的输入得到相同的输出,不依赖且不影响外部环境也不产生任何副作用

简而言之,输出完全取决于输入

补充:副作用

只要跟函数外部发生了交互就是副作用,如数据请求,改变数据,DOM操作,数据存取cookie/session

  1. //示例:
  2. //不是纯函数
  3. //1.依赖了外部
  4. //2.并没有参数输入
  5. var a = 1;
  6. function test(){
  7. console.log(a);
  8. }
  9. test();
  10. //如何提纯?
  11. function test(num){
  12. console.log(num);
  13. }
  14. test(a);

进一步理解纯函数,问题所在

  1. //splice修改原数组(副作用)
  2. //slice修改的是新的数组

说明slice方法是纯函数

对象会被修改数据,如不想被更改原有数据,需要克隆原有数据再修改,深度克隆的函数也是纯函数

纯函数的优点:

  • 可移植性高
  • 可测试性
  • 合理性(引用透明)
  • 并行执行
  • 可缓存性

纯函数使用场景:

可提纯则提纯,不行不强求

函数组合

它也叫饲养函数(compose),饲养高品质的函数做优质的输出,用一个新的函数把其他功能的函数组合起来后可以一次性解决这些函数的单个函数要解决的问题

若干个纯函数,偏函数,柯里化函数组合成一个新的函数形成数据传递,并实现一种有序执行的效果

左倾方式:

函数参数自右向左边执行函数的方式

  1. function(f, g){
  2. //返回函数的目的为了不立即执行f,g函数
  3. return function(x){
  4. //x是通过f,g之间的管道传输值
  5. return f(g(x));
  6. }
  7. }

写法一:

  1. function toUpperCase(str) {
  2. return str.toUpperCase();
  3. }
  4. function join(str) {
  5. return str.join('-');
  6. }
  7. function reverse(str) {
  8. return str.reverse();
  9. }
  10. function exclaim(str) {
  11. return str + '!';
  12. }
  13. function split(arr) {
  14. return arr.split('');
  15. }
  16. function compose1() {
  17. //将传参组合的列表转为数组
  18. var args = Array.prototype.slice.call(arguments);
  19. // console.log(args);
  20. //[ƒ, ƒ]
  21. //传参列表最后一项
  22. var len = args.length - 1;
  23. return function (x) {
  24. //执行参数列表最后一项的函数
  25. var res = args[len](x);
  26. // console.log(res);
  27. //HELLO
  28. //while循环每次执行完毕向左继续执行直到长度为0结束循环
  29. while (len--) {
  30. //执行前一项函数,并传入上次计算出的结果
  31. res = args[len](res);
  32. }
  33. return res;
  34. }
  35. }
  36. var f = compose1(split, toUpperCase);
  37. console.log(f('hello')); //['H', 'E', 'L', 'L', 'O']

写法二:

  1. function compose2() {
  2. var args = Array.prototype.slice.call(arguments);
  3. return function (x) {
  4. return args.reduceRight(function (res, cb) {
  5. // console.log(res); //ƒ toUpperCase(str)
  6. // console.log(cb); //ƒ split(arr)
  7. return cb(res);
  8. }, x);
  9. }
  10. }
  11. var g = compose2(split, toUpperCase);
  12. console.log(g('hello'));

问题:什么叫结合律?

Associativity,结合律是在组合函数的参数中再进行分组,它和原来函数组合得出的结果是一样的

柯里化

curry,接受多个参数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术

目的:

  • 简化代码
  • 提高维护性
  • 功能单一化

优点:

  • 功能内聚,关键性的业务逻辑在主体(前面)去完成,最后完成整个功能体程序
  • 降低耦合,闭包形式
  • 降低代码的重复性
  • 提高代码的适应性,适用于任何类型的项目

实现过程:

  1. var add2 = curry(add);
  2. var add3 = curry(1);
  3. var add4 = curry(2);
  4. var add5 = curry(3);
  5. //如果没有传完参数每次都要返回一个新的函数(本应该多传参数)
  6. function curry(fn, len) {
  7. var len = len || fn.length;
  8. var _func = function (fn) {
  9. //获取除了第一个参数以外的其他参数列表
  10. var _arg = [].slice.call(arguments, 1);
  11. return function () {
  12. //获取内部函数新的参数列表并合并外层的参数列表
  13. var newArgs = _arg.concat([].slice.call(arguments));
  14. //执行并传入合并后的参数列表
  15. return fn.apply(this, newArgs);
  16. }
  17. }
  18. return function () {
  19. var argLen = arguments.length;
  20. if (argLen < len) {
  21. //合并后的格式:[add, 1, 2]
  22. var formatedArr = [fn].concat([].slice.call(arguments));
  23. return curry(_func.apply(this, formatedArr), len - argLen);
  24. } else {
  25. return fn.apply(this, arguments);
  26. }
  27. }
  28. }
  29. function add(a, b, c, d) {
  30. return a + b + c + d;
  31. }
  32. var add2 = curry(add);
  33. // var res = add2(1)(2)(3)(4);
  34. // var res = add2(1, 2)(3)(4);
  35. var res = add2(1, 2, 3)(4);
  36. console.log(res); //10

应用:

  1. //封装ajax请求函数
  2. function ajaxRequest(opt, data, successCb, errorCb) {
  3. $.ajax({
  4. url: opt.url,
  5. type: opt.type,
  6. dataType: opt.dataType,
  7. data: data,
  8. success: successCb,
  9. error: errorCb
  10. });
  11. }
  12. var $ajax = curry(ajaxRequest);
  13. //只固化传了参数1
  14. var ajaxApi = {
  15. getCourses: $ajax({
  16. url: 'http://xxx.com/getCourse',
  17. type: 'POST',
  18. dataType: 'JSON'
  19. })
  20. }
  21. //继续传参
  22. ajaxApi.getCourses({
  23. page: 1
  24. })
  25. (function (data) {
  26. console.log(data)
  27. })
  28. (function (err) {
  29. console.log(err)
  30. });

偏函数

问题:什么是函数的元?

它是函数参数的个数,如有两个参数的函数时叫二元函数

问题:什么是偏函数?

partial application,部分应用,在计算机科学中,偏函数叫做部分应用,局部应用,指固定一个函数的一些参数,然后产生另一个更小元的函数,实际上是一种降元的过程

偏函数与柯里化函数的区别:

用法比较像,但是实现的目的不一样,柯里化传参的形式不止一种,而偏函数传参是较为单一的把剩余参数传入

  • 柯里化:将一个多参数的函数转成多个单参数的函数,将n元转换成n个一元函数

  • 偏函数:固定一个函数的一个或多个参数,先执行一次返回新函数,将n元函数转换为n-x元的函数

问题:为什么存在偏函数的思想?

在开发的时候,一部分的函数参数是可以直接先传(固定,如系统内部程序),后面传的值是由其他程序通过用户交互来接收新的参数

实现:

利用bind绑定参数并返回一个未执行的新函数特性

  1. function add(a, b, c, d){
  2. return a + b + c + d;
  3. }
  4. var newAdd = add.bind(null, 1, 2);

封装:

  1. function add(a, b, c, d) {
  2. return a + b + c + d;
  3. }
  4. Function.prototype.partial = function () {
  5. var _self = this,
  6. _args = [].slice.call(arguments);
  7. return function () {
  8. var newArgs = _args.concat([].slice.call(arguments));
  9. return _self.apply(null, newArgs);
  10. }
  11. }
  12. var newAdd = add.partial(1, 2);
  13. console.log(newAdd(3, 4));

惰性函数

适用于底层代码封装,程序优化等方面,优化函数本身,函数内部改变自身的机制

  1. var getTimeStamp = function(){
  2. //第一次才会执行
  3. var timeStamp = new Date().getTime();
  4. getTimeStamp = function(){
  5. return timeStamp;
  6. }
  7. return timeStamp;
  8. }
  9. console.log(getTimeStamp());

总结:

惰性函数就是惰性加载表示函数执行的分支只会在函数第一次调用的时候执行,在第一次调用的过程中,该函数被覆盖为另一个按照合适的方式执行的函数,这样任何对原函数的调用就不会再经过执行的分支

函数记忆

它是函数优化的一种方式,也叫缓存函数(memorize)

实现过程:

  1. //统计阶乘函数递归的次数
  2. var time = 0,
  3. cache = [];
  4. function factorial(n){
  5. time++;
  6. if(cache[n]){
  7. return cache[n];
  8. }
  9. if(n === 0 || n === 1){
  10. cache[0] = 1;
  11. cache[1] = 1;
  12. return 1;
  13. }
  14. return cache[n] = n * factorial(n - 1);
  15. }

另一种写法:

  1. function memorize(fn) {
  2. var cache = {};
  3. return function () {
  4. // console.log(cache); {16: undefined}
  5. //获取逗号隔开的参数列表字符串
  6. var str = [].join.call(arguments, ',');
  7. // console.log(str); //6
  8. var k = arguments.length + str;
  9. // console.log(k); //16
  10. return cache[k] = cache[k] || fn.apply(this, arguments);
  11. }
  12. }
  13. function factorial() {}
  14. var f = memorize(factorial);
  15. console.log(f(6));

防抖

防抖操作实现延迟执行,防止用户交互时产生多次触发

  • 延迟执行:对于在事件被触发n秒后再执行的回调
  • 如果在这n秒内再次触发事件时,重新开始计时

应用:

在发送网络请求时进行防抖处理,初次下拉不会延迟操作,再次触发时才需要延迟操作

封装:

  1. //1.是否首次延迟执行
  2. //2.n秒之内,触发事件不执行事件处理函数(n秒之内频繁触发事件,计时器会频繁重新开始计时)
  3. function debounce(fn, time, triggerNow) {
  4. var t = null,
  5. res;
  6. var debounced = function () {
  7. var _self = this,
  8. args = arguments;
  9. //无论什么情况都先清除计时器
  10. if (t) {
  11. clearTimeout(t);
  12. }
  13. if (triggerNow) {
  14. var isFirstTrigger = !t;
  15. //重置t
  16. t = setTimeout(function () {
  17. t = null;
  18. }, time);
  19. if (isFirstTrigger) {
  20. res = fn.apply(_self, args);
  21. }
  22. } else {
  23. t = setTimeout(function () {
  24. res = fn.apply(_self, args);
  25. }, time);
  26. }
  27. return res;
  28. }
  29. debounced.remove = function () {
  30. clearTimeout(t);
  31. t = null;
  32. }
  33. return debounced;
  34. }

节流

事件被触发,n秒之内只执行一次事件处理函数

应用:

输入框文本检测验证

封装:

  1. function throttle(fn, delay) {
  2. var t = null,
  3. begin = new Date().getTime();
  4. return function () {
  5. var _self = this,
  6. args = arguments,
  7. cur = new Date().getTime();
  8. clearTimeout(t);
  9. if (cur - begin >= delay) {
  10. fn.apply(_self, args);
  11. begin = cur;
  12. } else {
  13. t = setTimeout(function () {
  14. fn.apply(_self, args);
  15. }, delay);
  16. }
  17. }
  18. }

归类

数据归类技术,前端处理后端接口数据

  • 单一归类:一条数据对应一个分类
  • 复合归类:一条数据对应多个类别hobby=[1,2,3]
  1. function sortDatas(sort, data) {
  2. var cache = {};
  3. /**
  4. * 数据格式1:
  5. * hobby = [{"id":"1","name":"football"}]
  6. * 数据格式2:
  7. * person = [{"name":"zhangsan","hobby":"1,3"}]
  8. *
  9. * @foreign_key 两种数据关联的值
  10. * @sortType 归类类型 单一/复合
  11. */
  12. return function (foreign_key, sortType) {
  13. //排除不合法的归类类型
  14. if (sortType !== 'single' && sortType !== 'multi') {
  15. console.log(new Error('Invalid sort type.'));
  16. return;
  17. }
  18. sort.forEach(function (sort) {
  19. var _id = sort.id;
  20. cache[_id] = [];
  21. data.forEach(function (elem) {
  22. var foreign_val = elem[foreign_key];
  23. switch (sortType) {
  24. case 'single':
  25. if (foreign_val == _id) {
  26. cache[_id].push(elem);
  27. }
  28. break;
  29. case 'multi':
  30. var _arr = foreign_val.split(',');
  31. _arr.forEach(function (val) {
  32. if (val == _id) {
  33. cache[_id].push(elem);
  34. }
  35. });
  36. break;
  37. default:
  38. break;
  39. }
  40. });
  41. });
  42. return cache;
  43. }
  44. }
  45. var sex = [{
  46. "id": "1",
  47. "sex": "male"
  48. },
  49. {
  50. "id": "2",
  51. "sex": "female"
  52. }
  53. ];
  54. var users = [{
  55. "id": "1",
  56. "name": "zhangsan",
  57. "sex": "1"
  58. },
  59. {
  60. "id": "2",
  61. "name": "lisi",
  62. "sex": "2"
  63. }
  64. ];
  65. //单一归类
  66. var singleSort = sortDatas(sex, users);
  67. console.log(singleSort('sex', 'single'));
  68. /**
  69. * {
  70. * 1: [{id: '1', name: 'zhangsan', sex: '1'}],
  71. * 2: [{id: '2', name: 'lisi', sex: '1'}]
  72. * }
  73. */
  74. var hobby = [{
  75. "id": "1",
  76. "name": "football"
  77. },
  78. {
  79. "id": "2",
  80. "name": "basketball"
  81. }
  82. ];
  83. var person = [{
  84. "name": "wangwu",
  85. "hobby": "1,3"
  86. },
  87. {
  88. "name": "zhaoliu",
  89. "hobby": "2,4"
  90. }
  91. ];
  92. //复合归类
  93. var multiSort = sortDatas(hobby, person);
  94. console.log(multiSort('hobby', 'multi'));
  95. /**
  96. * {
  97. * 1: [{name: 'wangwu', hobby: '1,3'}],
  98. * 2: [{name: 'zhaoliu', hobby: '2,4'}]
  99. * }
  100. */

扁平化

将多维数组变为一维数组

写法1:

  1. function flatten(arr) {
  2. //用户没有填写时
  3. var _arr = arr || [],
  4. finalArr = [],
  5. len = _arr.length,
  6. item;
  7. for (var i = 0; i < len; i++) {
  8. item = _arr[i];
  9. //某项元素是数组时
  10. if (_isArr(item)) {
  11. //递归并拼接新的数组
  12. finalArr = finalArr.concat(flatten(item));
  13. } else {
  14. //不是数组时
  15. finalArr.push(item);
  16. }
  17. }
  18. return finalArr;
  19. function _isArr(obj) {
  20. return {}.toString.call(obj) === '[object Array]';
  21. }
  22. }
  23. var arr = [1, 2, ['a', 'b'], 3, [
  24. [
  25. ['c', ['d', 'e']]
  26. ], 'f'
  27. ], {}, null, undefined];
  28. var res = flatten(arr);
  29. console.log(res);
  30. //[1, 2, 'a', 'b', 3, 'c', 'd', 'e', 'f', {…}, null, undefined]

写法2:

  1. Array.prototype.flatten = function () {
  2. var _arr = this,
  3. toStr = {}.toString;
  4. if (toStr.call(_arr) !== '[object Array]') {
  5. throw new Error('only array type can use flatten.')
  6. }
  7. return _arr.reduce(function (prev, elem) {
  8. return prev.concat(
  9. toStr.call(elem) === '[object Array]'
  10. ? elem.flatten()
  11. : elem
  12. );
  13. }, []);
  14. }
  15. var arr = [1, 2, ['a', 'b'], 3, [
  16. [
  17. ['c', ['d', 'e']]
  18. ], 'f'
  19. ], {}, null, undefined];
  20. var res = arr.flatten();
  21. console.log(res);
  22. //[1, 2, 'a', 'b', 3, 'c', 'd', 'e', 'f', {…}, null, undefined]

响应式

JavaScript是一种单向型的语言,并不能响应式的

问题:什么叫响应式编程?

一个方法的执行来通知其他的方法去执行,方法与方法之间是互相响应的,像开发模式中的订阅模式,或者是观察者模式,响应式是变相的订阅模式和观察者模式

问题:如何去做响应式?

通过defineProperty()方法进行对象属性的加工处理,取值重新赋值时逻辑拓展,可读写,可枚举,可删除

vue2.x版本 实际上都在用defineProperty来实现双向数据绑定,数据双向绑定实际上是响应式的编程方式,当一个方法执行或对一个属性进行设置时,希望有一些数据上响应的能力,数据一旦发生更改视图也会随之更改,视图上更改从而数据上也会更改,背后逻辑是响应式编程实现的

案例:计算器

思想:利用defineProperty()方法实现数据响应

实现:文本框输入数值的同时已将开始计算且实时显示计算结果

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>Document</title>
  8. <style>
  9. .btn-group button.current {
  10. background-color: orange;
  11. color: #fff;
  12. }
  13. </style>
  14. </head>
  15. <body>
  16. <div class="J_calculator">
  17. <div class="result">0</div>
  18. <div class="input-group">
  19. <input type="text" class="f-input" value="0">
  20. <input type="text" class="s-input" value="0">
  21. </div>
  22. <div class="btn-group">
  23. <button data-field="plus" class="current">+</button>
  24. <button data-field="minus">-</button>
  25. <button data-field="mul">*</button>
  26. <button data-field="div">/</button>
  27. </div>
  28. </div>
  29. </body>
  30. <script type="text/javascript">
  31. class Calculator {
  32. constructor(doc) {
  33. const oCal = doc.getElementsByClassName('J_calculator')[0];
  34. this.fInput = oCal.getElementsByTagName('input')[0];
  35. this.sInput = oCal.getElementsByTagName('input')[1];
  36. this.oBtnGroup = oCal.getElementsByClassName('btn-group')[0];
  37. this.oBtnItems = this.oBtnGroup.getElementsByTagName('button');
  38. this.oResult = oCal.getElementsByClassName('result')[0];
  39. //将取值/赋值的对象属性的函数返回的新对象赋值给data
  40. //此操作会形成数据响应式更新
  41. //实现:文本框输入数值的同时已将开始计算且实时显示计算结果
  42. this.data = this.defineData();
  43. this.btnIdx = 0;
  44. }
  45. init() {
  46. this.bindEvent();
  47. }
  48. bindEvent() {
  49. this.oBtnGroup.addEventListener('click', this.onFieldBtnClick.bind(this), false);
  50. this.fInput.addEventListener('input', this.onNumberInput.bind(this), false);
  51. this.sInput.addEventListener('input', this.onNumberInput.bind(this), false);
  52. }
  53. defineData() {
  54. let _obj = {},
  55. fNumber = 0,
  56. sNumber = 0,
  57. field = 'plus',
  58. _self = this;
  59. Object.defineProperties(_obj, {
  60. fNumber: {
  61. get() {
  62. console.log(`got fNumber: ${fNumber}`);
  63. return fNumber;
  64. },
  65. set(newValue) {
  66. fNumber = newValue;
  67. _self.computeResult(fNumber, sNumber, field);
  68. console.log(`fNumber has been changed to ${fNumber}`);
  69. }
  70. },
  71. sNumber: {
  72. get() {
  73. console.log(`got sNumber: ${sNumber}`);
  74. return sNumber;
  75. },
  76. set(newValue) {
  77. sNumber = newValue;
  78. _self.computeResult(fNumber, sNumber, field);
  79. console.log(`fNumber has been changed to ${sNumber}`);
  80. }
  81. },
  82. field: {
  83. get() {
  84. console.log(`got field: ${field}`);
  85. return field;
  86. },
  87. set(newValue) {
  88. field = newValue;
  89. _self.computeResult(fNumber, sNumber, field);
  90. console.log(`fNumber has been changed to ${field}`);
  91. }
  92. },
  93. });
  94. return _obj;
  95. }
  96. computeResult(fNumber, sNumber, field) {
  97. switch (field) {
  98. case 'plus':
  99. this.oResult.innerText = fNumber + sNumber;
  100. break;
  101. case 'minus':
  102. this.oResult.innerText = fNumber - sNumber;
  103. break;
  104. case 'mul':
  105. this.oResult.innerText = fNumber * sNumber;
  106. break;
  107. case 'div':
  108. this.oResult.innerText = fNumber / sNumber;
  109. break;
  110. }
  111. }
  112. onFieldBtnClick(ev) {
  113. const e = ev || window.event,
  114. tar = e.target || e.srcElement,
  115. tagName = tar.tagName;
  116. tagName === 'BUTTON' && this.fieldUpdate(tar);
  117. }
  118. onNumberInput(ev) {
  119. const e = ev || window.event,
  120. tar = e.target || e.srcElement,
  121. className = tar.className,
  122. val = Number(tar.value.replace(/\s+/g, '')) || 0;
  123. // console.log(className); f-input/s-input
  124. switch (className) {
  125. case 'f-input':
  126. this.data.fNumber = val;
  127. break;
  128. case 's-input':
  129. this.data.sNumber = val;
  130. break;
  131. }
  132. }
  133. fieldUpdate(target) {
  134. // console.log(target);
  135. //<button data-filed="plus" class="current">+</button>
  136. this.oBtnItems[this.btnIdx].className = '';
  137. //indexOf 找到的元素在数组中的索引
  138. this.btnIdx = [].indexOf.call(this.oBtnItems, target);
  139. target.className += ' current';
  140. this.data.field = target.getAttribute('data-field');
  141. }
  142. }
  143. new Calculator(document).init();
  144. </script>
  145. </html>