JavaScript学习笔记04-JS函数式编程

参数对象arguments,纯函数,柯里化,组合函数,with语句,eval函数,严格模式

1. arguments对象

1.1. 认识arguments

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

    1
    2
    3
    4
    5
    6
    function foo(x,y,z){
    // [Arguments] { '0': 1, '1': 2, '2': 3 }
    console.log(arguments);
    }

    foo(1,2,3);
  • array-like意味着它不是一个数组类型,而是一个对象类型

    • 但是它却拥有数组的一些特性,比如说length,比如可以通过index索引来访问
    • 但是它却没有数组的一些方法,比如forEach、map等
    1
    2
    3
    4
    5
    6
    7
    8
    function foo(x,y,z){
    // 3
    console.log(arguments.length);
    // 6
    console.log(arguments[0] + arguments[1] + arguments[2]);
    }

    foo(1,2,3);

1.2. arguments转成array

1.2.1. push

1
2
3
4
5
6
var length = arguments.length;
var arr = [];
for (var i = 0; i < length; i++) {
arr.push(arguments[i]);
}
console.log(arr);

1.2.2. slice

1
2
3
4
5
var arr1 = Array.prototype.slice.call(arguments);
console.log(arr1);

var arr2 = [].slice.call(arguments);
console.log(arr2);

其中slice的实现是数组的复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function.prototype.hyslice = function(start, end){
var arr = this;

start = start || 0;
end = end || arr.length;

var newarr = [];
for (var i = start; i < end; i++) {
newarr.push(arr[i]);
}
return newarr;
}

var arr = Function.prototype.hyslice.call([1,2,3],1);
console.log(arr);

1.2.3. Array.from

1
2
var arr3 = Array.from(arguments);
console.log(arr3);

1.2.4. …

1
2
var arr4 = [...arguments];
console.log(arr4);

1.3. 箭头函数不绑定arguments

箭头函数是不绑定arguments的

1
2
3
4
5
6
// 箭头函数不绑定arguments
var bar = (m,n) => {
console.log(arguments);
}
// Uncaught ReferenceError: arguments is not defined
// bar(1,2);

在箭头函数中使用arguments会去上层作用域查找

1
2
3
4
5
6
7
8
9
10
11
function fun(m,n){
console.log('bar',arguments);

return (x,y,z) => {
console.log('arrow',arguments);
}
}
// bar [Arguments] { '0': 1, '1': 2 }
var fn = fun(1,2);
// arrow [Arguments] { '0': 1, '1': 2 }
fn(3,4,5);

2. 纯函数

2.1. 认识纯函数

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

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

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

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

2.2. 副作用的理解

  • 什么是副作用

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

    • 副作用往往是产生bug的 “温床”

2.3. 纯函数的案例

  • 一个对数组操作的两个函数
    • slice:slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组
    • splice:splice截取数组,会返回一个新的数组,也会对原数组进行修改
    • slice就是一个纯函数,不会修改传入的参数
1
2
3
4
5
6
7
8
9
10
11
var names = ['abc','cba','nba'];

// slice截取数组时不会对原数组进行任何操作
var newNames = names.slice(0,2);
// [ 'abc', 'cba' ] [ 'abc', 'cba', 'nba' ]
console.log(newNames,names);

// split会截取数组的同时对原数组进行截取
var newNames2 = names.splice(0,2);
// [ 'abc', 'cba' ] [ 'nba' ]
console.log(newNames, names);

2.4. 纯函数判断

  • 相同的输入有相同的输出

  • 执行的过程中不会产生任何副作用

1
2
3
function sum(num1, num2){
return num1 + num2;
}

不是

相同的输入没有相同的输出

1
2
3
4
5
6
7
let foo = 5;
function add(num){
return foo + num;
}
console.log(add(5));
foo = 10;
console.log(add(5));

不是

修改了传入的参数

1
2
3
4
function printInfo(info){
console.log(info.name, info.age);
info.name = "哈哈哈";
}

2.5. 纯函数的优势

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

    • 因为可以安心的编写和安心的使用
    • 在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或 者依赖其他的外部变量是否已经发生了修改
    • 在用的时候,确定输入内容不会被任意篡改,并且确定的输入,一定会有确定的输出
  • React中就要求无论是函数还是class声明一个组件,这个组件都必须像纯函数一样保护它们的props不被修改

3. 柯里化

3.1. 认识柯里化

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

  • 维基百科的解释

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

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

