找回密码
 立即注册
首页 业界区 业界 Maui 实践:让 JavaScript 的 this 怪物如同邻居家(强 ...

Maui 实践:让 JavaScript 的 this 怪物如同邻居家(强类型)的乖孩子

龙梨丝 5 天前
Maui 实践:让 JavaScript 的 this 怪物如同邻居家(强类型)的乖孩子

原创 夏群林 2025.10.20
MAUI,不能不说,好。也不得不说,好——多坑。
指望一个 Windows 平台当家的大厂,把自家与 Windows 平台深度绑定的 .Net 运行时,和 .Net 的天生语言 C#,如同支持 Windows 平台一样支持其他平台,那是想多了。因为喜欢 C# 的优美,还有 Visual Studio 的近乎完美,我选择性相信了当初他们宣称的跨平台支持能力。事已至此,得失不论。没必要质疑他们的努力,也没必要纠结他们的动机,反正结果是:现实不由人。
于是,我尝试引入 HTML 5,作为前端。而且是纯粹的那种,不要框架,只依赖原生。我本喜欢强类型语言,但这次,JavaScript  绕不过。好在语言相互借鉴,大概念趋同。本文专门谈谈 this, 似同非同的处理。
一、C# 中的 this:乖孩子

在 C# 这类强类型语言中,this 是让开发者省心的乖孩子,行为稳定、规则清晰,从定义到运行始终如一。

  • 编译期绑定指向,this 在写代码时锁死了关联当前类的实例,无论方法是直接调用、作为委托传递,还是通过其他方式触发,this 都不会“跑偏”。
    1. public class Person {
    2.     public string Name { get; set; }
    3.     public void SayHi() => Console.WriteLine($"Hi, {this.Name}");
    4. }
    5. var person = new Person { Name = "张三" };
    6. Action say = person.SayHi;                   // 提取方法引用
    7. say();                                                         // 输出 "Hi, 张三"(this 仍指向 person 实例)
    复制代码
  • 继承的先父后子,无论是隐式调用父类无参构造,还是显式调用有参构造,父类的构造函数总是先执行,父类成员先行完成初始化,不会出现子类访问未就绪的父类属性的情况。
    1. // 父类:仅定义有参构造,无无参构造
    2. public class Parent {
    3.     protected string Name;
    4.     public Parent(string name) { // 有参构造
    5.         this.Name = name;
    6.     }
    7. }
    8. // 子类:必须显式调用父类的 Parent(string) 构造,否则报错
    9. public class Child : Parent {
    10.     public int Age;
    11.     // 正确:显式调用父类有参构造
    12.     public Child(string name, int age) : base(name) {
    13.         this.Age = age;
    14.         Console.WriteLine($"子类初始化:{this.Name},{this.Age}"); // 正常(Name 已由父类初始化)
    15.     }
    16.     // 错误示例(无法编译):未显式调用 base(name)
    17.     // 父类:有隐式无参构造(未定义任何构造时,编译器自动生成)
    18.     // public class Parent {
    19.     //     protected string Name; // 父类成员
    20.     // }
    21.     // public Child(string name, int age) {
    22.     //     this.Age = age;
    23.     // }
    24.     // 编译报错:“Parent”不包含采用 0 个参数的构造函数
    25. }
    复制代码
  • 事件回调中的身份自觉,在 UI 事件(如按钮点击)中,this 始终指向当前组件/页面实例,绝不会指向触发事件的控件(如按钮本身)。
    1. // 按钮点击事件中,this 指向当前页面,而非按钮
    2. button.Click += (s, e) => Console.WriteLine(this.Title);
    复制代码
C# 的 this 之所以“乖”,核心是静态绑定——行为在编译期就确定,运行时无需额外判断。这个核心特性,恰恰是 JavaScript 早期 this 所缺乏的。
二、JavaScript  的 this:似是而非

早期 JavaScript 没有类的概念,通过 “构造函数+原型链” 模拟面向对象,this 因“动态绑定”特性,从强类型语言的角度看,其表现堪比怪物,this 指向完全依赖调用方式,稍不注意就出错。
  1. // 早期模拟类的方式,this 容易失控
  2. function Person(name) {
  3.     this.name = name; // 构造函数中 this 指向实例(需用 new 调用)
  4. }
  5. Person.prototype.sayHi = function() {
  6.     console.log(`Hi, ${this.name}`); // 原型方法中 this 依赖调用者
  7. };
  8. const person = new Person("张三");
  9. person.sayHi(); // 正常(this 指向 person)
  10. const say = person.sayHi;
  11. say(); // 报错(this 指向全局)
复制代码
2015 年 ES6 引入 class、extends 等特性,明显吸收了 C#、Java 等强类型语言的设计思想,让 JavaScript 的面向对象更贴近开发者直觉。
  1. // 类似 C# 的类定义,结构更清晰
  2. class Person {
  3.     constructor(name) {
  4.         this.name = name; // 构造函数中 this 指向实例
  5.     }
  6.     sayHi() {
  7.         console.log(`Hi, ${this.name}`); // 类方法中的 this
  8.     }
  9. }
复制代码
这种借鉴并非复制粘贴,JavaScript 仍保留动态语言特性,但 class 语法,降低了理解成本。
class 本质是“语法糖”,底层仍基于原型链(prototype),只是包装后更像 C# 的类:

  • 实例初始化 。constructor 对应 C# 的构造函数,new 调用时,this 指向新创建的实例,用于初始化实例属性(this.xxx)。
    1. class Person {
    2.     constructor(name) {
    3.         this.name = name; // name 是实例属性(每个实例单独拥有)
    4.     }
    5. }
    复制代码
  • 类方法默认挂载在原型上。类中定义的方法(如 sayHi)会被挂载到类的原型(Person.prototype)上,所有实例共享该方法。这和 C# 中”方法在类中,实例共享方法定义“的逻辑一致,但底层实现不同,C# 基于类,JavaScript 基于原型链。
    1. const p1 = new Person("张三");
    2. const p2 = new Person("李四");
    3. p1.sayHi === p2.sayHi; // true(共享原型上的方法)
    复制代码
  • static 方法挂载在类本身,而非原型,this 指向类本身。C# 静态方法中无 this,但逻辑类似:不依赖实例。
    1. class Person {
    2.     static createDefault() {
    3.         return new Person("默认名称"); // this 指向 Person 类
    4.     }
    5. }
    6. const defaultPerson = Person.createDefault();
    复制代码
  • 基于原型链封装的 extends 继承,class Child extends Parent 本质是让 Child.prototype.__proto__ 指向 Parent.prototype,但语法上模拟了 C# 的继承。super 对应 C# 的 base,用于调用父类的构造函数或方法。
三、让 JavaScript 的 this 怪物变成乖孩子

JavaScript 的 this 像怪物,核心是指向由函数调用时的方式决定,属于动态绑定,而非定义时的静态绑定。 动态绑定规则决定 this 指向:
绑定类型调用方式示例this 指向与 C# 的对比默认绑定fn()全局对象(非严格模式)/undefined(严格模式)无对应(C# 无全局 this)隐式绑定obj.fn()调用方法的对象 obj类似 C# 实例调用方法(this 指向实例)显式绑定fn.call(obj)/fn.apply(obj)/fn.bind(obj)被指定的对象 obj无对应(C# this 不可改)new 绑定new Fn()新创建的实例对象类似 C# new 实例化(this 指向实例)示例:
  1. // 同个函数,不同调用方式,this 指向不同**  
  2. function showThis() {
  3.     console.log(this);
  4. }
  5. const obj = { name: "测试对象", showThis };
  6. showThis(); // 默认绑定 → 全局对象
  7. obj.showThis(); // 隐式绑定 → obj
  8. showThis.call({ custom: "自定义对象" }); // 显式绑定 → 自定义对象
  9. new showThis(); // new 绑定 → 新实例
复制代码
但 JavaScript 提供了显式绑定工具,call / apply / bind ,可以手动控制this 指向,让它像 C# 的 this 那样驯服。
方法作用调用时机适用场景call强制 this 指向第一个参数,立即执行函数立即执行明确参数数量时调用函数apply强制 this 指向第一个参数,立即执行函数立即执行参数以数组形式存在时bind强制 this 指向第一个参数,返回新函数(延迟执行)延迟执行固定事件回调、方法提取后调用1)bind 优先级最高,绑定后不可修改,类似 C# 中 this 的不可变性:
  1. function sayHi() {
  2.     console.log(`Hi, ${this.name}`);
  3. }
  4. const person = { name: "张三" };
  5. const boundSayHi = sayHi.bind(person); // 绑定 this 到 person
  6. boundSayHi(); // 输出 "Hi, 张三"
  7. boundSayHi.call({ name: "李四" }); // 仍输出 "Hi, 张三"(bind 不可覆盖)
