【JS基础】JS学习道路上的难点之一,原型对象和原型链,尽量讲的简单通俗一些,看这篇文章足够了

前言

  1. 本文章是看了各种博客后结合自己的理解记录的,可能会有错误的地方,欢迎评论区指出
  2. 红宝书和犀牛书我都翻过了,在这方面解释的不够仔细。
  3. 先自行理解构造函数
  4. 先自行了解什么是引用类型,可以看我这篇文章

原型对象的定义

我的理解是,当某个对象能够给其他对象提供了共享的属性和方法时,它就是其他对象的原型对象(也就prototype对象)。

可以看出prototype对象和它的被提供者对象是相互联系的。

所以我们说prototype对象的时候,要说某某对象的prototype对象才是完整的说法。


为什么要发明原型对象

我们来看看es5的构造函数的一个问题

function Person(name) {
    this.name = name
    this.showName = function () { // 直接加在实例化对象上的写法
        console.log('我叫' + this.name);
    }
}
let p1 = new Person('a')
let p2 = new Person('a')
console.log(p1.showName == p2.showName); // false

俩个实例对象的同名的showName方法不是相同的引用地址,因为他们是不同的对象,有独自的内存空间

详细说就是,当我们实例化p1后,堆内存中对应的空间是这样的:

p1: {
	name: 'a',
	showName: '引用地址0x1111'
}
引用地址0x1111: {
	console.log('我叫' + this.name);
}

然后我们又实例化p2,堆内存中对应的空间是这样的:

p2: {
	name: 'a',
	showName: '引用地址0x1112'
}
引用地址0x1112: {
	console.log('我叫' + this.name);
}

看见没,showName明明可以复用的,但它被创建了两次,分配了不同内存空间。在一定程度上造成了内存的浪费

咋办呢?可以把函数作为全局变量:

function showName() {
    console.log('我叫' + this.name); // 暂时忽略this的指向问题,不在本次文章的讨论范围
}
function Person(name) {
    this.name = name
    this.showName = showName
}
let p1 = new Person('a')
let p2 = new Person('a')
console.log(p1.showName == p2.showName); // true

此时堆内存为:

p1: {
	name: 'a',
	showName: '引用地址0x1113'
}
p2: {
	name: 'a',
	showName: '引用地址0x1113'
}
引用地址0x1113: {
	console.log('我叫' + this.name);
}

这样看来内存占用是优化了,但是也有新的问题,每写一个构造函数的方法都要写个对应的全局变量再引用,如果写多了加上全局变量这么多,变量名称有可能重复了,就会带来变量污染的问题

咋办?这个时候想想之前说的原型对象概念,咱能不能模拟下?

// 模拟原型对象
let protoMan = {
    showName() {
        console.log('我叫' + this.name); // 暂时忽略this的指向问题,不在本次文章的讨论范围
    }
    // ...
}
function Person(name) {
    this.name = name
    this.showName = protoMan.showName
}
let p1 = new Person('a')
let p2 = new Person('a')
console.log(p1.showName == p2.showName); // true

堆内存:

p1: {
	name: 'a',
	showName: '引用地址0x1113'
}
p2: {
	name: 'a',
	showName: '引用地址0x1113'
}
引用地址0x1114: {
	showName: '引用地址0x1113',
	// ...
}
引用地址0x1113: {
	console.log('我叫' + this.name);
}

这样,所有构造函数里的方法都被一个全局的对象管理起来了,完美解决变量名称污染的问题。

看到这里是不是对原型对象的作用更加了解了呀!

哈哈,但是咧,咱们真正的原型对象并不是声明在全局上的,因为如果都声明在了全局上,最终代码量多了,也会有变量污染问题。


原型对象在哪里

咱们的函数声明的时候,js底层就会帮我们创建一个原型对象了:

function Person(name) {
    this.name = name
}
console.dir(Person); // dir用这个才能查看内存里的构造函数

在这里插入图片描述
prototype属性相当于我们上面写的protoMan对象,只不过前者是构造函数的一个属性,后者是全局变量。二者的作用是一样的。


