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。


mahongqin
16 声望0 粉丝