ES6学习笔记之class

概述

ES6引入了class以接近传统的面向对象(java、c++)语法。我觉得这不是很有必要。因为在继承方面它和java/c++完全不一样,这样会对新手可能会造成困扰(虽然java我已经忘掉了)。

实际上,class可以看做一个语法糖,它的绝大部分功能都可以由ES5做到,在此基础之上,增加了一些功能而已。使用class只是让js更像面向对象编程的语法而已。。

在es6中,定义一个class可能会如下所示。

class Point {
  constructor(x, y) { // 定义一个classconstructor是可选的
    this.x = x;
    this.y = y;
  }

  toString() {
    return '('+ this.x + ', '+ this.y +')';
  }
}

var p = new Point(1, 1);

这里定义了一个“类”(我很不愿意说在js中是有类的)。可以看到有一个constructor方法,这就是构造方法。除此之外,还有一个toString方法,用以改写原型链上层的toString方法。需要注意的是:

  • 使用class而不是function
  • Point后面没有括号
  • 方法定义之间没有逗号
  • 方法定义的时候直接是方法名后面跟着括号
  • 除了constructor方法外别的方法都定义在原型上

所以如果用ES5的写法,上面的定义为:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function() {
  return '('+ this.x + ', '+ this.y +')';
}

var p = new Point(1, 1);

有所区别的是:类(class)中定义的所有方法都是不可枚举的。

Object.keys(Point.prototype)
[]

Object.getOwnPropertyNames(Point.prototype)
["constructor", "toString"]

这不同与ES5。ES5原型上constructor本来就是不可枚举的,而自定义的toString是可枚举的。

Object.keys(Point.prototype)
["toString"]

Object.getOwnPropertyNames(Point.prototype)
["constructor", "toString"]

类和对象

new用来生成一个一个对象,它会有以下四个步骤:(摘自你不知道的javascript)

  • 创建(或者说构造)一个全新的对象
  • 这个新对象会被执行[[Prototype]]连接
  • 这个新对象会绑定到函数调用的this
  • 如果函数没有返回其他对象,那么new表达式中的函数会自动返回这个新对象。

也就是说,在constructor或者function Point(){}中,如果返回了一个新对象,那么new操作得到的就是这个return的对象(上述三步无效)。否则,就是new出来的对象。

class Point {
  constructor() {
    return Object.create(null);
  }
}

new Point() instanceof Point // false

在ES5中,使用

var p = Point(1, 1)

时构造函数中的this会绑定到window(非严格模式)。但是在ES6中省略new会报错。

与ES5一样。类的所有实例共享同一个原型对象。

var p1 = new Point(1, 1);
var p2 = new Point(1, 2);

p1.__proto__ === Point.prototype; //true
p1.__proto__ === p2.__proto__;

每个对象都有__proto__属性,这是一种非正式的写法。如果要取得一个对象的原型,可以使用Object.getPrototypeOf方法。

Object.getPrototypeOf(Object.prototype); //null

同理,如果要手动设置一个对象的原型,可以使用Object.setPrototypeOf(后续的继承中会写到)。

name属性

与ES5一样,name属性总是返回跟在class关键字后面的类名。

class Point {}
Point.name; // "point"

length属性

与ES5一样,length属性返回构造器参数的个数。

class Foo {
  constructor(x, y, z) {}
}
Foo.length; // 3

class表示式

const MyClass = class Me {
  getClassName() {
    return Me.name;
  }
}

上述的代码使用表达式定义了一个类。需要注意的是,这个类的名字是MyClass而不是Me。Me只是对类的内部可见的,在外部是未定义的。

不存在变量提升

在ES5中,存在变量提升(函数会提升到变量前面)。

new Foo();
function Foo() {};

上述的代码是没有问题的。然而,在ES6中,

new Foo()
class Foo {}

这样是会报错的。

严格模式

类和模块的内部默认就是严格模式。所以不需要再次使用”use strict”声明了。

继承

class之间可以通过extends继承,更加清晰明了。

class ColorPoint extends Point {}

上面的代码表示类ColorPoint继承了Point,如果在ColorPoint中显式使用了constructor,那么必须要使用到super方法调用父类的构造器(借用构造函数继承)。
如果子类没有显式定义constructor方法,那么这个方法会被默认添加,代码如下。

