JavaScript 基础
Document
运算符
深入数据和类型
函数进阶
原型、继承
类
浏览器存储
Web API
事件
错误处理
异步编程
网络请求
模块
练习
实例
工具与规范
软件架构模式
设计模式
原型继承
前言
继承是面向对象中的一个重要特征,它使得我们可以扩展或修改对象。
比如我们有以下两个对象。
const animal = {
sleep() {
alert("睡觉")
},
}
const cat = {
sleep() {
alert("睡觉")
},
talk() {
alert("喵喵")
},
}
cat
与 animal
存在相同的方法 sleep
,那我们完全可以让 cat
继承自 animal
,此时 animal
便也是 cat
的原型。
原型继承
[[Prototype]]
在 JavaScript 的对象中,有一个隐藏属性 [[Prototype]]
(原型),这个属性要么只能为 null
或对象。
当我们在对象读取一个属性时,如果属性并不存在,则会到原型中去读取,如果原型中也不存在,则到原型的原型中去读取,直到碰到原型为 null
。
[[Prototype]]
是一个隐藏属性,并不允许直接赋值,不过也有很多设置的方法。
设置原型的方法
Object.setPrototypeOf 方法
$$jsdemo$$
$$edit$$
const animal = {
sleep() {
alert("睡觉")
},
}
const cat = {
talk() {
alert("喵喵")
},
}
// 设置 cat.[[Prototype]] 为 animal
Object.setPrototypeOf(cat, animal)
cat.talk() // 喵喵
cat.sleep() // 睡觉
当调用 cat.talk()
方法时,可以直接在 cat
中找到,因此直接使用。而 cat.sleep()
并未在 cat
中找到 sleep
方法,因此到其原型 animal
中读取并执行。
将 cat
的 [[Prototype]]
设置为 animal
后,可以称 animal
为 cat
的原型,或称 cat
继承自 animal
。
与之相对的,存在一个 Object.getPrototypeOf
方法读取对象的原型。
$$jsdemo$$
$$edit$$
const animal = {
sleep() {
alert("睡觉")
},
}
const cat = {
talk() {
alert("喵喵")
},
}
// 设置 cat.[[Prototype]] 为 animal
Object.setPrototypeOf(cat, animal)
console.log(Object.getPrototypeOf(cat)) // {sleep: ƒ}
$$warning
避免循环继承,像以下代码就是错误的。
$$
const animal = {
sleep() {
alert("睡觉")
},
}
const cat = {
talk() {
alert("喵喵")
},
}
// 设置 cat.[[Prototype]] 为 animal
Object.setPrototypeOf(cat, animal)
// Uncaught TypeError: Cyclic __proto__ value
Object.setPrototypeOf(animal, cat)
设置 __proto__ 属性
通过 __proto__
可以去设置或读取对象的 [[Prototype]]
,可以将 __proto__
理解为 [[Prototype]]
的 getter/setter
。
$$jsdemo$$
$$edit$$
const animal = {
sleep() {
alert("睡觉")
},
}
const cat = {
__proto__: animal,
talk() {
alert("喵喵")
},
}
cat.talk() // 喵喵
cat.sleep() // 睡觉
$$warning
__proto__
是一个历史遗留的特性,虽然浏览器仍然支持,但在标准中已经被放弃,可以参考 proto MDN 。
标准中更推荐使用 setPrototypeOf/getPrototypeOf
。
$$
Object.create 方法
Object.create
可以以一个对象为原型创建一个新的对象。
$$jsdemo$$
$$edit$$
const animal = {
sleep() {
alert("睡觉")
},
}
const cat = Object.create(animal)
cat.talk = function () {
alert("喵喵")
}
cat.talk() // 喵喵
cat.sleep() // 睡觉
写入时不会用原型
仅在读取时会使用到原型,写入时不会使用到原型。
$$jsdemo$$
$$edit$$
const animal = {
name: "动物",
sleep() {
alert("睡觉")
},
}
const cat = {
__proto__: animal,
talk() {
alert("喵喵")
},
}
cat.name = "猫猫"
alert(cat.name) // 猫猫
alert(animal.name) // 动物
this 的指向
在一个方法中, this
指向的永远是调用自己的对象,也就是符号 .
前面的对象。
因此在以下代码中尽管 setName
存在于 animal
中,但在 cat.setName
调用时指向的是 cat
。
$$jsdemo$$
$$edit$$
const animal = {
name: "动物",
setName(value) {
this.name = value
},
sleep() {
alert("睡觉")
},
}
const cat = {
__proto__: animal,
talk() {
alert("喵喵")
},
}
// setName 方法中的 this 指向 cat
cat.setName("猫猫")
alert(cat.name) // 猫猫
alert(animal.name) // 动物
原型继承的一些好处
节省内存
如果没有继承,我们必须写很多重复的代码,并且在运行时产生过多不必要内存的开销。
const cat = {
name: "猫猫",
sleep() {
alert("睡觉")
},
}
const dog = {
name: "狗狗",
sleep() {
alert("睡觉") // 重复
},
}
const rabbit = {
name: "兔兔",
sleep() {
alert("睡觉") // 重复
},
}
原型继承后可以共用一个 sleep
方法。
const animal = {
sleep() {
alert("睡懒觉")
},
}
const cat = {
__proto__: animal,
name: "猫猫",
}
const dog = {
__proto__: animal,
name: "狗狗",
}
const rabbit = {
__proto__: animal,
name: "兔兔",
}
一处修改处处应用
只需要修改原型的代码便能应用到所有继承的对象中。
面向对象
继承是面向对象的三大特性之一。
练习
- 说说以下代码的输出结果,为什么?
const animal = {
name: "动物",
}
const cat = {
name: "猫",
}
Object.setPrototypeOf(cat, animal)
alert(cat.name) // ?
delete cat.name
alert(cat.name) // ?
delete animal.name
alert(cat.name) // ?
$$answer
$$jsdemo$$
$$edit$$
const animal = {
name: "动物",
}
const cat = {
name: "猫",
}
Object.setPrototypeOf(cat, animal)
alert(cat.name) // 猫,cat 的 name
delete cat.name
alert(cat.name) // 动物,cat 没有 name,读取原型的
delete animal.name
alert(cat.name) // undefined,此时 animal 原型中也没有 name
$$
- 说说以下代码的输出结果,为什么?
$$jsdemo$$
$$edit$$
const animal = {
_name: "动物",
set name(value) {
this._name = value
},
}
const cat = {
__proto__: animal,
}
cat.name = "猫猫"
alert(cat._name) // ?
alert(animal._name) // ?
$$answer
const animal = {
_name: "动物",
set name(value) {
this._name = value
},
}
const cat = {
__proto__: animal,
}
// 执行了 animal 的 set name 访问器
// this._name 被赋值为 猫猫
// 此时的 this 指向 cat
cat.name = "猫猫"
alert(cat._name) // 猫猫
alert(animal._name) // 动物
$$
- 说说以下代码的输出结果,为什么?如果不符合预期的话应该怎么修改?
$$tip
hasOwnProperty 可以判断一个属性是否属于某个对象自身(非原型)。
$$
const animal = {
stomach: [],
eat(food) {
this.stomach.push(food)
},
}
const cat = {
__proto__: animal,
}
const dog = {
__proto__: animal,
}
cat.eat("鱼")
cat.eat("老鼠")
dog.eat("骨头")
alert(cat.stomach) // ?
alert(dog.stomach) // ?
$$answer
$$jsdemo$$
$$edit$$
const animal = {
stomach: [],
eat(food) {
// 因为 this(cat、dog)中并不存在 stomach
// 因为读取 this.stomach 时会读取原型 animal 的 stomach
// 所以如果直接 push 会将所有 food
// push 至原型 animal 的 stomach 中
// 改进如下
// 如果自身(非原型)有了 stomach 属性
// 则直接 push
if (this.hasOwnProperty("stomach")) {
this.stomach.push(food)
} else {
this.stomach = [food]
}
},
}
const cat = {
__proto__: animal,
}
const dog = {
__proto__: animal,
}
cat.eat("鱼")
cat.eat("老鼠")
dog.eat("骨头")
alert(cat.stomach) // 鱼,老鼠
alert(dog.stomach) // 骨头
$$