JS05 - JavaScript 函数 - 声明、默认参数、变量提升、重名问题、作用域、闭包、防御式、截断式、断点调试、递归

360 阅读23分钟

函数概念

  • 函数:function,又被称为子程序,面向对象时称之为实现某些功能的方法(method),例如:console.log(),这里的log()就是console这个对象里面所包含的其中一个方法,这个方法的功能便是函数的log()函数的功能
  • 本质:将一段代码予以封装,以便于以后重用(反复使用)
  • 对于 JavaScript 来说,函数就是把任意一段代码放在一个盒子里面,当想要让这段代码执行的时候,就直接执行这个盒子里面的代码就可以了

函数声明

声明式 - 命名函数

  • 使用 function 关键字声明一个函数
  • 声明式,可以先调用后定义
  • 语法
function fn(){
    //code......
}

赋值式 - 匿名函数

  • 也称为“函数表达式”创建方式,这种创建方式是匿名函数的创建方法,即使用 var 定义一个变量,把一个函数当作值直接赋值给这个变量
  • 赋值式,不能先调用后定义
  • 语法
var fn = function(){
    //code......
}
//不需要再 function 后面书写函数的名字了,因为在前面已经有了

声明式VS赋值式

test001()   //正常
// test002()  //报错,赋值式不能先调用后声明

function test001(){
    console.log("test001")
}

var test002 = function (){
    console.log("test002")
}
test001()   //正常
test002()   //正常

自执行

  • 自执行函数是把创建函数和执行函数放在一起完成,具体做法是通过两个小括号实现
  • 第一个小括号:一是存放函数,二是为了解决语法报错问题,而除了使用小括号()之外,还可以使用 ~ + - ! 让语法正确
  • 第二个小括号:执行函数,也是从这里传递实参
(function(...params){
    console.log("()"+params)
})(11.12,12)
//()11.12,12
//----------------------------

~function(...params){
    console.log("~"+params)
}(12,50)
//~12,50
//----------------------------

+function(){
    console.log("+",arguments)
}()
//+ Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ]
//----------------------------

-function(...params){
    console.log("-"+params)
}(0,1,2)
//-0,1,2
//----------------------------

!function(...params){
    console.log("!",params)
}()
//! []

箭头函数 ES6 - Lambda 表达式

  • 箭头函数:ES6中创建函数的新方式,箭头函数与普通函数只是形式不同,本质相同,类似于 Java 中的 Lambda 表达式。
  • 基本语法:将 () => {} 赋值给一个变量
  • 省略小括号:只有一个参数
  • 省略大括号:仅仅返回一个值,可以省略 return 和大括号
  • 剩余运算符:没有内置arguments,需要使用剩余运算符
  • this指向父级:普通函数 this 指向运行时所在的对象,箭头函数的 this 指向定义时所在的对象(父级作用域对象,在哪里出生就指向谁,可以解决计时器的this指向问题),因此不能当作构造函数使用
//基本语法
var variable = () => { 
    console.log("sonething...");
}

//省略小括号:如果箭头函数只传递一个参数,可以省略括号
var variable = x => {  
    console.log("x is " + x);
}

//省略大括号和return:只有一句代码(这句话就是return,并且大括号省略,return 就必须省略,否则报错)
var fn = x => x+1

//剩余/展开运算符:箭头函数是没有自带的arguments集合的,只能使用剩余运算符,并且就是数值,不用转换
var fn001 = () => console.log(arguments)
var fn002 = (...params) => console.log(params)
// fn001(10,20)    //ReferenceError: arguments is not defined
fn002(10,20)    //[10,20]

//this指向父级:解决计时器的this指向问题
//example 1
function normalFn() {
    let count;
    count = setInterval(function () {
        clearInterval(count);
        console.log("count - 计时器ID值 - " + count);
        console.log(this);
    }, 1000)
}
function normalFnThat(){
    let count;
    let that = this;    //强制改变this指向
    count = setInterval(function(){
        clearInterval(count);
        console.log("count - 计时器ID值 - " + count);
        console.log(that);
    },1000);
}
function narrowFn() {
    let count;
    count = setInterval(() => {
        clearInterval(count);
        console.log("count - 计时器ID值 - " + count);
        console.log(this);
    }, 1000)
}
let objA = {
    skill: "as a coder A",
    work: normalFn
};
let objB = {
    skill: "as a coder B",
    work: normalFnThat
}
let objC = {
    skill: "as a coder C",
    work: narrowFn
};
objA.work(); // 1 Window 对象
objB.work(); // 2 {skill: 'as a coder', work: ƒ} 对象
objC.work(); // 3 {skill: 'as a coder', work: ƒ} 对象
//example 2
<body>
    <input id="tx_1" type="button" value="待确认No.1" />
    <input id="tx_2" type="button" value="待确认No.2" />
    <input id="tx_3" type="button" value="待确认No.3" />
