Skip to content

函数

函数(有返回值的函数)可以被用作值进行赋值,或者参与运算

函数的使用价值是将一段重复使用的功能用函数进行封装,以后可以直接进行调用,可以反复调用很多次


基本形式

JavaScript 函数是被设计为执行特定任务的代码块, 函数会在代码调用它时被执行

js
function name(参数 1, 参数 2, 参数 3) {  // 具名函数
    要执行的代码;
}

在函数中,参数是局部变量;当 JavaScript 到达 return 语句时,函数将停止执行;函数通常会计算出返回值,这个返回值会返回给函数的调用者

函数可以做为对象的方法使用:针对于某个功能,可能需要很多的函数来完成,一般将函数封装成对象的方法

js
let user = {
    name: null,
    setUserName(name){
        this.name = name;
    },
    getUserName(){
        return this.name;
    }
};
user.setUserName("jlc");
console.log(user.getUserName());  // jlc

JavaScript 函数定义不会为参数(parameter)规定数据类型,参数的改变在函数之外是不可见的


定时器函数

setTimeout:延迟多长时间执行该函数

js
setTimeout(function() {
    console.log("111");
}, 1000)   // 延迟1秒执行该函数

setInterval:每隔多久执行一次函数

js
setInterval(function() {
    console.log("111");
}, 1000)   // 每隔1秒执行该函数

Function()构造器

函数也可以通过名为 Function() 的内建 JavaScript 函数构造器来定义

js
var myFunction = new Function("a", "b", "return a * b");
var x = myFunction(4, 3);

函数的作用域

  • 局部变量:变量在函数内声明,变量为局部变量,具有局部作用域,只能在函数内部访问,因为局部变量只作用于函数内,所以不同的函数可以使用相同名称的变量,局部变量在函数开始执行时创建,函数执行完后局部变量会自动销毁

  • 全局变量:变量在函数外定义,即为全局变量,全局变量有全局作用域: 网页中所有脚本和函数均可使用全局变量,如果变量在函数内没有声明(没有使用 var 关键字),该变量为全局变量

JavaScript中,函数内部的局部变量通常不可以直接被外部作用域访问,但有几种方式可以将函数内的局部变量暴露给外部作用域:

  • **通过全局对象:**在函数内部,可以通过将局部变量赋值给window对象的属性来使其成为全局可访问的。例如,使用 window.a = a; 语句,可以在函数外部通过 window.a 访问到这个局部变量的值
  • **定义全局变量:**在函数内部不使用 varletconst等关键字声明变量时,该变量会被视为全局变量,从而可以在函数外部访问。但这种做法通常不推荐,因为它可能导致意外的副作用和代码难以维护
  • **返回值:**可以通过在函数内部使用 return 语句返回局部变量的值,然后在函数外部接收这个返回值,这样,虽然局部变量本身不会被暴露,但其值可以通过函数调用传递到外部
  • 闭包:JavaScript 中的闭包特性允许内部函数访问外部函数的局部变量,即使外部函数执行完毕后,其局部变量仍然可以被内部函数引用
  • **属性和方法:**定义在全局作用域中的变量和函数都会变成 window 对象的属性和方法,因此可以在调用时省略 window,直接使用变量名或函数名

全局函数

当创建完函数后,默认情况会将函数压入到window全局对象当中(window也能调用该函数)

js
function hd(){
    console.log("jlc");
}
window.hd();  // jlc

这种情况是不合适的(万一window下有同名称的方法,那么声明函数后,原方法就会被覆盖),这种情况是历史的遗留问题,所以建议创建函数的时候,不要将函数进行独立存放,一般是将其编到类中进行存放


函数表达式(匿名函数)

匿名函数:没有给函数体声明函数的名字;匿名函数在对象方法中使用较多(箭头函数)

JavaScript 函数也可以使用表达式来定义,函数表达式可以在变量中存储:

js
var x = function (a, b) {return a * b};  // 将匿名函数的返回值赋值给了变量
var z = x(4, 3);
js
let cms = function() {
    console.log("jlc");
};
cms();  // jlc
// 这样就不能通过window.cms()进行调用了,但是如果通过var声明,那还是可以通过window.cms()进行调用的

