原型与原型链


# 原型与原型链

# 前言

网上有句话:JavaScript 中万物皆对象,对象皆出自构造函数(这里的万物主要指引用类型)。

比如下面代码:

function Foo() {

}
var foo = new Foo();
foo.name = 'hello';
console.log(person.name) // hello
1
2
3
4
5
6

在这个例子中,Foo 就是一个构造函数,我们使用 new 创建了一个实例对象 foo

# 函数对象和普通对象

在 ES6 以前没有 class 关键字,所以 JavaScript 用函数来模拟的类实现,也就是函数对象。因此 JavaScript 的对象分为函数对象和普通对象。

// 函数对象: typeof 打印出来是 function
Object
Function
function fun1(){};
const fun2 = function(){};
const fun3 = new Function('name','console.log(name)');
1
2
3
4
5
6
// 普通对象: typeof 打印出来是 object
const obj1 = {};
const obj2 = new Object();
const obj3 = new fun1();
const obj4 = new new Function();
1
2
3
4
5

# 三大属性

  • 对象独有的属性:

    • __proto__
    • constructor
  • 函数独有的属性(函数也是对象):

    • prototype
    • __proto__
    • constructor

# prototype

又称显式原型,每个函数在创建之后都会拥有一个 prototype 属性,它指向函数的原型对象(又称:它指向以当前函数作为构造函数构造出来的对象的原型对象)。

Foo(构造函数)Foo.prototype(构造函数的原型对象)prototype

(prototype 指向函数的原型对象)

作用:给其它对象提供共享属性,用来实现基于原型的继承与属性的共享。

ECMAScript 规范约定了访问和操作 prototype 属性的 API:

# __proto__

又称隐式原型,是每个对象(除了 null)都具有的属性,它指向该对象的原型。

Foo(构造函数)Foo.prototype(构造函数的原型对象、实例的原型)prototypefoo(实例对象)__proto__

(__proto__ 指向对象的原型)

// 实例对象的 __proto__  = 实例对象的构造函数的 prototype
foo.__proto__ === Foo.prototype;      // true

// 构造函数(也是个对象)的 __proto__ = Function 的 prototype
Foo.__proto__ === Function.prototype; // true
1
2
3
4
5

为什么叫隐式原型呢?因为这个 __proto__ 是一个隐藏的属性,它只是开发者工具方便开发者查看原型而故意渲染出来的一个虚拟节点,实则并不存在于该对象上。这其实是一个历史问题,当时一些浏览器私自实现了__proto__ 这个属性(后被 ES5 纳入规范),使得可以通过 obj.__proto__ 来访问对象的原型。

特点:

  • __proto__ 属性既不能被 for in 遍历出来,也不能被 Object.keys(obj) 查找出来。
  • 访问对象的 obj.__proto__ 属性,默认走的是 Object.prototype 对象上 __proto__ 属性的 get/set 方法。
Object.defineProperty(Object.prototype, '__proto__', {
  get(){
    console.log('get')
  }
});

({}).__proto__;
console.log((new Object()).__proto__);
1
2
3
4
5
6
7
8

作用:__proto__ 的本质是一个对象指向另一个对象(可以理解为父类对象),有了它,当访问一个对象属性的时候,如果该对象内部不存在这个属性,那么就会去它的 __proto__ 所指向的对象(父类对象)上查找,如此一层层往上查找,直到找到 null。所以可以说,__proto__ 构成了原型链,同样用于实现基于原型的继承。

提示

有时候还会看到一个 [[prototype]],它和 __proto__ 意义相同,均表示对象的内部属性,其值指向对象原型。前者在一些书籍、规范中表示一个对象的原型属性,默认情况下是不可以再被外部访问的,估计是会被一些内部方法使用的,例如用 for in 来遍历原型链上可以被枚举的属性的时候,就需要通过这个指针找到当前对象所继承的对象;后者则是在浏览器实现中支持的一个属性,用于指向对象的原型。

# constructor

每个原型对象都有一个 constructor 属性指向关联的构造函数。

Foo(构造函数)Foo.prototype(构造函数的原型对象、实例的原型)prototypefoo(实例对象)__proto__constructor

(原型对象的 constructor 指向构造函数)

// 原型对象的 constructor 指向关联的构造函数
Foo.prototype.constructor === Foo;                // true
// ES5 提供的 API
Object.getPrototypeOf(foo) === Foo.prototype;     // true
1
2
3
4

换言之:只有原型对象(prototype 对象)才有这个属性。但对于通过函数创建的实例对象,虽然没有这个属性,也能通过 __proto__ 获取原型对象然后间接找到这个属性。

