函数对象的属性

我们知道JavaScript中函数也是一个对象,那么对象中就可以有属性和方法。

属性name:一个函数的名字我们可以通过name来访问;

属性length:属性length用于返回函数参数的个数;

  • 注意:rest参数是不参与参数的个数的

	  // 定义函数
      function foo(a, b, c) {}
      
      var bar = function (m, n = 20, ...other) {};

      /* var obj = {};
      obj.address = "东京";
      console.log(obj.address); */

      // 自定义属性
      foo.message = "Hello Foo";
      console.log(foo.message);

      // 默认函数对象中已经有自己的属性
      // 1.name属性
      /* console.log(foo.name)
      console.log(bar.name) */

      // 将两个函数放在数组中
      /*  var fns = [foo,bar]
      for (var fn of fns) {
        console.log(fn.name)
      } */

      // 2.length属性: 本来需要接受参数的个数
      function test() {}
      test(111, 222, 333);
      console.log(foo.length); // 3
      console.log(bar.length); // 2
      console.log(test.length); // 0

      //  额外补充
      // 默认值和...arguments不算在内
      function demo(arg1, arg2 = "hhh", ...args) {}
      demo("abc", "cba", "nba", "mba");

认识arguments

arguments 是一个 对应于 传递给函数的参数 的 类数组(array-like)对象。

array-like意味着它不是一个数组类型,而是一个对象类型:

  • 但是它却拥有数组的一些特性,比如说length,比如可以通过index索引来访问;
  • 但是它却没有数组的一些方法,比如filter、map等;

arguments转Array

在开发中,我们经常需要将arguments转成Array,以便使用数组的一些特性。

常见的转化方式如下

转化方式一:

  • 遍历arguments,添加到一个新数组中;

转化方式二:较难理解(有点绕),了解即可

  • 调用数组slice函数的call方法;

转化方式三:ES6中的两个方法

  • Array.from
  • […arguments]

    function foo(m, n) {
        // arguments 是一个类数组对象
        // console.log(m, n, arguments);
        // 1.默认用法
        // 可以通过索引来操作内容
        // console.log(arguments[0]);
        // console.log(arguments[3]);

        // 遍历
        /* for (var i = 0; i < arguments.length; i++) {
          console.log(arguments[i]);
        }

        for (var arg of arguments) {
          console.log(arg);
        } */

        // 2.需求获取所有参数中的偶数
        // 数组 filter
        /* for (var arg of arguments) {
          if (arg % 2 === 0) {
            console.log(arg);
          }
        }

        var evenNums = arguments.filter((item) => item % 2 === 0);
        console.log(evenNums); 报错*/

        // 2.1 将 arguments 转换成数组

        // 方式一:
        /* var newArguments = [];
        for (var arg of arguments) {
          newArguments.push(arg);
        }

        console.log(newArguments); */

        // 方式二: ES6中方式
        /*  var newArg1 = Array.from(arguments);
        console.log(newArg1);
        var newArg2 = [...arguments];
        console.log(newArg2) */

        // 方式三: 调用 slice 方法
        var newArgs = [].slice.apply(arguments);
        console.log(newArgs)
      }

      foo(10, 25, 32, 41);

      // slice 方法的回顾

      /* var names = ["abc", "cba", "nba", "mba"];
      var newNames = names.slice(1, 3); // this -> names

      // slice 方法 -> 函数
      console.log(newNames); */

箭头函数不绑定arguments

箭头函数是不绑定arguments的,所以我们在箭头函数中使用arguments会去上层作用域查找:

      // 1.箭头函数不绑定 arguments
      /* var bar = () => {
              console.log(arguments)
         };
         bar(11, 22, 33); */

      // 2.函数的嵌套箭头函数
      function foo() {
        var bar = () => {
          console.log(arguments);
        };
        bar();
      }
      foo(11, 22, 33);

函数的剩余(rest)参数

ES6中引用了rest parameter,可以将不定数量的参数放入到一个数组中:

  • 如果最后一个参数是 … 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组;

那么剩余参数和arguments有什么区别呢?

  • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参;
  • arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作;
  • arguments是早期的ECMAScript中为了方便去获取所有的参数提供的一个数据结构,而rest参数是ES6中提供并且希望以此来替代arguments的;

剩余参数必须放到最后一个位置,否则会报错。

      // 剩余参数: rest parameters
      function foo(num1, num2, ...otherNums) {
        // otherNums 本身就是一个数组
        console.log(otherNums);
      }

      foo(20, 30, 111, 222, 333);
      
      // 默认一个函数只有一个剩余参数
      function bar(...args) {
        console.log(args);
      }

      bar("abc", 123, "cba", 321);
      
      // 注意: 剩余参数需要写到其他参数的最后

理解JavaScript纯函数

