头图

Decorator装饰器

针对属性 / 方法的装饰器

// decorator 外部可以包装一个函数,函数可以带参数

    function Decorator (type) {
      /**
       * 这里是真正的decorator
       * @description: 装饰的对象的描述对象
       * @target:装饰的属性所述类的原型,不是实例后的类。如果装饰的是Animal的某个属性,这个target就是Animal.prototype
       * @name 装饰的属性的key
       */

      return function (target, name, desciptor) {
        // 因为babel的缘故 通过value并不能获取值,以此可以获取实例化的时候此属性的默认值
        let v = desciptor.initializer && desciptor.initializer.call(this)
        // 返回一个新的描述对象,或者直接修改desciptor也可以
        return {
          enumerable: true, //可以遍历
          configurable: true, //可以删除
          get: function () {
            return v
          },
          set: function (c) {
            v = c
          }
        }
      }
    }
    
    
    
    
    // 上面的不能和业界商用的Decorator混用

    function Check (type) {
      return function (target, name, desciptor) {
        let v = desciptor.initializer && desciptor.initializer.call(this)
        // 将属性名字以及需要的类型的对应关系记录到类的原型上
        if (!target.constructor._checkers_) {
          // 将这个隐藏属性定义成no enumerable,遍历的时候是取不到的
          Object.defineProperty(target.constructor, '_checkers_', {
            value: {},
            enumerable: false,
            writable: true,
            configurable: true
          })
        }
        target.constructor._checkers_[name] = {
          type: type
        }
        return desciptor
      }
    }
    // 装饰函数的第一个参数 target 是包装属性所属的类的原型(prototype)
    // 也就是把对应关系挂载到了开发定义的子类上。

vue中使用Decorator

  • ts开发一定对vue-property-decorator不会感到陌生,这个插件提供了许多装饰器
  • 在methods里面的方法上面使用装饰器,这时候装饰器的target对应的是methods。
  • 可以在生命周期钩子函数上面使用装饰器,这时候target对应的是整个组件对象。
  import {log,confirmation} from "./test"
  
  methods: {
    @log()
    name() {
      console.log("获取数据");
    },
    @confirmation('此操作将永久删除文件,是否继续?')
    deleteFile(data){
      //删除文件操作
    }
  },
  mounted () {
    this.name()
  }

test.js

 import {MessageBox}from "element-ui"
export function confirmation(message){
    return function(target,name,descriptor){
        let oldValue = descriptor.value
        descriptor.value = function(...args){
            MessageBox.confirm(message,'提示').then(oldValue.bind(this,...args)).catch(()=>{})
        }
        return descriptor
    }
}
export function log(){
    /**
     * @description: 
     * @param {*} target  对应methods
     * @param {*} name 对应属性方法的名称
     * @param {*} descriptor 对应属性方法的修饰符
     * @return {*}
     */      
    return function(target,name,descriptor){
      console.log(target,name,descriptor);
      // 获取实例化的时候此属性的默认值
      const fn = descriptor.value
      /* 重写 */
      descriptor.value = function(...rest){
        console.log(`调用${name}方法打印的`);
        fn.call(this,...rest)
      }
    }
  }

