带你搞懂JavaScript中的原型和原型链

简介

原型和原型链是JavaScript中与对象有关的重要概念,但是部分前端开发者却不太理解,也不清楚原型链有什么用处。其实,学过其他面对对象语言的同学应该了解,对象是由类生成的实例,类与类之间有继承的关系。在ES6之前,JavaScript中并没有class,实现类和继承的方法就是使用原型。在我个人看来,JS中类和原型链的设计和语法由于一些历史或包袱问题而不易用,也不易于理解。因此在ES6中推出了class相关的语法,和其他语言更接近,也更易用。

ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。(ECMAScript6入门教程 阮一峰)

虽然有了class,但是原型链相关的内容我们依然要掌握。不仅是因为作为前端开发者,我们要深入理解语法。而且在查看源码,以及实现一些复杂的面对对象写法时,依然是有用的。因此在这篇文章中,我们一起搞懂JavaScript中的原型和原型链。(这篇文章并不会涉及class相关语法)

构造函数与原型

构造函数

在JS中创建实例的方法是通过构造函数。在构造函数中通过this实现对实例的操控,比如赋值各种属性和方法。我们看个例子:

// Person构造函数
function PersonFun(name) {this.name = name;this.getName = function() {return this.name;}
}
// 创建实例
const p1 = new PersonFun('jz');
console.log(p1.name, p1.getName());
// 输出结果:
// jz jz

我们创建了PersonFun构造函数,使用new关键字创建了实例p1。可以看到,在构造函数中对this增加了属性和方法,最后成为了实例的属性。注意构造方法必须使用new调用。但是这样所有的属性都是实例属性,包括那个getName方法:

const p1 = new PersonFun('jz');
const p2 = new PersonFun('jz');
console.log(p1.getName === p2.getName);
// 输出结果:
// false

原型对象

只用上面的构造函数,依然没有“类”的存在。这时候我们增加原型这一概念,可以理解为是实例对象的类。原型对象可以通过构造函数的prototype属性访问。

// Person构造函数
function PersonFun(name) {this.name = name;
}
// Person原型对象
PersonFun.prototype.getName = function() {return this.name;
}
// 创建实例
const p1 = new PersonFun('jz');
const p2 = new PersonFun('jz');
console.log(p1.name, p1.getName());
console.log(p1.getName === p2.getName);
// 输出结果:
// jz jz
// true

可以看到,我们没有在构造函数中添加实例对象的属性方法getName,仅仅在原型对象上添加。但实例对象上依然能使用属性方法getName,而且对于不同的实例来说,这个方法是共享的,是同一个。通过原型,我们不仅能共享方法名也能共享属性值:

// Person原型对象
PersonFun.prototype.title = 'hello';
const p1 = new PersonFun('jz');
const p2 = new PersonFun('jz');
console.log(p1.title, p1.title);
p1.title = '你好'
console.log(p1.title, p2.title);
// 输出结果:
// hello hello
// 你好 你好

可以看到,在实例中修改原型上提供的属性,实际上是增加实例中的属性值,因此这个修改是不在实例中共享的。但如果原型提供的属性是个对象,我们修改对象内部的值,这个值是实例间共享的。

PersonFun.prototype.obj = {};
p1.obj.a = 1;
console.log(p2.obj.a);
// 输出结果:
// 1

构造函数/原型的获取

通过上面的描述,我们了解了实例,构造函数和原型对象以及他们之间的关系。那么在代码中,如何获取构造函数和原型对象呢?我们列出了一些方法:

// 构造函数 PersonFun
// 获取原型对象
Person = PersonFun.prototype
// 创建实例对象
p1 = new PersonFun()// 原型对象 Person
// 获取构造函数
PersonFun = Person.constructor// 实例对象 p1
// 获取原型对象
Person = p1.__proto__
Person = Object.getPrototypeOf(p1)
// 获取构造函数
PersonFun = p1.constructor

其中的__proto__最好使用Object.getPrototypeOf代替:

__proto__并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的JS引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。(ECMAScript6入门教程 阮一峰)

字面量的原型

字面量对象的原型

如果我们创建对象的时候,没有使用构造函数,而是直接是用大括号,以字面量的形式创建的对象,那么它的原型是什么?它有没有构造函数呢?是有的。我们一起来看一下:

const obj = { a: 1 };
console.log(obj.__proto__)
console.log(obj.constructor)
// 输出结果:
// {constructor: ƒ, __defineGetter__: ƒ, …}
// ƒ Object() { [native code] }

输出了一些奇怪的东西,我们还是不知道字面量对象的原型是什么。我们换个角度想一想,以字面量形式创建的对象,是不是就相当于直接使用new Object()形式创建的对象?这里的Object也是一个构造函数。我们来试验下:

