4.函数

169 阅读8分钟

4-1. 参数默认值

使用

在书写形参时,直接给形参赋值,附的值即为默认值

这样一来,当调用函数时,如果没有给对应的参数赋值(给它的值是undefined),则会自动使用默认值。

[扩展]对arguments的影响

只要给函数加上参数默认值,该函数会自动变量严格模式下的规则:arguments和形参脱离

[扩展]留意暂时性死区

形参和ES6中的let或const声明一样,具有作用域,并且根据参数的声明顺序,存在暂时性死区。

例子

例子1

function test(a = b, b) {
    console.log(a, b);
}

test(undefined, 2);

例子2

function sum(a, b = 1, c = 2) {
    return a + b + c;
}

console.log(sum(10, undefined, undefined))
console.log(sum(11))
console.log(sum(1, undefined, 5))

例子3

function getContainer() {
    console.log("abc");
    return document.getElementById("container");
}

/**
 * 创建一个元素
 * @param {*} name 元素的名称 
 * @param {*} container 元素的父元素
 * @param {*} content 元素的内容 
 */
function createElement(name = "div", container = getContainer(), content = "") {
    const ele = document.createElement(name)
    if (content) {
        ele.innerHTML = content;
    }
    container.appendChild(ele);
}

createElement(undefined, undefined, "手动阀手动阀十分")
createElement(undefined, undefined, "234242342424")
createElement(undefined, document.getElementById("container"), "234242342424")

例子4

function test(a, b = 1) {
    console.log("arugments", arguments[0], arguments[1]);
    console.log("a:", a, "b:", b);
    a = 3;
    console.log("arugments", arguments[0], arguments[1]);
    console.log("a:", a, "b:", b);
}

test(1, 2);

4-2. 剩余参数

arguments的缺陷:

  1. 如果和形参配合使用,容易导致混乱
  2. 从语义上,使用arguments获取参数,由于形参缺失,无法从函数定义上理解函数的真实意图

ES6的剩余参数专门用于收集末尾的所有参数,将其放置到一个形参数组中。

语法:

function (...形参名){

}

一个具体的例子

/**
 * 对所有数字求和
 * @param  {...any} args 
 */
function sum(...args) {
    let sum = 0;
    for (let i = 0; i < args.length; i++) {
        sum += args[i];
    }
    return sum;
}

/**
 * 获取一个指定长度的随机数组成的数组
 * @param {*} length 
 */
function getRandomNumbers(length) {
    const arr = [];
    for (let i = 0; i < length; i++) {
        arr.push(Math.random());
    }
    return arr;
}

const numbers = getRandomNumbers(10);
//将数组的每一项展开,依次作为参数传递,而不是把整个数组作为一个参数传递
// sum(numbers)

console.log(sum(...numbers))//相当于传递了10个参数
console.log(sum(1, 3, ...numbers, 3, 5))

从上面可以看到一个场景就是我们的参数其实都放在一个数组里面,我们在调用函数的时候又希望按照每一个参数分别传给函数,此时函数的剩余参数就十分有用了。

细节:

  1. 一个函数,仅能出现一个剩余参数
  2. 一个函数,如果有剩余参数,剩余参数必须是最后一个参数

例子

例子1

function test(a, b, ...args) {
    
}

test(1, 32, 46, 7, 34); 

例子2

function sum(...args) {
    //args收集了所有的参数,形成的一个数组
    let sum = 0;
    for (let i = 0; i < args.length; i++) {
        sum += args[i];
    }
    return sum;
}

console.log(sum())
console.log(sum(1))
console.log(sum(1, 2))
console.log(sum(1, 2, 3))
console.log(sum(1, 2, 3, 4))

例子3

下面的写法报错,因为args1 args2 的个数没法确定。

function test(...args1, ...args2) {
    console.log(args1)
    console.log(args2)
}

test(1, 32, 46, 7, 34);

4-3. 展开运算符

使用方式: ...要展开的东西

对数组展开 ES6

对对象展开 ES7

例子1 浅克隆

const obj1 = {
    name: "成哥",
    age: 18,
    loves: ["邓嫂", "成嫂1", "成嫂2"],
    address: {
        country: "中国",
        province: "黑龙江",
        city: "哈尔滨"
    }
}

// 浅克隆到obj2
const obj2 = {
    ...obj1,
    name: "邓哥",
    address: {
        ...obj1.address
    },
    loves: [...obj1.loves, "成嫂3"]
};

console.log(obj2)

console.log(obj1.loves === obj2.loves)

这个展开默认是浅克隆。注意以下几点: 1.如果obj2我们想给某个属性重新赋值,我们只需要像 name: "邓哥"这样重新赋值就可以了。 2.由于对对象是浅克隆,这里的obj2 的loves数组 和obj1 的loves数组,克隆完是同一个对象,如果想对这个对象进行克隆,我们可以新建 [] 数组,在这个数组里面对该属性进行展开,并且可以对数组增加一些项。

例子2 数字求和

/**
 * 对所有数字求和
 * @param  {...any} args 
 */
function sum(...args) {
    let sum = 0;
    for (let i = 0; i < args.length; i++) {
        sum += args[i];
    }
    return sum;
}

克隆数组的元素

const arr1 = [3, 67, 8, 5];

//克隆arr1数组到arr2

const arr2 = [0, ...arr1, 1];

console.log(arr2, arr1 === arr2)

MAX也可以传入多个参数


btn.onclick = function () {
    const numbers = getValues(); //得到文本框中的所有数字形成的数组
    spanmax.innerText = Math.max(...numbers)
    spanmin.innerText = Math.min(...numbers)
}

