深入学习js之this的那些事

不得不说this是初学者对于理解js的一座山,现在我总结一些爬山心得。。

1.为啥要用this

function identify() {
				return this.name;
			}
			function speak() {
				var greeting = "Hello" + identify.call(this);
				console.log(greeting);
			}

			var you = {
				name: "xiaowang"
			}
			var me = {
				name: "dawang"
			}

			identify.call(me);//xiaowang
			identify.call(you);//dawang

			speak.call(me);//Helloxiaowang
			speak.call(you);//Hellodawang

如果不太理解这段代码,没关系后面会讲。

这段代码可以在不同的上下文对象(me和you)中重复使用函数huidentify()和speak(),不用针对每个对象编写不同版本的函数。

如果不使用this,那就需要给identify()和speak()显式传入一个上下文对象。

function identify(context) {
				return context.name;
			}
			function speak(context) {
				var greeting = "Hello" + identify(context);
				console.log(greeting);
			}

			var you = {
				name: "xiaowang"
			}
			var me = {
				name: "dawang"
			}

			identify(me);//xiaowang
			identify(you);//dawang

			speak(me);//Helloxiaowang
			speak(you);//Hellodawang
显然,this用了一种更优雅的方式来隐式传递一个对象引用,因此可以将API设计得更加简洁并且易于复用。随着代码的复杂度提高,显式传递上下文对象会让代码越来越混乱。使用this则会有序的多。

作为一个有一点java开发基础的人,当初我看到this是下意识的认为this指向他自己,然后事实就是this并不像我们所想的那样指向函数本身。


那么this究竟是什么? this是在运行时绑定的,并不是在编写时绑定的,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有关系,只取决于函数的调用方式。当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。


2.理解this的绑定规则


在理解this绑定之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置。只有仔细分析调用位置才能回答这个问题:这个this到底引用的是什么?

通常来说,寻找调用位置就是寻找函数被调用的位置,但是做起来并没有那么简单,因为某些变沉默是可能会隐藏真正的调用位置,

最重要的是要分析调用栈。

下面来看看到底什么是调用栈和调用位置:

