JS高级使用4.0——原型链、显式原型prototype和隐式原型__proto__
创作场景
原型链上的显示和隐式原型算是js中比较核心的知识点,如果想要真正了解js的函数,这个知识点是必须要掌握的。
阅读前提
初学者看这个会很抽象,只要不是刚接触编程的小伙伴就行。
提前了解的知识点
1. 堆和栈的区别
这篇文章是对这两个定义的详细解释,有兴趣的伙伴可以仔细研究一下,本文只是对这两个定义进行简单使用,大家只需要知道,栈存储的一般是常量和内存地址
,空间较小,而堆一般存放的就是对象本身
,空间较大,或者也可以理解为key和value两个。
2. 函数、方法和构造函数的区别
什么是函数,函数就是一段可执行的代码
,其中的this指向一般是window。
function fn() {
// 做一些操作
}
什么是方法,方法说白了其实就是函数,只不过它一般定义在对象上
,通过对象调用
,而this就是指向调用的对象。
var obj = {
name: "程序猿",
fun: function() {
// 做一些操作
}
}
什么是构造函数,同理,它也是一个函数,与普通函数的作用不同,一般构造函数都是使用new关键字
创建实例对象的,而this就是创建的实例对象。而且从书写来说,构造函数使用大驼峰
命名,也就是Fn这种,区分与普通函数。
function Fn(name, age) {
this.name = "程序猿",
this.age = 18,
this.fun = function () {
console.log("这是构造函数中的方法")
}
}
上面这个例子能很明显的看出来构造函数的作用,就是初始化一个对象
,给这个对象上添加一些属性和方法
,而普通函数只是对一些对象做一些操作。
3. 原型对象和实例对象的区别
实例对象比较好理解,可以认为就是一个有很多属性和方法的对象
,一般我们称用new关键字得到的对象为实例对象,但几乎所有对象都可以称为实例对象,因为Js中所有对象都是由顶级的构造方法Object()
创建的。
原型对象可能没听过,在JavaScript中规定,原型对象就是构造函数中的一个对象prototype
,在这个对象上可以挂载一些方法和属性。
先别着急疑惑原型对象到底是什么,能干什么,本文就是讲这个的,上面只是说一下区别,避免在后面搞混了。
原型对象(显式原型)prototype
定义
- 在每个函数上
都有一个prototype属性
,默认指向一个空的对象
,而这个对象我们就称为原型对象
。 - 原型对象上有一个
constructor
,指向函数对象。 - 显式原型程序猿可操作。
function fun() {
console.log("fun")
}
console.log(fun.prototype)
console.log(Date.prototype)
// 验证constructor是不是指向函数对象
console.log(fun.prototype.constructor === fun)
console.log(Date.prototype.constructor === Date)
上图验证了我们最开始说的两个结论,先不要着急去研究constructor中的哪个prototype是啥,这是原型链的知识,我们先看prototype哈。
作用
那么这个prototype有什么作用呢,这就是很多人疑惑的一点,看下图:
开发中我们经常new一个Date获取当前时间,单单这个肯定不好使啊,我们还需要使用getMonth获取月份,getFullYear获取年份,这是常用的手段,这些方法是哪里来的呢?答案就是Date原型对象prototype上面的,只要你new一个Date,new出来的实例对象就有这些个方法。
有些人就又说了,那我搞一个对象,对象上定义一个方法也可以实现啊,为啥要用这个,原型对象的好处就是它在内存中,也就是堆中只会存在一份
,而你不断地new实例对象,虽然创建的是新的对象,每个对象都有一个prototype,这个prototype在栈中的地址不一样,但是指向堆中的对象是一样滴,所有对象共享一个堆内存。
这样做的好处就是大大节省了内存空间
,堆都是存放很大的对象的,你放多了肯定就慢了,这种思想就类似于你还是你,但是你可以有很多名字,每个名字都指向的你。
怎么使用prototype
搞清楚它的定义和作用后,具体的使用场景就是当你需要不断创建一个对象,对象上需要一个方法进行某些特殊操作时,可以定义一个构造函数,给这个构造函数的prototype上挂载一些通用方法。
console.log('---------------------------------------------------------------')
function TestPrototype(type) {
this.type = type;
TestPrototype.prototype.fn1 = function () {
console.log("这是TestPrototype的fn1", this.type)
}
TestPrototype.prototype.fn2 = function () {
console.log("这是TestPrototype的fn2", this.type)
}
}
var n1 = new TestPrototype("info")
var n2 = new TestPrototype("warn")
n1.fn1() // 这是TestPrototype的fn1 info
n2.fn2() // 这是TestPrototype的fn1 info
隐式原型__proto__
- 每个实例对象都有一个__proto__属性,称为隐式原型。
- 对象的隐式原型的值默认为对象的构造函数的显式原型prototype的值。
- ES6前不可操作。
function Fun() {
console.log("Fun")
}
var fn1 = new Fun()
console.log("构造函数的显式原型", Fun.prototype)
console.log("实例对象的隐式原型", fn1.__proto__)
console.log("隐式原型是否等于显式原型", fn1.__proto__ === Fun.prototype)
上图证明了我们的两个结论是正确的,不要纠结为什么这么设计,你可以理解为你可以更改你的姓名
,但是身份证你改不了
,那是证明你身份的东西,而隐式原型也是这样的道理,这就是用来证明实例对象身份的属性
,肯定得指向显式原型啦。
隐式原型先了解到这里,至于用法得在原型链中体现,提前透露一下,隐式原型在instance关键字
中有用到,instance的原理就是看你的隐式原型是啥。
画图表示显示原型和隐式原型
上图就简单表示了隐式原型和显式原型的关系,其实最终都指向了同一个对象,这也变向的说明了prototype对象只会在内存中存在一份
,不会向普通对象那样,创建一个对象,在堆中会存在一份。
原型链(重点)
1. 原型链的定义
当我们访问一个对象的属性
时,如果本身上没有这个属性,会在__proto__这条链上面寻找,如果能找到就返回,找不到就返回undefined,而原型链其实就是__proto__链
。
上面这句话是原型链的定义,有两个重点:
1.访问对象的属性
有的人在最开始可能会疑惑,函数不也是对象吗?按道理应该也有隐式原型,没问题,确实有,但是我们看不到,原型链一般就是在实例对象使用的。
function Fun() {
console.log("Fun")
}
var fn1 = new Fun()
console.log("构造函数的显式原型", Fun.prototype)
console.log("构造函数的隐式原型", Fun.__proto__)
console.log("实例对象的隐式原型", fn1.__proto__)
console.log("隐式原型是否等于显式原型", fn1.__proto__ === Fun.prototype)
我们打印了一下函数的隐式原型,发现是native code
,这是c的底层代码,我们是看不到的,所以不要去纠结函数上的隐式原型了,我们看下显示原型到底是什么。
- 原型链就是__proto__隐式原型链。
为啥要强调这个,有的人还会纠结显示原型中也有__proto__这个,到底是什么。
首先强调一点,函数上的constructor属性上也有一个prototype,这个永远指向的是你的函数对象
,无论你怎么点下去,都是自己,所以不要纠结(我最开始也纠结过)。
能看到prototype上也有一个隐式原型属性,这个隐式原型意思是你这个显示原型是啥,也就是用来证明你显式原型是对象
,万物皆对象嘛,而hasOwnProperty这些方法就是对象上的方法,继承来的。
上图就更好说明了这就是用来证明你是个对象的,打开__proto__发现和显式原型是一样的,而对象上的隐式原型是null
,这是一个重点,对象上就没有隐式原型,这已经是最大的了。
2. 举例说明什么是原型链
function Fn() {
this.test1 = function () {
console.log("定义在对象上的方法test1")
}
}
Fn.prototype.test2 = function () {
console.log("定义在显式原型上的方法test2")
}
var fn1 = new Fn()
console.log("fn1对象", fn1)
console.log("fn1的隐式原型", fn1.__proto__)
fn1.test1()
fn1.test2()
fn1.test3()
上面的例子清晰的说明了原型链的用法,test3函数调用失败就是因为在对象本身和原型链上都没有找到有这个属性,而test2在原型链上,所以能找到。
3. instanceOf关键字解读
解读一下,instanceOf是用来判断类型的,而原理就是通过在原型链上查找
,上面这张图是用来表明函数Object()和函数Function()的关系,要知道,所有函数都是通过Function创建的
,而所有对象都是通过Object()函数创建的
,无论是函数还是对象,最终都是指向Object的prototype
,而Object最终指向就是null
,因为已经没有可指向的了。
下面我们通过一个例子来说明。
function Fn() {}
var fn1 = new Fn()
console.log(fn1 instanceof Fn) // true
console.log(fn1 instanceof Function) // false
console.log(fn1 instanceof Object) // true
上面的例子表明fn1是Fn的类型和Function的类型,那有的人就说了,说到底这不是通过函数创建的嘛,为啥类型不是Function?大家记着一句话,实例对象的隐式原型和其构造函数的显示原型相等
,那Fn的显式原型最终指向的是哪里呢?看下图
Fn的显式原型指向的是Object,但是隐式原型指向的Function,所以实例对象fn1的隐式原型指向Object,最终类型不会是Function。
原型链总结
- 方法一般定义在原型上,也就是prototype。
- 属性一般定义在对象上,读取属性会自动在__proto__中寻找,定义属性则不会。
- 所有函数都是Function的实例,包括Function自己。
- 所有对象都是Object的实例,包括Function。
- 原型链的尽头是null。
prototype的实际应用
其实说了这么多,对于原型链了解是了解了,但是有什么用呢?具体就要看你的业务场景,如果需要频繁创建一个对象,对象上要调用一个方法操作属性,比如有一万个员工,要计算员工的岗位工资和绩效工资之和,这时候就可以将方法定义在原型上,避免内存浪费啦。
今天就到这里,如果有问题大家及时沟通哈。