高阶函数

尾调根系
• 阅读 1267

NodeJS 系列文章,本篇是第一篇,首先,预计将后续高频使用逻辑串一遍,依次是高阶函数,promise以及事件机制。本篇主要是高阶函数。

call、bind、apply

  • call、apply 都是改变 this 指向,区别是接受参数的方式不一样,call 一个一个传参数,apply 接收一个数组。
  • bind 会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind 方第一个参数作为 this,传入 bind 的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。
  • 当你希望改变上下文环境之后并非立即执行,而是回调执行的时候,使用 bind() 方法,bind 是返回的函数。
  • apply、call 则会立即执行函数。

this 指向

this 指向问题,谁调用,我就指向谁。

全局上下文

非严格模式和严格模式中this都是指向顶层对象(浏览器中是window)。

  console.log(this === window); // true
  'use strict'
  console.log(this === window); // true
  this.name = 'Hiraku';
  console.log(this.name); // Hiraku

函数上下文

普通函数调用模式

  var name = '骁妈';
  var func = function() {
    console.log(this === window, this.name); // true 骁妈(浏览器)
  }; 

  func();

在 ES5 中,全局变量是挂载在顶层对象(浏览器是 window )中。

  const name1 = 'mahongqin';
  const func1 = function() {
    console.log(this === window, this.name1); // true undefined
  };
  const func2 = () => {
    console.log(this === window, this.name1); // true undefined
  };
  func1();
  func2();

let 没有给顶层对象中(浏览器是 window )添加属性,window.name1 和 window.func1,window.func2 都是 undefined。

严格模式中,普通函数中的 this 则表现不同,表现为 undefined。

  'use strict';
  var name2 = '骁妈';
  var func3 = function() {
    console.log(this === window); // false
  }; 

  func3();

call,apply 作用之一就是改变函数的 this 指向为第一个参数。 如果第一个参数是 undefined 或者 null,非严格模式下,是指向 window。严格模式下,就是指向第一个参数。

对象中的函数(方法)调用模式

  var name3 = 'Hiraku_Ma';
  var func4 = function() {
    console.log(this, this.name);
  }
  var obj = {
    name: 'mhq',
    func: func4,
    attr: {
      name: 'mhq-gs',
      func: func4,
    }
  }
  obj.func(); // Object{} obj 'mhq'
  obj.attr.func(); // Object{} obj.attr 'mhq-gs'
  // 用call类比则为:
  obj.func.call(obj); // 'mhq'
  // 用call类比则为:
  obj.attr.func.call(obj.attr); // 'mhq-gs'

函数赋值之后变成普通函数。

构造函数调用模式

function Example(type){
  this.type = type;
  console.log(this);
  // return this;
}
var exp = new Example('mhq');

使用 new 操作符调用函数,会自动执行以下步骤:

  • 创建了一个全新的对象。
  • 这个对象会被执行[[Prototype]](也就是__proto__)链接。
  • 生成的新对象会绑定到函数调用的 this。
  • 通过 new 创建的每个对象将最终被[[Prototype]]链接到这个函数的 prototype 对象上。
  • 如果函数没有返回对象类型 Object( 包含 Functoin, Array, Date, RegExg, Error ),那么 new 表达式中的函数调用会自动返回这个新的对象。

new 操作符调用时,this 指向生成的新对象。 new 调用时的返回值,如果没有显式返回对象或者函数,才是返回生成的新对象。

  function Example(type){
    this.type = type;
    console.log(this);
    return {};
  }
  var exp = new Example('mhq'); // { name: mhq }
  console.log(exp);
  // 如果返回函数 f ,则 exp 是函数 f,如果是对象 {} ,则 exp 是对象 {}

原型链中的调用模式

  function Example(type){
    this.type = type;
    console.log(this);
  }
  Example.prototype.func = function() {
    console.log(this.type);
  };
  var exp = new Example('mhq');
  exp.func();

