WEB前端JavaScript高级—函数进阶

本文讲解了JavaScript中函数的定义、this指向、call/apply/bind的区别、闭包机制、递归和预解析过程,以及如何利用它们进行编程实践。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

函数的定义方式

函数声明

function foo() {}
foo();

函数表达式

var foo = function() {};
foo();
  • 下面是一个根据条件定义函数的例子,在不同浏览器中结果不一致。函数声明放在if-else的语句中,在IE8浏览器中会出现问题

    if (true) {
        function f () {
            console.log(1);
        }
    } else {
        function f () {
            console.log(2);
        }
    }
    

    不过可以使用函数表达式解决,所以以后用函数表达式,不用函数声明:

    var f;
    
    if (true) {
        f = function() {
            console.log(1);
        }
    } else {
        f = function() {
            console.log(2);
        }
    }
    

函数声明与函数表达式的区别

  • 函数声明必须有名字
  • 函数声明会提升函数,在预解析阶段就已创建,声明前后都可以调用
  • 函数表达式类似于变量赋值
  • 函数表达式可以没有名字,例如匿名函数
  • 函数表达式没有变量提升,在执行阶段创建,必须在表达式执行之后才可以调用

函数的调用方式

  • 普通函数:方法名();

    function f1() {
        console.log("文能提笔控萝莉");
    }
    f1();
    
  • 构造函数:new

    function F1() {
        console.log("我是构造函数,我骄傲");
    }
    var f = new F1();
    
  • 对象方法:对象.方法名();

    function Person() {
        this.play = function () {
            console.log("玩代码");
        };
    }
    var per = new Person();
    per.play();
    

函数内 this 指向的不同场景

函数的调用方式决定了 this 指向的不同,这就是对函数内部 this 指向的基本整理,代码写多了自然就熟了

哪里的this是谁说明
普通函数中window严格模式下是undefined
定时器中window
构造函数中实例对象
对象.方法中该方法所属对象
原型对象方法中实例对象
事件绑定方法绑定事件对象

函数也是对象

  • 对象中有__proto__原型,是对象;函数中有prototype原型,是对象

  • 结论:所有的函数都是Function构造函数创建出来的实例对象

    var f1 = new Function("num1", "num2", "return num1+num2");
    
  • 对象与函数的标志:__proto__ 是对象; prototype是函数
  • 函数是对象,对象不一定是函数,如Math中有__proto__,但是没有prorotype

严格模式

  • 语法:在函数前边加上"use strict;"

  • 示例:

    "use strict";
    function f1() {
        console.log(this);
    }
    f1();			// strict下为undefined,普通模式正确
    window.f1();	// strict模式下正确,普通模式正确
    

call、apply、bind

​ 了解了函数this指向的不同场景之后,我们知道有些情况下为了使用某种特定环境的 this 引用,这时候我们就需要采用一些特殊手段来处理,例如经常在定时器外部备份 this 引用,然后在定时器函数内部使用外部 this 的引用。然而实际上对于这种做法JavaScript为我们专门提供了一些函数方法用来更优雅的处理函数内部 this 指向问题

call和apply

  • 作用:改变this的指向,传入哪个对象就指向谁
  • 参数:
    • apply(对象, [参数0, 参数1...])
    • call(对象, 参数0, 参数1...)
  • 返回值:调用applycall的返回值就是原函数的返回值,可用变量接收
  • 适应性:想使用别的对象的方法,并且希望这个方法是当前对象的。
  • 注意:
    • applycall若没传参或传null,那么调用该方法的函数对象中的this就是默认的window
    • applycall方法也是函数的调用方式,即f()f.apply()f.call()等效
    • applycall都可让函数来调用,传入参数和函数自己调用的写法不同,但效果一样
    • applycall方法实际上并不在函数这个实例对象中,而是在Functionprototype中(实例对象调用方法,方法要么在实例对象中存在,要么在原型对象中存在)
  • 区别call方法接受的是若干个参数的列表,而 apply() 方法接受的是一个包含多个参数的数组

call

  • call 方法调用一个函数,其具有一个指定的 this 值和参数列表
  • 语法fun.call(thisArg[, arg1[, arg2[, ...]]]);
  • 参数
    • thisArg:在 fun 函数运行时指定的this值,如果指定了nullundefined则内部this指向window
    • arg1, arg2, ...:参数列表

apply

  • apply 方法调用一个函数,其具有一个指定的 this 值,以及作为一个数组提供的参数

  • 语法fun.apply(thisArg, [argsArray])

  • 参数:和call类似,例如fun.apply(this, ['eat', 'bananas'])