</body>
<script>
    var value = "window's value";
    tx_1.onclick = function () {
        let countId = setTimeout(function () {
            clearTimeout(countId);
            console.log(this.value);    //window's value
        }, 1000);
    }
    //解决办法 1 :使用that = this
    tx_2.onclick = function () {
        let that = this;
        let countId = setTimeout(function () {
            clearTimeout(countId);
            console.log(that.value);    //待确认No.2
        }, 1000);
    }
    //解决办法 2 :使用箭头函数
    tx_3.onclick = function () {
        let countId = setTimeout(() => {
            clearTimeout(countId);
            console.log(this.value);    //待确认No.3
        }, 1000);
    }
</script>
  • 练习 - 将普通函数改写为箭头函数
/* 原函数 */
function fn(x){
    return function(y){
        return x+y;
    }
}
/* 改写 - 箭头函数 */
var fnNarrow = x => y => x+y
console.log(fn(2))
console.log(fnNarrow(2))
/**
 * 原函数输出:
 * ƒ (y){
 *      return x+y;
 *  }
 * 箭头函数输出:y => x+y
 */

函数参数

形参和实参

  • 在定义函数和调用函数的时候都出现过 (),这个 () 就是用来放置参数的,参数主要分为 形参实参 两种
//形参和实参
//声明式
function fn([形参]) {
    //code...
}
fn([实参])

//赋值式
function fn([形参]){
    //code...
}
fn([实参])

内置实参集合

  • 函数内置实参集合(arguments):执行函数的时候一般会传递实参值,但是传递多少个实参不确定,但我们又需要接受函数实时接收到的实参信息。而在以前传递实参的方法中,是需要需要知道传递实参数量顺序的,比如要传递3个值,那么就需要设置3个形参,并且这3个值的顺序也是需要明确的,但如果遇到不知道会传来多少个实参,也不知道先传什么后传什么,我们就需要采用一定的方法来解决这个问题,因此,我们可以通过自带的函数内置实参集合来实现。函数内置实参集合不论是否传递以及传递多少实参,在改集合中包含了所有传递进来的信息,如果不传递那就是一个空集合。
function fn(){
    console.log(arguments)
}
fn();
/**输出结果:
 *Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ]
 *  callee: ƒ fn()
 *  length: 0   这里说明没有传递实参,arguments是空集合
 *  Symbol(Symbol.iterator): ƒ values()
 *  __proto__: Object
 */
fn(10);
/** 
 *Arguments [10, callee: ƒ, Symbol(Symbol.iterator): ƒ]
 *  0: 10
 *  callee: ƒ fn()
 *  length: 1
 *Symbol(Symbol.iterator): ƒ values()
 *__proto__: Object
 */
 fn(10,20,30);
/** 
 *Arguments(3) [10, 20, 30, callee: ƒ, Symbol(Symbol.iterator): ƒ]
 *  0: 10
 *  1: 20
 *  2: 30
 *  callee: ƒ fn()
 *  length: 3	有length属性的不是数组就是类数组,从这里可以看出并不是Array类的实例,因此这里可知arguments集合是一个类数组
 *  Symbol(Symbol.iterator): ƒ values()
 *  __proto__: Object
 */
  • 从上述实例,可知,函数就不用设定形参了,arguments 是传递进来的所有实参的集合。
  • 如果注册了DOM事件,那么arguments将会包含有点击事件对象的参数
function de(x){
    console.log(arguments);
}
de(1);   //Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ]callee: ƒ de()length: 0Symbol(Symbol.iterator): ƒ values()__proto__: Object

let dc = function dd(){
    console.log(arguments);
}
btn.onclick = dc;   //Arguments [MouseEvent(鼠标事件对象), callee: ƒ, Symbol(Symbol.iterator): ƒ]

//在闭包测试中同样遇到这种情况
function defendShake(){
    let timer = null;
    return function(){
        clearTimeout(timer);
        timer = setTimeout(()=>{
            Array.from(arguments).forEach(item=>{
                console.log(arguments) 
                //Arguments [MouseEvent, callee: ƒ, Symbol(Symbol.iterator): ƒ]
            })
        },500);
    }
}
function fn1(){console.log("123")};
function fn2(){console.log("456")};
btn.onclick = defendShake(fn1,fn2);

