在一些编程语言(如 Java、C#)中, this 可以在类中代表当前实例。而到了 JavaScript 这里,this 不只在类中出现,所代表的东西也变化多端,很难理解。本文将简单地梳理一下 JavaScript 中的 this。
概括地说,JavaScript 中 this 的值要看它出现在哪种函数中:
普通函数 (function f() { ... }):this 的值取决于函数的调用方式
箭头函数 (() => { ... }):this 的值取决于函数的定义位置
下面来看一些具体的例子。
为了更好地贴合现代前端,本文所有的示例均为严格模式。另外,本文所有的示例均在 Node.js CommonJS 环境中运行。
普通函数 情况 1:独立函数 1 2 3 4 5 6 7 "use strict" ;function f ( ) { console .log (this ); }f ();
在独立的普通函数中,this 为 undefined(非严格模式下为 globalThis)。
情况 2:对象方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 "use strict" ;const obj = { name : "Tom" , f : function ( ) { console .log (this ?.name ); }, g ( ) { console .log (this ?.name ); }, }; obj.f (); obj.g (); const detached_f = obj.f ;const detached_g = obj.g ;detached_f (); detached_g ();
普通函数作为对象方法时,无论是什么写法,调用方式决定了 this 的值。上述代码中,obj.f === detached_f,然而 obj.f() 时 this 指向对象 obj 本身,detached_f() 时 this 却是 undefined。
情况 3:类方法 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 "use strict" ;class Person { constructor ( ) { this .name = "Tom" ; this .h = function ( ) { console .log (this ?.name ); }; } f ( ) { console .log (this ?.name ); } g = function ( ) { console .log (this ?.name ); }; }const p = new Person (); p.f (); p.g (); p.h (); const detached_f = p.f ;const detached_g = p.g ;const detached_h = p.h ;detached_f (); detached_g (); detached_h ();
普通函数作为类方法时,和对象方法一样,无论是原型方法还是类实例上的函数,无论是什么写法,调用方式决定了 this 的值。p.f 中的 this 指向当前实例,而 detached_f 中的 this 为 undefined。
情况 4:作为构造器 1 2 3 4 5 6 7 8 "use strict" ;function Person ( ) { this .name = "Tom" ; console .log (this ); }new Person ();
把一个普通函数当作构造器使用时,this 指向一个新的对象。
使用了简写的对象方法和类方法不能当作构造器使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const obj = { name : "Tom" , f ( ) {}, };class Person { g ( ) {} }const p = new Person ();new obj.f (); new p.g ();
箭头函数 情况 5:独立函数 1 2 3 4 5 6 7 "use strict" ;const f = ( ) => { console .log (this === module .exports ); };f ();
箭头函数没有自己的 this 绑定,而是利用闭包机制,保留了定义时外部词法环境的 this。这里是顶层的 this,在 Node.js CommonJS 中,也就是 module.exports。
情况 6:对象方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 "use strict" ;const obj = { name : "Tom" , f : () => { console .log (this === module .exports ); }, }; obj.f (); const detached_f = obj.f ;detached_f ();
由于箭头函数中的 this 来自定义时的外部词法环境,而对象亦没有自己的 this 绑定,所以 obj.f 中的 this 直接来自顶层,也就是 module.exports。而且,由于箭头函数并不像普通函数那样——由调用方式决定 this 的值——detached_f 中的 this 也是 module.exports。
情况 7:类方法 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 "use strict" ;class Person { constructor ( ) { this .name = "Tom" ; this .g = () => { console .log (this .name ); }; } f = () => { console .log (this .name ); }; }const p = new Person (); p.f (); p.g (); const detached_f = p.f ;const detached_g = p.g ;detached_f (); detached_g ();
类构造器中的 this 指向的是当前实例,这点 JavaScript 和 Java/C# 是一样的。因此,p.g 中的 this 是当前实例。而 p.f 和 p.g 基本是等效的,都是依附在每个实例上、而非原型上的。它等效于被定义在构造器中,因此它内部的 this 也是当前实例。更有意思的是,由于箭头函数中的 this 不取决于调用方式,而是依靠闭包机制,detached_f 和 detached_g 中的 this 也不会丢失。
情况 8:作为构造器 1 2 3 4 5 6 7 8 "use strict" ;const Person = ( ) => { this .name = "Tom" ; console .log (this ); };new Person ();
箭头函数和使用了简写的对象方法和类方法一样,不能当作构造器使用。
手动挂载函数 正如前面我们可以从对象/类实例中取下函数那样,我们也可以将函数挂到对象/类实例上。
情况 9:手动挂载的普通函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 "use strict" ;function sayName ( ) { console .log (this .name ); }const obj = { name : "Tom" , }; obj.sayName = sayName; obj.sayName (); class Person { constructor ( ) { this .name = "Tom" ; } }const p = new Person (); p.sayName = sayName; p.sayName ();
由于普通函数中的 this 取决于调用方式,当 sayName 被挂载到 obj 和 p 上、并以 obj.sayName() 和 p.sayName() 的形式被调用时,其中的 this 就会变成 obj 和 p。
情况 10:手动挂载的箭头函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 "use strict" ;const sayName = ( ) => { console .log (this === module .exports ); };const obj = { name : "Tom" , }; obj.sayName = sayName; obj.sayName (); class Person { constructor ( ) { this .name = "Tom" ; } }const p = new Person (); p.sayName = sayName; p.sayName ();
由于箭头函数中的 this 来自它定义时的外部词法环境,挂载并不能影响它其中的 this。
call/apply/bind 对于普通函数,可以利用 call/apply/bind 手动指定其中的 this。
情况 11:普通函数和 call/apply/bind 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 "use strict" ;function sayName (prefix ) { console .log (prefix + this .name ); }const obj = { name : "Tom" , }; sayName.call (obj, "Hi, " ); sayName.apply (obj, ["Hi, " ]); const boundSayName = sayName.bind (obj, "Hi, " );boundSayName ();
对于普通函数,call 会指定 this,接收一个或多个参数,并执行函数。apply 和 call 类似,只是以数组的形式接收参数。bind 则返回一个被预置了 this 和参数的新函数。
情况 12:箭头函数和 call/apply/bind 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 "use strict" ;const f = (prefix ) => { console .log (this === module .exports , prefix); };const obj = { name : "Tom" , }; f.call (obj, "call" ); f.apply (obj, ["apply" ]); const bound_f = f.bind (obj, "bind" );bound_f ();
箭头函数也可以使用 call/apply/bind。不过由于它的 this 来源于定义时的外部词法环境,这些方法都不能改变它的 this,bind 也只能预置参数。
附:手动实现 call/apply/bind 曾经在一次面试中遇到过让你自己实现一遍 call/apply/bind 的题目(当然不能用已有的 call/apply/bind)。虽然这种题目非常八股文,但若熟悉了以上的知识,其实不难拼凑出一个答案。
1 2 3 4 5 6 7 8 9 10 Function .prototype .myCall = function (context, ...args ) { context = context ?? globalThis; const key = Symbol ("fn" ); context[key] = this ; const result = context[key](...args); delete context[key]; return result; };
首先要实现的是 myCall。在 Function 的原型中加入一个函数 myCall,这里的 this 是 Function 类的实例,也就是目标函数(类似于上面的类方法)。而通过将目标函数挂载到 context 上,目标函数中的 this 就变成了 context。最后以 context[key]() 的形式调用,并确保参数和返回值的传递即可。
这里的 Symbol("fn") 是为了生成一个唯一的键,防止覆盖 context 中已有的键值。
1 2 3 4 5 6 7 8 9 10 11 Function .prototype .myApply = function (context, args = [] ) { return this .myCall (context, ...args); };Function .prototype .myBind = function (context, ...boundArgs ) { const fn = this ; return function (...args ) { return fn.myCall (context, ...boundArgs, ...args); }; };
接着,利用 myCall 和闭包就能实现 myApply 和 myBind。当然,这里的 myCall/myApply/myBind 只是教学用途的简化版,一些细节和真实的 call/apply/bind 是不一样的。
参考资料