JavaScript原型和原型链

60 阅读4分钟

一、3个基础知识

在理解原型和原型链之前,需要知道几个重要的前提知识,这样利于理解后面的内容。

  1. 每个函数(箭头函数除外)都有prototype属性,且只有函数有,普通对象没有。
  2. 所有对象都有__proto__属性。因为函数是特殊的对象,因此函数既有prototype也有__proto__属性。
  3. obj.__proto__Object.getPrototypeOf(obj)obj[[Prototype]]这三者都是指对象obj的原型,也称原型对象、实例原型。

二、3个重要属性和原型

1. prototype

(1)函数(尤其是构造函数)自带一个 prototype 属性,这个属性指向该函数的原型,这个原型也是对象,因此叫原型对象。

image.png

(2)原型对象最主要的作用就是用来存放实例对象的公有属性和公有方法,实例可以继承这些属性和方法。

(1)prototype是“可作为构造函数的函数” 才有的属性,普通对象如 {}[]new Object() 创建的对象没有prototype属性,访问 obj.prototype 会返回 undefined
(2)函数对象如 function fn() {}class 声明的类有 prototype 属性,因为函数可作为构造函数,prototype 用于给实例提供原型。

function Person() {}
console.log(Person.prototype); // 存在,是一个对象

// 函数的 prototype 属性 → 原型对象(模板)
Person.prototype.sayHi = function() {
  console.log(`你好,我是${this.name}`);
};

const obj = {};
console.log(obj.prototype); // undefined

const arrow = () => {};
console.log(arrow.prototype); // undefined(箭头函数)

2. __proto__属性

(1)实例对象通过构造函数创建,并通过__proto__ 指向该实例对象的原型。

image.png

(2)函数是特殊的对象,因此也有__proto__属性,函数的__proto__指向问题3.2会详细说明。
(3)虽然__proto__是非ECMAScript标准的,但是许多 JavaScript引擎实际上实现了它,因此使用obj.__proto__可以访问到原型对象。现在的规范建议使用Object.getPrototypeOf(obj)来获取原型对象。 

function Person() {}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true

console.log(person.__proto__ === Object.getPrototypeOf(person)) // true

3. constructor属性

  1. 对于原型对象来说,它有个constructor属性,指向它的构造函数。

image.png

  1. 对于实例对象来说,实例对象本身没有constructor属性,但是 person.constructor是有值的,因为它会沿着原型链向原型对象查找,而原型对象是有constructor属性的,因此person.constructor就是Person.prototype.constructor
function Person() {}
var person = new Person()
Person.prototype.constructor === Person // true

person.constructor === Person.prototype.constructor // true

三、原型链

3.1 原型链相关知识

  1. 由于对象的构造函数是Object,所以对象的原型对象,就是Object.prototypeObject.prototype该对象比较特殊,它没有上一层的原型对象,它的__proto__指向的是null
  2. 每个对象通过__proto__指向该对象的原型,而原型也是对象,因此也通过__proto__指向自己的原型,以此类推,直到原型是 null 的对象(原型链的尽头Object.prototype)。这种一层一层指向的关系就是原型链。
  3. 基于以上,在获取实例对象的属性时,先访问自身的属性,如果有就返回,如果没有就向原型访问该属性,如果原型也没有该属性,就向原型的原型访问该属性,直到找不到为止,其实当到Object.prototype就可以停止查找了。
// 获取实例对象自身属性(方法)而非继承来的使用以下两个方法
obj.hasOwnProperty('属性名')
Object.hasOwn(obj, '属性名');

function Graph() { 
    this.vertices = [];
    this.edges = [];
}
Graph.prototype.addVertex = function (v) {
    this.vertices.push(v);
};
const g = new Graph();
// 自有属性,返回true
g.hasOwnProperty("vertices"); // true
Object.hasOwn(g, "vertices"); // true
// 自有属性没有该属性,返回false,且不向原型链读取
g.hasOwnProperty("nope"); // false
Object.hasOwn(g, "nope"); // false 
// 原型对象的方法,非自有,返回false
g.hasOwnProperty("addVertex"); // false
Object.hasOwn(g, "addVertex"); // false 
// 原型对象本身含有'addVertex'方法,返回true
Object.getPrototypeOf(g).hasOwnProperty("addVertex"); // true

// 因此不能使用“属性是否为undefined”来检查属性,因为属性名很可能就是'undefined',使用以上两个是最规范安全的

3.2 函数原型相关知识点

函数自带prototype属性,并且函数也是一种特殊的对象,因此也有__proto__属性。函数的原型func.__proto__指向什么呢?

  1. 在JavaScript中,Function是所有函数的构造函数,Array、Object、String以及上面定义的Person等函数都继承于Function。并且因为Function是构造函数,也有自己的实例,它的实例是Function()
  2. 知道了所有函数的构造函数都是Function之后,func.__proto__的指向就是Function.prototype。由于Function本身也是函数,因此Function.__proto__的指向也是Function.prototype
  3. 此外,Function.prototype也是对象,它的原型对象Function.prototype.__proto__指向原型链的尽头Object.prototype

对于第2点和第3点,就会出现一个FunctionObject鸡和蛋的问题,就是先有哪个的问题。首先应该明确就是先有Object.prototype,然后再有Function.prototype,可以理解为Function.prototype是“内置”的原型一样,然后其他所有构造函数都继承于Function.prototype

所以,完整的原型链图如下(图源:www.mollypages.org/tutorials/j…

image.png