ES6 class与babel(2)

ES6提供了ES5这个构造函数的语法糖class用来模拟类

ES5中模拟类的流程

  • ES6中提供了class这个 构造函数的语法糖 来模拟类,但它并不是真正的类,实际上走的还是ES5中的模拟类的流程,只是看起来更像类了。

ES5中模拟类的流程

  1. 构造函数和正常创建函数方法一样
  2. 使用new关键字调用函数即相当于调用构造函数
1
2
3
4
5
6
// ES5中模拟类
function Person(name, age) {
this.name = name;
this.age = age;
}
console.log(new Person('张三',11)) // Person1 {name: '张三', age: 11}

使用new调用函数生成对象的流程

当用new关键字调用函数时,发生了什么?为什么会获得个新的对象?

  1. 创建一个空的对象
  2. 以构造函数的prototype属性作为空对象的原型
  3. this指向为这个空对象
  4. 执行构造函数
  5. 如果构造函数没有返回值则返回创建的空对象

使用原理模拟一个构造函数

根据上面“使用new调用函数生成对象的流程”的原理,我们自己创建一个构造函数Constructor(),该函数的参数1接受一个函数作为构造函数,参数2接受数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 模拟构造函数
function Constructor(fn, args) {
// 创建一个空的对象 以构造函数fn的prototype属性作为空对象的原型(可看下方“帮助理解”)
var _this = Object.create(fn.prototype)
// this 指向为这个空对象 执行构造函数 fn
var res = fn.apply(_this, args);
// 如果函数没有返回值则返回创建的空对象
return res ? res : _this;
}
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function() {
console.log( '我叫' + this.name);
}
var person = Constructor(Person, ['张三', 12]);
console.log(person); // Person {name: '张三', age: 12}
console.log(person.say()); // 我叫张三
  • 帮助理解:
    • fn.prototype: 构造函数 fn 的 prototype 属性(一个对象)
    • Object.create: 以一个现有对象作为原型,创建一个新对象 _this
    • 新对象读属性/方法读时会顺着原型链找,对象的隐式原型、构造函数的显式原型(即 Object.create() 方法的第一个参数)
  • 补充:Object.create() 静态方法以一个现有对象作为原型,创建一个新对象
    • 第一个参数被视为新创建的对象的构造函数的显式原型

ES6中class的继承

  • 继承(extends)可以让子类获得父类的方法、属性
  • 可以扩充增加新的方法属性等
  • 父类(基类)-被继承的类
  • 子类-继承后的类
  • 子类extends父类后,需要在构造函数中使用super();(相当于执行父类的构造函数)

简单例子:
给子类也定义父类的属性值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Human  {
constructor(name, age, sex, hobby) {
this.name = name;
this.age = age;
this.sex = sex;
this.hobby = hobby;
}
desc() {
const { name, age, sex, hobby } = this;
console.log(`我叫${ name },性别${ sex },爱好${ hobby },今年${ age }`)
}
eat() {
console.log("吧唧吧唧");
}
}
class FEEngineer extends Human {
constructor(name, age, sex, hobby, skill, salary) {
super(name, age, sex, hobby)
this.skill = skill;
this.salary = salary;
}
}
const feer = new FEEngineer(
"张四",
12,
"女",
"洗澡",
['es6' , 'vue', ' react', 'webgl'],
'1k'
)
feer.desc()
// 我叫张四,性别女,爱好洗澡,今年12
/*
打印feer会得到
FEEngineer {name: '张四', age: 12, sex: '女', hobby: '洗澡',
skill: ['es6' , 'vue', ' react', 'webgl'], salary: "1k" }
*/

单纯给子类定义属性值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class FEEngineer extends Human {
constructor(skill, salary) {
// 直接super()而不传参,父类的构造函数将会被调用,但不会传参
// 这可能会导致父类的属性在构造函数中被初始化为undefined
super()
this.skill = skill;
this.salary = salary;
}
}
const feer = new FEEngineer(['es6' , 'vue', ' react', 'webgl'], '1k')
/*
打印feer会得到
FEEngineer {name: undefined, age: undefined, sex: undefined, hobby: undefined,
skill: ['es6' , 'vue', ' react', 'webgl'], salary: "1k" }
*/

