Javascript 这门语言非常与众不同,它好像没有什么特别强的规定,这导致它异常灵活,但也很奇怪。在 Java 或者 C++ 中,实例化一个对象是很简单的事情:定义一个 类,然后 new 一个类。但是在Javascript中,没有类这个东西,所以光是对象的实例化就有好多种方式,对象的继承更是如此。
值得一提的是:每种方式都与众不同,每种方式都有自己独到之处。用的好就能把Javascript面向对象编程玩儿的出神入化;所有的这些方法,都有必要去了解,去实践,甚至精通!
虽然目的只是实例化对象和继承对象,但这并不是“回字的4种写法”
对象实例化
字面量模式
// 定义
let Person = {
name:'Jack'
age:25,
sayName:function(){
console.log('my name is'+this.name)
}
}
// 实例化
Person.sayName();
优点: 书写方便,定义后马上就能实例化,适合用来存储数据
缺点: 复用性差,所以也称为“单例模式”
工厂模式
所谓工厂模式就是定义一个函数(工厂),外界输入参数(原材料),然后运行这个工厂函数(生产),返回创建好的对象(产品)
// 定义
function CreatePerson(name,age){
let obj = new Object();
obj.name = name;
obj.age = age;
obj.eatFood = function(){
console.log('I am eating food');
}
return obj;
}
// 实例化
let Jack = CreatePerson('Jack',25)
// Jack就是createPerson类型的实例,可是却返回了 false
console.log(Jack instanceof createPerson) // false
**优点:**简单明了,一看就懂
缺点: 无法识别自定义的对象类型,因为该对象是通过函数来构造的
构造函数模式
// 定义
function Person(name,age){
this.name = name;
this.age = age;
this.eatFood:function(){
console.log('I am eating food')
}
}
// 实例化
let Jack = new Person('Jack',25)
优点: 根据传参来构造对象,这样的对象具有较好的独立性
缺点: 每实例化一个对象,就会重复的构造出对象中包含的方法,这些方法功能相同,重复创建会浪费内存
原型模式
// 定义
const Person = function (){};
Person.prototype = {
constructor:Person,
name: 'Jack',
age:25,
eatFood:function(){
console.log('I am eating food');
}
}
// 实例化
let Jack = new Person();
优点: 只需构造一个原型,其余实例化的对象都引用自这个原型对象,节省内存
缺点: 如果修改了原型对象的属性或方法,那么其余所有实例对象的属性或方法都会被更改;而且原型不能像函数一样接收参数,这样的对象没有独立性可言
原型+构造函数模式
// 定义
function Person(name,age){
// 需要自定义的的属性和方法放到构造函数中
this.name = name;
this.age = age;
}
Person.prototype = {
// 公共的属性和方法放到原型中
constructor:Person,
eatFood:function(){
console.log('I am eating food');
}
}
// 实例化
let Jack = new Person('Jack',25);
优点: 综合了构造函数模式和原型模式的优点,实例化的对象独立性比较高,也能很好的复用
动态原型模式
function Person(name, age) {
this.name = name
this.age = age
//定义方法前,要先检查是否已存在同名变量
if (typeof this.say != "function") {
// 不能使用对象字面量写法
Person.prototype.say = function () {
console.log(this.name)
}
}
}
const person = new Person("Jack", 29)
person.say()
**优点:**将属性和方法的定义都封装在构造函数中,这样代码可读性较高;可以确定其准确的类型
**缺点:**不能使用对象的字面量写法来给 prototype 赋值,如果定义的方法多,写法比较繁杂
最推荐的实例化对象写法
// 构造函数
function Person(name, age){
this.name = name
this.age = age
}
// 构造函数的原型对象
Person.prototype = {
constructor: Person,
gender: 'Male',
say: function(){
console.log(this.name, this.age, this.gender)
}
}
const person1 = new Person('Ethan', 28)
person1.say()
对于ES5来说,这是目前最推荐的实例化对象的方法;个人也觉得简单易懂,大部分场景下,都可以使用这种方式来创建对象
对象的继承
原型链继承
原型链继承的原理就是:将父类型的实例赋值给子类型的原型,这样子类型就能继承父类型的所有属性和方法
function Father() {
this.role = 'Father'
this.msg = 'this message is from Father'
}
Father.prototype.say = function () {
console.log(this.role)
}
function Child() {
this.role = 'Child'
this.age = 20
}
// 继承:将子对象的原型对象设置为父对象的实例
Child.prototype = new Father()
上面就是一个完整的实现了原型链继承的演示,其中最重要的一步就是:将 Child 的原型指定为 Father 的实例。
通过原型链,子类型可以继承到父类型的所有属性和方法,子类型还可以重写继承而来的方法或属性、添加自身的方法或属性
// 定义自身的方法
Child.prototype.hi = function () {
console.log(this.msg)
}
const child1 = new Child()
// 调用从父类型继承而来的方法
child1.say() // 'Father'
// 调用自身定义的方法,访问继承而来的属性
child1.hi() // 'this message is from Father'
注意:
-
子类型可以重写父类型中的方法,或者添加自定义的方法。但是,这些操作一定要放在替换原型的语句之后
-
子类型在自身的原型对象上,定义方法或属性时,不能使用对象字面量写法,因为这样会导致 prototype 被重写,继承也会无效
Child.prototype = { hi: function () { console.log(this.msg) } } const child1 = new Child() child1.say() // Uncaught TypeError: child1.say is not a function上面的代码之所以会报错,是因为:使用对象字面量写法来给 prototype 赋值,相当于重写了 prototype 对象,也会导致前面的
Child.prototype = new Father()这条语句等于没写
使用原型链来实现继承很简单,只需要把子类型的原型对象指向父类型的实例对象,但是它存在着一个不容忽视的问题:只要一个子类型修改了继承来的复合类型的属性,其余子类型中的复合类型属性也会被修改 。
要知道,所有实例对象都会共享原型对象的属性和方法,父类型实例中的属性和方法全都注入了子类型的原型中,原型一旦发生改变,所有的实例对象毫无疑问也会随之而变。
为了更清楚的解释,请看下面的代码:
function Father() {
this.list = [1, 2, 3]
}
function Child() { }
Child.prototype = new Father()
Child.prototype.say = function () {
console.log(this.list)
}
const child1 = new Child()
child1.say() // [1, 2, 3]
const child2 = new Child()
child2.list.push(4)
// child1 就受到了影响
child1.say() // [1, 2, 3, 4]
最后一行打印出 child1 打印出来的 list 继承自 Father,它应当是 [1, 2, 3],但是打印的结果却是 [1, 2, 3, 4] ? 这是因为 child2 修改了 list 属性,而 list 存在于 Father 的构造函数上(Child 的原型上),所以会造成 “牵一发而动全身” 的结果。这样的话,通过原型链继承而来的对象就毫无独立性可言了
借用构造函数继承
借用构造函数可以解决原型链继承中复合类型属性的问题。它的原理是:在子类型的构造函数中,调用父类型的构造函数
function Father(msg) {
this.msg = msg
this.list = [1, 2, 3]
}
function Child() {
// 继承
Father.call(this)
}
Child.prototype.say = function () {
console.log(this.list)
}
现在我们实例化两个对象,来看看这次复合类型的属性还会不会受影响
const child1 = new Child()
const child2 = new Child()
child2.list.push(4)
child1.say() // [1, 2, 3]
child2.say() // [1, 2, 3, 4]
好了,现在多个对象实例的复合类型属性各自独立,互不干扰
而且,借用构造函数模式的本质是通过 call 或 apply 在子类型的构造函数中调用父类型的构造函数,所以子类型可以向父类型传参:
function Father(name, age) {
this.name = name
this.age = age
}
function Child() {
// 继承
Father.call(this, 'Jack', 29)
}
Child.prototype.say = function () {
console.log(this.name, this.age)
}
const child = new Child()
child.say() // Jack 29
组合继承
原型链继承的优点就是属性和方法都定义在原型对象上,实例对象都能共享原型中的属性和方法;缺点则是实例对象都共享同一个原型,没有独立性可言。借用构造函数继承的优点则是实例的复合类型属性可以不受影响,因为方法和属性都定义在构造函数上;但是缺点也因此而生,属性和方法会被每个每个对象复制一份,没有复用性可言。
于是,原型和构造函数这对“好兄弟”再一次合作,产生了组合继承方式
组合继承结合了原型链和借用构造函数的优点,通过原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承,这样既保证了子类型的独立性又保证了父类型原型上方法的复用
function Father() {
// 父类型构造函数中的属性
this.msg = 'Hello World'
}
// 父类型原型上的方法
Father.prototype.say = function () {
console.log(this.name, this.age)
}
function Child(name, age) {
// 继承父类型构造函数中的属性
Father.call(this)
this.name = name
this.age = age
}
// 继承父类型原型上的方法
Child.prototype = new Father()
// 子类型自定义的方法
Child.prototype.hi = function () {
console.log(this.msg)
}
const child = new Child('Aelly', 18)
child.say() // 'Aelly', 18
child.hi() // 'Hello World'
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中最常用的继承模式。而且,instanceof 和isPrototypeOf() 也能够用于识别基于组合继承创建的对象
原型继承
Douglas Crockford (道爷)在2006年发明的继承方式,原理是:原型可以基于现有的对象而创建出新的对象,它实现起来非常简单!
function createObject(obj){
function F(){}
F.prototype = obj
return new F()
}
上面代码的意思是,定义一个空的函数 F ,将 F 的原型指向传入的对象,然后对 F 执行 new 操作,最后返回 new 出来的对象。函数 createObject 对传入的对象,执行了一次浅复制,相当于,以传入的对象为模板,创建了一个一模一样的新对象,然后返回。
原型继承的设计目的是:当你只想以一个对象为模板刻画出另一个对象,然后别有他用,同时你又不想大费周章的用上构造函数的话,那么你就可以使用原型继承。
注意: 包含复合类型值的属性始终都会被子类型共享或修改,就像使用原型链继承一样
const Father = {
name: 'Father',
list: [1,2,3]
}
const child1 = createObject(Father)
const child2 = createObject(Father)
console.log(child1.list) // [1,2,3]
child2.list.push(4)
console.log(child1.list) // [1,2,3,4]
ES6 中提供了一个 Object.create() 方法专门用来实现原型继承
寄生式继承
上面的原型继承是将传入的对象“复制一份”,然后返回,需要在函数 createObject 外部添加自定义的方法和属性。而寄生式继承,虽然名字听起来怪怪的,但其实就是在原型继承的基础上,把属性和方法定义封装在了一个函数中,然后函数返回继承后的对象
function createBy(original){
// 前面的 createObject 函数
const clone = createObject(original)
// 定义方法或属性
clone.hi = function(){
console.log(this.msg)
}
return clone
}
const Father = {msg: 'Hello', age: 29}
const child = createBy(Father)
child.hi() // 'Hello'
需要注意的是, createObject 函数不是必须的,只要是能够返回的一个新对象的函数,都是可以的
使用寄生式继承来为对象添加方法,会由于不能做到函数复用而降低效率——与构造函数模式类似。
寄生组合式继承
虽然组合继承是JavaScript最常用的继承模式,但是,它也存在缺点——任何情况下,都会调用两次父类型的构造函数;一次是在创建子类型原型的时候,另一次是在子类型构造函数内部
function Father() {
this.msg = 'Hello World'
}
Father.prototype.say = function () {
console.log(this.name, this.age)
}
function Child(name, age) {
// 第2次调用父类型的构造函数
Father.call(this)
this.name = name
this.age = age
}
// 第1次调用父类型的构造函数
Child.prototype = new Father()
函数的功能是固定,每调用一次就执行一次操作,但是同一个函数执行两次就有点“多余”。具有“代码洁癖”的程序员们为了解决掉这个问题,又提出了寄生组合式继承
寄生组合式继承的原理是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是父类型原型的一个副本而已,那么就通过寄生式继承来继承父类型的原型,通过借用构造函数来继承父类型构造函数中的属性
function inherit(Child, Father) {
// 1. 创建父类型原型的一个副本
const p = Object.create(Father.prototype)
// 2. 为副本添加constructor 属性,补上因重写原型而失去的默认的constructor 属性
p.constructor = Child
// 3. 将副本赋值给子类型的原型,从而实现继承
Child.prototype = p
}
function Father() {
this.list = [1, 2, 3]
}
function Child() {
Father.call(this)
}
// 继承操作
inherit(Child, Father)
Child.prototype.hi = function () {
console.log(this.msg)
}
const child1 = new Child()
const child2 = new Child()
child1.list.push(4)
child1.hi() // [1, 2, 3, 4]
child2.hi() // [1, 2, 3]
寄生组合式继承一种堪称完美的对象继承方式!通过代码你可以发现:
- 只调用一次父类型构造函数
- 子类型继承而来复合类型属性,不再受到实例的影响
- 继承了父类型构造函数中的属性,实现了子类型的独立性;继承了父类型原型对象中的方法,实现了函数的复用
简直完美!!!
本文所有内容都是参考自《Javascript高级程序设计》,文中代码都是亲手实现,并加上了一些自己的理解
如有错误,欢迎指出!!