剩余运算符

  • 剩余运算符:在ES6规则中,采用了...[剩余运算符]来表示传递的实参集合,一般会与params结合书写...params(params也可以任意自定义,例如,写成argus、data、digits等等,相当于一个数组名)。之所以称作剩余运算符,是在某些时候,会先制定几个确定的形参来接受实参,如果有更多的实参传递进来,则放进与剩余运算符关联的params集合中,这样就保证了确定的实参和多余的实参都能够获取到。因此,如果形参变量没有定义,则所有传递的实参信息都存到这个集合中,同时要注意使用剩余运算符所的集合是一个“数组”,并不是“类数组”
function fn01(...params){
    console.log(params)
}
fn01()      //[]
fn01(10)    //[10]
//----------------------
function fn02(...data){
    console.log(data)
}
fn02(10)    //[10]
//----------------------
function fn03(x,...digits){
    console.log(digits)
}
fn03(10,20,30)  //[20,30]

实参判断

  • 在实际项目中,一般会设置相应的函数对形参进行默认值处理,即当执行函数时,传入的实参值没有给定,而此时又没有给定的实参值,便会将其设置为 undefined,为了减少报错,就需要对参数进行判断处理
var fn = x => {
    x === undefined ? console.error(`x is ${x}`) : console.log(`OK,${x}`)
}
fn()    //x is undefined
fn("James")    //OK,James

形参默认值 ES6

  • 问题1 代码描述:函数实现功能,如果传入一个数字,就输出这个数字,否则输出函数默认指定的数字
//如果传入了数字就输出该数字,否则输出默认的 10
function checkNum(num){
    num = num || 10;
    console.log(num);
};
checkNum(5);    //5
checkNum(0);    //10 -> 原生函数对于默认值的bug

//ES6 形参默认值 改进
function checkNum(num = 10){
    console.log(num);
};
checkNum(5);    //5
checkNum(0);    //0
  • 问题2 对传入的实参进行判断,确实能够减少报错的情况,但实参依然还是undefined,因此,在ES6中有更好的解决办法,直接给形参设置默认值,当没有传入实参的时候,就能避免undefined的情况了
  • ES6形参赋值默认值:(规则)设定形参不传递值,走等号后面的默认值,但是一旦传递值了,不论传递什么都不会走默认值。
var average = (x = 0,y = 0) => {
    return (x+y)/2
}
console.log(average(20,10))  //15

回调函数

  • 回调:把函数作为参数,传递给另外一个函数,作为参数的函数就被称为回调函数
function isPositive(element){
    return element > 0
}
function filter(arr,isPositive){
    for (let index = 0; index < arr.length; index++) {
        if(isPositive(arr[index])){
            console.log(`${arr[index]} is positive.`)
        } // else none to do...
    }
}
filter([10,-10,20],isPositive)
//10 is positive.
//20 is positive.

回调函数.png

  • 示例:Array对象的forEach()方法就用到了回调函数,该方法的作用是遍历数组的元素,并依次执行将该方法中的回调函数。下面通过重写该方法说明。
Array.prototype.forEach = function forEach(callback){
    //遍历数组
    for(let i=0;i<this.length;i++){
        callback(this[i],i,this);
    }
    //每个元素依次执行回调函数
}
//回调函数
function callback(item, index, arr){
    console.log(`[${arr}] : ${index} - ${item}`);
}

//测试
let arr = ["one",2,"3","four"];
//重写前:
arr.forEach(callback);  //[one,2,3,four] : 0 - one [one,2,3,four] : 1 - 2 [one,2,3,four] : 2 - 3 [one,2,3,four] : 3 - four
//重写后:
arr.forEach(callback);  //与重写前相同

预解析(变量声明提升)

  • JavaScript 是一个解释型语言,就是在代码执行之前,先对代码进行通读和解释,然后再执行代码,即JavaScript代码在运行的时候,会经历两个环节 解释代码执行代码
  • 解释代码就是在所有代码执行之前进行整体地通读和解释,也称为 预解析(预解释),需要解释的内容有两个:声明式函数、var 关键字
  • 注意:变量提升/预解析,只会在一个script中进行提升,即不能跨脚本提升
sum(18,90)  //108 因为预解析,且预解析的是整个声明式的函数
// ave(18,90)  //TypeError:ave is not a function 预解析的是变量ave,因为是赋值的可能是函数,但此时还没有赋值(赋值在执行代码阶段),所以结果就不是undefined,而是not a function
console.log(num)    //undefined 预解析的是变量num,此时还未赋值
//---------------------------
function sum(x=0,y=0){  //声明式函数
    console.log(x+y)
}
var ave = (x=0,y=0) => console.log((x+y)/2)  //var关键字(赋值式函数、箭头函数)
var num = 90;
//---------------------------
sum(18,90)  //108
ave(18,90)  //54
console.log(num)    //90

重名问题

变量名重复