实例对象也有原型对象

当我们去实例化Person这个构造函数后,把对象打印出来:

let p = new Person('a')
console.log(p);

在这里插入图片描述

[[Prototype]]这个对象和Person里的prototype属性对象是一个东西。

有些浏览器打印出来显示的可能不是[[Prototype]],因为这个玩意是浏览器开发商自己定的,有的叫__proto__,有的叫<prototype>

为了方便大家理解咱们下面都把实例对象的原型对象统一叫做__proto__吧。

要注意的是,__proto__属性是一个隐式的属性,它不是规范的东西,只是浏览器厂商为了方便开发者去查看原型对象。

因为隐式,对象上的__proto__也是不可枚举的,既不能被for in 遍历出来,也不能被Object.keys(obj)查出来。

后面ES6提供了Object.getPrototypeOf(obj)方法来查看原型对象,然后还提供了Object.setPrototypeOf(obj)来修改原型对象。

分清构造函数的prototype属性和实例对象的__proto__属性

有的小伙伴可能会对构造函数的prototype属性和实例对象的__proto__属性的指向有点懵,其实很好理解,你就想象:

  • 构造函数和实例对象他们的原型对象真名叫做“周星驰”,他们都共同认识周星驰。
  • 构造函数叫周星驰都是直呼全名
  • 而实例对象叫周星驰叫的是别名“周星星”
  • 周星星和周星驰是同一个人

understand?

原生构造函数

js提供了很多的原生构造函数,例如Number、String、Object等等。咱们可以实例化创建不同的数据类型对象。

例如:

let str = new String('a')
console.dir(str) 

你看它的原型对象上挂满了字符串类型的api方法。

注意:基本类型以 let str = 'a' 这种方式创建的基本类型变量不是对象,所以也就看不到原型对象。


原型对象上的constructor属性

不知道你发现没有,咱们每次打印出来的原型对象上都挂着个constructor属性,这玩意其实指向这个原型对象的构造函数。

function Person(name) {
    this.name = name
}
let p = new Person('a')
console.dir(Person);
console.log(p);

在这里插入图片描述

他的作用就是原型对象要知道他自己是怎么来的。

所以下面的代码你应该了解了:

console.log(p.__proto__.constructor === Person); // true
console.log(p.__proto__ === Person.prototype); // true

阶段性总结

现在已经非常了解原型对象了吧,它其实就是为了解决内存占用和变量污染问题的一种方案,表现为给其他对象提供了共享的属性和方法


用Object.create创建带有原型对象的空对象

上面说了当我们声明了一个函数后,js会自动给这个函数加上原型对象。

那如果我想创建一个空对象,并且自动给这个对象添加上我指定的对象作为原型对象可以不?

let aProto = { a: 1 }
let obj = Object.create(aProto)
console.log(obj);

在这里插入图片描述


理解原型链

我们来看看一个现象:

console.log(obj.__proto__.a === obj.a); // true

你看中间的原型属性是可以省略的,是不是说明我们访问原型上的属性或者方法都是可以以直接调用的方式获取呢?我们一步步分析!

首先咱们把这个实例对象的原型对象全部展开看看:

在这里插入图片描述

可以看到obj的原型对象的原型对象是构造函数Object的原型对象(听起来很绕哈哈),这是js底层默认添加的。

js底层会默认给每个原型对象的prototype属性添加上Object的原型对象。所以一个变量如果有原型的话,那么他的最高层级的原型对象就是Object的原型对象(这种偷偷摸摸的原型继承叫做隐式继承)。

画个图(图中把实例对象的__proto__换成prototype来表达,偷懒了哈哈):

在这里插入图片描述

这样就很明白了吧。然后咱们再来试试obj能不能直接调用Object原型对象上的方法:

obj.b = 2
console.log(obj.hasOwnProperty('b')); // true

好奇Object原型对象上的proto指向谁吗?

console.log(obj.__proto__.__proto__.__proto__); // null

说明是真的到头了哈哈。

