class

首先, 在JavaScript中, class类是一种函数

class User {
    constructor(name) { this.name = name; }
    sayHi() {alert(this.name);}
} alert(typeof User); // function

class User {…} 构造器内部干了啥?

  1. 创建一个以User为名称的函数, 这是类声明的结果(函数代码来自constructor中)
  2. 储存所有方法, 例如User.prototype中的sayHi
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi

class并不是JavaScript中的语法糖, 虽然我们可以在没有 class 的情况下声明同样的内容:

// 以纯函数的重写 User 类

// 1. 创建构造器函数
function User(name) {
     this.name = name;
}
// * 任何函数原型默认具有构造器属性,
// 所以,我们不需要创建它 // 2. 向原型中添加方法
User.prototype.sayHi = function() {
    alert(this.name);
}; // 使用方法:
let user = new User("John");
user.sayHi();
两者存在重大差异
  1. 首先,通过 class 创建的函数是由特殊内部属性标记的 [[FunctionKind]]:"classConstructor"。不像普通函数,调用类构造器时必须要用 new 关键词:

    class User {
       constructor() {}
    } alert(typeof User); // function
    User(); // Error: 没有 ‘new’ 关键词,类构造器 User 无法调用

    此外,大多数 JavaScript 引擎中的类构造函数的字符串表示形式都以 “class” 开头

    class User {
     constructor() {}
    } alert(User); // class User { ... }
  2. 方法不可枚举。 对于 "prototype" 中的所有方法,类定义将 enumerable 标记为false

    这很好,因为如果我们对一个对象调用 for..in 方法,我们通常不希望 class 方法出现。

    枚举实例属性时, 不会出现class方法; 而普通创建的构造函数, 枚举实例属性时会出现prototype上的方法。

  3. 类默认使用 use strict。 在类构造函数中的所有方法自动使用严格模式。

Getters/setters 及其他 shorthands

就像对象字面量,类可能包括 getters/setters,generators,计算属性(computed properties)等。

使用 get/set 实现 user.name 的示例:

class User {

  constructor(name) {
    // 调用 setter
    this.name = name;
  }   get name() {
    return this._name;
  }   set name(value) {
    if (value.length < 4) {
      alert("Name is too short.");
      return;
    }
    this._name = value;
  } } let user = new User("John");
alert(user.name); // John user = new User(""); // Name too short.

除了使用getter/setter语法,大多数时候我们首选 get…/set… 函数

class CoffeeMachine {
  _waterAmount = 0;   set waterAmount(value) {
    if (value < 0) throw new Error("Negative water");
    this._waterAmount = value;
  }   get waterAmount() {
    return this._waterAmount;
  }
} new CoffeeMachine().waterAmount = 100; // setter 赋值函数
class CoffeeMachine {
  _waterAmount = 0;   setWaterAmount(value) {
    if (value < 0) throw new Error("Negative water");
    this._waterAmount = value;
  }   getWaterAmount() {
    return this._waterAmount;
  }
} new CoffeeMachine().setWaterAmount(100);

虽然这看起来有点长,但函数更灵活。他们可以接受多个参数(即使我们现在不需要它们)// 更加灵活,原来getter中不能加参数,setter中只可以加一个参数,newVal,但是使用了函数后可以自定义加任意的参数

类声明在 User.prototype 中创建 getterssetters,示例:

Object.defineProperties(User.prototype, {
  name: {
    get() {
      return this._name
    },
    set(name) {
      // ...
    }
  }
});
class属性
class User {
  name = "Anonymous";   sayHi() {
    alert(`Hello, ${this.name}!`);
  }
} new User().sayHi();

属性不在 User.prototype 内。相反它是通过 new 分别为每个对象创建的。所以,该属性永远不会在同一个类的不同对象之间共享。

总结

基本的类语法:

