Js中的函数

ⅰ亾dé卋堺 提交于 2020-02-07 03:12:24

函数柯里化

柯里化概念:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数
看一个例子:

var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

我们定义了一个add函数,它接受一个参数并返回一个新的函数。调用了add之后,返回的函数就通过闭包的方式记住了add的第一个参数。

因为一次性地调用它有点繁琐,所以使用一个特殊的curry帮助函数使得这类函数的定义和调用更加容易。

curry的封装

// 初步封装
var currying = function(fn) {
    // args 获取第一个方法内的全部参数
    var args = Array.prototype.slice.call(arguments, 1)
    return function() {
        // 将后面方法里的全部参数和args进行合并
        var newArgs = args.concat(Array.prototype.slice.call(arguments))
        // 把合并后的参数通过apply作为fn的参数并执行
        return fn.apply(this, newArgs)
    }
}

这边首先是初步封装,通过闭包把初步参数给保存下来,然后通过获取剩下的arguments(这里的arguments是函数function自带的一个隐式参数,表现为所有参数组成的数组)进行拼接,最后执行需要currying的函数。

但是好像还有些什么缺陷,这样返回的话其实只能多扩展一个参数,currying(a)(b)(c)这样的话,貌似就不支持了(不支持多参数调用),一般这种情况都会想到使用递归再进行封装一层。

// 支持多参数传递
function progressCurrying(fn, args) {

    var _this = this
    var len = fn.length;
    var args = args || [];

    return function() {
        var _args = Array.prototype.slice.call(arguments);
        Array.prototype.push.apply(args, _args);

        // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
        if (_args.length < len) {
            return progressCurrying.call(_this, fn, _args);
        }

        // 参数收集完毕,则执行fn
        return fn.apply(this, _args);
    }
}

拓展一道经典的面试题

// 实现一个add方法,使计算结果能够满足如下预期:
// add(1)(2)(3) = 6;
// add(1, 2, 3)(4) = 10;
// add(1)(2)(3)(4)(5) = 15;

function add() {
    // 第一次执行时,定义一个数组专门用来存储所有的参数
    var _args = Array.prototype.slice.call(arguments);

    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    var _adder = function() {
        console.log("接收了一个()");
        _args.push(...arguments); //...arguments在ES6中表示剩下所有的参数,接受剩下的所有参数
        return _adder; //再返回adder,进行下一次递归,把上次接收的结果作为第一个参数,
        //再接受下一个参数,这里相当于接受下一个()里的参数,直到接收完毕

    };

    // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    _adder.toString = function() {
         // return _args.reduce(function(a, b) {
        //     return a + b;
        // });
        return _args.reduce((a, b) => a + b);
        //reduce是ES5中的API,利用其能够遍历到数组的每一个元素,这里的a+b相当于当前所有的参数
    }
    return _adder; //这里相当于返回最后计算好的结果
}

var res = add(1, 2)(1)(2)(5);
console.log(res + '');
console.log(add(1)(2)(3) + '');
/*
接收了一个()
接收了一个()
接收了一个()
11
接收了一个()
接收了一个()
6
*/

为什么要进行隐式转化

因为当我们将函数参与其他的计算时,函数会默认调用toString方法,直接将函数体转换为字符串参与计算。

function fn() { return 20 }
console.log(fn + 10);     // 输出结果 function fn() { return 20 }10

我们可以重写函数的toString方法,让函数参与计算,输出我们想要的结果:,就像上面我们所写的_adder.toString =function(){},

function fn() { return 20; }
fn.toString = function() { return 30 }

console.log(fn + 10); // 40

除此之外,当我们重写函数的valueOf方法也能够改变函数的隐式转换结果:

function fn() { return 20; }
fn.valueOf = function() { return 60 }

console.log(fn + 10); // 70

函数柯里化的好处

参数复用

// 正常正则验证字符串 reg.test(txt)

// 函数封装后
function check(reg, txt) {
    return reg.test(txt)
}

check(/\d+/g, 'test')       //false
check(/[a-z]+/g, 'test')    //true

