阿卡不拉阿卡不拉
Vue3
阿卡的博客
Vue3
阿卡的博客
  • Vue3

    • 快速入门

      • 搭建工程 👌
      • 模板语法
      • 响应式基础
      • 响应式常用 API
      • 计算属性
      • 类与样式绑定
      • 条件和列表渲染
      • 事件处理
      • 表单处理
      • 生命周期
      • 侦听器
      • 组件介绍
      • Props
      • 自定义事件
      • 组件v-model
      • 插槽
      • 前端路由介绍
      • KeepAlive内置组件
      • 状态管理库
      • 组件库介绍
    • 深入本质

      • 虚拟DOM本质
      • 模板的本质
      • 组件树和虚拟DOM树
      • 数据拦截的本质
      • 响应式数据的本质
      • 响应式的本质
      • 响应式和组件渲染
      • 实现响应式系统 1
      • 实现响应式系统 2
      • 图解EFFECT
      • 实现响应式系统 3
      • 手写computed
      • 手写watch
      • 指令的本质
      • 插槽的本质
      • v-model的本质
      • setup 语法标签
      • 组件生命周期
      • keepalive 生命周期
      • keepalive的本质
      • key的本质
    • 细节补充

      • 【Vue】属性透传
      • 【Vue】依赖注入
      • 【Vue】组合式函数 👌
      • 【Vue】自定义指令
      • 【Vue】插件
      • 【Vue】Transition
      • 【Vue】TransitionGroup
      • 【Vue】Teleport
      • 【Vue】异步组件
      • 【Vue】Suspense
      • 【Router】路由模式
      • 【Router】路由零碎知识
      • 【Router】路由匹配语法
      • 【Router】路由组件传参
      • 【Router】内置组件和函数
      • 【Router】导航守卫
      • 【Router】过渡特效
      • 【Router】滚动行为
      • 【Router】动态路由
      • 【State】通信方式总结
      • 【State】Pinia 自定义插件
      • 【场景】封装树形组件
      • 【场景】自定义 ref 实现防抖
      • 【场景】懒加载
      • 【场景】虚拟列表
      • 【场景】虚拟列表优化
      • 【第三方库】VueUse
      • 【第三方库】vuedragable
      • 【第三方库】vue-drag-resize
      • 【第三方库】vue-chartjs
      • 【第三方库】vuelidate
      • 【第三方库】vue3-lazyload
      • 【场景】Websocket 聊天室
      • 【Vite】✨ 认识 Vite👌
      • 【Vite】配置文件 👌
      • 【Vite】✨ 依赖预构建 👌
      • 【Vite】构建生产版本 👌
      • 【Vite】环境变量与模式
      • 【Vite】CLI
      • 【Vite】Vite 插件
  • 笔面试

    • HTML

      • HTML 面试题汇总
      • 文档声明
      • 语义化
      • W3C 标准组织
      • SEO
      • iframe
      • 微格式
      • 替换元素
      • 页面可见性
    • CSS

      • CSS 面试题汇总
      • CSS 单位总结
      • 居中方式总结
      • 隐藏元素方式总结
      • 浮动
      • 定位总结
      • BFC
      • CSS 属性计算过程
      • CSS 层叠继承规则总结
      • @import 指令
      • CSS3 calc 函数
      • CSS3 媒体查询
      • 过渡和动画事件
      • 渐进增强和优雅降级
      • CSS3 变形
      • 渐进式渲染
      • CSS 渲染性能优化
      • 层叠上下文
      • CSS3 遮罩
    • JavaScript

      • JavaScript 面试题汇总
      • ✨ let、var、const 的区别
      • JS中的数据类型
      • 包装类型
      • 数据类型的转换
      • 运算符
      • ✨ 原型链
      • ✨ this 指向
      • ✨ 垃圾回收与内存泄漏
      • ✨ 执行栈和执行上下文
      • ✨ 作用域和作用域链
      • ✨ 闭包
      • DOM 事件的注册和移除
      • DOM 事件的传播机制
      • 阻止事件默认行为
      • 递归
      • ✨ 属性描述符
      • class 和构造函数区别
      • 浮点数精度问题
      • 严格模式
      • ✨ 函数防抖和节流
      • ✨ WeakSet 和 WeakMap
      • ✨ 深浅拷贝
      • 函数柯里化
      • Node 事件循环
      • 尺寸和位置
    • 浏览器

      • 浏览器面试题汇总
      • ✨ 浏览器的渲染流程
      • ✨ 资源提示关键词
      • 浏览器的组成部分
      • IndexedDB
      • ✨ File API
      • ✨ 浏览器缓存
      • ✨ 浏览器跨标签页通信
      • Web Worker
    • 网络

      • 网络面试题汇总
      • 五层网络模型 👌
      • 常见请求方法 👌
      • ✨cookie👌
      • 面试题
      • 加密
      • ✨JWT👌
      • ✨ 同源策略及跨域问题 👌
      • 文件上传
      • ✨ 输入 url 地址之后
      • 文件下载
      • ✨ session
      • ✨ TCP
      • ✨ CSRF 攻击
      • ✨XSS 攻击 👌
      • ✨ 网络性能优化
      • 断点续传
      • 域名和 DNS
      • SSL、TLS、HTTPS 的关系
      • ✨ HTTP 各版本差异 👌
      • HTTP 缓存协议
      • ✨ WebSocket
    • 工程化

      • CMJ 和 ESM
      • npx
      • ESLint
    • Vue2

      • Vue 面试题汇总相关
      • 组件通信方式总结
      • 虚拟 DOM
      • v-model
      • 数据响应式原理
      • diff
      • 生命周期详解
      • computed
      • 过滤器
      • 作用域插槽
      • 过度和动画
      • 优化
      • keep-alive
      • 长列表优化
      • 其他 API
      • 模式和环境变量
      • 更多配置
      • 更多命令
      • 嵌套路由
      • 路由切换动画
    • Vue3

      • ✨ Vue3 整体变化 👌
      • ✨ Vue3 响应式变化 👌
      • ✨ nextTick 实现原理 👌
      • 两道代码题 👌
      • Vue 运行机制
      • 渲染器核心功能
      • 事件绑定与更新

