What's the correct way to declare event handler in class of JavaScript?

huangapple go评论96阅读模式
英文:

What's the correct way to declare event handler in class of JavaScript?

问题

根据你提供的内容,我为你翻译了以下部分:

根据我在MDN上的查询,类的方法看起来像这样:

class Foo {
	method1 (){
	  // method1的内容
	}
}

然而,我发现这对于事件处理程序来说并不好用。

在上面的测试页面中,点击复选框后的结果如下图所示:

What's the correct way to declare event handler in class of JavaScript?

阅读关于箭头函数的作用域后,我理解了为什么会有这种差异。但是,使用箭头函数来声明类的方法看起来有些奇怪,我是否做得正确?

另外,由于我不喜欢在一个类中存在两种不同的函数风格,如果可能的话,我更喜欢对所有其他方法使用箭头函数,但我不确定这是否适用于constructor,或者它是否存在潜在的故障或安全问题。

请问对此有什么看法吗?

英文:

As I checked on MDN, method of a class looks like this:

class Foo {
	method1 (){
	  // content of method1
	}
}

However I found it's not good for event handlers

<!doctype html>
<html lang="en">
<head>
	<title>test</title>
</head>
<body>
	<div class="settings">
		<div>
			<label for="cb1">checkbox</label>
			<input id="cb1" type="checkbox"></input>
		</div>
	</div>
	
	<script>
		'use strict'
		class TEST{
			box = null;
			info = {content: {blah: "blah"}};
			
			init (){
				this.box = window.document.querySelector(".settings");
				this.box.addEventListener("change", this.handler);
				this.box.addEventListener("change", this.handler2);
			}
			
			handler = e=> {
				console.log("handler this: %o", this);
				console.log("handler info: %o", this.info.content);
			}
			handler2 (e) {
				console.log("handler2 this: %o", this);
				console.log("handler2 info: %o", this.info.content);
			}
		}
		let t = new TEST();
		t.init();
	</script>
</body>
</html>

In the test page above, click the checkbox then the result is
What's the correct way to declare event handler in class of JavaScript?

Reading about arrow function's scope then I understand why ther's the difference. But using arrow function to declare a method of class looks weird, did I do it correctly?

What's more since I don't like there're two kind of function style in one class, I prefer using arrow function for all other methods if possible, but I'm not sure this works for constructor or did it has any potential glitch or secure problem

Any opinion on this please?

答案1

得分: 0

handler()是一个箭头函数,因此它继承了外部作用域的this。不用担心。

但是对于作为实例原型中的函数的方法来说,情况就不同了。

当你将一个方法作为参数传递时,实际上是单独传递了该方法,而没有传递它的上下文(在我们的例子中是this)。有几种方法可以保持上下文:

使用.bind()

this.box.addEventListener("change", this.handler2.bind(this));

使用箭头函数:

this.box.addEventListener("change", e => this.handler2(e));

在构造函数中绑定this

constructor() {
    this.handler2 = this.handler2.bind(this);
}

你还可以在构造函数中循环遍历对象的原型,并绑定每个方法。

但更有趣的是,有一种通用的解决方案,而不需要修改类。

如果你想深入了解JS代理和原型,我们可以提供一个类包装器,自动绑定实例及其原型链中的所有方法(甚至支持super):

// 拦截`new`
const bindThis = what => new Proxy(what, {
    construct(_class, args, constructor) {

        const obj = Reflect.construct(...arguments);

        if (_class.name !== constructor.name) {
            return obj; // 基类,跳过
        }

        const bindContext = _obj => {
            for (const [name, def] of Object.entries(Object.getOwnPropertyDescriptors(_obj))) {
                
                if (typeof def.value === 'function' && name !== 'constructor' && 
                // 避免覆盖基类方法
                !Object.hasOwn(obj, name)) {
                    // 绑定所有方法的上下文
                    def.value = def.value.bind(obj);
                    // 使其看起来像普通属性(可枚举)
                    def.enumerable = true; 
                    Object.defineProperty(obj, name, def);
                }
            }
        };

        let context = obj;
        do {
            // 跳过Object.prototype以保持清晰
            Object.getPrototypeOf(context) && bindContext(context);
        } while (context = Object.getPrototypeOf(context));

        return obj;
    }
});

