JS学习笔记 (五) 函数进阶

发布于 2021-05-06  408 次阅读


1、函数基础

1.1 函数的基本概念

  • 函数是一段JavaScript代码,只被定义一次,但是可以被调用或者执行许多次。函数是一种对象,可以设置属性,或调用方法。

  • 函数中的参数分为实参和形参。其中,形参在函数体中类似局部变量,函数调用会为形参提供实参的值。函数使用实参的值来计算返回值,成为该函数调用表达式的值。除了实参外,函数每次调用都会有一个this的值。

  • 如果函数挂载在对象的属性上则该函数被称为对象的方法,当通过该对象调用函数时,该对象就是此时的上下文,也就是该函数的this。

  • 用于初始化一个新创建的对象的函数称为构造函数

  • 函数可以嵌套在其它函数上进行定义,从而可以访问它们被定义时所处的作用域中的任何变量。此时,函数就构成了一个闭包。

1.2 函数的定义方法

1.2.1 函数定义构成

  • 函数定义是由函数名称标识符、一对圆括号和一对花括号构成,如:

    function sum(a, b) {
        return a + b
    }

    其中,sum是该函数的函数名,圆括号中存放啊a,b两个参数,大括号中存放JavaScript语句,构成了函数体。

1.2.2 函数定义方式

函数定义方式共有一下四种。
1、函数声明语法,如:

function mul(a, b) {
    return a * b
}

2、函数表达式,如:

let mul = function (a, b) {
    return a * b
}

3、Function构造函数,如:

// 使用function构造函数
let mul = new Function('a', 'b', 'return a * b')
  • 这个构造函数接收任意多个字符串参数,最后一个参数始终会被当成函数体,而之前的参数都是新函数的参数。
    不推荐使用这种语法来定义函数,因为这段代码会被解释两次:第一次是将它当作常规ECMAScript 代码,第二次是解释传给构造函数的字符串。这显然会影响性能。

4、箭头函数(ES6),如:

// 使用箭头函数
let mul = (a, b) => a * b

1.2.3 函数命名方式

函数命名和变量命名差别不大,建议使用驼峰命名法。即混合使用大小写字母来构成变量和函数的名字。驼峰命名法一般以小写字母开头,后面的独立单词首字母大写,用以区分每个独立的单词。

1.2.4 函数参数

  • 特征:
    ECMAScript 函数既不关心传入的参数个数,也不关心这些参数的数据类型。在定义函数时接收多个参数并不一定在调用时就必须要传入多个参数。

  • 原因:
    因为 ECMAScript 函数的参数在内部表现为一个数组。函数被调用时总会接收一个数组,但函数并不关心这个数组中包含什么。传进函数的每个参数值都被包含在arguments 对象(类数组)中。

  • 有关arguments对象:

    • arguments对象是一个类数组对象(但不是 Array 的实例),因此可以使用中括号语法访问其中的元素。如:arguments[0],arguments[1]
    • 要确定接收到的参数个数,可以访问 arguments.length 属性,如:

      function likes(name, fav1, fav2) {
          console.log(`${name}喜欢${fav1}、${fav2}`);
          let output = `${name}喜欢`
          let argCount = arguments.length
          for (let i = 0; i < argCount.length; i++) {
              output += `${arguments[i]}、`
          }
          output += `${arguments[argCount - 1]}。`
          console.log(output);
      }
      likes("小明", "读书", "篮球")

      运行结果:
      小明喜欢读书、篮球
      小明喜欢篮球。

  • 参数的默认值
    比较下方两段代码:

    ES5写法:

    function f1(name, age) {
        name = name ? name : "User";
        age = age ? age : 0;
        console.log(name, age);
    }
    f1();
    f1("Tom");

    运行结果是:
    User 0
    Tom 0

    ES6写法:

    function f2(name = "User", age = 0) {
        console.log(name, age);
    }
    f2();
    f2("Tom");

    运行结果是:
    User 0
    Tom 0

    由此可见,第二段代码更加的简化,增加了可读性。

  • 扩展参数
    先看一段代码:

    function sum() {
        let r = 0
        for (let i = 0; i < arguments.length; i++) {
            r += arguments[i]
        }
        return r
    }
    let nums = [1, 2, 3, 4, 5, 6]

    对于上面的代码,如何将nums数组的值传入函数sum里面?

    在es5里面,提供了apply方法,第一个参数默认是null,如:

    // 使用apply
    console.log(sum.apply(null, nums))  // 21

    在es6里面,提供了扩展运算符,如:

    // 扩展符...
    console.log(sum(...nums));  // 21
  • 剩余参数
    剩余参数语法允许我们将一个不定数量的参数表示为一个数组。
    语法:function(a,b,...arr){}
    如果函数的最后一个命名参数以...为前缀,则它将成为一个由剩余参数组成的真数组,其中从0(包括)到arr.length(不包括)的元素由传递给函数的实际参数提供。

    举个例子:已知三门科目的成绩计算总分

    es5 写法:

    function sum1(name) {
        let r = 0
        for (let i = 1; i < arguments.length; i++) {
            r += arguments[i]
        }
        console.log(`${name}总分是:${r}`);
    }
    sum1("Tom", 80, 80, 80)

    es6 写法:

    function sum2(name, ...scores) {
        let r = scores.reduce((x, y) => x + y, 0)
        console.log(`${name}总分是:${r}`);
    }
    sum2("Tom", 80, 80, 80)

    在上面的例子中,arr将收集该函数的第三个参数(因为第一个参数被映射到a)和所有后续参数。

  • 剩余参数和 arguments对象的区别:

    • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参。
    • arguments对象不是一个真正的数组,而剩余参数是真正的 Array实例,也就是说你能够在它上面直接使用所有的数组方法,比如 sort,map,forEach或pop。
    • arguments对象还有一些附加的属性 (如callee属性)。
  • 常见问题:
    1、箭头函数不支持arguments对象。
    2、箭头函数支持剩余函数。
    3、arguments对象包含了剩余参数值。剩余参数没有接收到实参值时,为空数组
    4、剩余参数必须位于参数列表的末尾。

    举个例子:

    function sum1() {
        return Array.from(arguments).reduce((x, y) => x + y, 0)
    }
    console.log(sum1(1, 2, 3)); 
    
    let sum2 = () => {
        return Array.from(arguments).reduce((x, y) => x + y, 0)
    }
    console.log(sum2(1, 2, 3)); 

    其中,第一个声明的函数输出结果是:6,
    第二个箭头函数输出错误: Uncaught ReferenceError: arguments is not defined。这说明了箭头函数不支持arguments对象。

    再看个例子:

    let sum3 = (...nums) => {
        return nums.reduce((x, y) => x + y, 0)
    }
    console.log(sum3(1, 2, 3)); //6

    这个说明es6的箭头函数支持剩余函数。

    再看下面的例子:

    function f1(x, y, ...nums) {
        console.log(arguments);
        console.log(nums);
    }
    f1(1, 2, 3, 4, 5, 6);
    f1(1, 2);

    运行结果:
    第一次调用后:
    这里的1,2对应f1的x,y参数,剩下的3, 4, 5, 6对应...nums,所以依次输出:
    [Arguments] { '0': 1, '1': 2, '2': 3, '3': 4, '4': 5, '5': 6 };
    [ 3, 4, 5, 6 ]数组

    第二次调用后:
    这里的1,2对应f1的x,y参数,...nums为空,所以依次输出:
    [Arguments] { '0': 1, '1': 2 };
    []空数组

