1. JavaScript 基础

  2. Document

  3. 运算符

  4. 深入数据和类型

  5. 函数进阶

  6. 原型、继承

  7. 浏览器存储

  8. Web API

  9. 事件

  10. 错误处理

  11. 异步编程

  12. 网络请求

  13. 模块

  14. 练习

  15. 实例

  16. 工具与规范

  17. 软件架构模式

  18. 设计模式

原型继承

前言

继承是面向对象中的一个重要特征,它使得我们可以扩展或修改对象。

比如我们有以下两个对象。

const animal = {
    sleep() {
        alert("睡觉")
    },
}

const cat = {
    sleep() {
        alert("睡觉")
    },
    talk() {
        alert("喵喵")
    },
}

catanimal 存在相同的方法 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 后,可以称 animalcat 的原型,或称 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: "兔兔",
}

一处修改处处应用

只需要修改原型的代码便能应用到所有继承的对象中。

面向对象

继承是面向对象的三大特性之一。


练习

  1. 说说以下代码的输出结果,为什么?
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

$$

  1. 说说以下代码的输出结果,为什么?
$$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) // 动物

$$

  1. 说说以下代码的输出结果,为什么?如果不符合预期的话应该怎么修改?

$$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) // 骨头

$$