JavaScript高级笔记-coderwhy版本(四)

简介: JavaScript高级笔记-coderwhy版本

JavaScript高级笔记-coderwhy版本(三)https://developer.aliyun.com/article/1469639


this规则之外

  • 我们讲到的规则已经足够应付平时的开发了,但是总有一些语法,超出我们的规则之外。

特殊绑定--忽略显示绑定

apply、call、bind:当传入null/undefined时,自动绑定成全局对象

function foo(){
    console.log(this);
}
foo()
foo.apply(null)
foo.apply(undefined)
//打印出来全部都是window,我们可以看到填入 null跟undefined打印出来的也是全局的对象

特殊绑定--间接函数引用

  • 另外一种情况,创建一个函数的间接引用,这种情况使用默认绑定规则
  • 赋值(obj2.foo = obj1.foo)的结果时foo函数
  • foo函数被直接调用,那么是默认绑定

第二种情况是一种独立函数调用,将obj2.foo = obj1.foo作为一个整体来调用。这种情况叫做间接引用,我们并没有直接拿到这个函数,而是通过obj2.foo = obj1.foo这个表达式来返回函数,然后对这个函数做一个调用。这种情况也属于独立函数的调用

//争论:代码规范,到底加不加分号;
var obj1 = {
    name:"这是onj1",
    foo:function(){
        console.log(this);
    }
}
var obj2 = {
    name:"这是obj2",
}
obj2.foo = obj1.foo
obj2.foo()//{ name: '这是obj2', foo: [Function: foo] }
//第二种情况,比较难的情况
(obj2.foo = obj1.foo)()

第二种情况特殊情况(了解就行,一般没人这么写)


如果我们不再obj2对象结束那里加上分号的话,编辑器会连带这下面的调用当作一个整体,这是语法分析的一个问题

var obj2 = {
    name:"这是obj2",
}
//会将obj2对象连着下面调用当作一个整体
//第二种情况,比较难的情况
(obj2.foo = obj1.foo)()
----------------------------------
//相当于变成如下情况
var obj2 = {
    name:"这是obj2",
}(obj2.foo = obj1.foo)()
//会报错:Uncaught TypeError: Cannot set properties of undefined (setting 'foo')
---------------------------------------
//加上分号后:
var obj2 = {
    name:"这是obj2",
};
(obj2.foo = obj1.foo)()
//正常返回window

测试代码(来自你不知道的JavaScript)

function foo(el){
    console.log(el,this);
}
var obj = {
    id:"I am is XiaoYu"
}
[1,2,3].forEach(foo,obj)
//无法运行
//报错:Uncaught TypeError: Cannot read properties of undefined (reading 'forEach')
---------------------------------------
//解决方法1:
function foo(el){
    console.log(el,this);
}
var obj = {
    id:"I am is XiaoYu"
}
var names = [1,2,3]
names.forEach(foo,obj)
------------------------------------------
//解决方法2:
function foo(el){
    console.log(el,this);
}
var obj = { 
    id:"I am is XiaoYu"
};//加上分号,不然会将obj对象和底下的当作一个整体
[1,2,3].forEach(foo,obj)//foo是我们在上面独立定义了,obj是我们传入forEach中this要绑定的对象

箭头函数arrow function

  • 箭头函数是ES6之后增加的一种编写函数的方法,并且它比函数表达式要更加简洁:
  • 箭头函数不会绑定this、argument属性
  • 箭头函数不能作为构造函数来使用(不能和new一起来使用,会抛出错误)

箭头函数的使用解析

编写箭头函数


():参数


=>:箭头


{}:函数执行体(在一些特殊场景大括号可以省略 )

//方式1:
var nums = [10,20,30,40]
nums.forEach((num1,num2,num3)=>{
    console.log(num1,num2,num3)
})
----------------------------------------------------------
//方式2(完整写法):
 var foo = (num1,num2,num3)=>{
    console.log(num1,num2,num3)
}
var nums = [10,20,30,40]
nums.forEach(foo)

箭头函数常见简写方法

