9.1面向对象编程

OOP的基础理念非常简单直观:对象是一个逻辑相关的数据和功能的集合。
它以人类对世界的自然理解为设计理念。
类指的是通用的东西(模板)。实例(或者对象实例)指具体的东西。人类==》郭靖

9.2创建类和实例

在ES6之前,JavaScript 中创建类的过程比较繁琐且不直观。ES6 新引入了一些创
建类的便捷语法:

  1. class Car {
  2. constructor(make, model) {
  3. this.make = make;
  4. this.model = model;
  5. this.userGears = ['P', 'N', 'R', 'D'];
  6. this.userGear = this.userGears[0];
  7. }
  8. shift(gear) {
  9. if (this.userGears.indexOf(gear) < 0) {
  10. throw new Error(`Invalid gear ${gear}`);
  11. }
  12. this.userGear = gear;
  13. }
  14. }
  15. const car1 = new Car("Tesla", "ModelS");
  16. const car2 = new Car("Mazda", "3i");
  17. console.log(car1 instanceof Car);
  18. console.log(car2 instanceof Car);
  19. car1.shift('D');
  20. car2.shift('R');
  21. console.log(car1.userGear);
  22. console.log(car2.userGear);

9.3类即函数

类实际上就是函数。在ES5中,会这样编写Car类
ES6中并没有什么新东西,仅仅是多出了一些更易用的新语法,

  1. //构造方法 始终以大写字母开头
  2. function Car(make, model) {
  3. this.make = make;
  4. this.model = model;
  5. this._userGears = ['P', 'N', 'R', 'D'];
  6. this._userGear = this._userGears[0];
  7. }
  8. //使用关键字new来创建一个新的实例对象
  9. let car = new Car("Tesla","ModelS");
  10. //验证方式
  11. class ES6Car {}
  12. function ES5Car() {}
  13. console.log(typeof ES6Car);
  14. console.log(typeof ES5Car);

9.4遍历属性(枚举属性)

对象属性枚举是无序的

  1. const SYM = Symbol("SYM");
  2. const o = {
  3. a: 1,
  4. b: 2,
  5. c: 3,
  6. [SYM]: 4
  7. };
  8. for (let prop in o) {
  9. if (!o.hasOwnProperty(prop)) {
  10. continue;
  11. }
  12. console.log(`${prop}:${o[prop]}`);
  13. }
  14. Object.keys(o).forEach(prop => console.log(`${prop}:${o[prop]}`));
  15. let syms = Object.getOwnPropertySymbols(o);
  16. console.log(syms);
  17. for(let i=0;i<syms.length;i++){
  18. console.log(syms[i].description);
  19. console.log(o[syms[i]]);
  20. }

Symbol属性遍历
for in不能遍历到Symbol类型的属性
for of不能遍历到Symbol类型的属性
Object.keys()不能遍历到Symbol类型的属性
Object.getOwnPropertySymbols(obj)仅遍历obj的Symbol类型的属性

9.5动态属性(get/set)

大部分面向对象语言都会通过封装来阻止直接赋值对象属性赋值。
JavaScript没有访问权限机制,动态属性可以稍微有效地弥补这种不足
私有属性加上下划线作为前缀,是一种约定俗成的做法
get/set关键字 关键字将对象属性与函数进行绑定,当属性被访问时,对应函数被执行。
表示即时调用

  1. class Car {
  2. constructor(make, model) {
  3. this.make = make;
  4. this.model = model;
  5. this._userGears = ['P', 'N', 'R', 'D'];
  6. this._userGear = this._userGears[0];
  7. }
  8. get userGear() {
  9. return this._userGear;
  10. }
  11. set userGear(value) {
  12. if (this._userGears.indexOf(value) < 0) {
  13. throw new Error(`Invalid gear ${value}`);
  14. }
  15. this._userGear = value;
  16. }
  17. shift(gear) {
  18. this.userGear = gear;
  19. }
  20. }
  21. const car1 = new Car("Tesla", "ModelS");
  22. const car2 = new Car("Mazda", "3i");
  23. console.log(car1 instanceof Car);
  24. console.log(car2 instanceof Car);
  25. car1.shift('D');
  26. car2.shift('R');
  27. console.log(car1.userGear);
  28. console.log(car2.userGear);

