前言
this关键字是前端面试题中最常见的考点,本文章将由浅入深,从最基本的例子开始介绍this的使用方法,以及每道面试题的原理。
this指向什么
在全局作用域下,this
指向的是window
。
console.log(this); // window
var name = "codereasy";
console.log(this.name); // codereasy
console.log(window.name); // codereasy
然而,在实际项目中,我们一般不会在全局作用域下使用this
关键字。接下来,我们看看在函数中,this
关键字指向的是什么。
// 定义一个函数
function foo() {
console.log(this);
}
// 1.调用方式一: 直接调用
foo(); // window
// 2.调用方式二: 将foo放到一个对象中,再调用
var obj = {
name: "codereasy",
foo: foo
}
obj.foo() // obj对象
// 3.调用方式三: 通过call或者apply调用
foo.call("ctgu"); // String {"ctgu"}对象
通过以上例子,我们可以总结出以下几点:
- this和函数定义的位置没有关系,只和调用者有关系。
- this是在运行时被绑定的。
那么,this是根据什么规则被绑定的呢?
this的绑定规则
默认绑定
普通调用
如果我们不给函数绑定调用者,那么在默认情况下,this指向的是window
。
function foo() {
console.log(this); // window
}
foo();
链式调用
function fun1() {
console.log(this); // window
fun2();
}
function fun2() {
console.log(this); // window
fun3()
}
function fun3() {
console.log(this); // window
}
fun1();
函数作为参数,传给另一个函数
function foo(func) {
func()
}
function bar() {
console.log(this); // window
}
foo(bar);
可以发现,不论函数在哪调用,只要没有给函数绑定对象,this永远指向window
隐式绑定
通过对象调用函数
在下面例子中,obj
调用了foo()
方法。因此,this
会隐式的被绑定到obj
对象上。
function foo() {
console.log(this); // obj对象
}
var obj = {
name: "codereasy",
foo: foo
}
obj.foo();
示例一的变体
初学者可能会疑惑,下例中,this指向的是obj1
还是obj2
呢?答案是obj1
。永远记住,谁直接调用foo()
(换而言之,谁离foo()
更近),那么foo()
中的this就指向谁。
function foo() {
console.log(this); // obj对象
}
var obj1 = {
name: "obj1",
foo: foo
}
var obj2 = {
name: "obj2",
obj1: obj1
}
obj2.obj1.foo();
隐式绑定失效
以下例子最终输出的是window
。为什么呢?按照上文的思路,找到调用foo()的位置。我们发现最终是通过bar()
来执行函数的,在执行bar()
的时候,没有给它绑定任何对象,因此根据默认绑定规则,this
指向window
。
function foo() {
console.log(this);
}
var obj = {
name: "obj",
foo: foo
}
// 讲obj的foo赋值给bar
var bar = obj.foo;
bar();
显示绑定
根据上文的例子,大家可以发现,隐式绑定的都是通过如下方式实现的:
obj.fun()
因此,隐式绑定有一个前提条件,obj
对象上有fun
方法,才能像上述代码那样绑定。
如果obj
对象上没有fun
方法,obj
能否调用fun()函数呢?答案是可以的。通过显示绑定来实现。
call函数
function foo() {
console.log(this);
}
foo.call(window); // window
foo.call({name: "codereasy"}); // {name: ""codereasy"}
foo.call(666); // Number对象
bind函数
foo()是原函数,bar是新函数(通过bind给bar绑定了调用者)。因此,今后执行bar的时候,函数中this永远指向obj。
function foo() {
console.log(this);
}
var obj = {
name: "codereasy"
}
var bar = foo.bind(obj);
bar(); // obj对象
new绑定
在JavaScript中,我们可以使用new来创建一个新对象。
当我们使用new关键字调用函数的时候,会执行如下操作:
1.创建一个空对象。
2.空对象的__proto__
属性指向构造函数的Prototype属性。
3.执行构造函数,如果构造函数中有this
,则此this
指向刚刚创建的空对象。
4.返回刚刚创建的对象。
function Student(name) {
console.log(this); // Student {}
this.name = name; // Student {name: "codereasy"}
}
var p = new Student("codereasy");
console.log(p);
不同绑定方式的优先级
显示绑定优先于隐式绑定
从下面例子中可以看出:obj1.foo()
属于隐式绑定,foo.call(obj2)
属于显式绑定。当他们同时存在时,隐式绑定失效了,显式绑定生效了。最终,this
指向的是obj2
。
function foo() {
console.log(this);
}
var obj1 = {
name: "obj1",
foo: foo
}
var obj2 = {
name: "obj2",
foo: foo
}
// 隐式绑定和显示绑定同时存在
obj1.foo.call(obj2); // obj2
new绑定优先级高于显示绑定
从下面例子中可以看出,foo.bind(obj)
是显示绑定,new foo(...)
是new 绑定。
最终console.log()
输出值是foo,说明new绑定生效了。
function foo() {
console.log(this);
}
var obj = {
name: "obj"
}
var bar = foo.bind(obj);
var foo = new bar(); // foo
优先级顺序总结
new绑定 > 显示绑定 > 隐式绑定 > 默认绑定
箭头函数
箭头函数中的this不受以上规则约束,它的this
完全取决于外层作用域。
示例1:
我们发现,下述代码中,greet()
中打印的this,和 outer()
中打印的 this 是一样的。这说明箭头函数的 this 和外层作用域中的 this 是一样的。
function outer() {
console.log(this); // 输出:{ name: 'Alice', greet: [Function: greet] }
const greet = () => {
console.log(this); // 输出:{ name: 'Alice', greet: [Function: greet] }
};
greet();
}
const person = {
name: "Alice",
greet: outer,
};
person.greet();
示例2:
箭头函数的 this
在定义的时候决定,而非调用的时候决定。
从下面代码可以看出,arrow()
在outer()
内部被调用,但是 arrow
的 this
指向的是 window
,和 outer
的 this
不同。这说明,在定义arrow的时候,它的 this 就已经固定了,是 window。因此,不管在哪里调用 arrow 函数,它内部的 this 始终指向的是 window。
const arrow = ()=>{
//this是什么? window
console.log("this是什么",this);
}
function outer() {
//我是外层函数 {name: 'coderEasy', outer: ƒ}
console.log("我是外层函数",this);
arrow();
}
obj = {
name:"coderEasy",
outer : outer,
}
obj.outer();
其他规则
显示绑定失效
通过上文可知,通过call、apply、bind
可以给函数显示绑定对象(调用者)。但是,如果我们绑定的对象是null、undefined
,那么这个显示绑定就会被忽略,函数中的this依然指向window,而不是null、undefined
。