js 构造函数与类

133 阅读6分钟

构造函数

1. 定义一个构造函数

在调用函数时使用 new 关键字,则该函数被当作构造函数执行,返回一个实例对象。构造函数声明时,首字母应当大写,方便与普通函数区分。

function Person(name, age) {
    this.name = name
    this.age = age
}
Person.prototype.sayName = function() {
    return this.name
}
let p = new Person('小明', 18)

new Person() 执行时,会有以下操作:

  1. 在内存中创建一个空对象 obj
  2. 这个新对象的 [[Prototype]] 属性被赋值为构造函数的 prototype 属性,即 obj.__proto__ = Person.prototype
  3. 构造函数中的 this 被赋值为新对象,即 this 指向 obj
  4. 执行构造函数内部的代码(给新对象添加属性)
  5. 如果构造函数显式 return 了一个对象,则返回该对象,否则返回刚才创建的 obj ( 例如 return 123 的结果依然是返回刚才创建的 obj )

上面这段代码等价于以下代码:

function Person(name, age) {
    let obj = Object.create(Person.prototype)
    obj.name = name
    obj.age = age
    return obj
}
Person.prototype.sayName = function() {
    return this.name
}
let p = new Person('小明', 18)

注意:箭头函数不支持 this , 也没有 prototype 属性,不能作为构造函数使用。

2. 寄生式组合继承

红宝书中列举了多种继承方式,个人比较喜欢其中的最后一种。

function Person(name, age) {
    this.name = name
    this.age = age
}
Person.prototype = {
    constructor: Person,
    sayName() {
        return this.name
    }
}

function Student(name, age, grade) {
    Person.call(this, name, age)
    this.grade = grade
}
Student.prototype = Object.create(Person.prototype)
Student.prototype.constructor = Student
Student.prototype.sayGrade = function () {
    return this.grade
}

上面这段代码中构造函数 Student 的 prototype 被赋值为一个以 Person.prototype 为原型创建的对象,并将其 constructor 属性指向 Student。
这样就实现了对 Person 原型上的方法和属性的继承,现在可以在 Student.prototype 上添加 .xxx方法或属性了。( 不能像 Person.prototype 那样对整个原型进行赋值了 )。
然后,通过 Person.call(this) 盗用构造函数的方式,可以实现对 Person 实例自身属性的继承。

3. 私有属性

  1. js 中可以用 this._xxx 表示私有属性。
  2. 可以通过 Symbol() 来实现私有属性。
const _luckyColor = Symbol('luckyColor')
function Person(name, age, luckyColor) {
    this.name = name
    this.age = age
    this[_luckyColor] = luckyColor
}
Person.prototype = {
    constructor: Person,
    sayLuckyColor() {
        return this[_luckyColor]
    }
}

由于 Symbol() 的值唯一,除非直接通过变量获取,否则无法在实例上表示,故可以用来实现私有属性。

es6中的 class 很大程度上是基于原型链基础上创造出语法糖,可以更加方便的支持面向对象编程。

1. 定义一个类

//声明
//方式一
console.log(new A())    //报错
class A {}              //不存在变量提升  

//方式二
const B = class ClasName {}     //此处的 ClassName 可以省略,且 ClassName 也只在类作用域里生效
console.log(new ClassName())    //报错 

//调用
//1. class 可以像函数一样立即执行
console.log(new class C {}())

//2. 生成实例时如果不需要传递参数,() 可以省略
class D {}
console.log(new D)
  
//class 是一个函数
class D {}
console.log(typeof D) //'function'

2. 类的构造函数

class Person {
    constructor(name) {
        this.name = name
    }
}
let p = new Person('小明')

类的构造函数与普通构造函数并没有什么不同,在生成实例时会执行相同的几个操作,唯一的区别是必须要使用 new 来调用。

3. 类的成员

1. 实例成员

class Person {
    constructor(name) {
        this.name = name
    }
}

实例成员是实例对象自己拥有的属性,不会通过原型与其他实例共享。
实例成员应该在 constructor 方法中定义,但实际上也可以在原型方法里可以通过 this.xxx 来添加,或者在实例生成后添加。

2. 原型成员

class Person {
    constructor(name) {
        this.name = name
    }
    sayName() {
        return this.name
    }
    age = 18
    sayAge = function () {
        return this.age
    }
}
let p = new Person('小明')
console.log(Object.getOwnPropertyNames(p))  //[ 'age', 'sayAge', 'name' ]

原型成员是 Person.prototype 的属性,Person 的实例共享这些属性。
在类块中定义的方法会成为 Person.prototype 上的方法。不应该将原始值和非 Function 对象的属性定义在类块中,即使定义了,这些属性也会设置在实例对象上,而非 Person.prototype 上。而 sayAge = function(){} 这样定义的方法同样会被认为是实例成员。
这些属性可以在原型上手动添加 ( Person.prototype.age = 18 ) 。