函数的提升

Hoisting JavaScript将声明移动到当前作用域顶端的默认行为,Hoisting 应用于变量声明和函数声明

所以,JavaScript 函数能够在声明之前被调用,但是,使用表达式定义的函数(匿名函数)不会被提升


函数是对象

JavaScript 中的 typeof 运算符会为函数返回 "function",但是最好将JavaScript函数描述为对象(引用类型),JavaScript函数是有属性和方法的

arguments

JavaScript 函数有一个名为arguments的内置对象,arguments 对象包含函数调用时使用的参数数组,调用时所有的参数值都被放入这个列表中,这样就可以在声明函数的时候不设置参数了(很多情况下参数的数量是不确定的,这种情况下可以使用arguments进行处理)

js
x = findMax(1, 123, 500, 115, 44, 88);

function findMax() {  // 括号中不用设置参数了
    var i;
    var max = -Infinity;
    for (i = 0; i < arguments.length; i++) {
        if (arguments[i] > max) {
            max = arguments[i];
        }
    }
    return max;
}

以上函数不属于任何对象,但是在JavaScript中,始终存在一种默认的全局对象,在 HTML 中,默认全局对象是 HTML 页面本身,所有上面的函数“属于”HTML 页面,在浏览器中,这个页面对象就是浏览器窗口,myFunction()window.myFunction() 是同一个函数

js
// arguments.length 会返回函数被调用时收到的参数个数
function myFunction(a, b) {
    return arguments.length;
}
myFunction(1);   // 1

arguments是伪数组:

js
function sum() {
    return arguments.reduce((a, b) => a + b);  // 报错
    return [...arguments].reduce((a, b) => a + b);  // 正确
}
console.log(sum(1, 2));  // 3

目前,在新版本的js中,一般通过点语法来代替arguments方法来接收数组:

js
function sum(...args) {
    console.log(args);
    return args.reduce((a, b) => a + b);
}
sum(1, 2, 3); 
// [1, 2, 3]
// 6

toString() 方法以字符串返回函数

js
var txt = myFunction.toString();  // 返回的是声明函数的整段字符串
// 'function myFunction(a, b) {\n    return arguments.length;\n}'

定义为对象属性的函数,被称为对象的方法


立即执行函数

函数自动调用表达式(立即执行函数:不需要刻意的调用):(function(){})()

封装在括号里面的子函数名就不属于全局了


函数的参数

形参和实参

js
function sum(a, b) {
    return a + b;
}
console.log(sum(1, 2));
// 1和2为实参,具体要进行计算的参数
// a和b为形参,需要由实参进行赋值的

一般情况下,实参的数量要对应形参的数量,如果形参数量多于实参,多余的形参为undefined;如果实参的数量多余形参,不影响函数的使用,多余的实参会被忽略掉

默认参数

js
function sum(a, b = 2) {
    return a + b;
}
console.log(sum(1));  // 3
// 此时b就是默认参数,如果调用函数时,没有给定b,那么b就使用默认的值,如果给定了b,那就使用给定的值

默认参数要放在所有形参的后面

函数参数

函数作为参数进行传递,函数参数是不加括号的

js
let arr = [1, 2, 3, 4, 5, 6, 7].filter(hd);
function hd(a) {
    return a <= 3;
}
console.log(arr);  // [1, 2, 3]

箭头函数

箭头函数允许使用简短的语法来编写函数表达式

js
// 正常匿名函数编写方式
let hd = function() {
    return 1 + 2;
};
// 箭头函数的简写方式
let hd = ()=>{
    return 1 + 2;
};

具体例子:

