本文总结了原型以及原型链的一些概念和规律,并且提供了代码示例进行说明。有助于更深一步的理解原型与原型链

一、原型

简介

  • 每个函数对象都有一个 prototype 属性,默认指向一个空的 Object 实例对象(即称为原型对象)
  • 原型对象中有一个 constructor 属性,它指向当前的函数对象

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Test() {}

Test.prototype.say = () => {
console.log('Say Hello');
}

const t1 = new Test();
const t2 = new Test();

Test.prototype.say = () => {
console.log('Say Hello');
}

console.log("原型对象:", Object.keys(Test.prototype)); // [],说明函数的原型对象是一个空对象
console.log(Test.prototype.constructor === test); // true,说明 原型对象的 constructor 属性,它指向当前的函数对象

显式原型和隐式原型

  • 每个函数都有一个 prototype 属性,即显式原型
  • 每个实例对象都有一个 __proto__ 属性,即隐式原型
  • 实例对象的隐式原型的值等于对应函数的显式原型的值
1
2
3
4
5
6
7
function Test() {}

const t1 = new Test();

console.log(Test.prototype); // 一个空对象,包含 __proto__ 和 constructor 属性(指向 Test)
console.log(t1.__proto__); // 一个空对象,包含 __proto__ 和 constructor 属性(指向 Test)
console.log(Test.prototype === t1.__proto__); // true

显式原型和隐式原型的实现机制

  • 函数的 prototype 属性:在定义函数时自动添加的,默认值是一个空的 Object 对象
  • 对象的 __proto__ 属性:创建对象时自动添加的,默认值为构造函数的 prototype 属性值
  • 开发人员一般都是通过操作函数的 prototype 属性来修改原型

原型补充

  • 函数的显式原型指向的对象默认是空 Object 对象(但是 Object 除外,Object 的原型不是空对象,且没有 __proto__ 属性)

    • 所以 Object 的原型对象是原型链的尽头,Object 的原型对象的原型就是 null
      1
      2
      3
      4
      5
      6
      7
      8
      9
      function Test() {}

      const o = {};
      const t = new Test();

      console.log(o.__proto__); // 包含多个方法的 Object 对象,没有 __proto__ 属性
      console.log(t.__proto__); // 空对象

      console.log(o.__proto__.__proto__); // null
  • 每个函数对象既有一个隐式原型,也有一个显式原型

    • 所有函数都是 Function 的实例(包括 Function 自身)
    • 每个函数的隐式原型都是同一个实例对象
    • 唯一的一个隐式原型等于显式原型的函数对象就是 Function 这个函数对象
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      function Test() {}

      function Hello() {}

      console.log(Test.prototype); // 空对象
      console.log(Test.__proto__); // ƒ () { [native code] }

      console.log(Hello.prototype === Test.prototype) // false
      console.log(Hello.__proto__ === Test.__proto__); // true
      console.log(Hello.__proto__ === Function.__proto__); // true
      console.log(Function.prototype === Function.__proto__); // true

二、原型链(别名:隐式原型链)

概念

  • 访问一个对象的属性时

    • 先在自身属性中查找,找到返回
    • 如果没有,再沿着 __proto__ 这条链向上查找,找到返回
    • 如果最终没有找到,返回 undefined
  • 作用:查询对象属性

对象读写

  • 读取对象的属性时,会自动到对象的原型链中查找

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function Person() {}

    Person.prototype.speak = () => {
    console.log('Speak Chinese!');
    }

    const p1 = new Person();
    const p2 = new Person();

    // 每个实例对象都拥有函数原型中的属性/方法
    p1.speak(); // Speak Chinese!
    p2.speak(); // Speak Chinese!
  • 设置对象的属性时,不会查找原型链,如果当前对象中没有此属性,直接添加此属性并设置值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function Person() {}

    Person.prototype.name = '小明';

    const p1 = new Person();
    const p2 = new Person();

    // 读取属性时,会从当前对象以及原型链中进行查找
    console.log(p1.name); // 小明

    // 设置属性时,直接修改当前对象上的属性,当前对象上没有,则直接添加,不会修改原型链上的属性
    p2.name = '小红';

    console.log(p1.name); // 小明
    console.log(p2.name); // 小红
  • 方法一般定义在原型上,属性一般通过构造函数定义在对象本身上

扩展:instanceOf 操作符原理

  • a instanceOf C: 如果 C 的显式原型在 a 的隐式原型链上,返回 true,否则返回 false
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function A() {};

    function B() {};

    function C() {}

    // 将 C 的原型赋值给 B 的原型,将 B 的原型赋值给 A 的原型,这两行代码位置不能颠倒
    B.prototype = C.prototype;
    A.prototype = B.prototype;

    const a = new A();
    console.log(a instanceof C);

三、实例讲解

1、实例一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function A() {}

A.prototype.n = 1;

// 对象的原型在对象初始化时赋值为函数的显示原型,因此 b 的隐式原型为 { n: 1 };
const b = new A();

A.prototype = {
n: 2,
m: 3
}

// 原因同上,因此 c 的隐式原型为 { n: 2, m: 3 };
const c = new A();

console.log(b.n, b.m, c.n, c.m); // 1 undefined 2 3

实例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function Test() {}

Object.prototype.a = function() {
console.log('a 方法执行');
}

Function.prototype.b = function() {
console.log('b 方法执行');
}

var t = new Test();

/**
* 这里解释一波
* 1、每个函数对象的显式原型都指向一个空对象,而每个实例对象的隐式原型 === 函数对象的显式原型,
* 那么 t.a() 会按照这个顺序查找: t.__proto__ === Test.prototype = {}; {}.__proto__ === Object.prototype;
* 而 Object 的显式原型中有方法 a,因此可以正确执行;
* 2、t.b() 同理,在原型链中没有找到 b 方法,因此会报错:t.b is not a function;
* 3、Test.a() 的查找顺序:Test.__proto__ === Function.prototype = { b: function() { console.log('b 方法执行'); } };
* {b: fucntion() {console.log('b 方法执行');}}.__proto__ === Object.prototype;
* 因此:Test.a() 和 Test.b() 都可以正确执行
*/
t.a(); // a();
t.b(); // t.b is not a function
Test.a(); // a();
Test.b(); // b();

四、总结

  • 每个函数对象(Object 函数对象除外)都默认有一个显式原型对象,指向一个空的 Object 对象,空对象中有一个 constructor 属性,指向当前函数对象
  • 所有函数对象都有一个隐式原型对象(包括 Function 函数对象),且都默认指向 Function 函数对象的显式原型对象
  • 函数对象的显式原型等于实例对象的隐式原型
  • 访问对象属性时,会先在实例对象本身进行查找,找到则返回,没找到则在隐式原型链中继续查找,找到则返回,没找到返回 undefined

  • 额外补充一点:函数对象的隐式原型和显示原型没有关系(Function 函数对象除外),Function 函数对象的显式原型等于自身的隐式原型(因为所有函数都是 Function 的实例(包括 Function 自身))


五、推荐链接

之前看到这篇博客里有详细的原型链示意图,有兴趣的朋友可以详细去看一下:

一个例子让你彻底明白原型对象和原型链