前端开发那些事儿

再谈JavaScript原型和类

2020-10-25  本文已影响0人  读行笔记

原型Prototype是JavaScript对象继承体系的根基,而类和对象是构建复杂系统的有效方法,它们的重要程度不言而喻。

原型Prototype

前面的文章中,已经对原型的概念和使用方式进行了介绍,并且对原型链也进行了讨论。下面,再来谈谈一些其他问题。

F.prototype

每个函数都有一个属性prototype,指向它的原型,只能是对象object或者null,而不能是其他类型,比如基本类型。

prototype只能在调用new时使用。如果有动态修改的需求,还可以随时修改,但是在修改之后,并不会修改已经创建的对象的原型,而只能对后续新创建的对象产生影响。

let animal = {
  eats: true
};

function Rabbit(name) {
    this.name = name;
}

Rabbit.prototype = animal;
let rabbit = new Rabbit("One");

let toy = {
    playable: true
}

Rabbit.prototype = toy;
let toyRabbit = new Rabbit("Another")

rabbit.__proto__ === animal // true
toyRabbit.__proto__ === toy // true

再来看看另一些例子。在函数对象的原型上直接修改、删除某些属性:

function Rabbit() {}
Rabbit.prototype = {
  eats: true
};

let rabbit = new Rabbit();

// 修改原型对象的属性
Rabbit.prototype.eats = false;

alert( rabbit.eats );   // ① false

// 删除对象属性
delete rabbit.eats;
alert( rabbit.eats );   // ② true

// 删除原型对象上的属性
delete Rabbit.prototype.eats;
alert( rabbit.eats );   // ③ undefined

为什么会这样?首先因为:

这样,就比较好理解上面例子了:

Object.prototype

在JavaScript中,所有对象对继承自Object,它的原型是Object.prototype,再往上寻找,就成了null

let obj = {};

alert(obj.__proto__ === Object.prototype); // true
alert(Object.prototype.__proto__); // null

所有原生对象也都继承自Object,比如Array、Date、Function等,下面是它们的继承关系。

原生对象继承关系
let arr = [1, 2, 3];

alert( arr.__proto__ === Array.prototype ); // true

alert( arr.__proto__.__proto__ === Object.prototype ); // true

// 已经达到继承关系链的顶部
alert( arr.__proto__.__proto__.__proto__ ); // null

甚至,我们还可以借用原型方法,比如一个对象需要某个原生对象的内置方法,则可以很容易的实现。

let obj = {
  0: "Hello",
  1: "world!",
  length: 2,
};

obj.join = Array.prototype.join;

alert( obj.join(',') ); // Hello,world!

这种方式得以奏效,是因为原生对象Array的方法join的实现逻辑,只关注对象索引和length属性,它并不管对象是不是真正的Array。可以看出,这就是原型概念的微观呈现,它只关注对象的具体行为,并以此划分类型。

下面,给所有function添加一个方法defer,允许它们延迟一定时间之后再执行。

Function.prototype.defer = function(ms) {
  setTimeout(this, ms);
};

function f() {
  alert("Hello!");
}

f.defer(1000); // 1秒之后显示Hello!

但是这种方式并不能接受参数,可以结合前面说活的装饰器重新实现。

Function.prototype.defer = function(ms) {
    let f = this;
    return function(...args) {
        setTimeout(() => f.apply(this, args), ms);
    }
}
function f(a, b) {
  alert( a + b );
}

f.defer(1000)(1, 2); // 1秒之后:3

类Class

从ES6开始,类class正式成为JavaScript官方支持的基础设施,并且支持继承。虽然在细节上还是基于原型实现的,但这将对熟悉基于类的面向对象语言的开发者更加友好。

基本用法

一个典型的Class如下:

class MyClass {
  prop = value; // property

  constructor(...) { // constructor
    // ...
  }

  method(...) {} // method

  get something(...) {} // getter method
  set something(...) {} // setter method

  [Symbol.iterator]() {} // method with computed name (symbol here)
  // ...
}

继承

继承,重写父类方法等。

class Animal {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  run(speed) {
    this.speed = speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }

  stop() {
    this.speed = 0;
    alert(`${this.name} stands still.`);
  }

}

class Rabbit extends Animal {

  constructor(...args) {
    super(...args);
  }

  hide() {
    alert(`${this.name} hides!`);
  }

  stop() {
    super.stop(); // call parent stop
    this.hide(); // and then hide
  }
}

let rabbit = new Rabbit("White Rabbit");

rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.stop(); // White Rabbit stands still. White rabbit hides!

静态*

静态方法

静态方法是属于类本身的方法,而不是具体的每一个对象的方法。在JavaScript中,当方法前有static关键字,就变成了静态方法,此时,this表示类本身,而非具体的对象。

// 1. 定义在类内
class User {
  static staticMethod() {
    alert(this === User);
  }
}

