脚本宝典收集整理的这篇文章主要介绍了

关于JavaScript函数调用的几种模式

脚本宝典小编觉得挺不错的,现在分享给大家,也给大家做个参考,希望能帮助你少写一行代码,多一份安全和惬意。

函数的调用有五种模式:方法调用模式,函数调用模式,构造器调用模式,apply/call调用模式以及回调模式,下面分别对这几种模式进行说明。

1.函数调用与方法调用模式:

1.1 声明一个函数并调用它就是函数调用模式,这是最简单的调用,但其中也关系到this的指向问题。普通函数是将this默认绑定到全局对象,而箭头函数时不绑定this的,在函数所在的父作用域外面this指向哪里,在箭头函数内部this也指向哪里。

    function show(name) {
        console.log(name);
    }

    show('shotar');                // shotar

    // 普通函数的符符作用域this指向全局作用域,在调用的时候再一次绑定this到全局作用域
    function say() {
        console.log(this);
    }

    say();                        // 浏览器环境输出window  node环境输出global

    // 箭头函数父作用域的this指向全局对象,调用的时候并没有绑定this,而是继承父作用域后指向全局对象
    var sayName = () => {
        console.log(this);
    }

    sayName()                     // window

1.2 方法调用时将一个函数作为对象的方法调用,作为方法调用的函数会将this绑定到该对象,但如果方法内部再嵌套一个函数,内部函数再次调用的时候又属于函数调用模式,此时this又将绑定到全局对象。

    window.name = 'Jane';        // node环境下是global
    var obj = {
        name: 'shotar',
        sayName: function() {
            console.log(1, this.name);
            sayWindowName();

            function sayWindowName() {
                console.log(2, this.name);
            }
        }
    };

    obj.sayName();                // 1, shotar      2, Jane

如果想让内部函数(sayWindowName)指向该对象也很简单,在此列举三种方法。第一种是在外部将this保存到一个变量里面,再在内部函数中使用即可。

    window.name = 'Jane';
    var obj = {
        name: 'shotar',
        sayName: function() {
            var _this = this;
            console.log(1, this.name);
            sayWindowName();

            function sayWindowName() {
                console.log(2, _this.name);
            }
        }
    };

    obj.sayName();                // 1, shotar      2, shotar

第二种解决办法是使用ES6的箭头函数,箭头函数不绑定this,父作用域的this是哪个对象在箭头函数中的this仍然是哪个对象(注意:箭头函数只能使用函数字面量的形式命名函数名,调用也要在语句之后)。

    window.name = 'Jane';
    var obj = {
        name: 'shotar',
        sayName: function() {
            console.log(1, this.name);

            var sayWindowName = () => {
                console.log(2, _this.name);
            };

            sayWindowName();
        }
    };

    obj.sayName();                // 1, shotar      2, shotar

第三种使用call或apply方法是改变内部函数的this值。

    window.name = 'Jane';
    var obj = {
        name: 'shotar',
        sayName: function() {
            console.log(1, this.name);
            sayWindowName.call(this);        // 或 sayWindowName.apply(this);

            function sayWindowName() {
                console.log(2, this.name);
            }
        }
    };

    obj.sayName();                // 1, shotar      2, shotar

在此说明一下阮大大在ES6标准入门里面列举的关于箭头函数this指向的例子,因为在foo函数的作用域下指向window的,使用函数调用模式调用foo函数,setTimeout内的箭头函数不绑定this,还是指向父作用域foo函数所指向的this。foo是普通函数,他将this指向全局对象,因此箭头函数也指向全局变量。这时会打印undefined,为什么又会打印出undefined呢,这是因为在声明id的时候使用了var关键字,他是一个变量并不是全局对象(window或global)的属性,如果将var id = 21;这句改为window.id = 21;(或者global.id = 21)后将打印出21。使用call方法调用会改变this的值,下面到call/apply调用模式的时候会讲到。

    function foo() {
        setTimeout(() => {
            console.log('id:', this.id);
        }, 100);
    }

    var id = 21;

    foo();

    foo.call({ id: 42 });        // id: 42