Iterator 迭代器

  • 目的是为不同的数据结构提供统一的数据访问机制 主要为for of 服务的 (当for of执行的时候,循环过程中会自动调用这个对象上的迭代器方法,依次执行迭代器对象的next方法,并把next返回结果赋值给for of的变量,从而得到具体的值
  • 迭代器对象,返回此对象的方法叫做迭代器方法 此对象有一个next方法 每次调用next方法都会返回一个结果值
  • 这个结果值是一个object 包含两个属性value和done
  • value表示具体的返回值 done是布尔类型的,表示集合是否遍历完成或者后续还有可用数据,没有可用返回true, 否则返回false
  • 内部会维护一个指针 用来指向当前集合的位置 每调用一次next方法 指针都会向后移动一个位置(可以想象成数组的索引)

    代码实现

  getInterator(list){
    var i = 0;
    return {
      next:function(){
        var done = (i>=list.length);
        var value = !done ? list[i++]:undefined
        return {
          done:done,
          value:value
        }
      }
    }
  }
  
  
  var it = this.getInterator(['a','b','c'])
    console.log(it.next());// {done: false, value: 'a'}
    console.log(it.next());//{done: false, value: 'b'}
    console.log(it.next());//{done: false, value: 'c'}
    console.log(it.next());//{done: true, value: undefined}
可迭代对象
  • Symbol.Iterator是一个表达式 返回Symbol的Iterator属性, 这是一个预定好的类型为Symbol的特殊值
  • ES6规定,只要在对象上部署了Iterator接口,具体实现为给对象添加Symbol.Iterator属性,此属性指向一个迭代器方法,这个迭代器会返回一个迭代器对象。
  • 而部署了这个属性,并且实现迭代器方法 返回的对象就是迭代器对象,此时这个对象就是可迭代的 可以被for for遍历

    实现一个可迭代对象

     getIterator(){
        let iteratorObj = {
          items:[100,200,300],
          [Symbol.iterator]: function(){
            var self = this
            var i =0
            return {
              next:function(){
                var done = (i>=self.items.length)
                var value = !done?self.items[i++]:undefined
                return {
                  done:done,
                  value:value
                }
              }
            }
          }
        }
        for(var item of iteratorObj){
          console.log(item); //100 200 300
        }
      }
      
      //上面的对象就是可迭代对象,可以被for of遍历
      
      this.getIterator()
    for of 中断

    如果for of 循环提前退出,则会自动调用return方法,需要注意的是return 方法必须有返回值,且返回值必须是一个object

     var arr = [100, 200, 300]
        arr[Symbol.iterator] = function () {
          var self = this
          var i = 0
          return {
            next: function () {
              var done = i >= self.length
              var value = !done ? self[i++] : undefined
              return {
                done: done,
                value: value
              }
            },
            return (){
              console.log('提前退出');
              return { //必须返回一个对象
                done:true
              }
            }
          }
        }
    
        for (var o of arr) {
          if(o == 200){
            break;
          }
          console.log(o) // 100   提前退出
        }

    除了for of 会调用对象的Iterator, 结构赋值也会

        var str = '123'
        let [a,b]=str
        console.log(a,b); // 1 2
    
        let map = new Map()
          map.set('q','1')
          map.set('a','2')
          map.set('b','3')
        let [c,d] = map
        console.log(c,d); //['q', '1'] ['a', '2']

    因为普通对象不是可迭代对象。

    自定义的可迭代对象进行解构赋值

     var interatorObj = {
          items: ['橙', '红', '白'],
          [Symbol.iterator]: function () {
            let i = 0
            let self = this
            return {
              next: function () {
                let done = i >= self.items.length
                let value = !done ? self.items[i++] : undefined
                return {
                  done: done,
                  value: value
                }
              }
            }
          }
        }
       let [a,b] = interatorObj
       console.log(a,b); //橙 红

    解构赋值的变量的值就是迭代器对象next方法的返回值 且是按顺序返回

    扩展运算符

    // 扩展运算符的执行(...)也会默认调用它的Symbol.iterator方法,可以将当前迭代对象转换为数组

    */\* 字符串 \*/*
    
      var str = "1234"
    
      console.log([...str]);*//【1,2,3,4】转换成数组*
    
    
    
      */\* map对象\*/*
    
      var map = new Map([[1,2],[3,4]])
    
      [...map]*//[[1,2],[3,4]]*
    
      */\* set对象 \*/*
    
      var set = new Set([1,2,3])
    
      [...set] *//[1,2,3]*

使用普通对象是不可以转换为数组的

var obj = {name:'zhang'}

[...obj] *//报错*
作为数据源

作为一些数据的数据源 比如某些参数是数组的API方法,都会默认的调用自身的迭代器

例如 map set Array.from等

为了证明,先把一个数组的默认迭代器给覆盖掉

var arr= [100,200,300]
      arr[Symbol.iterator]=function(){
        var self = this
        var i = 0;
        return {
          next:function(){
            var done = (i>=self.length)
            var value = !done?self[i++]:undefined
            return {
              done:done,
              value:value + '前端'
            }
          }
        }
      }
     

用for of

 for(var o of arr){
        console.log(o);//100前端 200前端 300前端
      }

生成set对象

  var set = new Set(arr)

 set*//Set(3) {'100前端', '200前端', '300前端'}*

调用Array.from方法

  Array.from(arr) *//['100前端', '200前端', '300前端']*
yield*关键字

yield*后面跟的是一个可遍历的结构,执行时也会调用迭代器函数

 let foo = function*(){

​        *yield* 1;

​        *yield** [1,2,3];

​        *yield* 5;

​      }
判断对象是否可迭代

既然可迭代对象的规则必须在对象上部署Symbol.iterator属性,那么就可以依次来判断是否为可迭代对象,然后就知道是否能使用for of 取值了

  function isIterable(*object*){

​        *return* typeof *object*[Symbol.iterator] === 'function'

​      }

​      console.log(isIterable('asdd'));*//true*

​      console.log(isIterable([1,2,3]));*//true*

​      console.log(isIterable(new Map()));*//true*

​      console.log(isIterable(new Set()));*//true*

​      console.log(isIterable(new WeakMap()));*//false*

​      console.log(isIterable(new WeakSet()));*//false
Generate生成器*
 let obj = {

​      *[Symbol.iterator]("Symbol.iterator"){

​        *yield* 'hello';

​        *yield* 'world';

​      }

​    }

​    *for*(let x of obj){

​      console.log(x);

​    } *//hello world

Generator

Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同

Generator函数将javascript异步编程带入了一个全新的阶段

声明

与函数声明类似,不同的是function关键字与函数名之间有一个星号,以及函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是产出)

 function* foo(){
        yield 'result1'
        yield 'result2'
        yield 'result3'
      }
      const gen = foo()
      console.log(gen.next().value);
      console.log(gen.next().value);
      console.log(gen.next().value);

