1.简介
JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。
1.jpg
上面这种写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习这门语言的程序员感到困惑。
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。
基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 ES6 的class改写,就是下面这样。
2.jpg上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而 this关键字则代表实例对象。也就是说,ES5 的构造函数Person,对应 ES6 的Person类的构造方法。
Person类除了构造方法,还定义了一个toString方法。注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。
上面代码表明,类的数据类型就是函数,类本身就指向构造函数。
使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。
构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。
在类的实例上面调用方法,其实就是调用原型上的方法。
prototype对象的constructor属性,直接指向“类”的本身,这与 ES5 的行为是一致的。
1.jpg另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable)。
上面代码中,toString方法是Person类内部定义的方法,它是不可枚举的。这一点与 ES5 的行为不一致。
1.jpg上面代码采用 ES5 的写法,toString方法就是可枚举的。
2.严格模式
类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。
考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。
3.constructor 方法
constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。
// 定义类
class Person{
constructor(name,age){
this.name=name;
this.age=age;
console.log(2213) // 2213
}
}
let p1=new Person('Andy',31)
可以看到控制台打印了2213, 证明通过new命令生成实例对象时,自动调用constructor方法
另外,类里面的this指代的都是实例对象,请看下面这个例子
// 定义类
class Person{
constructor(name,age){
this.name=name;
this.age=age;
}
toString(){
console.log(this)
}
}
let p1=new Person('Andy',31) // Person {name: "Andy", age: 31}
p1.toString()
一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
1.jpg上面代码中,定义了一个空的类Person,JavaScript 引擎会自动为它添加一个空的constructor方法。
constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。
1.jpg上面代码中,constructor函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。
类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。
4.类的实例对象
生成类的实例对象的写法,与 ES5 完全一样,也是使用new命令。
需要注意的是,需要加上new,如果忘记加上new,像函数那样调用Class,将会报错。
1.jpg与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。
toString
上面代码中,定义了一个 Person类,constructor中的sex
和age
是new出来的实例对象的属性,打印 Person.hasOwnProperty('sex')结果是false
, 打印 p1.hasOwnProperty('sex')结果是true
。 这也证明了 constructor中的this指代的是实例对象。
与 ES5 一样,类的所有实例共享一个原型对象。
上面代码中,p1和p2都是Person的实例,它们的原型都是Person.prototype,所以 _proto_属性是相等的。
这也意味着,可以通过实例的_proto_属性为“类”添加方法。
1.jpg上面代码在p1的原型上添加了一个printName方法,由于p1的原型就是p2的原型,
因此p2也可以调用这个方法。而且,此后新建的实例p3也可以调用这个方法。
这意味着,使用实例的_proto属性改写原型,必须相当谨慎,
不推荐使用,因为这会改变“类”的原始定义,影响到所有实例。
5.class表达式
与函数一样,类也可以使用表达式的形式定义。
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
上面代码使用表达式定义了一个类。
需要注意的是,这个类的名字是MyClass而不是Me,Me只在 Class 的内部代码可用,指代当前类。
6.不存在变量提升
类不存在变量提升(hoist),这一点与 ES5 完全不同
new Foo(); // ReferenceError
class Foo {}
上面代码中,Foo类使用在前,定义在后,这样会报错,因为 ES6 不会把类的声明提升到代码头部。这种规定的原因与下文要提到的继承有关,必须保证子类在父类之后定义。
{
let Foo = class {};
class Bar extends Foo {
}
}
上面的代码不会报错,因为Bar继承Foo的时候,Foo已经有定义了。但是,如果存在class的提升,上面代码就会报错,因为class会被提升到代码头部,而let命令是不提升的,所以导致Bar继承Foo的时候,Foo还没有定义。
7.私有方法和私有属性
私有方法是常见需求,但 ES6 不提供,只能通过变通方法模拟实现。
8.this的指向
类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。
1.jpg上面的例子说明,在单独执行printName这个函数的时候,由于this的指向问题,函数中的代码 this.print()当前上下文环境中找不到print这个函数,所以报错。
一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。
请看下面这个更为复杂的情况。
2.jpg这个例子中,在class Logger的constructor中我们使用了bind方法,把printName这个方法的执行上下文环境绑定到了class Logger的实例上。
bind方法是新创建一个函数,然后把它的上下文绑定到bind()括号中的参数上,然后将它返回。
所以,bind后函数不会执行,而只是返回一个改变了上下文的函数副本,而call和apply是直接执行函数。
后面的代码中,我们new了一个 Logger的实例,打印出来。
发现实例中有userName passwords 和 printName三个属性,前面两个是key value属性,最后的一个 printName是Logger实例的一个方法。
通过var {printName}=logger把printName单独拿出来,打印printName发现它是一个函数,它是实例logger的一个OwenProperty,其原型proto指向Function的prototype属性。
注意到printName._proto与实例 logger._proto显然是不等的。
它与Function的原型才相等!
前面提到,我们使用bind方法,将printName的上下文环境this改变成了class Logger的实例对象上,所以直接调用 printName方法可行了,它会打印出hello lv的信息。
1.jpg关于bind()函数
这个例子中,我们在constructor中使用了bind方法,将实例对象logger的上下文环境this绑定到了一个新的对象上面,新的对象上面也有print方法
我们把实例对象logger的print方法赋值给printName方法,单独调用printName方法时,发现打印的都是 敌法师,新的对象里面的name,sex属性值。
9.name 属性
由于本质上,ES6 的类只是 ES5 的构造函数的一层包装,所以函数的许多特性都被Class继承,包括name属性。
class Point {}
Point.name // "Point"
前面有提到过。
Person.hasOwnProperty('name');打印的是true,是因为巧合就是这个原因。实际上打印 Person.hasOwnProperty('age');打印的就是false
10.Class 的取值函数(getter)和存值函数(setter)
与 ES5 一样,在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
1.jpg上面代码中,prop属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。
存值函数和取值函数是设置在属性的 Descriptor 对象上的。
12.class的静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
1.jpg上面代码中,Person类有个静态方法hello(),可以在Person类上调用,但是不能在Person的实例上调用。
尝试 p.hello()是报错的。
注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。
1.jpg上面代码中,静态方法bar调用了this.baz,这里的this指的是Person类,而不是Person的实例,等同于调用Person.baz。
另外,从这个例子还可以看出,静态方法可以与非静态方法重名。
父类的静态方法,可以被子类继承。
静态方法也是可以从super对象上调用的。
13.class的静态属性和实例属性
静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。
// 定义类
class Person{
}
Person.prop=1;
console.log(Person.prop); // 1
上面的写法为Person类定义了一个静态属性prop。
目前,只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。
// 以下两种写法都无效
class Person {
// 写法一
prop: 2
// 写法二
static prop: 2
}
Person.prop // undefined
网友评论