1.3 关于函数this指向问题
普通函数的this是会被绑定的,根据调用方式的不同绑定不同的对象到this(this只能绑定对象),而箭头函数是不绑定this的。有这样一道面试题:

    window.bar = 2
    var obj = {
        bar: 1,
        foo: function() {
            return this.bar;
        }
    };

    var foo = obj.foo;

    console.log(obj.foo()); // 1
    console.log(foo());        // 2

JavaScript的this设计很内存里的数据结构有很大的关系。当把一个对象赋给一个变量的时候,大家都知道是引用关系,上面的obj是一个地址,指向那个对象,而在对象存储的时候,其属性(方法)的值也是同样的存储形式,每个属性对应一个属性描述对象,举例来讲,上面obj的bar属性其实是以下面的形式保存起来的。

    bar: {
        [[value]]: 1,
        [[configurable]]: true,
        [[enumerable]]: true,
        [[writable]]: true
    }

其属性的值被保存在[[value]]中。但如果属性的值是个对象(函数也是对象)呢?此时JavaScript引擎会将对象的地址保存在描述符对象的[[value]]位置,像上面的foo属性(方法)则是这样保存的:

    foo: {
        [[value]]: 对象的地址,
        [[configurable]]: true,
        [[enumerable]]: true,
        [[writable]]: true
    }

函数是个单独的值,因此他可以在任何不同的上下文环境中执行,也正因为如此,有必要需要一种机制能够在函数内部获得当前的执行上下文(context),因此this就出现了。在上面的那道面试题中,是将该函数的地址赋给变量foo。通过foo变量调用时,其是在全局作用域下执行,因此this指向全局对象。如图1:

图片描述

而使用obj.foo执行时,函数是在obj环境下运行,如图2,所以this是指向obj的。上面提到普通函数是绑定this值,this值指得是当前运行环境,当在obj环境下调用时指向obj,而在全局调用时指向全局对象。所以this是在调用时才确定值,并不是在声明时就绑定值。

图片描述

2.call/apply调用模式

call和apply都是Function.prototype中的方法,可以通过Function.prototype.hasOwnProperty('call')验证。因此每一个函数或者方法都可通过call或apply调用,call和apply都是函数上的方法,每声明一个函数,就像prototype属性一样,都会有call和apply方法。每个函数或方法都可以通过call或者apply改变当前的执行上下文,他们的第一个参数就是要将this绑定的值。区别是后面的传参形式不同,前者是将参数逐个传入调用的函数中,而apply是将参数作为一个数组传给要调用的函数。就拿那道面试题做例子:

    window.bar = 2
    var obj = {
        bar: 1,
        foo: function() {
            return this.bar;
        }
    };

    var foo = obj.foo;

    // ①
    foo.call(obj);            // 1
    // ②
    obj.foo.call(window);    // 2
    // ③
    foo.call({bar: 3})        // 3

①如果foo是普通的调用,其this是指向全局对象的,而通过call改变将this绑定到obj后,this将指向obj。我们可以这样理解,foo是这样调用的obj.foo()
②这种调用方式我们可以这样理解,foo是obj的方法,就当他是一个普通的函数,相当于window.foo这样调用,那么this就是指向全局对象的。
③这种调用方式是将{bar: 3}作为this的绑定对象,这样调用foo就相当于{bar: 3}.foo(),this指向{bar: 3}。

3.构造器调用模式:
构造函数的new调用方式被称为构造器调用模式,这是模拟类继承式语言的一种调用方式。在使用new操作符调用函数时,函数内部将this绑定到一个新对象并返回。如下

    var Person = function(name) {
        this.name = name;
    };

    var shotar = new Person(shotar);
    // 为了区别于普通函数,约定构造函数的首字母大写。使用new操作内部会替你做以下操作:
    Person(name) {
        // 以下都是使用new操作符时内部做的事
        // var obj = new Object();
        // this = obj;
        // obj.name = name;
        // obj.prototype = Person.prototype;

        // return obj;
    }