class MyClass {
    prop = value;  // filed 公有字段声明(通过new分别为每个对象创建)
    #prop = value; // field 私有字段声明(从类外部引用私有字段是错误的。它们只能在类里面中读取或写入。)     static prop = value; // 静态属性(存储类级别的数据,MyClass本身的属性, 而不是定义在实例对象this上的属性, 只能通过 MyClass.prop 访问);静态属性是继承的。     constructor(...) { // 构造器
        // ...
    }     method(...) {} // 方法     static method(...) {} // 静态方法被用来实现属于整个类的功能,不涉及到某个具体的类实例的功能;静态方法是继承的;     get something(...) {} // getter 方法
    set something(...) {} // setter 方法     [Symbol.iterator]() {} // 计算 name/symbol 名方法 // 变量做属性
}

由于extends创建了两个[[prototype]]的引用

  1. Rabbit方法原型继承自Animal方法
  2. Rabbit.prototype 原型继承自Animal.prototype

Rabbit.__proto__ === Animal,因此对于class B extends A,类B的prototype指向了A,所以如果一个字段在B中没有找到,会继续在A中查找。故而静态属性和方法都是被继承的

技术上来说,静态声明等同于直接给类本身赋值:

class MyClass {
  static property = ...;   static method() {
    ...
  }
}
// 等同于
MyClass.property = ...
MyClass.method = ...

实例属性的新写法:

实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层

class IncreasingCounter {
  constructor() {
    this._count = 0; // (*)
  }
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

上面代码中,实例属性this._count定义在constructor()方法里面。另一种写法是,这个属性也可以定义在类的最顶层,其他都不变。

class IncreasingCounter {
  _count = 0; // (**)
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

上面代码中,实例属性_count与取值函数value()increment()方法,处于同一个层级。这时,不需要在实例属性前面加上this

这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性

class foo {
  bar = 'hello';
  baz = 'world';   constructor() {
    // ...
  }
}

上面的代码,一眼就能看出,foo类有两个实例属性,一目了然。另外,写起来也比较简洁。

extends

根据规范,如果一个类继承了另一个类并且没有constructor,那么将生成以下"空" constructor:

class Rabbit extends Animal {
    // 为没有构造函数的继承类生成以下的构造函数
    constructor(...ars) {
        super(...args);
    }
}
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} stopped.`);
  }
} class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }
} let rabbit = new Rabbit("White Rabbit");
console.log(rabbit); // console: Rabbit {speed: 0, name: "White Rabbit"}
rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.hide(); // White Rabbit hides!

extends干了啥?

通过指定"extends Animal"让 Rabbit继承自 Animal

Rabbit内部,extends关键字添加了[[Prototype]]引用: 从 Rabbit.prototypeAnimal.prototype

`extends`允许后接任何表达式(高级编程模式中用到)

类语法不仅可以指定一个类,还可以指定extends之后的任何表达式

ex.一个生成父类的函数调用

function f(phrase) {
    return class {
        sayHi() { alert(phrase) }
    }
} class User extends f("Hello") {} new User().sayHi(); // Hello

这里是 class User继承自f("Hello")的结果

我们可以根据多种状况使用函数生成类,并继承它们,这对于高级编程模式来说可能很有用。

super

通常来说,我们不希望完全替换父类的方法,而是希望基于它做一些调整或者功能性的扩展。我们在我们的方法中做一些事情,但是在它之前/之后或在执行过程中调用父类方法。

super关键字提供了上述功能

  1. 执行 super.method(…)调用父类方法; (借用并改造父类方法, 生成自己的方法)
  2. 执行super(…)调用父类构造函数(只能在子类的构造函数中运行) (继承父类属性)
重写原型方法
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} stopped.`);
  } } class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }   stop() { // (*)
    super.stop(); // 调用父类的 stop 函数
    this.hide();  // 然后隐藏
  }
} let rabbit = new Rabbit("White Rabbit"); rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.stop(); // White Rabbit stopped. White rabbit hides!

箭头函数没有super