vue中使用

 textMy () {
      const gen = this.foo()
      console.log(gen.next().value);
      console.log(gen.next().value);
      console.log(gen.next().value);
    },
    * foo(){
        yield 'result1'
        yield 'result2'
        yield 'result3'
      }

​ 执行Generator会返回一个对象,而不是像普通函数返回return 后面的值

​ 调用指针next方法,会从函数的头部或上一次停下来的位置开始执行,直到遇到下一个yield表达式或者return语句暂停,也就是执行yeild这一行

​ value就是执行yield后面的值 done表示函数是否执行完毕

console.log(g.next()); *//{value: 7, done: false}*

​      console.log(g1.next()); *//{value: 2, done: false}*

​      console.log(g.next());*//{value: undefined, done: true}

​ 因为最后一行 return y 被执行完成 所以done为true

​ 调用Generator函数后,该函数并不执行,返回的也不是函数运行结果 而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object).必须调用遍历器对象的next方法

​ 使得指针移向下一个状态

      function* fetch(){

​        *yield* ajax('aaa')

​        *yield* ajax('bbb')

​        *yield* ajax('ccc')

​      }

​      let gen = fetch()

​      let res1 = gen.next() *//{value:'aaa',done:false}*

​      let res2 = gen.next() *//{value:'bbb',done:false}*

​      let res3 = gen.next() *//{value:'ccc',done:false}*

​      let res4 = gen.next() *//{value:undefined,done:true} //done为true表示执行结果

由于Generator函数返回的是遍历器对象 只有调用next方法才会遍历下一个内部状态 所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志

​ 遍历器对象的next方法运行逻辑:

​ 1 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的表达式的值,作为返回对象的value属性值

​ 2. 如果下次调用next方法时,再继续往下执行,直到遇到下一个yield表达式

​ 3 如果没有再遇到新的yiled表达式 就一直运行到函数结束

​ 4 如果该函数没有return 语句 则返回对象的value属性值就是underfined

​ yiled表达式本身没有返回值 或者说总返回underfined next 方法可以带一个参数,该参数就会被当做上一个yield表达式的返回值

​ 一个函数可以有多个yield,但是只能有一个return

Proxy

​ - Proxy用于创建对象的代理,用于监听对象的相关操作。代理对象可以监听我们对原对象的操作

​ - 在实例化Proxy对象时,第二个参数传入的是捕获器集合,我们在其对象内定义一个get捕获器,用于监听获取对象值的操作

