02 知识点回顾——this、call和apply

写在前面

这个系列的文章是通过对《JavaScript设计模式》一书的学习后总结而来,刚开始觉得学习的时候只需看书即可,不用再另外记录笔记了,但是后面发现书中有些内容理解起来并不是很容易,所以结合书中的描述就将自己的理解也梳理了一下并将它记录下来,希望和大家一起学习,文章中如果有我理解错的内容,请各位批评指正,大家共同进步~

目录

写在前面

目录

内容

this基础知识

this丢失问题

call和apply的基础知识

apply和call的用途

总结

内容

this基础知识

在我们学习JS的时候最让人困惑的就是这个this,跟很多语言中不同的是,在JS中的this始终是指向一个对象的,但是this的这个指向并不是在对象定义或者函数定义的时候确定的,而是在我们程序运行的时候它是动态绑定的。下面我们就来看看几种最常见的关于this指向的场景:

1、函数作为对象的方法调用

在我们编写代码时,如果我们将函数作为某一个对象的方法来调用时,例如“obj.code()”这种,那么这时候我们的this始终是指向这个对象的,我们来看下面这段代码:

        var objectone = {
            name: 'xbeichen.cn',
            getName: function () {
                console.log(this === objectone);
                console.log(this.name);
            }
        };
        objectone.getName();

由上可看到,我们的getName函数是objectone对象中的一个方法,在第8行我们通过objectone.getName()来调用这个函数的时候,函数里面的this就动态绑定到了objectone这个对象上面,所以我们得到了上图中的结果。

2、函数作为普通函数调用

在大多数情况下我们将函数仅仅是作为普通函数来调用,这个时候的this默认是指向全局对象的,在我们浏览器的JS里面这个全局对象默认是window对象,但是这种情况在ES5的严格模式下是不太准确的,在ES5的严格模式下函数作为普通函数来调用的时候,我们的this规定为不指向全局对象,而是指向undefined。下面来看下这两种情况:

普通场景:

        window.name = "xbeichen.cn";

        var getName = function () {
            return this.name;
        };
        console.log(getName());

        //或者这样
        var objecttwo = {
            name: "test",
            getName: function () {
                return this.name;
            }
        };

        var getNameNew = objecttwo.getName;
        console.log(getNameNew());

上面两段代码可看到,第一段代码我们很好理解,getName()就是作为一个普通函数来调用,所以函数里面的this默认是指向了全局对象window;第二段代码我们是先定义了一个对象,然后在此对象里也有一个name属性,然后我们接下来将此对象中的getName()方法地址引用到了一个变量getNameNew,然后通过这个变量来调用对象里面的方法的时候,其实就是在通过普通函数的形式来调用,所以此处的this还是指向了全局的window对象,所以我们才看到了上图中的输出结果。

上述的代码如果我们用ES5的严格模式来写,它会是以下情形:

严格模式场景:

        "use strict"

        window.name = "xbeichen.cn";

        var getName = function () {
            return this.name;
        };
        console.log(getName());

        //或者这样
        var objecttwo = {
            name: "test",
            getName: function () {
                return this.name;
            }
        };

        var getNameNew = objecttwo.getName;
        console.log(getNameNew());

上述代码是在ES5的严格模式下运行的,我们可以看到最后运行结果报错了,说是找不到“name”这个属性,但代码里明明指定了window对象的name属性啊,这是为什么呢?原因就是我们之前说的,在ES5的严格模式下,函数作为普通函数调用时它里面的this是指向undefined的,不再指向默认的window对象了,所以我们虽然定义了window对象的name属性,但它运行时依然会报错。为了更加清晰的看到这个this到底在运行时指向了哪,我们再运行下面这段代码看看:

        "use strict"

        window.name = "xbeichen.cn";

        var getName = function () {
            return this;
        };
        console.log(getName());

        //或者这样
        var objecttwo = {
            name: "test",
            getName: function () {
                return this;
            }
        };

        var getNameNew = objecttwo.getName;
        console.log(getNameNew());

