this既不指向函数自身,也不指向函数的词法作用域!它指向谁完全取决于它在哪里被调用,被谁调用!
绑定规则
总体来说,this的绑定规则有:
- 默认绑定(严格模式/非严格模式)
- 隐式绑定
- 显式绑定
- new存在时绑定
- 箭头函数绑定
1.默认绑定:
默认绑定就是没有应用其他绑定规则时的绑定方式。
在非严格模式下,直接调用函数默认this指向全局对象,即window。这里要注意!即使是在某个函数中,如果自执行某个函数或者是使用setTimeout内部执行某个函数,它的this都指向全局变量!
var value = 1; var obj = { value: 2 } function funA() { setTimeout(function () { console.log(this.value) }, 100) } funA.call(obj) // 1,这里setTimeou相当于window调用这个函数,即默认调用。
如果想输出obj内部定义的value,可以在setTimeout外层设变量var that = this; 然后console.log(that.value)即可。将this设定指向本函数的this。
在严格模式下,并不能将全局对象用于默认绑定,this会绑定到undefined
function foo() { 'use strict' // 严格模式下会输出undefined,非严格模式输出2 console.log(this.a); } var a = 2; foo()
但是在严格模式下,调用函数不影响默认绑定!
function foo() { console.log(this.a); } var a = 2; (function () { 'use strict' foo() // 在严格模式下调用函数并不影响默认绑定!所以输出2 })()
2.隐式绑定:
就是前面有对象引用时,this会指向这个对象,如果有多层,则只有上一层起作用!并不会一直向上寻找!!!切记!!!
var obj = { a: 1, b: 2, getA: function () { console.log(this.a) }, getB: { b: 4, subGetA: function () { console.log(this.a) }, subGetB: function () { console.log(this.b) } } } obj.getA() // 1 obj.getB.subGetA() // undefined,这里因为getB中并没有a,因此输出undefined obj.getB.subGetB() // 4
有时候会有隐式丢失的情况出现,比如:
// 代码同上,加如下语句: var a = "global" var other = obj.getA // 将obj.getA赋值给other,other执行的时候其实是相当于 other(){ console.log(this.a) } other() // "global"
此时,other函数实际上是var other = function(){ consle.log(this.a) },相当于默认绑定this,this指向window!(自我感觉这时的other其实与obj和getA摆脱了关系,刚才那一步也就是进行了赋值操作而已~)
间接引用时(最容易发生在赋值操作!!),调用函数会应用默认绑定规则:
function foo() { console.log(this.a) }; var a = 1; var obj = { a: 3, foo: foo }; var fun = { a: 2 }; (fun.foo = obj.foo)() // 1 // 如果将上面一句拆成如下两句来写,结果又不同 fun.foo = obj.foo fun.foo() // 2
(fun.foo = obj.foo)得到的结果是( function foo(){ console.log(this.a) } ),所以这个函数执行等同于自执行foo这个函数,this指向全局对象
两句拆开后,第一句是将obj的foo函数赋值给fun,此时,fun对象内部也有了一个foo函数,那fun再调用foo则是应用了this的隐式调用规则,this指向调用对象fun,因此这时输出2
函数中传入参数也属于隐式丢失的一种:
var obj = { a: 1, getA: function () { console.log(this.a) } } var a = "global"; function getFn(fn) { // 这里的fn = obj.getA,相当于上面一例当中的other = obj.getA fn() } getFn(obj.getA) // global
此时,obj.getA当作参数传到getFn中,这时在getFn中调用它等同于默认调用,this指向全局对象或undefined。
与此情况类似的还有使用定时器调用函数,setTimeout(function(){ ... //在函数中的this也存在隐式丢失 }, time),原理同参数传递!
3.显示绑定:使用call()/apply()
call()和applay()的区别:前者接受的是若干参数的列表,而后者接受的是一个包含多个参数的数组
fn.call(obj, arg1, arg2,...)
fn.apply(obj, [arg1, arg2,...])
延伸一下:
fn.bind(obj, [arg1, [arg2, [, ...]]])
bind的特性:
- 指定this
- 返回一个函数
- 可以指定参数
- 柯里化
call() applay()和bind的区别:call和apply是直接执行了前面的fn函数,而bind()返回的是一个新函数,当再次调用它时,它的this值会绑定到obj上。
call()和apply()中的第一个参数为一个对象,使用call()或applay()会默认将this绑定到这个对象上。
function foo() { console.log(this.a) } var obj = { a: 3 } var fun = function () { foo.call(obj) // 强制把foo的this绑定到了obj上 } fun() // 3 setTimeout(fun, 1000) // 3 fun.call(window) // 3 fun函数内部已经存在硬绑定,不可能再修改它的this
显示绑定无法解决丢失绑定的问题。?????啥意思????
有时候把null或者undefined作为this的绑定对象传入call()、apply()的时候,实际应用的是默认规则!即相当于没有应用显示绑定!!
但是,有以下两种情况可以利用null:
- 使用apply()展开一个数组,因为apply的语法是:apply(obj, [para1, para2, para3...]),参数放在一个数组当中传入到函数中
- 利用bind预先设置一些参数
function foo(a, b) { console.log("a:" + a + ",b:" + b); } // 1. 利用null把数组“展开”成参数 foo.apply(null, [2, 3]); // a:2,b:3 // 2. 利用bind(..)进行柯里化(即预先设置一些参数) var bar = foo.bind(null, 2); bar(3); // a:2,b:3
但是,传入null会带来一些副作用:比如某个函数确实使用了this,那默认绑定规则会把this绑定到全局对象中
所以,可以用var empty = Object.create(null);生成一个空对象,用它代替null,此时的empty不会创建Object.prototype这个委托,比{}更空!
4.new绑定:
构造函数和普通函数的差别就在于:构造函数是被new操作符调用的函数,会生成一个实例对象。该实例对象有两个特性:1. 能访问到构造函数内部的属性;2. 能访问到原型里的属性
使用new调用函数,会自动执行如下操作:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此,this指向了这个新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
function create() { // 创建一个对象 var obj = new Object(); // 将arguments的第一个参数,即作为要传入的构造函数 var Constructor = [].shift.call(arguments); // 将obj的原型指向构造函数 obj.__proto__ = Constructor.prototype; // 改变Constructor的this指向,指到obj对象,并给obj添加新属性arguments Constructor.apply(obj, arguments); // 返回obj return obj; }
解析:
- [].shift的作用是将arguments这个伪数组对象转成数组,等价于:Array.prototype.shift;因为arguments只能获取索引值和length,没有数组的一系列方法,因此加call是通过显示绑定让arguments变相有shift这个方法
- var Constructor = [].shift.call(arguments);这句其实得到的就是create这个函数,并将其赋给Constructor
- 但是还有一个问题是:需要判断return的值是不是对象!!!
如果函数没有return返回值 or return的是String/Number/null,那么new表达式中的函数调用会自动返回这个新对象;
如果函数return了一个Object类型,则this指向return返回的对象。
所以create()得到如下优化代码:
function create() { var obj = new Object(); var Constructor = [].shift.call(arguments); obj.__proto__ = Constructor.prototype; var result = Constructor.apply(obj, arguments); return typeof result == 'object' ? result : obj; }
__proto__是访问器属性,通过它可以访问到对象的内部属性[[prototyoe]],
但是在使用时并不推荐使用__proto__,原因有两点:
- __proto__在ES6时才被标准化,存在浏览器兼容问题
- 通过改变一个对象的[[prototype]]去改变和继承对象的属性会造成严重的性能问题,所以应该尽量避免去改变一个对象的[[prototype]]属性
因此,如果要创建一个对象并且继承这个对象的[[prototype]],这里推荐使用Object.create(),因此代码优化如下:
function create() { var Constructor = [].shift.call(arguments); var obj = Object.create(Constructor.prototype); var result = Constructor.apply(obj, arguments); return result instanceof Object ? result : obj; }
5.箭头函数(不按照以上四种绑定规则,而是由外层(函数或者全局)作用域来决定this)
以上四种绑定规则实际上总结为:this总是指向调用该函数的对象!
箭头函数的this总结:
- 箭头函数不绑定this
- 其this寻值行为与普通变量相同,是在作用域中逐级寻找
- 无法通过bind、call、apply来直接修改(可以间接修改)
- 改变作用域中的this指向可以改变箭头函数的this
function foo() { return (a) => { console.log( this.a ); // this指向的是foo(),因为箭头函数在foo函数这个作用域中 }; } var obj1 = { a: 2 }; var obj2 = { a: 3 } var bar = foo.call( obj1 ); bar.call( obj2 ); // 2,不是3!这里实际是foo.call(obj1).(obj2)
foo的this已经绑定到了obj1上,foo()的内部创建了一个箭头函数会捕捉foo()的this,箭头函数的绑定无法被修改!!即使使用new也不会修改!
并且我们知道通过硬绑定后不能再次修改它的绑定,所以这里是把foo里的this指向obj1而不是obj2
几种绑定方式的优先级:
new绑定 > 显示绑定 > 隐式绑定 > 默认绑定