函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念;

  • 在react开发中纯函数是被多次提及的;
  • 比如react中组件就被要求像是一个纯函数(为什么是像,因为还有class组件),redux中有一个reducer的概念,也是要求必须是一个纯函数;
  • 所以掌握纯函数对于理解很多框架的设计是非常有帮助的;

纯函数的维基百科定义:

  • 在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:
  • 此函数在相同的输入值时,需产生相同的输出。
  • 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
  • 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。

当然上面的定义会过于的晦涩,所以我简单总结一下:

  • 确定的输入,一定会产生确定的输出;
  • 函数在执行过程中,不能产生副作用;

副作用概念的理解

那么这里又有一个概念,叫做副作用,什么又是副作用呢?

  • 副作用(side effect)其实本身是医学的一个概念,比如我们经常说吃什么药本来是为了治病,可能会产生一些其他的副作用;
  • 在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储;

纯函数在执行的过程中就是不能产生这样的副作用:

  • 副作用往往是产生bug的 “温床”。
	 function sum(num1, num2) {
        return num1 + num2;
     }

      // 不是一个纯函数
      var address = '东京'
      function printInfo(info) {
        console.log(info.name, info.age, info.message);
        info.flag = "已经打印结束";
        address = info.address;
      }
      
      var obj = {
        name: "what",
        age: 18,
        message: "hhhh",
      };

      printInfo(obj);
      console.log(obj); // {name: 'what', age: 18, message: 'hhhh', flag: '已经打印结束'}

纯函数的案例

我们来看一个对数组操作的两个函数:

  • slice:slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组;
  • splice:splice截取数组, 会返回一个新的数组, 也会对原数组进行修改;

slice就是一个纯函数,不会修改数组本身,而splice函数不是一个纯函数;

      var names = ["abc", "cba", "nba", "mba"];
      
      // 1.slice: 纯函数
      var newNames = names.slice(0, 2);
      console.log(names);

      // 2.splice: 操作数组的利器(不是纯函数)
      names.splice(2, 2);
      console.log(names);

判断下面函数是否是纯函数?

纯函数的作用和优势

为什么纯函数在函数式编程中非常重要呢?

  • 因为你可以安心的编写和安心的使用;

  • 你在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改;

  • 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;

  • React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改:

      // 1.安心编写代码:不需要去关系外层作用域中的值,目前是什么状态
      var counter = 0;
      
      function add(num) {
        return num * 2;
      }

      // 2.安心的使用: 调用函数时,可以明确的指定: 确定的输入一定会产生确定的输出
      add(5); // 10
      add(5); // 10
      
      // react 中编写函数组件
      function Foo(props) {
        console.log(props.name);
        props.name = "what"; // 修改了就不是纯函数了
      }

柯里化概念的理解

柯里化也是属于函数式编程里面一个非常重要的概念。

是一种关于函数的高阶技术;
它不仅被用于 JavaScript,还被用于其他编程语言;

我们先来看一下维基百科的解释:

  • 在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化;
  • 是把接收多个参数的函数,变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数,而且返回结果的新函数的技术;
  • 柯里化声称 “如果你固定某些参数,你将得到接受余下参数的一个函数”;

维基百科的解释非常的抽象,我们这里做一个总结:

  • 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数;
  • 这个过程就称之为柯里化;

柯里化是一种函数的转换,将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)©。

  • 柯里化不会调用函数。它只是对函数进行转换

柯里化的代码转换

那么柯里化到底是怎么样的表现呢?

	// 普通的函数
      function foo(x, y, z) {
        console.log(x + y + z);
      }

      /*
      foo(10, 20, 30);
      foo(20, 33, 55);
      */

      // 因为foo不是一个柯里化的函数,所以目前是不能这样调用的
      // foo(10)(20)(30)
      // 柯里化函数
      function foo(x) {
        return function (y) {
          return function (z) {
            console.log(x + y + z);
          };
        };
      }

      foo(10)(20)(30);
      foo(35)(65)(87);
            
     // 另一种写法: 箭头函数的写法
      /* function foo3() {
        return (y) => {
          return (z) => {
            console.log(x + y + z);
          };
        };
      } */

柯里化优势一 - 函数的职责单一

那么为什么需要有柯里化呢?

  • 在函数式编程中,我们其实往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个函数来处理;
  • 那么我们是否就可以将每次传入的参数在单一的函数中进行处理,处理完后在下一个函数中再使用处理后的结果;

比如上面的案例我们进行一个修改:传入的函数需要分别被进行如下处理

  • 第一个参数 + 2
  • 第二个参数 * 2
  • 第三个参数 ** 2

 var foo3 = (x) => (y) => (z) => console.log(x + y + z);

柯里化优势二 - 函数的参数服用

