class添加断点无效_ES6的Class中那些高手都知道的秘密

前言

本文是关于ES6的Class类相关知识的翻译文章,对于很多ES6的Class相关内容分析还是很独到的。知识点包括但不局限于继承,组合,多继承的实现等等。通过本文的阅读,可以很有信心的告诉你,对于ES6的类Class的理解肯定会有不一样的收获。同时,作者长期坚持更新高质量技术文章,欢迎关注~

1、ES6中类Class简介

通常我们需要在程序中表示一个想法或概念,可能是汽车引擎,计算机文件,路由器或温度读数。 直接在代码中表示这些概念分为两部分:表示状态的数据和表示行为的函数。 ES6类为我们提供了一种方便的语法,用于定义代表我们概念的对象的状态和行为。

ES6类通过保证调用初始化函数,从而使代码更安全,并且可以更容易地定义一组固定的函数,这些函数对该数据进行操作并保持有效状态。 如果您可以将某个事物视为一个单独的实体,那么您可能应该定义一个类来表示程序中的“事物”。考虑这个非类代码。 你能找到多少错误? 你会如何修复它们?

// set today to December 24const today = { month: 24, day: 12,};const tomorrow = { year: today.year, month: today.month, day: today.day + 1,};const dayAfterTomorrow = { year: tomorrow.year, month: tomorrow.month, day: tomorrow.day + 1 <= 31 ? tomorrow.day + 1 : 1,};

今天的日期无效:没有第24个月。此外,today还没有完全初始化:它缺少了一年。 如果我们有一个不能忘记的初始化函数会更好。 另请注意,在添加一天时,我们检查了一个地方,如是否超过了31。但在另一个地方错过了该检查。 如果我们只通过一组小而固定的函数来处理数据会更好,每个函数都保持有效状态。这是使用类的更正版本。