​ - objProxy 对象的拦截器中set捕获器,用于监听对象的某个属性被设置时触发

// 定义一个普通的对象
        const obj = {
          name:'_isLand'
        }
        // 代理obj这个对象  并传入get捕获器
        const objProxy = new Proxy(obj,{
          // get捕获器
          get:function(target,key){
            console.log(`触发对象捕获${key}`);
            return target[key]
          },
          set:function(target,key,val){
            console.log(`捕获到对象设置${key},新值为${val}`);
            return target[key] = val
            
          }
        })
        objProxy.name = 'liming'
        // 通过代理对象操作obj对象
        console.log(objProxy.name);//触发对象捕获name  _isLand
        // 捕获到对象获取name属性值的操作

如果不想这个属性被设定成这个值,你可以抛出异常告诉开发者,这个值不能设定

 set:function(target,key,val){
          if(key==='age' && typeof val ==='number'){
            return target[key] = val
          }else{
            throw new TypeError('该属性的值必须是number类型')
          }
        }

监听对象是否调用了getPrototypeof操作,使用getPrototypeOf捕获器

 getPrototypeOf:function(){
          console.log('监听到对象的protptypeOf操作');
        }

Proxy一共有十三个捕获器,用于我们对 对象或者函数的方法调用的监听

请添加图片描述

this指向的问题

​ Proxy对象可以对我们的目标对象进行访问,但没有做任何拦截时,也不能保证与目标行为一致,因为目标对象 内部的this会自动改变为Proxy代理对象

      let obj = {
        name:'lili',
        foo:function(){
          return this === objProxy
        }
      }
      let objProxy = new Proxy(obj,{})
      console.log(obj.foo()); //false
      console.log(objProxy.foo());  //true
对象监听

​ 在vue2中的watchApi是使用的Es5的Object.defineProperty(对象属性描述符)对对象监听,将一个对象进行遍历,

​ 并设定setter。getter方法进行监听和拦截

const obj = {
        name: 'liki',
        age: 12
      }
      Object.keys(obj).forEach(key => {
        let val = obj[key]
        Object.defineProperty(obj, key, {
          get: function () {
            console.log('触发get')
            return val
          },
          set: function (newVal) {
            console.log('触发set')
            val = newVal
          }
        })
      })
      obj.age = 100
      console.log(obj.name);
  • Object.defineProperty的设计初衷并不是为了去拦截拦截一个对象中的属性,且他也实现不了更加丰富的操作,例如删除、添加属性等操作

​ - 所以在Es6中新增了Proxy,对象,用于监听Object,Function的操作。

​ - 在Vue3框架中的响应式原理也是用到了Proxy对象进行对属性的监听操作

proxy是一个代理对象 他可以代理我们对原目标的操作。相比Object.defineProperty方法 Proxy监听的事件更加方便

Reflect隐射对象

Reflect是一个对象,翻译过来就是反射的意思,它提供了很多操作Javascript对象的方法,是为弥补Object中对象的一些缺陷。且所有的属性和方法都是静态的

最初,js的一些内部方法都是放在Object这个对象上的,比如getPrototye,defineProperty等API,

in、delete操作符都放在了Object对象上。但是Object作为一个构造函数,这些方法都放在Object上并不合适,

所以ES6之后的内部新方法都在Reflect对象中。Refelect不是构造函数

使用Reflect对象操作Object对象

Reflect对象让我们操作Object对象不再是通过点语法 而是变成了函数行为

所以 获取对象属性可以使用Reflect.get方法,将对象的属性赋值使用Reflect.set方法

    const obj = {
         name:'lili',
         age:12
       }

       /* 获取对象的属性值 */
      console.log(obj.name);
      console.log(Reflect.get(obj,'name'));
     
    //  对对象的属性赋值
      obj.name = 'lala'
      Reflect.set(obj,'name','lala')
      console.log(Reflect.get(obj,'name'));
      // 判断一个对象中是否有该属性
      console.log('name' in obj);
      console.log(Reflect.has(obj,'name'));

Reflect中的方法

请添加图片描述