在上面的代码中,我们修改了原有代码中函数返回值,让它仅仅返回this,这时候我们的代码没报错,最后返回了undefined,这就验证了我们之前说的:在ES5的严格模式下,函数作为普通函数调用时它里面的this是指向undefined的,不再指向默认的window对象。

函数作为普通函数调用的场景下我们经常会遇见一种情况:在一个dom节点的事件内部有一个局部函数,这个局部函数被作为普通函数调用的时候,它里面的this却指向了window,但我们最初的意愿是将它里面this指向这个dom节点。为了解决上述的问题,我们来看下面的代码:

原始代码:

<body>
    <div id="xbeichen">
        click me
    </div>
    <script>
        window.id = "xbeichen.cn";

        document.getElementById("xbeichen").onclick = function () {
            console.log(this.id);

            var currentCallback = function () {
                console.log(this.id);
            };
            currentCallback();
        }
    </script>
</body>

上述代码运行结果可看到,我们div节点的点击事件中,第一次运行输出命令时这个this是指向我们这个dom节点的,这跟我们的想法一致,但是在点击事件的局部函数currentCallback()中的这个this却指向了window,这就违背了我们最初的意愿,这也是我们JS开发中最典型的“this丢失”的情形。那怎么样才能解决此问题呢,其实方法很简单,既然在局部函数中this的指向改变了,那我们手动将它的指向修改回来就OK了,所以,修改后的代码如下所示:

优化后的代码:

<body>
    <div id="xbeichen">
        click me
    </div>
    <script>
        window.id = "xbeichen.cn";

        document.getElementById("xbeichen").onclick = function () {
            var _self = this;

            console.log(this.id);

            var currentCallback = function () {
                console.log(_self.id);
            };
            currentCallback();
        }
    </script>
</body>

上述优化代码可看到,其实我们就增加了“var _self = this;”这一行代码,作用就是将我们点击事件内部的this绑定在这个自定义的_self变量上面,绑定之后不管你在点击事件内部嵌套了多少层的局部函数,只要用_self变量来获取this,我们获取到的this始终是指向这个dom节点的。

3、构造器调用

从上一节我们了解到JS中没有类,它里面所有的对象都是基于原型拷贝而来,但是我们在编码的时候,可以用构造器和new运算符来创建一个对象,这样的写法看上去构造器也像个类。除了我们JS宿主提供的一个现成的构造器之外,在JS中大部分函数都可以当做构造器来使用,构造器函数和普通的函数在外表看起来是一模一样的,唯一不同的就是它们的调用方式,构造器函数通过new运算符来调用。

当调用构造器函数来创建一个对象的时候,我们的构造器函数总会返回一个对象,通常情况下,我们构造器函数里面的这个this就指向返回的这个对象,如下代码:

        var myBlog = function () {
            this.url = "http://www.xbeichen.cn";
        };

        var blogone = new myBlog();
        console.log(blogone.url);

上面我们说了,通常情况下,用构造器创建对象的时候,构造器里面的this指向的是返回的这个对象,那在特殊情况下是怎样的呢?如果我们用构造器创建对象,构造器显式地返回了一个对象,那我们最终会得到返回的这个对象,而不是之前期待的this,如下代码:

        var myBlog = function () {
            this.url = "http://www.xbeichen.cn";
            return {
                url: "http://www.geov.online"
            }
        };

        var blogone = new myBlog();
        console.log(blogone.url);

上述代码可看出,构造器显式地返回了一个对象,此时我们接收到的url属性是这个显式返回的对象的url属性,并不是构造器中定义的url属性,但如果构造器没有显式返回对象,就不会造成以上的情形,哪怕它显示地返回一个非对象类型数据,如下:

        var myBlog = function () {
            this.url = "http://www.xbeichen.cn";
            return  "http://www.geov.online";
        };

        var blogone = new myBlog();
        console.log(blogone.url);

4、Function.prototype.call和Function.prototype.apply