另外一个使用柯里化的场景是可以帮助我们可以复用参数逻辑:

  • makeAdder函数要求我们传入一个num(并且如果我们需要的话,可以在这里对num进行一些修改);
  • 在之后使用返回的函数时,我们不需要再继续传入num了;

柯里化案例练习

这里我们在演示一个案例,需求是打印一些日志:

  • 日志包括时间、类型、信息;

普通函数的实现方案如下:

	 // 案例一: 打印一些日志
      // 信息一: 日志的时间
      // 信息二: 日志的类型: info/debug/feature
      // 信息三: 具体的信息

      // 1.没有柯里化的时候做法
      /* function logInfo(date, type, message) {
        console.log(`时间:${date} 类型:${type} 内容:${message}`);
      }

      // 打印日志
      logInfo("2022-06-01", "DEBUG", "修复界面搜索按钮点击的bug");
      // 又修复了一个bug
      logInfo("2022-06-01", "DEBUG", "修复了从服务器请求数据后展示的bug");
      logInfo("2022-06-01", "DEBUG", "修复了从服务器请求数据后展示的bug");
      logInfo("2022-06-01", "DEBUG", "修复了从服务器请求数据后展示的bug");

      logInfo("2022-06-01", "FEATURE", "增加了商品的过滤功能"); */

      // 2.对函数进行柯里化: 柯里化函数的做法
      /* var logInfo = (date) => (type) => (message) =>
        console.log(`时间:${date} 类型:${type} 内容:${message}`);
      */

      function logInfo(date) {
        return function (type) {
          return function (message) {
            console.log(`时间:${date} 类型:${type} 内容:${message}`);
          };
        };
      }

      var logToday = logInfo("2023-01-08");
      var logTodayDebug = logToday("DEBUG");
      var logTodayFeature = logToday("FEATURE");

      // 打印 debug 日志
      logTodayDebug("修复了从服务器请求数据后展示的bug");
      logTodayDebug("修复界面搜索按钮点击的bug");
      logTodayDebug("修复界面搜索按钮点击的bug");
      logTodayDebug("修复界面搜索按钮点击的bug");
      logTodayDebug("修复界面搜索按钮点击的bug");

      logTodayFeature("新建过滤功能");
      logTodayFeature("新建搜索功能");
	  function sum(num1, num2) {
        return num1 + num2;
      }
      
      sum(5, 10);
      sum(5, 15);
      sum(5, 18);

      // makeAdder 函数就是对sum的柯里化
      function makeAdder(count) {
        function add(num) {
          return count + num;
        }
        return add;
      }

      // 1.数字和5相加
      var adder5 = makeAdder(5);
      adder5(10);
      adder5(15);
      adder5(18);

      // 2.数字和10相加
      var adder10 = makeAdder(10);
      adder10(10);
      adder10(16);
      adder10(19);

      // 避免内存泄漏
      // adder5 = null
      // adder10 = null

柯里化高级 - 自动柯里化函数

目前我们有将多个普通的函数,转成柯里化函数:

	function foo(x, y, z) {
        console.log(x + y + z);
    }

      function sum(num1, num2) {
        return num1 + num2;
      }
      function logInfo(date, type, message) {
        console.log(`时间:${date} 类型:${type} 内容:${message}`);
      }

      // 自动转化: 封装自动柯里化函数
      function rinCurrying(fn) {
        function curryFn(...args) {
          // 两类操作
          // 第一种:继续返回一个新的函数,继续接受参数
          // 第二种操作: 直接执行fn的函数
          if (args.length >= fn.length) {
            // 执行第二种操作
            //  return fn(...args);
            return fn.apply(this,args);
          } else {
            // 执行第一种
            return function (...newArgs) {
              // return curryFn(...args.concat(newArgs));
              return curryFn.apply(this,args.concat(newArgs));
            };
          }
        }
        return curryFn;
      }

      // 对其他的函数柯里化
      var fooCurry = rinCurrying(foo);
      fooCurry(10)(20)(30);
      fooCurry(55, 32, 54);

      var sumCurry = rinCurrying(sum);
      console.log(sumCurry(14, 16));
      var sum5 = sumCurry(5);
      console.log(sum5(10));
      console.log(sum5(16));
      console.log(sum5(18));

      var logInfoCurry = rinCurrying(logInfo);
      logInfoCurry("2023-01-08", "debug")("我发现一个bug");

组合函数概念的理解