箭头函数调用模式

  var name = 'mhq';
  var student = {
    name: 'hiraku',
    func: function () {
      var arrowFunc = () => {
        console.log(this.name);
      }
      arrowFunc();
    },
    arrowFunc2: () => {
      console.log(this.name);
    }
  }
  student.func(); // 'hiraku'
  student.arrowFunc2(); // 'mhq'
  • 语法更加简洁、清晰
  • 箭头函数不会创建自己的 this

    • 箭头函数的 this 指向定义时所在的外层第一个普通函数,跟使用位置没有关系
    • 被继承的普通函数的 this 指向改变,箭头函数的 this 指向会跟着改变
  • 箭头函数外层没有普通函数,严格模式和非严格模式下它的 this 都会指向 window (全局对象)
  • call()、apply()、bind() 无法改变箭头函数中 this 的指向
  • 箭头函数不能作为构造函数使用
  • 箭头函数没有自己的 arguments

    • 箭头函数的 this 指向全局,使用 arguments 会报未声明的错误
    • 箭头函数的 this 指向普通函数时,它的 arguments 继承于该普通函数
    • rest参数(...扩展符)取参数
  • 箭头函数没有原型 prototype
  • 箭头函数不能用作 Generator 函数,不能使用 yeild 关键字
  var obj = {
    name: 'mhq',
    func: function () {
      console.log(this.name);
      return () => {
        console.log('arrowFn:', this.name);
      }
    }
  }
  var obj1 = {
    name: 'hiraku',
  }
  obj.func().call(obj1); // 'mhq'  'arrowFn:' 'mhq'
  obj.func.call(obj1)(); // 'hiraku' 'arrowFn:' 'hiraku'

DOM 事件处理函数调用

  • onclick 和 addEventerListener 是指向绑定事件的元素。
  • 一些浏览器,比如 IE6~IE8 下使用 attachEvent,this 指向是 window。

高阶函数(Higher-order function)

什么是高阶函数?高阶函数至少满足两个条件:接受一个或多个函数作为输入,输出一个函数。也可以这样理解,一个函数的参数是一个函数(回调函数);一个函数返回一个函数(函数柯里化)。

我们写代码时不希望破坏原有逻辑,所以将原有逻辑包上一个函数,这个函数内部实现自己的逻辑,即切片编程。

  const originFunc = (...args) => {
    console.log('原函数', args);
  };

  // 希望在调用 originFunc 之前做一些事情,使用 Function.prototype 给每个函数扩展一些功能
  Function.prototype.before = function(cb) {
    return (...args) => {
      cb();
      this(...args);
    };
  };

  let newFunc = originFunc.before(() => {
    console.log('before originFunc');
  });

  newFunc('a', 'b', 'c');

一个异步并发问题

我同时发送多个请求,希望拿到最终的结果

  function after(time, fn) {
    return () => {
      if (--time === 0) {
        fn();
      }
    };
  }

  const exaFun = after(3, () => {
    console.log('执行');
  });

  exaFun();
  exaFun();
  exaFun();

示例:

  const fs = require('fs');
  const path = require('path');

  function run(times, fn) {
    const renderObj = {};
    return (key, value) => {
      renderObj[key] = value;
      console.log(key, value);
      if(--times === 0) fn(renderObj);
    };
  }

  let out = run(2, (result) => {
    console.log(result);
  });

  fs.readFile(path.resolve(__dirname, '1.txt'), 'utf8', (_, data) => {
    console.log(data, 'ddd')
    out('1', data);
  });
  fs.readFile(path.resolve(__dirname, '2.txt'), 'utf8', (_, data) => {
    out('2', data);
  });

函数柯里化

柯里化,即 Currying,为了给多参数函数提供了一个递归降解的方式,把提供多个参数的函数变换成接受一个单一参数的函数,并且返回余下参数而且返回结果的新函数。

  const curring = (fn, arr = []) => {
    // 长度值的是参数个数
    let len = fn.length;
    return (...arg) => {
      arr = [...arr, ...arg];
      if (arr.length < len) return curring(fn, arr);
      return fn(...arr);
    };
  };

  const add = (a, b, c, d, e, f, g, h) => {
    return a + b + c + d + e + f + g + h;
  }

  console.log(curring1(add, [1, 2])(3, 4)(5)(6)(7, 8)) // 15

