不要再说你不懂原型链了---从原型、构造函数、实例开始理解原型链

489 阅读7分钟

LgHMYB7EytlcC6O6cI8_ZoEY_1920x1080.jpg 这几天,又重新梳理了原型链的相关知识,原型链是个怪圈,每次看都会又有一些新的体会,所以趁着记忆还是深刻的时候整理出文章,希望能帮助社区的同学对原型链有不一样的理解。

我是个没有写过文章的小白,可能写的思路会有些混乱,如果有错的地方欢迎大佬们指出~~

什么是原型链?

因为JavaScript是一个基于原型的语言。每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层的类推,这就是原型链。

所以当我们试图去访问一个对象的属性时,会先从这个对象自身去查找,如果这个对象自身找不到,则往对象的原型上找,如果对象的原型找不到,依次往上级原型查找,找到Object.prototype为止。因为Object.prototype === null

三个概念

原型(原型对象)prototype

原型对象就是一个在原型链上可被继承的属性及方法。也就是说被继承的属性及方法定义的地方。

举个例子:

const obj = {}
const myString = '我是文本'
console.log(obj , 'obj')
console.log(myString , 'myString')

从打印的结果

image-20210813172816778.png

myString.__proto__打印的结果

image-20210813173100616.png

image-20210813173204209.png

从打印结果可以看出,myString及obj从上级原型继承了很多的方法,这些方法就是从原型对象中继承过来的

仔细看一下两个的打印结果可以分析出来在myString.[[prototype]].[[prototype]]中,有一个跟obj的原型对象一样的原型对象,根据原型链的定义,每个对象都有一个原型对象,obj对象的原型对象的原型指向的就是Object的原型对象。

画个图可以这么理解

myString -> myString.__proto__(String.prototype) ->String.prototype.__proto__ -> Object.prototype

建议同学们在浏览器的控制台敲下这几句判断

myString.__proto__ === String.prototype // true
String.prototype.__proto__ === Object.prototype // true
myString.__proto__.__proto__ === obj.__proto__ //true

注意:没有官方的方法用于直接访问一个对象的原型对象——原型链中的“连接”被定义在一个内部属性中,在 JavaScript 语言标准中用 [[prototype]] 表示。

现在大多数的浏览器会提供一个__proto__的方法去访问对象的原型对象

构造函数constructor

用于创建和初始化函数创建的对象的特殊方法。就是new 后面的那个方法(new constructor)new constructor 会返回这个函数的实例化对象。

实例

通过new运算符返回的值,是一个对象。在new的过程中继承了构造函数和原型中的属性及方法。

语法

new constructor[([arguments])]
// constructor  一个指定对象实例的类型的类或函数
// arguments  constructor(构造函数)调用的参数列表 

new 运算符里面到底帮我们做了什么?

  • 从结果导向
    • 创建一个空的对象
    • 继承了构造函数及原型对象的属性方法
    • 绑定this为上面创建的空对象

实现

那下面我们就来实现一下吧~

function _new(fn, ...args) {
  // 1、创建一个空对象,里面继承了fn原型对象上的方法
  const obj = Object.create(fn.prototype);
  // 2、继承构造函数上的属性及方法,并改变this指向
  const res = fn.apply(obj, args);
  // 3、返回执行后的结果
  return res ? res : obj;
}

// 关于第二步如何继承了构造函数的属性及方法
// apply的作用:改变一个函数的this执行并执行这个函数。
// 在执行这个函数的过程中,我们会将函数属性拷贝了一份,这样就能得到构造函数上的属性及方法 

Object.create() 里面是怎么帮我们创建一个对象的呢?

返回一个新对象,使用现有的对象创建新创建对象的__proto__属性。

语法

Object.create(proto,[propertiesObject])
// proto  新创建对象的原型对象。
// propertiesObject 可选。传入一个对象  该对象的属性类型参照Object.defineProperties()的第二个参数。
// 以伪代码的形式补充Object.defineProperties的相关属性
Object.defineProperties(obj, prop:Prop)
interface Prop {
  configurable: boolean,
  enumerable: boolean,
  value?: unknown,
  writable: boolean,
  get?: function,
  set?: function,,
}

