JS原型及原型链总结
前言
关于原型链的问题之前学习好多次了,但是一直没有总结过,尤其是对于一个函数的原型,一个实例对象的原型还有函数内部的prototype属性,实例对象的内部特性[[Prototype]]等概念都比较模糊,这次就做一个详细的总结。
一、为什么我们要使用原型?
①对象字面量创建对象的缺点
想要介绍原型,就不得不提为什么我们要使用原型,在js早期,我们创建一个对象,比较流行的做法是使用对象字面量去创建一个对象,例如:
const person = {
name: "翻滚吧斑马",
age: 18,
hobby: "看jojo"
}
用这种方式去创建一个对象,虽然简洁明了,但是如果我们需要大批量的创建这一类的对象,就像person这个对象,我们可能需要去创建多个不同的人,那么每一次都需要去声明创建,那么毫无疑问工作量是巨大的,为了解决这个问题我们引入了工厂函数的概念。
②工厂函数
什么是工厂函数,顾名思义就可以把工厂函数看作是一个流水线的工厂,这个工厂的作用就是批量生产person对象,就像下面这样:
function createPerson(name, age, hobby) {
const obj = {};
obj.name = name;
obj.age = age;
obj.hobby = hobby;
return obj;
}
const person1 = createPerson("zs", 23, "法外狂徒");
const person2 = createPerson("lm", 26, "韩梅梅");
console.log(person1);
console.log(person2);
这样我们在创建person对象的时候只需要调用这个函数即可,并把每个对象对应的属性值传入就好了,这样相对于用对象字面量去创建一个个的person确实简化了代码量,但是工厂函数也有自身的缺点,就是我们不能去判断出这个对象的类型,按我们知道的,在js中复杂引用数据类型进行细分,有Array,Function,String等,但是通过工厂函数模式创建的这些对象用控制台打印去全部都是Object类型,假如你有多个工厂函数,用多个工厂函数创建了多个实例,但是你却并不知道这些对象属于哪个工厂。为此又推出了构造函数这个概念。
③构造函数
关于构造函数其实它和工厂函数非常相似,在js的函数中其实并没有单独的一类函数叫做构造函数,构造函数总是和new关键字一起使用,更准确地说一个函数被构造调用了。当一个构造函数不使用new关键字调用时,他和普通的函数无异。
我们来对上面的工厂函数进行一些改造:
function CreatePerson(name, age, hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
}
const person1 = new CreatePerson("zs", 23, "法外狂徒");
const person2 = new CreatePerson("lm", 26, "韩梅梅");
console.log(person1);
console.log(person2);
仔细观察,不难发现我们对上面的工厂函数进行了以下几点改造:
1、我们将函数名的首字母大写了,在js中有个规定如果你以后打算将一个函数作为构造函数去使用那么最好把它的函数名首字母大写,来提醒使用者这是一个构造函数。其实不大写也不会有什么语法错误。
2、我们取消了在函数内部显示的声明一个对象“const obj = {}”,并一并取消了最后返回这个对象的操作"return obj"。
3、在调用这个这个CreatePerson函数时在前面加上了new关键字。
我们再去看控制台打印的结果:
我们惊喜的发现这个时候我们打印的对象不在是Object了,而是我们自己创建的函数CreatePerson。看来构造函数确实解决了对象无法判定类型的问题。那么神奇的new关键字在后台做了什么呢?其实它做了下面五件事:
一、在内存中创建一个新的对象。
二、让新对象的内部特性[[Prototype]]保存函数CreatePerson的prototype的属性值,也就是把函数的原型的指针保存到了[[Prototype]]中。
三、把函数内部的this指向这个新创建的对象。
四、执行函数内部的代码。
五、如果函数本身没有返回对象,那么就把这个新对象返回。
关于[[Prototype]]我会在后面讲到,this的问题需要另起一篇博客来说了。回归正题,我们看构造函数原来在后台为我们做了这么多事。那么构造函数就完美了吗?并不是的,我们想一个person是不是应该也该给他加一个方法呢?那么我们就给加一个说话的方法吧:
function CreatePerson(name, age, hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
// 添加一个方法
this.sayHi = function() {
console.log("你好,我叫" + this.name)
}
}
const person1 = new CreatePerson("zs", 23, "法外狂徒");
const person2 = new CreatePerson("lm", 26, "韩梅梅");
console.log(person1);
console.log(person2);
但是我们发现这样做无疑为每一个实例对象都添加了一个sayHi方法,而且每个实例上的方法都不相等,但我们的目的是让每个实例都有这么一个功能就好了,不必创建这么多的sayHi方法而去浪费内存空间。说的直白一点,比如家里有五个孩子,每个孩子都想玩switch游戏,那么家长要给每个孩子买一台switch吗?当然家里有矿的当我没说,一般家庭是不是就买一台,然后哪个孩子想玩就管家长去要就行了是不是。
同样的,代码就是对现实生活的抽象,那么我们是不是也可以这样做,把方法添加到这些实例都拥有的一个爸爸或者妈妈上是不是就好了,而仔细想想在new的五步中第二步是不是做了这么一件事,没错他就是js中的原型。这个原型就是这些实例的爸爸(我更喜欢把构造函数叫做妈妈,个人习惯罢了)。
说了这么多我们总算要讲原形了,但是我们要深入的了解原型就应该知其然知其所以然。当我们一步一步的引出原型,相信我们对于原型的理解会更加深刻。
二、使用原型
通过上面的讲解我们似乎对原型的作用有了大致的理解,就是把实例对象上需要用到的共有方法添加到原型上,而实例对象的自己的私有属性写在构造函数内部。接下来我们对构造函数进行再一次改造:
function CreatePerson(name, age, hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
}
const person1 = new CreatePerson("zs", 23, "法外狂徒");
const person2 = new CreatePerson("lm", 26, "韩梅梅");
CreatePerson.prototype.sayHi = function() {
console.log("你好,我叫" + this.name)
};
person1.sayHi();
person2.sayHi();
console.log(person1.sayHi === person2.sayHi) // true
首先向大家说明一点我并没有按着定义一个构造函数,然后在构造函数的原型上添加sayHi方法,接着使用new创建实例的顺序来写代码。而是先new 创建了实例,然后再在原型上添加方法,这样的目的是想告诉大家,原型是具有动态性的,即你先创建了实例,在实例之后给原型添加了方法那么实例依然是可以访问的。而且可以看到通过比较person1和person2的sayHi方法我们发现这是同一个方法。这样我们完美的解决了构造函数的问题。
另外我再补充一些:
一、每一个函数在创建的时候内部都会有一个prototype属性指向它的原型。但是有一个函数是个特例他就是Function.prototype。(关于这点这里不详细解释我推荐大家去看看这篇文章,文章的博主平时也给我解答了不少问题。链接:https://www.cnblogs.com/echolun/p/12384935.html)
二、每一个原型都有一个初始的属性constructor指回与之对应的函数。
三、每当我们使用new关键字创建一个实力的时候,实例的内部特性[[Prototype]]就指向了他的构造函数对应的原型,但是我们js中不能直接使用这个特性,但一些浏览器通过对外暴露__proto__这个属性使我们可以访问实例对应的原型。
三、原型链
当我们在一个对象上查找一个属性的时候,会先在对象的本身去查找,然后如果没有找到,就会去他的原型上查找,如果这个原型也没有,而原型又可以看作一个实例对象(一般是Object()的实例对象),那么他也拥有原型,那么就会去原型的原型查找,这样一路向上查找,直到找到原型链的顶端null,还是没有找到的话就会返回undefined,这样就形成了原型链。下面是一条我绘制的比较完整的原型链:
可以看到每一个原型可以看成一个实例对象并与之拥有与其对应的原型,而每一个函数(函数也是对象)都是Function构造函数的实例,但是Function.prototype是一个特例,它不像其他构造函数的原型是一个对象,而是一个函数,并且他也不像其他的函数那样内部有一个属于自己的prototype属性。我们可以验证下:
console.dir(Function.prototype) // 打印Function.prototype
console.log(Object.getOwnPropertyNames(Function.prototype)) // 打印Function.prototype的自有属性包括可枚举和不可枚举
console.log(Object.getOwnPropertyNames(CreatePerson)); // 打印构造函数CreatePerson的自有属性包括可枚举和不可枚举
另外我们可以看到Function.prototype是一个函数并且上面有apply, bind, call这些方法,这也就是为什么函数可以调用这些方法。并且确实可以看到Function.prototype是不含有prototype属性的。
下面如果再讲的话就是把原型和继承联系上了,这个要另起一篇文章了。