对比 构造函数
ES5 构造函数
function Point(x, y) {this.x = xthis.y = y}Point.prototype.toString = function() {return `(${this.x}, ${this.y})`}
ES6 引入了类这个概念,作为对象的模板。class 比构造函数更清晰,更像面向对象编程的语法。
class Point {constructor(x, y) {this.x = xthis.y = y}toString() {return `(${this.x}, ${this.y})`}}
constructor方法就是构造方法, 对应Es5 的构造函数Point。
类的方法之间不需要逗号,加了会报错。
class Point {}typeof Point // "function"Point === Point.protoype.constructor // true
上例: 类的数据类型就是函数,类本身就指向构造函数。
使用时, 直接对类使用new 命令,跟构造函数的用法基本一致。
class Bar {doStuff() {console.log('stuff')}}var b = new Bar()b.doStuff() // 'stuff'
构造函数的prototype属性,在 Es6的‘类’上继续存在。实际上,类的所有方法都定义在类的 prototype 属性上。
class Point {constructor() {}toString() {}toValue() {}}// 等同于Point.prototype = {constructor() {},toString() {},toValue() {}}
在类的实例上调用方法,其实就是调用原型上的方法。
class B {}let b = new B()b.contructor === B.prototype.constructor // true
类内部定义的所有方法,都是不可枚举的。
class Point {constructor(x, y) {}toString() {}}Object.keys(Point.prototype) // []Object.getOwnPropertyNames(Point.prototype) // ['constuctor', 'toString']
上例:toString 方法是Point 类内部定义的方法,它是不可枚举的。这一点与Es5的行为不一致。
let Point = function (x, y) {}Point.prototype.toString = function() {}Object.keys(Point.prototype) // ['toString']Object.getOwnPropertyNames(Point.prototype) // ['constructor', 'toString']
你在 Es5 构造函数 prototype 定义的方法是可枚举的。
constructor 方法
constructor 方法是类的默认方法,用new 命令生成对象时,会自动调用该方法。一个类必须有constructor 方法,如果没有显示定义,JavaScript 引擎会自动添加一个空的constructor 方法.
class Point {}// 等同于class Point {constructor() {}}
constructor 方法默认返回实例对象(即this), 但完全可以返回另一个对象。
class Foo {constructor() {return Object.create(null)}}new Foo() instanceof Foo // false
上例: constructor 函数返回一个全新的对象,结果导致实例对象不是Foo 类的实例。
类必须使用new 调用,否则会报错。但是普通构造函数不用new 也可以执行。
类的实例
生成类的实例,必须使用new命令。
class Point {}var point = Point(2, 3) // 报错var point2 = new Point(2, 3) // 正确
实例的属性都定义在原型上(即定义在class上),除非显示定义在其本身(即定义在this 对象上)。
class Point {constructor(x, y) {this.x = xthis.y = y}toString() {return `(${this.x}, ${this.y})`}}var point = new Point(2, 3)point.toString() // (2, 3)point.hasOwnProperty('x') // truepoint.hasOwnProperty('y') // truepoint.hasOwnProperty('toString') // falsepoint.__proto__.hasOwnProperty('toString') // true
上例: x,y 实例对象自身的属性,定义在 this 上。toString 是原型上的属性, 定义在Point 类上。
与 Es5 一样, 类的所有实例共享一个原型对象。
var p1 = new Point(2, 3)var p2 = new Point(3, 2)p1.__proto__ === p2.__proto__ // true
proto 不是语言本身的特性,这是各大厂商在具体实现上添加的私有属性,虽然目前很多现代的浏览器的JS引擎都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。在生产环境中,使用Object.getprototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。
let p1 = new Point(2, 3)let p2 = new Point(3, 2)p1.__proto__.printName = function() { return 'Oops' }p1.printName() // 'Oops'p2.printName() // 'Oops'let p3 = new Point(4, 3)p3.printName() // 'Oops'
使用实例的proto 属性改写原型,会改变‘类’的原始定义,影响所有实例。
取值函数(getter) 和 存值函数(setter)
与Es5一样,在 ‘类’的内部可以使用 get 和 set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
class MyClass {get prop() {return 'getter'}set prop(value) {console.log(value)}}let inst = new MyClass()inst.prop = 123 // setter: 123inst.prop // 'getter'
存值函数和取值函数是设置在属性的Descriptor 对象上的。
class CustomHTMLElement {constructor(elemet) {this.element = element}get html() {return this.element.innerHTML}set html(value) {this.element.innnerHTML = value}}var decriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.proptotype, 'html');'get' in descriptor // true'ste' in descriptor // true
属性表达式
类的属性名,可以采用表达式
let methodName = 'getArea'class Square {constructor(length) {}[methodName]() {console.log(methodName)}}let squ = new Square()squ.getArea() // 'getArea'
class 表达式
与函数一样,类也可以使用表达式的形式定义。
const MyClass = class Me {getClassName() {return Me.name}getOutClassName() {return MyClass.name}}let inst = new MyClass()inst.getClassName() // meinst.getOutClassName() // melet myInst = new Me() // ReferenceError: Me is not defined
上例: Me 只能在Class 内部使用,指代当前类,在Class 外部只能使用MyClass 引用。
如果类的内部没有使用到的话,可以省略Me.
const MyClass = class {}
采用Class 表达式,可以写出立即执行的Class
let person = new class {constructor(name) {this.name = name}sayName() {console.log(this.name)}}('张三')person.sayName() // ‘张三’
注意点
严格模块
类和模块的内部,默认都是严格模式
不存在提升
类不存在变量提升
new Foo() // ReferenceErrorclass Foo() {}
name 属性
name 属性总是返回紧跟在class 关键字后面的类名
class Point {}point.name // 'Point'
Generator 方法
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)}
上例: Symbol.iterator 方法前有一个星号,表示该方法是一个Generator 函数。Symbol.iterator 方法返回一个Foo 类的默认遍历器,for…of 循环自动调用这个遍历器。
this 指向
类的方法内部的 this ,默认指向类的实例。一旦单独使用该方法,可能会报错。
class Logger {printName(name = 'there') {this.print(`Hello ${name}`)}print(text) {console.log(text)}}class logger = new Logger()const { printName } = logger;printName(); // TypeError: Cannot read property 'print' of undefined
单独使用, this 实际指向的是undefined,找不到print 方法。
在构造函数中绑定this, 这样就不会找不到print 方法。
class Logger {constructor() {this.printName = this.printName.bind(this)}printName(name = 'there') {this.print(`Hello ${name}`)}print(text) {console.log(text)}}class logger = new Logger()const { printName } = logger;printName() // hello world
使用箭头函数
class Obj {constructor() {this.getThis = () => this}}const myObj = new Obj()getObj.getThis() ==== myObj // true
使用Proxy,获取方法时,自动绑定this
function selfish(target) {const cache = new WeakMap()const handler = {get(target, key) {const value = Reflect.get(target, key)if (typeof value !== 'function') {return value}if (!cache.has(value)) {cache.set(value, value.bind(target))}return cache.get(value)}}const proxy = new Proxy(target, handler)return proxy}const { printName } = selfish(new Logger()) // Hello world
静态方法
在类的方法上加上 static 关键字,就称为静态方法。静态方法不会被实例继承,而是通过类来调用。
class Foo {static classMethod() {return 'hello'}}Foo.classMethod() // hellolet foo = new Foo()foo.classMethod() // TypeError: foo.classMethod is not a function
在实例上调用静态方法,会抛出一个错误。
静态方法的 this 关键字,指的是类, 而不是实例。
class Foo {static bar() {this.baz()}static baz() {console.log('This is static baz')}baz() {console.log('world')}}Foo.bar() // this is static bar
静态方法可以与非静态方法重名。
父类的静态方法可以被子类的继承。
clas Foo {static clsMethod() {return 'hello'}}class Bar extends Foo {}Bar.cleMethod() // 'hello'
静态方法可以从super 对象上调用
class Foo {static clsMethod() {return 'hello'}}class Bar extends Foo {static clsMethod() {return super.clsMethod() + ', too!'}}Bar.clsMethod() // 'hello, too!'
实例属性新写法
将这个属性定义在类的最顶层, 就不用定义在 constructor()里。
class Foo {_count = 0;get value() {return this._count;}addCount() {this._count++}}
整齐,清晰易读。
静态属性
静态属性是类本身的属性,Class.name, 而不是实例对象(this)上的属性。
// 普通写法class Foo {}Foo.prop = 1Foo.prop // 1// 静态属性class Foo {static myStaticProp = 42}Foo.myStaticProp // 42
静态属性 语义更好,更清晰。
在实例上使用 静态属性,返回 undefined.
私有方法 解决方法
私有属性和私有方法, 是只能在类的内部访问的属性和方法,外部不能访问。利于代码的封装。
命名区分(模拟)
class Widget {// 公有foo(baz) {this._bar(baz)}// 私有_bar(baz) {return this.snaf = baz}}
不保险,在类的外部,仍可以用这个方法。
将私有方法移除模块
class Widget {foo(baz) {bar.call(this, baz)}}function bar(baz) {return this.snaf = baz}
私有方法 => symbol
利用 symbol 值的唯一性,将私有方法的名字命名为一个 Symbol 值。
const bar = Symbol('bar')const snaf = Symbol('snaf')export default class myClass {// 公有foo(baz) {this[bar](baz)}// 私有[bar](baz) {return this[snaf] = baz}}Reflect.ownKeys(myClass.prototype) // ["constructor", "foo", Symbol(bar)]
一般情况无法获取 bar 和 snaf ,但 Reflect.ownKeys() 依然可以拿到它们。
私有属性的提案
在属性名前,使用#表示
class Foo {#count = 0;get value() {return this.#count}increment() {this.#count++}}
私有属性只能在类的内部使用,如果在类的外部使用就会报错。
new.target 属性
new.target属性一般用在构造函数中,返回new命令作用的那个构造函数。
如果不是通过 new 命令或 Reflect.construct() 调用,new.target 会返回undefined, 因此可用这个属性来确定构造函数是怎么调用的。
function Person(name) {if (new.target !== undefined) {this.name = name} else {thorw new Error('必须使用new 命令生成实例')}}// 另一种写法function Person(name) {if (new.target === Person) {this.name = name} else {throw new Error('必须使用 new 命令生成实例')}}let person = new Person('张三') // oklet notAPerson = Person.call(pserson, '张三') // 报错
上面代码确保构造函数只能通过new命令调用。
Class 内部调用 new.target, 返回当前 Class
class Rectangle {constructor(length, width) {console.log(new.target === Rectangle);this.length = length;this.width = width;}}let obj = new Rectangle(3, 4) // true
子类继承父类时, new.target 会返回子类
class Rectangle {constructor(length, width) {console.log(new.target === Rectangle);this.length = length;this.width = width;}}class Square extends Rectangle {constructor(length) {super(length, width)}}let obj = new Square(3); // false
利用这个属性,可以写出不能对立使用、必须继承后才能使用的类。
class Shape {constructor() {if (new.target === Shape) {throw new Error('本类本能实例化')}}}class Rectangle extends Shape {constructor(length, width) {super(length, width);}}let x = new Shape() // 报错let y = new Rectangle(3, 4) // ok
Shape 不能实例化,只能用于继承。链接
