目录
JavaScript中的this
介绍
JavaScript 中的 this
对初学者来说一直是一个棘手的问题,因为它的指向实在太灵活了,如果没有真的理解它,在面试或笔试中被问到 this
指向谁,就只能靠猜。不仅是面试,实际开发中用到 this
的场景也特别多,很多隐蔽的 bug
都有可能是它导致的,用好了 this
,也能写出更简洁的代码。本文我们一起来学一学 this
吧~
this是什么
首先,学习一个知识,先要理解它是什么,在大多数面向对象语言中,this
表示当前对象的一个引用,而 JS 中的 this
是完全不同的概念。
我们查阅 MDN 文档可知,this
是当前执行上下文(global
、function
或 eval
)的一个属性。
也就是说,我们可以把 JS 中的 this
分为三种,分别是:
- 全局上下文中的
this
。 - 函数上下文中的
this
。 eval
上下文中的this
。
关于 eval ,在 MDN 或很多书籍和文档中都建议永远不要使用它,我们就不讨论它了,接下来,我们将主要讨论全局上下文和函数上下文的 this
。
有一点需要注意的是,node
环境中的 this
和 web
环境中的 this
是不同的,为了避免大家一开始接受太多概念,我们先只讨论 web
环境的 this
。
全局上下文的this
全局上下文的 this
指向非常明确,不管有没有启用严格模式,都指向 window
对象。
对应代码如下:
<body>
<script>
console.log(this === window); // 输出 true
("use strict");
console.log(this === window); // 输出 true
</script>
</body>
运行上述代码可以看到,的确是不管有没有启用严格模式,全局上下文的 this
都指向 window
对象。
给 this
添加属性,就相当于给 window
添加属性,给 window
添加属性,就相当于给 this
添加属性,如下代码所示:
this.userName = "zhangsan";
window.age = 18;
console.log(this.age); // 输出 18
console.log(window.userName); // 输出 'zhangsan'
函数上下文的this
全局上下文的 this
很简单,记住无脑指向 window
就完事,复杂就复杂在函数上下文的 this
,函数上下文中的 this
与 arguments
一样,就是函数的隐式参数,可以在任意函数中调用,它的指向不是固定不变的,取决于函数处于什么位置、以什么方式调用,可以总结成如下图:
全局上下文中的函数
直接调用全局上下文中的函数,this
指向默认情况下为 window
。
function fn() {
console.log(this); // 输出 window
}
fn();
function fn() {
let a = 1;
console.log(this.a); // 输出 2
}
var a = 2;
fn();
但是在严格模式下,this
指向的就是 undefined
。
function fn() {
"use strict";
console.log(this); // 输出 undefined
}
fn();
对象中的函数
调用对象中的函数,this
指向为这个对象。
const obj = {
a: 1,
fn() {
console.log("this :>> ", this); // 输出 obj
console.log("this.a :>> ", this.a); // 输出 1
},
};
obj.fn();
但是如果函数嵌套有函数,此时的 this
指向为 window
,就非常令人迷惑。
const obj = {
a: 1,
fn() {
return function () {
console.log("this :>> ", this); // 输出 window
console.log("this.a :>> ", this.a); // 输出 100
};
},
};
var a = 100;
obj.fn()();
其实可以这么理解:
obj.fn()();
等价于;
const temp = obj.fn(); // 定义一个临时变量来存储 obj.fn 返回的函数
temp(); // 执行这个函数
上面代码示例中的 temp
在运行时是处在 window
环境中的,所以 this
指向 window
。
箭头函数
遇到对象里有函数嵌套函数的情况,想要 this
指向这个对象,es6 之前,可以用一个临时变量 _this
来暂存 this
,代码如下:
const obj = {
a: 1,
fn() {
const _this = this;
return function () {
console.log("this :>> ", _this); // 输出 obj
console.log("this.a :>> ", _this.a); // 输出 1
};
},
};
obj.fn()();
ES6 推出了箭头函数的概念,之后我们就可以使用箭头函数解决这个问题,代码如下:
const obj = {
a: 1,
fn() {
return () => {
console.log("this :>> ", this); // 输出 obj
console.log("this.a :>> ", this.a); // 输出 1
};
},
};
obj.fn()();
对于普通函数来说,内部的 this
指向函数运行时所在的对象。对于箭头函数,它不会创建自己的 this
,它只会从自己的作用域链的上一层继承 this。所以这里 fn
中嵌套的匿名箭头函数中的 this
,指向它作用域链的上一层的 this
,也就是函数 fn
的 this
,也就是 obj
。
构造函数
构造函数内,如果返回值是一个对象,this
指向这个对象,否则 this
指向新建的实例。
function Person(name) {
this.name = name;
}
const p = new Person("zhangsan");
console.log(p.name); // 'zhangsan'
function Person(name) {
this.name = name;
return {
name: "xxx",
};
}
const p = new Person("zhangsan");
console.log(p.name); // 'xxx'
function Person(name) {
this.name = name;
return {};
}
const p = new Person("zhangsan");
console.log(p.name); // 'undefined'
如果有返回值,但是不是一个对象,this
还是指向实例。
function Person(name) {
this.name = name;
return 123;
}
const p = new Person("zhangsan");
console.log(p.name); // 'zhangsan'
显示改变函数上下文的this
了解了函数中的 this
指向后,我们可以使用 call
、apply
和 bind
来显式改变函数中的 this
指向。
call
Function.prototype.call() 方法使用一个指定的 this
值和单独给出的一个或多个参数来调用一个函数。
function fn() {
console.log(this.name);
}
const obj = {
name: "zhangsan",
};
fn.call(obj); // 指定 this 为 obj,输出 'zhangsan'
apply
Function.prototype.apply() 方法调用一个具有给定 this
值的函数,以及以一个数组(或类数组对象)的形式提供的参数。
apply
和 call
的功能完全一样,只是传参形式不一样,call
是传多个参数,apply
是只传参数集合。
function add(x, y, z) {
return this.x + this.y + this.z;
}
const obj = {
x: 1,
y: 2,
z: 3,
};
console.log(add.call(obj, 1, 2, 3)); // 输出 6
console.log(add.apply(obj, [1, 2, 3])); // 输出 6,只是传参形式不同而已
bind
Function.prototype.bind() 方法创建一个新的函数,在 bind()
被调用时,这个新函数的 this
被指定为 bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
bind
和 call
、apply
的区别是,函数调用 call
和 apply
会直接调用,而调用 bind
是创建一个新的函数,必须手动去再调用一次,才会生效。
function add(x, y, z) {
return this.x + this.y + this.z;
}
const obj = {
x: 1,
y: 2,
z: 3,
};
const add1 = add.bind(obj, 1, 2, 3); // bind 会返回一个新的函数
console.log(add1()); // 执行新的函数,输出 6
练习
第 1 题
function fn() {
console.log(this); // ?
}
fn();
第 2 题
function fn() {
"use strict";
console.log(this); // ?
}
fn();
第 3 题
const obj = {
userName: "zhangsan",
fn() {
console.log(this); // ?
console.log(this.userName); // ?
},
};
obj.fn();
第 4 题
const obj = {
userName: "zhangsan",
fn() {
console.log(this); // ?
console.log(this.userName); // ?
},
};
const fn = obj.fn;
fn();
第 5 题
const person = {
userName: "zhangsan",
wife: {
userName: "xxx",
fn() {
console.log(this); // ?
console.log(this.userName); // ?
},
},
};
person.wife.fn();
第 6 题
const o1 = {
text: "o1",
fn() {
return this.text;
},
};
const o2 = {
text: "o2",
fn() {
return o1.fn();
},
};
const o3 = {
text: "o3",
fn() {
var fn = o1.fn;
return fn();
},
};
console.log(o1.fn()); // ?
console.log(o2.fn()); // ?
console.log(o3.fn()); // ?
第 7 题
const o1 = {
text: "o1",
fn() {
return this.text;
},
};
const o2 = {
text: "o2",
fn: o1.fn,
};
console.log(o1.fn()); // ?
console.log(o2.fn()); // ?
第 8 题
const foo = {
name: "zhangsan",
sayName() {
console.log(this.name);
},
};
const bar = {
name: "xxx",
};
foo.sayName.call(bar); // ?
foo.sayName.apply(bar); // ?
foo.sayName.bind(bar)(); // ?
第 9 题
function Person1(name) {
this.name = name;
}
function Person2(name) {
this.name = name;
return {};
}
function Person3(name) {
this.name = name;
return {
name: "xxx",
};
}
function Person4(name) {
this.name = name;
return 1;
}
const p1 = new Person1("zhangsan");
const p2 = new Person2("zhangsan");
const p3 = new Person3("zhangsan");
const p4 = new Person4("zhangsan");
console.log(p1.name); // ?
console.log(p2.name); // ?
console.log(p3.name); // ?
console.log(p4.name); // ?
第 10 题
var userName = "xxx";
const obj1 = {
userName: "zhangsan",
fn() {
setTimeout(function () {
console.log(this.userName); // ?
});
},
};
const obj2 = {
userName: "zhangsan",
fn() {
setTimeout(() => {
console.log(this.userName); // ?
});
},
};
obj1.fn();
obj2.fn();
第 11 题
var userName = "xxx";
const obj1 = {
userName: "lin",
fn() {
return () => {
console.log(this.userName); // ?
};
},
};
const obj2 = {
userName: "lin",
fn() {
return () => {
return () => {
console.log(this.userName); // ?
};
};
},
};
obj1.fn()();
obj2.fn()()();
第 12 题
function foo() {
return () => {
console.log(this.a);
};
}
const obj1 = {
a: 2,
};
const obj2 = {
a: 3,
};
const bar = foo.call(obj1); // ?
bar.call(obj2); // ?
第 13 题
var a = 100;
const foo = () => () => {
console.log(this.a);
};
const obj1 = {
a: 2,
};
const obj2 = {
a: 3,
};
const bar = foo.call(obj1); // ?
bar.call(obj2); // ?
如果你对上面的几道练习题都可以完成,则基本对该知识点掌握了。具体的题目答案再评论区。有啥不懂不理解的也欢迎在评论区进行提问和讨论。
总结
最后,我们再回顾一下 this
的具体指向,如果是全局上下文的 this
,就指向 window
,如果是函数上下文的 this
,指向不是固定不变的,取决于函数处于什么位置、以什么方式调用,牢记下面这张图,我们本文举的所有例子都可以通过这张图找到答案。
当然,还有一个优先级的概念,优先级是 new
调用 > call
、apply
、bind
调用 > 对象上的函数调用 > 普通函数调用,一般面试不会考得这么细,同学们可以去自行敲代码实践一下,记住上面那张图,足够了。文章内容来源