//变量名重复
//1. 后者覆盖前者
var name = "James"
var name = "Charles"
console.log(name); //Charles 
//--> TIPS:下一句以小括号开头,上一行没有终止符,console.log可能会报错:console.log(...) is not a function,因此需要在console.log()后面加上分号
//2. 就近原则
(function(name){
    var name = "Hello"
    console.log(name)   //Hello
})("James")

函数名重复

taken()     //taken002 --> 声明预解析(声明提升)后也是一样,后者覆盖前者

function taken(){ console.log("taken001") }
function taken(){ console.log("taken002") }

taken()     //taken002 --> 后者覆盖前者

变量名与函数名重复

  • 结论先行:变量名与函数名不能重复,因为从本质上来说,函数名是一种特殊的变量名
/**
 * 预解析阶段(变量提升)
 * var name;
 * function name(){ console.log("Charles") }
 * --> 在此阶段输出name:因为在变量提升之后的顺序中,函数的位置在变量位置后面,因此此时的name是输出函数name()
 * --> 在此阶段调用name(),便是执行函数,而不是调用变量
 */
console.log(name)   //ƒ name(){ console.log( "Charles") } --> 声明预解析(变量提升),函数name()提升之后的位置在变量name的位置后面,因此,这里的name是函数name()
console.log(name()) //Charles 和 undefined --> 由于这里的name是函数name(),因此执行name()便能够得到函数name()的输出值
//-----------------在前面调用(预解析/变量提升)----------------//
var name = "James"
function nam(){ console.log( "Charles") }
//-----------------在后面调用(赋值阶段)----------------//
/**
 * 赋值阶段
 * name = "James"
 * --> 在此阶段输出name: 因为函数已经在预解析阶段提升了,从而赋值阶段便只有变量被赋值,因此,输出就是变量name的值
 * --> 输出name就是变量name的值,执行name()相当于是把变量name作为函数在执行,则会报错
 */
console.log(name)   //James
console.log(name()) //Uncaught TypeError: name is not a function --> 说明这里的name并不是函数name(),而是变量name,所以,在开发中要注意变量尽量不与函数名相同,避免报错

作用域

  • 基本概念:一个变量可以生效的范围 --> 变量不是在所有地方都可以使用,而可以使用的范围就是作用域

静态 VS 动态

  • 静态作用域/词法作用域(static scope/lexical scope):一种根据语言文本的位置确定变量引用的规则,其作用域是定义函数的时候就已经决定了,即看的是书写位置,而不是看调用位置,JavaScript 就是静态作用域
  • 动态作用域:不关心函数和作用域是如何声明以及在何处声明的,它们只关心在哪里调用,就是看调用位置,书写位置不重要
var lexicalValue = 100
function foo(){
    console.log(lexicalValue);
}
function tool(){
    var lexicalValue = 200
    foo()
}
tool()
//静态作用域 -> 100 (JavaScript)
/**
 * 原理:静态作用域看定义位置
 * foo() 与 tool() 一样,定义的位置都是全局的下一级,
 * 此时,执行 tool() 便会执行 内部调用的 foo() ,
 * 而此时的 foo() 定义位置并不在 tool() 函数内部,
 * 那么 foo() 中的 lexicalValue 变量就要向上级查找,
 * 发现在全局作用域中有声明的 lexicalValue 变量,便直接拿来输出
 */
//动态作用域 -> 200
/**
 * 原理:动态作用域看调用位置
 * 不管 foo() 的定义位置,此时 foo() 是在 tool() 函数中调用的,
 * 其上级作用域则属于 tool(),那么 foo() 在向上级查找 lexicalValue 时,
 * 就是在 tool() 函数中的定义的 lexicalValue
 */

全局作用域

  • 全局作用域是最大的作用域,即在全局作用域中定义的变量可以在任何地方使用,包括可以跨script使用
  • JavaScript 所有数据变量都属于 window 对象,变量在函数外定义,即为全局变量,因此页面开启,浏览器会自动生成一个全局作用域 window,这个作用域直到页面关闭前都会一直存在

局部作用域

  • 局部作用域是在全局作用域下面开辟出来的相对小一些的作用域,在局部作用域中定义的变量只能在这个局部作用域内部使用
  • JavaScript 中只有函数能够生成一个局部作用域,即变量在函数内声明,则变量为局部作用域,包括函数形参
function myFunction() {
    var carName = "Volvo"; // 局部变量: carName
}
/*---------------------*/
//形参是局部变量
function fn(x){
    console.log(x);
}
fn(1)   //1
console.log(x);   //Uncaught ReferenceError: x is not defined
//将 x 不设置为形参,而是在函数体中直接赋值,则会向上级查找,如果外层都没有声明x,会定义为全局变量
function fun(){
    x = 1
    console.log(x)
}
fun()  //1
console.log(x);   //1

