前言
相信对其他语言有过一点了解的同学们都知道,在Java中有一句话广为流传,那就是万物皆对象。其实除了Java,其他的编程语言中或多或少都存在这个概念,例如c++中就有类和结构体两种数据结构,而这两种数据结构其实跟对象差不多。在这么多门语言中都有对象这个概念,那么JavaScript中有吗?答案是有的,并且它还有类似Java对象中的原型这一属性。下面呢我们将深入学习一下js中的原型以及它的进阶原型链。
1. 构造函数
在讲原型之前呢,我们先来聊一聊构造函数。这时,有同学可能就会问了,要讲原型跟构造函数有什么关系?知道大家很急,但是先别急,下面听我慢慢道来。
首先呢我们得知道构造函数 与普通函数 的区别,那就是没有区别 。这是因为构造函数本质上它也是一个函数,只不过是用new来创建了一个对象而已。
在定义构造函数的时候最好是首字母大写(这是程序员默认的规则,当然不用也可以),这样可以便于区分两个函数。在了解完了这两个的区别之后,接下来我们先从基础数据类型和引用数据类型开始讲起。
基础数据类型
在js这门语言中,基础数据类型有七种,分别是:
- String(字符串)
- Number(数字)
- Boolean(布尔值)
- Symbol(符号)
- null(空值)
- undefined(未定义)
- bigint(大整形)
在定义上面的数据类型的时候我们都知道有以下两种定义的方法:
js
//先用字符串举个例子
let s1 = 'hello'
let s2 = new String()
s2 = 'hello'
console.log(s1)
console.log(s2)
//输出:
//hello
//hello
大家看上面代码可能看出点了区别,但是在实际使用过程中我们用s1的定义方法就行,没必要用s2的定义方法,因为这两种方法所定义的变量都是一样的,这就和v8有关系了。
V8在执行过程中会将s1以s2的形式来定义会先让s1 = new String() ,然后将值赋予给它,这种方法是new一个实例对象出来并且赋值。(对于各种数据类型基本都会执行该操作)那么这个时候就会有人问了,为什么V8要这样多此一举呢,直接定义不就行了为什么需要用构造函数来定义呢?这里我们就得聊聊字符串身上的方法了。
众所周知,在字符串身上有很多种方法,例如:tostring、split、trim等很多种方法,大家有没有想过为什么这些方法可以在每一个不同的字符串身上所使用呢?答案就是这些方法都是放在构造函数身上,当要使用这些方法的时候,就可以方便调用。在使用构造函数的时候我们并不能看到这些方法,那么这些方法都是存放在构造函数的哪里呢?答案是原型(prototype)。
可以打开浏览器的开发者工具查看:
引用数据类型
在js这门语言中引用数据类型有很多种,我们先了解以下几个常用的:
- Array(数组)
- Object(对象)
- Function(函数)
在定义引用数据类型的时候,它同样有基础数据类型的那两种定义方法:
js
let obj1 = new Object()
let obj2 = {}
这两种方法其实是一样的效果,众所周知,引用数据类型例如数组,它身上有很多的方法。那么它的方法是定义在哪里的呢?答案是跟基础数据类型一样,定义在构造函数的原型上。而构造函数是在哪里呢?想必大家已经知道了,就是new后面的那个函数。
2. 原型 (prototype
) 显式原型
在前面对构造函数以及它的使用有了一个基本的认识之后,接下来我们来聊一聊它身上的原型。那么就会有同学问了,前面说原型中有很多种方法,那么原型它到底是什么呢?答案是:原型(prototype) 是一个函数被定义出来天生就具有的属性,同时呢它也是一个对象。
在知道了原型是什么东西的时候,我们就可以来看看它存在的意义,在上文构造函数的部分其实已经说明了。
函数原型存在的意义:因为实例对象能访问到函数原型上的属性,所以原型存在的意义就是为了让某一种数据结构拥有更多的方法可用
接下来呢我们来通过几段代码了解一下原型的一些特点,最近雷总的su7很火,下面呢我们来造几辆su7:
js
function Car(color, owner) {
this.name = 'su7'
this.height = 1400
this.lang = 5000
this.color = color
this.owner = owner
}
let car1 = new Car('red', '小朱')
let car2 = new Car('green', '牛哥')
console.log(car1)
console.log(car2)
输出:
ok啊,现在我们小朱和牛哥的su7已经做好了。现在可以看到在这两辆车中,除了颜色和名字不是一样的,其他的都是生产厂家所固定的配置,都是一样的,那么我们是不是可以把这几个固定的配置提取出来但是还能被访问呢?答案是肯定的,在之前我们就提到原型上的属性可以用来被访问,那么我们就可以将属性定义在原型上方便访问,接下来我们根据这一点来改进一下代码:
js
Car.prototype.name = 'su7'
Car.prototype.height = 1400
Car.prototype.lang = 5000
function Car(color, owner) {
// this.name = 'su7'
// this.height = 1400
// this.lang = 5000
this.color = color
this.owner = owner
}
let car1 = new Car('red', '小朱')
let car2 = new Car('green', '牛哥')
接下来我们来看看输出:
这时候就有同学会问了,怎么那三个属性不见了呢?这是因为这三个属性被定义在了构造函数的原型身上,但是我们还是可以访问的,接下来我们就访问car1试试:
js
Car.prototype.name = 'su7'
Car.prototype.height = 1400
Car.prototype.lang = 5000
function Car(color, owner) {
this.color = color
this.owner = owner
}
let car1 = new Car('red', '小朱')
console.log(car1.name);
console.log(car1.height);
console.log(car1.lang);
console.log(car1);
根据输出结果我们可以看到虽然那三个属性被定义在了原型身上但是我们同样可以进行访问。这就引出了V8的一个查找规律:
V8查找对象属性规则:v8 在查找对象上的一个属性时,如果该属性不是对象显式原型拥有的,那么 v8 就会去对象的隐式原型上查找
(隐式原型在接下来一节,大家可以先简单理解为隐式原型 === 构造函数原型)
那这时候就有同学可能会想,既然我能定义那能不能修改呢?下面我们来试试:
js
Car.prototype.name = 'su7'
Car.prototype.height = 1400
Car.prototype.lang = 5000
function Car(color, owner) {
this.color = color
this.owner = owner
}
let car1 = new Car('red', 'zyz')
console.log(car1.name, car1);
car1.name = '奔驰'
console.log(car1.name, car1);
console.log(Car.prototype);
根据输出结果我们可以看到,实例对象想修改构造函数原型上面的属性是不行的,它只能修改自身的属性。在经过了上面两段代码的学习之后,下面我们来小结一下原型。
原型小结
- 原型(
prototype
) 是一个函数被定义出来天生就具有的属性- 函数原型存在的意义:因为实例对象能访问到函数原型上的属性,所以原型存在的意义就是为了让某一种数据结构拥有更多的方法可用
- 可以将一些固定的属性提取到原型上,减少重复的代码执行
- 实例对象无法修改(增加或者删除)函数原型上的属性
3.对象原型 (__proto__
)隐式原型
在前面的篇幅中我们对原型已经有了一个了解,接下来呢我们聊一聊对象原型,防止怕大家对概念名称绕懵圈,就把原型称为显示原型 ,把对象原型称为隐式原型。
在上文中我们提到了当实例对象中的属性被重复使用时,我们可以将之定义在构造函数的原型身上,那为什么实例对象可以调用原型上面的方法呢?这就和我们呢接下来要讲的隐式原型有关了。
在上面原型创建su7的例子中我们可以发现,在访问实例对象中某个属性时,如果实例对象上没有该属性,那么就会去创建该对象的构造函数的原型身上进行查找,而这正是上文中V8查找对象属性的规则 。而后面小括号中的内容之所以说成隐式原型 === 构造函数原型 ,这是因为在创建对象时,对象的隐式原型(__proto__
)会被赋值成创建该对象的构造函数的显示原型(prototype
)。下面将用一段简单的代码来展示:
js
let arr = []
let res = arr.__proto__ === Array.prototype
console.log(res)
//输出:
//true
由上面的代码我们可以知道,当创建了一个arr数组的时候,就相当于运行了arr = new Array(),而arr就是被创建出来的实例对象,实例对象的隐式原型和构造函数的显式原型相等,这就是为什么V8的查找机制会那样执行。
对象原型小结
- v8 在查找对象上的一个属性时,如果该属性不是对象显式拥有的,那么 v8 就会去对象的对象原型上查找
- 对象的隐式原型会被赋值成创建该对象的构造函数的显示原型
4. this 的原理
在经过了上面显示原型和隐式原型的洗礼之后,大家心中可能还有点困惑,为什么构造函数创建实例对象时,要使用this这个关键字呢?下面用一段代码来展示一下:
js
Car.prototype.run = 'running'
function Car() {
this.name = 'su7'
this.height = 1400
// var this = {}
// var this = {
// name: 'su7',
// height: 1400
// }
// this.__proto__ = Car.prototype
// return this
}
let car = new Car()
console.log(car);
上述代码是调用构造函数Car来创建一个实例对象car,而在这个过程中使用了之前多次出现的this,函数中所注释的代码就是 new 的执行原理,下面给大家梳理一下
new 的执行原理
- 创建一个 this 对象
- 让构造函数中的逻辑正常执行(相当于往this对象上添加属性)
- 让this对象的
__proto__
= 构造函数的prototype
- return this 对象
5. 原型链
在上面了解完了显式原型和隐式原型的关系之后,接下来了解原型链就很简单了。
- 对象可以调用其构造函数的
prototype
原型对象的属性和方法 - 原型对象也是对象,同样也可以调用其构造函数的
prototype
原型对象的属性和方法 - 一层一层的形成一条链路就是原型链
下面来看一张非常经典的图片:
对于上面由__proto__
和prototype
所形成的链状结构我们称之为原型链,解释:
- 在浏览器中,通常使用对象的
__proto__
即可找到对象的原型对象,每个对象都有__proto__
属性(要去除Object.create创建的),用于指向它的原型对象。 - 前面基于原型编程有一个规则是,必须有一个根对象,JavaScript中根对象是:
Object.prototype
,它是一个空对象。
6. 所有的对象都有隐式原型吗
答案是否定的
在Object这个构造函数中有一个特殊的方法create(),这个方法会让用Object创建的对象的__proto__
等于create函数传入参数的__proto__
。 下面举个例子:
js
let a = new Object()
let b = Object.create(null)
console.log(a.__proto__);
console.log(b.__proto__);
所以说以后如果有人问到是不是所有对象都有隐式原型,要回答不是,由Object.create()所创建的可能没有隐式原型
谢谢大家的观看,喜欢的话点个赞吧!