//简写1:如果参数只有一个,小括号可以省略
//简写前:
nums.forEach((item)=>{
    console.log(item)
})
//简写后:
nums.forEach(item=>{
    console.log(item)
})
//简写2:如果执行体只有一个,大括号可以省略
nums.forEach(item => console.log(item))
//强调:并且它会默认将这行代码的执行结果作为返回值
var newNums = nums.filter(item => item % 2 === 0)//item % 2 === 0的结果会默认返回
console.log(newNums)
//一般情况下我们带大括号的是需要手动return返回的,就像这样
var newNums = nums.filter(item => {
    return item % 2 === 0//这种有大括号的情况下,如果我们不return的话,会返回[]
})

filter/map/reduce结合使用


用一行完成了对初始值的:过滤出偶数并将其每个偶数扩大100倍,使其相加

var nums = [10,20,30,40,51]
// filter/map/reduce结合使用
var result = 
    nums.filter(item => item % 2 === 0)
        .map(item => item *100)
        .reduce((preValue,item)=>preValue+item)
    console.log(result)
//简写3:如果一个箭头函数,只有一行代码,并且返回一个对象,这个时候如何编写简写
var bar = ()=>{
    return {
        name:"小余",
        age:20
    }
}
//如果你按照上面的简写思路的话,那应该是
var bar = ()=> {name:"小余",age:18}//但这种写法其实是错误的,因为这里会发生混乱,这个大括号到底是判定为执行体还是对象呢?JS引擎会发生错乱
//正确的简写方式
var bar = ()=> ({name:"小余",age:18})
//使用小括号将对象包裹起来,这个是将对象当作一整个整体

箭头函数的this获取

箭头函数不会绑定this


  • 为什么3种不同方式的调用都是window,首先那是因为我们foo的上层作用域是全局的,全局的可不就是window,然后就是我们的call怎么没有改变成功this的指向呢?那是因为箭头函数的原因,箭头函数不会绑定this属性,而我们的foo函数恰巧使用了箭头函数,造成了所有的绑定效果都是指向window,这是很有用的一个特点
var name = "小余"
var  foo = ()=>{
    console.log(this);
}
foo()//window
var obj = {foo:foo}
obj.foo()//window
foo.call("这是call调用的")//window

有无使用箭头函数的this对比

//对比前,没有使用箭头函数
var name = "小余"
function foo(){
    console.log(this);
}
// foo()
var obj = {name:"你已经被小余绑定到obj上啦",foo:foo}
obj.foo()//{ name: '你已经被小余绑定到obj上啦', foo: [Function: foo] }
-----------------------------------------------------------------------------------------------
//对比后,使用箭头函数
var name = "小余"
var  foo = ()=>{
    console.log(this);
}
var obj = {name:"你已经被小余绑定到obj上啦",foo:foo}
obj.foo()//window

箭头函数应用场景

发送网络请求,将结果放到上面data属性中


在没有箭头函数的时候,我们通过在getData中创建一个变量将getData内的this的指向进行一个接收来进行使用


有了箭头函数就不需要这么麻烦,因为箭头函数没有绑定this,所以我们在调用obj.getData的时候,this不会被隐式绑定给强行改变,没有被改变的话,当this在当层找不到想要的就会直接去自己的上层找

//无箭头函数时候
var obj = {
    data:[],
    getData:function(){
        //在没有箭头函数的时候,大家通常是这么解决问题的
        var _this = this//这里的this就是obj对象了,getData的上一层可不就是obj
        setTimeout(function(){//没有使用箭头函数,会出现问题,所以要加上var _this = this,然后使用_this
            var result = ["小余",'小满','康老师']
            _this.data = result//_this是外层的变量,这里就形成了一个闭包
            console.log(this)
        },2000)
    }
}
obj.getData()//没有使用箭头函数或者没有声明一个变量来接收getData里面的this的时候,为什么是window,那是因为foo函数绑定到obj上面啦,obj的上层就是全局window了,这是隐式绑定
//有箭头函数的时候
var obj = {
    data:[],
    getData:function(){
        setTimeout(()=>{
            var result = ["小余",'小满','康老师']
            this.data = result//直接使用this
            console.log(this)//通过直接打印this进行检测
        },2000)
    }
}
obj.getData()

