ES6小计(6)

17. Class

17.1 Class基本语法

JavaScript中其实是没有类这个概念的,有的只是对象的引用。

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

上面这种写法不方便,且封装不完全。故ES6引入了Class概念做为对象模板,新的这个写法仅仅是让对象原型的写法更加清晰而已。

class Point {
    constructor(x, y) {
        this.x = x
        this.y = y
    }

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

可以看出ES6中类的写法中,有一个constructor方法,称为构造函数方法,类中this代表实例对象,另类内部函数类似于对象,但是属性之间不要加,,否则会报错。自定义属性需要放在constructor构造函数中。

typeof Point // function
Point === Point.prototype.constructor

类的数据类型就是一个函数,类本身指向构造函数。类的所有方法(包括构造方法)均定义在类的prototype属性上。实例方法的调用,就是调用原型上的方法。类内部定义的所有方法都是不可枚举的,这点与ES5中prototype上的方法不同,但很有意义。

constructor方法是类的默认方法,在实例化类new时执行,若没有显示定义constructor方法,系统将会自动为类添加一个空的constructor方法。

类的实例化同ES5的构造函数一致,使用new关键字,若没有使用new实例化,将会报错。类的所有实例共享一个原型对象,同ES5的继承一致。

类的name属性总是返回class关键字后面的类名。class Point {}; Point.name // Point

Class定义不存在变量提升,与let一致,必须先定义,再使用,当使用未定义的Class时,会报错。

类和模块的内部默认为严格模式,所以不需要使用use strict指定运行模式。

17.2 Class的继承

Class之间可以通过extends关键字实现继承。

class ColorPoint extends Point {
    constructor(x, y, color) {
        super(x, y)
        this.color = color
    }
    toString() {
        return this.color + ' ' + super.toString()
    }
}

let cp = new ColorPoint(25, 7, 'green')
cp instanceof ColorPoint // true
cp instanceof Point // true

Class内部的super指代父类的实例。子类必须在constructor方法中调用super方法,否则在新建实例的时候会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其加工。如不调用super方法,则得不到this对象,自然也就无法返回this实例。故必须在使用super后再使用this

类的prototype属性和__proto__属性:大多数浏览器的ES5实现中,每一个对象均有__proto__属性,指向对应构造函数的prototype属性。而类同时有prototype属性和__proto__属性,因此存在两条继承链。

  1. 子类的__proto__属性表示构造函数的继承,总指向父类(实例继承实例);
  2. 子类prototype属性的__proto__属性表示方法的继承,总指向父类的prototype属性(原型继承原型)。

    class A {}
    class B extends A {}
    B.proto === A // true
    B.prototype.proto === A.prototype // true
    B.Proto.proto === A.proto // true

Object.getPrototypeOf()
Object.getPrototypeOf()可以用于从子类上获取父类,可以用此方法判断一个类是否继承了另一个类。

Object.getPrototypeOf(ColorPoint) === Point // true

17.3 原生构造函数的继承

ECMAScript的原生构造函数:

  • Boolean()
  • Number()
  • String()
  • Array()
  • Date()
  • Function()
  • Regexp()
  • Error()
  • Object()

ES6之前,由于子类无法获取原生构造函数的内部属性,故无法对原生构造函数进行继承。而通过extends进行继承,是通过先建立父类实例对象的this,再对this对象进行修饰,故extends的继承可以继承原生构造函数,也就意味着可以在原生构造函数的基础上进行新的类定义。

class MyArray extends Array {
    constructor(...args) {
        super(...args)
    }
}

17.4 Class的getter和setter

与ES5一致,在Class内部可以使用getset关键字对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

class MyClass {
    constructor() {}
    get prop() {
        return 'getter'
    }
    set prop(value) {
        console.log('setter:', value)
    }
}

let inst = new MyClass()
inst.prop = 123 // setter: 123
inst.prop // getter

17.5 Class的Generator方法

同对象的Generator函数一致,只需要在属性函数名前加上*即可。

17.6 Class的静态方法

在类中定义的方法均会被实例继承,但若在方法前加上static关键字,则表示该方法不会被实例继承。而只能通过类取调用,称之为“静态方法”。

class Foo {
    static classMethod() {
        return 'abc'
    }
}
Foo.classMethod() // abc
var foo = new Foo()
foo.classMethod() // classMethod is not a function

class Bar extends Foo {}
Bar.clasMethod() // abc

静态方法可以被子类继承及调用。

17.7 Class的静态属性

静态属性是指在Class本身的属性,即Class.propName,而不是在实例对象this上的属性。ES6规定类内只能定义属性,不能定义方法,故想给Class定义属性,需要在外部定义,且Class实例的属性也只能在constructor方法内定义。

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

17.8 new.target属性

new.target是构造函数的内部属性,只能在构造函数内使用。new.target返回通过new命令所作用的构造函数,若构造函数不是通过new调用,则返回undefined。此属性可以用于指定类实例的创建方式。

function Person(name) {
    if (new.target !== undefined) { // 或 new.target === Person
        this.name = name
    } else {
        throw new Error('请使用new进行实例化')
    }
}

注意子类继承父类时,new.target会返回子类

18. Module

模块加载规范:
CommonJS: 模块就是对象,通过对象的加载而引用,运行时加载,同步执行。Node中,主要就是使用CommonJS进行模块加载。

// file 
exports.sum = function() {}
// file2
module.exports = {sum: 123}
// main
let {sum} = require('file')

AMD: 异步的加载模块,先定义所有依赖,然后在加载完成后的回调函数中执行。

define(['clock'],function(clock){
  clock.start()
})

CMD: 依赖就近,用的时候再require。

define(function(require, exports, module) {
   var clock = require('clock')
   clock.start()
})

ES6模块加载: ES6模块不是对象,而是通过export命令显示指定输出的代码,编译时加载。

import {stat, exists, readFile} from 'fs'
// 从fs模块加载3个方法,其它不加载

ES6的模块自动采用严格模式

18.1 export命令

export用于规定模块对外接口。

// file.js
export var firstName = 'Michael'
export var lastName = 'Jackson'

// file2.js
var firstName = 'Michael'
var lastName = 'Jackson'
export {firstName, lastName}

export命令可以出现在模块的任意位置,只要处于模块的顶层即可,当处于块级作用域时,会报错。

18.2 import命令

import用于加载文件模块。

import {firstName as fName, lastName as lName} from 'file2.js'

import的变量名可以用as进行重命名。注意import命令具有变量提升效果,会优先定义。建议不去掉大括号,当多变量引用时,方便阅读。

18.3 模块的整体加载

当需要加载整个模块时,可用*as指定一个对象用于承接export的所有属性和方法。

import * as total from 'file.js'
total // {firstName, lastName}

18.4 module命令

module命令可以取代import做整体模块输入。

module total from 'file.js'
total // {firstName, lastName}

18.5 export default命令

export default用于指定模块的默认输出,当文件需要引用一个模块的默认输出时,直接import到自定义变量即可。显然export default一个模块只能有一个。

// file.js
export default {
    a: 1,
    b: function() {}
}
// main.js
import file from 'file.js'
file // {a: 1, b: function() {}}

18.6 ES6模块加载实质

ES6模块输出的是对象的引用,而CommonJS输出的是值得拷贝。

// file.js
var counter = 3
function incCounter() {
    counter++
}
module.exports = { counter, incCounter }

// main.js
var counter = require('file.js').counter
var incCounter = require('file.js').incCounter
console.log(counter) // 3
incCounter()
console.log(counter) // 3

// file2.js
export let counter = 3
export function incCounter() {
    counter++
}

// main2.js
import {counter, incCounter} from 'file2.js'
console.log(counter) // 3
incCounter()
console.log(counter) // 4

THE END!