如果构造函数内部返回了一个不是对象的值,则new会忽略其返回值而返回新建的对象,如果返回的是一个对象则将其返回。另外,如果不使用new操作符调用,并不会在编译时报错,这是非常糟糕的事情,因此,我们通常会在调用的时候检查是否为new操作符调用,如下:

    function Person(name) {
        if (this instanceof Person) {
            this.name = name;
        } else {
            return new Person(name);
        }
    }

4.回调模式
回调函数是在满足某种情况或者达到某种要求时立即调用。回调函数通常作为函数的参数传入,其本质也还是一种普通的函数,只是在特定的情况下执行而已,先看一个例子:

    function sayName(obj) {
        var fullName = '';
        if (obj.firstName && obj.lastName) {
            fullName = typeof obj.computedFullName === 'function' ?
                obj.computedFullName() :
                obj.lastName + ' ' + obj.firstName;
        return fullName;
    }

    var obj = {
        firstName: 'Sanfeng',
        lastName: 'Zhang',

        computedFullName: function() {
            return this.lastName + ' ' + this.firstName;
        }
    };

    sayName(obj);            // Zhang Sanfeng

此处的computedName就是一个回调函数,在给sayName函数传值的时候,我们传入了一个对象,前两个属性都是直接在sayName中使用,如果满足这两个属性都有值,那就调用obj的computedName方法(也就是函数),在此处调用就称他为回调函数,回调函数常用于异步操作的场合,比如ajax请求,当请求成功并返回数据时再执行回调函数。一般也用于同步阻塞的场景下,比如执行某些操作后执行回调函数。请先看下面的异步情况的例子:

    function ajax(callback) {
        var xhr = new XMLHttpReauest(); 

        if (xhr.readystate === 4 && xhr.status === 200) {
            typeof callback === 'function' && callback();
        } else {
            alert('请求失败!')
        }

        xhr.open('get', url);
        xhr.send();
    }

    var fn = function() {
        alert('请求成功!');
    };

    ajax(fn);

这里会有一个问题,如何给回调函数传参,让回调函数在里面处理一些问题,这里我们就可以用到call或者apply方法了。比如有这样一个问题:统计若干个人的考试成绩,只有90分以上的才发奖学金,请看下面同步阻塞的例子:

    function startGive(arr, giveMoney) {
        // 先把分数超过90分的过滤出来
        let adult = arr.filter(item => item > 90);

        // 将过滤结果传入回调函数,发奖金给他们
        return giveMoney.call(null, adult);
    }

    let giveBonuses = function(arr) {
        return arr.map(item => item + 'giveMoney');
    };

    console.log(startGive([70, 80, 92, 96, 85], giveBonuses));        // [ '92giveMoney', '96giveMoney' ]

上面的例子主要是在将分数在90分以上的过滤出来之后再执行操作。回调传参还可以通过传递匿名函数的形式接收该参数,如下例子:

    function fn(arg1, arg2, callback){
        var num = Math.ceil(Math.random() * (arg1 - arg2) + arg2);
        callback(num);
    }
     
    fn(10, 20, function(num){
        console.log("Callback called! Num: " + num); 
    }); 

5.总结
本文讲了关于函数调用的五种模式。五种模式包括函数调用模式、方法调用模式、call/apply调用模式、构造器调用模式和回调模式。其中前三种调用模式类似,主要会涉及到this的指向问题,第四种调用方式总返回一个对象,并将this绑定到此对象。回调模式属于前四种模式中的一种,可以是函数调用模式,也可以是方法调用模式,回调的使用很灵活,其主要场景是用于异步操作或同步阻塞操作的场合。

本文参考《JavaScript语言精粹》一书的函数章节及阮大大的《JavaScript 的 this 原理》一文撰写而出,文中若有表述不妥或是知识点有误之处,欢迎留言指正批评!

总结

以上是脚本宝典为你收集整理的

关于JavaScript函数调用的几种模式

全部内容,希望文章能够帮你解决

关于JavaScript函数调用的几种模式

所遇到的程序开发问题,欢迎加入QQ群277859234一起讨论学习。如果觉得脚本宝典网站内容还不错,欢迎将脚本宝典网站推荐给程序员好友。 本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。

80%的人都看过