实际案例:网游职业系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 网游职业系统
// 基类Character 基础的职业类 代表一个角色
// 子类Wizard 代表一个具有职业的角色
class Character {
constructor(name, sex) {
this.name = name;
this.sex;
this.skill = []
}
}
class Wizard extends Character {
constructor(name, sex) {
super(name, sex);
this.initSkill();
}
initSkill() {
this.skill = [
{
name: '阿瓦达索命!',
mp: 666,
level: 999
},
{
name: '守护神咒',
mp: 333,
level: 888
}
]
}
}
const voldemort = new Wizard('伏地魔', '30')
/* { name: '伏地魔', skill: [ {name: '阿瓦达索命!', mp: 666,
level: 999}, {name: '守护神咒', mp: 333, level: 888} ] }
*/
const potter = new Wizard('哈利波特', '13')
/* { name: '哈利波特', skill: [ {name: '阿瓦达索命!', mp: 666,
level: 999}, {name: '守护神咒', mp: 333, level: 888} ] }
*/

super 关键字

注意:super 会执行父类的构造函数,在执行过程中,父类构造函数中的 this 指向正在被创建的子类的实例MDN参考

  1. super作为父类构造函数调用
    • 比如上面的例子,也就是将子类的this丢到父类的构造函数中跑一遍
  2. super作为对象的方式调用
    • 非静态方法中访问super->super指向 父类原型
    • 静态方法中访问super->super指向 父类

非静态方法中访问super->super指向 父类原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Human  {
constructor(name, age, sex, hobby) {
this.name = name;
this.age = age;
this.sex = sex;
this.hobby = hobby;
}
desc() {
const { name, age, sex, hobby } = this;
console.log(`我叫${ name },性别${ sex },爱好${ hobby },今年${ age }`)
}
eat() {
console.log("吧唧吧唧");
}
}
class FEEngineer extends Human {
constructor(name, age, sex, hobby, skill, salary) {
super(name, age, sex, hobby)
this.skill = skill;
this.salary = salary;
}
say() {
console.log(super.eat());
console.log(this.skill.join(','));
}
}
const feer = new FEEngineer(
"张四",
12,
"女",
"洗澡",
['es6' , 'vue', ' react', 'webgl'],
'1k'
)
feer.say()
/*
吧唧吧唧
undefined 【注意:调用super.eat()时,实际上是在调用父类的eat()方法,而这个方法并没有返回值。js 中如果函数没有明确的返回值,默认会返回 undefined】
es6,vue, react,webgl
*/

在静态方法中访问super->super指向 父类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Human  {
constructor(name, age, sex, hobby) {
this.name = name;
this.age = age;
this.sex = sex;
this.hobby = hobby;
}
desc() {
const { name, age, sex, hobby } = this;
console.log(`我叫${ name },性别${ sex },爱好${ hobby },今年${ age }`)
}
eat() {
console.log("吧唧吧唧");
}
}
class FEEngineer extends Human {
constructor(name, age, sex, hobby, skill, salary) {
super(name, age, sex, hobby)
this.skill = skill;
this.salary = salary;
}
say() {
console.log(super.eat());
console.log(this.skill.join(','));
}
// 静态方法 test
static test() {
// 静态方法中的super指向父类 Human
console.log(super.name)
/*
在静态方法中使用 super.name 时,它指向的是父类 Human 的构造函数的名称,
即 "Human"。它并不会访问子类 FEEngineer 实例 feer 的 name 属性"张四"
注意理解:
访问到的是类的静态属性,比如下面这个例子的 Human.total ,
this.name 是在构造函数中为每个对象创建的实例属性,不是类的属性
*/

}
}
const feer = new FEEngineer(
"张四",
12,
"女",
"洗澡",
['es6' , 'vue', ' react', 'webgl'],
'1k'
)
FEEngineer.test()

静态方法与静态属性 都是类自身的,通过类创建的对象不会拥有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Human.total = 8999999; // 给Human类添加静态属性 total

class FEEngineer extends Human {
constructor(name, age, sex, hobby, skill, salary) {
super(name, age, sex, hobby)
this.skill = skill;
this.salary = salary;
}
say() {
console.log(super.eat());
console.log(this.skill.join(','));
}
// 静态方法test
static test() {
// 静态方法中的super指向父类 Human
console.log(super.total)
}
}
const feer = new FEEngineer(
"张四",
12,
"女",
"洗澡",
['es6' , 'vue', ' react', 'webgl'],
'1k'
)
FEEngineer.test() // 8999999
// super可以访问到静态属性 说明静态方法中的super指向父类

简单的多态

  • ES6中没有提供接口来实现 多态
  • 多态:同一个接口在不同情况下做不一样的事情,即 相同的接口不同的表现
  • 接口:接口本身只是一组定义,实现都是在类里面。
  • 也就是说我们可以通过方法的重写实现多态。