变量(作用域)访问规则

  • 访问:获取变量值的行为
  • 规则:
    1. 顺序:首先,在自己的作用域内部查找,如果有,就直接拿来使用;其次,如果没有找到,就去上一级作用域查找,如果有,就拿来使用;再次,如果还是未找到,就继续去上一级作用域查找,以此类推;最后,如果到全局作用域也没有这个变量,就报错 is not defined
    2. 注意:变量访问只能向上级作用域访问,不能向下访问
var num1 = 100
var num2Fn = () => {
    var num2 = 200
    var num3Fn = () => {
        var num3 = 300
        //从局部作用域开始查找
        console.log(num1);  //100
        console.log(num2);  //200
        console.log(num3);  //300
    } 
    num3Fn()
}
num2Fn()
//直接从全局作用域开始查找
console.log(num1);  //100  全局作用域
// console.log(num2);  //num2 is not defined  变量访问规则,只能向上查找
// console.log(num3);  //num3 is not defined  变量访问规则,只能向上查找

变量(作用域)赋值规则

  • 赋值前提:要给变量赋值,就需要先找到这个变量
  • 赋值规则:
    1. 首先,在自己作用域内部查找,有就直接赋值,其次,如果没有,就在上一级作用域查找,有就赋值,以此类推,如果一直找到全局作用域都没有进行过声明,那么就把这个变量定义为全局变量,再进行赋值
    2. 作用域为全局的变量可以作为window对象的属性进行调用、修改和删除,但一种特殊情况只能调用、修改,但不能删除:
      • 如果是在全局有明确声明的(例如关键词var),window只能调用,不能删除
function name(){
    name = "James"
    var age
    function age(){
        age = 25
        return age
    }
    console.log(name,age());
}
name()              //James 25
console.log(name);  //James
console.log(age);   //Uncaught ReferenceError: age is not defined
/**变量赋值规则
 * 1. name() 调用后,要给name和age分别赋值James和25
 * 2. 那么name和age就会先在自己的作用域内部查找是否有name和age变量
 * 3. 可见,name和age在自己的作用域内都没有发现已经声明的变量
 * 4. name的上一级作用域是全局作用域,age的上一级作用域是name()函数
 * 5. 此时,对于name而言,仍然没有已经声明的变量,因此,name被定义为全局变量
 * 6. 同时,对于age而言,在name()函数作用域内找到了声明,因此调用name()函数才可访问
 * 7. 注意:赋值的时候查找变量是就近原则的,因此要注意上一级相同变量名的变量值被覆盖
 */
//通过关键词明确声明变量
var host = "James"  
//未通过关键字声明变量,而是通过作用域赋值规则将其创建为全局变量
range = 26
~function college(){
    collegeSchool = "Peking University"
}()


//window 调用
console.log(window.host);   //James
console.log(window.range);  //26
console.log(window.collegeSchool);  //Peking University

//window 配置-修改
window.host = "Char"
window.range = 30
window.collegeSchool = "Qinghua"
console.log(window.host);   //Char
console.log(window.range);  //30
console.log(window.collegeSchool);  //Qinghua

//window 配置-删除
delete host
delete range
delete collegeSchool

console.log(window.host);   //Char --> 说明未被删除
console.log(window.range);  //undefined --> 被删除
console.log(window.collegeSchool);  //undefined --> 被删除

变量生命周期

  • JavaScript 变量的生命期从它们被声明的时间开始
  • 局部变量:会在函数运行以后被删除
  • 全局变量:会在页面关闭后被删除

闭包

  • 闭包(closure):(狭义)就是 函数套函数,内部函数使用了外部函数定义的局部变量
  • 解决问题:变量放在全局不安全,容易被篡改;也可以让临时变量永驻内存
  • 弊端:性能问题,内存泄漏/溢出(如果在调用一个闭包赋值给全局变量,那么就会有一个作用域释放不掉,如果使用的闭包过多,就会导致内存溢出,因为在全局声明的变量直到程序结束才会被释放)

广义的闭包

var number = 0;         //声明在函数外部的变量
function count(){
    return number++;    //在函数中使用外部声明的变量
}

狭义的闭包

