ES6类方法中this指向异常及私有属性访问问题
刚接触ES6类,原本以为类中的this指向应该可预测且始终指向实例对象,但实际情况并非如此:
class BodyPixController { #target; //Declare a private property constructor(target){ this.#target = target; // Set the property console.log(this); // logs: BodyPixController {#target: 'photo'} addEventListener('load', this.init); // Calls the method } init() { console.log(this); // logs: Window {window: Window, self: Window, document: document, name: '', location: Location,…} const img = document.getElementById(this.#target); // Throws error: Cannot read private member #target from an object whose class did not declare it console.log(img); async function loadAndPredict() { const net = await bodyPix.load( /** optional arguments, see below **/ ); const segmentation = await net.segmentPerson(img); console.log(segmentation); } loadAndPredict(); } }
上述代码中,this仅在构造函数内指向实例对象,调用init()方法时this指向Window,请问该现象的原因是什么?如何在类方法内正确访问私有属性?
原因分析
JavaScript中函数的this指向完全由调用方式决定,和定义位置无关。当你把this.init作为回调传给addEventListener时,本质是单独传递了这个函数的引用——事件触发时,浏览器会直接调用该函数,此时函数的执行上下文是触发事件的对象(这里是window,因为load事件默认绑定在全局window对象上),所以this自然指向window,而非类实例。私有属性#target属于类实例,window没有这个私有成员,因此报错。
解决方法
三种常用方式可确保类方法里的this始终指向实例,从而正常访问私有属性:
1. 在构造函数中用bind绑定this
修改构造函数,把init方法的this强制绑定到类实例:
constructor(target){ this.#target = target; console.log(this); // 绑定this到实例 this.init = this.init.bind(this); addEventListener('load', this.init); }
bind会返回一个新函数,这个函数的this永久绑定为指定实例,不管后续调用方式如何。
2. 用箭头函数作为事件回调
箭头函数的this继承自外层作用域的this,构造函数里的this就是类实例,用箭头函数包裹调用即可:
constructor(target){ this.#target = target; console.log(this); addEventListener('load', () => this.init()); }
事件触发时,箭头函数里的this是构造函数的实例,调用this.init()时,init内的this自然也指向实例。
3. 将init定义为类字段箭头函数
直接把init方法定义成类字段形式的箭头函数,箭头函数的this会固定指向类实例:
class BodyPixController { #target; constructor(target){ this.#target = target; console.log(this); addEventListener('load', this.init); } // 用箭头函数定义方法 init = () => { console.log(this); // 此时this指向类实例 const img = document.getElementById(this.#target); console.log(img); async function loadAndPredict() { const net = await bodyPix.load( /** optional arguments, see below **/ ); const segmentation = await net.segmentPerson(img); console.log(segmentation); } loadAndPredict(); } }
这种写法无需额外绑定,更简洁,缺点是每个类实例都会创建一个新的函数副本(普通类方法存在于原型上),但多数场景下该影响可忽略。
内容的提问来源于stack exchange,提问作者wagster