实现多态简单例子
只需要在子类中定义和父类同名的方法覆盖掉父类方法就可以实现多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Human {
say() {
console.log("我是人");
}
}
class Man extends Human {
say() {
console.log("我是小哥哥");
}
}
class Woman extends Human {
say() {
console.log("我是小姐姐");
}
}
new Man().say(); // 我是小哥哥
new Woman().say(); // 我是小姐姐

子类依然可以通过super访问父类同名方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Human {
say() {
console.log("我是人");
}
}
class Man extends Human {
say() {
// super 在非静态方法中调用则super指向 父类原型
super.say();
console.log("我是小哥哥");
}
}
class Woman extends Human {
say() {
super.say();
console.log("我是小姐姐");
}
}
new Man().say(); // 我是人 我是小哥哥
new Woman().say(); // 我是人 我是小姐姐

对比 重载

  • 重载 不是发生在父子中间的,他是根据函数的参数类型、个数 让函数做不一样的事。而多态是方法的重写。
  • 区分 多态、重载
    • 重载 通常是指在同一个函数名下,根据传递的参数的不同数量或类型,执行不同的操作
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      class SimpleCalc {
      addCalc(...args) {
      if (args.length === 0) {
      return this.zero();
      }
      if (args.length === 1) {
      return this.onlyOneArgument(args);
      }
      return this.add(args);
      }
      zero() {
      return 0;
      }
      onlyOneArgument() {
      return args[0];
      }
      add(args) {
      return args.reduce((a, b) => a + b, 0);
      }
      }
      function post(url, header, params) {
      // 其实不算准确的例子,只是是那么个意思
      // 函数重载通常是在不同的参数组合下执行不同的逻辑
      if (!params) {
      params = header;
      header = null; // undefined
      }
      }
      post('https://imooc.com', {
      a: 1,
      b: 2
      })
    • 多态方法的 重写,两者是没有关系的
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      const ModelMap = {
      '红眼僵尸': 1,
      '南瓜精': 2,
      '独眼蝠': 3,
      '绿眼僵尸': 4
      }
      // 怪物
      class Monster {
      constructor(name, level, model) {
      this.model = model;
      this.name = name;
      this.level = level;
      }
      attack() {
      throw Error('须由子类来实现 `attack`(攻击)方法');
      }
      }
      class RedEyeZombie extends Monster {
      constructor() {
      super('红眼僵尸', 10, ModelMap['红眼僵尸']);
      }
      }
      class GreenEyeZombie extends Monster {
      constructor() {
      super('绿眼僵尸', 10, ModelMap['绿眼僵尸']);
      }
      attack() {
      console.log('绿眼僵尸发动了攻击');
      }
      }
      const gez = new GreenEyeZombie();
      gez.attack();
      const rez = new RedEyeZombie();
      rez.attack();
      /*
      绿眼僵尸发动了攻击
      Uncaught Error: 须由子类来实现 `attack`(攻击)方法
      */

ES5继承的实现

方法有很多,这里只说其中一种。

方法:利用构造函数,但此方法不能继承父类原型链上的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 父类是P 子类是C
function P() {
this.name = 'parent'
this.gender = 2
this.say = function() {
console.log('好的好的,我一定到!咕咕咕')
}
}

P.prototype.test = function() {
console.log('我是test方法');
}

function C() {
/* 通过调用父类的构造函数并使用 P.call(this) 将父类的属
性和方法添加到子类实例中(this指向C的实例,call相当于让C
的实例执行P函数,即执行了父类P的构造函数),从而实现了属性
的继承,但它们并不共享原型,所以say()被继承但test()不会 */
P.call(this)
this.name = 'child'
this.age = 11
}

var child = new C()
child.say() // 好的好的,我一定到!咕咕咕
child.test() // 不能继承父类原型链上的方法 会报错
// Uncaught TypeError: child.test is not a function

让子类的原型=父类的实例,即可解决问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 父类是P 子类是C
function P() {
this.name = 'parent'
this.gender = 2
this.say = function() {
console.log('好的好的,我一定到!咕咕咕')
}
}

P.prototype.test = function() {
console.log('我是test方法');
}

function C() {
P.call(this)
this.name = 'child'
this.age = 11
}