function getCounts(){
    //变量声明在另一个函数内部
    var number = 0;             
    //函数表达式作为一个方法返回,这个返回的方法是还没有执行的,
    //由此,number的声明周期就被延长了,
    //因为,如果count()函数中没有使用number,那么,
    //number 就会在getCounts()被调用结束后被销毁(出栈)
    //但是,getCounts()执行完成后,返回的是一个函数(还未被调用),
    //那么,number因为count()只是被返回,而没有被调用,
    //所以,number的声明周期依然还在
    return function count(){
        return ++number;
    }
}
console.log(getCounts());
//输出:
// ƒ count(){    
//   return number++;    
// }
console.log(getCounts()());  //作为返回值的函数,采用自调用方式 
//输出:
//1
console.log(number()) //报错 - Uncaught ReferenceError: number is not defined
//变量因为作用域,不容易被全局篡改         function getCounts(){
    //变量声明在另一个函数内部
    var number = 0;             
    //函数表达式作为一个方法返回,这个返回的方法是还没有执行的,
    //由此,number的声明周期就被延长了,
    //因为,如果count()函数中没有使用number,那么,
    //number 就会在getCounts()被调用结束后被销毁(出栈)
    //但是,getCounts()执行完成后,返回的是一个函数(还未被调用),
    //那么,number因为count()只是被返回,而没有被调用,
    //所以,number的声明周期依然还在
    return function count(){
        return ++number;
    }
}
console.log(getCounts());
//输出:
// ƒ count(){    
//   return number++;    
// }
console.log(getCounts()());  //作为返回值的函数,采用自调用方式 
//输出:
//1
console.log(number()) //报错 - Uncaught ReferenceError: number is not defined
//变量因为作用域,不容易被全局篡改 

柯里化函数

  • 柯里化(Currying):是把接受多个参数的函数变成接受一个单一参数(最初函数的第一个参数)的函数,并且接受余下的参数且返回最终结果的新函数的技术。
  • 核心思想:是一种预先存储的函数思想,首先把一些信息存储到某一个不被释放的上下文中,而后基于所用域链的机制,让其"下级上下文"访问到存储的信息值。
/**举例说明 - 函数柯里化:
 *      let total = fun(10,20)(30,40);
 *      console.log(total);
这里,执行fun是为了把一些信息事先存储起来(例如,10、20),
 * 然后执行函数返回的小函数,这个过程中就传递另外的一些值(例如,30、40) ,
 * 同时,小函数执行的时候,还会拿到之前存储的值(例如,10、20),
 * 最后获取所有数值的和*/
function funDeclare() {
    let arr, result;
    //outerArgs: 存储的是预先存储的值(例如,预先存储10、20)
    let outerArgs = Array.from(arguments);
    return function proxy() {
        //innerArgs: 小函数执行传递进来的信息值(例如,执行小函数时,传递的30、40)
        let innerArgs = Array.from(arguments);
        arr = outerArgs.concat(innerArgs);
        /**Array.reduce()方法 -- 执行过程:
         *      第一轮:preValue ->10 curValue ->20 return ->30
         *      第二轮:preValue ->30 curValue ->30 return ->60
         *      第三轮:preValue ->60 curValue ->40 return ->100 
         *      此时数组遍历完,最后一轮返回的值,作为reduce最终的返回值即可 */
        arr.reduce((preValue, curValue) => {
            return result = preValue + curValue;
        });
        return result;
    }
}

//将上述声明式函数简化为箭头函数
let funNarrow = (...outerArgs) => (...innerArgs) => outerArgs.concat(innerArgs).reduce((pre,cur)=>pre+cur);

console.log(funDeclare(10, 20)(30, 40))
console.log(funNarrow(10, 20)(30, 40))

compose 组合函数

  • 核心思想:compose 是基于柯里化函数基础上的一种特殊应用场景,在函数式编程中起到非常大的作用,实际上就是把处理数据的函数像管道一样连接起来,然后让数据穿过管道得到最终的结果。例如:现在需要对某一个数据进行函数的调用,执行两个函数fn1和fn2,而如果每次调用时,都要操作两次,就会显得重复;因此,将这两个函数像管道一样组合起来,自动依次调用,就构成了组合函数(Compose Function)。
// ===========================================
/**举例说明(组合函数):
 *      const adv = a => a+1;
 *      const mis = m => m*3;
 *      const don = d => d/2;
 *      don(mis(adv(2)));   //4.5
 * 这样的写法明显太冗余了,因此,可以构建一个compose函数,
 * 让它接受任意多个函数作为参数(而这些回调函数都只接受一个参数),
 * 然后compose返回的也是一个函数。达到以下的效果:
 *      const compose = opera(adv, mis, don);
 *      operator(0);    //相当于 don(mis(adv(0)))
 *      operator(2);    //相当于 don(mis(adv(2)))
 * 由此可见,compose 把类似于 fn1(fn2(fn3(x))) 的这种写法简化成了 compose(fn1,fn2,fn3)(x)
 * 要实现这一功能,首先就需要将fn1、fn2、fn3等回调函数,事先存储起来,确定好执行顺序(这个过程就是 柯里化),
 * 其过程就是,分别把某个函数执行的返回值作为“实参”传递给下一个函数,最后实现函数的嵌套
 * 而compose组合函数本质上,就是把这个过程扁平化了
 */ 