this面试题

面试题1

//无答案解析版本
var name = "window"
var person = {
    name:"person",
    sayName:function(){
        console.log(this.name);//这里的答案是谁
    }
};
function sayName(){
    var sss = person.sayName
    sss();//调用打印出来的是什么
    person.sayName();//?
    (person.sayName)();//?
    (b = person.sayName)();//?
}
sayName()
//答案解析版本
var name = "小余window"
var person = {
    name:"person",
    sayName:function(){
        console.log(this.name);
    }
};
function sayName(){
    var sss = person.sayName
    sss();//this.name是小余window ,独立函数调用,所以这里的this指向最外层的window
    person.sayName();//隐式调用,this指向person,控制台打印的this.name是person
    (person.sayName)();//person,隐式调用
    (b = person.sayName)();//间接函数引用,是独立的函数调用,所以是小余window,(b = person.sayName)是一个整体
}
sayName()

面试题2

var name = 'window'
//person1是字面量对象
var person1 = {//定义对象的时候是不会产生作用域的,所以对象里面的上层在对象外面
  name: 'person1',
  foo1: function () {
    console.log(this.name)
  },//普通函数
  foo2: () => console.log(this.name),//箭头函数
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },//函数套函数,返回普通函数
  foo4: function () {
    return () => {
      console.log(this.name)
    }
  }//函数套函数,返回箭头函数
}
var person2 = { name: 'person2' }
// person1.foo1(); // person1(隐式绑定)
// person1.foo1.call(person2); // person2(显示绑定优先级大于隐式绑定)
// person1.foo2(); // window(不绑定作用域,上层作用域是全局)
// person1.foo2.call(person2); // window
//这里的person1.foo3()的调用下拿到结果在()继续调用,这种属于独立调用 
// person1.foo3()(); // window(独立函数调用)
// person1.foo3.call(person2)(); // window(独立函数调用)
// person1.foo3().call(person2); // person2(最终调用返回函数式, 使用的是显示绑定)
// person1.foo4()(); // person1(箭头函数不绑定this, 上层作用域this是person1)
// person1.foo4.call(person2)(); // person2(上层作用域被显示的绑定了一个person2)
// person1.foo4().call(person2); // person1(上层找到person1)

面试题3

  • 连续new了两次,代表构造函数会被连续调用两次
  • 每次new的时候都会创建一个新的对象,这里new了两次表示创建了两个新的对象


