JavaScript的继承方式
和经典的类继承模式不同,JavaScript使用的是基于原型的继承方式。
面向对象编程(OOP)
面向对象编程基本思想是一个对象是一个现实世界的模型,为了编程的方便,出现了一个叫抽象的概念,就是把某类事物共有的属性和方法提取出来,创建一个对象的模板,这个模板就是构造函数,它包含了某一类对象共有的数据和特性。通过实例化构造函数,可得到一个个具体的对象。
创建对象
举一个小例子,人都有姓名,性别、年龄,可以打招呼,这些可以抽象成一个构造函数,代码如下:
function Person (name, sex, age) {
this.name = name
this.sex = sex
this.age = age
this.sayHi = function () {
console.log(`Hi, my name is ${this.name}`)
}
}
实例化对象:
let personOne = new Person('lele', 'femal', 17)
console.log(personOne.name) // lele
personOne.sayHi() // Hi, my name is lele
通过运行的代码,可以看到我们成功通过构造创建了一个对象,对象可以访问构造函数里的属性和方法。
看到这里可能有些同学会有些迷惑,比如new关键字是什么,有什么作用。我们可以试一下,在不用new关键的情况下,写一个不使用new关键字就能实例化对象的“构造函数”,代码如下:
function creatNewPerson (name, sex, age) {
let obj = {}
obj.name = name
obj.sex = sex
obj.age = age
obj.sayHi = function () {
console.log(`Hi, name name is ${obj.name}`)
}
return obj
}
创建实例对象:
let personTwo = creatNewPerson('huanhuan', 'male', 20)
console.log(personTwo.name) // huanhuan
personTwo.sayHi() // Hi, name name is huanhuan
经测试,代码可以正常运行,得到的结果与用构造函数创建的对象结果相同,那么既然普通函数也可以作为构造函数使用,为什么JavaScript还提供一个构造函数,并且需要用new关键字才能实例化对象呢?我们先来比较一下两种方式的异同:
- 普通函数需要创建新对象并返回,构建函数不用
- 普通函数使用创建的新对象作为属性的载体,构造函数使用this作为属性的载体
- 普通函数创建对象无需关键字,构造函数需要关键字new
通过比对可以发现,构造函数简化了普通创建对象函数的步骤,让我们不用新建一个空对象和返回创建的对象,才能得到一个对象实例。而是只需要用this对对象的属性进行赋值,然后使用new关键字初始化构造函数,就可以得到一个想要的对象实例,所以我们可以知道,构造函数是JavaScript提供的一种更方便快捷的创建对象的方式,下面是摘自MDN的new操作符的解释。
使用new关键字会进行如下操作:
- 创建一个空的简单JavaScript对象(即{});
- 链接该对象(即设置该对象的构造函数)到另一个对象;
- 将步骤1心创建的对象作为this的上下文;
- 如果该函数没有返回对象,则返回this。
原型链
看到这里,我们已经知道如何抽象一个构造函数,以及如何实例化一个对象了。那么对象是如何实现继承的呢?
函数有一个特殊的prototype对象,prototype对象里面是构造函数和__proto__属性,构造函数指向当前的这个函数,__proto__指向当前构造函数的原型对象,prototype里面还可以放一些方法和属性。
构造函数也是函数,所以也拥有这个prototype对象。__proto__并不是标准的写法,标准的写法为[[prototype]],__proto__很多浏览器实现了,但是并没有标准化的一个属性,等同于[[prototype]],当前对象的原型对象,也就是构造函数的prototype对象,当在对象上面查找一个属性的时候,会先在当前对象上面查找,如果查找不到,会去它的原型对象上面查找,这个原型对象也就是__proto__指向的构造函数的prototype对象,而prototype对象又有自己的原型对象,这个链式查找的链,就是原型链。一层层的查找下去,直到查找到属性,或者到查找到为Null的原型对象,返回undefined值。
用前面的例子,我们可以给Person构造函数的原型添加一个hobby属性:
Person.prototy.hobby = 'reading'
当我们调用hobby属性时,它会先在personOne上面进行查找:personOne.hobby,personOne上面没有hobby属性,继续往上查找personOne.__proto__.hobby =Person.prototype.hobby,查找成功,返回reading。现在我们查找一个Person原型对象上不存在的属性 'job',personOne.__proto__.job = Person.prototype.job,查找失败,继续往上查找personOne.__proto__.__proto__ = Function.prototype,查找失败,继续往上查找personOne.__proto__.__proto__.__proto__= Object.prototype = Null,查找到Object的原型对象为Null,返回undefined。
继承的实现
通过上面的原型链作用机制,可以总结出一个结论:如果对象A的原型对象是B,那么A就可以访问B原型对象里的属性和方法,以及B继承的原型链里的所有的属性和方法,从而实现A对B的继承。
所以在JavaScript里面实现继承,只要修改原型对象的指向,并确保当前对象可以正常工作,整个继承过程就全部完成了。我们可以试着新建一个Teacher类,用它来继承Person,不过进行这一步之前,我们需要先把之前的代码优化一下。
每次实例化对象的时候,都会把构造函数初始化一下,但是对于构造函数里的方法来说,每次创建对象进行初始化,其实是不必要的耗损操作。前面我们提到,构造函数的prototype对象可以放方法和属性,实例化对象可以通过原型链访问到prototype对象上面的东西,所以可以把sayHi()方法提取出来,放到构造函数的prototype对象上面,代码如下:
function Person (name, sex, age) {
this.name = name
this.sex = sex
this.age = age
}
Person.prototype.sayHi = function () {
console.log(`Hi, my name is ${this.name}`)
}
测试:
let personOne = new Person('lili', 'femal', 12)
personOne.sayHi() // Hi, my name is lili
经测试,代码运行正常,现在新建一个Teacher类来继承Person的属性和方法:
function Teacher (name, sex, age, subject) {
Person.call(this, name, sex, age) // 在Teacher环境下执行Person的构造函数,获取Person构造函数内容
this.subject = subject
}
Teacher.prototype = Object.create(Person.prototype) // 把Person的原型对象设置为Teacher的原型对象的原型对象
Teacher.prototype.constructor = Teacher // 修复前面因改变原型对象指向造成的Teacher的constructor为空的问题
Teacher.prototype.intro = function () { // 在Teacher的原型上面添加方法
console.log(`Hi, I am a ${this.subject} teacher, my name is ${this.name}`)
}
上面的代码里出现了一个Object.create()函数,它是 ECMAScript5中引入了一个新方法,可以调用这个方法来创建一个对象,新对象的原型就是调用create方法时传入的第一个参数。
结合上面的代码,Teacher.prototype = Object.create(Person.prototype)这行代码的作用是把Teacher的原型对象的原型对象设为Teacher的原型对象,结果是得到这样一个链Teacher.__proto__ = Person.prototype,所以在Teacher实例化的对象上面查找属性或方法时,如果查找不到,会继续往上,到Person的原型对象上面查找。
__proto__指向的原型对象有两个属性,一个是prototype,已经通过Object.create()进行了赋值,但是另一个属性construtor现在还为空,需要我们手动的赋值,因为构造函数的构造函数指向的是当前的构造函数,所以有了代码Teacher.prototype.constructor = Teache。
为了验证代码的正确性,可以用代码对Teacher构造函数进行测试,测试代码如下:
console.log(Teacher.prototype)
打开调试模式可以看到如图所示,Teacher的原型对象为Person,Person的原型对象为Object,成功完成了Teacher到Person的继承。

下面实例化对象进行测试,测试代码如下:
let teacherOne = new Teacher('Mary', 'femal', 32, 'Math')
console.log(teacherOne)
打开调试模式,可以看到teacherOne的原型对象为Teacher,Teacher的原型对象为Person。不过为什么teacherOne中显示__proto__:Person我不太懂,有懂的同学麻烦告诉我一下,谢谢