✨ 原型链

面试题:

说一说你对 JS 中原型与原型链的理解?(美团 2019 年)

对一个构造函数实例化后,它的原型链指向什么?

  • 生产对象的方式
  • 原型对象与原型链
  • 原型链相关方法

简洁版

生产对象的方式

不同的语言,生产对象的方式其实并不相同,整体来讲,可以分为两大类:

  1. 基于类生产对象
  2. 基于原型生产对象

1. 基于类生产对象

这种生产对象的方式可能是最常见的方式,很多语言中要生产一个对象,都需要先书写一个类,然后通过类来实例化对象。

Java

public class Person {

  private String name;
  private int age;

  public Person(String name, int age) {
    this.name = name;
    this.age = age;
  }

  public void sayHello() {
    System.out.println("我的名字是" + name + ",我今年" + age + "岁");
  }

  public static void main(String[] args) {
    Person p = new Person("张三", 18);
    p.sayHello();
  }
}

Python

class Person:
  def __init__(self, name, age):
      self.name = name
      self.age = age

  def say_hello(self):
      print(f"我的名字是{self.name},我今年{self.age}岁")

p = Person("张三", 18)
p.say_hello()

PHP

class Person {
  private $name;
  private $age;

  public function __construct($name, $age) {
      $this->name = $name;
      $this->age = $age;
  }

  public function sayHello() {
      echo "我的名字是" . $this->name . ",我今年" . $this->age . "岁\n";
  }
}

$p = new Person("张三", 18);
$p->sayHello();

可以看到,很多主流的编程语言,都是通过实例化类的方式来产生对象。

但是,这并非唯一的方式。

2. 基于原型生产对象

还有一种方式,则是基于原型来生产对象。

这种方式的核心思想就是:先有一个对象 A,然后你要生产一个新的对象 B,就先克隆一份对象 A 从而得到新对象 B,新的对象 B 可以添加新的属性或者方法,对于对象 B 而言,对象 A 就是自己的原型对象。

image-20221212094353715

采用这种生产对象方式的语言,虽然不像上面所罗列的那些语言那么主流,但是也确确实实存在:

  1. Self - Self 语言彻底采用原型模式,没有类的概念,对象直接从其他对象克隆并可能修改。
  2. Lua - 虽然 Lua 中有模块和包的概念,但其表(table)结构可以用来实现原型式继承。
  3. Io - Io 是一个纯原型语言,所有对象都来自于克隆其他对象。
  4. Proto - 一个较少知的语言,专门设计为原型继承语言。

3. JS 生产对象