3.2. 柯里化的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 未柯里化
function add1(x, y, z){
return x + y + z;
}
console.log(add1(1,2,3));

// 柯里化
function add2(x){
return function(y){
return function(z){
return x + y + z;
}
}
}
console.log(add2(1)(2)(3));


var add3 = x => y => z => {
return x + y + z
}

console.log(add3(1)(2)(3));

3.3. 柯里化作用

  • 为什么需要有柯里化

    • 在函数式编程中,往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个函数来处理
    • 那么是否就可以将每次传入的参数在单一的函数中进行处理,处理完后在下一个函数中再使用处理后的结果
  • 比如对上面的案例进行一个修改

    • 传入的函数需要分别被进行如下处理
    • 第一个参数 + 2
    • 第二个参数 * 2
    • 第三个参数 ** 2
1
2
3
4
5
6
7
8
9
10
function add2(x){
x += 2;
return function(y){
y *= 2;
return function(z){
z **= 2;
return x + y + z;
}
}
}

3.4. 柯里化的复用

  • 另外一个使用柯里化的场景是可以复用参数逻辑
    • makeAdder函数要求传入一个num(并且可以在这里对num进行一些修改)
    • 在之后使用返回的函数时,不需要再继续传入num了
1
2
3
4
5
6
7
8
9
10
11
12
13
function makeAdder(num){
return function(count){
return num + count;
}
}

var add5 = makeAdder(5);
add5(10);
add5(100);

var add5 = makeAdder(10);
add5(10);
add5(100);

3.5. 打印日志的柯里化

  • 演示一个案例,需求是打印一些日志
    • 日志包括时间、类型、信息
1
2
3
4
5
6
7
8
9
10
var log = date => type => message => {
console.log(`[${date.getHours()}:${date.getMinutes()}][${type}][${message}]`);
}
var logNow = log(new Date());
logNow("DEBUG")("轮播图bug");
logNow("DEBUG")("轮播图bug");
logNow("FEATURE")("添加新功能");

var logNowDebug= log(new Date())("DEBUG");
logNowDebug("轮播图bug");

3.6. 自动柯里化函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function add1(x, y, z){
return x + y + z;
}

function hyCurrying(fn){
function curried(...args){
// 判断当前已经接收的参数个数,和参数本身需要接收的参数是否已经一致
// 1. 传入的参数 大于等于 需要的参数时,执行函数
if (args.length >= fn.length) {
return fn.apply(this, args);
}
else {
// 2. 没有达到参数个数时,需要返回新的函数,继续接收参数
return function(...arg2){
// 递归调用curried函数
// 新接收的参数拼接到上一次调用的参数后
// 直至参数个数符合函数参数要求
return curried.apply(this, args.concat(arg2));
}
}
}
return curried;
}
var curryAdd = hyCurrying(add1);
var result = curryAdd(10,20,30,40);
console.log(result);

result = curryAdd(10)(20)(30);
console.log(result);

4. 组合函数

4.1. 理解组合函数

  • 组合(Compose)函数是在JavaScript开发过程中一种对函数的使用技巧、模式
    • 比如需要对某一个数据进行函数的调用,执行两个函数fn1和fn2,这两个函数是依次执行的
    • 如果每次都需要进行两个函数的调用,操作上就会显得重复
    • 那么可以将这两个函数组合起来,自动依次调用
    • 这个过程就是对函数的组合,称之为组合函数(Compose Function)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function double(num){
return num * 2;
}

function square(num){
return num * num;
}

function compose(fn1,fn2){
return function(x){
return fn2(fn1(x))
}
}

var calFn = compose(double,square)
console.log(calFn(20));

4.2. 实现组合函数

  • 需要考虑更加复杂的情况
    • 比如传入了更多的函数
    • 在调用 compose函数时,传入了更多的参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function double(num){
return num * 2;
}

function square(num){
return num * num;
}

function compose(...fns){

var length = fns.length;
// 遍历所有的原生函数,不是函数的话,直接报错
for (var i = 0; i < length; i++) {
var fn = fns[i];
if (typeof fn !== 'function') {
throw new TypeError('Expected a function');
}
}

// 取出所有的函数调用
return function(...args){
var index = 0;
var result = length? fns[index].apply(this,args) : args;

while (++index<length) {
result = fns[index].call(this,result);
}
return result;
}
}

var calFn = compose(double,square)
console.log(calFn(30));

5. with语句

  • with语句扩展一个语句的作用域链,形成自己的作用域
  • 不建议使用with语句,因为它可能是混淆错误和兼容性问题的根源