WeakMap
即时调用函数将WeakMap隐藏在一个闭包内,从而阻止了外界的访问。WeakMap可以安全地存储任何不想被Car类外部访问的属性。

  1. const Car = (function() {
  2. const carProps = new WeakMap();
  3. class Car {
  4. constructor(make, model) {
  5. this.make = make;
  6. this.model = model;
  7. this._userGears = ['P', 'N', 'R', 'D'];
  8. carProps.set(this, {
  9. userGear: this._userGears[0]
  10. });
  11. }
  12. get userGear() {
  13. return carProps.get(this).userGear;
  14. }
  15. set userGear(value) {
  16. if (this._userGears.indexOf(value) < 0) {
  17. throw new Error(`Invalid gear ${value}`);
  18. }
  19. carProps.get(this).userGear = value;
  20. }
  21. shift(gear) {
  22. this.userGear = gear;
  23. }
  24. }
  25. return Car;
  26. })();
  27. const car1 = new Car("Tesla", "ModelS");
  28. const car2 = new Car("Mazda", "3i");
  29. console.log(car1 instanceof Car);
  30. console.log(car2 instanceof Car);
  31. car1.shift('D');
  32. car2.shift('R');
  33. console.log(car1.userGear);
  34. console.log(car2.userGear);

9.6原型和原型链

大多数时候,不必搞懂原型链和动态调度的机制。但在不同的阶段,可能会遇到一
些迫使大家去深入学习它们的问题。最好在学习的时候能都明白其中的细节。

  1. function ES5Car() {
  2. this.make = "Tesla";
  3. //实例方法
  4. this.shift = function() {
  5. console.log('当前类挂挡');
  6. }
  7. }
  8. let car1 = new ES5Car();
  9. let car2 = new ES5Car();
  10. console.log(car1.shift ===car2.shift);
  11. class ES6Car {
  12. constructor(make, model) {
  13. this.make = make;
  14. this.model = model;
  15. this.userGears = ['P', 'N', 'R', 'D'];
  16. this.userGear = this.userGears[0];
  17. }
  18. //原型方法
  19. shift(gear) {
  20. if (this.userGears.indexOf(gear) < 0) {
  21. throw new Error(`Invalid gear ${gear}`);
  22. }
  23. this.userGear = gear;
  24. }
  25. }
  26. let car3 = new ES6Car("Tesla", "ModelS");
  27. let car4 = new ES6Car("Mazda", "3i");
  28. console.log(car3.shift===car4.shift);

对应名称
prototype :原型
proto :原型链
从属关系
每个函数都有一个叫作prototype(原型) 的特殊属性
每个对象都有一个叫作proto(原型链)的特殊属性
对象的proto保存着该对象的构造函数的prototype

  1. function Car() {
  2. }
  3. //prototype -> 函数的一个属性:值是对象
  4. console.log(Car.prototype);
  5. const car1 = new Car();
  6. //__proto__ -> 对象Object的一个属性:值是对象
  7. console.log(car1.__proto__);
  8. //对象的__proto__保存着该对象的构造函数的prototype
  9. console.log(car1.__proto__ === Car.prototype);
  10. //
  11. console.log(Car.prototype.__proto__);
  12. //
  13. console.log(Car.prototype.__proto__ === Object.prototype);
  14. console.log(Object.prototype.__proto__); //Object最顶层__proto__ null