在返回值方面Reflect对象中的方法设计的更加合理 比如defineProperty方法,如果没有将属性设置

成功,在Reflect中会返回boolean值,而Object对象中如果没有定义成功则会抛出TypeError

Reflect搭配Proxy

​ Reflect对象中的方法和Proxy对象中的方法对应的,Proxy对象中的方法也能在Reflect对象中调用

​ 通常我们将Reflect对象搭配Proxy一起使用

在下面Proxy对象中get,set捕获器多了一个recerive参数 这是这两个捕获器特有的 代表的是当前的代理对象

​ 当Proxy和Reflect搭配使用时 Proxy对象会拦截对应的操作 后者完成对应的操作,如果传入receiver,那么Reflect.get属性

​ 会触发Proxy.definProperty捕获器。如果没有传入receive参数 则不会触发defineProperty捕获器

     let obj = {
        name:'lili'
      }
      const objProxy = new Proxy(obj,{
        get:function(target,key,receiver){
          return Reflect.get(target,key,receiver)
        },
        set:function(target,key,val,receiver){
          return Reflect.set(target,key,val,receiver)
        },
        defineProperty:function(target,key,attr){
          console.log('defineProperty',target,key,attr); //defineProperty {name: 'lili'} name {value: 'ppp'}
          return Reflect.defineProperty(target,key,attr)
        }
      })
      objProxy.name = 'ppp'
      console.log(objProxy.name);
      //传入在我们获取代理对象中的name属性时,当Reflect有receive

总结

​ - Reflect 对象中集合了javascript内部方法

​ - 操作Object对象的方式变成了函数行为

​ - Reflect 对象中的方法返回结果更加合理

ES6中的class

​ 在ES6之前,js语法是不支持类的,导致面向对象编程无法直接使用,而是通过function来实现模拟类,随着js的更新,ES6中出现了Class,

​ 用于定义类

 class Animal {}

const Animal = class {}
类的构造函数

每一个类都可以有一个自己的构造函数,这个名称是固定的construtor,当我们通过new 调用一个类时,这个类就会调用自己的constructor方法(构造函数)

​ - 它用于创建对象时给类传递一些参数

​ - 每一个类只能有一个构造函数

​ - 通过 new 调用一个类时 会调用构造函数 执行如下操作

​ 1 在内存中开辟一块新的空间用于创建新的对象

​ 2 这个对象内部的_proto_属性会被赋值为该类的prototype属性

​ 3 构造函数内部的this,指向创建出来的新对象

​ 4 执行构造函数内部的代码

​ 5 如果函数没有返回值 则返回this

 class Animal {
          constructor(name){
            this.name = name
          }
        }
        var a = new Animal("ABC")
        console.log(a); //Animal {name: 'ABC'}
       

class 中定义的constructor,这个就是构造方法

而this代表的是实例对象

这个class 可以看作是构造函数的另一种写法 因为它和它的构造函数的相等的,即是 类本身指向构造函数

console.log(Animal === Animal.prototype.constructor); *//true*

所以其实类上的所有方法都会放到prototype属性上

类中的属性

实例属性

​ 实例的属性必须定义在类的方法中

class Animal {
      constructor(name,height,weight){
        this.name = name
        this.height = height
        this.weight = weight
      }
    }

静态属性

​ 当我们把一个属性赋值给类本身,而不是赋值给它的prototype,这样子的属性被称之为静态属性(static)

​ 静态属性直接通过类来访问 无需在实例中访问

class Foo{
      static name = "liLI"
    }
      console.log(Foo.name);

私有属性

