关于 this#
在 JavaScript 的对象和函数内部,如果需要再次调用对象或者函数时,有时候可以使用this代替这个对象或者函数。
const user = {
name: "Hale",
sayHi: function () {
// console.log('Hi', user.name)
console.log("Hi", this.name);
},
};
user.sayHi(); // Hi Hale上面的this代替对象user,即指向了user。但是this并不一定就是代替其作用域所在的对象或者函数。
function fn() {
const name = "Hale";
console.log(this.name);
}
var name = "Amy";
// window.name = 'Amy'
fn(); // Amy上面的this并不是指向函数fn,而是浏览器的全局对象window,又因为使用var声明的变量都会存储在全局对象window中,最后函数输出的值为Amy。
那么,this到底指向哪里呢?
this的指向#
对象的this指向#
对象里面的方法调用this一定指向该对象。不管时普通的对象还是通过构造函数或者类生成的实例对象,只要通过是obj.fn()调用方法,那么 方法fn里面的 this 指向obj对象。
// 普通对象
let user = {
name: "Hale",
printName: function () {
console.log(this.name); // this 指向对象 user
},
};
user.printName(); // Hale
// 类
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
printName() {
console.log(this.name); // this 指向实例对象 p
}
}
// 实例对象
const p = new Person("Hale", 18); // 生成实例对象 { name: 'Hale', age: 18, printName: function() { console.log(this.name) } }
p.printName(); // Hale函数的this指向#
普通函数
普通函数的this指向全局对象window或者global(Node.js 环境)。如果函数里面已经声明了this.xxx,则会把xxx属性或者方法挂载到全局对象中,并覆盖原来的值。另外函数里面使用var声明的变量并不是全局变量,不会挂载到全局对象中。如果没有声明this.xxx,则会在全局对象中去寻找xxx的值,找不到则为undefined。
// 浏览器环境
var name = "Amy"; // 全局变量
var age = 20; // 全局变量
console.log(name); // 'Amy'
const fn = function () {
this.name = "Hale";
var age = 18; // 函数中使用 var 声明的变量是块级变量,不是全局变量
console.log(this.name); // 'Hale'
console.log(this.age); // 20
console.log(this.score); // undefined
console.log(this === window); // true
console.log(window.name === "Hale"); // true
};
fn();
console.log(name); // 'Hale'全局变量一定要使用var来声明,只要用var声明的全局变量挂载到全局对象window中,而使用const或者let声明的变量挂载到全局对象。也可以直接在全局对象上添加值,比如window.name = 'Amy'。
构造函数
构造函数其实也可以当成是普通函数,此时它里面的this也是指向全局对象。但是构造函数的主要还是用来生成实例对象的,是通过new来生成,而不是调用它。构造函数其实也可以直接像普通函数一样调用,但这样就是没有意义了。当构造函数生成实例对象时,this指向的就是生成的实例对象。
- 构造函数生成实例对象
function Person(name, age) {
this.name = name;
this.age = age;
this.printName = function () {
console.log(this.name);
};
console.log(this); // { name: 'Hale', age: 18, printName: function() {console.log(this.name)} } ,指向实例对象
console.log(this === window); // false
}
const p = new Person("Hale", 18);
p.printName(); // Hale- 调用构造函数,和普通函数一样
function Person(name, age) {
this.name = name;
this.age = age;
this.printName = function () {
console.log(this.name);
};
console.log(this); // 打印 window 对象,指向全局对象 window
console.log(this === window); // true
}
Person("Hale", 18);
// 如果硬要搞个返回值
const p = Person("Hale", 18);
console.log(p); // 结果很明显,函数没有 return 值,所以是 undefined
p.printName(); // 无输出类 Class
在 ES6 中新增了类 Class,类其实是一个语法糖,本质还是 JavaScript 函数的原型的实现。在一些面向对象的编程语言中,类的一个主要作用就是生成实例对象。JavaScript 类也不例外,通过new生成实例对象,类是不能直接调用的,这时类的this指向生成的实例对象,类里面的属性和方法都会变成实例的属性和方法,这个和构造函数生成实例相似。
另外 class 里面的方法前面如果加上关键字static,而会把实例方法变成静态方法,静态方法是可以直接通过类来调用的,调用方法为className.staticMethod(),这时静态方法里面的this指向的就是类本身,有点类似于对象调用方法。目前类只有静态方法而没有静态属性,所以静态方法里面的this只能调用其他的静态方法,而不能获取实例属性。除非通过new生成实例后,this指向变成实例对象,才可以获取实例的属性和方法。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
this.score = 100;
}
printName() {
console.log(this.name); // this 指向实例对象
}
static printAge() {
this.getAge(); // this 指向类
}
static getAge() {
console.log(18);
}
static printScore() {
console.log(this.score);
}
}
// 实例对象
const p = new Person("Hale", 18);
p.printName(); // Hale
Person.printAge(); // 18 直接调用类的静态方法
Person.printScore(); // undefined 无法获取属性箭头函数
箭头函数没有this,在箭头函数内部访问的this都是来自外部作用域的this。
function sayName() {
this.name = "Hale";
setTimeout(() => {
console.log(this.name); // this === 外面的 this
}, 1000);
}
sayName(); // Hale通过上面代码,我们发现this只有在普通函数里面才有效,即使是对象必须在对象的普通函数里面才能生效。
const obj = {
a: 1,
b: this.a,
fn: function () {
console.log(this.a);
},
};
console.log(obj.b); // undefined,this.a 获取不到 a 的值
obj.fn(); // 1 在函数里面才能获取到 a我们可以得出一条规律,this的指向由其所在的函数调用时(运行时)的上下文环境决定的。
this 的灵活运用#
知道了this的指向判定后,其实还不够,还需要在实际的使用中灵活运用才行。
下面举一个在小程序的例子:
class Token {
constructor() {
this.verifyUrl = "/token/verify"; // this1
}
_verifyFromServer(token) {
// this2
const self = this;
wx.request({
url: self.verifyUrl, // this3
method: "POST",
data: {
token,
},
success: function (res) {
const valid = res.data.data.is_valid;
if (!valid) {
self.getTokenFromServer();
// this4
}
},
});
}
getTokenFromServer() {}
}这是一个小程序项目中的辅助内,用来验证token是否合法。在上面代码的不同位置上标注了 4 个 this,下面分析下这 4 个 this 的指向。
-
this1 和 this2 的指向判定比较简单,它们都在类的方法中,指向的是类
Token生成的实例对象。 -
this3 的位置是在函数
request传入的参数对象中,而且不是在对象的函数里面,这个this是在对象里面是不存在的。但是因为它外面包裹了一个函数request,所以它实际上是函数request的this,而这个函数又是对象wx的调用的方法,所以this指向的是对象wx。但是因为对象wx中并没有verifyUrl这个属性,而属性verifyUrl是类的实例的属性,所以把 this3 替换成了 this2 的指向才能拿到这个属性值。 -
this4 的位置在函数
request传入的参数对象里面的方法中,所以this的指向为作为参数的对象本身。但是因为这个对象没有getTokenFromServer这个方法,而方法getTokenFromServer是类的实例的方法,所以把 this4 替换成了 this2 的指向才能拿到这个方法。
改变 this 的指向#
上面所展示的所有代码中this的指向,都是由函数运行时的上下文决定的,而不是由我们自己来决定的。
那么我们可以指定 this 的指向吗?答案是肯定的。
可以通过 JavaScript 函数实例方法中call,apply,bind的这 3 个方法来改变this的指向。
示例代码如下:
const obj = {
value: "obj",
};
window.value = "val";
function getValue(name, age) {
console.log(name, age, this.value); //普通函数 this 指向全局对象
}
getValue("Hale", 18); // Hale 18 val 。this 指向 window
// 改变 this 指向为 obj 对象
// 通过 call 改变
getValue.call(obj, "Hale", 18); // 改变指向后直接调用函数,打印结果为 Hale 18 obj
// 通过 apply 改变
getValue.apply(obj, ["Hale", 18]); // 改变指向后直接调用函数,打印结果为 Hale 18 obj
// 通过 bind 改变
const fn = getValue.bind(obj, "Amy", 20); // 改变指向并传入参数,生成一个新的函数
fn(); // Amy 20 obj
fn("Hale", 18); // Amy 20 obj 传入的参数无效,绑定时的参数不会被覆盖
const fn2 = getValue.bind(obj); // 只改变指向而不传参数,生成一个新的函数
fn2(); // undefined undefined 'obj'
fn2("Hale", 18); // Hale 18 obj 传入参数有效上面代码中,函数getValue的this一般情况下指向是全局对象window。但是通过使用call,apply,bind方法之后,函数getValue的this指向已经改变了,变成了指向传入的第一个参数obj对象了,而从第二个参数开始的参数则为getValue函数所要传入的参数,按照顺序传入。
对于函数传入的参数类型,是一个个传入,还是传入参数集合的数组,apply是传入数组的,而call、bind是一个个参数传入的。其实这个规则很好记忆。apply方法的首字母和array数组的首字母都是a,所以传入的是参数是一个数组,而其它 2 个的方法call、bind的首字母不是a,所以传入的不是数组,而是一个个参数了。
补充
其实,call,apply,bind方法改变了this指向,把函数中this的指向由一个对象换成了另一个对象。比如某个函数fn中的this的指向原来是指向对象a的,现在换成了指向对象b,那么是不是意味着原来对象b中根本就没有fn这个方法,但是当fn的this指向b的时候,对象b就有了对象a的这个方法了,相当于对象b“借用”a的方法。
const a = {
val: 1,
fn: function () {
console.log(this.val);
},
};
const b = {
val: 2,
};
a.fn(); // 1
a.fn.call(b); // 2这样的话,某个对象就不用添加一些方法了,而是通过改变其它对象的方法的this指向,“借用” 其它对象的方法。
举一个非常简单的场景。假如我们获取的多个 DOM 的集合,然后想要使用forEach来遍历这些集合,并打印出每个 DOM 的内容。可是因为这个集合不是 JavaScript 规范(严格意义上)的数组,无法使用数组的forEach方法了。怎么办?其实可以 “借用” 数组的这个方法。
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
const lis = document.getElementsByTagName("li");
// lis.forEach(item => console.log(item.innerText)) // 报错
Array.prototype.forEach.call(lis, (item) => console.log(item.innerText));
</script>
</body>通过document.getElementsByTagName('tag')获取到的lis的类型是HTMLCollection类型,这不是一个规范的数组,而是一个类数组,无法使用数组的 API。这时候就可以通过call使得Array.prototype.forEach方法的this指向lis,让lis“借用” 这个方法了。
这种 “借用” 方法的手段在一些设计模式中用得很广,以后有机会再介绍下。
手动实现#
call、apply和bind方法的改变this指向原理到底是什么呢?
其实很简单,我们知道对象里面的方法的this一定指向该对象本身,只要我们把函数放入到所需的对象里面,然后像调用方法一样调用这个函数,就可以使这个函数的this指向这个对象了。
下面分别实现call、apply和bind方法。
手动实现call#
实现原理:
- 把需要改变
this指向的函数放入到指向的对象中。 - 然后把放入的函数使用对象的方法去调用,调用时记得传入函数所需的参数,并且存储调用的结果。
- 调用完成之后在删除这个函数。
- 最后返回函数调用后的结果。
Function.prototype.selfCall = function (context = window) {
// 给 context 添加一个属性 fn ,赋值为 this,即赋值为需要改变指向的函数本身,即 getValue
// getValue.call(context, 'hale', 18) => context.fn = getValue
context.fn = this;
const args = [...arguments].slice(1); // 取出 context 后面的参数
const result = context.fn(...args); // => getValue('hale', 18)
delete context.fn; // 删除 fn
return result; // 返回结果
};手动实现apply#
apply的实现原理和call是一样的,但是因为它的参数是一个数组,所以需要额外处理一下参数,即把参数组成的数组展开。
Function.prototype.selfApply = function (context = window) {
context.fn = this;
const result = arguments[1] ? context.fn(...arguments[1]) : context.fn();
delete context.fn;
return result;
};手动实现bind#
bind的实现稍微复杂点,需要考虑更多的因素。因为bind不是改变指向后直接调用函数,而是生成一个新的函数,所以需要判断生成的函数的原型和原来的函数是否相同。如果相同,函数返回原来函数的实例对象,如果不相同,则和上面的call和apply原理一样。
这里的参数有两种,第一种是改变this指向时传入的参数,第二种是调用生成的新函数时传入的参数,第一种参数优先级更高,优先使用它,如果没有第一种参数,而使用第二种参数。
Function.prototype.selfBind = function (context = window) {
const self = this;
const args = [...arguments].slice(1);
// 返回一个函数
return function F() {
return this instanceof F
? new self(...args, ...arguments)
: self.apply(context, args.concat(...arguments));
};
};测试#
测试上面的手动函数,你会发现和 JavaScript 原生的方法效果是一样的。
const obj = {
value: "obj",
};
window.value = "val";
function getValue(name, age) {
console.log(name, age, this.value);
}
const obj = {
value: "obj",
};
window.value = "val";
function getValue(name, age) {
console.log(name, age, this.value);
}
getValue("Hale", 18); // Hale 18 val 。this 指向 window
// 改变 this 指向为 obj 对象
// 通过 selfCall 改变
getValue.selfCall(obj, "Hale", 18); // 直接调用 Hale 18 obj
// 通过 selfApply 改变
getValue.selfApply(obj, ["Hale", 18]); // 直接调用 Hale 18 obj
// 通过 selfBind 改变
const fn = getValue.selfBind(obj, "Amy", 20); // 传入参数,返回一个新的函数
fn(); // Amy 20 obj
fn("Hale", 18); // Amy 20 obj 参数不会被覆盖
const fn2 = getValue.selfBind(obj); // 不传入参数
fn2(); // undefined undefined 'obj'
fn2("Hale", 18); // Hale 18 obj 参数为传入的参数总结#
this算是 JavaScript 语言的一个知识难点,如果想要用好它,请记住一点:this的指向由其所在的函数调用时(运行时)的上下文决定。
另外,箭头函数没有this,箭头函数内部的this指向的是自外部函数的this值。最后具体情况具体分析,根据实际情况判断this的指向,灵活使用this。
本文都是个人使用 JavaScript 后的一些见解,如有错误,欢迎指正。