理解JavaScript纯函数(Pure Function )
- 函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念
- 在react开发中,纯函数被多次提及
- 比如react中组件就被要求像是一个纯函数(为什么是像,因为还有class组件),redux中有一个reducer的概念,也是要求必须是纯函数
- 所以掌握纯函数对理解很多框架的设计是非常有帮助的
- 纯函数的维基百科定义:
- 在程序设计中,若一个函数符合以下条件,那么这个函数就被称为纯函数
- 此函数在相同的输入值时,需产生相同的输出
- 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关
- 该函数不能有语义上可观察的函数副作用,诸如"触发事件",使输出设备输出,或更改输出值以外物件的内容等
- 总结:
- 确定的输入,一定会产生确定的输出
- 函数在执行过程中,不能产生副作用
副作用的理解
- 这里有一个概念,叫做副作用,什么叫做副作用呢?
- 副作用(side effect)其实本身是医学的一个概念,比如我们经常说吃什么药本来是为了治病,可能会产生一些其他的副作用
- 在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值以外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储
- 纯函数在执行的过程中就是不能产生这样的副作用:
- 副作用是产生bug的温床
纯函数的案例
- 我们来看一个对数组操作的两个函数:
- slice:slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组
- splice:splice截取数组,会返回一个新的数组,也会对原数组进行修改
- slice就是一个纯函数,不会修改传入的参数
纯函数-柯里化-组合
- slice函数只要是确定的输入,就会产生确定的输出
- slice在执行的时候,不会产生副作用(没有修改外部的变量,也没有修改传入的参数)
var names = ["小余",'小满','骚满','Tom'] //slice只要给它传入一个start/end,那么对于同一个数组来说,它会给我们返回确定的值 //slice函数本身是不会修改原来的数组 //slice -> this var newNames1 = names.slice(0,2) console.log("newNames1",newNames1); console.log("names",names); //splice是会修改原来的数组对象本身的,所以它不是纯函数 var newNames2 = names.splice(2) console.log("newNames2",newNames2); console.log("names",names);
- 纯函数练习
//非纯函数,传入的值被修改了 function baz(info){ info.age = 100 } var obj = {name:"小满",age:23} baz(obj) console.log(obj) //{name: '小满', age: 100} //test是否是一个纯函数?是 function test(info){ return{ ...info, age:100 } } test(obj ) //React的函数组件(类组件) function HelloWorld(props){ } <HelloWorld info="{}"/>
纯函数的优势
- 为什么纯函数在函数式编程中非常重要呢?
- 因为你可以安心的编写和安心的使用
- 你在写的时候保证函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改
- 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定有确定的输出
- React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的orops不被修改:
JavaScript柯里化
- 柯里化也是属于函数式编程里面一个非常重要的概念
- 维基百科解释:
- 在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化
- 是把接收多个参数的函数,变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数,而且返回结果的新函数的技术
- 柯里化声称"如果你固定某些参数,你将得到接受余下参数的一个函数"
- 柯里化总结:
- 只传递给函数一部分参数来调用它,让它返回另一个函数处理剩下的参数
- 这个过程称为柯里化
//假设我们有一个需要填入4个参数的 函数 function foo(m,n,x,y){ } foo(10,20,30,40) //柯里化的过程 //我们对其进行转化,变得只需要传入一个参数,但这里面需要返回一个函数继续处理剩下的参数 function bar(m){ return function(n){ return function(x,y){ //你也可以将y参数继续return m+n+x+y } } } bar(10)(20)(30,40)
柯里化的结构
//正常结构 function add(x,y,z){ return x+y+z } var result = add(10,20,30) console.log(result); //柯里化 function sum(x){ return function(y){ return function(z){ return x+y+z } } } var result1 = sum(10)(20)(30) console.log(result1); //简化柯里化代码 var sum2 = x=>y=>z=>{ return x+y+z } //还能再次简化var sum2 = x=>y=>z=>x+y+z var result2 = sum2(20)(30)(40) console.log(result2,"使用箭头函数简化柯里化的方式")
柯里化的作用
- 那么为什么需要有柯里化呢?
- 在函数式编程中,我们其实往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个函数来处理
- 那么我们是否就可以将每次传入的参数在单一的函数中进行处理,处理完后在下一个函数中再使用处理后的结果
单一职责原则(SRP) 面向对象 -> 类 -> 尽量只完成一件单一的事情
柯里化 - 单一职责的原则
//全部挤在一起处理 function add(x,y,z){ x = x + 2 y = y * 2 z = z * z return x + y +z } console.log(add(10,20,30)); //柯里化处理 function sum(x){ x = x + 2 return function(y){ y = y * 2 return function(z){ z = z * z return x + y + z } } } console.log(sum(10)(20)(30));
柯里化 - 逻辑的复用
function foo(m,n){ return m + n } console.log(foo(5,1)) console.log(foo(5,2)) console.log(foo(5,3)) console.log(foo(5,4)) console.log(foo(5,5))//第一个数一直都是不变的,但是我们每次都是需要重复输入,使用柯里化就能实现逻辑上的复用了 function makeAdder(count){ return function(num){ return count + num } } var adder5 = makeAdder(5) console.log(adder5(1));//重复的逻辑就直接复用了 console.log(adder5(2)); console.log(adder5(3)); console.log(adder5(4)); console.log(adder5(5));
案例2
//打印日志时间 function log(date,type,message){ console.log(`[${date.getHours()}:${date.getMinutes()}][${type}]:[${message}]`) } log(new Date(),'DEBUG','查找到轮播图的bug')//[22:24][DEBUG]:[查找到轮播图的bug] log(new Date(),'DEBUG','查询菜单的bug')//[22:24][DEBUG]:[查询菜单的bug] log(new Date(),'DEBUG','查询数据的bug')//[22:24][DEBUG]:[查询数据的bug] --------------------------------------------------------------------------------------------- //柯里化优化 var log = date => type => message =>{ console.log(`[${date.getHours()}:${date.getMinutes()}][${type}]:[${message}]`) } //如果我打印的都是当前的时间,我们就可以将时间复用 var nowLog = log(new Date()); nowLog("DEBUG")("查找小满去哪了")//[22:32][DEBUG]:[查找小满去哪了] //或者时间+类型都全部复用 var nowLog1 = log(new Date())("小满系列查找"); nowLog1("查找小满人去哪了")//[22:34][小满系列查找]:[查找小满人去哪了] nowLog1("查找小满的黑丝去哪了")//[22:34][小满系列查找]:[查找小满的黑丝去哪了] nowLog1("查找小满的裤衩子被谁拿走了")//[22:34][小满系列查找]:[查找小满的裤衩子被谁拿走了] nowLog1("查找小满有没有去按摩店找小姐姐")//[22:34][小满系列查找]:[查找小满有没有去按摩店找小姐姐]
柯里化函数的实现
实现将正常普通的函数转换成柯里化函数,这样要怎么实现呢?
- 传入一个函数,返回一个function
- 想要获取参数的个数方式
function foo(x,y,z,q){ console.log(foo.length)//4 } foo()
function add1(x, y, z) { return x + y + z } function hyCurrying(fn){ function curried(...args){ //1.当已经传入的参数 大于等于 需要的参数时,就执行函数 if(args.length >= fn.length){ //不使用fn(...args)这种方式,可能会发生this指向问题 return fn.apply(this,args)//如果使用call的话,args就需要加上... //原因是apply第二个参数本身就是数组,所以直接args输出,但call函数第二个参数是一个一个的,需要扩展出来 }else{ //当出现hyCurrying(10)(20)(30)这种极端情况的时候,我们就需要再返回新的函数来接收参数 function curried2(...args2){//由于我们不知道要接收多少参数,这里还是需要... //接收到参数后,需要递归调用curried来检查函数的个数是否达到 //将第一个curried参数跟curried2的参数进行拼接 return curried.apply(this,args.concat(args2)) } return curried2 } } return curried } var curryAdd = hyCurrying(add1) console.log(curryAdd(10,20,30)); console.log(curryAdd(10,20)(30)); console.log(curryAdd(10)(20)(30)); //可能在一个里面将参数全部接收hyCurrying(10,20,30) //也可能分开接收hyCurrying(10,20)(30) //也可能全部分开hyCurrying(10)(20)(30)
理解组合函数
- 组合(Compose)函数是在JavaScript开发过程中一种对函数的使用技巧、模式:
- 比如我们现在需要对某一个数据进行函数的调用,执行两个函数fn1和fn2,这两个函数是依次执行的;
- 那么如果每次我们都需要进行两个函数的调用,操作上就会显得重复
- 那么是否可以将这两个函数组合起来,自动依次调用呢?
- 这个过程就是对函数的组合,我们称之为 组合函数(Compose Function);
function double(num){ return num*2 } function square(num){ return num ** 2//平方 } var count = 10 var result = square(double(count)) console.log(result); //如何将double和square结合起来,实现简单的组合函数 function composeFn(m,n){ return function(count){ return n(m(count)) } } var newFn = composeFn(double,square) console.log(newFn(10));
通用组合函数的实现
- 刚才我们实现的compose函数比较简单,我们需要考虑更加复杂的情况:比如传入了更多的函数,在调用 compose函数时,传入了更多的参数:
function hyCompose(...fns){ var length = fns.length for(var i = 0;i < length;i++){ if(typeof fns[i] !== 'function'){ throw new TypeError('要求都是函数类型')//new出一个异常的错误,抛出异常 } } function compose(...args){ var index = 0 //fns[index].apply(this,args):取出来fns第一个函数进行apply调用,并将args参数都传递进去。注意,我们是直接使用fns而不是...fns哦 var result = length ? fns[index].apply(this,args) : args while(++index < length){ result = fns[index].call(this,result) } return result } return compose } function double(m){ return m*2 } function square(n){ return n ** 2 } var newFn = hyCompose(double,square) console.log(newFn(30));
基于对象的封装、原型链
JavaScript额外知识补充
with语句
- with语句 扩展一个语句的作用域链。
- 不建议使用with语句,因为它可能是混淆错误和兼容性问题的根源。
var obj = { name:"Hello World", age:18 } with(obj){//会形成自己的作用域 console.log(name) console.log(age) }
eval函数的
- eval是一个特殊的函数,它可以将传入的字符串当做JavaScript代码来运行。
- 不建议在开发中使用eval:
- eval代码的可读性非常的差(代码的可读性是高质量代码的重要原则);
- eval是一个字符串,那么有可能在执行的过程中被刻意篡改,那么可能会造成被攻击的风险;
- eval的执行必须经过JS解释器,不能被JS引擎优化;
var evalString = `var message = "Hello World;console.log(message)"` eval(evalString) console.log(message)
认识严格模式
- 在ECMAScript5标准中,JavaScript提出了严格模式的概念(Strict Mode):
- 严格模式很好理解,是一种具有限制性的JavaScript模式,从而使代码隐式的脱离了 ”懒散(sloppy)模式“;
- 支持严格模式的浏览器在检测到代码中有严格模式时,会以更加严格的方式对代码进行检测和执行;
- 严格模式对正常的JavaScript语义进行了一些限制:
- 严格模式通过 抛出错误 来消除一些原有的 静默(silent)错误;
- 严格模式让JS引擎在执行代码时可以进行更多的优化(不需要对一些特殊的语法进行处理);
- 严格模式禁用了在ECMAScript未来版本中可能会定义的一些语法;
开启严格模式
- 那么如何开启严格模式呢?严格模式支持粒度话的迁移:
- 可以支持在js文件中开启严格模式
- 也支持对某一个函数开启严格模式;
- 严格模式通过在文件或者函数开头使用 "use strict " 来开启
"use strict"//开启严格模式 //使用let作为标识符的名称 var name = "abc" console.log(name) //定义变量时不使用var var message = "Hello World" console.log(message)
function foo(){//在函数内开启严格模式 "use strict"; m = "foo" console.log(m) } foo()
严格模式限制
- 这里我们来说几个严格模式下的严格语法限制:
- JavaScript被设计为新手开发者更容易上手,所以有时候本来错误语法,被认为也是可以正常被解析的
- 但是这种方式可能给带来留下来安全隐患
- ;
- 在严格模式下,这种失误就会被当做错误,以便可以快速的发现和修正
- 无法意外的创建全局变量
- 严格模式会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常
- 严格模式下试图删除不可删除的属性
- 严格模式不允许函数参数有相同的名称
- 不允许0的八进制语法
- 在严格模式下,不允许使用with
- 在严格模式下,eval不再为上层引用变量
- 严格模式下,this绑定不会默认转成对象
//常见的限制 //1.以外创建全局变量,不会生效而是报错 message = "Hello World" console.log(message); //同样的在严格模式下会报错 function foo(){ age = 18 } foo() console.log(age); //2.不允许函数有相同的参数名称 function foo(x,y,x){//两个x就是相同参数名称,如果不开启严格模式,后面的x会将前面的x覆盖掉 console.log(x,y,x); } foo(10,20,30)//30,20,30(非严格模式) //3.静默错误 true.name = "xiaoyu" NaN = 123//非严格模式下不会报错 var obj = {} Object.defineProperty(obj,'name',{ configurable:false,//不可配置 writable:false,//不可写 value:"why" }) console.log(obj.name) obj.name = "xiaoyu"//静默错误,因为我们已经设置不可写入了 //4.不允许使用原先的八进制格式(严格模式) var num = 0123//八进制 var num2 = 0x123//十六进制 var num3 = 0b100//二进制 console.log(num,num2,num3)//Uncaught SyntaxError: Octal literals are not allowed in strict mode //5.eval函数不会向上引用变量 var jsString = "var message = 'Hello World';console.log(message)" eval(jsString) console.log(message)//这里会报错
严格模式下的this
"use strict" //之前编写的代码中,自执行函数我们是没有使用过this直接去引用window的 function foo(){ console.log(this) //通常在自执行函数里面我们想要调用window中的name属性的时候,我们不使用this.name localStorage.setItem//localStorage也会指向window } foo()//正常情况下this指向window,当开启了严格模式后,自执行函数(默认绑定)会指向undefined //setTimeout的this setTimeout(()=>{ console.log(this) },2000)//window setTimeout(function(){ console.log(this) },2000)//非严格模式下是window,严格模式下依旧是window,而不是undefined //this指向window,且是自执行函数,为什么不会是undefined呢?那是因为里面可能执行了一次fn.apply(window),手动指向了this //这个是在浏览器中实现的(伪造fake出来的setTimeout),而不是在v8引擎中实现的