快速读懂 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?
这里提供一个推导步骤:
-
先找
__proto__
前面的对象,也就是 Student.prototype 的构造函数。 -
判断 Student.prototype 类型, typeof Student.prototype
是object
。 -
object
的构造函数是 Object。 -
得出 Student.prototype 的构造函数是 Object。 -
所以
Student.prototype.__proto__
是 Object.prototype。
这个推导方法很实用,除了自定义构造函数对象之外,其他对象都可以推导出正确答案。
原型链常见问题
原型链中的问题很多,这里再列举几个常见的问题。
Function.__proto__
是什么?
-
找 Function 的构造函数。
-
判断 Function 类型, typeof Function
是function
。 -
函数类型的构造函数就是 Function。 -
得出 Function 的构造函数是 Function。 -
所以
Function.__proto__
= Function.prototype。
Number.__proto__
是什么?
这里只是稍微变了一下,很多同学就不知道了,其实和上面的问题是一样的。
-
找 Number 的构造函数。
-
判断 Number 类型, typeof Number
是function
。 -
函数类型的构造函数就是 Function。 -
得出 Number 的构造函数是 Function。 -
所以
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 层面组件剖析了原型链的本质,希望大家都能有所收获。
最后,如果你对此有任何想法,欢迎留言评论!
本文分享自微信公众号 - 前端日志(gh_12dcc43e6039)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
微前端自检清单
最近在做公司微前端,整理了一份微前端搭建清单,如果你正在考虑是否要做微前端,不妨做个参考。 需求分析 技术方案分析 拆分方案分析 部署流程分析 需求分析 第一步,我们需要进行需求分析,以便真正清楚我们需要解决的问题是什么。 例如: 产品要新增一个业务模块 产品要修改项目样式 产品反馈项目启动太慢了 产品反馈页面跳转刷新很不友好 前两个需求是典型的业务需求,它的核心在于解决公司的业务问题,对于这一类需求,通常技术难度都不大,开发者只需要按照原型图,编写出对应的页面就可以了。 后两个需求是典型的技术需求,它的核心在于解决技术问题。通常来说,技术需求和用户体验有关,但不会影响项目功能,所以一般产品很少会提技术需求,都是由开发同学主导。 目前很多公司都不太重视技术需求,主要是因为和公司业务无关,不能带来真实可见的收益。其实不然,一些技术需求往往能产生巨大的成本收益,所以我们在做技术需求时,「首先需要得到公司的支持」。 为什么选择微前端 解决一个技术需求,有很多种方法,为什么选微前端? 我们看过微前端的发展史就会明白,它并不是凭空出现的,而是项目在不断发展过程中形成的,解决项目臃肿的技术方案。 ...
- 下一篇
java中的四种引用(强,软,弱,虚)
一、强引用 概念:当对象不可达时,即可回收。 /** * 强引用。当强引用指针不存在时,对象将被回收 * 也可以理解为 ROOT 引用消失时,对象将被回收 */ public class StrongReference { /** * jvm在执行gc时 将回调该方法 * @throws Throwable */ @Override protected void finalize() throws Throwable { System.out.println("gc doing now !"); } public static void main(String[] args) { StrongReference strongReference = new StrongReference(); // 将引用指针移除 strongReference = null; // 手动调用gc System.gc(); } } 二、软引用 概念: 当内存空间不足时,将被回收。 /** * Xmx2oM 设置最大堆内存为20M * 软...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS6,CentOS7官方镜像安装Oracle11G
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Hadoop3单机部署,实现最简伪集群
- MySQL8.0.19开启GTID主从同步CentOS8
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果