复制代码
2)箭头函数,天生继承外层 this,规避动态陷阱
ES6 箭头函数没有自己的 this,其 this 继承自外层作用域(定义时的上下文),行为类似 C# 匿名方法捕获当前 this 的特性。这是让 this 变“乖”的更简洁方式。对比普通函数与箭头函数:
  1. class Timer {
  2.     constructor() {
  3.         this.seconds = 0;
  4.         // 普通函数:this 指向调用者(setTimeout 的全局环境)
  5.         setInterval(function() {
  6.             this.seconds++; // 错误:this.seconds 未定义
  7.         }, 1000);
  8.         // 箭头函数:this 继承自 constructor(Timer 实例)
  9.         setInterval(() => {
  10.             this.seconds++; // 正确:this 指向 Timer 实例
  11.         }, 1000);
  12.     }
  13. }
复制代码
适用场景:事件回调、定时器、嵌套函数中,需要保留外层 this 时优先使用。
3) 内存泄漏:this 引发的暗坑
C# 有自动垃圾回收机制,但 JavaScript 中若 this 关联的事件回调未正确解绑,会导致对象无法被回收,引发内存泄漏。
错误示例:动态生成的函数无法解绑
  1. class Component {
  2.     constructor() {
  3.         this.name = "组件";
  4.         // 错误:每次 bind 生成新函数,后续无法解绑
  5.         document.querySelector('button').addEventListener('click', this.handleClick.bind(this));
  6.     }
  7.     handleClick() { console.log(this.name); }
  8.     destroy() {
  9.         // 失败:解绑的函数与绑定的不是同一个引用
  10.         document.querySelector('button').removeEventListener('click', this.handleClick.bind(this));
  11.     }
  12. }