js
let hd = [1, 2, 3, 4, 5, 6].filter(function(value) {
    return value <= 3;
});
// 箭头函数
let hd = [1, 2, 3, 4, 5, 6].filter((value) => {return value <= 3;});
// 第一步优化:如果只有一个参数时,可以不用括号包裹,如果一个参数都没有的话,就要带一个括号()
let hd = [1, 2, 3, 4, 5, 6].filter(value => {return value <= 3;});
// 第二步优化:如果只有一行的表达式,可以不加花括号,return和分号
let hd = [1, 2, 3, 4, 5, 6].filter(value => value <= 3);  
// 虽然没有加return,但是要知道会进行return

箭头函数看起来非常简洁,但是箭头函数不是万能的,它也有缺点,它不能完全的替代传统的function关键字,比如说在递归函数(箭头函数没有函数名字,在做递归回调处理的时候也是不方便的),构造函数和事件处理器的时候,是不方便使用箭头函数的,因为那个时候要考虑作用域和this关键词

  • 箭头函数没有自己的 this,它们不适合定义对象方法

  • 箭头函数未被提升,它们必须在使用前进行定义

  • 箭头函数形式函数自调用(立即执行函数):(()=>{})()


递归函数

递归的原理:不断重复的做一件事情,同时找到一个合适的时机去退出递归的过程

递归的应用(一般用于重复数量不定的情况),如数的阶乘:

js
function factorial(num) {
    if(num == 1){
        return 1;
    }
    return num * factorial(num - 1);
}
// 函数精简:
function factorial(num) {
    return num == 1 ? 1 : num * factorial(--num);
}
console.log(factorial(5));  // 120
js
// 通过递归来进行求和
function sum(...args){
    if(args.length == 0){
        return 0;
    }
    return args.pop() + sum(...args);
}
console.log(sum(1, 2, 3)); // 6

回调函数

回调函数:在其他函数中调用的函数

js
let hd = [1, 2, 3, 4];
hd.map(function(value, index, array){
    array[index] += 10;
});
console.log(hd);   // [11, 12, 13, 14]

map()是一个对象函数,map里面的function匿名函数,其实就是一个回调函数(在其他函数中又调用的函数)


函数中的展开语法/点语法

展开语法的功能是收和放

js
let hd = [1, 2, 3];
let [a, b, c] = [...hd];   // 展开语法作为值(在等号的右边),这时候的功能就是放;
console.log(a, b, c);  // 1 2 3

let [a, ...edu] = [1, 2, 3, 4];  // (在等号的左边),这时候的功能就是收;
console.log(edu); // [2, 3, 4]

函数中使用展开语法:

js
function sum(...args){
    console.log(args);  // [1, 2, 3, 4]
    return args.reduce((a ,b) => a + b);
}
console.log(sum(1, 2, 3, 4));  // 10

在函数中,展开语法和默认参数都是放在形参的后面的,一般展开语法放在默认参数的后面


this关键词

JavaScript 中,被称为 this 的事物,指的是“拥有”当前代码的对象,this 的值,在函数中使用时,是“拥有”该函数的对象

this是关键词,无法改变this的值

this表示对当前对象的引用:

js
let hd = {
    name: "jlc",
    show: function(){
        function render(){
            console.log(this);  // 这里的this指的是window
        }
        // 如果使用hd.name,当对象的名字改变,这里也要跟着改变,如果方法一多,就会很麻烦
        // this就是当前的对象{name: "jlc", show: f}
        return this.name;
    }
};
console.log(hd.show());
// 总结:如果函数是对象的属性(类方法),那么this就是当前的对象;如果只是在普通的函数中使用this,那么this就是全局对象window(可以在函数的外部加上const self = this;)

当不带拥有者对象调用对象时,this 的值成为全局对象,在 web 浏览器中,全局对象(window)就是浏览器对象

在全局环境下,this就是window

js
// 将列表的每个元素前面加上jlc-
let Lesson = {
    title: "jlc",
    lists: ["js", "css", "mysql"],
    // 写法一:
    show: function(){
        const self = this;  // 为了使不是方法的函数可以使用this
        return this.lists.map(function(value){
            return `${self.title}-${value}`;
        });
    }
    // 写法二:
    show: function(){
        return this.lists.map(function(value){
            return `${this.title}-${value}`;
        }, this);  // map()方法特有的第二个参数的传递,将this传入
    }
};