const obj1 = { a: 1 };
const obj2 = new Object({ b: 2 });
console.log(obj1.__proto__ === obj2.__proto__)
console.log(Object.prototype === obj1.__proto__)
console.log(obj1.constructor === Object)
// 输出结果:
// true
// true
// true

可以看到,使用字面量形式和new Object()形式,创建出来对象的原型是一样的。既然Object是个构造函数,那么Object.prototype即是Object实例的原型对象。

至于obj.constructor实际上就是构造函数Object()。它是JS内部生成的,因此这里展示[native code]

new Function()

在JS中函数实际上也是个对象。既然它是个对象,那么它应该也有构造函数和原型吧。我们来试验下:

// Person构造函数
function PersonFun(name) {this.name = name;
}
console.log(PersonFun.__proto__)
console.log(PersonFun.constructor)
// 输出结果:
// ƒ () { [native code] }
// ƒ Function() { [native code] }

又输出了一些奇怪的东西,其中还有个Function()。我们继续联想下:对象可以用new Object()形式创建,那么函数是不是也可以?可以的!使用new Function()可以创建函数。我们来看下:

const fun = new Function('a', 'b', 'return a + b');
console.log(fun(1, 2));
// 输出结果:
// 3

new Function()可以使用字符串作为代码执行的函数体,感觉有点像eval。但是eval是局部作用域,new Function()一直都是全局作用域。我们来看下例子:

const a = 1;
function envir() {const a = 2;eval('console.log(a)');const fun = new Function('console.log(a)');fun();
}
envir();
// 输出结果:
// 2
// 1

可以看到,eval输出的是局部作用域中的a值2,而new Function()虽然在局部作用域的位置中,但是内部获取到的依然是全局的变量。不过这些区别和我们要讨论的原型链无关,因此不再继续讨论。

字面量函数的原型

了解了new Function(),我们再回来看看字面量函数的原型。

// Person构造函数
function PersonFun(name) {this.name = name;
}
const fun = new Function('a', 'b', 'return a + b');
console.log(PersonFun.__proto__ === fun.__proto__)
console.log(PersonFun.__proto__ === Function.prototype)
console.log(PersonFun.constructor === Function)
// 输出结果:
// true
// true
// true

与对象类似,Function.prototype是函数的原型,我们函数字面量的原型都是它。函数的构造函数即是Function()。(构造函数与普通函数并无区别,都是函数)。在上面的输出中,函数的原型对象Function.prototype也是一个函数:ƒ () { [native code] }。关于这点我们会在后面讨论。

JS中的原型关系

了解了字面量相关的原型,现在我们再来刨根问底,看看JS中对象的原型关系。

对象的原型关系

首先看下Object原型的关系。

对象的尽头

首先看看对象的尽头。上面讲过字面量对象的原型即是Object.prototype。它也是个对象,那么它有没有原型呢?我们试一下:

const obj = { a: 1 };
console.log(obj.__proto__)
console.log(obj.__proto__.__proto__)
// 输出结果:
// {constructor: ƒ, __defineGetter__: ƒ, …}
// null

答案是没有的,Object.prototype是没有原型的。

自定义构造函数与原生对象的关系

我们的自定义构造函数与对应的实例原型和Object.prototype有关系么?我们试验下:

// Person构造函数
function PersonFun(name) {this.name = name;
}
// Person原型对象
PersonFun.prototype.getName = function() {return this.name;
}
console.log(PersonFun.prototype.__proto__);
console.log(PersonFun.prototype.__proto__ === Object.prototype);
console.log(PersonFun.prototype.constructor === Object);
// 输出结果:
// {constructor: ƒ, __defineGetter__: ƒ, …}
// true
// false

可以看到,Person构造函数对应实例的原型对象,它的原型即是Object.prototype。但是它与字面量对象不同的是,它的constructor属性表示的是它对应实例的构造函数,而不是字面量对象的Object()

原生类型的原型关系

在前面我们聊过了函数的原型,即是Function.prototype。但当时我们输出它,发现它是一个函数,那么它究竟是什么?它还有没有原型?

// Person构造函数
function PersonFun(name) {this.name = name;
}
console.log(PersonFun.__proto__);
console.log(PersonFun.__proto__.__proto__);
console.log(PersonFun.__proto__.__proto__ === Object.prototype);
console.log(PersonFun.__proto__.prototype);
// 输出结果:
// ƒ () { [native code] }
// {constructor: ƒ, __defineGetter__: ƒ, …}
// true
// undefined

可以看到,直接打印函数的原型也是一个函数,里面是[native code],即它也是由JS内部生成的。它的再深一层原型,居然又是Object.prototype。函数的原型虽然也是个函数,但是它并没有更深一层的prototype。

这时候我们返回去看看对象原型的构造函数,即Object()。作为一个函数,它的原型是什么?