const adv = a => a+1;
const mis = m => m*3;
const don = d => d/2;
const compose = (...params) => {
    //...params: 预先存储的就是未来要执行的函数集合 
    //          -> 柯里化要做好预先存储,就是要明确函数执行顺序。
    //              例如,这里就是 [adv,mis,don] 的顺序,当要执行operatCompose([实参])的时候,
    //              会按照事先先存储的函数执行顺序,以此执行
    //x: 作为operaCompose函数的形参,是参与回调函数计算的最原始参数
    return x => {
        //遍历执行传进来的回调函数 -> reduceRight()让数组元素从右向左遍历
        return params.reduceRight((pre,cur)=>{
            return cur(pre);
            //第一轮:pre -> 最右边的
        },x)
    }
}
//原函数调用:
console.log(don(mis(adv(2))));
//组合函数调用:
let operatCompose = compose(don,mis,adv);
console.log(operatCompose(2));

// ===========================================
/**组合函数改写练习: 
 * const fn1 = x => x+65;
 * const fn2 = x => x*30;
 * const fn3 = x => x/20;
 */

 //原始函数:
const fn1 = x => x+65;
const fn2 = x => x*30;
const fn3 = x => x/20;
//原始函数调用:
console.log(fn3(fn2(fn1(10))));     //112.5

//compose组合函数:
const composeFn = (...params) => {
    return x =>{
        //扁平化柯里化
        return params.reduceRight((result,item)=>{
            return item(result);   
        },x);
    }
}
let operatComposeFn = composeFn(fn3,fn2,fn1);
console.log(operatComposeFn(10));   //112.5

示例 - 标记列表索引

<ul id="ul">
    <li><button>1</button></li>
    <li><button>2</button></li>
    <li><button>3</button></li>
    <li><button>4</button></li>
</ul>
<script type="text/javascript">
    var lis = ul.querySelectorAll("li");
    for(var i = 0;i < lis.length;i++){
        //通过闭包,让 lis[i] 绑定的事件函数是相互独立的,
        //闭包的返回值不会被释放,因此每次循环所绑定的事件函数就被保留下来了
        //为了让事件绑定闭包函数,需要让外层函数自执行,同时为了获取到下标,需要传入下标的值
        //注意 -> 自执行性函数使用 ! + - ~ 四种符号,不能有效传递回调函数给事件,因此建议使用 ()
        lis[i].onclick = (function(index){
            return function(){
                console.log(index);
            };
        })(i);
    };
</script>

示例 - 优化 fetch

//不使用闭包 - fetch请求
fetch("http://localhost:5500/aaa").then(res => res.json()).then(res => console.log(res));
fetch("http://localhost:5500/bbb").then(res => res.json()).then(res => console.log(res));
fetch("http://localhost:5500/ccc").then(res => res.json()).then(res => console.log(res));
fetch("http://localhost:5500/ddd").then(res => res.json()).then(res => console.log(res));

//使用闭包 - fetch请求 -> 函数柯里化的体现
function fetchFunction(url) {
    return function (path) {
        return fetch(url + path);
    }
}
let fetchFn = fetchFunction("http://localhost:5500/");
fetchFn("aaa").then(res => res.json).then(res => console.log(res))
fetchFn("bbb").then(res => res.json).then(res => console.log(res))
fetchFn("ccc").then(res => res.json).then(res => console.log(res))
fetchFn("ddd").then(res => res.json).then(res => console.log(res))
fetchFn = null; //闭包需要释放内存,否则累积过多会导致内存溢出

示例 - 优化 jsonp

<input type="text" id="inp" />
<ul id="lists"></ul>
<script type="text/javascript">
    inp.oninput = (function () {
        var timer = null;
        return function () {
            if (timer) {
                clearTimeout(timer);
            } //else none
            timer = setTimeout(() => {
                var inpContent = this.value;
                if (inpContent < 1) {
                    lists.innerHTML = "";
                    return;
                }
                var jsonpScript = document.createElement("script");
                jsonpScript.src = `https://www.baidu.com/sugrec?pre=1&p=3&ie=utf-8&json=1&prod=pc&from=pc_web&sugsid=36556,37495,37841,37765,37866,37800,37760,37850,26350,37790&wd=${inpContent}&req=2&csor=14&pwd=1&cb=jQuery110206459462907160005_1669888754740&_=1669888754754`;
                document.body.appendChild(jsonpScript);
                jsonpScript.remove();
            }, 500);
        }
    })();

    function jQuery110206459462907160005_1669888754740(obj) {
        lists.innerHTML = obj.g.map(item => `<li>${item.q}</li>`).join("");
    }