1.2.5 函数调用

构成函数主体的JS代码在定义之时并不会执行,只有调用该函数时,它们才会执行。调用函数共有四种方式,分别是:作为函数、作为方法、作为构造函数以及通过它们的call()和apply()方法间接调用。
1、作为函数调用:

  • 使用调用表达式可以进行普通的函数调用也可进行方法调用。如:
    sum( 1, 2, 3+6, getNum( ) )

  • 对于普通的函数调用,函数的返回值成为调用表达式的值。如果该函数返回是因为解释器到达结尾,返回值就是undefined。
    如果函数返回是因为解释器执行到一条return语句,返回值就是return之后的表达式的值;如果return语句没有值,则返回undefined。

  • 非严格的ES5中,函数调用上下文(this的值)是全局对象。在严格模式下,调用上下文则是undefine

2、作为方法调用

  • 一般情况下,与普通函数的使用是调用上下文。
  • 属性访问表达式由两部分组成:一个对象(o)和属性名 (m)。在像这样的方法调用表达式里,对象o成为调用上下文,函数体可以使用关键字this引用该对象。
    举个例子:

    let obj = {
        a: 10,
        b: 20,
        add: function () {
            this.sum = this.a + this.b;
        },
    };
    obj.add();
    console.log(obj.sum);
    

    输出结果:30

  • 调用上下文。关键字this没有作用域的限制,嵌套的函数不会从调用它的函数中继承this。举个例子:

    let obj = {
        x: 10,
        fn: function () {
            this.x++;
            function ff() {
                console.log(this.x);
            }
            ff();
        },
    };
    obj.fn();

    输出:undefined
    若嵌套的函数要获取调用函数中的this,可以把它的this保存下来,然后在嵌套的函数里面调用。改进后的代码如下:

    let obj = {
        x: 10,
        fn: function () {
            this.x++;
            let self = this;
            function ff() {
                console.log(self.x);
            }
            ff();
        },
    };
    obj.fn();

    输出:11

3、作为构造函数调用

  • 构造函数调用指的是:函数或方法调用之前带有的关键字new的语句。
  • 构造函数调用会创建一个新的空对象,这个对象继承自构造函数的prototype属性。构造函数会初始化这个新创建的对象,并将这个对象用做其调用上下文,因此构造函数可以使用this关键字来引用这个新创建的对象
  • 构造函数通常不使用return关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会显式返回该对象。
    举个例子:
function Car(name) {
    this.name = name;
    return null;
}
console.log(Car("Benz"));
console.log(new Car("Benz"));