跟之前的三种情况相比,Function.prototype.call和Function.prototype.apply更多的是运用在改变传入函数的this指向问题上面,下面来看代码简单的了解下,更多的知识将在后半部分做详细介绍:

       var myBlog1 = {
            url: "http://www.xbeichen.cn",
            getUrl: function () {
                return this.url;
            }
        };

        var myBlog2 = {
            url: "https://xuqwblog.blog.csdn.net/"
        };

        console.log(myBlog1.getUrl());
        console.log(myBlog1.getUrl.call(myBlog2));

由上述代码可看到,我们定义了两个对象,第一行输出语句是按正常的书写方式,得到的结果也是我们预期的结果;但第二条语句我们用了call()方法去动态地改变了绑定在myBlog1对象上的this指向,将它改绑到myBlog2对象上,所以输出的是对象2上面的url属性。

以上四种情形是我们编码时最常见的关于this指向的场景,除了以上四种场景外,还有不太常见的with和eval场景,在此处就不做介绍了,大家感兴趣的可以自己百度了解下,因为日常开发中遇到的次数不太多。

this丢失问题

我们来看上文中的一段代码:

        var objecttwo = {
            name: "test",
            getName: function () {
                return this.name;
            }
        };

        console.log(objecttwo.getName());

        var getNameNew = objecttwo.getName;
        console.log(getNameNew());

由上述代码可看到,第一行输出语句正常输出我们的name属性,但是第二行输出语句输出了一个空值,这是因为第一行输出语句中getName()函数是作为对象的方法来调用,所以这里的this指向的是objecttwo这个对象;第二行输出语句输出空值是因为这里的getNameNew()函数虽然是指向了objecttwo.getName的地址,但它终究是通过普通函数方式来调用,所以这里的this默认指向了全局的window对象,输出了空值,如果此处我们用严格模式,那么第二行输出语句则会报错,如图:

这是因为全局window对象在此处我们并没有指定它的name属性。这是this丢失的其中一种情况,接下来我们再看另外一种情况。

上文中我们获取特定dom节点的时候,用了JS原生的写法“document.getElementById("xbeichen")”来获取id为“xbeichen”的一个div,那在编码时这种写法是很繁琐的,我们可以用prototype.js这种框架中的做法一样,将它简写,如下:

        var getId = function (id) {
            return document.getElementById(id);
        };

        getId("xbeichen");

从此在后续过程中要想获取特定的dom节点,我们只需调用getId()这个方法即可,这样就简单了很多,那我们不妨考虑下另外一种更简单的写法:

       var getId = document.getElementById;

        getId("xbeichen");

以上写法比prototype.js这种框架中的写法更加简单,只需一行代码就简化了原生JS获取dom节点的操作,但是在我们浏览器中运行时会报错,这是为什么呢?原来在我们的getElementById()这种函数作为document的方法来调用时,它里面用到了this,此时的this指向是document这个对象,所以用原生写法获取特定dom是可以获取到的,但像我们上述代码,用getId变量引用document.getElementById的地址,然后来调用getId()这个函数时,就变成了普通函数调用方式,此时函数内部的this已经不再指向document对象了,而是指向了默认的window对象,所以此种写法会报错。那接下来我们来修改下我们的代码,运用上部分介绍到的apply()方法来动态改变this指向,代码如下:

   <div id="xbeichen"></div>
    <script>
        document.getElementById = (function (func) {
            return function () {
                return func.apply(document, arguments);
            }
        })(document.getElementById);

        var getId = document.getElementById;

        console.log(getId("xbeichen"));
    </script>

以上是两种最常见的this丢失问题,加上之前dom节点事件中局部函数中的this丢失,已经介绍了三种this丢失问题,更加详细的this丢失情形和解决方法我后期会再整理出一篇文章来单独介绍,各位尽情期待。

call和apply的基础知识

Function.prototype.call和Function.prototype.apply两个方法是从ES3开始对Function的原型定义的两个方法,这两个方法在实际的函数式代码编写开发过程中特别有用,并且在JS的设计模式中也是非常重要和运用广泛的两个方法。call和apply两个方法的作用一模一样,区别仅仅是传入参数的不同而已。