</script>

优化jsonp.gif

防御式和截断式

  • 防御式编程:(防错误)在函数主体代码执行之前首先检查传入的参数是否适合该函数
//示例 - 防御式编程
function isPositive(num){
    if (isNaN(num)) {
        return "非数字,无法判断";
    } else {
        return num > 0
    }
}
console.log(isPositive(90))         //true
console.log(isPositive("90"))       //true
console.log(isPositive("ninety"))   //非数字,无法判断
  • 截断式编程:(简单化)将一个复杂问题中较为简单的部分剥离出来,优先处理
//示例 - 截断式编程
function login(name,psw){
    if (name == null || psw == null) {
        return "登录请输入"
    } //else to do none ...

    //other function...
}
console.log(login());    //登录请输入

断点调试

函数如果需要断点调式,有两个关键功能键:

  • F11 进入函数内部
  • Shift 跳出当前函数

递归

  • 递归调用:在方法体中调用方法自己
  • 优点:精简代码,例如,二分查找
  • 缺点:占用栈资源,导致运行效率从低,可能造成堆栈溢出(stack overflow)
function getFibonacci(a, b){
    // 递归函数一定要有终止条件
    if(b>10){
        return a+b
    }else{
        getFibonacci(b,a+b) //Fibonacci数列的核心 --> 递归调用(调用自身)
    }
}   //在函数执行结束时,a,b 都逐渐恢复到初始参数的值
console.log(getFibonacci(0,1))  //undefined --> 递归最终返回值是最初那一层的返回值,因此递归如果要返回值,最好是用第三个变量承接

//正确写法
var sum; //接受递归返回值
function getFibonacci(a, b){
    // 递归函数一定要有终止条件
    if(b>10){
        sum = a+b
    }else{
        getFibonacci(b,a+b) //Fibonacci数列的核心 --> 递归调用(调用自身)
    }
    return sum;
}   //在函数执行结束时,a,b 都逐渐恢复到初始参数的值,因为递归调用是压栈,结束函数,要等全部出栈才会结束
console.log(getFibonacci(0,1))  //21

示例练习

任意数求和

// version 1.0 声明式函数
function sum(){
    var result = 0;
    for(i=0;i<arguments.length;i++){
        result += Number(arguments[i])
    }
    isNaN(result)?console.log("Error"):console.log(result);
}
sum()           //0
sum(10,10)      //20
sum(10,"10")    //20
sum(10,"十")    //Error
sum(10,10.2,-45) //-24.8
// version 2.0 箭头函数
var sum = (...params) => {
    var result = 0;
    var i = 0;
    for(;i<params.length;){
        result += Number(params[i])
        i++
    }
    isNaN(result)?console.log("Error"):console.log(result)
}
sum()           //0
sum(10,10)      //20
sum(10,"10")    //20
sum(10,"十")    //Error
sum("10",10.2,-45) //-24.8
// version 3.0 箭头函数+forEach方法
var sum = (...params) => {
    result = 0
    params.forEach(function(value){
        result += Number(value)
    });
    isNaN(result)?console.log("error"):console.log(result)
}
sum(10,20)          //30
sum(10,-20)         //-10
sum(10,-20,"-82")   //-92
sum(10,"十",20)     //error
//version4.0 优化forEach方法中的函数为箭头函数
var sum = (...params) => {
    result = 0;
    params.forEach(value => {
        result += Number(value)
    })
    isNaN(result)?console.log("error"):console.log(result)
}
sum(10,20)          //30
sum(1.20,-20)       //18.8
sum(10,"-82",-20)   //-92
sum(10,"十",20.3)   //error

函数调用

var i=1;
function mySecFunciton(){
    console.log(i);
}
function myFunction(){
    var i = 2;
    mySecFunciton();
}
myFunction()    
// 1 
var i=1;
function myFunction(){
    var i = 2;
    console.log(i);
}
myFunction()    
//2
var i=1;
function mySecFunciton(m){
    console.log(m);
}
function myFunction(){
    var i = 2;
    mySecFunciton(i);
}
myFunction()
//2
var i=1;
function mySecFunciton(m){
    console.log(m);
}
function myFunction(){
    var i = 2;
    mySecFunciton(n);   //此处是调用,因此n是实参
}
myFunction()
//报错 ReferenceError: n is not defined
function mySecFunciton(){
    console.log(i);
}
function myFunction(){
    var i = 2;
    mySecFunciton();
}
var i=0;
var i=1;
myFunction()
//1