输出结果:
null
Car { name: 'Benz' }

4、间接调用方法

  • 使用函数对象的call( )和apply( )方法可以间接调用函数。其中第一个参数指定调用上下文(函数内部的this),第二个参数给函数传递参数。
    举个例子:
let obj1 = {
    x: 100,
    y: 100,
    show: function (n = 1, m = 1) {
        return `(${this.x * n},${this.y * m})`
    },
    concat: function () {
        let r = [this.x, this.y]
        for (let a of arguments) r.push(a)
        return r
    }
}
let obj2 = { x: 111, y: 222 }
let r1 = obj1.show.call(obj2)
let r2 = obj1.show.call(obj2, 10, 100)
console.log(r1, r2);
let r3 = obj1.concat.call(obj2, 11, 22, 33)
let r4 = obj1.concat.apply(obj2, [2, 3, 4, 5])
console.log(r3, r4);

输出结果:
(111,222) (1110,22200)
[ 111, 222, 11, 22, 33 ] [ 111, 222, 2, 3, 4, 5 ]

5、作为回调函数调用

  • 回调函数是被作为实参传入另一函数,并在该外部函数内被调用,用以来完成某些任务的函数。
    举个例子:
let arr = [1, 2, 12, 267, 21, 23, 78, 43, 10]
console.log("从大到小排序:" + arr.sort((a, b) => a - b));
console.log("从小到大排序:" + arr.sort((a, b) => b - a));

let data = {
    x: 10,
    y: 20,
    show: function (how) {
        how(this.x, this.y)
    }
};
data.show(function (a, b) {
    console.log(`(${a},${b})`);
})
data.show((a, b) => {
    console.log(`(${a},${b})`);
})

输出结果:
从大到小排序:1,2,10,12,21,23,43,78,267
从小到大排序:267,78,43,23,21,12,10,2,1
(10,20)
(10,20)

1.2.6 函数对象

说道函数对象,就不得不介绍一下它的一些常用的属性。
1、length属性

  • 在函数体里,arguments.length表示传入函数的实参的个数。
  • 函数本身的length属性是只读的,它代表函数声明的实际参数的数量。
    举个例子:
function fn(a, b) {
    console.log(arguments.length);
    console.log(fn.length);
    console.log(arguments.callee.length); // 当前函数对象执行次数
}
fn(1, 2, 3, 4, 5)

运行结果:
5
2
2

2、prototype属性

  • 每个函数都包含一个prototype属性,这个属性是指向一个对象的引用,这个对象称做“原型对象”。每一个函数都包含不同的原型对象。当将函数用做构造函数的时候,新创建的对象会从原型对象上继承属性。

3、自定义属性

  • 函数是一种特殊的对象,可以拥有属性
    举个例子:
function fn(a, b) {
    if (fn.count) fn.count++;
    else fn.count = 1
    return a + b
}
fn(1, 2)
fn(2, 3)
fn(3, 4)
fn(4, 5)
fn.count = 10;
// 记录函数调用了多少次(不可行,可直接赋值修改)
console.log(fn.count);
console.log(arguments.callee.length);

运行结果:
10
5

函数对象除了需要掌握的属性外,还有一些方法同等重要。如:

  • call( )和apply( )方法
    通过调用方法的形式来间接调用函数。
    call()和apply()的第一个实参是要调用函数的主体对象,它是调用上下文,在函数体内通过this来获得对它的引用。
  • bind( )方法
    将函数绑定至某个对象,且可以绑定参数值。
    当在函数f()上调用bind()方法并传入一个对象o作为参数,这个方法将返回一个新的函数。
    (以函数调用的方式)调用新的函数将会把原始的函数f()当做o的方法来调用。
    传入新函数的任何实参都将传入原始函数。
    通过bind()为函数绑定调用上下文后,返回的函数不能通过call和apply修改调用上下文对象

bind( )方法例子:

let obj = {
    x: 10,
    show: function (y) {
        let r = "";
        for (let i = 0; i < y; i++) {
            r += this.x + " ";
        }
        console.log(r);
    },
};
obj.show(4);
let ss = obj.show.bind({ x: 1000 });
ss(3);
ss.call({ x: 2000 }, 3);

运行结果:
10 10 10 10
1000 1000 1000
1000 1000 1000

bind( )应用举例,将对象某方法的调用上下文固定为当前对象,如:

let obj = { z: 1000 };
function sum(x, y) {
    let r = x + y;
    if (Object.getPrototypeOf(this) != window) {
        console.log(this); r += this.z;
    }
    return r;
}
obj.sum = sum;
console.log(obj.sum(20, 30));
console.log(obj.sum.call({ z: 1 }, 2, 3));

obj.asum = sum.bind(obj);
console.log(obj.asum(100, 200));
console.log(obj.asum.apply({ z: 1 }, [50, 60]));

运行结果:
{z: 1000, sum: ƒ}
1050
{z: 1}
6
{z: 1000, sum: ƒ, asum: ƒ}
1300
{z: 1000, sum: ƒ, asum: ƒ}
1110


活的像诗一样