乐趣区

快速读懂-JS-原型链

最近加入了公司外部技术分享,分享同学提到了 Js 原型链的问题,并从 V8 的视角开展发散,刷新了我之前对原型链的意识,听完后决定重学一下原型链,坚固一下根底。

  • 了解原型链
  • 深刻原型链
  • 总结与思考

了解原型链

Js 中的原型链是一个比拟有意思的话题,它采纳了一套奇妙的办法,解决了 Js 中的继承问题。

按我的了解,原型链能够拆分成:

  • 原型(prototype)
  • 链(__proto__

原型(prototype)

原型(prototype)是一个一般的对象,它为构造函数的实例共享了属性和办法。在所有的实例中,援用到的原型都是同一个对象。

例如:

function Student(name) {
  this.name = name;
  this.study = function () {console.log("study js");
  };
}
// 创立 2 个实例
const student1 = new Student("xiaoming");
const student2 = new Student("xiaohong");
student1.study();
student2.study();

下面的代码中,咱们创立了 2 个 Student 实例,每个实例都有一个 study 办法,用来打印 “study js”。

这样写会有个问题:2 个实例中的 study 办法都是独立的,尽管性能雷同,但在零碎中占用的是 2 份内存,如果我创立 100 个 Student 实例,就得占用 100 份内存,这样算下去,将会造成大量的内存节约。

所以 Js 发明了 prototype。

function Student(name) {this.name = name;}
Student.prototype.study = function () {console.log("study js");
};
// 创立 2 个实例
const student1 = new Student("xiaoming");
const student2 = new Student("xiaohong");
student1.study();
student2.study();

应用 prototype 之后,study 办法寄存在 Student 的原型中,内存中只会寄存一份,所有 Student 实例都会共享它,内存问题就迎刃而解了。

但这里还存在一个问题。

为什么 student1 可能拜访到 Student 原型上的属性和办法?

答案在 __proto__ 中,咱们接着往下看。

链(__proto__

链(__proto__)能够了解为一个指针,它是实例对象中的一个属性,指向了构造函数的原型(prototype)。

咱们来看一个案例:

function Student(name) {this.name = name;}
Student.prototype.study = function () {console.log("study js");
};

const student = new Student("xiaoming");
student.study(); // study js
console.log(student.__proto__ === Student.prototype); // true

从打印后果能够得出:函数实例的 __proto__ 指向了构造函数的 prototype,上文中遗留的问题也就解决了。

但很多同学可能有这个疑难。

为什么调用 student.study 时,拜访到的却是 Student.prototype.study 呢?

答案在原型链中,咱们接着往下看。

原型链

原型链指的是:一个实例对象,在调用属性或办法时,会顺次从实例自身、构造函数原型、构造函数原型的原型 … 下来寻找,查看是否有对应的属性或办法。这样的寻找形式就如同一个链条一样,从实例对象,始终找到 Object.prototype,业余上称之为原型链。

还是来看一个案例:

function Student(name) {this.name = name;}
Student.prototype.study = function () {console.log("study js");
};

const student = new Student("xiaoming");
student.study(); // study js。// 在实例中没找到,在构造函数的原型上找到了。// 理论调用的是:student.__proto__.say 也就是 Student.prototype.say。student.toString(); // "[object Object]"
// 在实例中没找到。// 在构造函数的原型上也没找到。// 在构造函数的原型的原型上找到了。// 理论调用的是 student.__proto__.__proto__.toString 也就是 Object.prototype.toString。

能够看到,__proto__ 就像一个链一样,串联起了实例对象和原型。

同样,下面代码中还会存在以下疑难。

为什么 Student.prototype.__proto__ 是 Object.prototype?

这里提供一个推导步骤:

  1. 先找 __proto__ 后面的对象,也就是 Student.prototype 的构造函数。

    1. 判断 Student.prototype 类型,typeof Student.prototypeobject
    2. object 的构造函数是 Object。
    3. 得出 Student.prototype 的构造函数是 Object。
  2. 所以 Student.prototype.__proto__ 是 Object.prototype。

这个推导办法很实用,除了自定义构造函数对象之外,其余对象都能够推导出正确答案。

原型链常见问题

原型链中的问题很多,这里再列举几个常见的问题。

Function.__proto__ 是什么?

  1. 找 Function 的构造函数。

    1. 判断 Function 类型,typeof Functionfunction
    2. 函数类型的构造函数就是 Function。
    3. 得出 Function 的构造函数是 Function。
  2. 所以 Function.__proto__ = Function.prototype。


Number.__proto__ 是什么?

这里只是略微变了一下,很多同学就不晓得了,其实和下面的问题是一样的。

  1. 找 Number 的构造函数。

    1. 判断 Number 类型,typeof Numberfunction
    2. 函数类型的构造函数就是 Function。
    3. 得出 Number 的构造函数是 Function。
  2. 所以 Number.__proto__ = Function.prototype。


Object.prototype.__proto__ 是什么?

这是个特例,如果依照常理去推导,Object.prototype.__proto__ 是 Object.prototype,但这是不对的,这样上来原型链就在 Object 处有限循环了。

为了解决这个问题,Js 的造物主就间接在规定了 Object.prototype.__proto__ 为 null,突破了原型链的无线循环。

明确了这些问题之后,看一下这张经典的图,咱们应该都能了解了。

深刻原型链

介绍完传统的原型链判断,咱们再从 V8 的层面了解一下。

V8 是怎么创建对象的

Js 代码在执行时,会被 V8 引擎解析,这时 V8 会用不同的模板来解决 Js 中的对象和函数。

例如:

  • ObjectTemplate 用来创建对象
  • FunctionTemplate 用来创立函数
  • PrototypeTemplate 用来创立函数原型

细品一下 V8 中的定义,咱们能够失去以下论断。

  • Js 中的函数 都是 FunctionTemplate 创立进去的,返回值的是 FunctionTemplate 实例
  • Js 中的对象 都是 ObjectTemplate 创立进去的,返回值的是 ObjectTemplate 实例
  • Js 中函数的原型(prototype)都是通过 PrototypeTemplate 创立进去的,返回值是 ObjectTemplate 实例

所以 Js 中的对象的原型能够这样判断:

  • 所有的对象的原型都是 Object.prototype,自定义构造函数的实例除外。
  • 自定义构造函数的实例,它的原型是对应的构造函数原型。

在 Js 中的函数原型判断就更加简略了。

  • 所有的函数原型,都是 Function.prototype。

下图展现了所有的内置构造函数,他们的原型都是 Function.prototype。

看到这里,你是否也能够一看就看出任何对象的原型呢?

附:V8 中的函数解析案例

理解完原型链之后,咱们看一下 V8 中的函数解析。

function Student(name) {this.name = name;}
Student.prototype.study = function () {console.log("study js");
};
const student = new Student('xiaoming')

这段代码在 V8 中会这样执行:

// 创立一个函数
v8::Local<v8::FunctionTemplate> Student = v8::FunctionTemplate::New();
// 获取函数原型
v8::Local<v8::Template> proto_Student = Student->PrototypeTemplate();
// 设置原型上的办法
proto_Student->Set("study", v8::FunctionTemplate::New(InvokeCallback));
// 获取函数实例
v8::Local<v8::ObjectTemplate> instance_Student = Student->InstanceTemplate();
// 设置实例的属性
instance_Student->Set("name", String::New('xiaoming'));
// 返回构造函数
v8::Local<v8::Function> function = Student->GetFunction();
// 返回构造函数实例
v8::Local<v8::Object> instance = function->NewInstance();

以上代码能够分为 4 个步骤:

  • 创立函数模板。
  • 在函数模板中,拿到函数原型,并赋值。
  • 在函数模板中,拿到函数实例,并赋值。
  • 返回构造函数。
  • 返回构造函数实例。

V8 中的整体执行流程是合乎失常预期的,这里理解一下即可。

总结与思考

本文别离从传统 Js 方面、V8 层面组件分析了原型链的实质,心愿大家都能有所播种。

最初,如果你对此有任何想法,欢送留言评论!

退出移动版