​ 私有属性只能在类中读取,写入,不能通过外部引用私有字段

   class Person{
        #age;
        constructor(age,name){
          this.#age = age
          this.name = name
        }
      }
      var a = new Person(17,'lili')
      console.log(a); //Person {name: 'lili'}
      console.log(a.name);//lili
      console.log(a.age);//undefined
      console.log(a.#age);// Private field '#age' must be declared in an enclosing class
    

console.log(Object.getOwnPropertyDescriptors(a));

通过getOwnPropertyDescriptors获取属性同样获取不到

 {
      name: {
        value: '_island',
        writable: true,
        enumerable: true,
        configurable: true
      }
      }

在ES6之前,我们定义类中的方法是类中的原型上定义的 防止类中的方法重复在多个对象中

 function Animal{}

​      Animal.prototype.eating = function(){

​        console.log(this.name + 'eating');

​      }

在Es6中定义类中的方法更简洁 直接在类中定义即可 这样的写法优雅可读性也强

  class Animal{

      eating(){

​         console.log(this.name + 'eating');

​       }

​     }
静态方法

静态方法是与静态属性是一样的 在方法前面加上stati关键字声明,之后调用这个方法时不需要通过类的实例来调用,可以直接通过类名来调用它

 class Animal{

​        static creatName(*name*){

​          *return* *name*

​        }

​      }

​      var a2 = Animal.creatName('lll')

​      console.log(a2); *//lll
私有方法

在面向对象中 私有方法是一个常见需求 ES6中没有提供,可以通过某个方法实现它

   class Foo{

​        _getBoodType(){

​          *return* '0'

​        }

​      }

需要注意的是,通过下划线开头通常我们会局限它是一个私有方法 但是在类的外部还是可以正常调用到这个方法的

类的继承

extends关键字用于扩展子类,创建一个类作为另一个类的子类

​ 它会将父类中的属性和方法一起继承到子类的,减少子类中重复的业务代码

​ 这比之前在ES5中修改原型链实现继承的方法可读性要强的多,而且写法更简洁

       extends的使用

​      class Animal{}

​      dog继承Animal类

​      class dog extends Animal{}

继承类的属性和方法

  • 定义一个dog类 通过extends关键字继承Animal类的属性和方法
  • 在子类的construtor方法中 使用super关键字,在子类中它上市必须存在的 否则新建实例会抛出异常。
  • 这是因为子类的this对象是继承父类的this对象 如果不调用super方法 子类就得不到this对象

class Animal {

​        constructor (*name*) {

​          this.name = *name*

​        }

​        eating () {

​          console.log(this.name + 'eating')

​        }

​      }

​      class dog extends Animal{

​        constructor(*name*,*legs*){

​          super(*name*)

​          this.legs = *legs*

​        }

​        speaking(){

​          console.log(this.name + "speaking");

​        }

​      }

​      var wang = new dog('tom',4)

​      wang.speaking()

​      wang.eating()

​      console.log(wang.name);
Super

super关键字用于访问调用一个对象的父对象上的函数

​ super指的是超级,顶级,父类的意思

​ 在子类的构造函数中使用this或者返回默认对象之前,必须先使用super调用父类的构造函数

​ 子类的construtor方法先调用了super方法,它代表了父类的构造函数,也就是我们把参数传递进去之后,其实它调用了父类的构造函数

 class Animal{

​        constructor(*name*)

​      }

​      class dog{

​        constructor(*name*,*type*,*weight*){}

​        super(*name*)

​        this.type = type

​        this.weight = weight

​      }

使用super调用父类的方法

                 class Animal{

​        constructor(*name*){

​          this.name = name

​        }

​        eating(){

​          console.log(this.name + 'eating');

​        }

​      }



​      *// dog继承Animal类*

​      class dog extends Animal{

​        constructor(*name*,*lengs*){

​          super(*name*)

​          this.lengs = *lengs*

​        }

​        speaking(){

        super.eating()

​          console.log(this.name + 'speaking');

​        }

​      }

​        var a = new dog('旺财',4)

​        a.speaking() *//旺财eating  旺财speaking
Getter和Setter

在类内部可以使用get和set关键字,对应某个属性设置存值和取值函数,拦截属性的存取行为

 class Animal{

​        constructor(){

​          this._age = 3

​        }

​        get age(){

​          console.log('get');

​          *return* this._age

​        }

​        set age(*val*){

​          console.log('set');

​          this._age = val

​        }

​      }

​      var a = new Animal()

​      console.log(a.age);

​      a.age = 4

​      console.log(a.age);
关于class扩展

严格模式

在类和模块的内部 默认是严格模式 所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用

name属性

Es6中的类只是Es5构造函数的一层包装,所以函数的许多属性都被class继承了,包括name属性

 class Animal{}

console.log(Animal.name);

变量提升

class不存在变量提升,这与Es5中实现类是不同的, function关键字会存在变量提升
   new Foo() *//// ReferenceError*

   class Foo{}

在Es6之后,在定义类以及它内部的属性方法,还有继承操作的语法变得很简洁易懂

class 是一个语法糖 其内部还是通过Es5中的语法实现的。且有些浏览器不支持class语法 我们可以通过babel来进行转换

函数式组合函数

组合函数就是将多个函数按照顺序进行执行,前一个函数返回的数据,作为下一个函数的参数,最后返回最终的结果

例子: 假如我们需要将一个数字先进行乘法运算,后进行求平方,可以将这两部分拆分成两个基础函数double、square.再通过double函数返回值传入到square中调用

function double (num) {
  return num * 2
}
function square (num) {
  return num ** 2
}
const count = 10
 const res = square(double(count))
 console.log(res);

柯里化和纯函数很容易写出可读性不强的代码square(double(count))
上面的调用方式代码可读性不是很好,我们可以组合上面两个基础函数

function composeFn (m, n) {
      return function (count) {
        return n(m(count))
      }
    }

使用組合函數 將兩個函數傳入,composeFn會返回一個新的函數,我們只需要將要處理的數據傳入到返回的這個新函數,這個新函數將會幫我們處理並返回處理好的數據

  const newFn = composeFn(double, square)
    const res2 = newFn(count)
    console.log(res2, 'res2')

通過compose組合而成的函數只能是一個參數,往往我們的基礎參數可能不止一個,這個時候我們將會用到柯里化函數進行處理

实现组合函数

      function MyCompose (...fns) {
        var length = fns.length
        /* 判断传入参数是否为函数 */
        for (var i = 0; i < length; i++) {
          if (typeof fns[i] !== 'function') {
            throw new TypeError('Expected arguments are function')
          }
        }
        // 返回一个接收参数的函数
        function compose (...args) {
          // 当前执行函数索引
          var index = 0
          // 调用当前索引对应的函数
          var result = length ? fns[index].apply(this, args) : args
          while (++index < length) {
            // 将上一次返回的结果,传入到下一个函数中
            var result = fns[index].call(this, result)
          }
          return result
        }
        return compose
      }
      const newFns = MyCompose(double, square)
      const res3 = newFns(count)
      console.log(res3, 'res3')

1 在一开始 函数接收一系列的函数
2 先判断传入的参数是否全为函数,如果有一个非函数的函数则抛出异常
3 创建一个新函数 它需要接收要处理的参数
4 函数内,声明一个变量,记录当前执行函数的索引 将传入的参数按函数顺序执行
5 最终返回这个函数

结合律

函数的组合要满足结合律,例如a、b、c三个函数进行组合,可以将a和b进行组合,或者b和c进行组合。这个特性就是计算结合律

 let fn = compose(a,b,c)
 let ass1 = compose(compose(a,b),c)
 let ass2 = compose(compose,compose(b,c))
pointfree

pointfree是一种编程风格,这种风格也被称为Tacit programming,point代表形参,意思是没有形参的编程风格

    pointfree风格  带有word形参
  var word = (word)=>word.toUppercase()
 //  pointfree风格  没有任何形参
 var word = compose(toUppercase)

也就是说 我们完全可以把数据处理的过程,定义成一种与参数无关的合成运算。不需要用到代表数据的那个参数,只要把一些简单的运算步骤合在一起即可
它省去了对参数命名的麻烦 代码更加简洁 pointfree 是基础函数组合的高级函数 这些高级函数往往应用在我们的业务中

Debug

对中间的值进行打印 并且知道当前执行位置

 var log = MyCurring((t,v)=>{
   console.log(t,v);
   return v
 })

在组合函数找那个 按照从左到右的顺序 给函数套个log函数,就可以知道每次输出的值

var newFn = MyCompose(double,log('double--'),square,log('square--'))
     var res2 = newFn(10)
     //double-- 20
     // square -- 400

函数式之柯里化

柯里化(currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术它能减少代码冗余 增加代码的可读性
例子

  function sum(a,b,c,d){
    return a + b + c + d
  }
  sum(10,20,30,60)

上面的sum函数是将传入的参数进行相加,如果把sum函数改成柯里化函数

function sum (a) {
       return function (b) {
         return function (c) {
           return function (d) {
             return a + b + c + d
           }
         }
       }
     }
     // 按照上面的写法,这个sum调用方式将是
     sum(10)(20)(30)(40) //100
     const add = sum(10)
     add(20)(30)(40)

通过这个例子你可以知道 柯里化即是把较多参数的函数转为可以分段传入函数参数的函数,可以减少对函数备份参数的传入

柯里化应用场景
在实际场景上的一个例子 MyURL函数用于生成一个拼接之后的url链接,它需要传入三个参数,分别是protocol,domain,path

  function MyURL (protocol, domain, path) {
    return protocol + '://' + domain + '/' + path
  }
  MyURL('https','fanyi.qq.com','user/2858385965322935');
  MyURL('https','fanyi.qq.com','post/122');
  MyURL('https','fanyi.qq.com','user/33');
柯里化函数实现

实现将函数转换为柯里化函数的函数
柯里化函数的实现

/* 思路步骤:
     1 创建一个名为myCurrying的函数,接受参数为需要变为函数柯里化的函数,这里用fn表示
     2 函数内部返回一个行为curried的函数 接受参数的个数为fn函数的参数(fn.length)个数 ,这里使用剩余参数 ...
     3 curried内部需要去判断当前已经接受的参数个数 是否与函数本身需要接收的参数个数一致
     4 如果当前传入的参数大于等于需要接收的个数时  执行函数fn.apply(this,args)
     5 如果不满足上述条件,也就是传入的参数没有达到个数要求,需要返回一个新函数,这里用curried2表示,接受的参数为第一次接受的参数后剩下的参数,...args2表示
     6.接受到参数后curried2函数执行 递归调用curried函数  继续判断传入的参数个数是够一致  一致则执行 步骤4,反之,继续递归调用curried函数
     7 调用myCurring函数  传入我们需要柯里化的函数作为参数
     */
     function myCurrying (fn) {
       return function curried (...args) {
         // 判断当前已经接受的参数个数 是否与参数本身需要接收的参数个数一致
         // 当前传入的参数,大于等于需要接收的参数个数时,执行函数
         if (args.length >= fn.length) {
           return fn.apply(this, args)
         } else {
           // 当前传入的参数,没有达到个数时 需要返回一个新的函数,继续接受剩余的参数
           return function curried2 (...arg2) {
             // 接受到参数后,需要递归调用curried来再一次检查函数的个数是否达标
             return curried.apply(this, [...args, ...arg2])
           }
         }
       }
     }
     function sum (a, b, c) {
       return a + b + c
     }
     /* 使用myCurryging将sum函数进行柯里化转化 */
     var result = myCurrying(sum)
     console.log(result(1)(2)(3));

简单来说就是
1 在一开始 函数接收一个函数 将这个函数进行柯里化处理
2 先进行判断当前函数传入的参数数量是否大于原函数参数的数量,如果大于,通过apply方式调用函数
3 如果没有达到原函数参数的数量: 将返回一个函数继续接受剩余的参数
4 调用返回的函数,当参数达到原函数参数的数量时,通过apply方式调用函数

思路
利用闭包的原理,将每次传递进来的参数存起来, 当参数不符合预期时,返回一个新的函数继续接受剩余参数,继续调用,不符合则再递归

知识点
1 Funtion.prototype.length 是获取函数参数的个数
2 如果不使用apply方式调用原函数, 会发生this指向不正确

柯里化的性能
在使用柯里化意味着有额外的内存开销
使用arguments对象比直接操作命名参数慢
作用域,闭包对内存的开销,性能下降
使用call、apply调用函数比直接调用函数会慢些,而且产生嵌套关系
总结:
柯里化只要是以闭包为基本 利用闭包将函数的参数存起来,等到参数达到一定数量时执行函数,使用函数柯里化会让代码更加灵活性
但也有一定的弊端,它用到了arguments,递归,闭包等会带来性能影响,所以结合实际情况使用


HappyCodingTop
526 声望847 粉丝

Talk is cheap, show the code!!