bind

  • bind函数会创建一个新函数(称为绑定函数),新函数与被调函数(目标函数)具有相同的函数体(在 ECMAScript 5规范中内置的call属性)。当目标函数被调用时 this 值绑定到bind()的第一个参数,该参数不能被重写。绑定函数被调用时,bind()也接受预设的参数提供给原函数。

  • 一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数

  • 作用:复制函数,并改变this的指向

  • 语法fun.bind(thisArg[, arg1[, arg2[, ...]]])

    // 参数可以在复制的时候传进去
    // 定义
    var 新名 = 原名.bind(对象, 参数0, 参数1...);
    // 调用
    新名();
    
    // 也可在复制之后调用时传进去
    // 定义
    var 新名 = 原名.bind(对象);
    // 调用
    新名(参数0,参数1...);
    
  • 参数

    • thisArg:当绑定函数被调用时,该参数会作为原函数运行时的 this 指向。当使用new操作符调用绑定函数时,该参数无效。
    • arg1, arg2, ...:当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法
  • 返回值: (复制之后的)函数,即由指定的this值和初始化参数改造的原函数拷贝

  • 注意:

    • applycall是调用的时候改变this指向;bind是复制一份时改变了this指向
    • 参数可以在复制的时候传进去,也可在复制之后调用时传进去
    • 对象不传或传入null时,this指向window
  • 示例

    this.x = 9; 
    var module = {
        x: 81,
        getX: function() {
            return this.x;
        }
    };
    
    module.getX(); // 返回 81
    
    var retrieveX = module.getX;
    retrieveX(); // 返回 9, 在这种情况下,"this"指向全局作用域
    
    // 创建一个新函数,将"this"绑定到module对象
    // 新手可能会被全局的x变量和module里的属性x所迷惑
    var boundGetX = retrieveX.bind(module);
    boundGetX(); // 返回 81
    
    function LateBloomer() {
        this.petalCount = Math.ceil(Math.random() * 12) + 1;
    }
    
    // Declare bloom after a delay of 1 second
    LateBloomer.prototype.bloom = function() {
        window.setTimeout(this.declare.bind(this), 1000);
    };
    
    LateBloomer.prototype.declare = function() {
        console.log('I am a beautiful flower with ' +
                    this.petalCount + ' petals!');
    };
    
    var flower = new LateBloomer();
    flower.bloom();  // 一秒钟后, 调用'declare'方法
    
    function ShowRandom() {
        this.number = parseInt(Math.random() * 10 + 1);	// 随机数
    }
    ShowRandom.prototype.show1 = function() {	// 添加原型方法
        // 改变定时器中的this的指向,window -> 实例对象
        window.setInterval(this. show2.bind(this), 1000);
    };
    ShowRandom.prototype.show2 = function() {	// 添加原型方法,显示随机数
        console.log(this.number);
    };
    var sr = new ShowRandom();				// 实例对象
    

总结

  • call和apply特性一样

    • 都是用来调用函数,而且是立即调用
    • 可以在调用函数的同时,通过第一个参数指定函数内部 this 的指向
    • call 调用的时候,参数必须以参数列表的形式进行传递,也就是以逗号分隔的方式依次传递
    • apply 调用的时候,参数必须是一个数组,然后在执行的时候,会将数组内部的元素一个一个拿出来,与形参一一对应进行传递
    • 如果第一个参数指定了 nullundefined 则内部 this 指向window
  • bind

    • 可以用来指定内部 this 的指向,然后生成一个改变了 this 指向的新的函数

    • 它和 call、apply 最大的区别是:bind 不会调用

    • bind 支持传递参数,它的传参方式比较特殊,一共有两个位置可以传递

      1. 在 bind 的同时,以参数列表的形式进行传递
      2. 在调用的时候,以参数列表的形式进行传递
      • 那到底以谁 bind 的时候传递的参数为准呢还是以调用的时候传递的参数为准
    • 两者合并:bind 时传递的参数和调用时传递的参数会合并到一起,传递到函数内部

函数的其它成员

  • arguments:实参数组
  • caller:函数的调用者,f1函数在f2函数中被调用,caller就是f2
  • length:形参的个数
  • name:函数的名称,只读不能修改
