面向对象的 JavaScript – 深入了解 ES6 类

面向对象的 JavaScript – 深入了解 ES6 类

通常我们需要在我们的程序中代表一个想法或概念 —— 也许是一个汽车引擎,电脑文件,路由器或温度读数。在代码中直接表示这些概念分为两部分:数据表示其状态和函数来表示行为。类给我们一个方便的语法来定义对象的状态和行为,来表示我们的这些概念。它们使我们的代码更安全,保证一个初始化函数能被调用,并且它们使得我们能更容易地定义一组固定的函数,来对数据进行操作并保持有效状态。如果你能把某些事物看成一个独立的实体,你可能应该定义一个类来表示你的程序中的“事物”。

更多精彩内容请看 web前端中文站
www.lisa33xiaoq.net 可按Ctrl + D 进行收藏

考虑这个 non-class(非类) 代码。 你能找到几个错误? 你如何解决他们?

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

日期 today 有一些问题,year 丢失了,如果我们有一个不能被遗忘的初始化函数会更好。另请注意,当加 1 天时,我们检查了一个地方,如果我们超过 31 日,但没有检查其他地方。所以,如果我们只通过一组固定的函数与数据交互,并且维护每个有效状态,这样才能更好。

这是使用类后的更正版本。

 class SimpleDate {   
 constructor(year, month, day) {     
 // 检查 (year, month, day) 是否为一个有效的日期     
 // ...      
 // 如果是, 使用她来初始化 "this" 的日期     
 this._year = year;     
 this._month = month;     
 this._day = day;   }    
 addDays(nDays) {     
 // 增加 "this" 日期      
 // ...   }    
 getDay() {     
 return this._day;   } }  
 // "today" 被保证是有效和完全初始化的 
 let today = new SimpleDate(2000, 2, 28);  
 // 仅通过一组固定的函数操作数据,确保我们保持有效状态 today.addDays(1);

提示:

当函数与类或对象相关联时,我们将其称为“方法”。

当从一个类创建对象时,该对象被认为是该类的“实例”。

构造函数

constructor 方法是指定的,它解决了第一个问题。它的工作是将一个实例初始化为一个有效的状态,它将被自动调用,所以我们不需要记住初始化我们的对象。

保持数据私有

我们试图设计我们的类,使他们的状态始终保存有效。我们提供一个只创建有效值的构造函数,并且我们设计的方法也总是只保留有效值。但是,只要我们把类的所有数据让大家可访问,那么有人会把它弄乱。除了通过我们提供的函数外,我们需要保护数据不可被访问。

提示:保护数据不被访问,称为“封装”。

通过公约实现属性私有化

不幸的是,JavaScript 中不存在私有的对象属性。我们要伪装他们。最常见的方法是遵守一个简单的惯例:在属性名称前加下划线(或者较不常见的是用下划线做后缀),那么它应该被视为是非公开的。我们在早期的代码示例中通常都使用这种方法。一般来说,这个简单的惯例能工作,但数据在技术上仍然可供大家使用,所以我们要靠自己的规范去做正确的事情。

通过特权方法实现属性私有化

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

 class SimpleDate {   
 constructor(year, month, day) {     
 // 检查 (year, month, day) 是否为一个有效的日期     
 // ...      
 // 如果是, 使用她来初始化 "this" 日期的普通变量     
 let _year = year;     
 let _month = month;     
 let _day = day;      
 // 在构造函数中定义的方法捕获闭包中的变量     
 this.addDays = function(nDays) {       
 // 增加 "this" 日期        
 // ...     }      
 this.getDay = function() {       
 return _day;     }   } }

通过 Symbol 实现属性私有化

Symbol 是 JavaScript 的新功能,他们给我们带来了另一种伪装私有对象属性的方法。代替带下划线的属性名称,我们可以使用唯一的 symbol 对象键,我们的 class(类) 可以在闭包中中捕获这些键。但是有一个漏洞,JavaScript 的另一个新功能是 Object.getOwnPropertySymbols,它允许外部访问我们试图保持私有的 symbol 键。

 let SimpleDate = (function() {   
 let _yearKey = Symbol();   
 let _monthKey = Symbol();   
 let _dayKey = Symbol();    
 class SimpleDate {     
 constructor(year, month, day) {       
 // 检查 (year, month, day) 是否为一个有效的日期       
 // ...        
 // 如果是, 使用她来初始化 "this" 日期       
 this[_yearKey] = year;       
 this[_monthKey] = month;       
 this[_dayKey] = day;     }      
 addDays(nDays) {       
 // 增加 "this" 日期        
 // ...     }      
 getDay() {       
 return this[_dayKey];     }   }    
 return SimpleDate; }());

通过 WeakMap 实现属性私有化

WeakMap(愚人码头注:MDN中关于弱映射的说明) 也是 JavaScript 的新功能。我们可以在使用我们的实例的作为 key 的键/值对中存储私有对象属性,并且我们的 class(类) 可以在闭包中中捕获这些键/值映射。

 let SimpleDate = (function() {   
 let _years = new WeakMap();   
 let _months = new WeakMap();   
 let _days = new WeakMap();    
 class SimpleDate {     
 constructor(year, month, day) {       
 // 检查 (year, month, day) 是否为一个有效的日期       
 // ...        
 // 如果是, 使用她来初始化 "this" 日期       
 _years.set(this, year);       
 _months.set(this, month);      
 _days.set(this, day);     }      
 addDays(nDays) {       
 // 增加 "this" 日期        
 // ...     }      
 getDay() {       
 return _days.get(this);     }   }    
 return SimpleDate; }());

其他访问修饰符

除了 “private” 之外,你会发现其他语言还有其他级别的属性可见性,如 “protected”, “internal”, “package private”, 或者 “friend”。JavaScript 仍然没有给我们一种方法来强制执行其他级别的可见性。如果你需要它们,你必须依靠公约和规范。

引用当前对象

再看一下 getDay()。它没有指定任何参数,那么它怎么知道它所调用的对象呢?当函数被作为方法调用时,使用 object.function 表示法,他有一个隐含的参数,用来标识对象,并将该隐式 argument 分配给一个名为 this 的隐式 parameter 。为了说明这一点,我们将明确地而不是隐式地发送对象参数。

 // 引用 “getDay” 函数 
 let getDay = SimpleDate.prototype.getDay;  
 getDay.call(today); 
 // "this" 指向 "today" 
 getDay.call(tomorrow); 
 // "this" 指向 "tomorrow"  
 tomorrow.getDay(); 
 // 与上一行相同,但是 "tomorrow" 被隐式地传递

静态属性和方法

我们有选择可以定义属性和函数,作为类的一部分的,但不作为该类任何一个实例的一部分(愚人码头注:就是说该类的实例不可以访问这些属性和方法)。我们分别称这些为静态属性和静态方法。每个实例只有一个静态属性的副本,而不是一个新的副本。

 class SimpleDate {   
 static setDefaultDate(year, month, day) {     
 // 静态属性可以引用,而实例不可以          
 // 相反,它在类上定义     
 SimpleDate._defaultDate = new SimpleDate(year, month, day);   }    
 constructor(year, month, day) {     
 // 如果构造没有参数,          
 // 然后通过复制静态默认日期来初始化“this”日期     
 if (arguments.length === 0) {       
 this._year = SimpleDate._defaultDate._year;       
 this._month = SimpleDate._defaultDate._month;       
 this._day = SimpleDate._defaultDate._day;        
 return;     }      
 // 检查 (year, month, day) 是否为一个有效的日期     
 // ...      
 // 如果是, 使用她来初始化 "this" 日期     
 this._year = year;     
 this._month = month;     
 this._day = day;   }    
 addDays(nDays) {     
 // 增加 "this" 日期      
 // ...   }    
 getDay() {     
 return this._day;   } }  
 SimpleDate.setDefaultDate(1970, 1, 1);  
 let defaultDate = new SimpleDate();

子类

我们经常会发现我们的类之间用共同点 – 重复的代码,我们想避免。子类让我们将另一个类的状态和行为合并到我们自己的类中。这个过程通常被称为“继承”,子类(subclass) 继承的父类,也称为超类(superclass)。继承可以避免重复并简化类的实现,比如当一个类需要使用另一个类相同的数据和函数时。继承还允许我们替换子类,只依靠一个共同的超类提供的接口。

继承避免重复

考虑下面这段 non-inheritance(非继承) 实现的代码。

 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 仍然可以跟上面的代码一样工作,但没有重复的代码 
 class Manager extends Employee {   
 constructor(firstName, familyName) {     
 super(firstName, familyName);     
 this._managedEmployees = [];   }    
 addEmployee(employee) {     
 this._managedEmployees.push(employee);   } }

IS-A(是一个) 和 WORKS-LIKE-A(工作起来像什么)

有个设计原则可以帮助您确定使用继承是否合适。继承应始终简历 IS-A(是一个) 和 WORKS-LIKE-A(工作起来像一个什么) 的关系模型。
也就是说,Manager “IS-A(是一个)”,“WORKS-LIKE-A(工作起来像一个)”特定的 Employee ,这样,在我们使用超类实例的任何地方,应该都能够使用一个子类实例替换,并且所有的事情都应该仍然有效。有时,违反和遵守这一原则的区别是微妙的。一个微妙违反原则的典型例子是 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;   } }  
 // 在 Rectangle(长方形) 实例上运行的函数 
 function f(rectangle) {   
 rectangle.width = 5;   
 rectangle.height = 4;    
 // 验证预期结果   
 if (rectangle.width * rectangle.height !== 20) {     
 throw new Error("Expected the rectangle's area (width * height) to be 20");} }  
 // 正方形 IS-A(是一个) 长方形... 对吗? 
 class Square extends Rectangle {   
 set width(w) {     
 super.width = w;      
 // 保持平方形     
 super.height = w;   }    
 set height(h) {     
 super.height = h;      
 // 保持平方形     
 super.width = h;   } }  
 // 但是可以用正方形代替长方形吗? 
 f(new Square()); 
 // error

一个正方形可以是数学上的长方形,但一个正方形在行为上不像长方形那样工作。

任何使用超类实例的地方,应该由一个子类实例来代替,这个规则称为里氏替代原则(Liskov Substitution principle),它是面向对象类设计的重要组成部分。(愚人码头注:里氏替换原则的内容可以描述为: “派生类(子类)对象能够替换其基类(超类)对象被使用。” 来自维基百科)

当心过度使用

在任何地方都很容易找到共同点,并且拥有一个提供完整功能的类的前景是很吸引人的,即使对于有经验的开发人员也是如此。但是继承也有缺点。回想一下,我们通过一组小的、固定的函数集来操纵数据,从而确保有效的状态。但是当我们使用继承时,我们增加了可以直接操作数据的一些函数,这些附加的函数也负责维护有效的状态。如果太多的函数可以直接操纵数据,那么数据几乎会和全局变量一样变得非常糟糕。过多的继承会创建单一的类,这些类会降低封装性,更难以纠正,更难以重用。相反,更喜欢设计只包含一个概念的最小类。

让我们再来看一下代码重复问题。我们不用继承可以解决它吗?另一种方法是通过引用来连接对象,以表示部分完整的关系。我们称之为“组合”。

这里是使用组合而不是继承的 manager-employee 的版本。

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

在这里,manager 不是一个单独的类。相反,一个 manager 是一个普通的 Employee 实例,Group 实例保持对其引用。如果继承模型是 IS-A(是一个) 关系,那么组合模型就是 HAS-A(有一个) 的关系。也就是说,一个 Group HAS-A(有一个) manager。(愚人码头注:更多概念可以阅读 JavaScript中的工厂函数 这篇文章。)

如果继承或组合可以合理地表达我们的程序概念和关系,那么更喜欢组合。

继承替换子类

继承还允许不同的子类通过通用超类提供的接口来替换使用。期望超类实例作为参数的函数也可以传递一个子类实例,而这个函数不必知道任何子类。
替换具有共同超类的类通常被称为“多态性”。

 // 这将是我们的共同超类 
 class Cache {   
 get(key, defaultValue) {     
 let 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);   }    
 // 必须重写   
 // _doGet()   
 // _doSet() }  
 // 子类不定义新的公共方法 
 // 公共接口完全在超类中定义 
 class ArrayCache extends Cache {   
 _doGet() {     // ...   }   
  _doSet() {     // ...   } }  
 class LocalStorageCache extends Cache {   
 _doGet() {     // ...   }    
 _doSet() {     // ...   } }  
 // 函数可以通过与超类接口进行交互,在任何 cache 上进行多态操作 
 function compute(cache) {   
 let cached = cache.get('result');   
 if (!cached) {     
 let result = // ...     cache.set('result', result);   }    
 // ... }  
 compute(new ArrayCache()); 
 // 通过超类接口使用数组 
 cache compute(new LocalStorageCache()); 
 // 通过超类接口使用本地存储的 cache

比语法糖更多

JavaScript 类语法通常被认为是语法糖,在很多方面确实如此,但也有真正的差异 – 我们可以用 ES6 classes 做 ES5 做到不到的事情。

静态属性被继承

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

 // ES5 function B() {} B.f = function () {};  
 function D() {} 
 D.prototype = Object.create(B.prototype);  
 D.f(); 
 // error
 // ES6 class B {   
 static f() {} }  
 class D extends B {}  
 D.f(); // ok

内置构造函数可以被子类化

一些对象是外来的,不像普通的对象。例如,数组,调整其 length 属性大于最大整数索引值。在ES5中,当我们尝试对 Array 进行子类化时,new 运算符将为我们的子类分配一个普通对象,不是我们超类的外来对象。

 // ES5 function 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 classes 通过更改何时和由谁分配对象来解决这个问题。在 ES5 中,对象在调用子类构造函数之前被分配,并且子类将该对象传递给超类构造函数。现在有了 ES6 classes ,在调用超类构造函数之前分配对象,并且超类使该对象可用于子类构造函数。这样,即使我们在子类中调用 newArray也可以分配一个异乎寻常的对象。

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

其他方面

还有其他一些不太明显的差异。类构造函数不能被当做函数调用。这样可以防止忘记用 new 来调用构造函数。此外,类构造函数的 prototype 属性无法重新分配。这可能有助于 JavaScript 引擎优化类对象。最后,类方法没有 prototype 属性。这可能是通过消除不必要的对象来节省内存。

富有想象力的方式使用新功能

这里和其他SitePoint文章中描述的许多 JavaScript 的新功能,社区现在正在尝试以 新的 和 富有想象力的方式使用这些功能。

通过 Proxies 实现多继承

这里有一个使用 proxies 的实验,一个 JavaScript 的新功能,实现多重继承。 JavaScript 的原型链只允许单一的继承。对象可以 委托 给另一个对象。Proxies 给我们一种方法来委托对多个其他对象的属性访问。

 let transmitter = {   
 transmit() {} };  
 let receiver = {   
 receive() {} };  
 // 创建一个 proxy 对象,拦截属性访问并发送给每个父对象, 
 // 返回找到的第一个定义的值 
 let inheritsFromMultiple = new Proxy([transmitter, receiver], {   
 get: function(proxyTarget, propertyKey) {     
 const foundParent = proxyTarget.find(parent => 
 parent[propertyKey] !== undefined);     
 return foundParent && foundParent[propertyKey];   } });  
 inheritsFromMultiple.transmit(); 
 // works inheritsFromMultiple.receive(); 
 // works

我们可以扩展这个和 classes 语法配合使用吗?一个类的 prototype(原型) 可以是一个 proxy(代理) ,它可以发送属性到多个其他原型上访问。
JavaScript社区现在正在努力。你能弄清楚吗?加入讨论并分享您的想法。

用 Class 工厂函数实现的多重继承

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()) {}  
 let inheritsFromMultiple = new InheritsFromMultiple();  
 inheritsFromMultiple.transmit(); 
 // works inheritsFromMultiple.receive(); 
 // works

还有其他想象力的方法来使用这些功能吗?现在是时候把你的足迹留在JavaScript世界了。

结论

希望这篇文章让您了解了如何在 ES6 中使用类,并且已经揭开了围绕它们的一些术语的神秘性。不幸的是,在撰写本文时,对类的支持不是很好,所以如果你想要尝试使用 ES6 中的类语法,你需要使用像 Babel 这样的转译器。尽管如此,我还是很想听听您对 classes 的看法,以及你是否是你会考虑使用的这个 ES6 特性,欢迎在下面的评论。

原文链接:https://www.sitepoint.com/object-oriented-javascript-deep-dive-es6-classes/

更多es(es5、es6)见 ES2015(2016)系列文章学习教程

推荐阅读关于 ECMAScript 的系列文章

【注:本文源自网络文章资源,由站长整理发布】

0
如无特殊说明,文章均为原作者原创,转载请注明出处

该文章由 发布

这货来去如风,什么鬼都没留下!!!
发表我的评论

Hi,请填写昵称和邮箱!

取消评论
代码 贴图 加粗 链接 删除线 签到