带你一起攻克js之原型到原型链~

前言

原型与原型链是JavaScript中的基础知识点,同时也是面试的高频考点,今天带大家来熟知它。 在讲原型前,我们先来看看构造函数。

构造函数创建对象

js 复制代码
function Person(name) {
    this.name = name
}

const p1 = new Person()
const p2 = new Person()
p1.name = 'Dog'
p2.name = 'Cat'
console.log(p1.name) // Dog
console.log(p2.name) // Cat

由上边代码可知,通过构造函数为实例对象定义属性虽然方便,但有一个缺点。同一个构造函数的多个实例之间无法共享属性,从而造成系统资源的浪费。

上面代码中p1p2同一个构造函数的不同实例,它们都具有name属性,由于name属性是生成在每个实例对象上边的,因而两个实例就生成了两次。也就是说,每创建一个实例就会新增一个name属性,这即没有必要的,又浪费了系统资源,对于每个实例都需要name属性,其次完全可以共享的。

这个问题的解决方法就是JavaScript的原型对象(prototype)

我们先看以下两点:

  • 每个构造函数都有一个prototype属性
  • prototype是函数才有的属性

我们先来验证上边第一点,每个构造函数身上都有一个prototype属性,我们打印下Person构造函数上的prototype属性:

js 复制代码
function Person(name) {
    this.name = name
}
console.log(Person.prototype) // Person {}

打印发现果真如此, 接着我们看下边例子:

js 复制代码
Person.prototype.name = 'Bill'
const p1 = new Person()
const p2 = new Person()
console.log(p1.name) // Bill
console.log(p2.name) // Bill

我们首先往Person构造函数上的prototype属性添加了一个name属性,然后使用Person构造了两个实例,发现两个实例的name属性都是Bill,说明这两个实例共享了同一个原型对象,即共享了Person.prototype,因而它们共享了同一个原型对象上的name属性

那么这两个实例为什么可以直接访问到Person.prototype上的属性呢?

其实,函数的prototype会有一个指向。 函数的prototype属性指向一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是上边p1p2实例对象的原型。

也就是说:构造函数(Person)有一个prototype属性指向其实例对象(p1、p2)的原型

那么实例对象(p1、p2)的原型是什么?

对于实例上的原型,它用__proto__来表示,即p1.__proto__p2.__proto__,我们打印下看看:

js 复制代码
console.log(p1.__proto__) // Person {}
console.log(p2.__proto__) // Person {}

好了,我们现在可以认识一下什么是原型了。

原型每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象(即原型),这个对象就是我们所说的原型,每个对象都会从原型"继承"属性

这里强调一下,JavaScript中的函数本质也是一个对象,所以函数也有原型。

__proto__每一个JavaScript对象(null除外)都具有的一个属性,叫__proto__,这个属性会指向其构造函数的原型。既然是指向那么它们一定相等。

js 复制代码
console.log(p1.__proto__ === Person.prototype) // true

那么Person.prototype同样也有__proto__这个属性:

js 复制代码
console.log(Person.prototype.__proto__ === Object.prototype) // true
// ES5的方法,获取对象的原型
console.log(Object.getPrototypeOf(p1) === Person.prototype) // true

既然实例对象构造函数都可以指向原型,那么原型是否有属性可以指向构造函数或者实例呢?

指向实例倒是没有,因为一个构造函数可能生成多个实例,但是原型对象指向构造函数是有的,这个属性就是constructor

constructor

每一个原型都有一个constructor属性指向关联的构造函数

既然每个原型都有一个constructor属性,那么我们可以通过p1实例对象上的原型__proto__上的constructor属性来验证其是由哪个构造函数产生的

作用:可以得知某个实例对象到底是哪一个构造函数产生的

js 复制代码
console.log(Person.prototype.constructor === Person) // true
console.log(p1.__proto__.constructor === Person); // true

通过打印可知道,Person的原型对象指向构造函数Person,即验证了原型对象的constructor属性指向关联的构造函数

上边主要对prototype__proto__进行了介绍。