constructor(...args) {
 super(...args)
}
class ColorPoint extends Point {
  constructor(x, y, color) { //如果有constructor,那么就必须使用super
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

这是因为子类没有自己的this对象,而是继承了父类的this对象,然后对其进行加工。如果不调用this方法,子类得不到this对象报错。只有通过super方法继承了父类的this后,才可以使用this。

class Point { constructor(x) {this.x = 1;} };
class ColorPoint extends Point {
  constructor() {
  }
}

var cp = new ColorPoint(); // ReferenceError: this is not defined

而在ES5中,类似于这种借用构造函数的继承是先创造子类的实例对象this,然后通过Point.call(this,x,y)这种方法实现。

实际上,这个继承操作做了两步动作:

ColorPoint.__proto__ === Point; // true
ColorPoint.prototype.__proto__ === Point.prototype; // true

使用ES6的写法就是:

Object.setPrototypeOf(ColorPoint, Point);
Object.setPrototypeOf(ColorPoint.prototype, Point.prototype);

有三种情况比较特殊:

  • 继承Object
class A extends Object {
}

A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
  • 不存在继承
class A {
}

A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
  • 继承null
class A extends null {
}

A.__proto__ === Function.prototype // true
A.prototype.__proto__ === undefined // true

Object.getPropertyOf()

这个方法可以从子类上获取父类。

Object.getPropertyOf(ColorPoint) === Point; // true

super

super代表父类实例或者父类(静态方法中)。

class B extends A {
  get m() {
    return this._p * super._p;
  }
  set m() {
    throw new Error('该属性只读');
  }
}

上面的代码中,子类通过super关键字调用了父类实例的属性。

由于对象总是继承其他对象的,所以可以在任意一个对象上调用super关键字。

var obj = {
  toString() {
    return super.toString()
  }
}

obj.toString(); // "[object Object]"

__proto__

关于__proto__。任意一个对象都有__proto__属性。你可以称呼为笨蛋proto。但是__proto__并不是标准而且无法兼容所有浏览器。如果要设置原型,还是推荐Object.setPrototypeOf()

function foo() {}
foo.__proto__ === Function.prototype; // true

class foo {}
foo.__proto__ === Function.prototype; // true

// 接着上面的ColorPoint继承Point
Point.__proto__ === Function.prototype; // 诸君看吧!

var p1 = new Point(2,3);
var p2 = new ColorPoint(2, 3, 'red');

// 下面的返回值都是true
p1.__proto__ === Point.prototype;
p2.__proto__ === ColorPoint.prototype;
ColorPoint.prototype.__proto__ === Point.prototype;
Point.prototype.__proto__ === Object.prototype;
Object.prototype.__proto__ === null; // 到此结束!

原生构造函数的继承

原生构造函数指语言内置的构造函数。比如object下面有九种内置子类型,都可以作为构造函数使用。分别是:

  • Number
  • String
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • RegExp
  • Error

以前,这些原生的构造函数是无法继承的。因为ES5的继承方式是先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承远程的构造函数。比如Array有个内部属性[[DefineOwnProperty]],用于定义新属性时更新length属性,这个内部属性无法在子类获取,导致子类的length属性行为不正常。

function MyArray() {
  Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
  // 属性描述符
  value: MyArray,
  writable: true,
  configurable: true,
  enumerable: true
})

var colors = new MyArray();
colors[0] = 'red';
colors.length; // 0
colors.length = 0;
colors[0]; // 'red';

而在ES6中,是可以继承的。

class MyArray extends Array {
  constructor() {
    super();
  }
}

var colors = new MyArray();
colors[0] = 'red';
colors.length; // 1
colors.length = 0;
colors; // []

class的getter和setter

同ES5。可能以后会写属性描述符,那里再介绍。

class+Generator

同ES5定义类似。如果在某个方法前加上(*),表示这是一个generator函数。

class Foo {
 * a() { yield 1; }
}

var f = new Foo();
var i = f.a();
i.next(); // Object {value: 1, done: false}
i.next(); // Object {value: undefined, done: true}

class的静态方法

class Foo {
  static classMethod() {  return 'hello';  }
}
var f = new Foo();
f.classMethod(); // TypeError: f.classMethod is not a function 
Foo.classMethod(); // "hello"

另外,父类的静态方法可以被子类继承。

class Foo {
  static classMethod() {  return 'hello';  }
}
class Bar extends Foo {
  static classMethod() { return super.classMethod() + ', too'; }
}

Bar.classMethod(); // "hello, too"

这里super指的是父类(foo),并不是父类的一个实例(所以上述的说法有失偏颇)。
只有static标注的方法是给类直接调用的,而且在子类的非静态方法中调用super是会报错的。比如去掉Bar类的static方法。使用实例调用classMethod会报错。

class的静态属性

静态属性指的是Class本身的属性,即Class.propname。而不是实例对象的属性(这个定义在this上)。

class Foo {}
Foo.prop = 1;
Foo.prop; // 1

上面的这种写法可以读写Foo类的静态属性。
ES7有一个静态属性的提案,目前babel转码器已经支持。
这个属性对静态属性和实例属性都规定了新写法。

// 实例属性的新写法
class MyClass {
  myProp = 1;

  constructor() {
    console.log(this.myProp);
  }
}

// 静态属性的新写法
class MyClass {
  static myStaticProp = 12;

  constructor() {
    console.log(MyClass.myStaticProp)
  }
}

new.target

new是构造函数生成实例的命令。ES6添加了new.target属性,返回构造函数的名字。如果构造函数不是通过new命令调用的,那么new.target会返回undefined。之前通过this instanceof Foo这种写法。

function Person(name) {
  if ( new.target == Person ) { // this instanceof Person
    this.name = name;
  } else {
    throw new Error('不是使用new调用的');
  }
}

子类继承父类时new.target会返回子类的名称。

Minxin

参考

http://es6.ruanyifeng.com/#docs/class

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值