了解this
1. this 是什么
this是JavaScript中一个很特别的关键字,被自动定义在所有函数的作用域中。
this会在执行上下文中绑定一个对象,但是是根据什么条件绑定的?在不同的执行条件下会绑定不同的对象,这也是让人捉摸不定的地方。
下来我们一起来彻底搞定this。
2. 为什么要使用this
this提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计得更加简洁并且易于复用。
在没有this的情况下,我们定义一个对象,我们要拿到obj中的属性,就必须通过obj去获取,如果我们将obj改成obj1,那么我们就需要将所有的obj改成obj1,
var obj = {
name: "monkey",
job: "web开发",
foo: function () {
console.log(`${obj.name}的职业是${obj.job}`); //monkey的职业是web开发
},
};
obj.foo();
我们使用在this调用的时候,就会方便很多,此时我们在修改obj为obj1的时候,我们就不需要去修改foo方法中的的引用,这里只有两个属性,如果属性再更多,使用this是不是更方便。
var obj = {
name: "monkey",
job: "web开发",
foo: function () {
console.log(`${this.name}的职业是${this.job}`); //monkey的职业是web开发
},
};
obj.foo();
通过上面这个场景,我们了解到使用this的方便性。下面我们继续了解this
3. this的指向
this的指向和函数在哪里定义无关,和调用方式有关。
在大多数情况下,this都是出现在函数中,很少在全局中去使用。
- 在浏览器中,
this指向的是window - 在node中,
this指向的是一个空对象{}
下面我们看下在浏览器中使用this
function foo() {
console.log(this);
}
// 1. 直接调用这个函数
foo(); // Window
// 2. 创建一个对象,对象中的函数指向foo
var obj = {
name: "monkey",
foo: foo,
};
obj.foo(); //{name: 'monkey', foo: ƒ}
// 3. apply调用
foo.apply("abc"); //String {'abc'}
通过上面这个示例可以看到,同一个函数,this的结果完全不同。下面我们就了解下,this的绑定规则是什么。
this的绑定规则
我们来看看在函数的执行过程中调用位置如何决定this的绑定对象。
1. 默认绑定
当独立函数调用,也就是说,我们直接调用函数,而这个函数没有绑定到任何对象上面。
/* 案例一 直接调用*/
function foo() {
console.log("foo", this);
}
foo(); // Window
/* 案例二 函数嵌套调用*/
function foo1() {
console.log("foo1", this); // Window
}
function foo2() {
console.log("foo2", this); // Window
foo1();
}
function foo3() {
console.log("foo3", this); // Window
foo2();
}
foo3();
/* 案例三 对象中得方法*/
var obj = {
name: "monkey",
foo: function () {
console.log("obj", this); // Window
},
};
var bar = obj.foo;
bar();
上面这三种都是通过独立函数调用的,所有的this都指向的是window。执行结果如下:
2. 隐式绑定
调用位置是否有上下文对象,也就是说通过某个对象发起的函数调用,该对象中必须有对函数的引用。
function foo() {
console.log(this);
}
/*案例一 */
var obj1 = {
name: "obj1",
foo: foo,
};
obj1.foo();
/* 案例二 */
var obj2 = {
name: "obj2",
bar: obj1.foo,
};
obj2.bar();
以上都属于隐式绑定,他们都是通过对象调用,this就指向了该对象。执行结果如下
注意:特殊情况 隐式丢失
function foo() {
console.log(this);
}
var obj1 = {
name: "obj1",
foo: foo,
};
// 将obj1的foo赋值给bar
var bar = obj1.foo;
bar();
这种情况下,因为foo最终被调用的位置是bar,而bar在进行调用时没有绑定任何的对象,也就没有形成隐式绑定。
这种情况下,相当于一种默认绑定。
3. 显示绑定
在上面的隐式绑定中,我们必须在一个对象内部中引用函数,从而把
this隐式绑定到这个对象上。如果我们不想通过对象内部属性对函数引用,那么我们就需要使用call/apply/bind进行显式绑定
call 和 apply 绑定
function foo() {
console.log(this);
}
var obj = {
name: "monkey",
};
// call 和 apply 是可以指定 this 的绑定对象
foo.call(obj);
foo.apply(obj);
console.log("--------特殊情况-----------")
foo.call(null);
foo.apply(undefined);
执行结果如下:
使用显示绑定时需要注意,如果使用
null和undefined,那么这个显示绑定会被忽略,从而使用默认绑定。
bind 绑定
function foo() {
console.log(this);
}
var obj = {
name: "monkey",
};
var newFoo = foo.bind(obj);
newFoo();
newFoo();
bind(..) 会返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数。
执行结果如下:
4. new绑定
通过new关键字来创建构造函数的实例,绑定this
使用new关键字来调用函数时,会执行如下的操作:
- (1)创建一个全新的对象。
- (2) 这个新对象会被执行
Prototype链接。 - (3)这个新对象会绑定到函数调用的this。
- (4)如果函数没有返回其他对象,那么
new表达式中的函数调用会自动返回这个新对象。
function Person(name) {
console.log(this); //指向的就是Person对象
this.name = name;
}
var p1 = new Person("moneky");
console.log(p1);
执行结果如下:
this的绑定例外
在某些场景下this的绑定行为会出乎意料,你认为应当应用其他绑定规则时,实际上应用的可能是默认绑定规则。
1. 忽略显示绑定
这种情况,我们在上面 “call 和 apply 绑定”中已经介绍了,把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
2. 间接函数引用
var obj1 = {
name: "obj1",
foo: function () {
console.log(this);
},
};
var obj2 = {
name: "obj2",
};
obj2.baz = obj1.foo;
obj2.baz();
(obj2.bar = obj1.foo)();
执行结果如下:
两种方式所绑定的this不同,第二种方式进行了赋值调用,实际上是间接函数引用
赋值表达式(obj2.bar = obj1.foo)的返回值是目标函数的引用,再加上一个小括号,表示立即执行,相当于是直接调用了foo()函数。
3.ES6箭头函数
箭头函数本身不绑定
this,this来源于它的上级作用域
var obj = {
name: "monkey",
foo: () => {
console.log(this);
},
};
obj.foo();
执行结果如下:
规则优先级
了解了规则,如果同时设置了两种规则,那么谁的优先级高呢。
1.默认规则的优先级最低
2.显示绑定优先级高于隐式绑定
function foo() {
console.log(this);
}
var obj1 = {
name: "obj1",
foo: foo,
};
var obj2 = {
name: "obj2",
foo: foo,
};
obj1.foo(); //{name: 'obj1', foo: ƒ}
// 隐式绑定和显示绑定同时存在
obj1.foo.call(obj2); //{name: 'obj2', foo: ƒ}
其中obj1.foo为隐式绑定call(obj2)为显示绑定
执行结果如下:
3.new绑定优先级高于隐式绑定
function foo() {
console.log(this);
}
var obj = {
name: "obj",
foo: foo,
};
new obj.foo(); //执行结果: foo {}
4.new绑定高于显示绑定(bind)
new绑定和call、apply是不允许同时使用的。
function foo() {
console.log(this);
}
var obj = {
name: "obj",
};
var bar = foo.bind(obj);
new bar(); //执行结果:foo {}
如此可以得出以下结论:
new绑定 > 显示绑定> 隐式绑定 > 默认绑定
this面试题
下面我们就通过面试题,再来理解this
1.面试题一-间接函数引用
var name = "window"
var person = {
name: "person",
sayName: function () {
console.log(this.name)
},
}
function foo() {
var sss = person.sayName;
sss(); // 1
person.sayName(); // 2
(person.sayName)(); //3
(b = person.sayName)(); //4
}
foo()
分析:
person.sayName直接赋值给了sss,直接调用sss函数,即为独立函数调用,默认绑定,所以应该是window。- 通过
person对象调用,隐式绑定,所以应该是person。 - 和2其实是一样的,都是对象调用,隐式绑定,所以应该是
person。。 - 赋值表达式调用,上面介绍
this的绑定例外中的间接函数引用,默认绑定,所以应该是window。
执行结果
2.面试题二-对象中定义函数
var name = 'window'
var person1 = {
name: 'person1',
foo1: function () {
console.log(this.name)
},
foo2: () => console.log(this.name),
foo3: function () {
return function () {
console.log(this.name)
}
},
foo4: function () {
return () => {
console.log(this.name)
}
}
}
var person2 = { name: 'person2' }
person1.foo1(); //1
person1.foo1.call(person2); //2
person1.foo2(); //3
person1.foo2.call(person2); //4
person1.foo3()(); //5
person1.foo3.call(person2)(); //6
person1.foo3().call(person2); //7
person1.foo4()(); //8
person1.foo4.call(person2)(); //9
person1.foo4().call(person2); //10
分析:
- 通过
persion1对象直接调用foo1(),隐式绑定,所以结果是person1。 - 使用了隐式绑定和
call显示绑定,显示绑定优先级高于隐式绑定,所以结果是person2 - 由于
foo2是箭头函数,其本身是不绑定this的;向上级作用域查找,由于对象不存在作用域,所以它的上级就是全局作用域,所以结果是window。 - 由于
foo2是箭头函数,不适用于绑定规则,同3,所以结果是window。 - 由于
foo3函数返回值是一个函数,因此“person1.foo3()()”相当于是立即执行了返回函数,即独立函数调用,默认规则,所以结果是window。 - 先是通过
call显示绑定到了person2上,由于foo3函数返回值是一个函数,再加上最有一个小括号,相当于是立即执行了返回函数。即独立函数调用,所以结果是window。 - 拿到
foo3的返回函数,通过call显示绑定到了person2中,所以结果是person2。 foo4()的函数返回的是一个箭头函数,箭头函数只看上层作用域,其上级作用域是foo4函数,而foo4中的this指向的是person1,所以结果是person1。foo4()显示绑定到person2中,并且返回一个箭头函数,所以结果是person2。foo4返回的是箭头函数,箭头函数只看上层作用域,其上级作用域是foo4函数,而foo4中的this指向的是person1,所以结果是person1。
注意:
person1.foo4:表示的是foo4这个函数。person1.foo4():表示的是foo4的返回值,也就是箭头函数。
执行结果:
3.面试题三-构造函数中定义函数
var name = "window";
function Person(name) {
this.name = name;
this.foo1 = function () {
console.log(this.name);
};
this.foo2 = () => console.log(this.name);
this.foo3 = function () {
return function () {
console.log(this.name);
};
};
this.foo4 = function () {
return () => {
console.log(this.name);
};
};
}
var person1 = new Person("person1");
var person2 = new Person("person2");
person1.foo1(); //1
person1.foo1.call(person2); //2
person1.foo2(); //3
person1.foo2.call(person2); //4
person1.foo3()(); //5
person1.foo3.call(person2)(); //6
person1.foo3().call(person2); //7
person1.foo4()(); //8
person1.foo4.call(person2)(); //9
person1.foo4().call(person2); //10
分析:
- 通过
persion1对象直接调用foo1(),隐式绑定,所以结果是person1。 - 隐式绑定+显示绑定,显示绑定优先级高,所以结果是
person2。 foo2是箭头函数,其本身是不绑定this的,向上级查找,上级作用域是Person构造函数,所以结果是person1。foo2是箭头函数,不适用于绑定规则,显示绑定无效,同3,所以结果是person1。- 由于
foo3()返回的是一个函数,再加上小括号,相当于是全局调用,独立函数调用,所以结果是window。 - 显示绑定后,又在全局调用,还是独立函数调用,所以结果是
window。 foo3()先是隐式绑定,返回函数后,再显示绑定到person2中,所以结果是person2。foo4()隐式绑定后,返回一个箭头函数,箭头函数本身是不绑定this的,向上级查找,上级作用域是Person构造函数,所以结果是person1。foo4显示绑定到person2中,返回一个箭头函数,箭头函数本身是不绑定this的,向上级查找,上级作用域是Person构造函数,已经显示绑定到了person2中, 所以结果是person2。- 先是隐式绑定,返回箭头函数,其本身是不绑定this的,向上级查找,上级作用域是
Person构造函数,所以结果是person1。
执行结果:
4.面试题四-构造函数中定义对象再嵌套函数
var name = "window";
function Person(name) {
this.name = name;
this.obj = {
name: "obj",
foo1: function () {
return function () {
console.log(this.name);
};
},
foo2: function () {
return () => {
console.log(this.name);
};
},
};
}
var person1 = new Person("person1");
var person2 = new Person("person2");
person1.obj.foo1()(); //1
person1.obj.foo1.call(person2)(); //2
person1.obj.foo1().call(person2); //3
person1.obj.foo2()(); //4
person1.obj.foo2.call(person2)(); //5
person1.obj.foo2().call(person2); //6
分析:
foo1()隐式绑定后,返回一个函数,后面再加一个小括号,相当于再全局调用,独立函数调用,所以结果是window。person1.obj.foo1只是一个赋值表达式,foo1.call()显示绑定到person2中,返回一个函数,后面再加一个小括号,相当于再全局调用,独立函数调用,所以结果是window。foo1()隐式绑定后,返回一个函数,又显示绑定到person2中,所以结果是person2。foo2()隐式绑定到obj中,返回一个箭头函数,然后向上级查找,上级是foo2所在的函数,foo2的this指向的是obj, 所以结果是obj。foo2显示绑定到person2中,返回一个箭头函数,没有作用域,向上级foo2中查找,而foo2已经被显示绑定到了person2中,所以结果是person2。foo2()隐式绑定到obj中,返回的是箭头函数,箭头函数没有作用,向上级查找,所以结果是obj。
执行结果: