class.xmind

对比 构造函数

ES5 构造函数

  1. function Point(x, y) {
  2. this.x = x
  3. this.y = y
  4. }
  5. Point.prototype.toString = function() {
  6. return `(${this.x}, ${this.y})`
  7. }

ES6 引入了类这个概念,作为对象的模板。class 比构造函数更清晰,更像面向对象编程的语法。

  1. class Point {
  2. constructor(x, y) {
  3. this.x = x
  4. this.y = y
  5. }
  6. toString() {
  7. return `(${this.x}, ${this.y})`
  8. }
  9. }

constructor方法就是构造方法, 对应Es5 的构造函数Point。
类的方法之间不需要逗号,加了会报错。

  1. class Point {}
  2. typeof Point // "function"
  3. Point === Point.protoype.constructor // true

上例: 类的数据类型就是函数,类本身就指向构造函数。

使用时, 直接对类使用new 命令,跟构造函数的用法基本一致。

  1. class Bar {
  2. doStuff() {
  3. console.log('stuff')
  4. }
  5. }
  6. var b = new Bar()
  7. b.doStuff() // 'stuff'

构造函数的prototype属性,在 Es6的‘类’上继续存在。实际上,类的所有方法都定义在类的 prototype 属性上。

  1. class Point {
  2. constructor() {}
  3. toString() {}
  4. toValue() {}
  5. }
  6. // 等同于
  7. Point.prototype = {
  8. constructor() {},
  9. toString() {},
  10. toValue() {}
  11. }

在类的实例上调用方法,其实就是调用原型上的方法。

  1. class B {}
  2. let b = new B()
  3. b.contructor === B.prototype.constructor // true

类内部定义的所有方法,都是不可枚举的。

  1. class Point {
  2. constructor(x, y) {}
  3. toString() {}
  4. }
  5. Object.keys(Point.prototype) // []
  6. Object.getOwnPropertyNames(Point.prototype) // ['constuctor', 'toString']

上例:toString 方法是Point 类内部定义的方法,它是不可枚举的。这一点与Es5的行为不一致。

  1. let Point = function (x, y) {}
  2. Point.prototype.toString = function() {}
  3. Object.keys(Point.prototype) // ['toString']
  4. Object.getOwnPropertyNames(Point.prototype) // ['constructor', 'toString']

你在 Es5 构造函数 prototype 定义的方法是可枚举的。

constructor 方法

constructor 方法是类的默认方法,用new 命令生成对象时,会自动调用该方法。一个类必须有constructor 方法,如果没有显示定义,JavaScript 引擎会自动添加一个空的constructor 方法.

  1. class Point {}
  2. // 等同于
  3. class Point {
  4. constructor() {}
  5. }

constructor 方法默认返回实例对象(即this), 但完全可以返回另一个对象

  1. class Foo {
  2. constructor() {
  3. return Object.create(null)
  4. }
  5. }
  6. new Foo() instanceof Foo // false

上例: constructor 函数返回一个全新的对象,结果导致实例对象不是Foo 类的实例。

类必须使用new 调用,否则会报错。但是普通构造函数不用new 也可以执行

类的实例

生成类的实例,必须使用new命令。

  1. class Point {}
  2. var point = Point(2, 3) // 报错
  3. var point2 = new Point(2, 3) // 正确

实例的属性都定义在原型上(即定义在class上),除非显示定义在其本身(即定义在this 对象上)。

  1. class Point {
  2. constructor(x, y) {
  3. this.x = x
  4. this.y = y
  5. }
  6. toString() {
  7. return `(${this.x}, ${this.y})`
  8. }
  9. }
  10. var point = new Point(2, 3)
  11. point.toString() // (2, 3)
  12. point.hasOwnProperty('x') // true
  13. point.hasOwnProperty('y') // true
  14. point.hasOwnProperty('toString') // false
  15. point.__proto__.hasOwnProperty('toString') // true

上例: x,y 实例对象自身的属性,定义在 this 上。toString 是原型上的属性, 定义在Point 类上。

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

  1. var p1 = new Point(2, 3)
  2. var p2 = new Point(3, 2)
  3. p1.__proto__ === p2.__proto__ // true

proto 不是语言本身的特性,这是各大厂商在具体实现上添加的私有属性,虽然目前很多现代的浏览器的JS引擎都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。在生产环境中,使用Object.getprototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。

  1. let p1 = new Point(2, 3)
  2. let p2 = new Point(3, 2)
  3. p1.__proto__.printName = function() { return 'Oops' }
  4. p1.printName() // 'Oops'
  5. p2.printName() // 'Oops'
  6. let p3 = new Point(4, 3)
  7. p3.printName() // 'Oops'

使用实例的proto 属性改写原型,会改变‘类’的原始定义,影响所有实例。

取值函数(getter) 和 存值函数(setter)

与Es5一样,在 ‘类’的内部可以使用 get 和 set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

  1. class MyClass {
  2. get prop() {
  3. return 'getter'
  4. }
  5. set prop(value) {
  6. console.log(value)
  7. }
  8. }
  9. let inst = new MyClass()
  10. inst.prop = 123 // setter: 123
  11. inst.prop // 'getter'

