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. MySQL InnoDB 实现高并发原理

    MySQL 原理篇 MySQL 索引机制 MySQL 体系结构及存储引擎 MySQL 语句执行过程详解 MySQL 执行计划详解 MySQL InnoDB 缓冲池 MySQL InnoDB 事务 My ...

  2. pat 1013 Battle Over Cities(25 分) (并查集)

    1013 Battle Over Cities(25 分) It is vitally important to have all the cities connected by highways i ...

  3. pat 1124 Raffle for Weibo Followers(20 分)

    1124 Raffle for Weibo Followers(20 分) John got a full mark on PAT. He was so happy that he decided t ...

  4. 函数的prototype

    1.函数的prototype属性 每一个函数都有一个prototype属性,默认指向object空对象(原型对象),每一个原型对象都有一个constructor属性,指向函数对象 2.给原型对象添加属 ...

  5. vue项目页面切换到默认显示顶部

    页面切换到默认显示顶部 方法一 使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样. vue-router 能做到,而且更好,它让你可以自定义路由切换时页 ...

  6. windows系统两个网卡如何同时使用?内网和外网共用,配置教程

    有些企业的网络分为内网和外网,一般内网不能访问Internet,但是又不提供外网,自己有上外网的需求,所以可能会用到双网卡,无线网卡+有线网卡或者有线+有线... 本文就是为了解决此问题,主要的做法就 ...

  7. postgresql12 b-tree v4空间上和性能上的优化

    在 pg v11 和 v12 上 常见测试用例 CREATE TABLE rel ( a bigint NOT NULL, b bigint NOT NULL ); ALTER TABLE rel A ...

  8. 在ensp上模拟企业网络场景并Access接口加入相应VLAN

    模拟的企业网络大概描述: 公司内网是一个大的局域网,二层交换机S1放置在一楼,在一楼办公的部门有IT部和人事部:二层交换机S2放置在二楼,在二楼办公的部门有市场部和研发部.由于交换机组成的是广播网,交 ...

  9. Windows之Java开发环境快速搭建

    说明:Node.js非必须,通常中小公司或创业公司,基本上都要求全栈. 补充说明: 除此之外,当公司固定JDK.Maven.Idea.Git.Node.js及其相关IDE等版本时,运维人员或者Team ...

  10. Spring IOC 的简单使用

    Spring IOC (Inversion Of Control反转控制容器 一.对于IOC容器的简单理解 在java开发中将程序中的对象交给容器管理,而不是在对象的内部管理. 那么两个简单的问题去分 ...