实现

function create(proto, prop) {
  // 1、创建一个空函数
  function F(){};
  // 2、将空函数的prototype指向传入的proto
  F.prototype = proto;
  // 3、给这个新函数添加属性
  if(Object.prototype.toString.call(prop) === "[object Object]") {
  Object.defineProperties(F, prop)
  }
  // 最后返回这个新函数
  return new F()
}

实例的属性

每个实例上都有一个__proto__的属性,指向构造函数的prototype 每个实例上有一个constructor属性,指向构造函数

写个代码验证一下吧~

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

所以我们能画出下面这个图

image-20210815075709398.png

原型、构造函数、实例三者之间的关系

构造函数与原型对象的关系

image-20210814163320799.png

Person.prototype === Person.prototype // true

有个小点可以补充一下,在实例中有一个从原型中继承的constructor属性,指向构造函数,如果遇到没有提供构造函数的情况下,可以使用 new instance.constructor() 当做一个构造函数使用。

原型对象(实例原型)与构造函数的关系

每个实例对象都从原型中继承了一个constructor属性,该属性指向了用于构造此实例对象的构造函数。

写个代码验证一下吧~

function Person() {}
console.log(Person === Person.prototype.constructor); // true

image-20210815083040859.png

实例与原型对象的关系

当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。

查找规则:从实例到原型,到原型的原型... Object.propotype -> null

function Person() {}
Person.prototype.name = 'Kevin';
var person = new Person();
person.name = 'Daisy';
console.log(person.name) // Daisy
delete person.name;
console.log(person.name) // Kevin
// 上面我们讲到,通过一个`__proto__`的属性能访问到原型对象,我们继续在控制台中验证一波
console.log(person.__proto__ === Person.prototype)// true

原型的原型

在JavaScript中,每个对象都拥有原型(除了null),每个原型也可能会有原型,对象原型上的属性及方法都是自己身上声明的和从上级原型上继承过来的。

image-20210815084145657.png

五条规则

综上所述,对于构造函数、原型对象、实例三者的关系可以总结出下面几句话:

  • 所有的引用类型(数组、对象、函数),都具有对象特性,即可自由扩展属性( null除外)
  • 所有的引用类型(数组、对象、函数),都有一个__proto__ 属性,属性值是一个普通的对象
    • 所有函数都有一个__proto__的属性,包括Function(Object 构造函数看作Function的实例)
  • 所有的函数,都有一个 prototype属性,属性值也是一个普通的对象
    • 所有的函数都有一个prototype的属性,包括Function
  • 所有的引用类型(数组、对象、函数), __proto__属性值指向它的构造函数的 prototype属性值
  • 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的__proto__ (即它的构造函数的prototype,也是原型对象 )中寻找

完整的原型链

R-C.jpeg

上面的图片中有两个需要注意的关系

Object.__proto__ === Function.prototype

Object可以看做是Function的实例,函数的__proto__等于Function.prototype

Function.__proto__ === Function.prototype

函数的构造函数的原型对象是自己的原型对象

从上面的原型图,我们可以整理出4条原型链

第一条:f1.__proto --> Foo.prototype --> Foo.prototype.__proto__ --> Object.prototype --> Object.prototype.__proto__ --> null

第二条: Foo.__proto__ --> Function.prototype --> Function.prototype.__proto__ --> Object.prototype--> Object.prototype.__proto__ --> null

第三条:Function.__proto__ --> Function.prototype -->Function.prototype.__proto__ --> Object.prototype--> Object.prototype.__proto__ --> null

第四条:Object.__proto__ --> Function.prototype --> Function.prototype.__proto__ --> Object.prototype--> Object.prototype.__proto__ --> null

每条原型链最后三个都是这样的Object.prototype--> Object.prototype.__proto__ --> null,说明JavaScript一切皆对象。

展望

下一篇文章的选题已经想好了,写的是原型链的下篇---继承,打算将JavaScript的每种继承方式以图片的形式展示出来,所以可能需要花费更多的时间

参考文章

对象原型

JavaScript深入之从原型到原型链