存值函数和取值函数是设置在属性的Descriptor 对象上的。

  1. class CustomHTMLElement {
  2. constructor(elemet) {
  3. this.element = element
  4. }
  5. get html() {
  6. return this.element.innerHTML
  7. }
  8. set html(value) {
  9. this.element.innnerHTML = value
  10. }
  11. }
  12. var decriptor = Object.getOwnPropertyDescriptor(
  13. CustomHTMLElement.proptotype, 'html'
  14. );
  15. 'get' in descriptor // true
  16. 'ste' in descriptor // true

属性表达式

类的属性名,可以采用表达式

  1. let methodName = 'getArea'
  2. class Square {
  3. constructor(length) {}
  4. [methodName]() {
  5. console.log(methodName)
  6. }
  7. }
  8. let squ = new Square()
  9. squ.getArea() // 'getArea'

class 表达式

与函数一样,类也可以使用表达式的形式定义。

  1. const MyClass = class Me {
  2. getClassName() {
  3. return Me.name
  4. }
  5. getOutClassName() {
  6. return MyClass.name
  7. }
  8. }
  9. let inst = new MyClass()
  10. inst.getClassName() // me
  11. inst.getOutClassName() // me
  12. let myInst = new Me() // ReferenceError: Me is not defined

上例: Me 只能在Class 内部使用,指代当前类,在Class 外部只能使用MyClass 引用。

如果类的内部没有使用到的话,可以省略Me.

  1. const MyClass = class {}

采用Class 表达式,可以写出立即执行的Class

  1. let person = new class {
  2. constructor(name) {
  3. this.name = name
  4. }
  5. sayName() {
  6. console.log(this.name)
  7. }
  8. }('张三')
  9. person.sayName() // ‘张三’

注意点

严格模块

类和模块的内部,默认都是严格模式

不存在提升

类不存在变量提升

  1. new Foo() // ReferenceError
  2. class Foo() {}

name 属性

name 属性总是返回紧跟在class 关键字后面的类名

  1. class Point {}
  2. point.name // 'Point'

Generator 方法

  1. class Foo {
  2. constructor(...args) {
  3. this.args = args
  4. }
  5. * [Symbol.iterator]() {
  6. for (let arg of this.args) {
  7. yield arg
  8. }
  9. }
  10. }
  11. for (let x of new Foo('hello', 'world')) {
  12. console.log(x)
  13. }

上例: Symbol.iterator 方法前有一个星号,表示该方法是一个Generator 函数。Symbol.iterator 方法返回一个Foo 类的默认遍历器,for…of 循环自动调用这个遍历器。

this 指向

类的方法内部的 this ,默认指向类的实例。一旦单独使用该方法,可能会报错。

  1. class Logger {
  2. printName(name = 'there') {
  3. this.print(`Hello ${name}`)
  4. }
  5. print(text) {
  6. console.log(text)
  7. }
  8. }
  9. class logger = new Logger()
  10. const { printName } = logger;
  11. printName(); // TypeError: Cannot read property 'print' of undefined

单独使用, this 实际指向的是undefined,找不到print 方法。

在构造函数中绑定this, 这样就不会找不到print 方法。

  1. class Logger {
  2. constructor() {
  3. this.printName = this.printName.bind(this)
  4. }
  5. printName(name = 'there') {
  6. this.print(`Hello ${name}`)
  7. }
  8. print(text) {
  9. console.log(text)
  10. }
  11. }
  12. class logger = new Logger()
  13. const { printName } = logger;
  14. printName() // hello world

使用箭头函数

  1. class Obj {
  2. constructor() {
  3. this.getThis = () => this
  4. }
  5. }
  6. const myObj = new Obj()
  7. getObj.getThis() ==== myObj // true

使用Proxy,获取方法时,自动绑定this

  1. function selfish(target) {
  2. const cache = new WeakMap()
  3. const handler = {
  4. get(target, key) {
  5. const value = Reflect.get(target, key)
  6. if (typeof value !== 'function') {
  7. return value
  8. }
  9. if (!cache.has(value)) {
  10. cache.set(value, value.bind(target))
  11. }
  12. return cache.get(value)
  13. }
  14. }
  15. const proxy = new Proxy(target, handler)
  16. return proxy
  17. }
  18. const { printName } = selfish(new Logger()) // Hello world

静态方法

在类的方法上加上 static 关键字,就称为静态方法。静态方法不会被实例继承,而是通过类来调用。

  1. class Foo {
  2. static classMethod() {
  3. return 'hello'
  4. }
  5. }
  6. Foo.classMethod() // hello
  7. let foo = new Foo()
  8. foo.classMethod() // TypeError: foo.classMethod is not a function

在实例上调用静态方法,会抛出一个错误。