组合(Compose)函数是在JavaScript开发过程中一种对函数的使用技巧、模式:

  • 比如我们现在需要对某一个数据进行函数的调用,执行两个函数fn1和fn2,这两个函数是依次执行的;
  • 那么如果每次我们都需要进行两个函数的调用,操作上就会显得重复;
  • 那么是否可以将这两个函数组合起来,自动依次调用呢?
  • 这个过程就是对函数的组合,我们称之为 组合函数(Compose Function);

      var num = 100;
      
      // 第一步对数字*2
      function double(num) {
        return num * 2;
      }

      // 第二步对数字**2
      function pow(num) {
        return num ** 2;
      }

      console.log(pow(double(num)));
      
      // 将上面两个函数组合在一起,生成一个新的函数
      function composeFn(num) {
        return pow(double(num));
      }

      console.log(composeFn(100));
      console.log(composeFn(66));
      console.log(composeFn(33));

实现组合函数

刚才我们实现的compose函数比较简单

我们需要考虑更加复杂的情况:比如传入了更多的函数,在调用compose函数时,传入了更多的参数:

      // 第一步对数字*2
      function double(num) {
        return num * 2;
      }

      // 第二步对数字**2
      function pow(num) {
        return num ** 2;
      }

      // 封装的函数: 你传入多个函数,我自动的将多个函数组合在一起调用
      function composeFn(...fns) {
        // 1.边界判断(edge case)
        var length = fns.length;
        if (length <= 0) return;
        
        for (var i = 0; i < length; i++) {
          var fn = fns[i];
          if (typeof fn !== "function") {
            throw new Error(`index position ${i} fn must be function`);
          }
        }

        // 2.返回的新函数
        return function (...args) {
          var result = fns[0].apply(this, args);
          for (var i = 1; i < length; i++) {
            var fn = fns[i];
            result = fn.apply(this, [result]);
          }
          return result
        };
      }

      var newFn = composeFn(double, pow);
      console.log(newFn(100))
      newFn(100);

with语句的使用

with语句 扩展一个语句的作用域链。

不建议使用with语句,因为它可能是混淆错误和兼容性问题的根源。

eval函数

内建函数 eval 允许执行一个代码字符串。

  • eval是一个特殊的函数,它可以将传入的字符串当做JavaScript代码来运行;
  • eval会将最后一句执行语句的结果,作为返回值;

不建议在开发中使用eval:

  • eval代码的可读性非常的差(代码的可读性是高质量代码的重要原则);
  • eval是一个字符串,那么有可能在执行的过程中被刻意篡改,那么可能会造成被攻击的风险;
  • eval的执行必须经过JavaScript解释器,不能被JavaScript引擎优化;

认识严格模式

JavaScript历史的局限性:

  • 长久以来,JavaScript 不断向前发展且并未带来任何兼容性问题;
  • 新的特性被加入,旧的功能也没有改变,这么做有利于兼容旧代码;
  • 但缺点是 JavaScript 创造者的任何错误或不完善的决定也将永远被保留在 JavaScript 语言中;

在ECMAScript5标准中,JavaScript提出了严格模式的概念(Strict Mode):

  • 严格模式很好理解,是一种具有限制性的JavaScript模式,从而使代码隐式的脱离了 ”懒散(sloppy)模式“;
  • 支持严格模式的浏览器在检测到代码中有严格模式时,会以更加严格的方式对代码进行检测和执行;

严格模式对正常的JavaScript语义进行了一些限制:

  • 严格模式通过 抛出错误 来消除一些原有的 静默(silent)错误;
  • 严格模式让JS引擎在执行代码时可以进行更多的优化(不需要对一些特殊的语法进行处理);
  • 严格模式禁用了在ECMAScript未来版本中可能会定义的一些语法;

开启严格模式

那么如何开启严格模式呢?严格模式支持粒度话的迁移:

  • 可以支持在js文件中开启严格模式;
  • 也支持对某一个函数开启严格模式;

严格模式通过在文件或者函数开头使用 use strict 来开启。

没有类似于 “no use strict” 这样的指令可以使程序返回默认模式。

  • 现代 JavaScript 支持 “class” 和 “module” ,它们会自动启用 use strict;

严格模式限制

这里我们来说几个严格模式下的严格语法限制:

  • JavaScript被设计为新手开发者更容易上手,所以有时候本来错误语法,被认为也是可以正常被解析的;
  • 但是这种方式可能给带来留下来安全隐患;
  • 在严格模式下,这种失误就会被当做错误,以便可以快速的发现和修正;
  1. 无法意外的创建全局变量
  2. 严格模式会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常
  3. 严格模式下试图删除不可删除的属性
  4. 严格模式不允许函数参数有相同的名称
  5. 不允许0的八进制语法
  6. 在严格模式下,不允许使用with
  7. 在严格模式下,eval不再为上层引用变量
  8. 严格模式下,this绑定不会默认转成对象

手写apply、call、bind函数实现(原型后)

接下来我们来实现一下apply、call、bind函数:

  • 注意:我们的实现是练习函数、this、调用关系,不会过度考虑一些边界情况

Q.E.D.