阅读 829

这一次彻底理清JavaScript的原型链

JS是一门基于原型的方式来实现继承的语言,对于刚刚入门的初学者来说,原型继承还是比较难理解的,而且加上JS的历史原因,导致JS的基于原型继承存在一定的失误,使得JS的原型链看起来很是诡异。今天来试着彻底的理清JS的原型以及原型链的设计,话不多说,让我们直接开始吧。

1. JavaScript原型

1. Object

在谈论JS中的原型之前,有必要先来了解一下JS中的 Object。 Object作为JS的原始类型之一,它是由键值对的形式构成,键名可以为String或Symbol,键值可以为JS中的任意类型。通常创建Object的方式有三种


let obj1 = {}; // 字面量方式创建 Object
let obj2 = Object.create(); // ES5中新增的Object.create()
let obj3 = new Constructor() // 通过new 关键字来创建构造函数的实例,实例就是一个Object
复制代码

而今天要说的就是通过 new Constructor() 生成Object的方式来理解原型以及原型链

2. 原型

JS中当调用 new Constructor() 之后,会生成一个 Object 。 这个Object 自动拥有 Constructor 内部的属性。

function Person(name, age) {
this.name = name;
this.age = age;
this.say = function () {
console.log(`my name is ${this.name} and I am ${this.age} years old`);
}
}
// 实例会继承构造函数的全部属性 对象的方法其实也是它的一种属性
const mike = new Person('mike', 11)
const jack = new Person('jack', 12)
console.log(mike); // Person {name: "mike", age: 11, say: ƒ}
console.log(jack); // Person {name: "jack", age: 12, say: ƒ}
mike.say() // my name is mike and I am 11 years old
jack.say() // prototype.js:12 my name is jack and I am 12 years old
复制代码

而在创建Object的过程中,JS会自动的创建一个Object,且称他为prototypeObject ,并将 Constructor的prototype属性指向prototypeObject,于此同时,prototypeObject内部有一个constructor 属性指向Constructor ,所以会有

console.log(Person.prototype.constructor === Person); // true
复制代码

prototypeObject便叫做Constructor的原型 ,Constructot叫做构造函数,而Object叫做实例(Instance)。对于一个构造函数函数来说可以拥有无数个实例,而每个实例仅仅只存在一个构造函数。对于实例与原型的关系来说,根据ECMAScript 标准规定:

All ordinary objects have an internal slot called [[Prototype]]. The value of this internal slot is either null or an object and is used for implementing inheritance. Data properties of the [[Prototype]] object are inherited (and visible as properties of the child object) for the purposes of get access, but not for set access. Accessor properties are inherited for both get access and set access. ——《ECMA-262》

实例内部存在一个 [[prototype]] 的属性链接到原型上,通过这个属性可以在实例中访问到原型上的属性与方法。

⚠️ 注意:在JS使用者的角度中其实是访问不到 [[prototype]] 属性的

不过现在的主流浏览器都实现了__proto__的属性,通过访问实例的__proto__属性,可以找到原型对象:

// 在 chrome浏览器中
console.log(mike.__proto__ === Person.prototype); // true

Person.prototype.eat = function() {
console.log(`${this.name} is eating!`);
}
mike.eat() // mike is eating!
jack.eat() // jack is eating!

复制代码

那么现在应该很清楚构造函数、原型以及实例之间的关系了吧,让我们用图形来总结一下:
prototype.png

  1. 通过构造函数可以创建无数个实例,一个构造函数对应众多实例,
  2. 一个构造函数对应一个原型,即原型与构造函数是一对一的,
  3. 原型与实例也是一对多的关系,众多的实例会共享原型的属性,

所以在当调用实例的某个属性时,如果实例中不存在,那么JS就会自动去实例的原型里面查找这个属性。倘若原型里面也没有找到的话,又将会发生什么样好玩的事呢?

2. 原型链

理解了第一点的原型结构,那么再来理解原型链就变得很容易了。从第一点我们可以知道当访问实例的属性时,若该属性不存在实例中,那么JS会自动的去实例原型中去查找,而我们知道,实例原型同样也是一个Object,所以按照Object的规则,它肯定也会拥有构造函数以及原型(原型的原型,听起来比较绕...),所以在实例与原型之间就形成了一条如下的链式结构
prototype-chain.png
即 instance -> prototype -> prototype1 形成一条链式结构,当我们执行instance.data 时,JS引擎会

  1. 在 instance中查找data属性,如果instance内存在data属性,那么会返回 instance内部的data属性
  2. 如果在 instance中没有查找到data属性,那么会去prototype 内部查找,如果找到,那么会返回 prototype内部的data属性
  3. 如果在 prototype中没有查找到data属性,那么会去prototype1 内部查找,如果找到,那么会返回 prototype1内部的data属性
  4. 如果一直查到最顶层的原型中都没找到的话,那么会返回 underfine

以上就是 JS引擎基于原型链查找属性的行为,那么现在还有一个问题,最顶层的原型是谁

3. 原型链的另一端

我们可以通过一个实例来一直向上查找,来看看原型链最顶层的元素,以上面的实例mike为例:

console.log(mike.__proto__) // {eat: ƒ, constructor: ƒ}
console.log(mike.__proto__.__proto__) // { constructor: ƒ Object() ... }
console.log(mike.__proto__.__proto__.__proto__) // null
复制代码

上面我们可以看到在原型链最顶端的竟然是 null ,而原型为null 的实例的constructor 是JS内置的Object,那么这个以null 为原型的实例可以表示为Object.prototype 。由此可以写出一条完整的链式结构图:
instance.png

所以一条JS完整的原型链就表示出来了,在访问实例属性的时候会沿着上图的顺序,从上往下依次查找直到查找到null。

总结

在JS中,原型链是比较基础的知识,同时也是比较重要的知识,js 引擎在访问对象中的属性时会沿着原型链一次一次向上查找,所以可以通过这个特性,可以使得前端很灵活的使用 JS 的继承特性来方便的实现想要的效果。

文章分类
前端
文章标签