// 通过 __proto__ 获取原型对象, 然后间接找到它的构造函数
foo.__proto__.constructor === Foo.prototype.constructor; // true
1
2

所以任何对象最终都可以找到其对应的构造函数。

# 理清错综复杂的关系

# 区分:__proto__prototype

  • __proto__ 指向的是当前对象(实例对象、函数对象)的原型对象。
  • prototype 指向以当前函数作为构造函数构造出来的对象的原型对象。

重要结论

实例的 __proto__ = 它的构造函数的 prototype

基于这个结论,可以推导出几个公式:

  • 如果 __proto__ 作用在实例对象上,prototype 作用在该实例对象的构造函数上,那么得到的结果是一个东西:
foo.__proto__ === Foo.prototype;            // true
1
  • 如果 __proto__ 作用在构造函数上,那么:
// JS 中所有函数都是 Function 的实例(函数也是对象 => 可以看成实例对象), 此时又回归了上一条公式
Foo.__proto__ === Function.prototype;       // true

// 由此可以类推内置函数, 因为它们也是由 Function 构造出来的
Object.__proto__ === Function.prototype;    // true
Number.__proto__ === Function.prototype;    // true
Boolean.__proto__ === Function.prototype;   // true
String.__proto__ === Function.prototype;    // true
Array.__proto__ === Function.prototype;     // true
RegExp.__proto__ === Function.prototype;    // true
Error.__proto__ === Function.prototype;     // true
Date.__proto__ === Function.prototype;      // true
1
2
3
4
5
6
7
8
9
10
11
12
  • Function 是老祖宗,它是它自己的构造函数:
Function.__proto__ === Function.prototype;  // true
1

# 区分:Object 和 Function

FunctionObject 都是构造函数,构造函数都有 prototype

所以它们的关系就比较绕了,但是只要记住几个最根本的结论,一切就可以慢慢推导了:

  • JS 中所有函数都是 Function 的实例,所以:
// JS 中所有函数都是 Function 的实例 (重要的结论 again)
// 实例的 __proto__ = 它的构造函数的 prototype (重要的结论 again)
Object.__proto__ === Function.prototype;         // true
Function.__proto__ === Function.prototype;       // true
1
2
3
4
  • 万物皆对象,原型对象也是对象,对象的构造函数是 Object,所以:
// 构造函数的原型对象 => 是个对象 => 是 Object 构造函数的实例对象
// 实例的 __proto__ = 它的构造函数的 prototype (重要的结论 again)
Foo.prototype.__proto__==Object.prototype;        // true

// 由此可以类推内置函数, 因为它们的原型对象的构造函数都是 Object
Function.prototype.__proto__==Object.prototype;   // true
Object.prototype.__proto__ === Object.prototype;  // true
Number.prototype.__proto__ === Object.prototype;  // true
Boolean.prototype.__proto__ === Object.prototype; // true
String.prototype.__proto__ === Object.prototype;  // true
Array.prototype.__proto__ === Object.prototype;   // true
RegExp.prototype.__proto__ === Object.prototype;  // true
Error.prototype.__proto__ === Object.prototype;   // true
Date.prototype.__proto__ === Object.prototype;    // true

// Object.prototype 是所有对象的顶层
// 或者说 Object.prototype 没有原型,原型链的尽头是 null
Object.prototype.__proto__ === null;              // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 原型链

原型链是 JavaScript 作者为了继承而设计的,简单理解就是从实例对象开始,通过 __proto__ 链接子父类对象,一层层查找对象自身拥有或继承的属性和方法,直到找到 null

整个查找的过程就像产生了一个链条,如下图所示:

fooFoo.prototype__proto__Object.prototype__proto__null__proto__

(原型链)

由图可知,查找属性的时候查到 Object.prototype 就可以停止查找了。

# 一张图回顾

下面这张图通过 __proto__prototype 把实例对象、构造函数、Object 和 Function 这些概念关联起来,乍一看很绕,但是如果能看懂,那么原型的这块知识就算理解了。

原型的核心还是那句话

实例的 __proto__ = 它的构造函数的 prototype

原型链fooFoo.prototypeObject.prototypenull__proto____proto____proto__FooFunction.prototypeObjectFunction__proto____proto____proto____proto__函数对象__proto__prototypeconstructorprototypeprototypeprototypeconstructorconstructorconstructorconstructorconstructorconstructorconstructorfunction Foo(){...};let foo = new Foo();

(原型关系图)

(完)