apply方法接受两个参数,第一个参数用于指定函数体内的this的指向,第二个参数是一个数组或者类数组,apply方法会将此集合或者数组中的元素作为参数传递给被调用的函数。

call方法也接受两个参数,第一个参数也用来指定函数体内this的指向,但是从第二个参数开始被依次传入函数。所以说call方法传入的参数数量是不固定的,它更像是apply方法之上的一个语法糖,如果我们知道函数接受多少个参数并且想一目了然的表达形参和实参的对应关系,那我们可以用call方法来传送参数;如果我们不关心具体有多少参数,那我们只需用apply方法把参数一股脑的推到函数里就可以了。下面我们来看看具体的实例代码:

apply方法演示:

       var testfunc = function (a, b, c) {
            console.log([a, b, c]);
        };

        testfunc.apply(null, [1, 3, 2]);

call方法演示:

        var testfunc = function (a, b, c) {
            console.log([a, b, c]);
        };

        testfunc.call(null, 1, 9, 7);

在第一段代码中,我们将三个数字放在了一个数组中通过apply方法传入到了testfunc()函数中,这数组中的三个值分别对应函数参数中的a,b,c三个参数。JS解释器在执行第一段代码时并不会计较形参和实参在数量、类型和顺序上的区别,因为JS的参数在内部就是用一个数组来表示的,所以我们也可以说apply方法比call方法使用率更高。

像上面两段代码一样,我们在使用apply和call方法时,如果第一个参数我们传入的是null,那函数体内的this会默认指向宿主对象,在浏览器中的话就是window对象,如果是在ES5的严格模式下,this指向是null,如下所示:

浏览器环境:

        var testfunc = function (a, b, c) {
            console.log(this === window);
        };

        testfunc.call(null, 1, 9, 7);

严格模式:

        var testfunc = function (a, b, c) {
            "use strict"
            
            console.log(this);
        };

        testfunc.call(null, 1, 9, 7);

以上是对apply和call方法的基础介绍,接下来我们看看两个方法都有哪些用处。

apply和call的用途

上面说过,在JS设计模式里面apply和call方法运用特别广泛,并且在函数式编程里面这两个方法也是尤其重要,那它们到底有什么用呢,我总结了以下三种用途,下面就来详细介绍下各种使用场景。

1、改变this的指向

像上文提到的一样,apply和call方法运用最广泛的就是用来动态的改变函数体内this的指向,如下代码所示:

        window.url = "http://www.geov.online";

        var myblog1 = {
            url: "http://www.xbeichen.cn"
        };

        var myblog2 = {
            url: "https://xuqwblog.blog.csdn.net/"
        };

        var getUrl = function() {
            console.log(this.url);
        };

        getUrl();
        getUrl.apply(myblog1);
        getUrl.apply(myblog2);

上述代码中getUrl()函数普通调用时,函数体内的this默认指向window对象,所以输出了window对象的url属性;但是当用apply方法来分别调用getUrl()函数时,函数体内的this指向分别被动态的绑定到了myblog1和myblog2对象上,所以最终分别输出这两个对象的url属性。

this被改变的场景我们在开发时会经常遇到,上文中我们就提到过两处:一是在dom节点的事件中,局部函数内的this指向会被改变,指向默认的window对象;二是在document.getElementById中修正的this指向场景。

关于dom节点的事件中,局部函数内部this被改变的场景,我们是采用了最简单的处理方式来解决,即定义一个_self变量,用它来保存这个dom节点的引用。在此处我们同样可以用apply或者call的方式来解决,代码如下:

<body>
    <div id="xbeichen">
        click me
    </div>
    <script>
        window.id = "xbeichen.cn";

        document.getElementById("xbeichen").onclick = function () {
            //var _self = this;

            console.log(this.id);

            var currentCallback = function () {
                console.log(this.id);
            };
            currentCallback.call(this);
        }
    </script>
</body>

除了上述dom节点事件内,局部函数内部this被改变的场景,我们在简写document.getElementById方法时也遇到了this被改变的场景,是通过apply方法来解决的,如下:

        document.getElementById = (function (func) {
            return function () {
                return func.apply(document, arguments);
            }
        })(document.getElementById);

        var getId = document.getElementById;

        console.log(getId("xbeichen"));

