javascript中令人迷惑的this

JS中的this很多时候会让人捉摸不透,不知道这个this到底指向的是什么。现在根据自己的理解写下这篇文章做一个总结。

我们知道this指向谁一般情况下是在运行时决定的,并且运行时决定this指向的因素又有很多,例如是不是被bind了,或者调用的时候使用了apply和call这类方法,还有是不是通过new来调用这个函数,如果没有以上显示绑定,那么是obj.fn()这样调用的吗?或者直接fn()?如果直接fn()调用,那么fn的函数体是严格模式吗?最后这个函数是ES6中的箭头函数吗?

默认绑定和隐式绑定

首先看最常见的调用方式,通过对象调用这个函数或者叫方法(this隐式绑定到了调用函数的对象上)。

var a = 2
var obj = {
    a: 1,
    fn: function () {
        console.log(this.a)
    }
}

obj.fn()

我们通过运行知道打印了1,也就是说这时fn中的this指向了调用他的obj,但是是否表示任何情况下fn中的this都是指向fn定义时的对象obj呢?显然不是的。在某些情况下这种隐式绑定的this会丢失,如下:

var fn1 = obj.fn
fn1()

上面打印了2,是的出乎意料并没有打印1,所以关于this的指向和函数的定义没有什么关系,看似函数fn属于对象obj,其实并不是。这时fn中this默认指向的是window对象。上面通过赋值就弄丢了原本的隐式绑定,没有了隐式绑定,只能使用默认绑定。

现在我们切换到严格模式:

'use strict'
var a = 2
var obj = {
    a: 1,
    fn: function () {
        console.log(this.a)
    }
}

var fn1 = obj.fn
fn1()

我们发现执行fn1的时候报了一个错误Uncaught TypeError: Cannot read property 'a' of undefined,这是因为在严格模式下fn1中的this指向的undefined,获取undefined的属性当然会报错,因为undefined不是一个对象也不能隐式转换成一个对象。

注:上面通过将对象的方法赋值给一个变量导致函数的方法中默认绑定this丢失,这种情况会出现在很多其他意想不到的地方,例如函数的传参(这也是一种隐式的赋值)。

显示调用call和apply的绑定

上面无论是通过对象还是直接通过函数名调用函数,其中的this指向谁好像编译器心里有数是一种默契。那么我们能不能不要这个默契,我们自己来指定函数调用的时候this指向谁。我们通过call方法和apply方法就可以轻易做到。

var a = 2
var obj = {
    a: 1,
    fn: function () {
        console.log(this.a)
    }
}
var fn1 = obj.fn
fn1.call(obj)

我们可以看到又打印出了1,不负所望。调用fn1的时候我们通过结果可以知道函数体内的this被绑定到了obj上。apply做的事情和call是一样的,区别就在于传入函数中参数的形式,call必须要和调用函数一样一个一个传入参数,但是apply允许我们通过一个数组将需要的参数一起传入函数中。这个神奇的功能就像是ES6中的 … 操作符。

bind的绑定

bind的也可以绑定函数中的this,但是和上面的call和apply有明显的不同,call和apply是直接就执行了函数,但是bind不是,bind会返回一个函数,这个特性就让这个bind不仅仅可以绑定this,还可以进行函数柯里化。

var a = 2
var obj = {
    a: 1,
    fn: function () {
        console.log(this.a)
    }
}
var fn1 = obj.fn
var fn2 = fn1.bind(obj)
fn2()

不出意料的也打印了1。

bind的简单Polyfill

if (!Function.prototype.bind) {
    Function.prototype.bind = function (obj) {
        // 这一句检测this是不是函数我本以为是多余,但是bind是可以被call的,这时候this就很有可能不指向function
        if (typeof this !== 'function') {
             throw new TypeError('不是函数的数据尝试调用bind方法!')
        }
        // obj 就是函数要绑定的this,而函数就是现在函数体中的this,因为bind函数是在Function.prototype上的
        // 这是在获取在bind的时候就传过来的参数
        var args = [].slice.call(arguments, 1)
        // 存一下需要bind的函数
        var fn = this    
        // 处理fn函数的prototype属性
        var _fn = function () {}
        // 这个函数将被返回
        var bindFn = function () {
            // 处理一下被bind的函数使用new调用的时候
            return fn.apply(this instanceof bindFn ? this : obj, args.concat([].slice.call(arguments)))
        }        
        // 处理fn.prototype
        if (fn.prototype) {
            _fn.prototype = fn.prototype
        }
        
        bindFn.prototype = new _fn()

        return bindFn
    }
}

上面的兼容是类似mozilla的写法,不仅仅绑定了this还考虑到了参数的拼接,还有函数的prototype属性的处理,还包括被bind的函数作为构造函数调用的时候其中this的指向。

new的this绑定

可以绑定this的不仅仅是上面的call,apply和bind,new也可以的。我们知道通过new调用一个函数的时候会有下面几个步骤:

  1. 新建一个对象
  2. 将对象的原型关联到函数的原型属性
  3. 将this指向这个对象
  4. 执行函数
  5. 如果函数没有返回值则返回这个对象

看上面第二步就将函数中的this关联到了新建的对象上了。那么对于一个bind的函数我们使用new来调用函数中的this到底是指向了new新建出来的对象还是bind时候的对象呢?

其实上面bind方法的Polyfill已经给出了答案,是会指向new新建出来的对象。fn.apply(this instanceof bindFn ? this : obj, args.concat([].slice.call(arguments))),这里通过this instanceof bindFn判断是不是通过new调用的该方法,如果是那么就指向当前的this也就是新建的对象,如果不是才指向传进来的obj。

##绑定的优先级

通过上面bind的Polyfill我们知道new绑定的this优先级要大于显示绑定的bind,并且bind的绑定优先级要高于call和apply方法。

隐藏是绑定优先级要高于默认绑定并且低于显示绑定的call和apply方法。

所以整理出来的优先级如下:

new > bind > (apply == call) > 隐式绑定 > 默认绑定

关于箭头函数

ES6的箭头函数和上面说的情况都不一样,箭头函数中的this指向并不是在调用的时候确定的,而是在定义的时候,和定义的时候的词法作用域有关,并且后期并不能通过上面显示绑定的方法修改this的指向。也就是说箭头函数定义的时候拿到当前上下文的this,然后就不会再改变了。

var a = 2
var obj = {
    a: 1,
    fn: function () {
        console.log(this.a)
        var b = () => {
            console.log(this)
        }
        b()
    }
}
obj.fn()

上面打印出了1 和 obj 对象

var a = 2
var obj = {
    a: 1,
    fn: function () {
        console.log(this.a)
        var b = () => {
            console.log(this)
        }
        b.call(window)
    }
}
obj.fn()

虽然使用了call指定this绑定,但是还是打印了1和obj对象,而不是window。call方法并没能修改箭头函数的this指向。

var a = 2
var obj = {
    a: 1,
    fn: function () {
        console.log(this.a)
        var b = () => {
            console.log(this)
        }
        var c = b.bind(window)
        c()
    }
}
obj.fn()

结果和call的绑定一致没有改变箭头函数中的this。

那么能使用new呢?箭头函数不能使用new调用,会报错的。

参考

你不知道的javascript上卷

发布了48 篇原创文章 · 获赞 52 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/letterTiger/article/details/85611842
今日推荐