理解this、call、bind、apply

67 阅读9分钟

理解this、call、bind、apply

本文摘录而来非原创:

目的:前端知识点学习,留痕备忘

作者:sunshine小小倩

地址:https://juejin.cn/post/6844903496253177863

1、this的指向

在ES5中,其实this的指向,始终坚持一个原理:this永远指向最后调用它的那个对象。

eg1:

var name = "windowname";
function fn() {
    var name = "name";
    console.log(this.name)       // windowname
    console.log("inner:" + this) // window
};

fn();
console.log("outer:" + this)    // window

这里的log打印的是windowname,因为根据原理this永远指向最后调用它的那个对象。我们看最后调用的fn的地方fn(),前面没有调用的对象那么就是全局对象window,这就相当于是window.fn();注意:这里我们没有使用严格模式,如果使用严格模式的话,全局对象就是undefined,那么就会报错Uncaught TypeError: Cannot read property 'name' of undefined

eg2:

var name = "windowname";

var a = {
    name: "name",
    fn: function() {
       console.log(this.name) // name
    }
}

a.fn()

在这个demo中,函数fn是对象a调用的,所以打印的值就是a中的name的值。

下面做一个小小的改动:

eg3

var name = "windowname";

var a = {
    name: "name",
    fn: function() {
        // name
       console.log(this.name)
    }
}

window.a.fn()

打印 name的原因也是因为刚刚那句话 this 永远指向最后调用它的那个对象,最后调用它的对象仍然是对象 a。

看下面一个例子:

eg4

var name = "windowsName";
var a = {
   // name: "Cherry",
   fn : function () {
      console.log(this.name);      // undefined
   }
 }
 window.a.fn();

这里为什么会打印 undefined 呢?这是因为正如刚刚所描述的那样,调用 fn 的是 a 对象,也就是说 fn 的内部的 this 是对象 a,而对象 a 中并没有对 name 进行定义,所以 log 的 this.name 的值是 undefined

这个案例还说明了:this永远指向最后调用它的那个对象。因为最后调用fn的对象是a,所以就算a中没有name这个属性,也不会继续向上一个对象寻找this.name,而是直接输出undefined

再来看一个比较坑的例子:

eg5:

var name = "windowname";

var a = {
    name: null,
    //name: "name",
    fn: function() {
        console.log(this.name)    // windowname
    } 
};

var f =  a.fn;
window.f();    // 这里就等同于 window.fn()

这里可能会有疑问,为什么不是 name,这是因为虽然将 a 对象的 fn 方法赋值给变量 f 了,但是没有调用,再接着跟我念这一句话:this 永远指向最后调用它的那个对象,由于刚刚的 f 并没有调用,所以 fn() 最后仍然是被 window 调用的。所以 this 指向的也就是 window。

由以上五个例子我们可以看出,this 的指向并不是在创建的时候就可以确定的,在 es5 中,永远是this 永远指向最后调用它的那个对象

eg6

var name = "windowsname";

function fn() {
    var name = 'Cherry';
    innerFunction();
    function innerFunction() {
       console.log(this.name);      // windowsName
    }
}

window.fn()

理解,over!

2、怎么改变this的指向

改变this的指向有几种方式:

  • 使用es6的箭头函数
  • 在函数内部使用that = this
  • 使用apply、call、bind
  • new 实例化一个对象

eg7

var name = "windowsname"
var a = {
    name: "name",

    fun1: function() {
        console.log(thi.name);    
    },

    fun2: function() {
        // 调用setTimeOut的对象是window
        setTimeOut(function() {
            this.fun1()
        }, 200)
    }
}

// 等价于 window.a.fun2()
a.fun2()    // this.fun2 is not a  function

在不使用箭头函数的情况下,会报错。因为最后调用setTimeOut的对象是window,但是在window中并没有fun1函数。

2.1 箭头函数

众所周知,ES6中的箭头函数时可以避免ES5中使用this的坑的。箭头函数的this始终指向函数定义时的this,而非执行时。箭头函数需要记着:箭头函数中没有this绑定,必须通过查找作用域链来决定其值。如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this,否则,this为undefined

