在 JavaScript 类中声明事件处理程序的正确方法是什么?[复制]

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

提问人:Byzod 提问时间:8/9/2023 更新时间:8/9/2023 访问量:71

问:

当我在 MDN 上检查时,类的方法如下所示:

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

但是,我发现它对事件处理程序不利

<!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>

在上面的测试页面中,单击复选框,然后结果是result

阅读箭头函数的作用域后,我就明白了为什么会有区别。但是使用箭头函数声明类的方法看起来很奇怪,我做对了吗?

更重要的是,由于我不喜欢一个类中有两种函数样式,如果可能的话,我更喜欢对所有其他方法使用箭头函数,但我不确定这是否适用于或是否有任何潜在的故障或安全问题constructor

请对此发表任何意见?

JavaScript 范围 事件处理 箭头函数

评论

0赞 evolutionxbox 8/9/2023
不知道为什么你不喜欢,有多种方法可以声明类方法。箭头函数不会重新绑定,这是它们存在的原因之一。构造函数不能是箭头函数。这没有意义。不要纠结于“什么看起来正确”this
0赞 Barmar 8/9/2023
如果将事件处理程序声明为普通函数,则将是事件目标,而不是类实例。this
0赞 Kaddath 8/9/2023
如果你不喜欢它的样式,你可以用它来代替:.这个问题可能更清楚,即如果将函数引用传递给事件处理程序,它将从绑定到 HTML 元素的全局范围执行。它更清晰,因为它就像说“使用一个将其保留为处理程序的函数”this.box.addEventListener("change", (e) => { this.handler2(e) });
1赞 evolutionxbox 8/9/2023
另一种方法是在构造函数中绑定方法
0赞 Alexander Nenashev 8/9/2023
添加了带有类代理包装器的自动解决方案

答:

0赞 Alexander Nenashev 8/9/2023 #1

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

但是对于实例原型中的方法,情况就不同了。function

当你将一个方法作为参数传递时,你基本上在没有上下文的情况下单独传递它(在我们的例子中)。有几个修复方法可以保留上下文: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

// intercept `new`
const bindThis = what => 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 => {
            for (const [name, def] of Object.entries(Object.getOwnPropertyDescriptors(_obj))) {
                
                if (typeof def.value === 'function' && name !== 'constructor' && 
                // 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) && 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();
<select class="settings">
<option>-</option>
<option value="1">option 1</option>
</select>

0赞 Bergi 8/9/2023 #2

但是使用箭头函数声明类的方法看起来很奇怪,我做对了吗?

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

更重要的是,由于我不喜欢一个类中有两种函数样式,如果可能的话,我更喜欢对所有其他方法使用箭头函数,但我不确定这是否适用于构造函数,或者它是否有任何潜在的故障?

是的,您不能将这种样式用于 ,并且通常不应该使用它,因为它不能与继承一起使用(不能正确覆盖,不能与 一起使用),并且比共享原型方法使用更多的内存 - 箭头函数是按实例创建的。constructorsuper

因此,请仅在您真正需要的地方使用它。替代方法是

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

    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);
        }
    }