复制代码
正确做法:保存绑定后的函数引用
  1. class Component {
  2.     constructor() {
  3.         this.name = "组件";
  4.         // 提前绑定并保存引用
  5.         this.boundHandleClick = this.handleClick.bind(this);
  6.         document.querySelector('button').addEventListener('click', this.boundHandleClick);
  7.     }
  8.     handleClick() { console.log(this.name); }
  9.     destroy() {
  10.         // 用同一引用解绑
  11.         document.querySelector('button').removeEventListener('click', this.boundHandleClick);
  12.         this.boundHandleClick = null; // 释放引用
  13.     }
  14. }
复制代码
四、HTML 5 自定义 UI 组件中 this 规范化

在 HTML5 开发自定义 UI 组件(如按钮、表单控件)时,this 的坑会集中爆发。结合类与继承,我们可以用规范化技术解决。
场景 1:自定义按钮组件(基础类)

问题:事件回调中 this 指向 DOM 元素(而非组件实例)。
  1. class CustomButton {
  2.     constructor(label) {
  3.         this.label = label; // 组件属性
  4.         this.btn = document.createElement('button');
  5.         this.btn.textContent = label;
  6.         
  7.         // 坑:点击时 this 指向 btn(DOM 元素)
  8.         this.btn.addEventListener('click', this.onClick);
  9.         
  10.         document.body.appendChild(this.btn);
  11.     }
  12.     onClick() {
  13.         console.log(`点击了 ${this.label}`); // 报错:this.label 不存在
  14.     }
  15. }