2、Function.prototype.bind

大部分的浏览器都实现了内置的Function.prototype.bind方法,用来绑定this的指向,代码如下:

        var myblog1 = {
            url: "http://www.xbeichen.cn"
        };

        var getUrl = function () {
            console.log(this.url);
        }.bind(myblog1);

        getUrl();

上述代码通过bind()方法来绑定了this的指向,所以最后输出的是myblog1对象的url属性。如果不用bind来绑定this指向,那我们的getUrl()函数作为普通函数来调用,它里面的this默认是指向window全局对象的。

但是在一些低版本的浏览器中是没有实现Function.prototype.bind方法的,所以如果我们想用bind方法来绑定this会报错,这时候我们可以自己编写一个简化版的bind方法,代码如下:

Function.prototype.bind = function( context ){
	var self = this; 							// 保存原函数
	return function(){ 							// 返回一个新的函数
		return self.apply( context, arguments ); // 执行新的函数的时候,会把之前传入的 context
												// 当作新函数体内的 this
	}
};

var obj = {
	name: 'sven'
};

var func = function(){
	alert ( this.name ); 						// 输出:sven
}.bind( obj);

func();

在以上的代码中,我们是通过Function.prototype.bind来“包装”func函数,并且传入一个对象context当作参数,这个context对象就是我们想修正的this对象。

在Function.prototype.bind的内部实现中,我们先把func函数的引用保存起来,然后返回一个新的函数。当我们在将来执行func函数时,实际上先执行的是这个刚刚返回的新函数。在新函数内部,self.apply(context, arguments)这句代码才是执行原来的func函数,并且指定context对象为func函数体内的this。

3、借用其他对象的方法

apply和call方法除了以上的两种用途之外,还有第三种用途,那就是借用其他对象的方法。这里可以分为两种场景向大家介绍:

3.1、借用构造函数

借用对象方法的第一种场景就是借用构造函数,通过这种技术,我们可以实现一些类似于继承的效果,一起来看下如下代码:

        var myblog1 = function (url) {
            this.url = url;
        };

        var myblog2 = function () {
            myblog1.apply(this, arguments);
        };

        myblog2.prototype.getUrl = function () {
            return this.url;
        };

        var testblog = new myblog2("http://www.xbeichen.cn");
        console.log(testblog.url);

3.2、操作函数的参数列表arguments

在JS中函数的参数列表arguments是一个类数组对象,它也有下标,但它终究不是真正的数组,所以我们并不能直接对它进行排序或者添加新元素等操作。这个时候就需要我们去借用Array.prototype对象上的方法来对arguments进行操作了,例如:如果我们想往arguments中添加一个新元素,就可以借用Array.prototype对象的push()方法,代码如下:

        (function () {
            Array.prototype.push.call(arguments, 3);
            console.log(arguments);
        })(1, 2);

其实在实际开发过程中,如果我们想操作arguments的时候,会非常频繁的借用Array.prototype对象上的方法。但是在借用Array.prototype.push方法时,并不是任何对象都可以借用,借用Array.prototype.push方法的对象必须要满足以下两点要求:

  • 对象本身要可以存取属性;
  • 对象的length属性可读写。

以上就是apply和call方法最常见的三种用途了,目前在开发中其实就第一种和第三种最常用,第二种是为了考虑到有些低版本浏览器不支持bind方法才介绍的。

总结

此篇文章是在第一篇《JavaScript的面向对象》文章基础之上的另一篇知识点回顾,通过这两篇的文章,我们已经简单回顾了一下JS中的面向对象、this、call、apply这些知识点,接下来我们还有一篇文章来最后对JS中的闭包和高阶函数做一下知识点回顾,然后就开始我们JS设计模式的学习之旅。

发布了138 篇原创文章 · 获赞 201 · 访问量 28万+

猜你喜欢

转载自blog.csdn.net/qq_35117024/article/details/104447779