function baz() {
//当前调用栈是:baz
//因此,当前调用位置是全局作用域


console.log("baz");
bar();//bar的调用位置
}
function bar() {
//当前调用栈是baz->bar
//因此,当前调用位置在baz中


console.log("bar");
foo();
function foo() {
console.log(this.a);
}
var a = 2;
foo();


}function foo() {//当前调用栈是baz->bar->foo//因此,当前调用位置在bar中console.log("foo");}baz();//baz的调用位置
注意分析如何从调用栈中分析出真正的调用位置,因为它决定了this绑定。
绑定规则
①默认绑定
首先要介绍的是最常用的函数调用:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。
function foo() {
console.log(this.a);
}
var a = 2;
foo();


首先,声明在全局作用域的变量就是全局对象的一个同名属性,本质上是一个东西,接下来当调用foo()的时候,this.a被解析成了全局变量a。因为在本例中,函数调用时应用了this的默认绑定,因此this指向全局变量。
那么什么时候是默认绑定呢?可以通过分析调用位置来看看foo()是如何调用的。在代码中,foo()是直接使用不带任何休息的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。
如果使用严格模式,则不能将全局对象用于默认绑定,因此this会绑定到undefined
function foo() {
"use strict";


console.log(this.a);
}


var a = 2;
foo();//TypeError: this is undefined
这里有个细节。只有foo()运行在非严格模式下时,默认绑定才能绑定到全局对象;在严格模式下调用foo()则不影响默认绑定:
function foo() {
console.log(this.a);
}


var a = 2;
(function () {
"use strict";
foo();//2
})();


②隐式绑定
另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。
思考以下代码
function foo() {
console.log(this.a);
}


var obj = {
a: 2,
foo: foo
};


obj.foo();//2
foo();//undefined
首先需要注意的是foo()的声明方式,及其之后是如何被当作引用属性添加到obj中的。但是无论是直接在obj中定义还是先定义再添加为引用属性,这个函数探戈来说都不属于obj对象。
然而,调用位置会使用obj上下文来引用函数,因此你可以说函数被调用obj对象“拥有”或“包含”
当函数引用有上下文对象时,隐式绑定规则会把函数嗲用中的this绑定到这个上下文对象。因为调用foo()时this被绑定到obj,因此this.a和obj.a。
对象属性链中只有上一层或者说最后一层在调用位置中起作用。举例来说:
function foo() {
console.log(this.a);
}


var obj1 = {
a: 2,
obj2: obj2
};
var obj1 = {
a: 3,
foo: foo
};


obj1.obj2.foo();//2


隐式丢失
一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或undefined上(取决于是否是严格模式)
思考下面的代码
function foo() {
console.log(this.a);
}


var obj1 = {
a: 2,
foo:foo
};

var bar = obj.foo;
var a = "oops, global";
bar();//"oops, global"
虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()是一个不带任何修饰foo的函数调用,因此默认绑定。
还有一种更常见的情况出现在回调函数中】
function foo() {
console.log(this.a);
}
function doFoo(fn) {
// fn 其实引用的是foo
fn();
}
var obj1 = {
a: 2,
foo:foo
};

var a = "oops, globel";
doFoo( obj.foo ); //"oops,global"
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样。


③显式绑定
就像我们刚才看到的那样,在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接绑定到这个对象上。
那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?
具体来说可以使用函数的call和apply方法。
js提供的绝大多数函数以及你自己创建的所有函数都可以使用call和apply方法
这两个方法是如何工作的呢?它们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。因为你可以直接指定this的绑定对象,因此我们称之为显式绑定。
看看以下代码
function foo() {
console.log(this.a);
}


var obj  = {
a: 2
};


foo.call(obj);//2


通过foo.call,我们可以在调用foo时强制把它的this绑定到obj上
如果传入的是原始值(string、number、boolean)来当作this的绑定对象,这个原始值会被转换成它的对象形式(new String()...),这通常被称为装箱。


从this绑定的角度看,call和apply是一样的,区别在于传入参数,call是接受多个参数而apply最多接收两个参数,第二个参数已数组的形式传入。


显式绑定依然无法解决我们之前提出的丢失绑定问题
但是显式绑定的一个变种可以解决


function foo() {
console.log(this.a);
}


var obj  = {
a: 2
};

var bar = function () {
foo.call(obj);
};


bar();//2
setTimeout(bar,100);//2
//硬绑定的bar不可能再修改他的this
bar.call(window);//2
首先创建了函数bar(),并在它的内部手动调用了foo.call(obj),因此强制把foo的this绑定到哦额obj,之后从外部绑定bar的this到window也没用,毕竟最后调用bar还是要执行foo.call(obj),无论如何调用函数bar,它总会手动在obj上调用foo。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。


由于硬绑定是一种非常常用的模式,所以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);//2 3
console.log(b);//5
bind(..)会返回一个新函数,它会把你自定的参数设置为this的上下文并调用原始函数


是不是每一次用到this绑定都要去显式绑定呢?
第三方库的许多函数包括js的内置函数,都提供了一个可选的参数,通常被称为“上下文”,其作用和bind一样,确保你的回调函数使用指定的this。
function foo(el) {
console.log(el,this.id);
}
var obj = {
id: "awesome"
}


//调用foo时把this绑定到obj
[1,2,3].forEach(foo,obj);
//1 awesome 2 awesome 3 awesome


④new绑定
js中的new和面向对象类语言的new完全不同,如果说其他面向对象类语言是复制,那么js就是关联,具体后面会说。


首先重新定义一下构造函数,在js中,构造函数只是一些使用new操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。


使用内来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
1、创建一个全新的对象
2、这个新对象会被执行[[Prototype]]连接
3、这个新对象会绑定到函数调用的this
4、如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象


思考以下代码
function foo(a) {
this.a = a;
}


var bar = new foo(2);
console.log(bar.a);//2
使用new来调用foo(...)时,我们会构造一个新对象并把它绑定到foo调用中的this上。


3.那么这四个绑定的优先级又是什么?
new>显式>隐式>默认


可以根据下面的顺序来进行判断this绑定
1.函数是否在new中调用?如果是的话this绑定的是新创建的对象。
var bar = new foo();
2.函数是否通过call、apply或者硬绑定调用(bind)?如果是的话绑定的是指定的对象
var bar = foo.call(obj2);
3.函数是否在某个上下文对象中调用(隐式绑定)?如果是的话绑定的是那个上下文对象
var bar = obj1.foo()
4.如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。
var bar = foo();


4.凡是都有例外(绑定例外)
4.1如果你把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:
function foo() {
console.log(this.a);
}


var a = 2;
foo.call(null);//2
那么说明情况下会传入null呢?
一种非常常见的做法是使用apply来展开一个数组,并当作参数传入一个函数,类似地,bind可以对参数进行珂里化(预先设置一些参数):
function foo(a,b) {
console.log("a:" + a + ",b:" + b);
}