布兰登・艾奇在设计这门语言时,选择了原型的方式来生产对象,他给出的理由有两个:

  • 他自己本身是一个 Lisp 程序员,主要方向和兴趣是函数式编程,因此在编程范式上更喜欢属于声明式编程的函数式编程。
  • JS 设计的初衷,定位是一门面向非专业的开发人员(例如网页设计者)的语言。由于大部分网页设计者都没有任何的编程背景,因此这门语言应该尽可能的简单。

在 JS 中,你可以很轻松的查看一个对象的原型。

const obj = {};
console.log(obj.__proto__);

通过上面的例子,我们可以得出一个结论:在 JS 中,无论你这个对象是如何书写的,该对象都有自己的原型对象。

在 ES5 中提供了一个 Object.create 方法,该方法的第一个参数就可以指定对象的原型对象。

const person = {
    arm: 2,
    legs: 2,
    walk() {
        console.log("walking");
    },
};

const john = Object.create(person, {
    name: {
        value: "John",
        enumerable: true,
    },
    age: {
        value: 18,
        enumerable: true,
    },
});
console.log(john.__proto__ === person); // true

布兰登・艾奇最初在设计这门语言时的构想是很美好的,但是现实是很残酷的。

当时的大环境下,流行的是“基于类生产对象”的方式,其中又以 Java、C++ 这样的语言最为代表。另外,当时网景公司的整个管理层,都是 Java 语言的信徒,因此在 1995 年 5 月,网景公司做出决策,未来的网页脚本语言必须“看上去与 Java 足够相似”,但是需要比 Java 简单。

没办法,受到了公司高层的命令,布兰登・艾奇游不得不对 JS 进行改造,添加了 this、new 这些关键字,使其看上去像是基于类生产的对象。不过早期没有 class 关键字,怎么办呢?没错,就是使用 function 来模拟类,为了区分普通函数,一个不成文的规定就是构造函数名称首字母大写。

function Computer(name, price) {
    this.name = name;
    this.price = price;
}

这里的构造函数本身是普通的函数,但如果你使用 new 的方式来调用,执行机制则和普通的函数调用不一样,会经历如下的步骤:

  1. 创建一个空的简单 JS 对象(即 { } )
  2. 为步骤 1 新创建的对象添加属性 __proto__,将该属性链接至构造函数的原型对象
  3. 将步骤 1 新创建的对象作为 this 的上下文
  4. 如果该函数没有返回对象,则返回 this
function Computer(name, price) {
    // 1. 创建一个普通的对象
    // const obj = {};

    // 2. 设置该对象的原型对象
    // obj.__proto__ = Computer.prototype;

    // 3. 设置 this 的指向,指向该 obj
    // this ---> obj
    this.name = name; // {name: "华为"}
    this.price = price; // {name: "华为", price: 5000}

    // 4. 如果代码里面没有返回对象,那么返回该 this
    // return this;
}
const huawei = new Computer("华为", 5000);
console.log(huawei);

这其实就是 JS 中函数二义性的由来。

不过,不管 JS 如何模拟面向对象的特性,哪怕 ES6 甚至新增了 class 关键字:

class Computer {
    constructor(name, price) {
        this.name = name;
        this.price = price;
    }
}
const huawei = new Computer("华为", 5000);
console.log(huawei);

JS 底层仍然是一门基于原型的语言,这一点是不会改变的。现在不会变,未来,也不会变。

原型对象与原型链

1. 三角关系

假设对象是由构造函数生产的,前面我们说过,那只是表象,只是模拟,最终底层仍然采用的是原型的方式。并且构造函数、实例对象以及原型对象这三者之间,还存在一个著名的三角关系,如下图所示:

image-20240507170307699

这里的三角关系指的是:

  1. 构造函数
  2. 实例对象
  3. 原型对象

这三者之间的关系。在 JS 中,只要是由构造函数 new 出来的对象,都满足这样的关系,不管你是自定义构造函数还是内置的构造函数。

function Computer() {}
const c = new Computer();
console.log(c.__proto__ === Computer.prototype);
console.log(c.constructor === Computer);
console.log(c.constructor === Computer.prototype.constructor);
console.log("-------");
console.log([].__proto__ === Array.prototype);
console.log([].constructor === Array);
console.log("-------");
console.log((1).__proto__ === Number.prototype);
console.log((1).constructor === Number);
console.log("-------");
console.log(true.__proto__ === Boolean.prototype);
console.log(true.constructor === Boolean);

