使用 new 构造函数来创建对象:
ES5 中是用构造函数去模拟类,构造函数名需要大写。通过 new 构造函数
来创建对象。
构造函数的正确写法:
- 由于属性占用的内存空间小,一般会将属性定义在实例对象上。
- 但是为了避免多个实例对象创建多个方法,一般会将方法定义在原型上。
使用 new 关键字调用构造函数创建对象,会执行以下操作:
- 在内存中创建一个空的新对象。
- 将这个新对象赋值给构造函数中的 this。
- 将构造函数的显式原型赋值给这个新对象的隐式原型。
- 执行构造函数中的代码。
- 如果构造函数没有返回其他对象,就将这个新对象默认返回。
// 创建构造函数
function Person() {
// 1. 创建一个空的新对象 {}
// 2. this = {}
// 3. {}.__proto__ = Person.prototype
// 4. 执行代码
// 5. 返回对象
}
// 使用 new 构造函数来创建实例对象
var p = new Person()
实例属性、实例方法:
实例属性、实例方法:定义在实例对象身上,通过实例对象来调用。
function Person (name) {
// 给实例对象添加属性:实例属性
this.name = name
// 给实例对象添加方法:实例方法
this.showName = function () {
console.log('showName:' + this.name)
}
}
var p = new Person('Lee')
console.log(p.name) // Lee。实例对象能访问实例属性和实例方法,在访问 p.name 时,在实例对象 p 自身上就找到了
原型属性、原型方法:
原型属性、原型方法:定义在构造函数的显示原型对象上,会被实例对象继承,可以通过 构造函数.prototype
或者实例对象来调用。
function Person (name) {}
// 给构造函数的原型对象添加属性:原型属性
Person.prototype.age = 18
// 给构造函数的原型对象添加方法:原型方法
Person.prototype.showAge = function(){
console.log('showAge:' + this.age)
}
var p = new Person()
console.log(p.age) // 18。实例对象能访问原型属性和原型方法。在通过 new Person 构造函数创建实例对象 p 时,会将构造函数 Person 的显式原型赋值给实例对象 p 的隐式原型,在访问 p.age 时,在实例对象 p 自身找不到,然后就去 p.__proto__,也就是 Person.prototype 上查找,就找到了
给构造函数的显式原型对象添加属性或方法。会影响到以前创建的实例对象,因为还是在旧的原型对象上增加的。
Person.prototype.name = 'Lee'
更改构造函数的显式原型对象,函数的显式原型指向了一个新的对象。不会影响到以前创建的实例对象,因为以前创建的实例对象的隐式原型还是指向旧的函数的显式原型。
Person.prototype = { name: 'Mary', age: 18, }
静态属性、静态方法:
静态属性、静态方法:定义在构造函数自身上,不会被实例对象继承,直接通过构造函数来调用。
function Person() {}
// 静态属性和静态方法:构造函数也是对象,因为可以直接给构造函数对象添加属性和方法,被称为静态属性和静态方法
Person.sex = '男'
Person.showSex = function(){
console.log('showSex:' + this.sex)
}
var p = new Person()
// 实例对象访问不到构造函数对象身上的静态属性和静态方法
console.log(p.sex) // undefined
console.log(Person.sex) // 男
实例属性、实例方法和原型属性、原型方法的区别:
-
实例属性、实例方法定义在实例对象自身身上,原型属性、原型方法定义在构造函数的显式原型对象上。
-
实例方法可以访问构造函数中的私有变量和私有方法;而原型方法,无法访问构造函数中的私有变量和私有方法。
function Person(){ // 私有变量 var message = "this is private message" // 通过 this 实现的实例方法 this.privateShow = function() { console.log(message) } } //通过 prototype 实现的原型方法 Person.prototype.publicShow = function(){ console.log(message) } var p1 = new Person(1) p1.privateShow() // "this is private message" p1.publicShow() // ReferenceError: message is not defined
-
通过 new 构造函数多次创建对象时,会为每个实例对象分别创建自己的实例属性和实例方法,而原型属性和原型方法只会创建一次。
function Person(name) { this.name = name this.showName = function() { console.log('showName:' + this.name) } } var p1 = new Person('Lee') var p2 = new Person('Mary') console.log(p1.showName === p2.showName) // false。每个通过 new Person 构造函数创建出来的实例对象 p 都会生成自己的 setName 方法,因此创建了很多重复的 setName 方法,造成了内存空间的浪费。
function Person(name) { this.name = name } Person.prototype.showName = function(name) { console.log('showName:' + this.name) } var p1 = new Person('Lee') var p2 = new Person('Mary') console.log(p1.showName === p2.showName) // true。无论 new Person 了多少次,setName 方法只创建了一次
使用 new 构造函数来实现继承:
子类构造函数可以继承父类构造函数的属性和方法。因此可以将重复的代码和逻辑抽取到父类构造函数中,子类构造函数继承后可以直接使用。
// 父类构造函数
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.show = function(name, age){
console.log('show 方法执行了')
}
利用原型链来实现继承:
利用原型链来实现继承:改变子类构造函数的显式原型对象,父类构造函数创建一个实例对象来作为子类构造函数的显式原型对象。
// 子类构造函数
function Student(name, age) {}
// 利用原型链来实现子类构造函数继承父类构造函数:父类创建一个实例对象来作为子类的显式原型对象
Student.prototype = new Person()
// 修改子类构造函数的显式原型的 constructor 指向
Student.prototype.constructor = Student
var s = new Student('Lee', 18)
console.log(s.name) // undefined
s.show() // show 方法执行了
查找 s.name
的过程:先在 s 对象自身查找,没有找到;然后去 s.__proto__
,也就是 Student.prototype
,也就是 new Person()
出来的实例对象上查找,找到是 name 是 undefined。
查找 s.show
的过下:先在 s 对象自身查找,没有找到;然后去 s.__proto__
,也就是 Student.prototype
,也就是 new Person()
出来的实例对象上查找,还是没找到;然后去 new Person().__proto__
,也就是 Person.prototype
上查找,最终找到了。
利用原型链来实现继承的问题:利用原型链可以实现对父类构造函数中原型属性和原型方法的继承,但是对于父类构造函数中的实例属性和实例方法无法实现继承。
Student.prototype = Person.prototype
不能通过直接将父类构造函数赋值给子类构造函数来实现继承,这样父类构造函数和子类构造函数的显式原型都指向同一个对象,那么改变子类构造函数的显式原型的话,导致父类构造函数的显式原型也改变了。
利用原型链 + 借用构造函数(组合继承)来实现继承:
在利用原型链实现对父类构造函数中原型属性和原型方法的继承的基础上,利用借用构造函数,在子类构造函数的内部调用父类构造函数,来实现对父类构造函数中实例属性和实例方法的继承。这种方式也称为组合继承。
// 子类构造函数
function Student(name, age) {
// 利用借用构造函数继承父类的实例属性和实例方法:在子类构造函数的内部调用父类构造函数。调用 Person 函数,并将 Person 函数中的 this 指向修改为 new Student() 创建出来的实例对象,为 new Student() 创建出来的实例对象添加实例属性和实例方法
Person.call(this, name, age)
}
// 利用原型链继承父类的原型属性和原型方法:父类创建一个实例对象来作为子类的显式原型对象
Student.prototype = new Person()
Student.prototype.constructor = Student
var s = new Student('Lee', 18)
console.log(s.name) // Lee
s.show() // show 方法执行了
组合继承已经大致上可以实现子类构造函数继承父类构造函数了,但是也存在一些问题:
- 会调用两次父类构造函数:一次是在创建子类构造函数的显式原型的时候;另一次是在子类构造函数内部调用借用构造函数的时候。
- 所有的子类实例对象实际上会有两份父类的属性:一份是在创建子类构造函数的显式原型的时候生成的,是在子类构造函数的显式原型上,也就是父类构造函数的实例对象上;另一份是在子类构造函数内部调用借用构造函数的时候生成的,是在子类构造函数的实例对象上。
利用寄生组合式继承来实现继承(最终方案):
ES6 中的 class 类转成 ES5 代码,就是使用的寄生组合式继承。
利用寄生组合式继承来实现继承:
- 核心就是创建一个对象作为中间对象,将其隐式原型指向父类构造函数的显示原型,然后修改子类构造函数的显示原型,使其指向创建的这个对象;修改子类构造函数的显式原型的 constructor 指向;让子类的隐式原型指向父类。
这一步解决上面的方案中
Person()
构造函数被调用两次,从而导致属性保存了两份的问题。 - 然后再利用借用构造函数继承父类构造函数中的实例属性和实例方法即可。
// 创建寄生式函数:SubClass 要使用 SuperClass 中的属性和方法,是寄生在 SuperClass 上的,这类函数就称为寄生式函数
function inherit(SubClass, SuperClass) {
// 1. 创建一个对象,使其隐式原型指向父类构造函数的显示原型;然后将子类构造函数的显式原型指向创建的对象,实现子类对父类原型属性和原型方法的继承。其实就是为了取代 Student.prototype = new Person()
SubClass.prototype = Object.create(SuperClass.prototype)
// 2. 修改子类构造函数的显式原型的 constructor 指向
SubClass.prototype.constructor = SubClass
// 3. 使子类作为一个对象时的隐式原型指向父类,实现子类对父类静态属性和静态方法的继承。
SubClass.__proto__ == SuperClass
}
function Student(name, age) {
// 继承父类构造函数的实例属性属性和实例方法
Person.call(this, name, age)
}
// 继承父类构造函数的原型属性、原型方法和静态属性、静态方法
inherit(Student, Person)
var s = new Student('Lee', 18)
console.log(s.name) // Lee
s.show() // show 方法执行了
Object 是所有类的父类:
Object 是所有类的父类。
function Person() {}
console.log(Person.prototype.__proto__ === Object.prototype) // Person.prototype 是一个对象,因此其隐式原型指向了 Object 构造函数的显示原型
Object 的显式原型对象上默认就存在一些方法。因为所有类都继承自 Object,因此所有类都有可以访问到这些方法。
console.log(Object.prototype)
function Person() {}
const p = new Person()
console.log(p.toString)