//把数组展开成参数
foo.apply(null,[2,3]);
//使用bind进行柯里化
var bar = foo.bind(null,2);
bar(3);//a:2, b:3
然而,总是使用null会出现一些问题,如果某个函数确实使用了this,那绑定规则会把this绑定到全局对象
那么我们可以传入一个特殊的对象去解决,把this绑定到这个对象不会对你的程序产生副作用,它就是一个空的非委托对象。
无论怎么命名,在js中创建一个空对象最简单的方法是Object.create(null).Object.create(null)和{}很像,但是并不会创建Object.prototype这个委托,所以它比{}更空
function foo(a,b) {
console.log("a:" + a + ",b:" + b);
}


//空对象
var nullObject = Object.create(null);


//把数组展开成参数
foo.apply(nullObject, [2,3] );//a:2, b:3


//使用bind进行柯里化
var bar = foo.bind(nullObject, 2);
bar(3);//a:2, b:3
4.2间接引用
在这种情况下,调用这个函数会应用默认绑定规则。
间接引用最容易在赋值时发生:
function foo() {
console.log(this.a);
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4};


o.foo();//3
(p.foo = o.foo)();//2


赋值表达式p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo而不是p.foo()或者o.foo()。根据我们之前说过的,这里会应用默认绑定。


注意:对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式,如果函数体处于严格模式,this会被绑定到undefined,否则this会被绑定到全局对象。
4.3软绑定
之前我们已经看到,硬绑定这种方式可以把this强制绑定到指定的对象,防止函数调用应用默认绑定规则。问题在于硬绑定会大大降低函数的灵活性,使用硬绑定后就无法使用隐式绑定或者显式绑定来修改this
如果可以给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改this的能力。
可以通过一种被称为软绑定的方法来实现我们的效果
if (!Function.prototype.softBind) {
Function.prototype.softBind = function (obj) {
var fn = this;
//捕获所有参数
var curried = [].slice.call(arguments, 1);
var bound = function () {
return fn.apply(
(!this || this === (window || global)) ?
obj : this,
curried.concat.apply(curried,arguments);
);
};
bound.prototype = Object.create(fn.prototype);
return bound;
};
}
function foo() {
console.log("name: " + this.name);
}


var obj = {name: "obj"},
obj2 = {name: "obj2"},
obj3 = {name: "obj3"};


var fooOBJ = foo.softBind( obj );


fooOBJ();//name: obj


obj2.foo = foo.softBind(obj);
obj2.foo();//name: obj2


fooOBJ.call(obj3);//name: obj3


setTimeout(obj2.foo, 10);
//name: obj应用了软绑定
可以看到,软绑定版本的foo()可以手动将this绑定到obj2或者obj3上,但如果应用默认绑定,则会将this绑定到obj。


5.箭头函数
ES6中介绍了一种基于作用域来决定this的机制


function foo() {
retrun (a)=> {
console.log(this.a);
};
}


var obj1 = {
a:2
};
var obj2 = {
a:3
};


var bar = foo.call(obj1);
bar.call(obj2);//2 not 3
foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。(new 也不行!)
箭头函数最常用于回调函数中,列如事件处理器或者定时器
function foo() {
setTimeout(() => {
//这里的this在词法上继承自foo()
console.log(this.a);
},100);
}


var obj = {
a:2
};


foo.call(obj);//2
这里的this会绑定到foo的执行上下文,这里的this一旦绑定就不能修改指的是this绑定到foo的上下文,所以foo的显式绑定改变foo中的this指向会影响foo箭头函数中的this(this继承自foo)




不建议混用箭头函数和this风格


小结:
根据下面的顺序来进行判断this绑定
1.函数是否在new中调用?如果是的话this绑定的是新创建的对象。
var bar = new foo();
2.函数是否通过call、apply或者硬绑定调用(bind)?如果是的话绑定的是指定的对象
var bar = foo.call(obj2);
3.函数是否在某个上下文对象中调用(隐式绑定)?如果是的话绑定的是那个上下文对象
var bar = obj1.foo()
4.如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。
var bar = foo();


要注意,有些调用可能在无意中使用默认绑定规则。可以使用 var nullObject = Object.create(null)以保护全局变量
ES6中的箭头并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说this会继承外层函数调用的this绑定。这其实和ES6之前的self=this机制一样




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值