var person1 = new Person('person1')
var person2 = new Person('person2')
//创建出来的2个新对象:
this = {name:"person1",foo1:function{}}
this = {name:"person2",foo1:function{}}
var name = 'window'
function Person (name) {//作为构造函数,一般情况下,我们都开头字母大写
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  },
  this.foo2 = () => console.log(this.name),
  this.foo3 = function () {
    return function () {
      console.log(this.name)
    }
  },
  this.foo4 = function () {
    return () => {
      console.log(this.name)
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.foo1() // person1
person1.foo1.call(person2) // person2(显示高于隐式绑定)
person1.foo2() // person1 (上层作用域中的this是person1)
person1.foo2.call(person2) // person1 (上层作用域中的this是person1)
person1.foo3()() // window(独立函数调用)
person1.foo3.call(person2)() // window
person1.foo3().call(person2) // person2
person1.foo4()() // person1
person1.foo4.call(person2)() // person2
person1.foo4().call(person2) // person1
var obj = {
  name: "obj",
  foo: function() {
  }
}

面试题4

通常我们会对什么时候调用感到疑惑,例如下面这两个



  • 他们的区别从foo2开始发生不同,foo2.xxx表示到了foo2还没调用,而是继续深入到里面
  • foo2()则是调用了,然后foo2属性对应的将会生效替代foo2()部分,变为foo2().call(person2)
  • 在下面的表达式中,foo2()属性调用则是return了一个箭头函数,既然return了,那就跳出外面一层function了,且箭头函数是不受call改变this的,this的指向当然就是obj咯(return出来的函数的上一层或者说父级作用域就是obj函数),所以this.name自然就是obj对象里面的name:obj了
person1.obj.foo2.call(person2)() // person2
person1.obj.foo2().call(person2) // obj
var name = 'window'
function Person (name) {
  this.name = name
  this.obj = {//对象里面封装对象
    name: 'obj',
    foo1: function () {
      return function () {//普通返回
        console.log(this.name)
      }
    },
    foo2: function () {
      return () => {//箭头函数返回
        console.log(this.name)
      }
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.obj.foo1()() // window
person1.obj.foo1.call(person2)() // window
person1.obj.foo1().call(person2) // person2
person1.obj.foo2()() // obj
person1.obj.foo2.call(person2)() // person2
person1.obj.foo2().call(person2) // obj
// 
// 上层作用域的理解
// var obj = {
//   name: "obj",
//   foo: function() {
//     // 上层作用域是全局
//   }
// }
// function Student() {
//   this.foo = function() {
//   }
// }

_06函数的柯里化

实现apply、call、bind

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

call函数的手写实现

给函数上面添加一个叫做hycall方法的方式:


  1. 只能给某个函数添加方法
  • foo.hycall = function(){}


  1. 给所有的函数添加一个hycall的方法
  • Funtion.prototype.hycall = function(){}


Function.prototype.hycall = function(){
    console.log("原型链调用了");
}
function foo(){
    console.log("foo函数调用了");
}
foo.hycall()//原型链调用了
----------------------------------------------------------
//我们在执行foo.hycall的时候会发现只执行了hycall函数,但是却把原本foo函数自带的信息给掩盖掉了,这肯定是不合理的,我们是要在原有的基础上进行调用,而不是另起大厦

改良写法

//差的写法,缺乏复用性
Function.prototype.hycall = function(){
    console.log("原型链调用了");
    foo()
}
function foo(){
    console.log("foo函数调用了");
}
foo.hycall()
//好的写法,可以多次复用
//本身我们在进行调用的时候,就相当于已经是隐式绑定了,foo.hycall()的时候,this的绑定就已经绑到foo上面了
Function.prototype.hycall = function(){
    console.log("原型链调用了");
    var fn = this
    fn()
}
function foo(){
    console.log("foo函数调用了");
}
foo.hycall()

怎么实现给函数传递参数

我们往call函数内传递参数,会在调用的this中打印出来,但是在我们自己手写的函数里面,打印出来的却是windows,如何解决这个问题?

Function.prototype.hycall = function(thisArg){
    var fn = this
    //调用需要被执行的函数
    thisArg.fn = fn
    thisArg.fn()
    //等函数执行完之后删掉这个属性
    delete thisArg.fn
}
function foo(){
    console.log(this);
}
foo.hycall({name:"小余",age:"20"})//{ name: '小余', age: '20', fn: [Function: foo] }

此时手写的函数已经可以传递参数了,但能传递的仅仅是对象,如果传递数字会报错,接下来我们就解决这个问题


//使用call函数传递数字参数的话
foo.call(123)//[Number: 123]
//对thisArg转成对象类型(防止传入非对象类型报错)
thisArg = thisArg ? Object(thisArg) : window

call除了第一个参数是用来改变this绑定的,后面还可以绑定一堆参数,这个放到我们自己实现要怎么去做呢?在之前是可以使用argument,现在ES6中有更好的解决方法了

ES6中的剩余参数

对于接收多少个参数我们是不确定的,那这个时候,我们形参的部分就不能够写死了,不然是写不尽写不完的,你也不能够100%猜到用户想输入几个参数

function sum(...args){
    //打印出来的args是数组形式 
    console.log(args)
    //展开运算符spread
    console.log(...args)//得到的直接是数值
}

很显然通过ES6中的...的方式来解决,接着进到最后的一步,返回结果

Function.prototype.hycall = function(thisArg,...args){
    // console.log("传递参数进来了噢",this);
    var fn = this
    //对thisArg转成对象类型(防止传入非对象类型报错)
    thisArg = thisArg ? Object(thisArg) : window
    //调用需要被执行的函数
    thisArg.fn = fn
    var result = thisArg.fn(...args)
    //等函数执行完之后删掉这个属性
    delete thisArg.fn
  //返回结果
    return result
}
function foo(){
    console.log(this);
}
foo.hycall(123,6,66,666,6666)
//==========================================>换个函数执行一下
function foo(num1,num2,num3){
    console.log("foo的this指向是",this,"三数相加的结果=",num1+num2+num3);
}
foo.mycall("小余",500,20,1)
//foo的this指向是 String {'小余', fn: ƒ} 三数相加的结果= 521

apply函数的手写实现

//自己实现hyapply
Function.prototype.myapply = function(thisArgs,argArray){//区别在于这里不需要ES6的...运算,因为传入的是一整个数组
    var fn = this
    thisArgs = thisArgs ? Object(thisArgs) : window
    thisArgs.fn = fn
    var arr = thisArgs.fn(...argArray)//但是数组需要解构出来
    delete thisArgs.fn
    return arr
}
function sum(num1,num2){
    console.log("sum被调用",this,num1,num2);
}
var result = sum.myapply("小余",[200,30])
console.log(result,'老铁666');

在call函数的基础上进行小范围修改,貌似已经满足了apply函数的要求了,但是当我们除了第一个用于this指定之外,其他参数我们不传就会出现问题了


  • 因为当我们不传递参数的时候,argArray就会是undefined,我们在解构的时候就会变成...undefined了,对underfunded进行扩展是错误的
  • 这个时候我们就可以进行一个判断来解决这个问题
  • 那为什么在call函数的手写的时候没有遇到这个问题呢?因为我们在call函数中的形参是...args,那这东西的格式就是个数组,你什么都不传都默认是空数组[]
Function.prototype.myapply = function(thisArgs,argArray){
    var fn = this
    thisArgs = thisArgs ? Object(thisArgs) : window
    thisArgs.fn = fn
  argArray = argArray || []//不止这种写法,也可以使用三元运算符
    var arr = thisArgs.fn(...argArray)
    delete thisArgs.fn
    return arr
}
function sum(num1,num2,num3){
    console.log("sum被调用",this,num1,num2+num3);
}
var result = sum.myapply("小余")
console.log(result,'老铁666');

手写call和apply的补充

在我们改变this指向的时候,输入0的时候,会指向window,这是由于我们三元运算符那些的写法导致的,也有其他情况可以避免,这样是对边界效应的一种延伸考虑

thisArg = (thisArg !== undefined && thisArg !== null) ? Object(thisArg) : window

bind函数的手写实现

首先我们来看看JS中bind传参数的3种方式,第一种传值方式跟call的方式很像,从第二种跟第三种开始,跟其他两个函数(call、apply)发生了不一样的变化,我们来看看他们是怎么实现的吧

//方式1:在bind中传值
function foo(num1,num2,num3,num4){
    console.log(this,num1,num2,num3,num4);
}
var bar = foo.bind('小余',10,20,30,40)
bar()
//方式2:在接收bind的bar中传值
function foo(num1,num2,num3,num4){
    console.log(this,num1,num2,num3,num4);
}
var bar = foo.bind('小余')
bar(10,20,30,40)
//方式3:方式1跟方式2的结合,从方式1到方式2中按顺序传递
function foo(num1,num2,num3,num4){
    console.log(this,num1,num2,num3,num4);
}
var bar = foo.bind('小余',10,20)
bar(30,40)
//以上三种方式的答案都是:String {'小余'} 10 20 30 40

我们是有变量接收值的,就像上方的bar来接收,对于这种情况就需要在手写的函数中最后进行一个return返回

Function.prototype.mybind = function(thisArg,...argArray){
    function proxyFn(){
    }
    return proxyFn
}

首先我们揭秘第二种方式是怎么做到的,也就是得到了值还能继续往里面传值,那是因为我们手写bind函数返回的还是一个函数,而里面这个函数是可以接收值的,所有自然就造成这种情况了,第三种则是手写bind函数跟bind函数内部的函数都可以接收值,然后再做一个拼接就完成了

Function.prototype.mybind = function(thisArg,...argArray){
    //1.获取真实要调取的函数
    var fn = this
    //对特殊情况的处理
    thisArg = (thisArg !== undefined && thisArg !== null) ? Object(thisArg) : window
    function proxyFn(...args){
        //将函数放到thisArg中进行调用
        thisArg.fn = fn
        //对传入的两个参数进行合并
        var finalArgs = [...argArray,...args]
        var result = thisArg.fn(finalArgs)
        delete thisArg.fn
        //返回结果
        return result
    }
    return proxyFn
}
function foo(num1,num2,num3,num4){
    console.log(this,num1,num2,num3,num4);
}
var bar = foo.bind('小余',10,20)
bar(30,80)

认识arguments

  • argument是一个对应于 传递给函数的参数类数组(array-like)对象
  • 类数组对象什么意思?就是长得像数组,但实际上是个对象
  • 我们在实参传递的个数如果超过形参的数量的话,多余的不是丢弃掉,而是跟着前面其他几个参数一起放到argument中了
  • array-like意味着它不是一个数组类型,而是一个对象类型:
  • 但是它却拥有数组的一些特性,比如说length,比如说可以通过index索引来访问
  • 但是它却没有数组的一些方法,比如forEach、map等等
//argument的基础使用
function foo(num1,num2,num3){
    // console.log(arguments);
    //常见对argument的3个操作
    //1.获取参数长度
    console.log(arguments.length);
    //2。根据索引值获取某一个参数,像数组一样的操作
    console.log(arguments[1]);
    //3.callee属性,获取argument中所在的函数
    console.log(arguments.callee);
}   
foo(10,20,30,40,50)

argument转数组

function foo(num1,num2,num3,num4){
    //1.自己遍历
    var newArr = []
    for(var i = 0;i < arguments.length;i++){
        newArr.push(arguments[i] *40)
    }
    console.log(newArr);
    //2.arguments转成array数组类型
    //2.1自己遍历arguments中所有的元素
    //2.2 使用slice
    var newArr2 = Array.prototype.slice.call(arguments)
    console.log(newArr2,"这是newArr2");
    //这里其实跟2.2是一样的,this显示绑定arguments大于隐式绑定
    var newArr3 = [].slice.call(arguments)
    console.log(newArr3)
    //2.3 ES6的语法
    var newArr4 = Array.from(arguments)
    console.log(newArr4)
    //展开运算符
    var newArr5 = [...arguments]
    console.log(newArr5)
}
foo(1,2,3,4)

数组里的slice实现

//补充:在原型链上加函数的方法,我们确实可以在每个函数上面都调用,但是当我们想要调用这个函数的方法本身的时候,就略显麻烦
Function.prototype.aaa = function(){
    //xxxx
}
Function.prototype.aaa()//这样调用
//自己简单实现的slice
Array.prototype.hyslice = function(start,end){//这里可以进行优化,看用户是否有传递进来这两个参数,没有的话我们就做一个判断处理
    var arr = this
    start = start || 0
    end = end || arr.length
    var newArray = []
    for(var i = start;i < end ;i++){
        newArray.push(arr[i])//就将this里的东西给填入进去,而this已经被我们手动改成我们想要的指向了
    }
    return newArray
}
var newArray = Array.prototype.hyslice.call(["小余","大余","超大余"],1,3)//相当于使用call来调用slice,我们上方函数里的this指向就被改变到我们手写的这个数组内容上了
console.log(newArray);

箭头函数-无arguments

我们在箭头函数中是没有arguments的,如果你需要的话,js会去上层作用域里面找


向下方这个,arguments到上层作用域,也就是全局作用域中寻找,这全局作用域是分两种情况的,在node中是有的,在浏览器中则是没有(显示你没有定义)

var name = "小余"
var foo = ()=>{
    console.log(name)
    console.log(arguments);
}
foo()

案例

在ES6中用...剩余函数来替代arguments,用来接收所有参数,形成数组

function foo(){
    var bar = ()=>{
        console.log(arguments);//打印出来的就是上层foo的arguments
    }
    return bar
}
var fn = foo(123)
fn()

07对象字面量和对象的封装

理解JavaScript纯函数(Pure Function )

  • 函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念
  • 在react开发中,纯函数被多次提及
  • 比如react中组件就被要求像是一个纯函数(为什么是像,因为还有class组件),redux中有一个reducer的概念,也是要求必须是纯函数
  • 所以掌握纯函数对理解很多框架的设计是非常有帮助的
  • 纯函数的维基百科定义:
  • 在程序设计中,若一个函数符合以下条件,那么这个函数就被称为纯函数
  • 此函数在相同的输入值时,需产生相同的输出
  • 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关
  • 该函数不能有语义上可观察的函数副作用,诸如"触发事件",使输出设备输出,或更改输出值以外物件的内容
  • 总结:
  • 确定的输入,一定会产生确定的输出
  • 函数在执行过程中,不能产生副作用

副作用的理解

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

纯函数的案例

  • 我们来看一个对数组操作的两个函数:
  • slice:slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组
  • splice:splice截取数组,会返回一个新的数组,也会对原数组进行修改
  • slice就是一个纯函数,不会修改传入的参数

纯函数-柯里化-组合

  1. slice函数只要是确定的输入,就会产生确定的输出
  2. 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)


JavaScript高级笔记-coderwhy版本(五)https://developer.aliyun.com/article/1469641

目录
相关文章
|
16天前
|
前端开发 JavaScript
JavaScript新科技:PostCSS的安装和使用,2024年最新2024网易Web前端高级面试题总结
JavaScript新科技:PostCSS的安装和使用,2024年最新2024网易Web前端高级面试题总结
|
17天前
|
JavaScript 前端开发
web前端JS高阶面试题(1),高级开发工程师面试
web前端JS高阶面试题(1),高级开发工程师面试
|
19天前
|
JSON JavaScript 前端开发
js的版本
【5月更文挑战第7天】js的版本
13 1
|
19天前
|
JavaScript 前端开发 Linux
|
19天前
|
JavaScript 前端开发
JavaScript高级主题:什么是 ES6 的解构赋值?
【4月更文挑战第13天】ES6的解构赋值语法简化了从数组和对象中提取值的过程,提高代码可读性。例如,可以从数组`[1, 2, 3]`中分别赋值给`a`, `b`, `c`,或者从对象`{x: 1, y: 2, z: 3}`中提取属性值给同名变量。
19 6
|
19天前
|
存储 JavaScript 前端开发
JavaScript高级主题:JavaScript 中的 Map 和 Set 是什么?它们有什么区别?
JavaScript的ES6引入了Map和Set数据结构。Map用于存储键值对,适合通过键进行查找,而Set则存储唯一值,无键且不支持键查找。两者在性能上表现出色,尤其在频繁的写入删除操作中。选择使用哪个取决于具体应用场景:键值对需求选Map,独特值集合则选Set。
21 2
|
19天前
|
JavaScript 前端开发 Web App开发
JavaScript基础语法(codewhy版本)(一)
JavaScript基础语法(codewhy版本)
95 1
JavaScript基础语法(codewhy版本)(一)
|
19天前
|
JavaScript 前端开发 Java
JavaScript高级笔记-coderwhy版本(六)
JavaScript高级笔记-coderwhy版本
77 0
|
19天前
|
JavaScript 前端开发 Java
JavaScript高级笔记-coderwhy版本(五)
JavaScript高级笔记-coderwhy版本
103 0
|
19天前
|
JavaScript 前端开发 Dart
JavaScript高级笔记-coderwhy版本(三)
JavaScript高级笔记-coderwhy版本
64 0
JavaScript高级笔记-coderwhy版本(三)