function fn(x, y, z) {
    console.log(fn.length); // 形参的个数
    console.log(arguments); // 伪数组实参参数集合
    console.log(arguments.caller === fn); // 函数本身
    console.log(fn.caller); // 函数的调用者
    console.log(fn.name); // 函数的名字
}
function f() {
    fn(10, 20, 30);
}

f();

高阶函数

作为参数

  • 例子:

    // fn是参数,最后作为函数使用
    function f1(fn) {
        console.log("f1...");
        fn();	// fn当成一个函数使用
    }
    // 传入匿名函数
    f1(function () {				
        console.log("匿名函数...");
    });
    // 传入命名函数
    function f2() {					
        console.log("f2...");
    }
    f1(f2);
    
  • 应用:

    var arr = [1, 100, 20, 200, 40, 50, 120, 10];
    // 排序(不稳定,很可能没排序)
    arr.sort();			
    console.log(arr);
    
    // 排序(稳定)
    // 函数作为参数使用,匿名函数作为sort方法的参数使用,此时匿名函数有两个参数
    arr.sort(function(obj1, obj2) {
        if (obj1 > obj2) {
            return 1;			// 1 -> 正序;-1 -> 倒序
        } else if(obj1 === obj2) {
            return 0;
        } else {
            return -1;			// -1 -> 正序;1 -> 倒序
        }
    });
    console.log(arr);	// 稳定
    

作为返回值

  • 获取某个对象的数据类型的样子:

    // 判断这个对象是不是某个类型的
    console.log(obj instanceof Object);
    // 获取某个对象的数据类型的样子
    Object.prototype.toString.call(对象);
    
  • 判断某个对象的类型是不是传入的类型:

    // [10,20,30] 是不是"[object Array]"
    // type-是变量-是参数-"[object Array]"
    // obj-是变量-是参数-[10,20,30];
    function getFunc(type) {
        return function (obj) {
            return Object.prototype.toString.call(obj) === type;
        }
    }
    var isArray = genFun('[object Array]')
    var isObject = genFun('[object Object]')
    
    console.log(isArray([124])) // true
    console.log(isObject({})) // true
    

JS预解析

JS执行过程

JavaScript 运行分为两个阶段:先预解析全局作用域,然后执行全局作用域中的代码,在执行全局代码的过程中遇到函数调用就会先进行函数预解析,然后再执行函数内代码

  1. 预解析
    • 全局预解析(所有变量和函数声明都会提前;同名的函数和变量函数的优先级高)
    • 函数内部预解析(变量、函数、形参)
  2. 执行

预解析

  • 定义:变量使用或函数调用时,会把变量或函数【声明】提升到作用域最上面。

  • 注意:预解析只将声明提前,赋值不会被提前。

    // var num;
    console.log(num);	// undefined
    var num = 10;
    
    // var num;
    f1();
    var num = 20;
    function f1() {
        console.log(num);	// undefined
    }
    
    // function a(){console.log('哈哈');};
    console.log(a);			// 函数a代码
    function a() {			// 注意:这相当于变量a的声明
        console.log('哈哈');
    }
    var a = 1;
    console.log(a);			// 1
    
    // var a(局部变量); a=9; b=9(隐式全局); c=9(隐式全局);
    f1();
    console.log(c);				// 9
    console.log(b);				// 9
    console.log(a);				// 报错
    function f1() {
        // var a;
        var a = b = c = 9;	
        console.log(a);			// 9
        console.log(b);			// 9
        console.log(c);			// 9
    }
    
    // var f1;
    f1(); // f1...
    function f1() {  
        console.log('f1...');
    }
    f2();	// Uncaught TypeError: f2 is not a function
    var f2 = function() {
        console.log('f2...');
    };
    
    // var fn;
    fn();	// undefined
    function fn() {
        // var ab;
        console.log(ab);	
        var ab = 1;
    }
    

函数闭包

  • 概念:函数A中有函数B,函数B可访问函数A中定义的变量,此时形成了闭包。闭包就是能够读取其他函数内部变量的函数,由于在js中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成 “定义在一个函数内部的函数”。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

  • 分类:函数模式的闭包、对象模式的闭包。

  • 作用:缓存数据,延长作用域链,在函数外部读取函数内部成员让函数内成员始终存活在内存中。优点同时也是缺陷,没有及时释放。

  • 适应性:如果想要缓存数据,就把这个数据放在外层的函数和里层的函数的中间位置。

    // 函数模式的闭包:在函数f1()有一个函数f2()访问f1的变量。
    function f1(){
        var num = 1;
        function f2(){
            console.log(num);
        }
        f2();
    }
    f1();
    // 对象模式的闭包: 函数f3()中有一个对象obj访问f3的变量。
    function f3() {
        var num = 10;
        var obj = {
            age: num
        };
        console.log(obj.age);	// 10
    }
    f3();
    
    function fn() {
        var count = 0;
        return {
            showCount: function () {
                console.log(count);
            },
            incrCount: function () {
                count++;
            }
        }
    }
    
    var fns = fn();
    
    fns.showCount(); // 0
    fns.incrCount();
    fns.showCount(); // 1
    

