前言
在JavaScript里面,一直有一个很神秘的关键字叫做this。对于this,在很多教程里面其实都没有讲得很细致,所以就导致了很多同行对于this感觉道不清说不明,充满着神秘。今天,我们就来探索一下this,试图揭开它神秘的面纱,探个究竟。
什么是this
this是当前执行上下文(global、function 或 eval)的一个属性,在非严格模式下,总是指向一个对象,在严格模式下可以是任意值。this是JavaScript里的关键字。
为什么要用this
this的作用其实只是为了提供一些遍历的属性访问方式而已。对于为什么要用this,我们这里使用反证法来说明。假如我们遇到以下场景:
Function1 需要调用function2 ,function2需要调用function3,在不用this的情况下,我们可能需要这么写:
function whoIam(context){
console.log("i am"+context.name);
sayHi(context)
}
function sayHi(context){
console.log(context.name+"say hi");
sayHello(context);
}
function sayHello(context){
console.log(context.name+"say hello");
}
var test1 = {
name:"test1"
}
whoIam(test1)
假如层级很多,模式很复杂,我们需要将这个对象一直传递下去,到时候代码混乱,跟踪就变得困难。假如我们使用this,我们则只需要:
function whoIam(){
console.log("i am"+this.name);
sayHi.call(this)
}
function sayHi(){
console.log(this.name+"say hi");
sayHello.call(this);
}
function sayHello(){
console.log(this.name+"say hello");
}
var test1 = {
name:"test1"
}
whoIam.call(test1)
this能够帮助我们解决隐式传递对象引用的问题,使得api 更加简洁且复用。
关于this的误区
对于this值的认识,一直以来有两个误区:
-
this等于函数本身
这个误解是自然而言,符合大多数人的第一直觉。甚至写过其他语言程序的都会习惯性认为this就等于函数本身,因为其他语言就是这样的。但是考虑以下代码:
function foo(num) { console.log("foo: " + num); // 记录 foo 被调用的次数 this.count++; } foo.count = 0; var i; for (i = 0; i < 10; i++) { if (i > 5) { foo(i); } } // foo: 6 // foo: 7 // foo: 8 // foo: 9 // foo 被调用了多少次? console.log(foo.count);在这个例子里,foo一种被调用了4次,但是最后再打印foo.count的时候,他还是等于0。所以this并不等于函数本身。
-
this等于作用域
思考以下代码:
function foo() { var a = 2; this.bar(); } function bar() { console.log( this.a ); } foo(); // ReferenceError: a is not defined按道理,如果this等于作用域的话,那么foo里面的bar应该是访问不了的,但事实上这段代码成功执行了bar,只不过因为bar无法访问到a而报错而已。所以其实this并不等于作用域
this值的计算方式
根据mdn的描述,this值的计算取决于它的调用方式
1.在全局上下文调用
无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this都指向全局对象。
// 在浏览器中, window 对象同时也是全局对象:
console.log(this === window); // true
a = 37;
console.log(window.a); // 37
this.b = "MDN";
console.log(window.b) // "MDN"
console.log(b) // "MDN"
2.函数上下文调用
在函数内部,this的值取决于函数被调用的方式。并且不同模式下结果不同
在一般宽松模式下,this的值默认指向全局对象,浏览器中就是window。
function f1(){
return this;
}
//在浏览器中:
f1() === window; //在浏览器中,全局对象是window
//在Node中:
f1() === globalThis;
在严格模式下,this会保持为undefined
function f2(){
"use strict"; // 这里是严格模式
return this;
}
f2() === undefined; // true
3.类上下文调用
this在 类 中的表现与在函数中类似,因为类本质上也是函数,但也有一些区别和注意事项。
在类的构造函数中,this是一个常规对象。类中所有非静态的方法都会被添加到 this 的原型中:
class Example {
constructor() {
const proto = Object.getPrototypeOf(this);
console.log(Object.getOwnPropertyNames(proto));
}
first(){}
second(){}
static third(){}
}
new Example(); // ['constructor', 'first', 'second']
4.派生类
不像基类的构造函数,派生类的构造函数没有初始的 this 绑定。在构造函数中调用 super()会生成一个 this 绑定,并相当于执行如下代码,Base为基类:
this = new Base();
需要注意的是,派生类的构造函数里在super()之前使用this将会报错
class Base {}
//没有构造函数,可以
class Good extends Base {}
//构造函数直接返回一个对象,可以
class AlsoGood extends Base {
constructor() {
return {a: 5};
}
}
//构造函数没有调用super也不返回对象,不可以
class Bad extends Base {
constructor() {}
}
//派生类在调用super之前,this是没有赋值的,调用super之后,this将被复制为父类对象,也就是this = new Base()
class Bad2 extends Base {
constructor() {
// console.log(this);// ReferenceError
super();
console.log(this);//
}
}
new Good();
new AlsoGood();
new Bad2(); // ReferenceError
5.箭头函数
无论如何,foo 的 this 被设置为他被创建时的环境。这同样适用于在其他函数内创建的箭头函数:这些箭头函数的this被设置为封闭的词法环境的。
// 创建一个含有bar方法的obj对象,
// bar返回一个函数,
// 这个函数返回this,
// 这个返回的函数是以箭头函数创建的,
// 所以它的this被永久绑定到了它外层函数的this。
// bar的值可以在调用中设置,这反过来又设置了返回函数的值。
var obj = {
bar: function() {
var x = (() => this);
return x;
}
};
// 作为obj对象的一个方法来调用bar,把它的this绑定到obj。
// 将返回的函数的引用赋值给fn。
var fn = obj.bar();
// 直接调用fn而不设置this,
// 通常(即不使用箭头函数的情况)默认为全局对象
// 若在严格模式则为undefined
console.log(fn() === obj); // true
// 但是注意,如果你只是引用obj的方法,
// 而没有调用它
var fn2 = obj.bar;
// 那么调用箭头函数后,this指向window,因为它从 bar 继承了this。
console.log(fn2()() == window); // true
6.对象方式调用
当函数作为对象里的方法被调用时,this 被设置为调用该函数的对象。
var o = {
prop: 37,
f: function() {
return this.prop;
}
};
console.log(o.f()); // 37
但需要注意的是
var o = {
prop: 37,
f: function() {
return this.prop;
}
};
var f = o.f;
console.log(f());//undefined
由于f只是function的一个引用,所以这种方式仅仅相当于单独调用function
7.原型链中的this和getter、setter中的this
原型链中的this和getter、setter中的this 指向都是调用的对象本身
var o = {
f: function() {
return this.a + this.b;
}
};
var p = Object.create(o);
p.a = 1;
p.b = 4;
console.log(p.f()); // 5
8.函数作为构造函数使用
当一个函数用作构造函数时(使用new关键字),它的this被绑定到正在构造的新对象。
function C(){
this.a = 37;
}
var o = new C();
console.log(o.a); // logs 37
function C2(){
this.a = 37;
return {a:38};
}
o = new C2();
console.log(o.a); // logs 38
9.作为DOM事件处理函数
当函数被用作事件处理函数时,它的 this 指向触发事件的元素(一些浏览器在使用非 addEventListener 的函数动态地添加监听函数时不遵守这个约定)。
this强制性绑定的三种方式
1.call
function add(c, d) {
return this.a + this.b + c + d;
}
var o = {a: 1, b: 3};
// 第一个参数是用作“this”的对象
// 其余参数用作函数的参数
add.call(o, 5, 7); // 16
2.call
function add(c, d) {
return this.a + this.b + c + d;
}
var o = {a: 1, b: 3};
// 第一个参数是用作“this”的对象
// 第二个参数是一个数组,数组中的两个成员用作函数参数
add.apply(o, [10, 20]); // 34
3.bind
ECMAScript 5 引入了 Function.prototype.bind()。调用f.bind(someObject)会创建一个与f具有相同函数体和作用域的函数,但是在这个新函数中,this将永久地被绑定到了bind的第一个参数,无论这个函数是如何被调用的。
function f(){
return this.a;
}
var g = f.bind({a:"azerty"});
console.log(g()); // azerty
var h = g.bind({a:'yoo'}); // bind只生效一次!
console.log(h()); // azerty
var o = {a:37, f:f, g:g, h:h};
console.log(o.a, o.f(), o.g(), o.h()); // 37, 37, azerty, azerty
主要再次注意的是:无论何种方式,箭头函数里面的this都是等于箭头函数被创建时的值,apply/call/bind都无法改变。
总结
this的作用是能够让开发者更加方便地使用属性和传递对象,从而也使得编写代码时更加通用。this的值跟调用方式相关,所以后续在使用this值时一定要多加注意。