const TEST = bindThis(class TEST {
    box = null;
    info = {
        content: {
            blah: "blah"
        }
    };

    init() {
        this.box = window.document.querySelector(".settings");
        this.box.addEventListener("change", this.handler);
        this.box.addEventListener("change", this.handler2);
    }

    handler = e => {
        console.log("handler this: %o", this);
        console.log("handler info: %o", this.info.content);
    }
    handler2(e) {
        console.log("handler2 this: %o", this);
        console.log("handler2 info: %o", this.info.content);
    }
});

const CHILD = bindThis(class CHILD extends TEST {

    isChild = true;
    handler2(e) {
        console.log("OVERRIDDEN");
        super.handler2(e);
    }

});

let t = new TEST();
let c = new CHILD();

t.init();
c.init();

希望对你有所帮助!

英文:

handler() is an arrow function, so it inherits this from the outer scope. No worries.

But with methods which are functions in an instance's prototype the situation is different.

When you pass a method as an argument you basically pass it alone without its context (in our case this). There're several fixes how to keep the context:

Use .bind():

this.box.addEventListener("change", this.handler2.bind(this));

Use an arrow function:

this.box.addEventListener("change", e => this.handler2(e));

Bind this in the constructor:

constructor() {
this.handler2 = this.handler2.bind(this);
}

You can also loop through an object's prototypes in the constructor and bind each method.

But more interestingly is to have some generic solution without modifying classes.

If you want to dive deep into JS proxies and prototypes we can provide a class wrapper to automatically bind all methods in an instance and its prototype chain (it even supports super):

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

// intercept `new`
const bindThis = what =&gt; new Proxy(what, {
construct(_class, args, constructor) {
const obj = Reflect.construct(...arguments);
if (_class.name !== constructor.name) {
return obj; // the base class, skip it
}
const bindContext = _obj =&gt; {
for (const [name, def] of Object.entries(Object.getOwnPropertyDescriptors(_obj))) {
if (typeof def.value === &#39;function&#39; &amp;&amp; name !== &#39;constructor&#39; &amp;&amp; 
// avoid overridding by base class methods
!Object.hasOwn(obj, name)) {
// bind context for all the methods
def.value = def.value.bind(obj);
// make look like ordinary props (enumerable)
def.enumerable = true; 
Object.defineProperty(obj, name, def);
}
}
};
let context = obj;
do {
// skip Object.prototype for clearness
Object.getPrototypeOf(context) &amp;&amp; bindContext(context);
} while (context = Object.getPrototypeOf(context));
return obj;
}
});
const TEST = bindThis(class TEST {
box = null;
info = {
content: {
blah: &quot;blah&quot;
}
};
init() {
this.box = window.document.querySelector(&quot;.settings&quot;);
this.box.addEventListener(&quot;change&quot;, this.handler);
this.box.addEventListener(&quot;change&quot;, this.handler2);
}
handler = e =&gt; {
console.log(&quot;handler this: %o&quot;, this);
console.log(&quot;handler info: %o&quot;, this.info.content);
}
handler2(e) {
console.log(&quot;handler2 this: %o&quot;, this);
console.log(&quot;handler2 info: %o&quot;, this.info.content);
}
});
const CHILD = bindThis(class CHILD extends TEST {
isChild = true;
handler2(e) {
console.log(&quot;OVERRIDDEN&quot;);
super.handler2(e);
}
});
let t = new TEST();
let c = new CHILD();
t.init();
c.init();

<!-- language: lang-html -->

&lt;select class=&quot;settings&quot;&gt;
&lt;option&gt;-&lt;/option&gt;
&lt;option value=&quot;1&quot;&gt;option 1&lt;/option&gt;
&lt;/select&gt;

<!-- end snippet -->

答案2

得分: 0

使用箭头函数来声明类的方法看起来有些奇怪,我做得对吗?