函数柯里化需要研究(暂时不懂)

<script>
        function cal(a, b, c, d) {
            return a + b * c - d;
        }
        //curry:柯里化,用户固定某个函数的前面的参数,得到一个新的函数,新的函数调用时,接收剩余的参数
        function curry(func, ...args) {
            return function(...subArgs) {
                const allArgs = [...args, ...subArgs];
                if (allArgs.length >= func.length) {
                    //参数够了
                    return func(...allArgs);
                } else {
                    //参数不够,继续固定
                    return curry(func, ...allArgs);
                }
            }
        }

        const newCal = curry(cal, 1, 2)

        console.log(newCal(3, 4)) // 1+2*3-4
        console.log(newCal(4, 5)) // 1+2*4-5
        console.log(newCal(5, 6)) // 1+2*5-6
        console.log(newCal(6, 7)) // 1+2*6-7

        const newCal2 = newCal(8)

        console.log(newCal2(9)); // 1+2*8-9
    </script>

4-5. 明确函数的双重用途

ES6提供了一个特殊的API,可以使用该API在函数内部,判断该函数是否使用了new来调用

new.target 
//该表达式,得到的是:如果没有使用new来调用函数,则返回undefined
//如果使用new调用函数,则得到的是new关键字后面的函数本身

例子

function Person(firstName, lastName) {
    //判断是否是使用new的方式来调用的函数

    // //过去的判断方式
    // if (!(this instanceof Person)) {
    //     throw new Error("该函数没有使用new来调用")
    // }

    if (new.target === undefined) {
        throw new Error("该函数没有使用new来调用")
    }
    this.firstName = firstName;
    this.lastName = lastName;
    this.fullName = `${firstName} ${lastName}`;
}

const p1 = new Person("袁", "进");
console.log(p1)

4-6. 箭头函数

回顾:this指向

  1. 通过对象调用函数,this指向对象
  2. 直接调用函数,this指向全局对象
  3. 如果通过new调用函数,this指向新创建的对象
  4. 如果通过apply、call、bind调用函数,this指向指定的数据
  5. 如果是DOM事件函数,this指向事件源

使用语法

箭头函数是一个函数表达式,理论上,任何使用函数表达式的场景都可以使用箭头函数

完整语法:

(参数1, 参数2, ...)=>{
    //函数体
}

如果参数只有一个,可以省略小括号

参数 => {

}

如果箭头函数只有一条返回语句,可以省略大括号,和return关键字,是一条语句,但不是返回语句也不行。

参数 => 返回值

简写返回一个对象的写法:直接返回一个对象的时候要加一个小括号,告诉编辑器这是一个表达式。

const sum = (a, b) => ({
    a: a,
    b: b,
    sum: a + b
});

console.log(sum(3, 5))

vue 里面的一个箭头函数

new Vue({
    // render(h) {
    //     return h(App);
    // },
    render: (h) => h(App), // 渲染组件App
}).$mount("#app");

上下两个render函数,可以从内部从这个配置对象调用方式去理解,调用方式是一致的。

注意细节

  • 箭头函数中,不存在this、arguments、new.target,如果使用了,则使用的是函数外层的对应的this、arguments、new.target
  • 箭头函数没有原型
  • 箭头函数不能作用构造函数使用

应用场景

  1. 临时性使用的函数,并不会可以调用它,比如:
    1. 事件处理函数
    2. 异步处理函数
    3. 其他临时性的函数
  2. 为了绑定外层this的函数
  3. 在不影响其他代码的情况下,保持代码的简洁,最常见的,数组方法中的回调函数

不会使用箭头函数的场景

对象的属性是不会用箭头函数的。 为什么不会用呢? 因为他会导致一个问题。箭头函数内部的this指向外层。

const obj = {
    count: 0,
    start: function () {
     //this
        setInterval(() => {
        //此处this指向箭头函数声明的地方,相当于上面一行的this,对象函数里面的this,如果是下面的 obj.start的方式调用,this指向window.
            this.count++;
            console.log(this.count);
        }, 1000)
    },
    
     start: function () {
        setInterval(function() {
        //此处 this指向window
            this.count++;
            console.log(this.count);
        }, 1000)
    },
    regEvent: function () {
    //this
        window.onclick = () => {
        //此处this指向箭头函数声明的地方,相当于上面一行的this,对象函数里面的this,如果是下面的 obj.start的方式调用,this指向window.
            console.log(this.count);
        }
    },
    
    regEvent1: function () {
        window.onclick = function() {
        //此处this指向事件源 window
            console.log(this.count);
        }
    },
    
    print: function () {
        console.log(this)
        console.log(this.count)
    }
     
     //不会像下面使用箭头函数
     print:()=> {
      // obj.print();调用的话,this指向window。
        console.log(this)
        console.log(this.count)
    }
    
    //因为上面可以简化为 
    print: this,  this不在函数里面,指向 window。这样会导致想用当前对象的一些东西,不能用this.属性名去调用了。
}

// obj.start();
// obj.regEvent();
obj.print();

this指向哪里呢

const func = () => {
    console.log(this)
}

const obj = {
    method: func
}

//这里this不指向 obj,而是指向window。
obj.method();

箭头函数里面的this取决于箭头函数声明的位置的this。

在有箭头的函数里面判断this的指向是该箭头函数的声明替换成this,然后看一下这是this指向哪里,函数里面的this就指向哪里,如果里面仍然是箭头函数,继续依次往外看。

const obj = {
// 指向window
this,
// 下面的this可能指向window,可能指向obj
 print: function () {
        console.log(this)
    }