eg8

var name = "windowname";

var a = {
    name: "name",

    fun1: function() {
        console.log(this.name)
    },

    fun2: function() {
        setTimeOut(() => {
            this.fun1()
        }, 200)
    },
}


a.fun2()    // name

2.2 在函数内部使用 that = this

如果不使用ES6,那么这种方式应该是最简单的不会出错的方式了,我们是先将调用这个函数的对象保存在变量that中,然后在函数中都是用这个that,这样that就不会改变了。

eg9

var name = "windowname"

var a = {
    name: 'name',

    fun1: function() {
        console.log(this.name)
    },

    fun2: function() {
        var that = this
        setTimeOut(function(){
            that.fun1()
        }, 200)
    }
}

在这个例子中,在fun2中,首先设置var that = this;,这里的this是调用fun2的对象a,为了防止在fun2中的setTimeOut被window调用而导致在setTimeOut中的this为window。我们将this(指向变量a)赋值给一个变量that,这样在fun2中我们使用that就是指向对象a了。

2.3 使用call、apply、bind

使用apply、call、bind函数也是可以改变this的指向的。先看下如何实现:

使用apply

eg10

var a = {
    name: "name",

    fun1: function() {
        console.log(this.name)
    },

    fun2: function() {
        setTimeout(function() {
            this.fun1()
        }.apply(a), 200)
    }
}

a.fun2()    // name

使用call

eg11

var a = {
    name: "name",

    fun1: function() {
        console.log(this.name)
    },

    fun2: function() {
        setTimeout(function() {
            this.fun1()
        }.call(a), 200)
    }
}

a.fun2()    // name

使用bind*

eg12

var a = {
    name: "name",

    fun1: function() {
        console.log(this.name)
    },

    fun2: function() {
        setTimeout(function() {
            this.fun1()
        }.bind(a)(), 200)
    }
}

a.fun2()    // name

2.4 call、apply、bind的区别

刚刚我们已经介绍了 apply、call、bind 都是可以改变 this 的指向的,但是这三个函数稍有不同。

理解:①这三个方法是被函数调用的 ②能改变this的指向 ③区别:call传递是字符串,apply传递的是数组,bind传递的也是字符串但会返回成一个方法进行调用

在 MDN 中定义 apply 如下:

apply() 方法调用一个函数, 其具有一个指定的this值,以及作为一个数组(或类似数组的对象)提供的参数。

语法:

fun.apply(thisArg, [argsArray])

thisArg:需要改变当前对象指向对应对象的那个对象

[argsArray]:以数组的方式尽心参数传递

  • thisArg:在 fun 函数运行时指定的 this 值。需要注意的是,指定的 this 值并不一定是该函数执行时真正的 this 值,如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。
  • argsArray:一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 fun 函数。如果该参数的值为null 或 undefined,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。
2.4.1 apply和call的区别

其实 apply 和 call 基本类似,他们的区别只是传入的参数不同。

call 的语法:

fun.call(thisArg[, arg1[, arg2[, ...]]])

所以 apply 和 call 的区别是 call 方法接受的是若干个参数列表,而 apply 接收的是一个包含多个参数的数组。

e13

    var a ={
        name : "Cherry",
        fn : function (a,b) {
            console.log( a + b)
        }
    }

    var b = a.fn;
    b.apply(a,[1,2])     // 3

e14

    var a ={
        name : "Cherry",
        fn : function (a,b) {
            console.log( a + b)
        }
    }

    var b = a.fn;
    b.call(a,1,2)       // 3
2.4.2 bind 和 apply、call区别

使用bind试上面的案例:

    var a ={
        name : "Cherry",
        fn : function (a,b) {
            console.log( a + b)
        }
    }

    var b = a.fn;
    b.bind(a, 1, 2)()       // 3

bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。

3、JS中的函数调用

函数调用的方法一共有4种:

  • 1、作为一个函数调用
  • 2、函数作为方法调用
  • 3、使用构造函数调用函数
  • 4、作为函数方法调用函数(call、apply)