// Currying后
function curryingCheck(reg) {
    return function(txt) {
        return reg.test(txt)
    }
}

var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)

hasNumber('test1')      // true
hasNumber('testtest')   // false
hasLetter('21212')      // false

上面的示例是一个正则的校验,正常来说直接调用check函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数reg进行复用,这样别的地方就能够直接调用hasNumber,hasLetter等函数,让参数能够复用,调用起来也更方便。

提前确认

var on = function(element, event, handler) {
    if (document.addEventListener) {
        if (element && event && handler) {
            element.addEventListener(event, handler, false);
        }
    } else {
        if (element && event && handler) {
            element.attachEvent('on' + event, handler);
        }
    }
}

var on = (function() {
    if (document.addEventListener) {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.addEventListener(event, handler, false);
            }
        };
    } else {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.attachEvent('on' + event, handler);
            }
        };
    }
})();

//换一种写法可能比较好理解一点,上面就是把isSupport这个参数给先确定下来了
var on = function(isSupport, element, event, handler) {
    isSupport = isSupport || document.addEventListener;
    if (isSupport) {
        return element.addEventListener(event, handler, false);
    } else {
        return element.attachEvent('on' + event, handler);
    }
}

我们在做项目的过程中,封装一些dom操作可以说再常见不过,上面第一种写法也是比较常见,但是我们看看第二种写法,它相对一第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了会走哪一个方法,避免每次都进行判断。

延迟运行

Function.prototype.bind = function (context) {
    var _this = this
    var args = Array.prototype.slice.call(arguments, 1)
 
    return function() {
        return _this.apply(context, args)
    }
}

像我们js中经常使用的bind,实现的机制就是Currying.

Js中的高阶函数

JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数
一个最简单的高阶函数:

function add(x, y, f) {
    return f(x) + f(y);
}

当我们调用add(-5, 6, Math.abs)时,参数x,y和f分别接收-5,6和函数Math.abs,根据函数定义,我们可以推导计算过程为:

x = -5;
y = 6;
f = Math.abs;
f(x) + f(y) ==> Math.abs(-5) + Math.abs(6) ==> 11;
return 11;

map/reduce

由于map()方法定义在JavaScript的Array中,我们调用Array的map()方法,传入我们自己的函数,就得到了一个新的Array作为结果:

'use strict';
function pow(x) {
    return x * x;
}
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var results = arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
console.log(results);

再看reduce的用法。Array的reduce()把一个函数作用在这个Array的[x1, x2, x3...]上,这个函数必须接收两个参数,reduce()把结果继续和序列的下一个元素做累积计算,其效果就是:

var arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
    return x + y;
});
//还可以写成箭头函数形式
arr.reduce((a,b)=> a+b);

filter(筛选器)

filter也是一个常用的操作,它用于把Array的某些元素过滤掉,然后返回剩下的元素。

map()类似,Array的filter()也接收一个函数。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是true还是false决定保留还是丢弃该元素。
把一个Array中的空字符串删掉,可以这么写:

var arr = ['A', '', 'B', null, undefined, 'C', '  '];
var r = arr.filter(function (s) {
    return s && s.trim(); // 注意:IE9以下的版本没有trim()方法
});
r; // ['A', 'B', 'C']

sort

幸运的是,sort()方法也是一个高阶函数,它还可以接收一个比较函数来实现自定义的排序。

要按数字大小排序,我们可以这么写:

'use strict';

var arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
    if (x < y) {
        return -1;
    }
    if (x > y) {
        return 1;
    }
    return 0;
});
console.log(arr); // [1, 2, 10, 20]
//对包含字母的排序
var arr = ['Google', 'apple', 'Microsoft'];
arr.sort(function (s1, s2) {
    x1 = s1.toUpperCase();
    x2 = s2.toUpperCase();
    if (x1 < x2) {
        return -1;
    }
    if (x1 > x2) {
        return 1;
    }
    return 0;
}); // ['apple', 'Google', 'Microsoft']

Array

every

every()方法可以判断数组的所有元素是否满足测试条件。

