ES6之class简介(一)
From 阮一峰 ECMAScript 6入门
1. 简介
在ES6之前,JavaScript模拟类来生成实例对象的经典方法是通过混合的构造函数/原型方式,如下:
function Point(x,y){
this.x=x;
this.y=y;
}
Point.prototype.toString = function(){
return ( '(' +this.x + ',' + this.y + ')');
}
const p = new Point(1,2);
而在ES6中提供了更接近面向对象的写法,引入了class(类)这个概念,作为对象的模板,通过class关键字,可以定义类。
实际上,ES6的class
只是一个语法糖,它的绝大部分功能,ES5都能实现,新的class
写法只是让对象原型的写法变得更加清晰、更像面向对象编程的语法而已。上面的代码用ES6改写,如下:
class Point{
constructor(x,y){
this.x=x;
this.y=y;
}
toString(){
return ( '(' +this.x + ',' + this.y + ')');
}
}
这里定义了一个“类”,constructor是构造方法,this关键字指向实例对象。可以把ES5的构造函数Point看作对应ES6的构造方法。
其实,ES6的类,完全可以看作构造函数的另一种写法。
class Point {
//...
}
typeof Point //'function'
Point === Point.prototype.constructor //true
上面代码表明,类的数据类型就是函数,类本身就指向构造函数。构造函数的prototype
属性,在ES6的“类”上继续存在。事实上,类的所有方法都定义在类的prototype
(原型)属性上面。
class Point {
constructor(){
// ...
}
toString(){
// ...
}
toValue(){
// ...
}
}
//等同于
Point.prototype={
constructor(){},
toString(){},
toValue(){}
}
在类的实例上调用方法,实际上就是调用原型上的方法,注:同一个类的所有实例对象共享一个原型
class A {}
const a =new A()
a.constructor === A.prototype.constructor //true
上面代码中,a是A类的实例,它的constructor方法就是A类原型上的constructor方法。
由于类的方法都定义在prototype
对象上面,所以类的新方法可以添加在prototype
对象上面。Object.assign
方法可以很方便地一次向类添加多个方法。
class Point{
constructor(){
//...
}
}
Object.assign(Point.prototype,{
toString(){},
toValue(){}
})
注意:ES6类内部定义的所有方法都是 non-enumerable (不可枚举的),这点与ES5的行为不一致。(通过Object.keys()
枚举出对象可枚举的属性,Object.getOwnPropertyNames()
返回对象自身的所有属性。返回值都以数组形式表示。)此外,类的方法名,可以采用表达式形式,由[]
括起来即可。
2. 严格模式
类和模块的内部,默认都是严格模式,不需要再显示使用use strict
指定严格模式。
3. constructor方法
constructor方法是类的默认方法,当通过new
命令生成实例对象时,会自动调用该方法。如果类中没有显示定义constructor
,则会默认添加一个空的construcotor
方法,constructor
方法默认返回实例对象this
,我们也可以自定义指定返回另外一个对象
class Foo{
constructor(){
return Object.create(null);
}
}
new Foo() instanceof Foo //false
如上,constructor
函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。
类必须通过new调用,否则会报错,这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。
4. 类的实例对象
与ES5一样,实例的属性除非显示定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class的prototype属性上)
class Point{
constructor(x,y){
this.x=x;
this.y=y;
}
toString(){
return ('('+this.x+','+this.y+')');
}
}
const p = new Point(2,3);
point.toString();// (2,3)
point.hasOwnProperty('x') //true
point.hasOwnProperty('y') //true
point.hasOwnProperty('toString') //false
point.__proto__.hasOwnProperty('toString') //true
类的所有实例都共享一个原型对象,这也意味着,可以通过实例的__proto__
属性为“类”添加方法。__proto__
并不是语言本身的特性,而是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的js引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用Object.getPrototypeOf
方法来获取实例对象的原型,然后再来为原型对象添加方法/属性。注意,修改原型上的属性必须相当谨慎,不推荐使用,因为这会改变“类”的原始定义,影响所有的实例对象。
5. class表达式
与函数一样,类也可以使用表达式的形式定义。
const MyClass = class Me {
getClassName(){
return Me.name;
}
};
上面代码使用表达式定义了一个类,注意,这个类的名字是MyClass而不是Me,Me只在class的内部代码可用,指代当前类。
const inst = new MyClass();
inst.getClassName() //Me
Me.name // ReferenceError : Me is not defined
上面代码表示,Me只在Class内部有定义,在外部使用是会报错的。所以如果类的内部没用到的话,一般情况下可以省略Me,也就是可以写成下面的形式。
const MyClass = class { /* ... */ }
采用class表达式,可以写出立即执行的class
const person = new class{
constructor(name){
this.name = name;
}
sayName(){
console.log(this.name);
}
}('张三');
person.sayName(); //‘张三’
6. 不存在变量提升
类不存在变量提升(hoist),let、const也都不存在变量提升。
7. 私有方法
私有方法是常见需求,但ES6不提供,只能通过一些变通方法模拟实现。
一种方式是在命名上加以区别
class Widget{
//公有方法
foo(baz){
this._bar(baz);
}
//私有方法
_bar(baz){
return this.sanf = baz;
}
}
上面代码中,_bar方法前面的下划线,表示这是一个只局限于内部使用的私有方法,但是,这种命名是不保险的,在类的外部,还是可以调用到这个方法。
另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的
class Widget{
foo(baz){
bar.call(this,baz);
}
}
function bar(baz){
return this.sanf = baz;
}
上面代码中,foo是公有方法,内部调用了bar.call(this,baz)
。这使得bar实际上成为了当前模块的私有方法。
还有一种方法是利用Symbol
值的唯一性,将私有方法的名字命名为一个Symbol
值,这样外部就无法获取到它们,因此达到了私有方法和私有属性的效果。
const bar = Symbol('bar')
const sanf = Symbol('sanf')
export default class MyClass{
//公有方法
foo(baz){
this[bar](baz);
}
[bar](baz){
return this[sanf] = baz;
}
}
8. 私有属性
与私有方法一样,ES6不支持私有属性。目前只是有一种提案--略
9. this的指向
类的方法内部如果含有this,它默认指向类的实例。但是,一旦单独使用该方法,this取决于该方法的调用位置,很可能会应用默认绑定,在严格模式下指向undefined
。
class Logger{
printName(name = 'ht'){
this.print(`hello ${name}`);
}
print(text){
console.log(text);
}
}
const logger = new Logger();
const {printName} = logger;
printName();//TypeError: Cannot read property 'print' of undefined
一个比较简单的解决方法是,在构造方法中绑定this
class Logger{
constructor(){
this.printName = this.printName.bind(this);
}
// ...
}
另一种解决方法是使用箭头函数
class Logger{
constructor(){
this.printName = (name = 'ht')=>{
this.print(`hello ${name}`);
}
}
}
这里有个困惑,在React中可以直接在constuctor外使用箭头函数形式定义方法,this将指向实例组件,但是,在一般定义类时直接放在外面会报错,不知道具体原因是什么。
还有一种方法是使用Proxy
,略
10. name属性
由于本质上,ES6的类只是ES5的构造函数的一层包装,所以函数的许多特性都被class继承,包括name
属性。
class Point {}
Point.name //"Point"
name属性总是返回紧跟在class关键字后面的类名
11. class的取值函数(getter)和存值函数(setter)
与ES5一样,在“类”的内部可以使用get
和set
关键字,对某个属性设置存值函数和取值函数,
class CustomElement{
constructor(element){
this.element = element;
}
get ele(){
return this.element.innerHTML;
}
set ele(value){
this.element.innerHTML = value;
}
}
const custom = new CustomElement(document.getElementById('root'))
custom.ele //id为'root'的元素的innerHTML
custom.ele = 'hello world' //修改id为‘root’的元素的
12. class的Generator方法
如果在某个方法之前加上星号(*),就表示该方法是一个Generator函数(Generator函数执行返回的是遍历器对象,也就是一个指向内部状态的指针对象,函数内遇到yield暂停,调用next方法使得指针移向下一个状态,从上次暂停的地方继续执行)
class Foo{
constructor(...args){
this.args = args ;
}
* [Symbol.iterator](){
for(let arg of this.args){
yield arg;
}
}
}
for (let x of new Foo('hello','world')){
console.log(x);
};
//hello
//world
Symbol.iterator
方法返回一个Foo类的默认遍历器,for ... of
循环会自动调用这个遍历器
13.class的静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承,如果在一个方法前,加上static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为‘静态方法’。
class Foo{
static classMethod(){
return 'hello';
}
}
Foo.classMethod() //'hello'
const foo = new Foo();
foo.classMethod();//TypeError: foo.classMethod is not a function
上面代码中,Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用,但不能在实例上调用。注意:如果静态方法中包含this关键字,这个this指向的是类,而不是实例
class Foo{
static bar(){
this.baz();
}
static baz(){
console.log('hello');
}
baz(){
console.log('world');
}
}
Foo.bar()//hello
上面代码中,静态方法bar调用了this.baz
,这里的this指得是Foo类,而不是Foo的实例对象,等同于调用Foo.baz
,另外,从这个例子还可以看出,静态方法可以与非静态方法重名。
父类的静态方法,可以被子类继承。静态方法也是从super
对象上调用的
class Foo{
static classMethod(){
return 'hello';
}
}
class Bar extends Foo{
static classMethod(){
return super.classMethod()+',too'
}
}
Bar.classMethod() //hello,too
14. class的静态属性和实例属性
静态属性指的是class本身的属性,即class.propName
,而不是定义在实例对象(this)上的属性
class Foo{
// ...
}
Foo.prop = 1;
console.log(Foo.prop) //1
上面的写法为Foo类定义了一个静态属性prop,目前,只有这种写法可行,因为ES6明确规定,class内部只有静态方法,没有静态属性。
//以下两种写法都无效
class Foo{
//写法一
prop:2
//写法二
static prop:2
}
console.log(Foo.prop) //undefined
目前有一个静态属性的提案,对实例属性和静态属性都规定了新的写法
(1)类的实例属性
类的实例属性可以用等式,写入类的定义之中
class MyClass{
myProp = 42;
constructor(){
console.log(this.myProp);
}
}
上面代码中,myProp就是MyClass的实例属性,在MyClass的实例上,可以读取这个属性。
以前,我们定义实例属性,只能写在类的constructor
方法里面
class ReactCounter extends Component{
constructor(props){
super(props);
this.state = {
count:0
}
}
}
如果使用提案中的写法,可以不在constructor
方法里面定义
class ReactCounter extends Component{
state={
count:0
}
}
这种写法比以前更清晰
为了可读性的目的,对于那些在constructor
里面已经定义的实例属性,新写法允许直接列出
class ReactCounter extends Component{
state;
constructor(props){
super(props);
this.state = {
count:0
}
}
}//这里直接列出是何作用?
(2)类的静态属性
类的静态属性只要在上面的实例属性写法前面,加上static关键字就可以了
class MyClass{
static myStaticProp = 1;
constructor(){
console.log(MyClass.myStaticProp);//1
}
}
同样,这个新写法大大方便了静态属性的表达
//老写法
class Foo{
//...
}
Foo.prop=1;
//新写法
class Foo{
static prop = 1;
}
在上面代码中,老写法的静态属性定义在类的外部。整个类生成以后,再生成静态属性,这样让人很容易忽略这个静态属性,也不符合相关代码应该放在一起的代码组织原则,另外,新写法是显示声明(declarative),而不是赋值处理,语义更好。
15. new.target属性
new是从构造函数生成实例的命令,ES6为new命令引入了一个new.target
属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令调用的, new.target
会返回undefined
,因此,这个属性可以用来确定构造函数是怎么调用的
function Person(name){
if(new.target !== undefined){
this.name = name;
}else{
throw new Error('必须使用new生成实例');
}
}
//另一种写法
function Person(name){
if(new.target===Person){
this.name = name;
}else{
throw new Error('必须使用new生成实例');
}
}
const person = new Person('ht');//正确
const notAPerson = Person.call(person,'ht');//报错
上面代码确保构造函数只能通过new命令调用。
class内部调用new.target,返回当前class
class Rectangle{
constructor(length,width){
console.log(new.target===Rectangle);
this.length=length;
this.width=width;
}
}
const obj = new Rectangle(3,4);//true
需要注意的是,子类继承父类时,new.target会返回子类
class Rectangle{
constructor(length,width){
this.length=length;
this.width=width;
console.log(new.target);
console.log(new.target===Rectangle);
//...
}
}
class Square extends Rectangle{
constructor(length){
super(length);
}
}
const square = new Square(3);//false
console.log(square.length);//3
利用这个特点,可以写出不能独立使用,必须继承后才能使用的类
class Shape{
constructor (length,width){
if(new.target===Shape){
throw new Error('本类不能实例化');
}
this.length=length;
this.width=width;
}
}
class Rectangle extends Shape{
constructor(length,width){
super(length,width);
// ...
}
}
const x = new Shape(3,4);//报错
const y = new Rectangle(3,4);//
上面代码中,Shape类不能被实例化,只能用于继承。注意,不能在函数外部使用new.target
,否则会报错