console.log(Object);
console.log(Object.__proto__);
console.log(Object.__proto__ === Function.prototype);
// 输出结果:
// ƒ Object() { [native code] }
// ƒ () { [native code] }
// true

看来这些原生类型的构造函数的原型,都同一个来源。我们再试一下其他的原生类型:

console.log(Number);
console.log(Number.__proto__);
console.log(Number.__proto__ === Function.prototype);
console.log(Array);
console.log(Array.__proto__);
console.log(Array.__proto__ === Function.prototype);
console.log(String);
console.log(String.__proto__);
console.log(String.__proto__ === Function.prototype);
new Function.prototype();
// 输出结果:
// ƒ Number() { [native code] }
// ƒ () { [native code] }
// true
// ƒ Array() { [native code] }
// ƒ () { [native code] }
// true
// ƒ String() { [native code] }
// ƒ () { [native code] }
// true
// Uncaught TypeError: Function.prototype is not a constructor

果然如此,原生类型的构造函数的原型都是同一个。而如上面实验得出的结论,这个原型是一个函数,它没有构造函数,它的原型是Object.prototype。我还尝试直接用这个构造函数原型创建实例,结果提示这不是一个构造函数。

原型链

有了上面这些关系,我们发现不同类型对象的原型似乎都是有关系的,好像有一条线可以把他们穿起来。这条线就是我们所说的原型链。在文章一开始的简介中说过,原型和原型链是JavaScirpt中实现类和继承的一种方式。原型就相当于实例的类,继承就像是原型链。因此,原型的特点也很像父类,即实例可以访问原型的属性,也可以覆盖原型属性。在浏览器的控制台中,我们打印一个对象,展示的[[Prototype]]即是它的原型。

原型链示意图

不仅我们自定义的类型有原型链的关系,JS内部的原生类型也存在原型链,且可以和我们自定义的类型串起来。这里我们用一个图片描述原型链之间的关系(图片来源MollyPages.org):

在这里插入图片描述

原型链文字版

假设我们有这样一些对象,我们来搞清楚它们的原型关系。

  • 构造函数 PersonFun
  • 实例对象 p1
  • 原型对象(类) Person
// 构造函数 生成 实例对象
p1 = new PersonFun()
// 构造函数 -> 原型对象
Person = PersonFun.prototype// 实例对象 -> 构造函数
PersonFun = p1.constructor
// 实例对象 -> 原型对象
Person = p1.__proto__
Person = Object.getPrototypeOf(p1) // 推荐// 原型对象 -> 构造函数
PersonFun = Person.constructor

再看一下字面量的原型关系,以及更深层次的关系:

  • 字面量对象 obj1
  • 字面量函数 fun1
// 字面量对象 -> Object构造函数
Object = obj1.constructor
// 字面量对象 -> Object原型
Object.prototype = obj1.__proto__
// Object原型的原型 为 null
null = Object.prototype.__proto__// 字面量函数 -> Function构造函数
Function = fun1.constructor
// 字面量函数 -> Function原型
Function.prototype = fun1.__proto__
// Function原型作为一个构造函数时的实例原型 -> undefined
undefined = Function.prototype.prototype
// Function原型的原型 为 Object原型
Object.prototype = Function.prototype.__proto__

然后我们就可以完整的得到原型链:

// 构造函数链
// 实例对象 -> 构造函数 -> Function构造函数
PersonFun = p1.constructor
Function  = p1.constructor.constructor
Function  = p1.constructor.constructor.constructor// 原型对象链
// 实例对象 -> 原型对象 -> Object原型 -> null
Person            = p1.__proto__
Object.prototype  = p1.__proto__.__proto__
null              = p1.__proto__.__proto__.__proto__// 字面量对象的原型对象链
// 字面量函数 ->  Object原型 -> null
Object.prototype  = obj1.__proto__
null              = obj1.__proto__.__proto__// 字面量函数的原型对象链
// 字面量函数 -> Function原型 -> Object原型 -> null
Function.prototype  = fun1.__proto__
Object.prototype    = fun1.__proto__.__proto__
null                = fun1.__proto__.__proto__.__proto__// Number对象的原型链
const n1 = new Number(1);
// Number对象 -> Number原型 -> Object原型 -> null
Number.prototype  = n1.__proto__
Object.prototype  = n1.__proto__.__proto__
null              = n1.__proto__.__proto__.__proto__

总结

通过原型链,我们可以了解JS中一些原生对象的原理和机制,比如为什么Function的实例也是对象,Number的实例也是对象,因为这些对象的原型都继承了Object原型,因此可以使用对象类型的方法。

使用原型链,也可以实现很多类的继承模式,后面有机会我们可以讨论一下。总体看来,虽然使用原型链确实可以实现类和继承的等面对对象特性,但是相比于其他语言更晦涩且不容易理解。