复制代码
解决:用 bind 或箭头函数固定 this 指向组件实例。
  1. class CustomButton {
  2.     constructor(label) {
  3.         this.label = label;
  4.         this.btn = document.createElement('button');
  5.         this.btn.textContent = label;
  6.         
  7.         // 方案 1:bind 绑定
  8.         this.btn.addEventListener('click', this.onClick.bind(this));
  9.         
  10.         // 方案 2:箭头函数回调(更简洁)
  11.         // this.btn.addEventListener('click', () => this.onClick());
  12.         
  13.         document.body.appendChild(this.btn);
  14.     }
  15.     onClick() {
  16.         console.log(`点击了 ${this.label}`); // 正确:this 指向组件实例
  17.     }
  18. }
复制代码
场景 2:带图标的按钮(子类继承)

问题:子类构造函数未调用 super() 就使用 this,直接报错。
  1. class IconButton extends CustomButton {
  2.     constructor(label, icon) {
  3.         this.icon = icon; // 报错:必须先调用 super()
  4.         super(label);
  5.     }
  6. }
复制代码
解决:严格遵循“先 super() 后 this”,对齐 C# 的 base() 逻辑。
  1. class IconButton extends CustomButton {
  2.     constructor(label, icon) {
  3.         super(label); // 先调用父类构造
  4.         this.icon = icon; // 再初始化子类属性
  5.         this.btn.innerHTML = `<i>${icon}</i> ${label}`; // 扩展父类 DOM
  6.     }
  7.     // 重写父类方法
  8.     onClick() {
  9.         console.log(`点击了带 ${this.icon} 图标的 ${this.label}`);
  10.     }
  11. }
复制代码
场景 3:组件移除/销毁与资源清理

问题:事件未解绑导致内存泄漏。
解决:提供 destroy 方法,手动解绑事件并释放引用。
  1. class CustomButton {
  2.     // ... 其他代码 ...
  3.     destroy() {
  4.         // 解绑事件(用绑定时期的引用)
  5.         this.btn.removeEventListener('click', this.boundOnClick || this.onClick);
  6.         this.btn.remove(); // 移除 DOM 元素
  7.         // 释放属性引用
  8.         this.btn = null;
  9.         this.label = null;
  10.     }
  11. }
复制代码
五、几点心得

如果您像我一样,熟悉 C# 或者 Java 这样的强类型语言, 只是偶尔使用 JavaScript  配置前端,我的建议,与其花时间通透掌握 JavaScript  本身,不如改造它适合自己的思维习惯。用强类型思维顺服 this,更顺手。具体来说:

  • 直接用 ES+,忽略传统 JavaScript  语法,尽管语言本身是向后兼容的。
  • 用 class 对齐结构,借助 class 和 extends,让 JavaScript  类的写法贴近 C# ,降低认知成本;
  • 用 bind 或箭头函数固定 this,抵消动态性,模拟 C# 中“方法与实例强绑定”的特性;
  • 子类构造函数先 super() 后 this,对齐 C# 的 base() 调用逻辑;
  • 主动将事件回调的 this 指向组件实例,避免指向 DOM 元素;
  • 显式保留 connectedCallback() /  disconnectedCallback() 方法,只要可能,统一在 connectedCallback 中注册事件,在 disconnectedCallback 移除事件。
  • 确保组件移除/销毁时解绑事件,释放 this 关联的引用,类似 C# 的 Dispose。
做到这些,JavaScript 的 this 就会像邻居家强类型语言的 this 一样,成为可靠、可控的乖孩子。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册