起因
对 js 的 this 指向问题还是会有点模糊,我决定下点功夫,写下这篇文章,彻底把 this 搞明白。
什么是 this ?
这也是我发出的第一个问题,究竟什么是 this ?在 js 中 this 代表的到底是什么?根据 w3c 的描述:
The JavaScript this keyword refers to the object it belongs to.
在 js 中 this 关键字代表它所属对象的引用。
再根据 MDN 的描述,
In the global execution context (outside of any function), this refers to the global object whether in strict mode or not.
this 表示当前执行上下文( global、function 或 eval )的一个属性,在非严格模式下,总是指向一个对象,在严格模式下可以是任意值。
由此可见 this 的指向是不确定的,是在运行时确定的,而且 this 在不同的情况下,其代表的含义也不一样。
下面我将通过本文,彻底分析 this 的所有形式。
注:全局对象,在浏览器端代表 window 对象,在 nodejs 环境下代表 global 对象,以下不再区分,简称全局对象。
this 存在的情形
首先要考虑 this 一般会出现在哪些情况呢?
全局状态下
一般函数内
this 和对象转换
原型链中
与 DOM 相关
其实 this 一般都是出现在函数内,所以在第六点单独称之为 「一般函数」,下面分别分析。
全局状态下
无论是否在严格模式下,在全局执行环境中(在任何函数体外部) this 都指向全局对象
。如:
一般函数内
在全局状态下、对象内、class 内等的函数,我称之为一般函数,也是使用最多的情况。
函数在全局状态下
在普通函数内部,this 的值取决于函数被调用的方式。
在严格模式下,如果进入执行环境时没有设置 this 的值,this 会保持为 undefined,非严格模式下指向全局对象。
在箭头函数中,this 取决于函数被创建时的环境。
因此在全局情况下,无论是否为严格模式,this 指向都是全局对象。
请务必记住以上重点标记的两句话,在很多地方也都用到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function f2 ( ) { "use strict" ; console .log(this ); } function f3 ( ) { console .log(this ); } const f4 = () => { console .log(this ); }; const f5 = () => { "use strict" ; console .log(this ); }; f2(); f3(); f4(); f5();
函数在对象内部
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const obj = { type: 1 , func1: function ( ) { console .log(this ); }, func2: () => { console .log(this ); }, }; obj.func1(); obj.func2(); const { func1, func2 } = obj;func1(); func2();
在严格模式下再运行一遍
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 "use strict" ;const obj = { type: 1 , func1: function ( ) { console .log(this ); }, func2: () => { console .log(this ); }, }; obj.func1(); obj.func2(); const { func1, func2 } = obj;func1(); func2();
当作为对象的函数时,this 的绑定只受最接近的成员引用的影响。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function independent ( ) { return this .prop; } const independent2 = () => { return this .prop; }; var obj = { prop: 37 , func1: function ( ) { return this .prop; }, func2: () => { return this .prop; }, }; obj.func3 = independent; obj.func4 = independent2; obj.child = { func5 : independent, func6 : independent2, prop : 42 }; console .log(obj.func1()); console .log(obj.func2()); console .log(obj.func3()); console .log(obj.func4()); console .log(obj.child.func5()); console .log(obj.child.func6());
函数在 class 内
this 在类中的表现与在函数中类似,因为类本质上也是函数,但也有一些区别和注意事项。在类的构造函数中,this 是一个常规对象。类中所有非静态的方法都会被添加到 this 的原型中
。
和其他普通函数一样,类方法中的 this 值取决于它们如何被调用。需要注意的是类内部总是严格模式
。类的方法内部如果含有 this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class TestClass { normalFunction() { console .log("normal function:" , this ); } arrowFunction = () => { console .log("arrow function:" , this ); }; } const t = new TestClass();const { normalFunction, arrowFunction } = t;normalFunction(); arrowFunction();
在上面代码中
1 const { normalFunction, arrowFunction } = t;
其实相当于如下代码
1 2 3 4 function normalFunction2 ( ) { "use strict" ; console .log("normal function:" , this ); }
因为普通函数的 this 是由调用者确定的,如果在非严格模式下,直接调用,则 this 指向全局对象,如果是严格模式下,this 则为 undefined。而箭头函数是由创建时就确定了,所以 arrowFunction 实际指向的仍是 TestClass 实例。
有时,也可以通过 bind 方法使类中的 this 值总是指向这个类实例。为了做到这一点,可在构造函数中绑定类方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Car { constructor () { this .sayBye = this .sayBye.bind(this ); } sayHi() { console .log(`Hello from ${this .name} ` ); } sayBye() { console .log(`Bye from ${this .name} ` ); } get name() { return "Ferrari" ; } } class Bird { get name() { return "Tweety" ; } } const car = new Car();const bird = new Bird();car.sayHi(); bird.sayHi = car.sayHi; bird.sayHi(); bird.sayBye = car.sayBye; bird.sayBye();
在派生类中的构造函数没有初始的 this 绑定。在构造函数中调用 super()
会生成一个 this 绑定。所以在子类的构造函数中,如果要使用 this 的话必须要调用 super()
,相当于 this = new Base();
。派生类不能在调用 super() 之前返回,除非其构造函数返回的是一个对象,或者根本没有构造函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Base {}class Good extends Base {}class AlsoGood extends Base { constructor () { return { a : 5 }; } } class Bad extends Base { constructor () {} } new Good();new AlsoGood();new Bad();
更多关于 class 的内容可以查看阮一峰老师关于 class 的说明
改变 this 指向
通过函数的 call, apply, bind 方法是可以改变 this 的指向的,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var obj = { a : "Custom" };var a = "Global" ;function func1 ( ) { return this .a; } const func2 = func1.bind(obj);func1(obj); func1.call(obj); func1.apply(obj); func2(obj);
this 和对象转换
在非严格模式下使用 call 和 apply 时,如果用作 this 的值不是对象,则会被尝试转换为对象。null 和 undefined 被转换为全局对象。原始值如 7 或 ‘foo’ 会使用相应构造函数转换为对象。因此 7 会被转换为 new Number(7) 生成的对象,字符串 ‘foo’ 会转换为 new String(‘foo’) 生成的对象。
1 2 3 4 5 6 7 8 9 function bar ( ) { console .log(this ); } bar.call(7 ); bar.call("foo" ); bar.call(undefined );
1 2 3 4 5 6 7 8 9 "use strict" ;function bar ( ) { console .log(this ); } bar.call(7 ); bar.call("foo" ); bar.call(undefined );
ECMAScript 5 引入了 Function.prototype.bind()
。调用 f.bind(someObject)会创建一个与 f 具有相同函数体和作用域的函数,但是在这个新函数中,this 将永久地被绑定到了 bind 的第一个参数,无论这个函数是如何被调用的。
1 2 3 4 5 6 7 8 9 10 11 12 function f ( ) { return this .a; } var g = f.bind({ a : "azerty" });console .log(g()); var h = g.bind({ a : "yoo" }); console .log(h()); var o = { a : 37 , f : f, g : g, h : h };console .log(o.a, o.f(), o.g(), o.h());
原型链中
对于在对象原型链上某处定义的方法,同样的概念也适用。如果该方法存在于一个对象的原型链上,那么 this 指向的是调用这个方法的对象,就像该方法就在这个对象上一样。
1 2 3 4 5 6 7 8 9 10 11 var o = { f: function ( ) { return this .a + this .b; }, }; var p = Object .create(o);p.a = 1 ; p.b = 4 ; console .log(p.f());
相同的概念也适用于当函数在一个 getter 或者 setter 中被调用。用作 getter 或 setter 的函数都会把 this 绑定到设置或获取属性的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function sum ( ) { return this .a + this .b + this .c; } var o = { a: 1 , b: 2 , c: 3 , get average() { return (this .a + this .b + this .c) / 3 ; }, }; Object .defineProperty(o, "sum" , { get : sum, enumerable: true, configurable: true, }); console.log(o.average, o.sum); // 2, 6
与 DOM 相关
当函数被用作事件处理函数时,它的 this 指向触发事件的元素(一些浏览器在使用非 addEventListener 的函数动态地添加监听函数时不遵守这个约定)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function bluify (e ) { console .log(this === e.currentTarget); console .log(this === e.target); this .style.backgroundColor = "#A5D9F3" ; } var elements = document .getElementsByTagName("*" );for (var i = 0 ; i < elements.length; i++) { elements[i].addEventListener("click" , bluify, false ); }
当代码被内联 on-event 处理函数 调用时,它的 this 指向监听器所在的 DOM 元素:
1 2 3 4 5 <button onclick ="alert(this.tagName.toLowerCase());" > Show this</button > <button onclick ="alert((function(){return this})());" > Show inner this</button >
最后
分析一道题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Test { prop = { func1: function ( ) { console .log(this ); }, func2: () => { console .log(this ); }, }; } const t = new Test();t.prop.func1(); t.prop.func2(); const { prop } = t;prop.func1(); prop.func2(); const { func1, func2 } = prop;func1(); func2();
答案已经公布,想想为什么呢?