闭包例子

var arr = [10, 20, 30];
for(var i = 0; i < arr.length; i++) {
  arr[i] = function () {
    console.log(i);
  };
}
console.log(111);
for(var i = 0; i < 3; i++) {
    setTimeout(function () {
        console.log(i);
    }, 0);
}
console.log(222);

示例3:投票

示例4:判断类型

示例5:沙箱模式

沙箱

  • 黑盒环境,在一个虚拟的环境中模拟真实世界、做实验,实验结果和真实世界的结果是一样,但是不会影响真实世界。

  • 示例

    var num = 100;			
    (function () {			// 沙箱:小环境内部的作用域
        var num = 10;		// 和全局的互不影响
        console.log(num);	// 10
    }());
    console.log(num);		// 100
    

闭包思考题

var name = "The Window";
const object = {
    name: "My Object",
    getNameFunc: function() {
        return function() {
            return this.name;
        };
    }
};
console.log(object.getNameFunc()());	// The Window
  • getNameFunc 函数内部,返回了一个匿名函数,而匿名函数中使用了 this 关键字来引用对象的属性 name。然而,由于匿名函数是作为全局函数调用的,因此 this 关键字将指向全局对象,而不是 object 对象。因此,匿名函数中的 this.name 实际上指向了全局对象的 name 属性,其值为 “The Window”
var name = "The Window";  
const object = {    
  name: "My Object",
  getNameFunc: function () {
    var that = this;
    return function () {
      return that.name;
    };
  }
};
console.log(object.getNameFunc()());
  • getNameFunc 函数返回了一个匿名函数,而在 getNameFunc 函数内部,使用了变量 that 来保存对外部 this 的引用。在匿名函数中,使用了 that.name 来获取对象的 name 属性,因为 that 指向了外部函数的 this,即指向了 object 对象。因此,最终输出的结果是 “My Object”。

应用—缓存

  • 对比:

    // 普通函数 -> 不缓存
    function f1() {
        var num = 10;
        num++;
        return num;
    }
    console.log(f1());  // 11
    console.log(f1());  // 11
    console.log(f1());  // 11
    
    // 闭包 -> 缓存
    function f2() {
        var num = 10;
        return function () {
            num++;
            return num;
        }
    }
    var ff = f2();
    console.log(ff());	// 11
    console.log(ff());	// 12
    console.log(ff());	// 13
    
  • 相同的随机数:

    // 产生3个随机数,但都是不同的,因为数据没缓存
    function showRandom() {		
        var num = parseInt(Math.random()*10+1);
        console.log(num);
    }
    showRandom();
    showRandom();
    showRandom();
    
    // 闭包的方式,产生三个随机数,但是都是相同的
    function f1() {				
        var num = parseInt(Math.random() * 10 + 1);
        return function () {
            console.log(num);
        }
    }
    var ff = f1();
    ff();
    ff();
    ff();
    

函数递归

递归执行模型

function fn1 () {
    console.log(111);
    fn2();
    console.log('fn1');
}

function fn2 () {
    console.log(222);
    fn3();
    console.log('fn2');
}

function fn3 () {
    console.log(333);
    fn4();
    console.log('fn3');
}

function fn4 () {
    console.log(444);
    console.log('fn4');
}

fn1();

例子

// 计算阶乘的递归函数
function factorial (num) {
    if (num <= 1) {
        return 1;
    }
    return num * factorial(num - 1);
}

// 求前n项和
function getSum(n){
    if(n == 1) {
        return 1;
    }
    return n + getSum(n-1);
}

// 求各位数字的和
function getEverySum(x){
    if(x < 10){					
        return x;
    }
    // 返回个位数的值+其他位数的值
    return x % 10 + getEverySum(parseInt(x/10));	
}

// 求斐波那契数列
function getFib(n){
    if(n==1 || n==2){	// 如果到了1或2项,返回1
        return 1;
    }
    // 否则返回前1项+前2项的和
    return getFib(n-1) + getFib(n-2);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值