class SimpleDate { constructor(year, month, day) { // Check that (year, month, day) is a valid date // ... // If it is, use it to initialize "this" date this._year = year; this._month = month; this._day = day; } addDays(nDays) { // Increase "this" date by n days // ... } getDay() { return this._day; }}// "today" is guaranteed to be valid and fully initializedconst today = new SimpleDate(2000, 2, 28);// Manipulating data only through a fixed set of functions ensures we maintain valid statetoday.addDays(1);

2、构造函数

构造函数方法很特殊,它解决了第一个问题。 它的工作是将一个实例初始化为一个有效状态,它将被自动调用,所以不会忘记初始化对象的代码。

2.1、让数据保持私有(private)

尝试设计类,以确保它们的状态有效。 我们提供了一个只创建有效值的构造函数,设计的方法也总是只留下有效值。 但只要让每个人都可以访问类数据,就会有人搞砸。 除了通过我们提供的功能之外,可以通过保持数据不可访问来防止这种情况发生。

提示: 保存数据私有而不被任意访问也被成为封装

2.2、隐私与公约

不幸的是,JavaScript中不存在私有对象属性。 必须伪造它们。 最常见的方法是遵循一个简单的约定:如果属性名称以下划线为前缀(或者不太常见,后缀为下划线),则应将其视为非公开。 在前面的代码示例中使用了这种方法。 通常这个简单的约定是有效的,但数据在技术上仍然可供所有人访问,因此必须依靠自己的规则来做正确的事情。

2.3、隐私权与特权方法

伪造私有对象属性的下一种最常见的方法是在构造函数中使用普通变量,并在闭包中捕获它们。 这个技巧为我们提供了外部无法访问的真正私人数据。 但为了使它工作,类的方法本身需要在构造函数中定义并附加到实例:

class SimpleDate { constructor(year, month, day) { // Check that (year, month, day) is a valid date // ... // If it is, use it to initialize "this" date's ordinary variables let _year = year; let _month = month; let _day = day; // Methods defined in the constructor capture variables in a closure this.addDays = function(nDays) { // Increase "this" date by n days // ... } this.getDay = function() { return _day; } }}

2.4 、私有private与符号(Symbols)

从ES6开始,Symbol是JavaScript的一个新特性,它们提供了另一种伪造私有对象属性的方法。 比如,可以使用唯一符号对象键,而不是下划线属性名称,我们的类可以在闭包中捕获这些键。 但是依然会存在泄漏。 JavaScript的另一个新功能是Object.getOwnPropertySymbols,它允许外部访问试图保密的符号键

const SimpleDate = (function() { const _yearKey = Symbol(); const _monthKey = Symbol(); const _dayKey = Symbol(); class SimpleDate { constructor(year, month, day) { // Check that (year, month, day) is a valid date // ... // If it is, use it to initialize "this" date this[_yearKey] = year; this[_monthKey] = month; this[_dayKey] = day; } addDays(nDays) { // Increase "this" date by n days // ... } getDay() { return this[_dayKey]; } } return SimpleDate;}());

这样当通过SimpleDate类来创建实例后依然可通过如下代码访问到实例内部的Symbol属性:

Object.getOwnPropertySymbols(new SimpleDate())

2.5 Weak Maps与私有变量

Weak Map也是JavaScript的新功能。 我们可以使用实例作为键来存储键/值对中的私有对象属性,并且类可以在闭包中捕获这些键/值映射:

const SimpleDate = (function() { const _years = new WeakMap(); const _months = new WeakMap(); const _days = new WeakMap(); class SimpleDate { constructor(year, month, day) { // Check that (year, month, day) is a valid date // ... // If it is, use it to initialize "this" date _years.set(this, year); _months.set(this, month); _days.set(this, day); } addDays(nDays) { // Increase "this" date by n days // ... } getDay() { return _days.get(this); } } return SimpleDate;}());

2.6、其他访问修饰符

除了“private”之外,还有其他级别的可见性,您可以在其他语言中找到它们,例如“protected”,“internal”,“package private”或“friend”。 JavaScript仍然没有为我们提供强制执行其他级别可见性的方法。 如果你需要它们,你将不得不依赖惯例和自律。

3、引用当前对象

再看看getDay, 它没有指定任何参数,那么它如何知道调用它的对象? 当函数作为使用object.function表示法的方法调用时,会有一个隐式参数用于标识对象,并且该隐式参数被赋值给名为this的隐式参数。 为了说明,下面例子是我们如何显式而不是隐式地设置对象参数的方法:

// Get a reference to the "getDay" functionconst getDay = SimpleDate.prototype.getDay;getDay.call(today); // "this" will be "today"getDay.call(tomorrow); // "this" will be "tomorrow"tomorrow.getDay(); // same as last line, but "tomorrow" is passed implicitly

到了这里有一点需要说明,如果是箭头函数并不能通过如上方式(SimpleDate.prototype.getDay)调用,比如下面的例子:

class SimpleDate { addDays=(nDays)=> {  } getDay=()=> { return new Date() } }

这是箭头函数本身的特性,正如无法在箭头函数里面调用super.method方法一样

4、静态属性和方法

可以选择定义某些属性和方法作为类的一部分但不属于该类的任何实例。 我们分别称这些静态属性和静态方法。静态属性和方法,所有实例共享的是同一份为不存在某一个实例单独享有一份的特权

class SimpleDate { static setDefaultDate(year, month, day) { // A static property can be referred to without mentioning an instance // Instead, it's defined on the class SimpleDate._defaultDate = new SimpleDate(year, month, day); } constructor(year, month, day) { // If constructing without arguments, // then initialize "this" date by copying the static default date if (arguments.length === 0) { this._year = SimpleDate._defaultDate._year; this._month = SimpleDate._defaultDate._month; this._day = SimpleDate._defaultDate._day; return; } // Check that (year, month, day) is a valid date // ... // If it is, use it to initialize "this" date this._year = year; this._month = month; this._day = day; } addDays(nDays) { // Increase "this" date by n days // ... } getDay() { return this._day; }}SimpleDate.setDefaultDate(1970, 1, 1);const defaultDate = new SimpleDate();

5、子类

我们经常发现类之间的共性,于是想要整合重复的代码。 子类让我们将另一个类的状态和行为合并到我们自己的类中。 这个过程通常称为继承,子类被称为从父类“继承”,其中父类也称为超类。 继承可以避免重复,并简化需要与另一个类持有相同的数据和函数的类的实现。 继承还允许我们替换子类,仅依赖于公共超类提供的接口。

5.1、继承以避免重复

下面是非继承的代码:

class Employee { constructor(firstName, familyName) { this._firstName = firstName; this._familyName = familyName; } getFullName() { return `${this._firstName} ${this._familyName}`; }}class Manager { constructor(firstName, familyName) { this._firstName = firstName; this._familyName = familyName; this._managedEmployees = []; } getFullName() { return `${this._firstName} ${this._familyName}`; } addEmployee(employee) { this._managedEmployees.push(employee); }}

数据属性_firstName和_familyName以及方法getFullName在我们的类之间重复。 我们可以通过让Manager类从Employee类继承来消除重复。 当我们这样做时,Employee类的状态和行为 ,包括它的数据和函数,将被合并到我们的Manager类中。下面是一个使用继承的版本。 注意使用super:

// Manager still works same as before but without repeated codeclass Manager extends Employee { constructor(firstName, familyName) { super(firstName, familyName); this._managedEmployees = []; } addEmployee(employee) { this._managedEmployees.push(employee); }}

5.2、是一个或像一个东西来判断是否适用继承

有一些设计原则可以帮助您确定何时适合继承。 继承应始终建立“像A”和“与A类似”这样关系的模型。 也就是说,经理“是一个”,“就像一个”特定类型的员工,同时我们在超类实例上操作的任何地方,都应该能够在子类实例中替换,而且一切都应该仍然有用。 违反和遵守这一原则之间的区别有时可能是微妙的。 细微违规的典型示例是Rectangle超类和Square子类:

class Rectangle { set width(w) { this._width = w; } get width() { return this._width; } set height(h) { this._height = h; } get height() { return this._height; }}// A function that operates on an instance of Rectanglefunction f(rectangle) { rectangle.width = 5; rectangle.height = 4; // Verify expected result if (rectangle.width * rectangle.height !== 20) { throw new Error("Expected the rectangle's area (width * height) to be 20"); }}// A square IS-A rectangle... right?class Square extends Rectangle { set width(w) { super.width = w; // Maintain square-ness super.height = w; } set height(h) { super.height = h; // Maintain square-ness super.width = h; }}// But can a rectangle be substituted by a square?f(new Square()); // error

正方形可以是数学上的矩形,但是正方形在行为上不像矩形。任何超类实例的使用都应该由子类实例替代的规则称为Liskov替换原则(Liskov Substitution Principle),它是面向对象类设计的重要部分。

5.3、小心过度使用继承

在任何地方都很容易找到共性,特别是对经验丰富的开发人员,拥有提供完整功能的类前景也很诱人。但是继承也有缺点。回想一下,我们仅通过一组小而固定的函数来操作数据来确保有效状态。但是当我们继承时,我们会增加可以直接操作数据的函数列表,然后这些附加函数也负责维护有效状态。如果太多函数可以直接操作数据,那么该数据几乎与全局变量一样糟糕。过多的继承会产生单一的类,这些类会稀释封装(封装性),更难以正确使用,并且难以重用。相反,更喜欢设计仅包含一个概念的最小类。

让我们重新审视代码重复问题。我们能否在不继承的情况下解决它?另一种方法是通过引用连接对象以表示部分整体关系。下面是使用组合而不是继承的经理 - 员工关系的一个版本:

class Employee { constructor(firstName, familyName) { this._firstName = firstName; this._familyName = familyName; } getFullName() { return `${this._firstName} ${this._familyName}`; }}class Group { constructor(manager /* : Employee */ ) { this._manager = manager; this._managedEmployees = []; } addEmployee(employee) { this._managedEmployees.push(employee); }}

在这里,经理不是一个单独的类。 相反,管理器是Group实例持有引用的普通Employee实例。 如果继承或组合可以合理地表达我们的程序概念和关系,那么更喜欢组合。

5.4、继承替换子类

继承还允许通过公共超类提供的接口,进而派生不同的子类。 将超类实例作为参数的函数也可以传递给子类实例,而函数不必知道任何子类。 从同一个超类派生出不同子类的方式也被称为多态

// This will be our common superclassclass Cache { get(key, defaultValue) { const value = this._doGet(key); if (value === undefined || value === null) { return defaultValue; } return value; } set(key, value) { if (key === undefined || key === null) { throw new Error('Invalid argument'); } this._doSet(key, value); } // Must be overridden // _doGet() // _doSet()}// Subclasses define no new public methods// The public interface is defined entirely in the superclassclass ArrayCache extends Cache { _doGet() { // ... } _doSet() { // ... }}class LocalStorageCache extends Cache { _doGet() { // ... } _doSet() { // ... }}// Functions can polymorphically operate on any cache by interacting through the superclass interfacefunction compute(cache) { const cached = cache.get('result'); if (!cached) { const result = // ... cache.set('result', result); } // ...}compute(new ArrayCache()); // use array cache through superclass interfacecompute(new LocalStorageCache()); // use local storage cache through superclass interface

5.5、 不仅仅是语法糖

JavaScript类语法通常被认为是语法糖,并且在很多情况下确实如此,但也存在真正的差异 。很多通过ES6可以实现的事情在ES5中可能根本没法做到,或者不容易做到。

静态属性是继承的

ES5没有让我们在构造函数之间创建真正的继承。 Object.create可以创建一个普通的对象,但不能创建一个函数对象。 我们通过手动复制它们来伪造静态属性的继承。 现在有了ES6类,我们在子类构造函数和超类构造函数之间得到了一个真正的原型链接:

// ES5function B() {}B.f = function () {};function D() {}D.prototype = Object.create(B.prototype);D.f(); // error

而下面是ES6版本:

// ES6class B { static f() {}}class D extends B {}D.f(); // ok

内置构造函数可以派生

有些对象是“异国情调”,并且不像普通对象那样。 例如,数组将其length属性调整为大于最大整数索引。 在ES5中,当我们尝试子类化Array时,new运算符将为我们的子类分配一个普通对象,而不是超类的对象

// ES5function D() { Array.apply(this, arguments);}D.prototype = Object.create(Array.prototype);var d = new D();d[0] = 42;d.length; // 0 - bad, no array exotic behavior

ES6类通过更改分配对象的时间和分配者来修复此问题。 在ES5中,在调用子类构造函数之前分配了对象,子类将该对象传递给超类构造函数。 现在使用ES6类,在调用超类构造函数之前分配对象,并且超类使该对象可用于子类构造函数。 这使得Array即使在我们的子类上调用new时也会分配一个期望的对象:

// ES6class D extends Array {}let d = new D();d[0] = 42;d.length; // 1 - good, array exotic behavior

5.6、大杂烩

还有一小部分其他的,可能不太重要的差异。 类构造函数不能被函数调用。 这可以防止忘记使用new调用构造函数。 此外,无法重新分配类构造函数的prototype属性。 这可以帮助JavaScript引擎优化类对象。 最后,类方法没有原型属性(prototype)。 这可以通过消除不必要的对象来节省内存。

6、以富有想象力的方式运用新特征

本文讨论的都是JavaScript的新功能,社区正在尝试以新的和富有想象力的方式使用这些功能。

使用代理进行多重继承

其中一个方法就是使用代理,这是JavaScript的一个新功能,用于实现多重继承。 JavaScript的原型链只允许单继承。 对象只能委托给另一个对象。 代理为我们提供了一种将属性访问委托给多个其他对象的方法:

const transmitter = { transmit() {}};const receiver = { receive() {}};// Create a proxy object that intercepts property accesses and forwards to each parent,// returning the first defined value it findsconst inheritsFromMultiple = new Proxy([transmitter, receiver], { get: function(proxyTarget, propertyKey) { const foundParent = proxyTarget.find(parent => parent[propertyKey] !== undefined); return foundParent && foundParent[propertyKey]; }});inheritsFromMultiple.transmit(); // worksinheritsFromMultiple.receive(); // works

我们可以扩展它以适应ES6类吗? 类的原型可以是转发属性访问多个其他原型的代理。 JavaScript社区正在努力解决这个问题。 你能搞清楚吗? 加入讨论并分享您的想法。

类工厂的多重继承

JavaScript社区一直在尝试的另一种方法是按需生成扩展变量超类的类。 每个类仍然只有一个父类,但我们可以用有趣的方式将这些父类联系起来:

function makeTransmitterClass(Superclass = Object) { return class Transmitter extends Superclass { transmit() {} };}function makeReceiverClass(Superclass = Object) { return class Receiver extends Superclass receive() {} };}class InheritsFromMultiple extends makeTransmitterClass(makeReceiverClass()) {}const inheritsFromMultiple = new InheritsFromMultiple();inheritsFromMultiple.transmit(); // worksinheritsFromMultiple.receive(); // works

7、类Class的浏览器支持情况

如下图所示,现在浏览器对类Class的支持都是非常好的:

8dd6787347d9a887195e336f38a5e438.png

希望本文能让您深入了解ES6中的类如何工作,并揭开了围绕它们的一些术语的神秘面纱。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值