3.1 作为一个函数调用

    var name = "windowsName";
    function a() {
        var name = "Cherry";

        console.log(this.name);          // windowsName

        console.log("inner:" + this);    // inner: Window
    }

    a();

    console.log("outer:" + this)         // outer: Window

这样一个最简单的函数,不属于任何一个对象,就是一个函数,这样的情况在 JavaScript 的在浏览器中的非严格模式默认是属于全局对象 window 的,在严格模式,就是 undefined

但这是一个全局的函数,很容易产生命名冲突,所以不建议这样使用

3.2 函数作为方法调用

更多的情况是将函数作为对象的方法使用。

    var name = "windowsName";
    var a = {
        name: "Cherry",
        fn : function () {
            console.log(this.name);      // Cherry
        }
    }
    a.fn();

这里定义一个对象 a,对象 a 有一个属性(name)和一个方法(fn)。

然后对象 a 通过 . 方法调用了其中的 fn 方法。

然后我们一直记住的那句话this 永远指向最后调用它的那个对象,所以在 fn 中的 this 就是指向 a 的。

3.3 使用构造函数调用函数

如果函数调用前使用了 new 关键字, 则是调用了构造函数。
这看起来就像创建了新的函数,但实际上 JavaScript 函数是重新创建的对象:

// 构造函数
function myFunction(arg1, arg2) {
    this.firstName = arg1;
    this.lastName= arg2;
}


// This creates a new object
var a = new myFunction('Li', 'Cherry');
a.lastName    // 返回"Cherry"

经典面试题: new的过程

1、创建一个空对象obj

2、将创建的空对象的隐式原型指向其构造函数的显示原型

3、使用call改变this的指向

4、如果无返回值或者返回一个非对象值,则将obj返回作为新对象;

     如果返回值是一个新对象的话,那么直接返回该对象

// 伪代码
var a = new myFunction("Li", "Cherry");


new myFunction {
    var obj = {};
    obj.__proto__ = myFunction.prototype;
    var result = myFunction.call(obj, 'Li', 'Cherry');
    return typeof result === 'obj' ? result : obj;
}

所以我们可以看到,在 new 的过程中,我们是使用 call 改变了 this 的指向。

3.4 作为函数方法调用函数(call、apply)

在JavaScript中,函数就是对象

JavaScript函数有它的属性和方法

call() 和 apply() 是预定义的函数方法。两个方法可用于调用函数,两个方法的第一个参数必须是对象本身。

在JavaScript严格模式下(strict mode)下,在调用函数时第一个参数会成为this的值,即使该参数不是一个对象。

在JavaScript非严格模式下(no-strict mode)下,如果第一个参数的值是 null 或 undefined,它既将使用全局对象替代。

eg:

    var name = "windowsName";

    function fn() {
        var name = 'Cherry';
        innerFunction();
        function innerFunction() {
            console.log(this.name);      // windowsName
        }
    }

    fn()

这里的 innerFunction() 的调用是不是属于第一种调用方式:作为一个函数调用(它就是作为一个函数调用的,没有挂载在任何对象上,所以对于没有挂载在任何对象上的函数,在非严格模式下 this 就是指向 window 的)

eg:

    var name = "windowsName";

    var a = {
        name : "Cherry",

        func1: function () {
            console.log(this.name)     
        },

        func2: function () {
            setTimeout(  function () {
                this.func1()
            },100 );
        }

    };

    a.func2()     // this.func1 is not a function

这个简单一点的理解可以理解为“匿名函数的 this 永远指向 window”,你可以这样想,还是那句话this 永远指向最后调用它的那个对象,那么我们就来找最后调用匿名函数的对象,这就很尴尬了,因为匿名函数名字啊,笑哭,所以我们是没有办法被其他对象调用匿名函数的。所以说 匿名函数的 this 永远指向 window。

如果这个时候你要问,那匿名函数都是怎么定义的,首先,我们通常写的匿名函数都是自执行的,就是在匿名函数后面加 () 让其自执行。其次就是虽然匿名函数不能被其他对象调用,但是可以被其他函数调用啊,比如例 7 中的 setTimeOut。