我们接着来看看实例和原型之间关系:

实例与原型

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

js 复制代码
function Person() {
    
}
Person.prototype.name = '张三'
const p3 = new Person()
p3.name = '李四'
console.log(p3.name) // 李四
delete.p3.name
console.log(p3.name) // 张三

在上边例子中,我们先是往实例对象上添加了name属性,当我们打印时,结果自然为张三,接着我们删除了p3实例对象上的name属性,再次打印时,由于p3实例对象上已没有name属性,所以会从实例对象的原型对象,也就是p3.__proto__上查找,我们知道实例的原型对象指向其构造函数的原型对象,所以会找到Person.prototype上的name属性,最后打印结果为张三。

幸运的是,我们上边Person构造函数上刚好有name属性。 如果Person构造函数上没有name属性呢?Person构造函数原型的原型又是什么呢?

原型的原型

在前边例子已经讲了,原型也是一个对象,既然是对象,我们就可以用最原始的方式创建它,那就是:

js 复制代码
const obj = new Object()
obj.name = 'Bill'
console.log(obj.name) // Bill

其实原型对象就是通过Object构造函数生成的,结合之前所讲,实例的__proto__指向构造函数的prototype

原型链

Object.prototype原型呢? 我们打印下:

js 复制代码
console.log(Object.prototype.__proto__) // null

null表示没有对象,即该处不应该有值。 所以Object.prototype.__proto__等于nullObject.prototype没有原型,其实表达的是一个意思。 所以查找属性时查找到Object.prototype就可以停止查找了。

由此我们可以得出:由相互关联的原型组成的链状结构就是原型链

大家结合上边例子来看下边的图,由相互关联的原型组成的链状结构就是原型链,多看几遍你肯定能搞懂👍

扩展

1.constructor

js 复制代码
function Person() {

}
const p1 = new Person()
console.log(p1.constructor === Person) // true

我们知道构造函数的原型对象上有一个constructor属性指向构造函数,当读取p1.constructor时,其实p1上并没有constructor属性,当不能读取到constructor属性时,就会从p1的原型也就是Person.prototype中读取,正好原型上有该属性,所以:

js 复制代码
p1.constructor === Person.prototype.constructor // true

2.__proto__

绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于Person.prototype中,实际上,它是来自于Object.prototype,与其说它是一个属性,不如说是一个getter/setter,当使用obj.__proto__时,可以理解成返回了Object.getPrototypeOf(obj)

3.真的是继承吗? 前面讲到"每一个对象都会从原型上'继承'属性",实际上,继承是一个非常具有迷惑性的说法。 继承意味着复制的操作,然而JavaScript默认并不会复制对象的属性,相反,JavaScript只是在两个对象之间创建一个关联,这样,一个对象可以通过委托访问另一个对象上的属性和函数,所以与其叫它继承,委托的说法反而更准确些。

看到这里的小伙伴给你们自己点个赞哈👏,现在咱动手敲起代码来,验证上边说的每一点,做到真正把它记住,多实践、多思考、多总结,画图形成你们的知识脉络。

结语

以上就是本篇的所有内容,喜欢的点点赞支持下luckyCover哈,欢迎评论区互动呀~

相关推荐
bug_kada2 小时前
Flex布局/弹性布局(面试篇)
前端·面试
元元不圆2 小时前
JSP环境部署
前端
槿泽2 小时前
Vue集成Electron目前最新版本
前端·vue.js·electron
麦当_2 小时前
SwipeMultiContainer 滑动切换容器算法指南
前端·javascript·算法
星斗大森林2 小时前
Flame游戏开发——噪声合成、域变换与阈值/调色映射的工程化实践(2)
前端
用户31506327304872 小时前
使用 vue-virtual-scroller 实现高性能传输列表功能总结
javascript·vue.js
星斗大森林2 小时前
flame游戏开发——地图拖拽与轻点判定(3)
前端
samonyu2 小时前
fnm 简介及使用
前端·node.js
bug_kada2 小时前
玩转Flex布局:看完这篇你也是布局高手!
前端