this 指向(你不知道的javascript版)
一、关于this
为什么我们要用this
this相比于显示的传递上下文对象,会更简洁并且易于复用
对this的误解
1)this并不指向它本身
function foo(num) {
console.log("foo:"+num);
this.count++; //在无意中创建了一个全局变量,值为NaN
}
foo.count = 0; //这里确实是向函数对象foo中添加了一个新属性count,但是函数中的this.count++中的this并不指向这个count
var i;
for(i =0;i<10;i++) {
if(i<5){
foo(i);
}
}
//foo:6
//foo:7
//foo:8
//foo:9
//foo被调用了多少次?
console.log(foo.count);//0
因为 执行foo.count =0 时,确实向函数对象foo中添加了个属性count,但是函数内部的代码this.count中的this并不是指向那个函数对象,若要强制使this指向函数本身,可将foo(i) 改为 foo.call(foo,i) 后面会解释具体原理
2)this并不指向函数的作用域
这个说法在某种情况下是正确的,在某种情况下是错误的,this在任何情况下都不指向函数的词法作用域
3)this 到底是什么
当一个函数调用时,会创建一个活动记录(执行上下文)。这个记录包含函数在哪里被调用(调用栈),函数的调用方式,传入的参数等信息。this就是这个记录的一个属性,会在函数执行时用到。
this是在运行时绑定的,并不是在编写时绑定的,它的上下文取决于函数调用时的各种条件,this的绑定和函数声明的位置没有任何的关系,只取决于函数的调用方式。
this实际上就是在函数调用时发生的绑定,它指向什么完全取决于函数在哪里被调用
二 、this的全面解析
调用位置
调用位置: 函数在代码中被调用的位置(并不是声明的位置),需要分析调用栈(就是为了到达当前执行位置所调用的所有函数)
function baz() {
//当前的调用栈是baz
//因此当前的调用位置是全局作用域
conlose.log("baz");
bar();// bar()的调用位置
}
function bar() {
//当前的调用栈是baz->bar
//当前的调用位置在baz中
console.log("bar");
foo();//foo的调用位置
}
function foo() {
//当前的调用栈是baz -> bar-> foo
//因此当前的调用位置是在bar中
console.log("foo");
}
baz();//baz的调用位置
绑定规则
1.默认绑定
function foo() {
console.log(this.a);
}
var a = 2;
foo();//2
我们可以看到this.a被解析成了全局变量a,在本例中,函数调用应用了this的默认绑定,因此this指向全局变量,通过分析调用位置来看foo()是如何调用的,在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他的规则
在严格模式下,则不能将全局对象用于默认的绑定,因此this会绑定到undefined
且严格模式是指要函数体处于严格模式,而不是调用位置处于严格模式
2.隐式绑定
要考虑调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含
function foo() {
console.log(this.a):
}
var obj = {
a : 2,
foo: foo
};
obj.foo();//2
//调用位置会使用obj上下文来引用函数,因此可以说函数被调用时,obj对象“拥有”或“包含”函数引用
当函数有上下文对象时,隐形绑定规则会把函数调用中的this绑定到这个上下文对象中。因为调用foo()时,this被绑定到obj中,因此this.a和obj.a是一样的
分析隐式绑定的时候,我们必须在一个对象的内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)的绑定到这个对象中
对象属性引用链中只有最顶层或者最后一层在调用位置起作用 如:
function foo() {
console.log(this.a);
}
var obj2 = {
a:42,
foo:foo
};
var obj1 = {
a:2,
obj2:obj2
};
obj1.obj2.foo();//42 这里指42,是最顶层即最后一层的obj2,起作用
隐式丢失 :一个常见的this 的绑定问题就是 被隐式绑定的函数会丢失绑定对象,也就是说,他会应用默认绑定,从而把this 绑定到全局对象上或者undefined,取决于是否在严格模式 如:
function foo(){
console.log(this.a);
}
var obj = {
a :2,
foo:foo
};
var bar = obj.foo;//函数别名
var a = "global";
bar();//"global"//这里的bar()实际上是一个不带任何修饰的函数,所以应用了默认绑定
虽然bar 是obj 的引用 ,但实际上,它引用的是foo函数本身,因为此时,bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
传入回调函数时,会发生一种更意外的情况 如:
function foo() {
console.log(this.a);
}
function doFoo(fn){
fn();
}
var obj = {
a :2,
foo:foo
};
var a = "global";
doFoo(obj.foo);//"global" 参数传递其实就是一种隐式传递,因此我们传入的参数会被隐式赋值,所以结果和上一个一样
将参数传入语言内置函数和上面自己声明的函数, 显示的结果都一样
3.显式绑定
关于call(),apply(),bind() 请看这篇文章
function foo() {
console.log(this.a);
}
var obj = {
a:2
};
foo.call(obj);//2
//通过foo.call() 我们可以在调用foo时强制把它的this 绑定到obj 上
call() 与 apply(功能一样) 传参不一样
硬绑定:
硬绑定的典型应用场景就是创建一个对象,传入所有的参数并返回接收到的所有值
function foo(something) {
console.log(this.a,something);//2 3
return this.a + something
}
var obj = {
a:2
}
var bar = function() {
return foo.apply(obj,arguments)
}
var b = bar(3)
console.log(b); //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); //2 3
console.log(b);//5
bind(…)会返回一个硬编码的函数,他会把你指定的参数设置为this 的上下文对象并调用原始函数
4.new绑定(手动实现new)
先来了解下new操作 用new来调用函数时 会自动执行下面的操作
- 创建 和 构造 一个全新的对象
- 这个对象会被执行[Prototype] 连接
- 这个对象会被被绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a);//2
//使用 new 来调用foo(..)时,我们会构造一个新对象并把它绑定到
//foo(..)调用中的this上
绑定优先级
new绑定 -》 显示绑定 -》 隐式绑定 -》 默认绑定
判断this
1.函数是否new绑定,若是,this绑定的是新创建的对象
2.函数是否为显示绑定,若是,this绑定指定的对象
3.函数是否在某个上下文对象中调用隐式绑定?若是,则绑定那个上下文对象
4.都不是的话 默认绑定 严格模式 绑定undefined 非严格模式
绑定到全局对象
绑定例外
被忽略的this
当你把null 或者undefine 作为this的绑定对象传入call、apply或者bind,这些值在调用时就会被忽略,实际应用的是默认绑定的规则。
但是如果某个函数确实使用了this,那默认绑定规则会将this绑定到全局对象,这将会导致修改全局对象。
若希望this 指向为空 可以考虑 :
function (a,b) {
console.log("a:"+a+",b:" +b);
}
//我们的DMZ空对象
var k = Object.create(null);
//把数组展开成参数
foo.apply(k,[2,3]);//a:2,b:3
//使用bind(..)进行柯里化
var bar = foo.bind(k,2);
bar(3); // a:2,b:3
间接引用
有可能无意中创建一个函数的间接引用,这时,调用这个函数会应用默认绑定 如:
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
//此赋值表达式的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo
//或者o.foo
软绑定
硬绑定会大大降低函数的灵活性,使用硬绑定后就无法使用隐式绑定或者显示绑定来修改this
如果给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式和显示修改this的能力,这就是软绑定
if(!Function.prototype.softBind){
Function.prototype.softBind=function(obj){//先通过判断,如果函数的原型上没有softBind()这个方法,则添加它
var fn=this;
var args=Array.prototype.slice.call(arguments,1);//获取传入的外部参数,这样做是为了函数柯里化
var bound=function(){
return fn.apply( //首先判断调用软绑定之后的函数的调用位置,或者说它的this的指向
//!this(this指向undefined)或者this===(window||global)(this指向全局对象),那么就将函数的this绑定到传入softBind中的参数obj上
//如果此时this不指向undefind或者全局对象,那么就将this绑定到现在正在指向的函数(即隐式绑定或显式绑定)
(!this||this===(window||global))?obj:this,//
args.concat.apply(args,arguments)
);
};
bound.prototype=Object.create(fn.prototype);
return bound;
};
}
软绑定的用法
function foo(){
console.log("name: "+this.name);
}
var obj1={name:"obj1"},
obj2={name:"obj2"},
obj3={name:"obj3"};
var fooOBJ=foo.softBind(obj1);
fooOBJ();//"name: obj1" 在这里软绑定生效了,成功修改了this的指向,将this绑定到了obj1上
obj2.foo=foo.softBind(obj1);
obj2.foo();//"name: obj2" 在这里软绑定的this指向成功被隐式绑定修改了,绑定到了obj2上
fooOBJ.call(obj3);//"name: obj3" 在这里软绑定的this指向成功被硬绑定修改了,绑定到了obj3上
setTimeout(obj2.foo,1000);//"name: obj1"
/*回调函数相当于一个隐式的传参,如果没有软绑定的话,这里将会应用默认绑定将this绑定到全局环
境上,但有软绑定,这里this还是指向obj1*/
this词法
箭头函数 不适用前面所说的this的四条标准规则,而是根据外层(函数或者全局)作用域来决定this。具体来说,箭头函数继承外层函数调用的this绑定(无论this绑定到啥)
function foo() {
//返回一个箭头函数
return (a) => {
//this 继承来自foo()
console.log(this.a);
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
var bar = foo.call(obj1);
bar.call(obj2);//是2 不是3
foo()内部创建的箭头函数会捕获调用时foo()的this,由于foo()的this 绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改(new也不行)
最后我们会得出结论,要判断一个运行函数中的this指向的时候,就需要找到这个函数的直接调用位置,找到之后用以下四个规则来判断this的绑定对象
1、由new调用?绑定到新创建的对象
2、由call或者apply(或者bind)调用?绑定到指定的对象
3、由上下文对象调用?绑定到那个上下文对象
4、默认:在严格模式下绑定到undefined,否则绑定到全局对象
箭头函数的this(箭头函数并不会使用四条标准的绑定规则)
箭头函数体内的this对象,就是定义该函数时所在定义域指向的对象,而不是使用时所在的作用域指向的对象
var name = 'window';
var A = {
name: 'A',
sayHello: () => {
console.log(this.name)
}
}
A.sayHello();// 还是以为输出A ? 错啦,其实输出的是window
由上面可知,箭头函数体内的this对象,就是定义该函数时所在定义域指向的对象 作用域是指函数内部,这里的箭头函数,也就是sayHello,所在的作用域其实是最外层的js环境,因为没有其他函数包裹;然后最外层的js环境指向的对象是winodw对象,所以这里的this指向的是window对象。