构造函数
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() 执行时,会有以下操作:
- 在内存中创建一个空对象
obj - 这个新对象的
[[Prototype]]属性被赋值为构造函数的prototype属性,即obj.__proto__ = Person.prototype - 构造函数中的
this被赋值为新对象,即this指向obj - 执行构造函数内部的代码(给新对象添加属性)
- 如果构造函数显式
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. 私有属性
- js 中可以用
this._xxx表示私有属性。 - 可以通过
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 方法
class中的getter和setter与对象中获取函数和设置函数的行为相同。getter和setter不一定都要设置,只设置getter意味这该属性只读。非严格模式下,对该属性的赋值会被忽略,在严格模式下则会报错,而class作用域中是默认使用严格模式的。- 在设置了
get name和set name方法后,name属性并不实际存在于实例对象上,即使在constructor方法中this.name = xxx也不会在实例上新增属性,仅仅会调用一次set name方法。 - 上面的代码中,
Student的类块里声明了name属性,这不同于this.name = xxx添加属性。在类块里声明的属性是通过Object.defineProperty()添加到实例对象上的,因此实例对象上实际有了一个name属性,而set name()和get name()则会被屏蔽掉。
5. 私有成员
私有成员只能在 class 内才能被直接访问。
- 私有实例成员
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)
- 私有原型成员
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)
- 私有静态成员
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 使用方法:
- 在
constructor中使用super()会调用父类的构造函数,并将返回的实例赋值给this,在super之前不能使用this。 - 如果派生类没有定义
constructor函数,会自动调用super()并传递所有参数,如果定义了constructor则必须手动调用super()并传递参数。 - 在
constructor和 原型方法中super.xxx可以访问父类的原型方法和属性。 - 在静态函数中
super.xxx可以访问父类的静态方法和静态属性。