JavaScript 语言中,生成实例对象的传统方法是通过构造函数。相对于传统的面向对象语言,语义化等方面差强人意。ES6引入了 Class概念,作为对象的模板,一定程度上可以将class视为构造函数的语法糖。
|
传统构造函数 |
function Person(name,age){
this.name=name;
this.age=age;
}
Person.prototype.toString=function(){
return `My name is ${this.name} , i am ${this.age} years old.`
}
var Joe=new Person('Joe',24);
Class |
class Person{
constructor(name,age) {
this.name=name;
this.age=age;
}
toString(){
return `My name is ${this.name} , i am ${this.age} years old.`
}
}
const Joe=new Person('Joe',24)
ES6 的类,完全可以看作构造函数的另一种写法。
class Person{}
console.log(typeof Person);//function
console.log(Person===Person.prototype.constructor);//true
特性 |
1.class 内定义的成员方法都是在类的prototype上的 ,如果是箭头函数则是定义在类实例上。
2.class 内声明的方法直接写方法名,使用function声明报错 。也无法在class内显示声明变量。
3.class 内部所有定义的方法,都是不可枚举的(non-enumerable)。
class Teacher{
say(){}
do(){}
run=()=>{}
}
Object.keys(Teacher.prototype);//[]
Object.getOwnPropertyNames(Teacher.prototype);//["constructor", "say","do"]
4.通过Object.assign()
方法可以向class批量添加方法。
Object.assign(Person.prototype, {
toString(){},
toValue(){}
});
5.类必须使用new
调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new
也可以执行。
constructor() |
1.constructor()
是类的默认方法,通过new
命令生成对象实例时,自动调用该方法。如果没有显式定义,一个空的constructor()
会被默认添加。
2.constructor()
默认返回实例对象(this),因此完全可以显示return obj;
以此改变默认返回的对象。
class Foo {
constructor() {
return Object.create(null);
}
}
console.log(new Foo() );//{}
console.log(new Foo() instanceof Foo);//false null没有原型对象
3.创建实例的时候constructor()
等同于构造函数,代码块内this
指向的属性都会作为实例自身的属性。(没有修改默返回对象情况下)
4.实例属性除了定义在constructor()
里面的this上面,也可以定义在类的最顶层。
class Foo{
bar = 1;
baz = 2;
fun(){}
}
new Foo();// Foo {bar: 1, baz: 2}
可见定义在顶层的属性实则还是放进了constructor()
中执行的。
取值函数 getter 和 设值函数 setter |
取值函数和设值函数,就是get set属性描述符。因为是方法的形式,所以也是定义在prototype上的。
class Teacher{
constructor(name) {
this._name=name;
}
get privateName(){
return this._name;
}
set privateName(name){
this.privateName=_name;
}
}
const Joe=new Teacher('Joe');
console.log(Joe.privateName);//Joe
console.log(Object.getOwnPropertyDescriptor(Teacher.prototype,"privateName"));
//{enumerable: false, configurable: true, get: ƒ, set: ƒ}
class表达式 |
声明 |
const MyClass = class { /* ... */ };
立即执行 |
const MyClass = new class { /* ... */ }();
特殊 |
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
//let in = new Me();//报错
let inst = new MyClass();
console.log(inst.getClassName());
这个类的名字是Me,但是Me只在 Class 的内部可用,指代当前类。在 Class 外部,这个类只能用MyClass引用。
class需要注意的地方 |
严格模式 |
类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。
class不能提升 |
class 声明的类名不存在变量提升。
new Foo(); // ReferenceError
class Foo {}
this 的指向 |
类的方法内部如果含有this
,它默认指向类的实例。但是必须非常小心,一旦单独使用该方法,很可能因为this
指向丢失报错。
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
//logger.printName(); //Hello there
const { printName } = logger;//传递了引用
printName();//报错 Uncaught TypeError: Cannot read property 'print' of undefined
logger.printName()
是正常打印的,因为printName()
使用的this
指向的就是logger实例对象。
const printName =logger.printName
建立一个指向logger.printName
方法体的引用。printName()
执行的时候,依赖的上下文对象是全局对象。相当于要把全局对象传递进去当成this
,然而,严格模式下全局环境禁止 this
指向全局对象,而是指向undefined
的。
📌构造函数模拟上述class
function Logger(){
}
Logger.prototype.printName=function(name = 'there'){
console.log(this);//window
this.print(`Hello ${name}`);
}
Logger.prototype.print=function(text){
print(text);
}
const logger = new Logger();
const { printName } = logger;
printName();//执行了window.print() 触发页面打印功能
📌解决class 方法调用失去上下文(this
)问题
一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
// ...
}
创建实例的时候,调用构造方法时候已经绑定了printName 方法的this就是实例对象了。
另一种解决方法是使用箭头函数。
|
类的普通的方法都是定义在类的原型上的,方法会被继承,需要通过类的实例对象来调用。
特征 |
1.类的静态方法则是定义在类上的,方法不会被继承,直接通过类调用。
class Foo{
static fun(){console.log('static');}
bar(){}
}
console.dir(Foo)//class Foo
Foo.fun();//static
2.该方法没被继承,隶属于类对象的,无法被实例调用。
new Foo().fun();//Uncaught TypeError: (intermediate value).fun is not a function
3.如果静态方法包含this
关键字,这个this
指的是类,而不是实例。
4.静态方法可以与非静态方法重名。
class Foo {
static bar() {
this.baz();
}
static baz() {
console.log('hello');
}
baz() {
console.log('world');
}
}
Foo.bar() // hello
静态属性 |
静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。
两种方式:1.class 内static声明 2.class 外像普通对象一样加属性
class Foo{
bar = 1;
static baz=2
}
Foo.bac=3
console.log(new Foo())// Foo {bar: 1}
console.log(Foo.bac,Foo.baz);//3 2
|
ES6 不提供私有方法和私有属性,只能通过变通方法模拟实现。
私有方法和私有属性,指的是只能在类的内部访问的方法和属性,外部不能访问。
声明式的私有 |
变量名方法名前面加下划线,告诉别人只是私有的,一种约定俗成的做法!明显,这样做并不真正的私有,掩人耳目罢了!
class Foo{
_bar = 1;
_baz(){}
}
使用Symbol类型的属性名 |
这个只要编码者不透露信息给使用者捕捉并解读到,就相当于私有了
const bar=Symbol()
const baz=Symbol()
class Foo{
[baz] = 1;
[bar](){}
}
let foo=new Foo()
console.log(foo);//Foo {Symbol(): 1}
console.log(Reflect.ownKeys(foo))//[Symbol()]
|
Class 可以通过extends关键字实现继承, ES5 则通过修改原型链实现继承。
继承机制 |
ES5 的继承,实质是先创造子类的实例对象this
,然后再将父类的方法添加到this
上面Parent.apply(this)
。
ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到子类的this
上面(所以必须先调用super
方法),然后再用子类的构造函数修改this
。
1.子类必须在constructor()中调用super(),否则新建实例时会报错。
因为子类自己的this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super()
,子类就得不到this
对象。
class Parent{
constructor(name){
this.name=name
}
}
class Son extends Parent{
constructor(name,age) {
super(name);
this.age=age
}
}
let s=new Son('Joe',25);
2.子类没有显示定义constructor方法,会在默认构造方法调用super方法。
class Son extends Parent{
}
// 等同于
class Son extends Parent{
constructor(...args) {
super(...args);
}
}
3.在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。
class Son extends Parent{
constructor(name,age) {
console.log(this.name);//Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
super(name);
this.age=age
}
}
4.Object.getPrototypeOf()
判断一个类是否继承了另一个类。
console.log(Object.getPrototypeOf(Son)===Parent);//true
super |
super
这个关键字,既可以当作函数使用,也可以当作对象使用。
super作为函数 |
代表父类的构造函数,只能用在子类的构造方法中。
class A {}
class B extends A {
constructor() {
super();
}
}
super
虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super
内部的this
指的是B的实例,因此super()
在这里相当于A.prototype.constructor.call(this)。
super作为对象 |
在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
class Father{
constructor(){
this.other=1;
}
}
class Son extends Father{
constructor() {
super();
console.log(super.other);//undefined
}
}
console.log(new Son());
other属性是定义在父类的实例对象上的(constructor方法是用来创建实例的,this指向实例对象),而在普通方法中super是指向原型对象的。super指向Father.prototype。
在子类普通方法中通过super调用父类的方法时,父类方法内部的this也指向当前的子类实例。`super.say.call(this)`
class Father{
x=1;
say(){
console.log(this.x);
}
}
class Son extends Father{
x=2;
speak(){
super.say()
}
}
let boy=new Son();
boy.speak();//2
子类对super的属性的读取操作:读取得是父类原型属性,写入的是子类实例
class Father{
x=1;
say(){
console.log(this.x);
}
}
class Son extends Father{
constructor(arg) {
super();
this.x=2;
super.x=3;
console.log(this.x);//3
console.log(super.x);//undefined
}
}
let boy=new Son();
super在静态方法之中指向父类自身,在普通方法之中指向父类的原型对象。
class Father{
x=1;
static x=2;
static say(){
console.log('c',this.x);
}
}
class Son extends Father{
x=3
static speak(){
console.log('a',super.x);//读取父类自身属性x
console.log('b',this.x);//读取子类自身属性x
super.say()//父类静态方法中的this是指向子类自身
}
}
Son.speak();
console.dir(Son)
b 2 原因是Son类自身没有x属性,会往原型链查找该变量,找到父类身上的x,就是2
改一下:
class Son extends Father{
static x=3
static speak(){
console.log('a',super.x);//读取父类自身属性x
console.log('b',this.x);//读取子类自身属性x
super.say()//父类静态方法中的this是指向子类自身
}
}
Son.speak();
console.dir(Son)
原生构造函数的继承 |
- Boolean()
- Number()
- String()
- Array()
- Date()
- Function()
- RegExp()
- Error()
- Object()
class myArray extends Array{
constructor(...args) {
super(...args)
}
//自定义方法 获取对象数组特定key对应的值数组
getMappingValueArrayOfKey(keyName){
if(Object.prototype.toString.call(this)=='[object Array]'){
return this.map((item,index)=>{
return item[keyName]
})
}
return 'null(参数一应为对象数组)';//不是数组
}
}
const arr=new myArray({name:'dog',sound:'汪汪'},{name:'cat',sound:'喵喵'})
arr.push({name:'pig',sound:'哼哼'});
console.log(arr);//[{…}, {…}, {…}]
console.log(arr.getMappingValueArrayOfKey('name'));//["dog", "cat", "pig"]
参考文档:
Class 的基本语法