所以咱们总结下:

  • 从某个变量的prototype属性的指向出发,一路指向最后的Object原型对象的这一条链就叫做原型链
  • 当一个变量访问某个属性时,会先从自己身上找,找不到就去沿着原型链一路往下找,看那个原型对象身上有,有就拿过来用。
  • 如果找到最顶层的原型对象null上还没找到,js就会给你返回undefined

添加原型链的方式

隐式继承

前面也说了js会偷偷帮你的构造函数添加prototype属性、或者是给你的最顶层添加Object原型对象。

这就是隐式继承的概念。

显式继承

说白了就是手动搬砖写原型链,前面也提到了用Object.create() 创建空对象,然后用Object.setPrototypeOf(obj) 写原型链的关系。例如:

const obj1 = {1:1}
const obj2 = {2:2}
const obj3 = {3:3}
const obj4 = {4:5}
const obj5 = {5:5}

Object.setPrototypeOf(obj1, obj2) // 设置obj2为obj1的原型对象
Object.setPrototypeOf(obj2, obj3)
Object.setPrototypeOf(obj3, obj4)
Object.setPrototypeOf(obj4, obj5)

console.dir(Object.create(obj1))

instanceof语法可以沿着原型链找原型对象

如果A沿着原型链能找到B的原型对象,那么A instanceof B 为 true。

let arr = []
arr instanceof Array // arr上的__proto__,是Array.prototype,所以为true
arr instanceof Object // arr上的__proto__.__proto__是Object.prototype,所以为true

但是非构造函数创建的基本类型没有原型,所以没有原型链。

let str = 'aa'
console.dir(str instanceof String) // false

而非构造函数创建的引用类型有原型:

let arr = [1]
console.dir(arr instanceof Array) // true

一些其他知识点

关于原型上的方法与属性

这里就先讲两个吧。

toString()

如果是内置的原生构造函数,这个方法在构造函数的原型上。如果是自定构造函数创建的对象,在prototype属性上默认是没有的,在最外层Object.prototype上存在。

是最外层Object的原型上toString()的功能是判断对象的具体类型(我这篇文章有梳理)。

而内置对象例如:数组,数字,函数。原型上的toString()的功能一般是转成字符串:

var arr = [1,2,3];
console.log(typeof arr.toString());  //变成字符串 1,2,3  具体来说就是调用的是原生对象Array.prototype上的toString()

数字类型的还有另一个作用,转换成进制位:

// 应用例子2: 把255 转换成 16进制 color:#ffffff
var num = 255;
console.log(num.toString(16));

constructor

这个属性就是用来查看构造函数的,上面讲过了,可用来做类型判断。

得注意的是,当去自定义原型对象的时候,记得要把constructor也写好,让原型对象知道自己哪来的。

例如:

function Fn() {}
Fn.prototype = {a:1}
console.log(Fn.prototype.constructor) //显示的是最上级的Object() { [native code] }

所以可以这样写

Fn.prototype = {
	a:1,
	constructor: Fn
}

使用new调用构造函数做了哪些事

使用new调用构造函数做了哪些事,这里简要总结下

  1. 默认创建一个新对象。
  2. 将构造函数的作用域给新对象,所以执行构造函数内部的this直接指向新对象。
  3. 执行构造函数中的代码,为这个新对象添加构造函数内的属性。
  4. 新对象的_proto_等于构造函数的prototype属性。
  5. 返回新对象。

包装对象

不用内置构造函数new的方法,直接 let str = 'a' 创建基本类型,为什么创建出来的变量能直接使用内置类型原型上的属性和方法呢?

因为这种直接法创建的基本类型会触发包装对象机制。

具体的我这里就不记录了,看别人写的博客吧,也蛮清楚的哈哈。

这里只留下一个思考:

let str = 'aa'
console.dir(str instanceof String) // false
console.dir(str.constructor)  // 没有原型却能拿得到构造函数
console.dir(Object.getPrototypeOf(str)) // 没有原型却能拿到原型

let str1 = new String('aa')
console.dir(str1 instanceof String) // true
console.dir(str1.constructor)  // 能拿得到构造函数
console.dir(Object.getPrototypeOf(str1)) // 能拿到原型

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值