1
2
3
4
5
6
7
8
9
var obj = {
name: 'test',
age: 18
}

with(obj){
console.log(name);
console.log(age);
}

6. eval函数

  • eval是一个特殊的函数,它可以将传入的字符串当做JavaScript代码来运行
  • 不建议在开发中使用eval
    • eval代码的可读性非常的差(代码的可读性是高质量代码的重要原则)
    • eval是一个字符串,那么有可能在执行的过程中被刻意篡改,那么可能会造成被攻击的风险
    • eval的执行必须经过JS解释器,不能被JS引擎优化
1
2
var evalStr = `var name = 'test'; console.log(name);`;
eval(evalStr);

7. 严格模式

7.1. 认识严格模式

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

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

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

7.2. 开启严格模式

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

    • 支持在js文件中开启严格模式
    • 也支持对某一个函数开启严格模式
  • 严格模式通过在文件或者函数开头使用 use strict 来开启

  • 开发中,打包工具会自动开启严格模式

1
2
3
4
5
6
7
function foo(){
"use strict";

// ReferenceError: m is not defined
m = "foo";
console.log(m);
}

7.3. 严格模式限制

  • 严格模式下的严格语法限制
  • JavaScript被设计为新手开发者更容易上手,所以有时候本来错误语法,被认为也是可以正常被解析的
  • 但是这种方式可能给带来留下来安全隐患
  • 在严格模式下,这种失误就会被当做错误,以便可以快速的发现和修正
  1. 无法意外的创建全局变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    "use strict";

    // ReferenceError: name is not defined
    name = "Hello World";
    console.log(name);


    function foo(){
    m = "foo";
    }
    // ReferenceError: m is not defined
    foo();
    console.log(m);
  2. 严格模式会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // TypeError: Cannot create property 'name' on boolean 'true'
    // true.name = "abc";

    // TypeError: Cannot assign to read only property 'NaN' of object '#<Object>'
    // NaN = 123;

    var obj = {}
    Object.defineProperty(obj,"name",{
    writable: false,
    value: 'test'
    });
    // TypeError: Cannot assign to read only property 'name' of object '#<Object>'
    // obj.name = '123';
  3. 严格模式下试图删除不可删除的属性

  4. 严格模式不允许函数参数有相同的名称

    1
    2
    3
    4
    5
    6
    7
    function foo(x, y, x){
    // SyntaxError: Duplicate parameter name not allowed in this context
    console.log(x);

    }

    foo(1, 2, 3);
  5. 不允许0的八进制语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 八进制
    // SyntaxError: Octal literals are not allowed in strict mode.
    // var num = 0123;
    // console.log(num);
    var num1 = 0o123;
    // 十六进制
    var num2 = 0x123;
    // 二进制
    var num3 = 0b100;
    console.log(num1, num2, num3);
  6. 在严格模式下,不允许使用with

  7. 在严格模式下,eval不再为上层引用变量

    1
    2
    3
    4
    var evalStr = `var message = 'test'; console.log(message);`;
    eval(evalStr);
    // ReferenceError: message is not defined
    // console.log(message);
  8. 严格模式下,this绑定中自执行函数(默认绑定)会指向undefined

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function foo(){
    console.log(this);
    }
    // undefined
    foo();
    // {}
    foo.call({});
    // {name:'test'}
    foo.apply({name:'test'});

    var obj = {
    name: 'test',
    foo: foo
    }
    // obj
    obj.foo();

    // foo
    var fn = new foo();

    而 setTimeout中的this仍指向window

    https://github.com/chromium/chromium/blob/main/third_party/google-closure-library/closure/goog/debug/errorhandler_test.js

    1
    2
    3
    4
    5
    6
    7
    8
    const recordFunction = goog.require('goog.testing.recordFunction');
    state.real = {setTimeout, setInterval, requestAnimationFrame};

    state.fake = {
    setTimeout: recordFunction(state.real.setTimeout.bind(null))
    };

    window.setTimeout = state.fake.setTimeout;

    https://github.com/chromium/chromium/blob/main/third_party/google-closure-library/closure/goog/testing/recordfunction.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    goog.testing.recordFunction = function(opt_f) {
    'use strict';
    var f = opt_f || goog.nullFunction;

    function recordedFunction() {
    var owner = /** @type {?} */ (this);
    var ret = f.apply(owner, arguments);
    }
    }
本文结束  感谢您的阅读