箭头函数给this带来的变化

this的指针上,箭头函数会有些不同,箭头函数中的this,指向的是父级作用域中的this(上下文)

js
// 通过箭头函数,将列表的每个元素前面加上jlc-
let Lesson = {
    title: "jlc",
    lists: ["js", "css", "mysql"],
    show: function(){  // 箭头函数中的this,指向父级作用域中的this
        return this.lists.map((value) => `${this.title}-${value}`);
    }
};

箭头函数中使用this也会有不方便的地方:

html
<!--需求,取按钮中的值,加上Dom中site的值-->
<body>
    <button>my name is jlc</button>  
</body>
<script>
    let Dom = {
        site: "jlc",
        bind: function(){
            const button = document.querySelector('button');  // 找到按钮元素
            console.log(button);  //<button>my name is jlc</button>
            button.addEventListener('click', function(){   // 给按钮加上点击事件,点击时执行函数
                // 点击函数是作为按钮的方法,this指向的是button
                console.log(this);   //<button>my name is jlc</button>   
            });
            //通过普通函数实现上述的功能
            const self = this;
            button.addEventListener('click', function(){
                console.log(this);   //<button>my name is jlc</button>   
                console.log(self.site + this.innerHTML);  //jlcmy name is jlc
            });
            
            //如果将点击函数换成箭头函数,那么this就会变成上下文,会变到父级作用域中的this
            button.addEventListene('click', () => {
                console.log(this);  //{site: "jlc", bind: f}
                console.log(this.site);  //jlc
            });
            // 为了实现上述的功能,箭头函数的this不能指向标签元素,需要对其进行改进
            button.addEventListene('click', event => {
                console.log(event.target);  //<button>my name is jlc</button>
                console.log(this.site + event.targe.innerHTML); //jlcmy name is jlc
            });
        }
    };
    Dom.bind();
</script>

也可以在对象函数中写一个方法,进行绑定:

js
let Dom = {
    site: "jlc",
    handeEvent: function(event){  // 绑定的特殊方法,通过this进行调用
        console.log(this);  // 指向Dom对象  {site: "jlc", handeEvent: f, bind: f}
        console.log(event.targe);  // <button>my name is jlc</button>
        console.log(this.site + event.targe.innerHTML);  //jlcmy name is jlc
    },
    bind: function(){
        const button = document.querySelector('button');  // 找到按钮元素
        button.addEventListener('click', this);  // 点击按钮时,会自动调用handeEvent方法
    }
};
Dom.bind();

总之,如果函数中需要大量的调用html元素,推荐使用普通函数的方法;如果需要大量的调用父级的方法,推荐使用箭头函数,但是最好的情况是,能使用箭头函数就使用箭头函数


函数方法

构造函数:this最开始的时候是空的,通过构造产生出一个对象

js
function User(name){
    this.name = name;
}  // 一开始的时候this是空的,通过上述的构造产生了一个空的对象{}
let lisi = new User("李四");  
console.log(lisi);  // 加工完后; {name: "李四"}

this是可以被改变的,apply,call,bind方法都是可以改变this的

js
let myAge = {age: "24"};
User.call(myAge, "我的年龄");  // 通过call方法调用构造函数,第一个参数是要改变的this,此时this不为空
console.log(myAge);  // {age: "24", name: "我的年龄"}

applycall

applycall方法的区别仅仅在于传递参数的时候不同

js
// 定义两个对象
let lisi = {
    name: "李四"
};
let wangwu = {
    name: "王五"
};

function User(web, url){
    console.log(web + url + this.name);
}
//call的作用:1.将对象传递给了this,改变了this的指针;2.它会立刻执行这个函数
//第一个参数是指要改变this指针的对象,后面的就是要传递的参数,call方法多个参数要用逗号相隔,apply方法传递一个数组即可,会将参数分布到各个形参当中,两个方法都会立刻执行
User.call(lisi, "百度", "baidu.com");  //百度baidu.com李四
User.call(wangwu, ["百度", "baidu.com"]);  //百度baidu.com王五
//apply和call的区别就是:一个要方括号,一个不需要