关于原型,有一个重要的机制叫作动态调度
当试图访问对象的某个属性或者方法时,如果它不存在于当前对象中,JavaScritp会检查它是否存在于对象原型中。因为同一个类的所有实例共用同一个原型,如果原型中存在某个属性或者方法,则该类的所有实例都可以访问这个属性或方法。

  1. function Car() {
  2. this.make = "Tesla";
  3. this.shift = function(){
  4. console.log('当前类挂挡');
  5. }
  6. }
  7. console.log(Car.prototype);
  8. Car.prototype.mode = "ModelS";
  9. const car1 = new Car();
  10. console.log(car1);
  11. Object.prototype.userGears = ['P', 'N', 'R', 'D'];
  12. console.log(car1.make);
  13. console.log(car1.mode);
  14. console.log(car1.userGears);
  15. // Car {
  16. // make:"Tesla",
  17. // __proto__:Car.prototype = {
  18. // mode:"ModelS",
  19. // __proto__:Object.prototype = {
  20. // userGears:['P', 'N', 'R', 'D'],
  21. // }
  22. // }
  23. // }
  24. Car.prototype.shift = function(){
  25. console.log('prototype原型挂挡');
  26. }
  27. //动态调度
  28. car1.shift();

9.7继承

在分析原型的时候,已经看到了继承的身影:当创建一一个类的实例时,它继承了类
原型中所有的功能。尽管如此,它并没有就此打住:如果一个方法没有在对象原型
中找到其定义,它会检查原型的原型。这样就建立了一个原型链。JavaScript 会沿
着原型链走下去,直到某个原型满足了需求。如果找不到这样的原型,程序最终会报错

  1. //交通工具
  2. class Vehicle{
  3. constructor() {
  4. this.passengers = [];
  5. console.log("Vehicle Create");
  6. }
  7. addPassenger(p){
  8. this.passengers.push(p);
  9. }
  10. }
  11. //汽车 继承 交通工具
  12. class Car extends Vehicle{
  13. constructor() {
  14. super();
  15. console.log("Car created");
  16. }
  17. deployAirbags(){
  18. console.log("BWOOSH");
  19. }
  20. }
  21. const v = new Vehicle();
  22. v.addPassenger("Frank");
  23. v.addPassenger("Judy");
  24. console.log(v.passengers.join(","));
  25. const c = new Car();
  26. c.addPassenger("Alice");
  27. c.addPassenger("Cameron");
  28. console.log(c.passengers.join(","));
  29. c.deployAirbags();
  30. // 注意,可以在c上调用deployAirbags,但是不能在v上调用。换言之,继承
  31. // 是单向的。Car类的实例可以访问所有Vehicle类的方法,反之却不行。
  32. //v..deployAirbags();//报错

9.8静态方法

静态方法(也叫类方法),它不与实例绑定。在静态方法中,this绑
定的是类本身,但通常使用类名来代替this是公认的最佳实践。

  1. class Car {
  2. static getNextVin() {
  3. return Car.nextVin++;
  4. }
  5. constructor(make, model) {
  6. this.make = make;
  7. this.model = model;
  8. this.vin = Car.getNextVin();
  9. }
  10. //判断辆车是否具有相同的品牌和型号
  11. static areSimilar(car1, car2) {
  12. return car1.make === car2.make && car1.mode === car2.model;
  13. }
  14. //判断辆车是否具有相同VIN
  15. static areSame(car1, car2) {
  16. return car1.vin === car2.vin;
  17. }
  18. }
  19. Car.nextVin = 0;
  20. const car1 = new Car("Tesla", "s");
  21. const car2 = new Car("Mazda", "3");
  22. const car3 = new Car("Mazda", "3");
  23. console.log(car1.vin);
  24. console.log(car2.vin);
  25. console.log(car3.vin);
  26. Car.areSimilar(car1, car2);
  27. Car.areSimilar(car2, car3);
  28. Car.areSame(car2, car3);
  29. Car.areSame(car2, car3);

9.9多态

