对象创建
首先,我们知道对象有两种创建方式:
字面量
创建法
// 字面量
const p = { age: 23, weight: 60 }
new
创建法
// 构造函数
function Person(age, weight) {
this.age = age
this.weight = weight
}
const p = new Person(23, 60)
console.log(p) // {age: 23, weight: 60}
或者使用es6语法
// 类
class Person {
constructor(age, weight) {
this.age = age
this.weight = weight
}
}
const p = new Person(23, 60)
console.log(p) // {age: 23, weight: 60}
对象的行为
上面创建了一个简单的对象,只有两个属性,但是通常情况下,对象还应该有些行为
(方法),
比如“说话”、“写字”、“唱歌”
我们给对象增加一个“说话”的方法
const p = {
age: 23,
weight: 60,
say() {
console.log(`大家好!我已经${this.age}岁了`)
}
}
我们思考,通过字面量
给对象添加方法似乎很方便,
但是考虑一种情况,如果我想创建很多个p对象,就得写很多个字面量,这样是不是有点不优雅
于是我们采用构造函数
方式添加实例方法
function Person(age, weight) {
this.age = age
this.weight = weight
this.say = () => {
console.log(`大家好!我已经${this.age}岁了`)
}
}
const p1 = new Person(23, 60)
const p2 = new Person(12, 30)
// ...
const pN = new Person(40, 70)
console.log(p1) // {age: 23, weight: 60, say:Function}
- 我们创建了n个p实例,每个实例都有了say方法,似乎我们的目标达成了,
- 但是回过头发现,这个say方法做的事同一件事情(输出自己的年龄),
- 给每一个p实例都加上这个方法好像没有必要,要是他们能共用一个say方法就好了,这样浏览器就不用创建那么那么多重复的say方法了。
- 该怎么办呢?我们尝试把say方法放到所有实例都能访问到的地方,也就是构造函数Person的原型(
prototype
)上,这样大家就共用一个方法了。 - 话说回来,为什么Person的
prototype
能被所有人访问到呢,这是因为在new Person()
之后,都会默认把p的一个属性__proto__
指向Person的原型(prototype)
改造代码:
function Person(age, weight) {
this.age = age
this.weight = weight
}
Person.prototype.say = () => {
console.log(`大家好!我已经${this.age}岁了`)
}
const p = new Person(23, 60)
console.log(p) // {age: 23, weight: 60}
控制台打印
可以看到,p的原型上多了一个say方法
- 接下来,我们考虑一个需求,我现在不单单想得到一个p实例,我想得到一个更强大的人,一个超人,并且他会飞。
- 普通的做法是,还是新建一个p对象,给这个p添加一个飞行(fly)的方法。这样也不是不可以,那如果我要创建很多个超人呢?给每个p添加fly?这样显然不合理。
- 又有同学说,直接在Person的原型上添加fly方法不就好了,我想说,如果你这么干了,就会影响到之前创建的那些p实例,他们莫名奇妙多了一个fly方法,搞得普通人都能飞了,显然不合理!
- 那应该怎么做的?其实在js中,对象可以拥有多个原型,上面的例子中我们可以理解为p拥有一个原型,因此继承了Person的一些方法,那么可不可以有一个对象同时继承了Person的一些方法和SuperMan的一些方法?这样他又可以说话又可以飞了
原型继承
// 普通人构造函数
function Person(age, weight) {
this.age = age
this.weight = weight
}
Person.prototype.say = () => {
console.log(`大家好!我已经${this.age}岁了`)
}
// 超人构造函数
function SuperMan(age, weight, height) {
this.age = age
this.weight = weight
this.height = height
}
SuperMan.prototype = new Person()// 直接把Person实例当作SuperMan的原型
SuperMan.prototype.fly = () => {
console.log(`大家好!我飞起来了,咻咻咻`)
}
const s = new SuperMan(23, 60, 2)
这里新建一个SuperMan构造函数,用来创建超人实例s
其中一步操作是把Person实例当作SuperMan的原型,这样会发生什么事?
- 首先s是SuperMan的
实例
,于是s肯定能访问到fly方法,紧接着SuperMan.prototype作为Person的一个实例,意味着SuperMan.prototype拥有一个__proto__
属性可以访问到say方法。 - 事实确实如此,当执行s.say() 的时候,js首先在s身上查找say方法,发现没有找到,于是查找s._proto_:发现还是没找到,继续查找s._proto_._proto_:在这里终于找到的say方法。
我们来测试一下,看看s到底是个什么东西
if (p instanceof Person) {
console.log('p 是个人,会说话')
}
if (p instanceof SuperMan) {
console.log('p 是个超人,会飞')
}
if (s instanceof Person) {
console.log('s 是个人,会说话')
}
if (s instanceof SuperMan) {
console.log('s 是个超人,会飞')
}
console.log('p的构造器是:' + p.constructor)
console.log('s的构造器是:' + s.constructor)
输出结果:
- p是普通人,输出“p 是个人,会说话”符合我们的预期,他肯定不是超人所以没有输出"p 是个超人,会飞",也符合预期
- s即使超人也是普通人,这合情合理,但是
instanceof
是怎么知道这一点的呢?其实instanceof 不仅考虑当前对象的类型,还考虑它继承的所有的对象,s虽然是作为SuperMan被创建的,但是他继承自Person,因此他也是普通人 - 接下来,p的构造器是Person,这合情合理,因为他就是Person创建的
- s的构造器也是Person,这不合理,因为他是使用SuperMan创建的,看看这是为什么:查看属性s.constructor,由于s身上和s._proto__身上都没有constructor,于是在s._proto.__proto__的身上找到了constructor,于是表现为Person。
坦率讲这是一个漏洞,下面我们来修复这个问题
function SuperMan(age, weight, height) {
this.age = age
this.weight = weight
this.height = height
}
SuperMan.prototype = new Person()// 直接把Person实例当作SuperMan的原型
SuperMan.prototype.constructor = SuperMan // 显式的声明SuperMan.prototype.constructor
继续处理其他问题:重复代码
你可能没注意到SuperMan和Person做了一些重复的事情
function Person(age, weight) {
this.age = age
this.weight = weight
}
function SuperMan(age, weight, height) {
this.age = age
this.weight = weight
this.height = height
}
他们都会去设置实例的age和weight这俩属性,显然,我们可以这么做
function SuperMan(age, weight, height) {
Person.call(this, age, weight)// 这行代码重用了Person中处理age, weight的代码
this.height = height
}
写到这里,就粗略的实现了原型链继承,s继承SuperMan,同时继承Person。如果继续深究,还继承了Object
继续处理下一个问题:原型污染
由于SuperMan.prototype = new Person(),你是否可以猜到,这句代码执行完成后,生成的对象有点"不干净",好像多了age和weight属性。我们希望SuperMan.prototype是个干净的空对象,而不要平白无故多了这两属性。虽然对代码运行没什么影响,我们还是可以做一点优化,把上面那句代码改写成
// SuperMan.prototype = new Person() 删除这句
SuperMan.prototype = Object.create(Person.prototype)// 改成这句
总结
实现一个构造函数继承另一个构造函数要做哪些事情
- 把父构造器的实例作为自己的原型
SuperMan.prototype = new Person()
// 或者 SuperMan.prototype = Object.create(Person.prototype)
- 声明自己的原型的constructor
SuperMan.prototype.constructor = SuperMan
- 父构造器在自己身上执行一下
function SuperMan(age, weight, height) {
Person.call(this, age, weight)// 这行代码重用了Person中处理age, weight的代码
this.height = height
}
最后,用es6的语法将会十分轻松的实现继承,上述代码可以改写成:
class Person {
constructor(age, weight) {
this.age = age
this.weight = weight
}
say() { }
}
class SuperMan extends Person {
constructor(age, weight, height) {
super(age, weight)// super在这里指代父类构造器 等效于Person.call(this, age, weight)
this.height = height
}
fly() { }
}
const s = new SuperMan(23, 60, 2)
console.log(s);