this
上次讲了闭包,其实我感觉我上次没讲什么,闭包的确神秘,但是又不是很复杂,需要长篇大论去解释。刚好,我看到了js中的另一个重点,this。this也是会让初学者头疼的一个知识点。有时候感觉莫名其妙,直觉上应该是这个答案,实际上却是错的。
错误的认识
- this不只在js中出现,java, c++ 这些强类型语言也是有this的,不过它们都是叫this, 本质是不太一样的。学过这些强类型语言的同学很容易就认为this是指向自己的。然而,这在js中,是不正确的。下面的代码会打脸。
function fun (val) {
this.a = val;
}
fun(0);
fun.a++;
console.log(fun.a);
console.log(a);
如果this是指向自己的话,那么现在fun.a应该是1咯,对吧。然而真的是这样么? 打开chrome, F12将代码拷贝在控制台看看结果,fun.a的输出是NaN,第二个log出却是0。傻眼了吧?好吧,其实当时我也是一脸懵逼。这个结果,告诉了我们js的this不如你想象的那样。这是你不知道的javaScript的this。
- 除了认为this指向自己,《你不知道的javaScript》中也提到另一种错误的理解。this指向函数的作用域。详细点说,就是一个函数,它的this是指向它的父级作用域。嗯,好吧,我觉得这种理解很自然,很舒服。下面的代码,就是这种错误理解导致的错误写法。
function a () {
console.log(this.c);
}
function b() {
let c = 1;
this.a();
}
b(); // undefined
其实上面的结果不会输出1,所以说明了这个理解也是错误的。
this的正确使用姿势
this的指向呢,其实一句话就概况了。
this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
其实this的指向就看谁调用了它吧,总结下来,有以下4种情况:
-
默认绑定
默认绑定,我曾经也听到说成是直接函数调用。下面的代码就是默认绑定
function fun () {
console.log(this.a);
}
var a = 1;
fun() // 1
默认绑定的情况,函数中的this是绑定在window上,当然是在非严格模式下。在严格模式下,this是指向undefined, 上面的代码就会抛出一个错误。说回非严格模式下的默认绑定,由于this指向了window。所以, this.a其实就是window.a。那么自然输出了1。
-
隐式绑定
当一个函数是某个对象的属性时,我们直接对象.函数名调用该函数时,该函数的this是指向该对象的。其实,我个人认为,默认绑定的情况下fun()
等价于window.fun()
,这也正好解释了为什么默认绑定时,this是指向了全局对象。下面是隐式绑定的一个例子:
let obj = {
name: '123',
func () {
console.log(this.name);
}
}
obj.func() // '123'
注意:隐式绑定当使用不当时,会出现this的丢失现象。比如:
// 情况一
window.name = '1234';
let obj = {
name: '123',
func () {
console.log(this.name);
}
}
func = obj.func;
func(); // '1234'
// 情况二
window.name = '1234';
let obj = {
name: '123',
func () {
return function () {
console.log(this.name);
}
}
}
func = obj.func();
func(); // '1234'
// 情况三
window.name = '1234';
let obj = {
name: '123',
func () {
console.log(this.name);
}
}
setTimeout(obj.func, 1000); // '1234'
情况一:不是隐式绑定了obj么,输出应该是123才对啊。结果却输出了1234。嗯,我们要根据代码来分析,this的指向要对比四种情况,然后去找到对应的情况来确定。func()
很明显就是之前我们说的默认绑定,所以输出了全局对象中的a属性的值。
情况二:情况二其实就是返回了一个函数,然后这个函数在全局作用域直接调用了,所以就是默认绑定,如果需要绑定原来的obj,可以用以下的办法:
// 解法1
window.name = '1234';
let obj = {
name: '123',
func () {
let that = this;
return function () {
console.log(that.name);
}
}
}
func = obj.func();
func(); // '123'
// 解法2
window.name = '1234';
let obj = {
name: '123',
func () {
return () => console.log(this.name);
}
}
func = obj.func();
func(); // '123'
// 解法3
window.name = '1234';
let obj = {
name: '123',
func () {
return function () {
console.log(this.name);
}.bind(this);
}
}
func = obj.func();
func(); // '123'
情况三: setTimeout的第一个参数是一个函数,其实setTimeout的内部实现的伪代码应该是这样的
function setTimeout (fn, delay) {
// 等了 delay 毫秒
fn()
}
所以其实也是变成了默认绑定嘛。
-
显式绑定
显式绑定就是用js的call, apply, bind的这些函数来绑定this啦。
window.name = 'window';
let xiaoming = { name: 'xiaoming' };
function bar() { console.log(this.name); }
// call
bar.call(xiaoming); // 'xiaoming';
// apply
bar.apply(xiaoming); // 'xiaoming';
// bind
bar.bind(xiaoming)(); // 'xiaoming';
它们间的异同:
- 同
- 它们三者可以用来改变函数this的指向, 第一个参数都是要指向的对象
- 异
- apply 和 call 接受的参数不太一样,call只能一个一个传入参数,apply可以传入一个参数的数组
- bind返回的是绑定后的函数,apply 和 call是绑定时同时执行。
-
new 绑定
在js中,我们使用new运算符时,其实,经过了下面的操作
1、创建(或者说构造)一个全新的对象
2、这个新对象会被执行[[原型]]连接
3、这个新对象会被绑定到函数的this
4、如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
综上,那么下面的代码,
function fn (a) { this.a = a; }
let a = new fn(1);
console.log(a.a); // 1
就是上面的四个步骤过一遍的结果了,this绑定到了的这个新对象a上。
总结
逼逼了这么多。。其实this的指向只要弄清楚这四种情况,绝大部分的情况都可以自己判断出来。其实还是会有些很奇葩的情况的,如果我没记错,但是根据我目前的开发经验,只要你或者你同事不作死,就不会写出那么奇葩的情况。如果说是面试题,当我没说。来来来,最后总结遍,四个方法就是:
- 如果是new出来的对象,那么this指向该对象无误。
- 如果有用显式绑定,那么this指向绑定的对象
- 是否作为一个对象的属性来调用,是的话,this指向该对象。当然要注意this丢失的情况。
- 如果上面都没有,那么默认绑定应该无误。
多练习慢慢就会一眼就看出this的指向了,工多手熟。如果有误,请指出。还有祝大家国庆快乐!