javascript中this的指向问题
this存在于哪里?
this是一个很特别的关键字,被自动定义在所有函数的作用域中。
为什么要使用this,好处是什么?
能将“显式”传参转化为“隐式”传参。更简洁且易于复用。
function say1(){ let message = "Hello,My name is " + this.name; console.log(message); } var I = {name: "Mary"}; var You = {name: "Lily"}; say1.apply(I); //Hello,My name is Mary say1.apply(You); //Hello,My name is Lily //对比: function say2(obj){ let message = "Hello,My name is " + obj.name; console.log(message); } var I = {name: "Mary"}; var You = {name: "Lily"}; say2(I); //Hello,My name is Mary say2(You); //Hello,My name is Lily
这么好。该如何使用,又有什么难点呢?
我们对this有什么误解呢?
1、认为this指向函数自身
foo.count = 0;
var count = 0;
function foo(num){
console.log("foo: "+num);
console.log(this);
this.count++;
}
for(let i=0;i<5;i++){
if(i>2){
foo(i); //foo: 3 foo: 4
}
}
//与我们设想的结果不一样,显然,this并不是指向函数自身
console.log(foo.count); //0
console.log(count); // 2 (增加数量的是window下的count)
//解决方法1:(回避this问题) bar.count = 0; function bar(num){ console.log("bar: "+num); bar.count++; } for(let i=0;i<5;i++){ if(i>2){ bar(i); //bar: 3 bar: 4 } } console.log(bar.count); //2 //解决方法2:修改this的指向 baz.count = 0; function baz(num){ console.log("baz: "+num); this.count++; } for(let i=0;i<5;i++){ if(i>2){ baz.call(baz,i); //baz: 3 baz: 4 } } console.log(baz.count); //2
2、认为this指向该函数的作用域。
var a = 454;
function foo(){
var a = 2;
bar();
}
function bar(){
console.log(this.a);
}
//bar() 中的this 是无法访问到foo() 词法作用域中的变量a的
foo(); //454 (访问到的是最外层window下的变量a)
那么,
this的指向是什么呢?
this 的原理类似于我们之前在 作用域 里面提到的动态作用域,是在运行时才进行绑定的,也就是说它的指向完全取决于函数在哪里被调用。
//当前 调用位置 是 当前调用栈 的上一级 function foo(){ //当前调用栈是:foo ; 当前调用位置是:全局作用域 console.log('foo'); bar(); } function bar(){ //当前调用栈是:foo>bar ; 当前调用位置是:foo console.log('bar'); baz(); } function baz(){ //当前调用栈是:foo>bar>baz ; 当前调用位置是:foo>bar console.log('baz'); } foo();
this的绑定规则是什么呢?有没有什么规律
有四种,分别是 : 默认规则、隐式绑定、显示绑定、new绑定。
1、默认绑定:直接使用不带任何修饰的函数引用进行调用,才使用默认绑定,指向全局对象。(注意:严格模式下,不能使用)
function foo(){ console.log(this.a); } var a = 2; foo(); //2 //严格模式下不能使用默认模式 function bar(){ "use strict"; console.log(this.a); } var a = 2; bar(); //Cannot read property 'a' of undefined
2、隐式绑定:
对象属性引用链:最后一层控制this上下文。常出现的问题(通常出现在调用函数别名和调用回调函数)是隐式丢失,使用了默认规则
function foo(){ console.log(this.a); } const obj1 = { a: 1, foo: foo }; const obj2 = { a: 2, foo: obj1.foo, bar:obj1 }; var a = 3; obj2.foo(); //2 obj2.bar.foo(); //1 var baz = obj1.foo; //隐式丢失(调用函数别名),使用默认规则,这时的this指向全局对象 baz(); //3 //对比:此时使用的隐式规则 obj1.foo(); //1 function doFun(fn){ fn(); } //隐式丢失(调用回调函数),使用默认规则,这时的this指向全局对象 doFun(obj1.foo); //3
3、显式绑定
与原型有关,可以通过call() 和 apply() 来修改this的指向
function foo(b,c){ console.log(this.a); console.log(b,c); } var a = 0; foo(2,3); //0 2 3 var obj = { a:'obj', b:'foo-a', c:'foo-b' }; //call() 和 apply() 只有参数格式不同的区别 foo.call(obj,obj.b,obj.c); //obj foo-a foo-b foo.apply(obj,[obj.b,obj.c]); //obj foo-a foo-b
call() 的实现原理:
Function.prototype.myCall = function(context,...args){ let cxt = context || window; //将当前被调用的方法定义在cxt.func上.(为了能以对象调用形式绑定this) //新建一个唯一的Symbol变量避免重复 let func = Symbol(); console.dir(this); //getName() ---> 这里是隐式绑定, cxt[func] = this; args = args ? args : [] //以对象调用形式调用func,此时this指向cxt 也就是传入的需要绑定的this指向 const res = args.length > 0 ? cxt[func](...args) : cxt[func](); //删除该方法,不然会对传入对象造成污染(添加该方法) delete cxt[func]; return res; } let obj = { name: "obj", getName(){ console.log(this.name); } }; let obj1 = { name: "obj1" }; obj.getName.myCall(obj1); // obj1
3.1 显式绑定的变种(硬绑定)能解决前面的隐式丢失的问题
var a = 1; function foo(b=''){ console.log(this.a,b); } var obj1 = { a: 2, foo:foo }; var obj2 = { a: 3, b:obj1.foo, c:obj1 }; //隐式规则 obj2.b(); //3 obj2.c.foo(); //2 //默认规则 foo(); //1 //隐式丢失的两种情况: 函数别名 和 回调函数 var bar = obj2.b; bar(); //1 setTimeout(obj2.c.foo,100); //1 //解决方法:硬绑定 var bar1 = function(){ obj2.b.call(obj2,arguments[0]); } bar1('bar1'); //3 "bar1" var bar2 = function(){ obj2.c.foo.call(obj1,arguments[0]); } bar2('bar2'); //2 "bar2"
4、new 绑定
使用new操作符调用的函数被我们称之为 “构造函数 ”。它与普通函数并没有什么区别,只是被new操作符调用而已。发生构造函数调用时,会自动执行下面的操作:
(1)创建(或者说构造)一个全新的对象。
(2)这个新对象会被执行【原型】连接。
(3)这个新对象会绑定到函数调用的this。
(4)如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(){ this.a = 2; } bar = new foo(); console.log(bar.a); //2
上面这四种规则的优先级是什么呢?
new绑定 > 显示规则 > 隐式规则 > 默认规则
function foo(){ console.log(this.a); } var obj1 = { a: 1, foo: foo, baz:baz }; var a = 2; //隐式 obj1.foo(); //1 //显示 obj1.foo.call(window); //2 //结论:显式 > 隐式 function baz(num){ this.a = num; } obj1.baz('隐式规则'); console.log(obj1.a); //隐式规则 var bar = new obj1.baz("new模式"); console.log(obj1.a); //隐式规则 console.log(bar.a); //new模式 //结论: new模式 > 隐式 console.log(".........."); var fn = baz.bind(window); fn(3); console.log(a); //3 var obj = new fn(100); //等价于 var obj1 = new (baz.bind(window))(100); console.log(obj1.a); //100 console.log(obj.a); //100 //结论: new模式 > 显式
规则以外的意外
万事总有例外,并不总是遵循上面 的规则。如一下情况:
1、被忽略的this
在调用call 或 apply 时, 第一参数传入了null 或 undefined 来进行this 的绑定,此时使用的是默认规则。 一般这么做,只是占位而已,函数中都不会调用到this,但不排除某些函数用到了this。所以为了安全起见,还是传入一个空的对象更好。
//通常的情况 function foo(a,b){ console.log(`${a}...........${b}`); } foo.call(null,3,4); //3...........4 //最好使用 var NONE = Object.create(null); foo.call(NONE,3,4); //3...........4
2、es6中的箭头函数
不适用上面的四个规则,是根据外层的作用域来确定 this 的,用更常见的词法作用域来代替传统的this机制。
var a = 1; function fun(fn){ fn(); } function foo(){ fun(()=> { console.log(this.a); }); } var obj = { a: 2 }; //这里的箭头函数跟外层的 foo() 指向一致 foo(); //1 foo.call(obj); //2 //等价于(定义变量替换this,绕过this机制) function foo1(){ var self = this; fun(function (){ console.log(self.a); }); } //等价于(使用bind()修改this指向) function foo2(){ fun(function (){ console.log(this.a); }.bind(foo2)); } //效果跟上面的箭头函数是一样的 foo1(); //1 foo1.call(obj); //2 foo2(); //1 foo2.call(obj); //2
3、隐式丢失
在前文介绍隐式规则的时候已经有提到了,这里就不再多说。
总结
this的内容大概就这么多。重点是看函数运行时来确定this 的绑定,绑定的规则有:new绑定(使用new关键字生成的实例) > 显式规则 (通过call() 、apply()、bind()等方法来修改this的指向)> 隐式规则(通过对象属性的引用,由最后一层控制this的指向) > 默认规则(其他规则都不适用时,使用默认规则,一直都指向全局对象)。不过有些this的绑定并不遵守上面的规则,例如:ES6中的箭头函数、使用函数别名和回调函数后出现的隐式丢失等情况。