函数柯里化使用场景:

  • 参数复用
  • 延迟执行
  • 预加载
  • 动态创建函数

发布订阅、观察者模式

观察者模式

  • 定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新
  • 属于行为型模式,行为型模式关注的是对象之间的通讯
  • 观察者和被观察者之间的通讯
  const fs = require('fs');
  const e = {
    arr: [],
    on(fn) {
      this.arr.push(fn);
    },
    emit() {
      this.arr.forEach(fn => fn());
    }
  };

  e.on(() => {
    console.log('读取到了数据');
  })
  const renderObj = {};
  e.on(() => {
    if (Object.keys(renderObj).length === 2) {
      console.log('都读取完毕了');
    }
  });

  fs.readFile('./name.txt', 'utf8', (_, data) => {
    renderObj['name'] = data;
    e.emit();
  });
  fs.readFile('./age.txt', 'utf8', (_, data) => {
    renderObj['age'] = data;
    e.emit();
  });

发布订阅

  • 就是事先存储好,稍后发布的时候让事先订阅的事执行
  • 发布者并不会直接通知订阅者,换句话说,发布者和订阅者,彼此互不相识
  • 在发布者和订阅者之间存在第三个组件,称为调度中心或事件通道,它维持着发布者和订阅者之间的联系,过滤所有发布者传入的消息并相应地分发它们给订阅者
  class Subject { // 被观察者
    constructor(name) {
      this.name = name;
      this.arr = [];
      this.state = '我很开心';
    }
    attach(observer) { // 注册观察者 基于发布订阅
      this.arr.push(observer);
    }
    setState(newState) {
      this.state = newState;
      this.arr.forEach(o => o.update(this)); // 通知所有观察者 我的状态发生了变化
    }
  }

  class Observer { // 观察者
    constructor(name) {
      this.name = name;
    }
    update(s) {
      console.log(s.name + '当前状态是' + s.state + '对:' + this.name);
    }
  }

  let s = new Subject('小宝宝');
  let o1 = new Observer('我');
  let o2 = new Observer('我媳妇');

  s.attach(o1);
  s.attach(o2);
  console.log(s.state);
  s.setState('不开心了');

本篇文章涉及到的高阶函数使用有函数柯里化、发布订阅|观察者模式等。下篇将分享 Promise 源码,手写 Promise。

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Dax Dax
4年前
JavaScript中call()、apply()、bind()的用法
call()apply()bind()都是用来更改this的指向的其中bind()返回的是一个函数,必须执行才行传参差异:call、bind、apply这三个函数的第一个参数都是this的指向对象,第二个参数差别就来了:call的参数是直接放进去的,第二第三第n个参数全都用逗号分隔,直接放到后面obj.myFun.call(db,'
巴拉米 巴拉米
4年前
bind、call、apply 区别?如何实现一个bind?
一、作用call、apply、bind作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向那么什么情况下需要改变this的指向呢?下面举个例子var name"lucy";const obj{    name:"martin",    say:function (){        co
Wesley13 Wesley13
4年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
4年前
mysql中时间比较的实现
MySql中时间比较的实现unix\_timestamp()unix\_timestamp函数可以接受一个参数,也可以不使用参数。它的返回值是一个无符号的整数。不使用参数,它返回自1970年1月1日0时0分0秒到现在所经过的秒数,如果使用参数,参数的类型为时间类型或者时间类型的字符串表示,则是从1970010100:00:0
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
4年前
HIVE 时间操作函数
日期函数UNIX时间戳转日期函数: from\_unixtime语法:   from\_unixtime(bigint unixtime\, string format\)返回值: string说明: 转化UNIX时间戳(从19700101 00:00:00 UTC到指定时间的秒数)到当前时区的时间格式举例:hive   selec
Wesley13 Wesley13
4年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n