是的,这是有效的,但请注意它们是类字段中的箭头函数,而不是方法。

另外,由于我不喜欢在一个类中存在两种函数风格,如果可能的话,我更喜欢对所有其他方法使用箭头函数,但我不确定这对构造函数是否有效或者是否存在潜在的问题。

是的,你不能在构造函数中使用这种风格,通常不应该使用它,因为它与继承不正常工作(无法正确覆盖,无法与super一起使用)并且使用的内存比共享原型方法多 - 箭头函数是每个实例创建的。

因此,只在真正需要的地方使用它。替代方法包括:

  • 在构造函数中显式创建箭头函数,而不使用类字段语法:

    class TEST {
    constructor() {
    this.box = null;
    this.info = {content: {blah: "blah"}};
    this.handler = e => {
    console.log("handler this: %o", this);
    console.log("handler info: %o", this.info.content);
    };
    }
    init() {
    this.box = window.document.querySelector(".settings");
    this.box.addEventListener("change", this.handler);
    this.box.addEventListener("change", this.handler2);
    }
    handler2(e) {
    console.log("handler2 this: %o", this);
    console.log("handler2 info: %o", this.info.content);
    }
    }
    
  • 在构造函数中定义方法并显式使用.bind()绑定它们:

    class TEST {
    box = null;
    info = {content: {blah: "blah"}};
    constructor() {
    this.handler = this.handler.bind(this);
    }
    init() {
    this.box = window.document.querySelector(".settings");
    this.box.addEventListener("change", this.handler);
    this.box.addEventListener("change", this.handler2);
    }
    handler(e) {
    console.log("handler this: %o", this);
    console.log("handler info: %o", this.info.content);
    }
    handler2(e) {
    console.log("handler2 this: %o", this);
    console.log("handler2 info: %o", this.info.content);
    }
    }
    
英文:

> But using arrow function to declare a method of class looks weird, did I do it correctly?

Yes, this works, but notice they are arrow functions in class fields and not methods.

> What's more since I don't like there're two kind of function style in one class, I prefer using arrow function for all other methods if possible, but I'm not sure this works for constructor or did it has any potential glitch?

Yes, you cannot use this style for the constructor, and you should not generally use this because it doesn't work properly with inheritance (cannot be overridden properly, cannot be used with super) and uses more memory than a shared prototype method - the arrow functions are created per instance.

So use this only where you really need it. Alternative approaches are

  • creating the arrow functions explicitly in the constructor, without class field syntax:

    class TEST {
    constructor() {
    this.box = null;
    this.info = {content: {blah: &quot;blah&quot;}};
    this.handler = e =&gt; {
    console.log(&quot;handler this: %o&quot;, this);
    console.log(&quot;handler info: %o&quot;, this.info.content);
    };
    }
    init() {
    this.box = window.document.querySelector(&quot;.settings&quot;);
    this.box.addEventListener(&quot;change&quot;, this.handler);
    this.box.addEventListener(&quot;change&quot;, this.handler2);
    }
    handler2(e) {
    console.log(&quot;handler2 this: %o&quot;, this);
    console.log(&quot;handler2 info: %o&quot;, this.info.content);
    }
    }
    
  • defining methods and .bind()ing them explicitly in the constructor:

    class TEST {
    box = null;
    info = {content: {blah: &quot;blah&quot;}};
    constructor() {
    this.handler = this.handler.bind(this);
    }
    init() {
    this.box = window.document.querySelector(&quot;.settings&quot;);
    this.box.addEventListener(&quot;change&quot;, this.handler);
    this.box.addEventListener(&quot;change&quot;, this.handler2);
    }
    handler(e) {
    console.log(&quot;handler this: %o&quot;, this);
    console.log(&quot;handler info: %o&quot;, this.info.content);
    }
    handler2(e) {
    console.log(&quot;handler2 this: %o&quot;, this);
    console.log(&quot;handler2 info: %o&quot;, this.info.content);
    }
    }
    

huangapple
  • 本文由 发表于 2023年8月9日 00:48:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/76861645.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定