多态是面向对象的一个术语,意思是一个实例不仅是它自身类的实例,也可以被当做它的任何父类的实例来使用。
JavaScript弱类型语言,就是数据类型可以被忽略的语言,天生具有多态性。
JavaScript提供了instanceof运算符,它会指出某个对象是否属于某个给定类。

  1. class Vehicle{}
  2. class Motorcycle extends Vehicle {}
  3. class Car extends Vehicle {}
  4. const c = new Car();
  5. const m = new Motorcycle();
  6. console.log(c instanceof Car);
  7. console.log(c instanceof Vehicle);
  8. console.log(m instanceof Car);
  9. console.log(m instanceof Motorcycle);
  10. console.log(m instanceof Vehicle);
  11. function run(obj){
  12. if(obj instanceof Car){
  13. console.log("汽车对象,执行汽车的方法");
  14. }
  15. if(obj instanceof Motorcycle){
  16. console.log("摩托车对象,执行汽车的方法");
  17. }
  18. }

9.10hasOwnProperty()和toString()方法

  1. class Super {
  2. constructor() {
  3. this.name = 'Super';
  4. this.isSuper = true;
  5. }
  6. }
  7. //合法,但不推荐这么做
  8. Super.prototype.sneaky = 'not recommended';
  9. class Sub extends Super {
  10. constructor() {
  11. super();
  12. this.name = 'Sub';
  13. this.isSub = true;
  14. }
  15. }
  16. const obj = new Sub();
  17. // 如果obj中存在x属性,obj . hasOwnProperty(x)返回true。反之,如果属性
  18. // 没有定义或者定义在原型链中,obj .hasOwnProperty (x)结果为false。
  19. for (let p in obj) {
  20. console.log(`${p}:${obj[p]}`
  21. +(obj.hasOwnProperty(p)?'':'(inherited)'));
  22. }
  23. // name,isSuper 和isSub三个属性都被定义在实例中,而不是在原型链中,
  24. // 属性sneaky被手动添加到父类的原型中。
  25. // 使用object. keys就可以完全避免这个问题,因为object.keys只包含了原型中定义的属性。

每个对象最终都是继承自object类,所以可以在object中使用的方法在任何,其他对象中都可以用。其中的一个方法是toString,该方法是为了给对象提供一个默认的字符串表示。toString默认的方法会返回”[object object]”, 这
种表示并没有什么实际用处。

  1. class Car {
  2. static getNextVin() {
  3. return Car.nextVin++;
  4. }
  5. constructor(make, model) {
  6. this.make = make;
  7. this.model = model;
  8. this.vin = Car.getNextVin();
  9. }
  10. toString() {
  11. return `${this.make} ${this.model}:${this.vin}`;
  12. }
  13. }
  14. Car.nextVin = 0;
  15. let car = new Car("Tesla", "s");
  16. console.log(car.toString());

9.11多继承

一些面向对象语言(C++)支持多继承,也就是说一个类可以有两个直接的父类。多继承有引发冲突的风险。
比如两个父类都有一个同名的方法,那么子类该继承谁的方法呢?所以很多编程语言选择单继承来避免这个问题。但是 当思考现实世界的问题时,多继承往往是有意义的。例如,汽车可能继承自交通工具和“可接受保险的”。不支持多继承的编程语言通常会引人接口的概念来绕过这个问题。
一个类(Car)只能有一个父类(Vehicle),但它可以有多个接口(Insurable,Container等等)。
JavaScript是一种有趣的混合。技术上,它是一一个单继承的语言,因为在原型链并
不会去寻找多个父类,但是它确实提供了一些方法,有时候这些方法比多继承或者
接口还要好用。

  1. class InsurancePolicy {
  2. }
  3. class Car{
  4. }
  5. function makeInsurable(o) {
  6. o.addInsurancePolicy = function(p) {
  7. this.insurancePolicy = p;
  8. }
  9. o.getInsurancePolicy = function() {
  10. return this.insurancePolicy;
  11. }
  12. o.isInsured = function() {
  13. return !!this.insurancePolicy;
  14. }
  15. }
  16. const car1 = new Car();
  17. makeInsurable(car1);
  18. car1.addInsurancePolicy(new InsurancePolicy())