什么是this
this是JS中定义的一个保留字,被自动的定义在作用域中,每一个作用域中有且只有一个this。
为什么学习this
看个例子
function fun() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, I'm " + fun.call( this );
console.log(greeting)
}
var me ={ name: "Kyle"};
var you = { name: "Reader"};
fun.call( me ); // KYLE
fun.call( you ); // READER
speak.call( me ); // Hello, I'm KYLE
speak.call( you ); // Hello, I'm READER
// 这两个函数,在不同的上下文环境里(me, you)被重用了!省事儿,不需要写两个重复的函数。
// 当然,也可以直接传一个上下文对象context给他们
function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identify( context );
console.log( greeting );
}
identify( you ); // READER
speak( me ); // Hello, I'm KYLE
this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计得更加简洁并且易于复用。随着你的使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this 则不会这样。
this是运行的时候才被绑定的。它的上下文,取决于据函数是如何被调用的,和函数在哪里定义完全无关。
this绑定规则
调用位置(call-site):就是函数在代码中被调用的位置 。
调用栈(call-stack):到达当前执行位置所调用的全部函数。
一个例子理解上面两个名词
function foo() {
//foo函数调用栈 全局 => foo
//调用位置为全局
bar(); // bar的调用位置
}
function bar () {
// bar的函数调用栈 全局 => foo => bar
baz(); // baz的调用位置
}
function baz() {
// baz的函数调用栈 全局 => foo => bar =>baz
console.log('baz');
}
// foo的调用位置
foo();
- 默认绑定
发生在最常见的独立函数调用情况。
function foo() {
console.log(this.a);
}
var a = 2;
foo();
// 结果:2。对于浏览器来说,是window对象,对于node来说,是global obeject。
"use strict"
function foo() {
console.log(this.a);
}
var a = 2;
foo();
// 结果:Uncaught TypeError: Cannot read property 'a' of undefined
没有其它绑定规则时,应用默认规则, 上面的例子在函数调用时,应用了this的默认绑定,因此this指向全局对象。
严格模式下 默认绑定会报错,如果使用严格模式 全局对象将无法使用默认绑定。
- 隐式绑定
1. 常规隐式
调用位置(call-site)是否有上下文对象,或者是某个对象拥有或包含。 例子:
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo();
// 结果:2
foo()先被定义,之后作为一个引用属性被加在obj上了。其实obj从来没有拥有过foo()但是因为调用位置(call-site) 用了obj作为上下文来引用foo(),所以可以看成是:foo()被调用的时候,obj拥有foo()的引用。因为foo()被obj引用了,相当于有了上下文,所以this就被绑在了这个对象上。this.a就相当于obj.a。
function foo() {
console.log( this.a );
}
var obj2 ={
a: 42,
foo: foo
};
var obj1={
a: 2,
obj2: obj2
};
obj1.obj2.foo();
// 结果:42
对象引用链中只有最后一层会影响调用位置。
2. 隐式丢失
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
// 函数别名!
var bar = obj.foo;
// a 是全局对象的属性
var a = "oops, global";
bar();
// 结果:oops, global
虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
另外一种是传入回调函数时。
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn 其实引用的是 foo
fn(); // <-- 调用位置!
}
var obj={
a: 2,
foo: foo
};
// a 是全局对象的属性
var a = "oops, global";
doFoo( obj.foo );
// 结果:oops, global
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一 个例子一样。用JS内置的setTimeout()测试下:
function foo() {
console.log( this.a);
}
var obj ={
a: 2,
foo: foo
};
// a 是全局对象的属性
var a = "oops, global";
setTimeout( obj.foo, 100 );
// 结果:oops, global
- 显示绑定
通过使用call apply 方法
function foo() {
console.log( this.a );
}
var obj={
a:2
};
foo.call( obj );
// 结果:2
上面这种写法还是能被随意更改this指向,怎么解决this被其他库覆盖改写的问题呢?
- 硬绑定
function foo() {
console.log( this.a );
}
var obj={
a:2
};
var bar = function() {
foo.call( obj );
};
bar();
setTimeout( bar, 100 );
// 硬绑定的 bar 不可能再修改它的 this
bar.call( window );
// 结果:2
// 结果:2
// 结果:2
在函数bar的内部手动调用 了 foo.call(obj),因此强制把 foo 的 this 绑定到了 obj。无论之后如何调用函数 bar,它 总会手动在 obj 上调用 foo。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。
硬绑定的典型用法:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
// 简单的辅助绑定函数
function bind(fn, obj) {
return function() {
return fn.apply( obj, arguments );
};
}
var obj={
a:2
};
var bar = bind( foo, obj );
var b = bar(3);
console.log( b );
// 结果:2 3
// 5
由于硬绑定是一种非常常用的模式 以在 ES5 中提供了内置的方法 Function.prototype. bind,它的用法如下:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj={
a:2
};
var bar = foo.bind( obj );
var b = bar(3);
console.log( b );
// 结果:2 3
// 5
- new绑定
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
1. 创建(或者说构造)一个全新的对象。
2. 这个新对象会被执行[[Prorotype]]连接。
3. 这个新对象会绑定到函数调用的this。
4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function Foo(a) {
this.a = a;
}
var bar = new Foo(2);
console.log(bar.a)
// 结果:2
使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。
优先级
默认绑定是优先级中最低的,我们来比较下 其它的优先级 隐式绑定和显式绑定的优先级,我们来对比下:
function foo() {
console.log(this.a);
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo();
obj2.foo();
obj1.foo.call( obj2 );
obj2.foo.call( obj1 );
// 结果:2
// 3
// 3
// 2
隐式绑定和new 优先级
function Foo(name){
this.name = name;
}
var obj = {
name : "obj",
foo:Foo
}
var obj2 = new obj.foo("new")
console.log(obj2)
// 结果:Foo {name: "new"}
new 和 显示 优先级 由于new没法直接与apply和 call 使用,我们用硬绑定的方式来判断 new foo.call(obj) 是不允许的。
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar(2);
console.log( obj1.a );
var baz = new bar(3);
console.log( obj1.a );
console.log( baz.a );
// 结果:2
// 2
// 3
bar被硬绑定到obj1上,但是new bar(3)并没有像我们预计的那样把obj1.a 修改为 3。相反,new 修改了硬绑定(到 obj1 的)调用 bar(..) 中的 this。因为使用了 new 绑定,我们得到了一个名字为 baz 的新对象,并且 baz.a 的值是 3。
总结一下this的判断流程:
从call-site 调用位置 开始,逐一判断下列条件,第一个符合的就是当前有效规则。
1 函数调用前面有没有new?有 this->新建对象
2 有没有call/apply?有 this->绑定指定对象
3 函数有没有上下文?有 this->绑定上下文对象
4 其他情况就是默认绑定,严格模式指向全局报错
例外的this
- 被忽略的this
function foo() {
console.log( this.a )
}
var a = 2;
foo.call( null );
// 结果:2
如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
- 箭头函数
箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决 定 this。
function foo() {
setTimeout(() => {
// 这里的 this 在此法上继承自 foo()
console.log( this.a );
},100);
}
var obj={
a:2
};
foo.call( obj );
// 结果:2
- ES6之前
function foo() {
var self = this;
setTimeout( function(){
//这是一个闭包
console.log( self.a );
}, 100 );
}
var obj={
a:2
};
foo.call( obj );
// 结果:2
- 当this遇到return
如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例。
function fn() {
this.b = "哈哈";
return {
b: "嘿嘿"
};
};
var a = new fn();
console.log(a.b);
// 结果:嘿嘿
function fn() {
this.b = "哈哈";
return 123
};
var a = new fn();
console.log(a.b);
// 结果:哈哈
测试
var object = {
myName:"object",
getName: function() {
return function() {
console.log(this)
console.info(this.myName)
}
}
}
object.getName()()
var o = {
a:10,
b:{
fn:function(){
console.log(this.a);
}
}
}
o.b.fn();
var o = {
a:10,
b:{
a:12,
fn:function(){
console.log(this.a);
console.log(this);
}
}
}
var j = o.b.fn;
j()
var obj = {
a: 'obj'
}
var a = 'window'
var arr = [1, 2, 3]
arr.filter(function (i) {
console.log(i, this.a)
return i > 2
}, obj)