ES6语法中的class、extends与super的原理
class
首先, 在
JavaScript中,class类是一种函数
class User {
constructor(name) { this.name = name; }
sayHi() {alert(this.name);}
}
alert(typeof User); // function
class User {…} 构造器内部干了啥?
- 创建一个以
User为名称的函数, 这是类声明的结果(函数代码来自constructor中) - 储存所有方法, 例如
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();
两者存在重大差异
首先,通过
class创建的函数是由特殊内部属性标记的[[FunctionKind]]:"classConstructor"。不像普通函数,调用类构造器时必须要用new关键词:class User {
constructor() {}
} alert(typeof User); // function
User(); // Error: 没有 ‘new’ 关键词,类构造器 User 无法调用此外,大多数 JavaScript 引擎中的类构造函数的字符串表示形式都以 “class” 开头
class User {
constructor() {}
} alert(User); // class User { ... }方法不可枚举。 对于
"prototype"中的所有方法,类定义将enumerable标记为false。这很好,因为如果我们对一个对象调用 for..in 方法,我们通常不希望 class 方法出现。
枚举实例属性时, 不会出现class方法; 而普通创建的构造函数, 枚举实例属性时会出现prototype上的方法。
类默认使用
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 中创建 getters 和setters,示例:
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]]的引用
Rabbit方法原型继承自Animal方法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.prototype到Animal.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关键字提供了上述功能
- 执行
super.method(…)调用父类方法; (借用并改造父类方法, 生成自己的方法) - 执行
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 方法
}
}
因此,箭头函数中的super与stop()中的是相同的,所以它能按预期工作。但如果我们在这里指定一个"普通"函数,那么将会抛出错误: (找不到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,而并没有在原型链上向上寻找方法。
在
longEar.eat()中,(**)这一行调用rabbit.eat并且此时this=longEar。// 在 longEar.eat() 中 this 指向 longEar
this.__proto__.eat.call(this) // (**)
// 变成了
longEar.__proto__.eat.call(this)
// 即等同于
rabbit.eat.call(this);之后在
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);…所以
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的方法从一个对象复制到另一个对象是不安全的。
补充:
- 箭头函数没有自己的
this或super,所以它们能融入到就近的上下文,像透明似的。
class Rabbit与class 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 也有一些函数的通用方法,比如 call、bind 等等。在上述的两种情况下他们都是可用的,因为对于内置的 Object 构造函数来说,Object.__proto__ === Function.prototype。(所有函数都是默认如此)
因此class Rabbit与class 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的原理的更多相关文章
- 如何理解 Java 中的 <T extends Comparable<? super T>>
Java 中类似 <T extends Comparable<? super T>> 这样的类型参数 (Type Parameter) 在 JDK 中或工具类方法中经常能看到. ...
- 理解 ES6 语法中 yield 关键字的返回值
在 ES6 中新增了生成器函数的语法,本文解释了生成器函数内 yield 关键字的返回值. 描述 根据语法规范,yield 关键字用来暂停和继续执行一个生成器函数.当外部调用生成器的 next() 方 ...
- 理解 ES6 语法中 yield* 关键字的作用
在 ES6 中新增了生成器函数的语法,本文解释了与生成器函数有关的 yield* 关键字,及其使用场景. 描述 根据语法规范,yield* 的作用是代理 yield 表达式,将需要函数本身产生(yie ...
- es6语法中promise的使用方法
Promise是一个构造函数,它有resolve,reject,race等静态方法;它的原型(prototype)上有then,catch方法,因此只要作为Promise的实例,都可以共享并调用Pro ...
- es6语法中的arrow function=>
(x) => x + 相当于 function(x){ ; }; var ids = this.sels.map(item => item.id).join() var ids = thi ...
- ES6语法:函数新特性(一)
ES6 函数 引言: 函数在任何语言中偶读很重要,java里面的函数通常叫做方法,其实是一个东西,使用函数可以简化更多的代码,代码结构看着更加清晰.今天我们来学学ES6语法中,函数有什么变化. 虽然现 ...
- [ES6]react中使用es6语法
前言 不论是React还是React-native,facebook官方都推荐使用ES6的语法,没在项目中使用过的话,突然转换过来会遇到一些问题,如果还没有时间系统的学习下ES6那么注意一些常见的写法 ...
- 浅谈Java泛型中的extends和super关键字(转)
通配符 在本文的前面的部分里已经说过了泛型类型的子类型的不相关性.但有些时候,我们希望能够像使用普通类型那样使用泛型类型: 向上造型一个泛型对象的引用 向下造型一个泛型对象的引用 向上造型一个泛型对象 ...
- .vue文件在webstorm中es6语法报错解决方法
1 语法支持es6设置 Preferences > Languages & Frameworks > JavaScript 把 Javascript Language versio ...
随机推荐
- python:模块1——标准库简介
一.文档 windows系统:IDLE中打开帮助文档 Tutorial:简单入门 Library Reference:python内置函数和标准库(看不完的,当做字典来查)(此外还有pypi(拍派社区 ...
- ZeroC ICE的远程调用框架 AMI与AMD -Why?
在Ice有两种异步使用的方式,AMI和AMD.AMI是异步方法调用,AMD是异步方法调度(分派).前者用在代理端,后者用在饲服实现端. AMI其实就是在代理端,使用Future机制进行异步调用,而不阻 ...
- SQL Server设计三范式
第一范式(1NF) (必须有主键,列不可分) 数据库表中的任何字段都是单一属性的,不可再分 create table aa(id int,NameAge varchar(100)) insert aa ...
- deepin安装pip
sudo apt install python3-venv python3-pip 升级最新版 pip3 install --upgrade pip 更新完以后就报错网上的解决办法没有好使的 退回版 ...
- Django使用mysql数据的流程
创建一个mysql数据库 1.打开终端(cmd): 输入: mysql -uroot -p 密码:*** 输入: create database 数据库名字; 2.在settings中进行配置 DAT ...
- Hadoop运行模式
Hadoop运行模式 (1)本地模式(默认模式): 不需要启用单独进程,直接可以运行,测试和开发时使用. 即在一台机器上进行操作,仅为单机版. 本地运行Hadoop官方MapReduce案例 操作命令 ...
- 2019-11-20:xss学习笔记
xxe漏洞防御使用开发语言提供的禁用外部实体的方法phplibxml_disable_entity_loader(true); 卢兰奇对象模型,bom由于现代浏览器实现了js交互性方面的相同方法和属性 ...
- SpringBoot学习(六)—— springboot快速整合RabbitMQ
目录 Rabbit MQ消息队列 简介 Rabbit MQ工作模式 交换机模式 引入RabbitMQ队列 代码实战 Rabbit MQ消息队列 @ 简介 优点 erlang开发,并发能力强. 社区活跃 ...
- 1sql
------------------ MySQL 服务-- sudo service mysql start/stop/restart/status ------------------ 数据库相关的 ...
- 01-tornado学习笔记-Tornado简介
01-Tornado简介 Tornado是使用Python编写的一个强大的.可扩展的Web服务器.它在处理严峻的网络流量时表现得足够强健,但却在创建和编写时有着足够的轻量级,并能够被用在大量的应用 ...