var arr = ['Apple', 'pear', 'orange'];
console.log(arr.every(function (s) {
    return s.length > 0;
})); // true, 因为每个元素都满足s.length>0

console.log(arr.every(function (s) {
    return s.toLowerCase() === s;
})); // false, 因为不是每个元素都全部是小写

find

find()方法用于查找符合条件的第一个元素,如果找到了,返回这个元素,否则,返回undefined

var arr = ['Apple', 'pear', 'orange'];
console.log(arr.find(function (s) {
    return s.toLowerCase() === s;
})); // 'pear', 因为pear全部是小写

console.log(arr.find(function (s) {
    return s.toUpperCase() === s;
})); // undefined, 因为没有全部是大写的元素

findIndex

findIndex()find()类似,也是查找符合条件的第一个元素,不同之处在于findIndex()会返回这个元素的索引,如果没有找到,返回-1

var arr = ['Apple', 'pear', 'orange'];
console.log(arr.findIndex(function (s) {
    return s.toLowerCase() === s;
})); // 1, 因为'pear'的索引是1

console.log(arr.findIndex(function (s) {
    return s.toUpperCase() === s;
})); // -1

forEach

forEach()和map()类似,它也把每个元素依次作用于传入的函数,但不会返回新的数组。forEach()常用于遍历数组,因此,传入的函数不需要返回值:

var arr = ['Apple', 'pear', 'orange'];
arr.forEach(console.log); // 依次打印每个元素

Generator函数 async函数

概念:

  • Generator 函数是 ES6 提供的一种异步编程解决方案
  • generator 由function *定义(注意多出的*号);函数内部使用yield表达式,定义不同的内部状态
  • 调用Generator函数后,该函数并不执行,返回的不是函数运行结果,而是一个指向内部状态的指针对象
  • 必须调用遍历器对象的next方法,使得指针移向下一个状态,输出返回的结果
  • next方法返回的是一个对象。它的value属性就是当前yield表达式的值hello,done属性的值false则表示遍历还没有结束(即没有遇到return)。
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

next方法可以传参数:

function* gen(x){
 const y = yield x + 6;
 return y;
}
const g = gen(1);
g.next() // { value: 7, done: false }
g.next(2) // { value: 2, done: true } 
// next 的参数是作为上个阶段异步任务的返回结果

异步应用

因为yield能够中断执行代码的特性,可以帮助我们来控制异步代码的执行顺序。

例如有两个异步的函数AB, 并且B的参数是 A的返回值,也就是说,如果 A没有执行结束,我们不能执行B

function* effect() {
  const { param } = yield A();
  const { result } = yield B(param);
  console.table(result);
}
//看到结果
const iterator = effect()
iterator.next()
iterator.next()

async和await

这两个相当于generator函数的另一种语义表示

  • async函数将Generator函数的星号(*)替换成async
  • yield替换成await

async函数对Generator函数的改进体现在以下3点:
1.内置执行器

也就是说async函数的执行,和普通函数一样,只需要一行就可以。不用像Generator函数需要调用next方法才能真正执行。

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
//调用只需要一行代码,而且自带执行器,不需要next()
asyncReadFile();

2.更好的语义

asyncawait比起星号和yield,语义更加清楚了。async表示函数里有异步操作await表示紧跟在后面的表达式需要等待结果。

3.返回值是promise

async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是Iterator 对象方便多了。你可以用then方法指定下一步的操作。

async function getStockPriceByName(name) {
  const symbol = await getStockSymbol(name);
  const stockPrice = await getStockPrice(symbol);
  return stockPrice;
}

getStockPriceByName('goog').then(function (result) {
  console.log(result);
});

先执行了第一个await后的getStockSymbol(name)函数;得到了股票的名称symbol后,将symbol传给第二个await后面的getStockPrice(symbol)作为参数;最后返回股票价格stockPrice

参考

https://www.jianshu.com/p/2975c25e4d71
https://delaprada.com/2020/01/14/Generator%E5%87%BD%E6%95%B0-async%E5%87%BD%E6%95%B0/#more

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!