Class基本语法
语法
class MyClass {
constructor() {...}
method() {...}
}
// 通过new实例化,自动调用constructor,因此可以在这里初始化对象
new MyClass()
class User {
constructor(name) {
this.name = name;
}
sayHi() {
alert(this.name);
}
}
// 用法:
let user = new User("John");
user.sayHi();
注意:类的方法之间无逗号
什么是class
实际上就是函数
class User {
constructor(name) { this.name = name; }
sayHi() { alert(this.name); }
}
// 佐证:User 是一个函数
alert(typeof User); // function
class可以认为就是语法糖,他做了哪些?
- 创建一个名为User的函数,函数的代码实际上来自于
constructor
里。 - 类中的方法,在
prototype
里。比如上面的User.prototype.sayHi
typeof User // function
User.prototype.constructor === User // true
Object.getOwnPropertyNames(User.prototype) // [constructor, sayHi]
不仅仅是语法糖!
表面上看是一样,上面的代码用纯函数重写
// 1. 创建构造器函数
function User(name) {
this.name = name;
}
// 函数的原型(prototype)默认具有 "constructor" 属性,
// 所以,我们不需要创建它
// 2. 将方法添加到原型
User.prototype.sayHi = function() {
alert(this.name);
};
// 用法:
let user = new User("John");
user.sayHi();
差异
必须new调用
class创建的函数,会有内部属性标记[[IsClassConstructor]]
class User {
constructor() {}
}
alert(typeof User); // function
User(); // Error: Class constructor User cannot be invoked without 'new'
类上的方法不可枚举
即方法的属性描述 enumerable: false
,所以,通过for in也无法遍历出不可枚举属性。
但纯函数,除了 constructor
属性,其他原型方法都是可枚举的。
类使用user strict
类表达式
// 类似 匿名函数
let User = class {
sayHi() {
alert("Hello");
}
};
// 也可以具名,但类的名外部不可见,内部可见
let User = class My {
say() {alert(My)}
}
new User().say() // 正常运行,打印类的字符串化内容
alert(My) // error
动态创建类
有啥不可以的呢?!
function makeClass(name) {
return class {
sayHi() {
alert(name)
}
}
}
let User = makeClass('Jack')
User.sayHi() // Jack
Getters/setters
类的方法本质就是在 prototype
上挂载方法,所以 setter/getter
也当然可以。
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 is too short.
class字段(属性)
类可以有方法,也可以有属性(类字段),实际上就是 constructor
里初始化类属性的操作 this.name = 'jack'
class User {
name = 'jack'
say() {alert(this.name}
}
new User().say() // jack
利用类字段制作绑定方法
// 类方法传递时丢失this
class Button {
constructor(value) {
this.value = value;
}
click() {
alert(this.value);
}
}
let button = new Button("hello");
setTimeout(button.click, 1000); // undefined
// 2个方式解决
// 一、包装函数setTimeout(() => button.click(), 1000)
// 二、类字段形式
class Button {
constructor(value) {
this.value = value;
}
click = () => {
alert(this.value);
}
}
let button = new Button("hello");
setTimeout(button.click, 1000); // hello
// 原因:
// 1. 当类进行实例化时,会创建一个新对象,并将该对象分配给this。
// 2. 因此Button类实例化,click属于每个实例自身的属性
// 3. 又是箭头函数,无自己的this。
例子
类写法实现一个闹钟,这个代码的精髓在于模板的替换思想,而不是根据传入的模板类型来拼接,更灵活
class Clock {
constructor({temp}) {
this.temp = temp
}
render() {
let date = new Date()
let h = `${date.getHours()}`.padStart(2,'0')
let m = `${date.getMinutes()}`.padStart(2,'0')
let s = `${date.getSeconds()}`.padStart(2,'0')
let output = this.temp
.replace('h', h)
.replace('m', m)
.replace('s', s)
console.log(output)
}
}
let c = new Clock({temp: 'h:m'}) // 01:02
let b = new Clock({temp: 'h:m:s'}) // 01:02:58
类继承
extends关键字
class Animal {
constructor(name){
this.name = name
}
run() {
`${this.name} can run`
}
stop() {
`${this.name} can stop`
}
}
let animal = new Animal('isAnimal')
// 现在有兔子,继承自animal,依旧可以使用动物类具备的方法
class Rabbit extends Animal {
hide() {
alert(`${this.name} hides!`);
}
}
let rabbit = new Rabbit("White Rabbit");
rabbit.run() // White Rabbit can run
rabbit.stop() // White Rabbit can stop
做了什么
通过 extends
,将 Rabbit.prototype
设置为了 Animal.prototype
。
上图中可以看到 extends
做的事,而 new
出的兔子实例对象,其 prototype
指向的是其类的原型 Rabbit.prototype
。
所以查找方法的路径是:查找 rabbit
实例自身 -> 查找 Rabbit.prototype
-> 查找 Animal.prototype
重写方法
上面的Animal和Rabbit,后者此时如果希望有自己的stop方法,应该怎么做?因为Animal已经有stop方法了
一般来说,不希望完全替换父类方法,而是拓展或调整
因此ES6 Class提供了 super
关键字
class Rabbit extends Animal {
stop() {
super.stop()
// do others
}
}
// 此时声明的实例,调用stop,将执行Animal类中的stop方法,再执行others
箭头函数没有super
当在箭头函数内调用 super
, super
其实是外层的 super
,如下:
class Rabbit extends Animal {
stop() {
// 这里super是外层的,也就是Rabbit类方法stop中的,super指向父类,也就是Animal
setTimeout(() => super.stop(), 1000);
}
}
但如果在上例中使用了普通函数,则报错: Uncaught SyntaxError: ‘super’ keyword unexpected here
class Rabbit extends Animal {
stop() {
// 这里super并不是外部Rabbit类方法stop的,而且普通函数自己的,但是他没有继承自谁,只是普通函数,不能使用。
setTimeout(function() {super.stop()}, 1000);
}
}
重写constructor
子类无constructor
根据规范,子类没有自己的constructor的话,会默认使用父类的构造。
class Son extends Parent {
// 生成如下
constructor(...args) {
super(...args)
}
}
子类有constructor
必须要调用父类构造函数,其次,才能制定自己独有的属性。
class Son extends Parent {
constructor(name){
super(name) // 第一步,必须
this.x = 0 // 其次
}
}
内部属性[[ConstructorKind]]: “derived”
这是一个特殊内部标签,影响 new
构造实例行为。
- new时,将创建一个新对象,且将空对象赋值给this
- 当继承的类(子类)的constructor执行时,不会执行上述操作,而是期待父类的constructor来完成上一步的工作。
所以如下代码,没有生成新对象,没有this:
class Son extends Animal {
constructor(name) {
this.name = name // 缺少调用父类构造,没有新对象生成,不存在this
}
}
报错信息:Uncaught ReferenceError: Must call super constructor in derived class before accessing ‘this’ or returning from derived constructor
重写类字段
父类和子类的构造调用 和 初始化(比如 this.name = name
这类属性初始化)的顺序不一样。
父类是先调用构造函数,再初始化。子类恰好相反,子类在 super
调用后再初始化,这种顺序差异,会导致字段被父类构造器使用时会出现异常!
class Animal {
name = 'animal';
constructor() {
alert(this.name); // 父类构造函数中访问了this.name
}
}
class Rabbit extends Animal {
name = 'rabbit'; // 子类重写了name字段。但如上所说,父类构造函数在被子类继承,执行super时,子类的name = 'rabbit'还没运作,所以访问的是父类的name
}
new Animal(); // animal
new Rabbit(); // animal
super的内部机制
比如super如何知道是调用父类的方法?难道是 son.__proto__.fn
?
// 原型链来实现super的设想,有很大问题,会造成无限循环查找
let animal = {
name: 'animal',
eat() {
this.name
}
}
let rabbit = {
__proto__: animal,
name: 'Rabbit',
eat() {
// super.eat的工作方式,猜想利用原型链
// 这里需要call(this),否在this.__proto__.eat(),this变为原型animal了
this.__proto__.eat.call(this)
}
}
rabbit.eat() // Rabbit 调用原型对象里的eat方法,且绑定了this指向rabbit,看起来没问题
但如果在原型链上在加一个对象,就有问题了。
let longEar = {
__proto__: rabbit,
eat() {
this.__proto__.eat.call(this)
}
}
longEar.eat()
// this此时是longEar
// 所以longEar.__proto__.eat.call(longEar)
// => rabbit.eat.call(longEar)
// 在rabbit中的eat如下
// longEar.__proto__eat.call(longEar)
// => rabbit.eat.call(longEar) 这里绕回来了,死循环!
揭秘
内部使用了 `[[HomeObject]]`` ,一个内部属性,当一个函数被定义为类或者对象方法时,该属性绑定为该对象
let animal = {
name: "Animal",
eat() { // animal.eat.[[HomeObject]] == animal
alert(`${this.name} eats.`);
}
};
let rabbit = {
__proto__: animal,
name: "Rabbit",
eat() { // rabbit.eat.[[HomeObject]] == rabbit
super.eat();
}
};
let longEar = {
__proto__: rabbit,
name: "Long Ear",
eat() { // longEar.eat.[[HomeObject]] == longEar
super.eat();
}
};
// longEar.eat() this指向longEar
// => super.eat 内部:这里使用了super,所以查找longEar.eat.[[HomeObject]]的原型里的eat方法,longEar.eat.[[HomeObject]]指向longEar,它的原型是rabbit
// rabbit.eat()
// => super.eat 内部:这里使用了super,查找rabbit.eat.[[HomeObject]]的原型里的eat方法,rabbit.eat.[[HomeObject]]指向rabbit,它的原型是animal
// animal.eat()
// => this.name,因为是longEar.eat(),所以this是longEar,打印Long Ear eats.
方法不是自由的了
因为有 super
的存在,可以绑定一个上下文,且不可修改。它没有使用 this
的机制,调用时获取上下文。
只使用方法!
super
只能用于方法,函数属性中使用会报错!
let animal = {
eat: function() {} // 对象的 函数属性 eat
}
let animal = {
eat() {} // 对象的方法 eat
}
静态属性和静态方法
static
通过关键字 static
来声明静态属性和方法
class Animal {
static type = 'animal'
}
Animal.type // animal
// 效果同:
class Animal {}
Animal.type = 'animal'
静态方法用法
工厂方法?
class Animal {
constructor(name) {
this.type = `${name} is am animal`
}
static createAnimal(name) {
return new this(name)
}
}
let a = Animal.createAnimal('rabbit')
a.title // rabbit is am animal
可继承
静态方法属性相当于在类上直接挂在(函数的话,就是函数上直接挂)
class Aniaml {
static type = 'animal'
say() {}
}
class Rabbit extends Animal {}
// extends的关联了2个原型!!!
// Rabbit.__proto__ === Aniaml
// Rabbit.__proto__.__proto__ === Animal.prototype
// 所以
Rabbit.type // animal
Rabbit.prototype.say // say() {}
巩固
// 说好的JS中万物皆对象(所有对象都继承自Object.prototype),那下面2者有区别?
class Animal extends Object {}
class Animal {}
// 有区别
// 前者:
Aniaml.__proto__ === Object
Animal.prototype.__proto__ === Object.prototype
// 后者:
Animal.__proto__ === Function.prototype
// class本质也是函数,所有函数的原型都继承自Function.prototype,就像所有的数组继承自Array.prototype,数字继承自Number.prototype一样。
私有的和受保护的属性和方法
受保护的
JS中约定以_开头,允许类自身,继承的类的内部来方法。但目前JS没有提供语言上的能力保障受保护的字段,也就是说,可以直接修改它。
class Parent {
_smoke = true
}
let p = new Parent()
p._smoke = false; // ok的
私有的
JS语言级支持,以 #
开头。不能从类的外部(实例)访问,继承的类也不行。
不支持通过 this['#smoke']
来访问,语法层面限制,只能是 this.#smoke
扩展内建类
内建类可以扩展
扩展后的内建类具有【传递性】。
class PowerArray extends Array {
isEmpty() {
return this.length = 0
}
}
let arr = new PowerArray(1,2,3)
arr.isEmpty() // false
//arr也可以继续使用内建Array的原型方法
let filterArr = arr.filter(item => item >= 2) // [2,3]
filterArr.isEmpty() // false
// 这里很有趣,注意,filter,map等返回的新数组,使用的是 arr.constructor 也就是 PowerArray来生成新的数组的。所以filterArr 依旧是 PowerAarry 的实例。
filterArr instanceof PowerArray // true
filterArr.__proto__ === PowerArray.prototype // true
如果不想有这种传递行为,希望 filter
map
使用内建 Array
来生成的话,利用 Symbol.species
,一个特殊的静态属性,当调用 filter
map
这类内建方法时,会使用该静态属性指定的内(构造函数)
class PowerArray extends Array {
isEmpty() {
return this.length === 0;
}
// 内建方法将使用这个作为 constructor
static get [Symbol.species]() {
return Array;
}
}
let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false
let filterArr = arr.filter(item => item >= 10)
// filter 触发Symbol.species
fitlerArr.isEmpty() // isEmpty is not a function 。看不生效了
内建类没有继承静态方法
在静态属性和静态方法一节里,有说过静态方法和属性的继承是这么实现的
class A {
static type = 'A'
}
class B extends A {}
let b = new B() // 注意不是实例b.type === 'A'
B.type === 'A' // true
JS中一切都是对象,所以Array这些扩展自Object。复习一下:
Array.prototype.__proto__ === Object.prototype // true
Array.__proto__.__proto__ === Object.prototype // true
// 看来Array.prototype === Array.__proto__ // false
// 错!
// 函数的原型都是继承自函数原型对象
Array.__proto__ === Function.prototype // true
// 而对象都继承自Object原型,prototype是函数的一个普通属性(没什么特别的,声明时就有了,可以被改写,覆盖)
// 所以可以如下
Function.prototype.__proto__ === Object.prototype
// 而下面,仅仅是Array的一个普通对象,是所有数组实例的原型对象。
Array.prototype
Array.key
并不存在,所以Array并没有继承Object的静态方法,同样, Date.key
之类的静态方法也不存在。
类检查
instanceof
语法: obj instancof class
,也包括了原型链检查。
class A {}
let a = new A()
a instanceof A // true
class B extends A {} // 这里B.prototype.__proto__ === A.prototype
let b = new B()
b instanceof B // true 因为b.__proto__ == B.prototype。
b instanceof A // true 因为b.__proto__.__proto__ === A.protoype
Symbol.hasInstance
instanceof的检查,会去调用该方法,如果没有该方法,则走标准的逻辑:检查 class.protoype是否是obj的原型链中的原型之一
。
我们来看下有 hasInstance
时
class Animal {
static [Symbol.hasInstance](obj) {
if(obj.canEat) {return true}
}
}
// 只是声明了一个跟Animal毫无关系的普通对象
let obj = {canEat: true}
obj instanceof Animal // true 因为触发了hasInstance
isPrototypeOf
跟 instanceof
类似,也会检查原型链上是否有。语法: class.protoype.isPrototypeOf(obj)
最安全的Object.prototype.toString
调用则抽取成如下结构: [object Number]
Object.prototype.toString.call([]) === '[object Array]'
Mixin
mixin
是可被其他类使用,但无需继承的方法的类。
JS里面原型链模式规定,每个对象只能有一个原型对象,每个类只可以扩展另外一个类,也就是单继承。
一个Mixin实例
构造一个拥有实用方法的对象,复制到需要拓展的类的原型中。
// 这是一个mixin,根据维基百科的定义,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);
// 现在 User 可以打招呼了
new User("Dude").sayHi(); // Hello Dude!
mixin内部继承
let sayMixin = {
say(name) {
alert(name)
}
}
let sayHiMixin = {
__proto__: sayMixin // 这里设置了sayHiMixin的原型对象
sayHi() {
super.say(`Hi ${this.name}`)
}
}
class User {
constructor(name){
this.name = name
}
}
User.__proto__ = sayHiMixin
new User('Jack').sayHi()
// User.prototype ->
// 找到sayHi ->
// 利用[HomeObject]找到其sayMixin 中的 say方法。
注意mixin 同名方法造成的冲突,因此需要注意命名。