资讯专栏INFORMATION COLUMN

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

邹强 / 496人阅读

摘要:函数的调用有五种模式方法调用模式,函数调用模式,构造器调用模式,调用模式以及回调模式,下面分别对这几种模式进行说明。构造器调用模式构造函数的调用方式被称为构造器调用模式,这是模拟类继承式语言的一种调用方式。

函数的调用有五种模式:方法调用模式,函数调用模式,构造器调用模式,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 原理》一文撰写而出,文中若有表述不妥或是知识点有误之处,欢迎留言指正批评!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/99489.html

相关文章

  • 影响JavaScript中this指向几种函数调用方法

    摘要:前言初学总会对指向感到疑惑,想要深入学习,必须先理清楚和相关的几个概念。中总是指向一个对象,但具体指向谁是在运行时根据函数执行环境动态绑定的,而并非函数被声明时的环境。除去不常用的和的情况,具体到实际应用中,指向大致可以分为以下种。 前言 初学javascript总会对this指向感到疑惑,想要深入学习javascript,必须先理清楚和this相关的几个概念。javascript中t...

    Drinkey 评论0 收藏0
  • JavaScript 严格模式下this几种指向

    摘要:前言曾经被中的弄晕了,今天整理总结一下在严格模式下的几种指向。严格模式构造函数中的事件处理函数中的在严格模式下,在事件处理函数中,指向触发事件的目标对象。 前言 曾经被 JavaScript 中的 this 弄晕了,今天整理总结一下在严格模式下 this 的几种指向。 1. 全局作用域中的this 在严格模式下,在全局作用域中,this指向window对象 use stric...

    smallStone 评论0 收藏0
  • JS常用几种异步流程控制

    摘要:虽然这个模式运行效果很不错,但是如果嵌套了太多的回调函数,就会陷入回调地狱。当需要跟踪多个回调函数的时候,回调函数的局限性就体现出来了,非常好的改进了这些情况。 JavaScript引擎是基于单线程 (Single-threaded) 事件循环的概念构建的,同一时刻只允许一个代码块在执行,所以需要跟踪即将运行的代码,那些代码被放在一个任务队列 (job queue) 中,每当一段代码准...

    Barry_Ng 评论0 收藏0
  • javascript高级程序设计》第六章 读书笔记 之 javascript对象几种创建方式

    摘要:三种使用构造函数创建对象的方法和的作用都是在某个特殊对象的作用域中调用函数。这种方式还支持向构造函数传递参数。叫法上把函数叫做构造函数,其他无区别适用情境可以在特殊的情况下用来为对象创建构造函数。 一、工厂模式 工厂模式:使用字面量和object构造函数会有很多重复代码,在此基础上改进showImg(https://segmentfault.com/img/bVbmKxb?w=456&...

    xiaotianyi 评论0 收藏0
  • 基本方法笔记 - 收藏集 - 掘金

    摘要:探讨判断横竖屏的最佳实现前端掘金在移动端,判断横竖屏的场景并不少见,比如根据横竖屏以不同的样式来适配,抑或是提醒用户切换为竖屏以保持良好的用户体验。 探讨判断横竖屏的最佳实现 - 前端 - 掘金在移动端,判断横竖屏的场景并不少见,比如根据横竖屏以不同的样式来适配,抑或是提醒用户切换为竖屏以保持良好的用户体验。 判断横竖屏的实现方法多种多样,本文就此来探讨下目前有哪些实现方法以及其中的优...

    maochunguang 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<