参考

  • Class的基本语法 ECMAScript6入门教程 阮一峰
    https://es6.ruanyifeng.com/#docs/class
  • Class 的继承 ECMAScript6入门教程 阮一峰
    https://es6.ruanyifeng.com/#docs/class-extends
  • 一文搞懂JS原型与原型链(超详细,建议收藏)
    https://juejin.cn/post/6984678359275929637
  • 你可能不太理解的JavaScript - 原型与原型链
    https://juejin.cn/post/7254443448563040311
  • js从原型链到继承——图解来龙去脉
    https://juejin.cn/post/7075354546096046087
  • Javascript Object Layout
    http://www.mollypages.org/tutorials/js.mp

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/263562.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MyBatisX插件

MyBatisX插件 MyBatis-Plus为我们提供了强大的mapper和service模板,能够大大的提高开发效率。 但是在真正开发过程中,MyBatis-Plus并不能为我们解决所有问题,例如一些复杂的SQL,多表联查,我们就需要自己去编写代码和SQ…

Java 简易版 UDP 多人聊天室

服务端 import java.io.*; import java.net.*; import java.util.ArrayList; public class Server{public static ServerSocket server_socket;public static ArrayList<Socket> socketListnew ArrayList<Socket>(); public static void main(String []args){try{…

UDP实现群聊

代码&#xff1a; import java.awt.*; import java.awt.event.*; import javax.swing.*; import java.net.*; import java.io.IOException; import java.lang.String;public class liaotian extends JFrame{private static final int DEFAULT_PORT8899;private JLabel stateLB…

数据分析基础之《numpy(1)—介绍》

一、numpy介绍 1、numpy 数值计算库 num - numerical 数值化的 py - python 2、numpy是一个开源的python科学计算库&#xff0c;用于快速处理任意维度的数组 numpy支持常见的数组和矩阵操作。对于同样的数值计算任务&#xff0c;使用numpy比直接使用python要简洁的多 numpy使…

输入一组数据,以-1结束输入[c]

我们新手写题时总能看到题目中类似这样的输入 没有给固定多少个数据&#xff0c;我们没有办法直接设置数组的元素个数&#xff0c;很纠结&#xff0c;下面我来提供一下本人的方法&#xff08;新手&#xff0c;看到有错误或者不好的地方欢迎大佬指出&#xff0c;纠正&#xff0…

web 前端之标签练习+知识点

目录 实现过程&#xff1a; 结果显示 1、HTML语法 2、注释标签 3、常用标签 4、新标签 5、特殊标签 6、在网页中使用视频和音频、图片 7、表格标签 8、超链接标签 使用HTML语言来实现该页面 实现过程&#xff1a; <!DOCTYPE html> <html><head>…

c语言选择排序总结(详解)

选择排序cpp文件项目结构截图 项目cpp文件截图 项目具体代码截图 #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> #include <math.h> #include <iostream> #include <string.h> #include <time.h> #include &…

【C++】输入输出流 ⑥ ( cout 标准输出流对象 | cout 常用 api 简介 | cout.put(char c) 函数 )

文章目录 一、cout 标准输出流对象1、cout 标准输出流对象简介2、cout 常用 api 简介 二、cout.put(char c) 函数1、cout.put(char c) 函数 简介2、代码示例 - cout.put(char c) 函数 一、cout 标准输出流对象 1、cout 标准输出流对象简介 cout 是 标准输出流 对象 , 是 ostrea…

C++初阶(十四)list

&#x1f4d8;北尘_&#xff1a;个人主页 &#x1f30e;个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上&#xff0c;不忘来时的初心 文章目录 一、 list的介绍二、list的模拟实现1、list的节点2、list 的迭代器3、list4、打印5、完整代码…

Doocker还原容器启动命令参数

get_command_4_run_container可以还原docker执行命令, 这是个第三方包&#xff0c;需要先安装&#xff1a; docker pull cucker/get_command_4_run_container 命令格式&#xff1a; docker run --rm -v /var/run/docker.sock:/var/run/docker.sock cucker/get_command_4_run…

Linux中的SNAT与DNAT实践

Linux中的SNAT与DNAT实践 1、SNAT的介绍1.1&#xff0c;SNAT概述1.2&#xff0c;SNAT源地址转换过程1.3&#xff0c;SNAT转换 2、DNAT的介绍2.1&#xff0c;DNAT概述2.2&#xff0c;DNAT转换前提条件2.3&#xff0c;DNAT的转换 3、防火墙规则的备份和还原4、tcpdump抓包工具的运…

ubuntu16.04升级openssl

Ubuntu16.04 默认带的openssl版本为1.0.2 查看&#xff1a;openssl version 1.下载openssl wget https://www.openssl.org/source/openssl-1.1.1.tar.gz 编译安装 tar xvf openssl-1.1.1.tar.gz cd openssl-1.1.1 ./config make sudo make install sudo ldconfig 删除旧版本 su…