2. 原型链全貌图

整个原型链的全貌图如下:

image-20220824120315785

  • JS 中的对象大体上分为两大类:普通对象 和 构造器对象

  • 无论是 普通对象 还是 构造器对象,都会有自己的原型对象,通过 __proto__ 这个隐式属性,就能找到自己的原型对象,并且一直向上找,最终会到达 null.

  • 普通对象 和 构造器对象 的区别在于是否能够实例化,构造器对象可以通过 new 的形式创建新的实例对象,这些实例对象的原型对象一直往上找最终仍然是到达 null.

  • 只有 构造器对象 才有 prototype 属性,其 prototype 属性指向实例对象的原型对象

  • 所有 构造器对象 的原型对象均为 Function.prototype

  • 无论是 普通对象 还是 构造器对象,最终的 constructor 指向 Function,而 Function 的 constructor 指向自己本身。

  • Object 这个 构造器对象 比较特殊,实例化出来的对象的原型对象直接就是 Object.prototype,而其他的构造器对象,其实例对象的原型对象为对应的 xxx.prototype,再往一层才是 Object.prototype.

3. 原型链实际应用

学习原型相关的知识有什么用?

其实你只有了解了原型,你才能深刻的理解为什么方法要挂在原型对象上面。

例如:

function Computer(name, price) {
    this.name = name;
    this.price = price;
}
Computer.prototype.showPrice = function () {
    console.log(`${this.name}的电脑价格为${this.price}`);
};

const huawei = new Computer("华为", 5000);
const apple = new Computer("苹果", 8000);

之所以要挂在原型对象上面,是因为由构造函数实例化出来的每一个实例对象,属性值是不相同的,所以需要每个对象独立有一份。

但是对于方法而言,所有对象都是相同的,因此我们不需要每个对象拥有一份,直接挂在原型对象上面共用一份即可。

如下图所示:

image-20220824152905132

你现在也就能够理解,为什么所有的构造函数内置方法都是挂在原型对象上面的。

例如:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array

另外,虽然我们能够轻松的给内置的构造器函数添加属性和方法:

Number.prototype.isEven = function () {
    return this % 2 === 0;
};
Number.prototype.isOdd = function () {
    return this % 2 === 1;
};
const i = 42;
console.log(i.isEven()); // true
const j = 13;
console.log(j.isOdd()); // true

但是目前 JS 社区的大部分人都不推荐这么做,这样的做法往往被称之猴子补丁(monkey-patching)

大部分人的观点是“别耍流氓,不是你的对象别动手动脚”。

一种更好的最佳实践是继承想要修改的构造函数,在子类上面添加新的方法:

class myNum extends Number {
    constructor(...args) {
        super(...args);
    }
    zhangsan() {}
}
const i = new myNum(1);
i.zhangsan();

原型链相关方法

1. Object.getPrototypeOf( )

该方法用于查找一个对象的原型对象。

function Computer() {}
const c = new Computer();
console.log(Object.getPrototypeOf(c) === c.__proto__);

2. instanceof 操作符

判断一个对象是否是一个构造函数的实例。如果是返回 true,否则就返回 false

function Computer() {}
const c = new Computer();
console.log(c instanceof Computer); // true
console.log(c instanceof Array); // false
console.log([] instanceof Array); // true

3. isPrototypeOf( )

主要用于检测一个对象是否是一个另一个对象的原型对象,如果是返回 true,否则就返回 false

function Computer() {}
const c = new Computer();
console.log(Computer.prototype.isPrototypeOf(c)); // true
console.log(Computer.prototype.isPrototypeOf([])); // false
console.log(Array.prototype.isPrototypeOf([])); // true

4. hasOwnProperty( )

判断一个属性是定义在对象本身上面还是从原型对象上面继承而来的。

如果是本身的,则返回 true,如果是继承而来的,则返回 false

const person = {
    arm: 2,
    legs: 2,
    walk() {
        console.log("walking");
    },
};

const john = Object.create(person, {
    name: {
        value: "John",
        enumerable: true,
    },
    age: {
        value: 18,
        enumerable: true,
    },
});
console.log(john.hasOwnProperty("name")); // true
console.log(john.hasOwnProperty("arms")); // false

-EOF-

最近更新:: 2025/7/16 12:57
Contributors: AK
Prev
运算符
Next
✨ this 指向