如果箭头函数中,super被访问,那么则会从外部函数中获取(类似this)

class Rabbit extends Animal {
    stop() {
        setTimtout(() => super.stop(), 1000); // 1 秒后调用父类 stop 方法
    }
}

因此,箭头函数中的superstop()中的是相同的,所以它能按预期工作。但如果我们在这里指定一个"普通"函数,那么将会抛出错误: (找不到super)

class Rabbit extends Animal {
  stop() {
    setTimeout(function () { super.stop() }, 1000); // Unexpected super
  }
}

代码解析会出错,报Uncaught SyntaxError: 'super' keyword unexpected here

重写构造函数

根据 规范,如果一个类继承了另一个类并且没有 constructor,那么将生成以下“空” constructor

class Rabbit extends Animal {
    // 为没有构造函数的继承类生成以下的构造函数
    constructor(...args) {
        super(...args);
    }
}

可以看到,它调用了父类的constructor, 并传递了所有的参数。

如果给继承类添加一个自定义的额构造函数

class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  // ...
} class Rabbit extends Animal {   constructor(name, earLength) {
    this.speed = 0;
    this.name = name;
    this.earLength = earLength;
  }   // ...
} // 不生效!
let rabbit = new Rabbit("White Rabbit", 10); 

报错: Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

解释下就是: 继承类的构造函数必须调用 super(...), 并且一定要在this之前调用

这是因为, 在JavaScript中,“继承类的构造函数" 与所有其他的构造函数之间存在区别。在继承类中,相应的构造函数会被标记为特殊的的内部属性[[ConstructorKind]]:"derived"

不同点在于:

  • 当一个普通构造函数执行时,它会创建一个空对象作为this并继续执行。
  • 但是当继承的构造函数执行时,它并不会做这件事。它期望父类的构造函数来完成这项工作。

因此,如果我们在继承类中构建了自己的构造函数,我们必须调用super,因为如果不这样的话this指向的对象不会被创建。并且会收到一个报错。

正确的写法;需要在使用this之前调用super()

class Rabbit extends Animal {
    constructor(name, earLength) {
        super(name);
        this.earLength = earLength;
    }
}
super内部探究: [[HomeObject]]

当一个对象方法运行时,它会将当前对象作为this,如果调用super.method(),它需要从当前的原型中调用method

super技术上的实现,首先会想到,引擎知道当前对象的this,因此它可以获取父method作为this.__proto__.method。但这个解决方法是行不通的。

让我们来说明一下这个问题。没有类,为简单起见,使用普通对象。

let animal = {
    name: 'Animal',
    eat() {
        alert(`${this.name} eats.`);
    }
}; let rabbit = {
    __proto__: animal,
    name: 'Rabbit',
    eat() {
        // 这是 super.eat() 可能运行的原因
        this.__proto__.eat.call(this); // (*)
    }
}; rabbit.eat(); // Rabbit eats

在(*)这一行,我们从原型animal,我们从原型animal上获取eat方法,并在当前对象的上下文中调用它。注意, .call(this)在这里非常重要,因为简单的调用this.__proto__.eat()将在原型的上下文中执行eat,而非当前对象。

上述代码中,我们获得了正确的父类方法。但如果在原型链上再添加一个额外的对象。这就不成立了

let animal = {
    name: 'Animal',
    eat() {
        alert(`${this.name} eats`);
    }
}; let rabbit = {
    __proto__: animal,
    eat() {
        this.__proto__.eat.call(this); // (*)
    }
}; let longEar = {
    __proto__: rabbit,
    eat() {
        this.__proto__.eat.call(this); // (**)
    }
}; longEar.eat(); // Error: Maxium call stack size exceeded
// InternalError: too much recursion

代码无法运行;这是由于在()和(*)这两行中,this的值都是当前对象(longEar)。