/* 解决方法:
C 的显式原型 = P 创建的实例,确立子类 C 的原型与父类 P 的实例之间的关系
那么 C 的实例可通过原型链访问 P 的原型上的方法和属性
帮助理解:
C 的实例读取不到对于方法/属性时,依次找
C实例的隐式原型=》C构造函数的显式原型=》P实例的隐式原型=》P构造函数的显式原型
注意:
一定是将P的实例赋值给C的显式原型,这样才能让C的实例通过原型链继承P的属性和方法
反过来赋值是无效的(结合上面原型链的查找路线来理解)
*/
C.prototype = new P()

var child = new C()
child.say() // 好的好的,我一定到!咕咕咕
child.test() // 我是test方法

babel

  • babel是一个JS编译器,他可以将大部分浏览器还不支持的特性编译成浏览器可良好支持的JS代码
  • 可以在babel中文官网中看到编译效果:
    babel中文官网

安装babel

  • 需要先安装node
  • 然后可以babel中文官网=>设置=>CLI中看到安装方法
  • 虽然可以全局安装,但建议对单个项目进行本地安装,因为:
    1. 同一机器上的不同的项目可以依赖不同版本的Babel,这允许你一次更新一个项目。
    2. 这意味着在你的工作环境中没有隐含的依赖项。它将使你的项目更方便移植、更易于安装。

本地安装Babel CLI的命令:

1
npm install --save-dev @babel/core @babel/cli

注意:安装之前项目中需要一个package.json(可以通过npm init-y来生成package.json),这可以保证npx命令产生合适的交互,安装成功后package.json中应包括

1
2
3
4
5
6
{
"devDependencies": {
+ "@babel/cli": "^7.0.0",
+ "@babel/core": "^7.0.0"
}
}

其中,**devDependencies的意思是生产时依赖的包,安装时使用的是--save-dev这些包上线时就不会使用了,因为上线的是编译后的结果。
而相对的就有
dependencies,安装时使用的是--save,它包含的包就是上线运行时依赖的包**。


配置babel运行命令

编译后显示在终端

package.json的scripts中,添加:

1
"build": "babel entry.js"

则在entry.js中书写ES6代码后,在终端输入npm run build即可执行babel,在终端可查看编译后的代码(但此时ES6代码还是毫无变化,这是因为我们没有设置“转换规则”)。

编译后输出到别的文件中

1
"build": "babel entry.js -o index.js"

也就是执行npm run buildentry.js进行babel编译后输出到index.js文件中
那么我们就能项目中就会生成index.js文件放置变异后的js代码。
但此时我们每次修改entry.js代码后都需要手动执行npm run build来编译。

运行以后一直自动编译

1
"build": "babel entry.js -o index.js -w"

加上-w后则只需执行一次npm run build,接下来每次entry.js修改报错都会自动编译


安装 转换规则babel-preset-env

1.安装 转换规则:

1
npm install @babel/preset-env --save-dev

2.devDependencies多了以下代码说明安装成功

1
"babel-preset-env": "^1.7.0"

创建配置文件(.babelrc)

  • **配置文件(.babelrc)**:用来告诉babel根据什么规则来编译代码的。
  • 当运行babel命令时,就会去找.babelrc文件,根据该文件内规定的配置去编译代码。
  • 注意:.babelrc文件中使用的是json格式,属性名必须加引号

在目录下新建.babelrc文件:

1
2
3
{
"presets": ["@babel/preset-env"]
}

上面试最简单的配置,详细配置可参考官网


例子

entry.js:

1
const add = (a, b) => (a + b);

运行npm run build后,index.js:

1
2
3
4
5
"use strict";

var add = function add(a, b) {
return a + b;
};

babel插件

  • npmjsbabel官网都提供了很多babel插件,可以在上面进行查询。
  • 通过插件可以实现一些ES6不支持的编译,比如ES6中不支持使用static生成静态属性,但我们可以通过**@babel/plugin-proposal-class-properties插件**使之成立。

例子

例子

安装@babel/plugin-proposal-class-properties插件

1
yarn add @babel/plugin-proposal-class-properties --dev

配置:
在配置文件(.babelrc)中添加:

1
"plugins": ["@babel/plugin-proposal-class-properties"]

正常运行不报错了:
正常运行不报错了
将entry.js放到index.html上后可在index.html上看到效果。


额外知识点

  • 遇到大量相同代码时,抽出作为一个方法,在不同地方调用该方法即可。如部分数据不同,可采用参数的方法进行传递:
    大量相同代码使抽出作为一个方法
  • 实现链式操作:想要实现最后的链式操作,就需要每一次操作都返回操作的对象
    实现链式操作
,