// 2. 直接赋值
class User { }

User.staticMethod = function() {
  alert(this === User);
};

User.staticMethod(); // true

工厂方法:

class Article {
  constructor(title, date) {
    this.title = title;
    this.date = date;
  }

  static createTodays() {
    // remember, this = Article
    return new this("Today's digest", new Date());
  }
}

let article = Article.createTodays();

alert( article.title ); // Today's digest

静态属性

同样,也是在属性之前加上static关键字,但是不能写在构造器中。

// 写法1
class Article {
  static publisher = "Ilya Kantor";
}

// 写法2
Article.publisher = "Ilya Kantor";

alert( Article.publisher ); // Ilya Kantor

对于静态方法和静态属性,继承同样也是适用的。

属性

在JavaScript中,类的所有属性默认都是公开的,如果需要限定作用域,需要加上特定符号:

和其他PL一样,受保护的属性只在当前类及其子类中可见。

class CoffeeMachine {
  _waterAmount = 0;

  set waterAmount(value) {
    if (value < 0) throw new Error("Negative water");
    this._waterAmount = value;
  }

  get waterAmount() {
    return this._waterAmount;
  }

  constructor(power) {
    this._power = power;
  }

}

// create the coffee machine
let coffeeMachine = new CoffeeMachine(100);

// add water
coffeeMachine.waterAmount = -10; // Error: Negative water

私有属性和方法只在当前类中可见。

class CoffeeMachine {
  #waterLimit = 200;

  #checkWater(value) {
    if (value < 0) throw new Error("Negative water");
    if (value > this.#waterLimit) throw new Error("Too much water");
  }

}

let coffeeMachine = new CoffeeMachine();

// 获取不到下面方法和属性
coffeeMachine.#checkWater(); // Error
coffeeMachine.#waterLimit = 1000; // Error

Mixin

mixin也是一种代码复用的方法,不过和继承不太一样,它允许其他类不通过继承就可以共享属于它的方法,在某些场合,也被叫做includeinterface,即组合或接口。这类做法相对继承的优点在于,它们的继承关系更加直观可控,而不像继承那样复杂,甚至有时候让人捉摸不透。

比如下面的例子:

// mixin
let sayHiMixin = {
  sayHi() {
    alert(`Hello ${this.name}`);
  },
  sayBye() {
    alert(`Bye ${this.name}`);
  }
};

class User {
  constructor(name) {
    this.name = name;
  }
}

// 是通过原型实现的
Object.assign(User.prototype, sayHiMixin);

new User("Dude").sayHi(); // Hello Dude!

下面这个例子是DOM元素通过Mixin响应事件的典型例子,一共三个重要方法:

let eventMixin = {
  /**
   * Subscribe to event, usage:
   *  menu.on('select', function(item) { ... }
  */
  on(eventName, handler) {
    if (!this._eventHandlers) this._eventHandlers = {};
    if (!this._eventHandlers[eventName]) {
      this._eventHandlers[eventName] = [];
    }
    this._eventHandlers[eventName].push(handler);
  },

  /**
   * Cancel the subscription, usage:
   *  menu.off('select', handler)
   */
  off(eventName, handler) {
    let handlers = this._eventHandlers?.[eventName];
    if (!handlers) return;
    for (let i = 0; i < handlers.length; i++) {
      if (handlers[i] === handler) {
        handlers.splice(i--, 1);
      }
    }
  },

  /**
   * Generate an event with the given name and data
   *  this.trigger('select', data1, data2);
   */
  trigger(eventName, ...args) {
    if (!this._eventHandlers?.[eventName]) {
      return; // no handlers for that event name
    }

    // call the handlers
    this._eventHandlers[eventName].forEach(handler => handler.apply(this, args));
  }
};

// 使用
class Menu {
  choose(value) {
    this.trigger("select", value);
  }
}
// Add the mixin with event-related methods
Object.assign(Menu.prototype, eventMixin);

let menu = new Menu();

// add a handler, to be called on selection:
menu.on("select", value => alert(`Value selected: ${value}`));

// triggers the event => the handler above runs and shows:
// Value selected: 123
menu.choose("123");

总结

原型是JavaScript语言的对象继承体系的核心,即使是自从ES6加入了class语法支持,但实际上类也是在原型的基础上实现的。下面是原型概念的核心内容:

另外,原型还可以被动态修改,但修改之后只能对后续新建对象产生影响,而不会影响现存对象。除此之外,还有一些特殊情况需要特别对待。

class语法的支持让JavaScript和其他语言在类的使用细节上保持了同步,这将降低使用门槛,让我们以熟悉的方式实现代码复用。需要注意的是,在底层上,不管是继承还是mixin等,class还是以原型概念实现的。

上一篇下一篇

猜你喜欢

热点阅读