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

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

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

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

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值。app

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调用模式的时候会讲到。dom

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 原理》一文撰写而出,文中如有表述不妥或是知识点有误之处,欢迎留言指正批评!

相关文章
相关标签/搜索