在()和(*)这两行中,this.__proto__的值是完全相同的: 都是rabbit。在这个无限循环中,它们都调用了rabbit.eat,而并没有在原型链上向上寻找方法。

  1. longEar.eat()中,(**)这一行调用rabbit.eat并且此时this=longEar

    // 在 longEar.eat() 中 this 指向 longEar
    this.__proto__.eat.call(this) // (**)
    // 变成了
    longEar.__proto__.eat.call(this)
    // 即等同于
    rabbit.eat.call(this);
  2. 之后在rabbit.eat的(*)行中,我们希望将函数调用再原型链上向更高层传递,但是因为this=longEar,因此this.__proto__.eat又是rabbit.eat

    // 在 rabbit.eat() 中 this 依旧等于 longEar
    this.__proto__.eat.call(this) // (*)
    // 变成了
    longEar.__proto__.eat.call(this)
    // 再次等同于
    rabbit.eat.call(this);
  3. …所以 rabbit.eat 不停地循环调用自己,因此它无法进一步地往原型链的更高层调用。

因此,super无法单独使用this来解决

[[HomeObject]]

为了提供super的解决方法,javascript为函数额外添加了一个特殊的内部属性: [[HomeObjext]]

当一个函数被定义为类或者对象方法时, 它的[[HomeObject]]属性就成为那个对象。

然后super使用它来解析父类原型和它自己的方法。

let animal = {
    name: 'Animal',
    eat() { // animal.eat.[[HomeObject]] == animal // (3)
        alert(`${this.name} eats.`);
    }
}; let rabbit = {
    __proto__: animal,
    name: 'Rabbit',
    eat() {
        super.eat(); // rabbit.eat.[[HomeObject]] == rabbit
        // rabbit.eat.[[HomeObject]].__proto__.eat.call(this); // (2)
    }
}; let longEar = {
    __proto__: rabbit,
    name: 'Lonet Ear',
    eat() { // longEar.eat.[[HomeObject]] == longEar
        super.eat();
        // longEar.eat.[[HomeObject]].__proto__.eat.call(this); // (1)
    }
}; // 正常运行
longEar.eat(); // alert: Lonet Ear eats.

上述代码按照预期运行,基于[[HomeObject]]运行机制。 像longEar.eat这样的方法,知道[[HomeObejct]],并且从它的原型中获取父类方法, 并没有使用 this。( 调用顺序(1) -> (2) -> (3) )

方法并不是"自由"的

通常函数都是"自由"的,并没有绑定到javascript中的对象。因此,它们可以在对象之间赋值,并且用另外一个this调用它。
[[HomeObject]]的存在违反了这个原则,因为方法记住了它们的对象[[HomeObject]]不能被修改,所以这个绑定是永久的。

在javascript语言中[[HomeObject]]仅被用于super所以,如果一个方法不使用super,那么仍然可以被视为自由且可在对象之间复制。但在super中可能出错。

let animal = {
  sayHi() {
    console.log(`I'm an animal`);
  }
}; let rabbit = {
  __proto__: animal,
  sayHi() {
    super.sayHi();
  }
}; let plant = {
  sayHi() {
    console.log("I'm a plant");
  }
}; let tree = {
  __proto__: plant,
  sayHi: rabbit.sayHi // (*)
}; tree.sayHi();  // I'm an animal (?!?)

原因很简单:

  • 在(*)行,tree.sayHi方法从rabbit复制而来。(可能是为了避免重复代码)
  • 所以它的[[HomeObject]]rabbit,因为它是在rabbit中创建的。无法修改[[HomeObject]]
  • tree.sayHi()内具有super.sayHi()。它从rabbit中上溯,然后从animal中获取方法。
方法, 不是函数属性

[[HomeObject]] 是为类和普通对象中的方法定义的。但是对于对象来说,方法必须确切指定为 method(),而不是 "method: function()"

这个差别对我们来说可能不重要,但是对 JavaScript 来说却是非常重要的。

下面的例子中,使用非方法(non-method)语句进行比较。[[HomeObject]] 属性未设置,并且继承不起作用:

let animal = {
    eat: function() { // eat() {...}
        // ...
    }
}; let rabbit = {
    __proto__: animal,
    eat: function() {
        super.eat();
    }
} rabbit.eat(); // 错误调用 super(因为这里并没有 [[HomeObject]])

总结

1.扩展类: class Child extends Parent:

  • 这就意味着Child.prototype.proto将是Parent.prototype,所以方法被继承

2.重写构造函数:

  • 在使用this之前,我们必须在Child构造函数中将父构造函数调用为super()。( super(…)用来初始化继承类构造函数里的 this值,相当于手动执行了 this = Reflect.construct(super.constructor, args, new.target))

3.重写方法:

  • 我们可以在Child方法中使用super.method()来调用Parent方法;(通过方法的内部属性[[HomeObject]]实现往原型链的更高层调用)

4.内部工作:

  • 方法在内部[[HomeObject]]属性中记住它们的类/对象。这就是super如何解析父类方法的。
  • 因此,将一个带有super的方法从一个对象复制到另一个对象是不安全的。

补充:

  • 箭头函数没有自己的thissuper,所以它们能融入到就近的上下文,像透明似的。

class Rabbitclass Rabbit extends Object的区别

extends语法会设置两个原型: (结果就是,继承对于常规的和静态的方法都生效)

1.在构造函数的prototype之间设置原型(为了获取实例方法)

2.在构造函数之间会设置原型(为了获取静态方法)

class Rabbit extends Object {}

alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true
alert( Rabbit.__proto__ === Object ); // (2) true // 所以现在 Rabbit 对象可以通过 Rabbit 访问 Object 的静态方法,如下所示:
class Rabbit extends Object {} // 通常我们调用 Object.getOwnPropertyNames
alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2}) ); // a,b (*)

但是如果我们没有声明 extends Object,那么 Rabbit.__proto__ 将不会被设置为 Object。

class Rabbit {}

alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true
alert( Rabbit.__proto__ === Object ); // (2) false (!)
alert( Rabbit.__proto__ === Function.prototype ); // 所有函数都是默认如此 // 报错,Rabbit 上没有对应的函数
alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2})); // Error

顺便说一下,Function.prototype 也有一些函数的通用方法,比如 callbind 等等。在上述的两种情况下他们都是可用的,因为对于内置的 Object 构造函数来说,Object.__proto__ === Function.prototype。(所有函数都是默认如此)

因此class Rabbitclass Rabbit extends Object有两点区别

class Rabbit class Rabbit extends Object
- needs to call super() in constructor
Rabbit.__proto__ === Function.prototype Rabbit.__proto__ === Object

