前置知识
下篇主要讲柯里化和组合,柯里化其实跟上篇讲的闭包藕断丝连,不可分割,闭包是实现柯里化的基础,不妨再详细说说闭包,不过这次我要很细很细的介绍一遍。但这里需要一写前置知识,容我简单介绍。
作用域
作用域指程序中定义变量的区域,它可以决定当前执行代码的变量的可访问权限。作用域就是代码中某些特定的变量、函数在特定的独立区域可以被访问到。 下面有一个小例子 可以加深对作用域的理解
function test(a,b){
let innerVariable=1;
return 0;
}
//下面这句肯定会出错,因为innerVariable作用域仅在函数内,外部无法访问
let outVariable=innerVariable;
作用域的分类
在es5及之前,作用域可分为全局作用域和函数作用域,在es6及以后,作用域又多了块级作用域(let const重点体现了块级作用域)
这里感觉可以再强行加餐一下var 和let const的区别。
由图可见a,b存放在脚本区(块级作用域) c存放在全局区。
var可以上浮,也就是var定义的变量可以先使用后定义。const,let定义的变量其实也可以上浮,但是变量在被定义之前是无法访问的,所以会报错,如果细心的话可以发现,const,let变量先使用后定义报的错和只使用不定义是有区别的。
函数上下文
参考文章 (理解JavaScript的执行上下文 - 知乎 (zhihu.com))
闭包
综合上述知识,就更能深刻的剖析闭包。 简言之,能够访问其他内部函数变量的函数,成为闭包。
function fun1() {
var a = 1;
return function fun2 () {
console.log(a);
};
}
var fn = fun1();
fn();//形成闭包,从外部获取内部作用域的信息。
(1)编译阶段,变量和函数被声明,作用域就确定下来。
(2)运行函数fun1(),创建fun1()函数的执行上下文,内部存储fun1()中所有的变量函数的信息。
(3)函数fun1()执行完之后,把fun2的引用赋值给外部变量fn,要明确的是此时fn的指针指向的是fun2,此时fn位于全局作用域,fun2位于函数作用域,所以可以看见fn位于fun1() 作用域之外,但是访问到了fun1()的内部变量。
(4)fn在全局被执行,内部代码console.log(a)向作用域请求获取a变量,在本级的作用域没有找到,就向上父级作用域一层一层找父亲(找爸爸)。在fun1()找到了a变量,返回给console.log所以打印出来了1。
一些基于闭包的杂想
c++面向对象中class中成员分为public,private,protected,抛开继承来讲,private,protected基本一样,这里我把他们都视作private,private存放的成员是不想外界访问的东西,但js并没有private关键字。可能对于初学者来讲public,private有个啥用,通通public岂不是更方便,注意一点用就不会泄露隐私成员了,但我觉得一个成熟的编程语言,不仅要可以完成功能逻辑,还要从语言层次强行避免一些不该发生的事情出现,比如类中不想让外界看到的成员,不应该在功能逻辑这一阶段上避免它出现,而是要在写代码的时候,就把问题暴露出来。
js利用闭包这一概念,就能模拟出private的效果。
function getImformation() {
var name = "gaby";
var age = 20;
return function () {
return {
getName: function () {
return name;
},
getAge: function () {
return age;
}
};
};
}
var obj = getImformation()();
obj.getName();
obj.getAge();
obj.age;
闭包也会带来一些问题,会带来一些问题垃圾回收的问题。(关于垃圾回收,会在之后的js性能优化这一章节再详细说说)主要是变量的引用带来的,如果了解c++ shared_ptr的话不难理解,问题十分相似,不了解也不用紧。
JavaScript内部有垃圾回收机制,用计数的方法 。当内存中的一个变量被引用一次,计数+1,垃圾回收机制会在固定的时间间隔内询问这些变量,将计数为0的变量标记为失效变量从而清除释放内存。
再来看第一个闭包代码,fun1()函数隔绝了外部的影响,所有变量在函数内部完成,fun1()执行后,理论上内部的变量就会被销毁,内存被回收。但是我们写了一个闭包,这就导致了全局作用域始终存在一个a变量,一直占用内存,造成内存泄漏。
柯里化
终于步入正题柯里化,其实对闭包深刻剖析后,柯里化的理解就是一个迎刃而解的事情。柯里化就是将函数多个参数压缩为一个,是对函数参数的降维,(突然想到高中数学老师老是说遇到高次函数先降次哈哈哈)这玩意不就是把正常的多参数函数一个一个,逐层逐层的当闭包写吗(可能形容不太恰当,看案例).
//正常多参数函数
function add(x,y,z){
return x+y+z;
}
//柯里化后
function add() {
return function(x) {
return function(y) {
return function(z) {
return x + y + z;
}
}
}
}
let f = add();
//测试用例
console.log(f(1)(2)(3));
有点洋葱一层一层剥下你的心那味了。这样将函数柯里化未免呆板,其实可以将柯里化单独写个方法,可以返回一个正常函数柯里化后的函数,lodash库中也提供了这个方法,方法名叫curry。 下面用lodash库中的curry给大家写几个柯里化案例(调库真方便)
//柯里化实现字符串匹配数字
const { method } = require('lodash');
const lodash = require('lodash');
let match = lodash.curry((arg, str) => str.match(arg));
//let findSpace = match(/\s+/g);
let findNumber = match(/\d+/g);
let filter = lodash.curry((method, str) => (lodash.filter(str, method)))
let filterNumber = filter(findNumber);
let str = "guaqiu52"
//看见了吧!是一个参数
console.log(filterNumber(str));
柯里化方法通用实现如下
const curry=function(fn){
return function curryFn(...args){
if(args.length<fn.length){
return function(){
return curryFn(...args,...arguments)
}
}else{
return fn(...args);
}
}
}
vue.js源码使用柯里化的地方:src/platform/web/patch.js 日后我也会在vue源码讲解部分给大家说说。
组合
纯函数之间是可以合并成为一个更强大的函数,直接上案例!
function f1() {
console.log(1);
}
function f2() {
console.log(2);
}
//组合实际上是对多个纯函数执行的包装。
function compose(f1, f2) {
f1();
f2();
}
compose(f1, f2);
ladash库中也提供了组合的方法:
flow 从左到右执行
flowRight 从右到左执行(更常用一些)
这里要提一下,如果使用flowRight 参与组合每个子纯函数可以有多个参数吗? 答案是只有最右边的函数可以有多个参数。 为什么?flowRight是从右到左执行函数,并且将前一个函数的执行结果作为参数给当前函数,大家可以将这一过程抽象为一个管道(即我的输出是你的输入),所以我们无法给他传参,(就像正常人也不能将自来水管的自来水变成可乐一样),最右边的函数(即第一个执行的子纯函数)他是没有函数给他传值,需要我们给它传值,那就可以随心所欲给他传多个参数了。(就像你如果负责给自来水管供水,如果有一天你偷偷把水加点可乐,那我们的水就变成可乐味了)
ok!丢个案例我就溜了,函数式编程我也是刚接触,需要大牛们勘误指导,我讲的我觉得也是浅浅的那一层,函子什么的也没介绍,或许等我彻底通透,心中自有丘壑的那一天才能真正讲透函数式编程。
const { method } = require('lodash');
const lodash = require('lodash');
const reverse = arr => arr.reverse();
const first = arr => arr[0]
const toUpper = s => s.toUpperCase();
const LastToUpper = lodash.flowRight(toUpper, first, reverse);
console.log(LastToUpper(['z', 'j', 'l']));