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就很有可能不指向functionif (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.prototypeif (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调用一个函数的时候会有下面几个步骤:
- 新建一个对象
- 将对象的原型关联到函数的原型属性
- 将this指向这个对象
- 执行函数
- 如果函数没有返回值则返回这个对象
看上面第二步就将函数中的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上卷