applycall方法的应用:

html
<!--点击不同的按钮弹出按钮的名称-->
<body>
    <button>jlc123</button>
    <button>jlc456</button>
</body>
<script>
    function show(){
        alert(this.innerHTML);  // this指向window
    }
    let buttons = document.querySelectorAll("button");
    for(let i = 0; i < buttons.length; i++){
        buttons[i].addEventListener("click", event => {
            show.apply(event.target);  // 我们需要将this进行改变,使其指向到按钮对象
        });
    }
</script>

我们知道数值是可以通过数值方法Max()获取最大值的,但是对于一个数组,我们是不能通过Max()来获取最大值,为了解决这个情况,我们可以通过使用apply()方法来使数组变成多个参数:

js
// 数组借助数学运算,来获得元素中的最大值
let arr = [1, 2, 3, 4, 5];
// 方法一:将数据展开
console.log(Math.max(...arr));  // 5
// 方法二:通过apply方法传入数值,键数组拆分
console.log(Math.max.apply(Math, arr));  // 5
// 注意:该情况使用call()方法是不行的

如果使用applycall方法不传递this时,可以通过输入null代替:

html
<!--折叠面板,点击<dt>标签时进行切换和显示隐藏-->
<body>
    <dl>
        <dt>第一层</dt>
        <dd>111</dd>
        <dt>第二层</dt>
        <dd hidden="hidden">222</dd>  <!--默认是隐藏的-->
    </dl>
</body>
<script>
    function panel(i){
        let dds = document.querySelectorAll("dd");  // 找到所有的dd标签
        dds.forEach(dd => dd.setAttribute("hidden", "hidden"));  // 让所有dd标签都消失
        dds[i].removeAttribute("hidden");  // 将选择的标签去掉隐藏
    }
    document.querySelectorAll("dt").forEach((dt, i) => {
        dt.addEventListener("click", () => panel.call(null, i));  // 调用函数不需要传递this
    });
    // 不传this调用函数和传统的调用函数本质上没有什么区别
</script>

bind

bind作用上和callapply是一样的,都是用来调用函数,来改变this

只不过使用callapply方法是立刻执行的,使用bind方法时,是不执行的,但是会返回一个新的函数,需要自调用这个函数才能执行

js
function show(){
    console,log(this.name);
}
show.bind({name: "jlc"})();
js
let a = function(){};
let b = a;  // 函数属于对象,是进行传址的
console.log(a === b)  // true  a和b共用一个内存地址,所以是完全相等的
b = a.bind();
console.log(a === b)  // false  将a函数赋值给了b,内存地址改变了
参数传递

因为bind的方法不是立刻执行的,所以对于参数传递,就有两个渠道进行传递

比如call和apply是立即执行的,调用的时候需要及时的将参数放上

但是bind不是立即执行的,可以在调用的时候传参数,也可以在最终返回函数调用的时候传参数

js
function hd(a, b){
    return this.f + a + b;
}
console.log(hd.call({f: 1}, 1, 1));  // 3

let func = hd.bind({f: 1}, 1, 1);  // 此时hd函数中a为1,b为1
func(2, 3);   // 因为之前bind传递过参数了,后续传参是没用的,hd函数中a还是为1,b还是为1
// 如果let func = hd.bind({f: 1});不传递参数,那么func(2, 3);传递参数是有用的

let func = hd.bind({f: 1}, 1);  // 只传递了一个参数,a为1
func(2, 3);  // 传递另一个参数,bhd函数中,b为2,3就没用了

bind的优势在于,之后的函数调用有用

html
<!--点击按钮,显示一段话加按钮文本,点击按钮后显示:baidu.comjlc-->
<body>
    <button>jlc</button>
</body>
<script>
    document.querySelector("button").addEventListener("click", function(event){
        document.write(this.url + event.target.innerHTML);
    }.bind({url: "baidu.com"}));
</script>

Released under the MIT License.