ES6语法中的class、extends与super的原理的更多相关文章

  1. 如何理解 Java 中的 <T extends Comparable<? super T>>

    Java 中类似 <T extends Comparable<? super T>> 这样的类型参数 (Type Parameter) 在 JDK 中或工具类方法中经常能看到. ...

  2. 理解 ES6 语法中 yield 关键字的返回值

    在 ES6 中新增了生成器函数的语法,本文解释了生成器函数内 yield 关键字的返回值. 描述 根据语法规范,yield 关键字用来暂停和继续执行一个生成器函数.当外部调用生成器的 next() 方 ...

  3. 理解 ES6 语法中 yield* 关键字的作用

    在 ES6 中新增了生成器函数的语法,本文解释了与生成器函数有关的 yield* 关键字,及其使用场景. 描述 根据语法规范,yield* 的作用是代理 yield 表达式,将需要函数本身产生(yie ...

  4. es6语法中promise的使用方法

    Promise是一个构造函数,它有resolve,reject,race等静态方法;它的原型(prototype)上有then,catch方法,因此只要作为Promise的实例,都可以共享并调用Pro ...

  5. es6语法中的arrow function=>

    (x) => x + 相当于 function(x){ ; }; var ids = this.sels.map(item => item.id).join() var ids = thi ...

  6. ES6语法:函数新特性(一)

    ES6 函数 引言: 函数在任何语言中偶读很重要,java里面的函数通常叫做方法,其实是一个东西,使用函数可以简化更多的代码,代码结构看着更加清晰.今天我们来学学ES6语法中,函数有什么变化. 虽然现 ...

  7. [ES6]react中使用es6语法

    前言 不论是React还是React-native,facebook官方都推荐使用ES6的语法,没在项目中使用过的话,突然转换过来会遇到一些问题,如果还没有时间系统的学习下ES6那么注意一些常见的写法 ...

  8. 浅谈Java泛型中的extends和super关键字(转)

    通配符 在本文的前面的部分里已经说过了泛型类型的子类型的不相关性.但有些时候,我们希望能够像使用普通类型那样使用泛型类型: 向上造型一个泛型对象的引用 向下造型一个泛型对象的引用 向上造型一个泛型对象 ...

  9. .vue文件在webstorm中es6语法报错解决方法

    1 语法支持es6设置 Preferences > Languages & Frameworks > JavaScript 把 Javascript Language versio ...

随机推荐

  1. 基于Docker的Mysql主从复制

    基于Docker的Mysql主从复制搭建 为什么基于Docker搭建? 资源有限 虚拟机搭建对机器配置有要求,并且安装mysql步骤繁琐 一台机器上可以运行多个Docker容器 Docker容器之间相 ...

  2. java编程思想第四版第八章总结

    1. 多态的含义 面向对象的三大基本特征: 封装,继承,多态. 多态是干什么的? 多态将做什么和怎么做分离开.从另一个角度将接口是实现类分离开. 多态的作用 消除类型之间耦合的关系 使用多态的好处? ...

  3. pat 1046 Shortest Distance(20 分) (线段树)

    1046 Shortest Distance(20 分) The task is really simple: given N exits on a highway which forms a sim ...

  4. 【Flume】Flume基础之安装与使用

    1.Flume简介 ​ (1) Flume提供一个分布式的,可靠的,对大数据量的日志进行高效收集.聚集.移动的服务,Flume只能在Unix环境下运行. ​ (2) Flume基于流式架构,容错性强, ...

  5. cocos creator 3D | 拇指射箭

    拇指射箭!你能射中靶心么? 效果预览 配置环境: cocos creator 3d v1.0.0 玩法介绍: 长按屏幕,拖动瞄准,放手发射.风向.重力和距离影响最终结果!越靠近中心得分越高!最高分10 ...

  6. web服务,ftp服务以及共享实现

    在开始服务前一定要确保可以ping通外网,在虚拟机联网但ping 不通外网下 确认vim /etc/sysconfig/network-scripts/ifcfg-ens33 (nmcli conne ...

  7. vim用户手册笔记常用操作整理

    "x"命令可以删除一个字符 "d"命令可以后跟任何一个位移命令,它将删除从当前光标起到位移的终点处的文本内容dw "c",改变命令例如cw ...

  8. 面试官:你连RESTful都不知道我怎么敢要你?

    目录 01 前言 02 RESTful的来源 03 RESTful6大原则 1. C-S架构 2. 无状态 3.统一的接口 4.一致的数据格式 4.系统分层 5.可缓存 6.按需编码.可定制代码(可选 ...

  9. ceph中rbd的增量备份和恢复

    ceph中rbd的增量备份和恢复 ceph的文档地址:Ceph Documentation ​ 在调研OpenStack中虚机的备份和恢复时,发现OpenStack和ceph紧密结合,使用ceph做O ...

  10. Spring Security之多次登录失败后账户锁定功能的实现

    在上一次写的文章中,为大家说到了如何动态的从数据库加载用户.角色.权限信息,从而实现登录验证及授权.在实际的开发过程中,我们通常会有这样的一个需求:当用户多次登录失败的时候,我们应该将账户锁定,等待一 ...