ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

JavaScript class从入门到精通:原型链、继承与私有字段实战

JavaScript class从入门到精通:原型链、继承与私有字段实战 JavaScript里的class很多新手第一次接触时都会觉得既熟悉又陌生。熟悉是因为它在Java、Python、C#这些语言里都有看起来就是“模板”“蓝图”那一套陌生是因为JavaScript的class底子里藏着的还是原型链和传统面向对象语言不是一个东西。这篇内容不打算讲太多理论我会用实际代码和踩坑经历把class的语法、继承、静态成员、私有字段这些核心点捋一遍让刚入门的朋友读完能直接上手也让写过一段时间但总觉得没吃透的人把最后那层窗户纸捅破。1. class到底是什么语法糖还是真面向对象1.1 从原型链说起在ES6之前JavaScript没有class关键字想要实现“类”的效果靠的是构造函数加prototype。很多老代码长这样function Person(name, age) { this.name name; this.age age; } Person.prototype.sayHello function () { console.log(你好我是 this.name); }; var p1 new Person(小明, 18); p1.sayHello();这段代码的核心逻辑是Person是一个普通函数但当用new调用它时它会创建一个新对象并把this绑定到这个新对象上然后执行函数体把属性挂上去。方法sayHello没有直接挂在this上而是放在Person.prototype上这样所有实例都可以通过原型链访问到它避免每个实例都复制一份函数节省内存。class的底层机制和上面完全一样只是换了一层更清晰、更接近传统面向对象语言的皮class Person { constructor(name, age) { this.name name; this.age age; } sayHello() { console.log(你好我是 this.name); } } const p1 new Person(小明, 18); p1.sayHello();这两段代码做的事情几乎一模一样。所以第一个要记住的结论是JavaScript的class不是全新的面向对象模型它是对原型继承的语法封装。理解这一点很多后面遇到的问题都不会觉得诡异。1.2 class声明和函数声明的三个关键区别既然底层都是原型链那class和构造函数是不是可以随便替换不是有几个实实在在的差异用错了会踩坑。第一class声明不会被提升hoisting。普通函数声明可以在定义之前调用因为函数声明会被整体提升到作用域顶部// 这样是可以的 sayHi(); function sayHi() { console.log(hi); } // 这样会报错Cannot access Person before initialization const p new Person(); class Person {}第二class体内的代码自动处于严格模式。不用写use strict里面的代码本身就是严格模式比如不小心给未声明的变量赋值会直接抛错。第三class方法是不可枚举的。遍历Object.keys(Person.prototype)得到的会是空数组而普通构造函数挂到prototype上的方法是可枚举的。这个差异在写工具库、做属性遍历时要注意。1.3 为什么推荐新手直接学class而不是先啃原型链很多教程会说“不懂原型链就学不会class”这话对也不对。我个人的建议是先通过class把对象创建和继承的流程跑通回头再补原型链的底层细节。原因很简单class的语法把constructor、super、extends这些概念具象化了写起来不容易出错。比如你定义一个子类忘了在constructor里调super()浏览器会直接报错提醒你而用纯原型链手写继承一旦步骤错了找问题能找一晚上。说到底class是JavaScript给开发者铺好的那条“走得通的路”先沿着路走再研究路下面的地基学习曲线会平缓很多。2. 构造函数、实例属性和方法新手最容易忽略的细节2.1 constructor里到底该放什么constructor是类里的一个特殊方法用new创建实例时会自动调用它。它通常用来初始化实例属性比如给每个实例挂上自己独有的数据class User { constructor(username, email) { this.username username; this.email email; this.createdAt new Date(); } } const u1 new User(阿伟, aweiexample.com); const u2 new User(小丽, xiaoliexample.com); console.log(u1.createdAt); // 每个实例都有自己独立的时间戳这里有个容易被忽略的点如果类里没有写constructorJavaScript会自动补一个空的constructor() {}。如果写了constructor但不调用super()在继承场景下程序运行到一半就会报错。新手最常见的问题就是子类里写了constructor却忘了super。还有一点constructor里用return要非常谨慎。如果return了一个对象那new出来的结果就不是当前实例而是你返回的那个对象class Foo { constructor() { return { custom: true }; } } const f new Foo(); console.log(f instanceof Foo); // false console.log(f.custom); // true基本类型数字、字符串、布尔等的return会被忽略还是返回当前实例但对象会被直接替换。实际项目里几乎没人会这么写但面试里不少见知道这个机制能少很多疑惑。2.2 实例属性写在constructor里还是类体里ES2022之后JavaScript支持在类的顶层直接声明实例字段不需要通过this赋值class Counter { count 0; step 1; increment() { this.count this.step; } } const c new Counter(); c.increment(); console.log(c.count); // 1这种写法的好处是一眼就能看到这个类有哪些实例属性不用在constructor里翻来翻去找。它和执行this.count 0是等价的而且无论constructor里做了什么字段初始化都会在constructor执行之前完成。如果字段初始化和constructor里重复赋值后者会覆盖前者。我个人在项目里的习惯是只要属性不依赖constructor的参数就写在类体顶部依赖参数的才放到constructor里。这样可读性好很多团队里其他人看代码时能快速了解数据结构。2.3 方法区分实例方法、静态方法、私有方法类里的方法默认都挂在prototype上所以每个实例通过原型链共享同一个方法不会各自复制一份。这里要区分三种方法实例方法通过实例调用如user.getInfo()。静态方法通过类直接调用如User.create()用static关键字声明内部拿不到实例的this。私有方法通过#前缀声明只能在类内部访问外部调用会报SyntaxError。实际场景里静态方法适合放一些和实例无关的工具函数或“工厂函数”。比如class ApiClient { constructor(baseURL) { this.baseURL baseURL; } static createDefault() { return new ApiClient(https://api.example.com); } static isApiClient(obj) { return obj instanceof ApiClient; } } const client ApiClient.createDefault(); console.log(ApiClient.isApiClient(client)); // true静态方法里的this指向的是类本身不是实例。所以静态方法之间可以通过this互相调用但静态方法里访问不到实例属性因为压根没有实例。3. getter、setter和静态块把属性玩出花样3.1 getter和setter的典型用法get和set关键字允许你给类定义“访问器属性”。从外部看它用起来和普通属性一样没有括号但实际上背后是函数。最典型的场景是对数据的校验和派生计算class Product { constructor(name, price) { this.name name; this._price price; } get price() { return this._price; } set price(value) { if (value 0) { throw new Error(价格不能为负数); } this._price value; } get formattedPrice() { return ¥ this._price.toFixed(2); } } const p new Product(键盘, 199); console.log(p.formattedPrice); // ¥199.00 p.price 99; console.log(p.price); // 99注意这里我用了_price来存真实值目的是避免和price访问器冲突。用下划线前缀表示“这是内部私有字段”是老派约定现在有了真正的#私有字段新代码可以直接用#price来存更安全。对比纯方法getPrice()和setPrice()getter/setter的好处是调用方写起来更自然product.price 99而不是product.setPrice(99)。但如果逻辑复杂、副作用多我还是建议用显式方法因为getter/setter里做太多事情会让代码难调试。3.2 静态块类里也能写初始化代码ES2022还引入了静态初始化块static initialization block它会在类第一次加载时执行适合做一些无法用一行静态字段表达式完成的初始化class Database { static connection; static retryTimes 3; static { try { this.connection createConnection(); } catch (e) { console.warn(初始化失败稍后重试); this.connection null; } } }静态块的主要价值是让“类级别的初始化逻辑”留在类内部而不是散落在类外面。比如配置信息从环境变量读取、或需要异常处理时静态块比静态字段表达式灵活得多。3.3 访问器与Proxy对比什么时候不该用gettergetter/setter适合在单个属性上做控制。如果你需要拦截对象上所有的属性读写比如做数据校验、日志记录和权限控制那应该考虑Proxy而不是在类里写一堆getter/setter。Proxy可以在对象层面对get、set、has、deleteProperty等操作做统一拦截不用逐个属性写访问器。不过Proxy的兼容性细节和性能开销都要注意而且它不会触发类实例的私有字段保护。我的建议是项目里如果只是少数几个属性需要处理用getter/setter就够了引入Proxy反而增加复杂度。4. extends和super继承体系的完整实践4.1 子类构造的黄金法则先super再thisextends让一个类可以继承另一个类的属性和方法。子类里如果写了constructor第一行必须是super()否则拿不到this。原因是ES6的class继承机制和普通构造函数不同普通构造函数是自己创建this而子类的this是由父类创建好的子类只能先调用super()拿到这个对象再在上面补充自己的属性。class Animal { constructor(name) { this.name name; } speak() { console.log(this.name 发出声音); } } class Dog extends Animal { constructor(name, breed) { super(name); this.breed breed; } speak() { console.log(this.name 汪汪叫); } } const d new Dog(旺财, 金毛); d.speak(); // 旺财汪汪叫如果Dog不写constructor它会自动调用super(...arguments)把父类参数原样传过去。所以在子类里能不写constructor就不写写的话一定记得先调super()。4.2 方法重写和super的多层传递子类可以重写父类方法然后在子类方法里通过super.method()调用父类的同名方法。这个机制特别适合“在父类基础上扩展”的场景class Order { constructor(items) { this.items items; } getTotal() { return this.items.reduce((sum, item) sum item.price, 0); } } class DiscountOrder extends Order { constructor(items, discount) { super(items); this.discount discount; } getTotal() { const baseTotal super.getTotal(); return baseTotal * (1 - this.discount); } } const order new DiscountOrder([{ price: 100 }, { price: 200 }], 0.2); console.log(order.getTotal()); // 240这里super.getTotal()拿到的是父类Order.prototype上的方法而且它内部的this仍然是当前实例所以能访问到子类添加的items。理解super的this指向很重要它不是父类实例而是指向当前实例。因为JavaScript里只有对象没有真正的“类实例”和“父类实例”分离的概念。4.3 instanceof和原型链的坑instanceof用于检测一个对象是否属于某个类或该类的子类。它检测的是原型链上是否有对应的原型对象console.log(d instanceof Dog); // true console.log(d instanceof Animal); // true console.log(d instanceof Object); // true这个机制在写业务代码时很好用但它有一个“跨执行环境”的坑如果在多个iframe或不同Node.js模块副本之间传递对象每个环境有自己的全局对象和内置类instanceof可能会失效。更稳健的检测方式是使用Object.prototype.toString.call(obj)或Symbol.toStringTag不过这些对class来说不是必选项知道有坑即可。4.4 继承多层和组合优先多层继承A继承BB继承C能写但层级越多越难维护。一个常见的坏味道是为了复用两三个方法硬生生造出一个“基类”让所有类都去继承它。实际情况中很多“继承”可以用“组合”来替代让一个类持有另一个类的实例把功能委托出去。class Engine { start() { console.log(引擎启动); } } class Car { constructor() { this.engine new Engine(); } start() { this.engine.start(); console.log(汽车出发); } }这种方式灵活得多不会因为父类改动影响一大片子类。我自己的原则是只在“is-a”关系明确时用继承在“has-a”关系时用组合。比如“小狗是一只动物”可以用继承“汽车有一个引擎”就该用组合。5. 私有字段、静态私有字段和类型保护的现代写法5.1 真正意义上的私有#前缀JavaScript在ES2022正式支持了私有字段用#声明这才是语言层面真正不可从外部访问的属性不像以前用_下划线约定私有实际上随便访问class BankAccount { #balance 0; constructor(initialBalance) { this.#balance initialBalance; } deposit(amount) { if (amount 0) { throw new Error(存款金额必须大于0); } this.#balance amount; } getBalance() { return this.#balance; } } const account new BankAccount(1000); account.deposit(500); console.log(account.getBalance()); // 1500 // console.log(account.#balance); // 这里会直接报语法错误私有字段的好处是真正的封装外部代码无论如何都无法直接读取或修改#balance只能通过类内方法操作数据。这比闭包实现私有变量更直观也比TypeScript的private关键字更严格——TypeScript的private只在编译期检查运行时照样能访问。需要特别注意的是私有字段的#不是装饰器语法它就是字段名的一部分。所以同名私有字段在不同类里互不干扰这也让私有字段的内存布局在V8里更紧凑理论上性能还更好。5.2 私有方法和私有静态方法#可以修饰方法也可以修饰静态方法。私有方法适合放类的内部辅助逻辑避免外部调用class StringBuilder { #chunks []; append(text) { this.#chunks.push(text); return this; } toString() { return this.#join(); } #join() { return this.#chunks.join(); } }这里#join()只能被类内部调用外部拿不到也没法直接调用。如果有人通过Object.getOwnPropertyNames()或反射去查也看不到私有字段/方法因为私有字段根本不参与常规的属性查找和枚举。5.3 检测私有字段存在in运算符的妙用#字段不能用key in obj去检测但可以写一个能访问该私有字段的方法来判断。还有一种常见技巧是利用in运算符对私有字段的语法支持class Foo { #bar 1; static hasBar(obj) { return #bar in obj; } } const f new Foo(); console.log(Foo.hasBar(f)); // true这在写库、做类型守卫时非常有用。你可以在不暴露私有字段值的情况下告知外部“这个对象是不是当前类的合法实例”。6. class和原型链的关系浓缩一张图和几句大白话6.1 class是prototype的语法糖但不完全是虽然前面说class是语法糖但ES6之后class和普通构造函数之间其实有细微差别class方法不可枚举、class声明不提升、class内部自动严格模式。所以更准确地说class是一套比裸写prototype更安全、更规范的对象创建和继承语法底层依然依赖原型链。看这个例子class Parent {} class Child extends Parent {} const c new Child(); // c - Child.prototype - Parent.prototype - Object.prototype - null console.log(c.__proto__ Child.prototype); // true console.log(Child.prototype.__proto__ Parent.prototype); // truec.__proto__指向Child.prototypeChild.prototype.__proto__指向Parent.prototype这就是原型链的链条。extends做的事情本质上就是设置这个链条关系而instanceof也是顺着这条链去查的。6.2 不影响prototype的继承方式Object.create和setPrototypeOf如果你不想用class只想改变两个对象之间的原型关系可以用Object.create()或Object.setPrototypeOf()const animal { speak() { console.log(this.name 发出声音); }, }; const dog Object.create(animal); dog.name 旺财; dog.speak(); // 旺财发出声音Object.create(animal)会创建一个新对象这个新对象的__proto__指向animal。Object.setPrototypeOf则可以在已有对象上改原型但它会触发V8引擎的优化降级性能较差能不动态改原型就尽量别改。6.3 原生构造器的继承Array、Error、Promise都可以extends用class可以继承JavaScript内置类这是原型链设计的一大好处class MyArray extends Array { first() { return this[0]; } last() { return this[this.length - 1]; } } const arr new MyArray(3, 1, 4, 1, 5); console.log(arr.first()); // 3 console.log(arr.last()); // 5 console.log(arr.length); // 5但这里有个著名的坑MyArray的map、filter等方法返回的是MyArray实例还是普通Array实例规范里规定Symbol.species这个静态getter可以控制这一点。默认情况下MyArray的map返回MyArray实例如果你想让它返回普通数组可以覆写class MyArray extends Array { static get [Symbol.species]() { return Array; } } const arr new MyArray(1, 2, 3); const mapped arr.map(x x * 2); console.log(mapped instanceof MyArray); // false console.log(mapped instanceof Array); // true这在写数据集合类时很关键如果不熟悉Symbol.species很容易在链式调用时出现“返回值类型和预期不一样”的诡异问题。7. 新手常见报错和排查思路7.1 Must call super constructor in derived class before accessing this这是新手在写继承时最常见的错误。原因就是子类的constructor里在调用super()之前使用了thisclass Parent {} class Child extends Parent { constructor() { this.name child; // 报错要先 super() super(); } }修复很简单把super()放到第一行。这个报错不是随随便便的约束而是由JS继承机制决定的子类没有自己的this必须由父类构造出来。7.2 Class constructor cannot be invoked without new这句话意味着你把class当成普通函数调用了class Foo {} // 报错 Foo();和构造函数不一样class不能被直接调用只能配合new或Reflect.construct()。调试时如果看到这个错先检查是不是忘了写new。7.3 Cannot access private field from outside the class这个错误说明你试图在类外部访问#私有字段。#字段是真正私有的不像_那样只是约定。要检查你的方法是否是实例方法、是否在class内部定义以及有没有把方法解构出来到外部调用class Demo { #secret 1; getSecret() { return this.#secret; } } const d new Demo(); const { getSecret } d; // 下面这行会报错this 丢失了 // getSecret();上面这个例子里的报错很典型方法内部的this取决于怎么调用解构后getSecret()的this不再是d自然访问不到#secret。7.4 判断this丢失问题的速查思路class方法里的this不是自动绑定的。所有实例方法在执行时this都取决于“调用者”。一旦把方法当作回调传给别人或者解构出来单独调用this就可能变成undefined严格模式或全局对象非严格模式。解决办法有几种绑定this.method this.method.bind(this)放在constructor里。箭头函数字段myMethod () {}让方法箭头化并固定this。调用时绑定onClick{() this.myMethod()}。每种方案都有取舍箭头函数字段牺牲了原型共享每个实例都会创建一份新函数在大量创建实例时会增加内存开销bind也有类似问题。所以如果只是几个事件回调无所谓如果是高频实例化的场景尽量在调用时绑定。8. class相关的实战建议和我的习惯用法8.1 什么时候该用class什么时候别用JavaScript里的class不是银弹。对新手来说搞清楚“什么场景适合class”比学会语法更重要。适合用class的场景需要创建多个结构一致的对象、有明确的继承层次、状态和行为紧密绑定在一起比如一个订单、一个购物车、一个用户会话。这时class让代码组织更清晰。不适合用class的场景只有一个简单对象直接字面量足够函数式工具集合直接导出函数即可没有状态、只有逻辑的模块不如用对象或模块封装。// 不需要class直接导出函数更简洁 function formatDate(date) { return date.toISOString().split(T)[0]; } function parseDate(str) { return new Date(str); }很多时候我在公司Code Review里看到新人硬把一对函数包装成class其实完全没有必要。class的价值是“状态行为”的封装没有状态时封装就成了负担。8.2 团队协作时的class书写规范我们前端团队现在对class的使用有一些约定分享出来供参考类名使用大驼峰PascalCase方法名和属性名使用小驼峰camelCase。constructor里只做属性初始化不写复杂逻辑。尽量用类体字段声明实例属性不要全堆在constructor里。私有字段统一用#不依赖_下划线约定。一个文件尽量只导出一个类除非有强相关的辅助类或常量。方法太长就拆分一个方法只做一件事。只有明确的is-a关系才用extends否则用组合或mixin。8.3 从零构建一个实用小类完整过程演示以一个简单的“事件发射器”为例完整走一遍class设计到实现的过程。这个类的作用是支持on、off、emit三个方法用来在对象间解耦通信class EventEmitter { #listeners new Map(); on(eventName, handler) { if (!this.#listeners.has(eventName)) { this.#listeners.set(eventName, []); } this.#listeners.get(eventName).push(handler); return () this.off(eventName, handler); } off(eventName, handler) { const handlers this.#listeners.get(eventName); if (!handlers) return; const index handlers.indexOf(handler); if (index ! -1) { handlers.splice(index, 1); } } emit(eventName, payload) { const handlers this.#listeners.get(eventName); if (!handlers) return; // 拷贝一份避免回调里改动原数组导致问题 [...handlers].forEach(handler { try { handler(payload); } catch (e) { console.error(事件处理函数异常, e); } }); } }这个类用到了私有字段#listeners、实例方法、返回值作为取消订阅函数、异常隔离等技巧代码量不大但覆盖面很广。新手可以试着从零默写一遍再想想怎么扩展成支持“只触发一次”的once方法。8.4 测试class代码先测行为再测内部结构class的单元测试里我建议优先测试公开行为和外部契约不要直接去测私有字段。私有字段可以变化公开接口才是稳定的。一个走心的测试用例会这样写const assert require(assert); const emitter new EventEmitter(); let received null; const unsubscribe emitter.on(data, value { received value; }); emitter.emit(data, 42); assert.strictEqual(received, 42); unsubscribe(); emitter.emit(data, 99); assert.strictEqual(received, 42);注意测试的是“订阅后能收到事件”“退订后不再收到事件”而不是去Contructor里看#listeners怎么存、存了多少。这样可以保证类的内部重构不影响测试结果。9. 结合ES2022以后的新特性class还能更好用9.1 自动访问器accessor关键字ES2022之后还有一个比较新的提案目前处于stage 3叫自动访问器auto-accessor。它可以让你把字段和访问器结合起来快速定义class Person { accessor name default; } const p new Person(); console.log(p.name); // default p.name Alice; console.log(p.name); // Alice自动访问器会自动创建getter/setter并且在后端框架、响应式库中很有用。不过目前浏览器和Node.js的支持还不是特别统一生产环境使用前建议用Babel或TypeScript转译否则可能遇到兼容性问题。9.2 装饰器class的AOP扩展装饰器Decorator一直是JavaScript社区的热门特性如今已经进入了stage 3。它可以用来给类、方法、访问器添加额外行为在日志、缓存、权限校验等场景下非常实用function logged(value, context) { if (context.kind method) { return function (...args) { console.log(调用 context.name, args); return value.call(this, ...args); }; } } class Calculator { logged add(a, b) { return a b; } } const calc new Calculator(); calc.add(2, 3); // 打印日志这里的装饰器语法在新版本Babel/TypeScript中已经能用了但要确认构建工具配置正确。装饰器容易让代码变得隐式团队里如果没人熟悉少用为宜。9.3 class和TypeScript的配合TypeScript是JavaScript的超集它里面也有class而且有很多JavaScript本身没有的语法特性比如private、protected、readonly、implements、abstract等。如果你在项目里用TypeScriptclass的体验会提升一个台阶abstract class Shape { abstract getArea(): number; describe() { return 面积是 this.getArea(); } } class Circle extends Shape { constructor(private radius: number) { super(); } getArea(): number { return Math.PI * this.radius ** 2; } }这里private是TypeScript编译期的访问限制#是JavaScript运行时的真正私有。如果两者都想要可以结合使用对外用public/private约束编译期对内用#保护运行时数据。10. 前面几轮的坑我都替你们踩过了10.1 这是我在生产环境里遇到过的一个真实案例this指向问题有一个老项目里一个类的方法经常被当作事件回调传递出去结果this指向了按钮DOM元素代码里全是错误。当时的代码长这样class Logger { logClick() { console.log(this); // 期望是Logger实例实际是button元素 } } const logger new Logger(); document.querySelector(#btn).addEventListener(click, logger.logClick);修复方式是把logClick改成箭头函数字段或者在constructor里绑定class Logger { logClick () { console.log(this); }; }这个案例的教训是只要类方法可能被当作独立函数传递就提前想好this绑定问题。现在我在写类方法时会主动区分“内部方法”和“回调方法”回调方法直接用箭头函数字段普通方法保持常规写法。10.2 另一个坑子类里调用父类方法时super指向混乱如果一个子类继承自父类父类内部调用了某个方法而子类重写了这个方法那父类里调用的会是子类重写后的版本。这就是“多态”的正常表现但新手往往不理解class Base { init() { this.setup(); } setup() { console.log(base setup); } } class Derived extends Base { setup() { console.log(derived setup); } } const d new Derived(); d.init(); // 输出derived setup这不是bug是面向对象里非常经典的多态。理解了这一点你就不容易在“为什么父类调用的是子类方法”上卡住。10.3 再说一个容易被忽略的class表达式除了常见的class声明JavaScript还支持把class当作表达式使用和函数表达式类似const Animal class { constructor(name) { this.name name; } }; const a new Animal(猫); console.log(a.name); // 猫class表达式也可以命名命名只在class内部可见外部拿不到。这种写法适合动态创建类、或者在一个表达式中迅速生成一个类并立刻使用。10.4 如何判断要不要把class拆成独立模块当class越来越大代码已经超过两三百行且修正一个需求的成本明显增加时就该考虑拆分。但拆分不是简单地把方法散到不同文件更合理的做法是“一个类只负责一个职责”。比如一个Order类如果既管订单金额计算、又管订单状态流转、还管数据库存储那该拆的其实是“存储”“状态”这些职责把它们变成独立的对象让Order只关注自己的核心业务。我自己在写class时有一个简单判断标准如果我不看注释只看类的属性和方法名就能脑补出这个类到底是干什么的说明结构是清晰的。否则就该重构了。11. 最后一小段一个老前端对class的真实态度这几年我在好几个项目里都用class自己也踩过不少坑最大的感受是class只是工具不是信仰。它让JavaScript的对象体系更规整尤其适合团队协作、框架开发和需要明确状态模型的模块。但遇到简单的数据集合或纯函数逻辑时硬套class反而别扭。如果你刚接触JavaScript不久我建议把class相关的每一个语法点都动手写一遍再翻一翻ES5时代用prototype模拟继承的旧代码两相对照你会对JavaScript的对象模型有一个更通透的理解。知道class怎么用只是第一步知道class为什么这样设计才算是真的入门了。等你在项目里实际遇到“this丢失”“super报错”“私有字段访问不了”这些问题时再回来看这篇内容会更有感觉——因为所有犹豫和报错都是和这门语言真正建立连接的过程。
返回列表