3. 类成员 ( 静态成员 )

class Person {
    static count_1 = 1
    static count_2 = 2
    static f() {
        return this
    }
}
class Student extends Person {}
console.log(Person.count_1)
let p = new Person()
console.log(p.constructor.count_2)
console.log(Person.f() === Person)           //true
console.log(Student.__proto__ === Person)    //true

类成员是类 Person 的属性,通过类名 Person.xxx 访问。
在类块中使用 static 关键字定义的属性会成为类成员。
与实例成员和原型成员中的方法不同,类方法中的 this 指向类自身 Person
类本身与类的实例是两条不同的原型链,Student.__proto__ 指向 Person 而非 Person.prototype

4. setter 和 getter

class Person {
    constructor(name, age) {
        this._name = name
        this.age = age
    }
    get name() {
        return 'my name is ' + this._name
    }
    set name(value) {
        this._name = value
        console.log('被调用了')
    }
}

let p = new Person('小明', 18)
console.log(p)                          //Person { _name: '小明', age: 18 }
console.log(p.name)                     //my name is 小明
console.log(p.hasOwnProperty('name'))   //false
  
class Student extends Person {
    name                                //声明属性 name
    constructor(name, age, grade) {
        super(name, age)
        this.grade = grade
    }
}
let s = new Student('小芳', 16, '高一')
console.log(s)                          //Student { _name: '小芳', age: 16, name: undefined, grade: '高一' }
s.name = '小月'                         //不会触发 set name 方法
  1. class 中的 gettersetter 与对象中获取函数和设置函数的行为相同。gettersetter 不一定都要设置,只设置 getter 意味这该属性只读。非严格模式下,对该属性的赋值会被忽略,在严格模式下则会报错,而 class 作用域中是默认使用严格模式的。
  2. 在设置了 get nameset name 方法后,name 属性并不实际存在于实例对象上,即使在 constructor 方法中 this.name = xxx 也不会在实例上新增属性,仅仅会调用一次 set name 方法。
  3. 上面的代码中,Student 的类块里声明了 name 属性,这不同于this.name = xxx 添加属性。在类块里声明的属性是通过 Object.defineProperty() 添加到实例对象上的,因此实例对象上实际有了一个 name 属性,而 set name()get name() 则会被屏蔽掉。

5. 私有成员

私有成员只能在 class 内才能被直接访问。

  1. 私有实例成员
class Person {
    #age = 0
    #getAge = function() {
        return this.#age
    }
    constructor(name, age) {
        this.name = name
        this.#age = age
    }
    sayAge() {
        return this.#getAge()
    }
}
let p = new Person('小明', 18)
  1. 私有原型成员
class Person {
    #age = 0
    constructor(name, age) {
        this.name = name
        this.#age = age
    }
    #getAge() {
        return this.#age
    }
    sayAge() {
        return this.#getAge()
    }
}
let p = new Person('小明', 18)
  1. 私有静态成员
class Person {
    static #count = 0
    constructor(name) {
        this.name = name
        Person.#count++
    }
    static sayCount_1() {
        return Person.#count
    }
    static sayCount_2() {
        return this.#count
    }
}
class Student extends Person {
    constructor(name, grade) {
        super(name)
        this.grade = grade
    }
}
let p = new Person('小明')
let s = new Student('小芳', '高一')
console.log(Person.sayCount_1())        //2
console.log(Person.sayCount_2())        //2
console.log(Student.sayCount_1())       //2
console.log(Student.sayCount_2())       //报错

静态私有成员拒绝派生类通过原型链访问,而这通常会导致一些问题。
Student.sayCount_2() 相当于在执行 Student.__proto__.#count

4. 继承

class Person {
    static #count = 0
    constructor(name) {
        this.name = name
        Person.#count++
    }
    static sayCount() {
        return Person.#count
    }
}
class Student extends Person {
    constructor(name, grade) {
        super(name)
        this.grade = grade
    }
    static sayCount() {
        return super.sayCount()
    }
}  

let s = new Student('小明', '高三')
console.log(Student.sayCount())         //1

super 使用方法:

  1. constructor 中使用 super() 会调用父类的构造函数,并将返回的实例赋值给 this ,在 super 之前不能使用 this
  2. 如果派生类没有定义 constructor 函数,会自动调用 super() 并传递所有参数,如果定义了 constructor 则必须手动调用 super() 并传递参数。
  3. constructor 和 原型方法中 super.xxx 可以访问父类的原型方法和属性。
  4. 在静态函数中 super.xxx 可以访问父类的静态方法和静态属性。