静态方法的 this 关键字,指的是类, 而不是实例。

  1. class Foo {
  2. static bar() {
  3. this.baz()
  4. }
  5. static baz() {
  6. console.log('This is static baz')
  7. }
  8. baz() {
  9. console.log('world')
  10. }
  11. }
  12. Foo.bar() // this is static bar

静态方法可以与非静态方法重名。

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

  1. clas Foo {
  2. static clsMethod() {
  3. return 'hello'
  4. }
  5. }
  6. class Bar extends Foo {}
  7. Bar.cleMethod() // 'hello'

静态方法可以从super 对象上调用

  1. class Foo {
  2. static clsMethod() {
  3. return 'hello'
  4. }
  5. }
  6. class Bar extends Foo {
  7. static clsMethod() {
  8. return super.clsMethod() + ', too!'
  9. }
  10. }
  11. Bar.clsMethod() // 'hello, too!'

实例属性新写法

将这个属性定义在类的最顶层, 就不用定义在 constructor()里。

  1. class Foo {
  2. _count = 0;
  3. get value() {
  4. return this._count;
  5. }
  6. addCount() {
  7. this._count++
  8. }
  9. }

整齐,清晰易读。

静态属性

静态属性是类本身的属性,Class.name, 而不是实例对象(this)上的属性。

  1. // 普通写法
  2. class Foo {}
  3. Foo.prop = 1
  4. Foo.prop // 1
  5. // 静态属性
  6. class Foo {
  7. static myStaticProp = 42
  8. }
  9. Foo.myStaticProp // 42

静态属性 语义更好,更清晰。
在实例上使用 静态属性,返回 undefined.

私有方法 解决方法

私有属性和私有方法, 是只能在类的内部访问的属性和方法,外部不能访问。利于代码的封装。

命名区分(模拟)

  1. class Widget {
  2. // 公有
  3. foo(baz) {
  4. this._bar(baz)
  5. }
  6. // 私有
  7. _bar(baz) {
  8. return this.snaf = baz
  9. }
  10. }

不保险,在类的外部,仍可以用这个方法。

将私有方法移除模块

  1. class Widget {
  2. foo(baz) {
  3. bar.call(this, baz)
  4. }
  5. }
  6. function bar(baz) {
  7. return this.snaf = baz
  8. }

私有方法 => symbol

利用 symbol 值的唯一性,将私有方法的名字命名为一个 Symbol 值。

  1. const bar = Symbol('bar')
  2. const snaf = Symbol('snaf')
  3. export default class myClass {
  4. // 公有
  5. foo(baz) {
  6. this[bar](baz)
  7. }
  8. // 私有
  9. [bar](baz) {
  10. return this[snaf] = baz
  11. }
  12. }
  13. Reflect.ownKeys(myClass.prototype) // ["constructor", "foo", Symbol(bar)]

一般情况无法获取 bar 和 snaf ,但 Reflect.ownKeys() 依然可以拿到它们。

私有属性的提案

在属性名前,使用#表示

  1. class Foo {
  2. #count = 0;
  3. get value() {
  4. return this.#count
  5. }
  6. increment() {
  7. this.#count++
  8. }
  9. }

私有属性只能在类的内部使用,如果在类的外部使用就会报错。

new.target 属性

new.target属性一般用在构造函数中,返回new命令作用的那个构造函数。
如果不是通过 new 命令或 Reflect.construct() 调用,new.target 会返回undefined, 因此可用这个属性来确定构造函数是怎么调用的。

  1. function Person(name) {
  2. if (new.target !== undefined) {
  3. this.name = name
  4. } else {
  5. thorw new Error('必须使用new 命令生成实例')
  6. }
  7. }
  8. // 另一种写法
  9. function Person(name) {
  10. if (new.target === Person) {
  11. this.name = name
  12. } else {
  13. throw new Error('必须使用 new 命令生成实例')
  14. }
  15. }
  16. let person = new Person('张三') // ok
  17. let notAPerson = Person.call(pserson, '张三') // 报错

上面代码确保构造函数只能通过new命令调用。

Class 内部调用 new.target, 返回当前 Class

  1. class Rectangle {
  2. constructor(length, width) {
  3. console.log(new.target === Rectangle);
  4. this.length = length;
  5. this.width = width;
  6. }
  7. }
  8. let obj = new Rectangle(3, 4) // true

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

  1. class Rectangle {
  2. constructor(length, width) {
  3. console.log(new.target === Rectangle);
  4. this.length = length;
  5. this.width = width;
  6. }
  7. }
  8. class Square extends Rectangle {
  9. constructor(length) {
  10. super(length, width)
  11. }
  12. }
  13. let obj = new Square(3); // false

利用这个属性,可以写出不能对立使用、必须继承后才能使用的类。

  1. class Shape {
  2. constructor() {
  3. if (new.target === Shape) {
  4. throw new Error('本类本能实例化')
  5. }
  6. }
  7. }
  8. class Rectangle extends Shape {
  9. constructor(length, width) {
  10. super(length, width);
  11. }
  12. }
  13. let x = new Shape() // 报错
  14. let y = new Rectangle(3, 4) // ok

Shape 不能实例化,只能用于继承。链接