原型和原型链真的是 JavaScript
面试中永远绕不开的话题,说实话,感觉理解起来其实也不复杂,但是吃透或者实际应用中做到活学活用又感觉总是差那么一点意思?两年前自己写过相关文章:关于实例、构造函数、原型、原型链内容整理 ,不过当时是用 ES5
写的,如今 ES5
快要成为过去式了,正好用 ES6
中的语法重新梳理一遍,加深自己的印象。
认识 Class
class
语法其实说白了就是一个语法糖而已,其底层还是通过构造函数
来创建的。ES6
中的 class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。我们先来用 ES5 的语法写一个构造函数
function student(name, number) {
this.name = name
this.number = number
}
student.prototype.sayHi = function () {
console.log(`姓名:${this.name},学号:${this.number}`)
}
let zhangsan = new student('张三', 100)
console.log(zhangsan.name) // 张三
console.log(zhangsan.number) // 100
console.log(zhangsan.sayHi()) // 姓名:张三,学号:100
当然,ES5
这种我估计大家都快写烂了,我们用 ES6
语法来将上述代码重写一遍
// 创建类
class Student {
constructor(name, number) {
this.name = name
this.number = number
}
sayHi() {
console.log(
`姓名:${this.name},学号:${this.number}`
)
}
}
// 通过类 new 对象/实例
let zhangsan = new Student('张三', 100)
console.log(zhangsan.name) // 张三
console.log(zhangsan.number) // 100
console.log(zhangsan.sayHi()) // 姓名:张三,学号:100
注意:我们在用
class
定义类的时候,前面不需要加function
, 类名的第一个首字母一般都会大写,而且方法之间不需要逗号分隔,加了会报错。
我们首先来看一下 Student
类中的第一个方法 constructor
,constructor
方法是类的默认方法,通过 new
命令生成对象实例时,自动调用该方法。一个类必须有 constructor
方法,如果没有显式定义,一个空的 constructor
方法会被默认添加。方法中的 this
关键字代表实例对象, 类的数据类型就是函数,前面我们也说了,其实 class
的写法本身就是 ES5
中构造函数写法的语法糖,类本身就指向构造函数。如下栗子:
class A {
}
// 等同于
class A {
constructor() {
}
}
console.log((typeof A)); // function
先不扯远了,毕竟这里我们主要记录的还是原型和原型链,关于 ES6
中 class
类的定义和一些用法后期会专门记录一篇文章深入理解。回到我们上面的 Student
类和实例 zhangsan
中来。
我们前面说过很多次,class
的写法本身就是 ES5
中构造函数写法的语法糖,所以我们在 Student
类当中定义的 sayHi
方法其实等同于 ES5
中直接在原型上添加的方法:
// ES5 通过 prototype 添加方法
student.prototype.sayHi = function () {
console.log(`姓名:${this.name},学号:${this.number}`)
}
// ES6 类中方法的定义
class Student {
sayHi() {
console.log(
`姓名:${this.name},学号:${this.number}`
)
}
}
那么我们在这里可以打印出 Student
类的 prototype
console.log(Student.prototype) // {constructor: ƒ, sayHi: ƒ}
接下来我们再来看一下实例 zhangsan
的 __proto__
console.log(zhangsan.__proto__) // {constructor: ƒ, sayHi: ƒ}
好吧,奇怪的化学反应好像发生了,实例的 __proto__
好像和类的 prototype
好像完全相等,用我们的 ===
验证一下
console.log(Student.prototype === zhangsan.__proto__) // true
那么问题来了,
prototype
和__proto__
是什么鬼?为啥这两者会相等?说了半天你也没告诉我原型是什么,只给了我 3 段莫名其妙的代码让我自己揣摩是吧!!!
-
01.pngprototype
:显示原型。我们可以这样理解,每个class
都有显示原型。无论什么时候,只要创建一个新函数,就会根据一组特定的规则为该函数创建一个prototype
属性,这个属性指向函数的原型对象。普通对象没有prototype
,但有__proto__
属性。这个类中定义的一些方法都存储在这个类的prototype
。如下图:
-
02.png__proto__
:隐式原型 。每个实例都有隐式原型,实例的隐式原型指向class
的显示原型,具体关系如下图:
基于原型的执行规则
- 获取属性
zhangsan.name
或执行方法zhangsan.sayHi()
时 - 先在自身属性和方法寻找
- 如果找不到则自动去
__proto__
中查找
好吧,感觉绕了一圈就是想引出实例的隐式原型等于类的显示原型呗,实例上不存在的方法,会通过隐式原型找到类的显示原型中去,如果类的显示原型中存在该方法,那么实例就可以直接调用到该方法。那么问题来了,实例可以通过
_ proto _
指向原型对象,构造函数可以通过prototype
指向原型对象,那么原型对象是否有属性指向构造函数或者实例呢?
原型对象无法直接指向实例,因为同一个类可以生成多个实例,但是原型对象指向构造函数倒是有,这里就引入了 constructor
,每个原型对象都有一个 constructor
属性指向关联的构造函数。
console.log(Student.prototype.constructor === Student) // true
所以我们的关系图又可以更新啦:
03.png
好吧,休整休整,通过一系列的证明和画图,我们总算是初步罗列了类、实例、原型对象之间的基本关系。再啰嗦的汇个总:
每一个类都有自己的显示原型
prototype
,类中的方法基本都存储在原型对象(类的prototype
)中;实例通过类生成,每一个实例都有自己的隐式原型__proto__
,并且实例的隐式原型等于类的显示原型,所以实例调用的方法如果自身找不到就会找到类的显示原型中;同时类的显示原型通过constructor
也可以得到类本身。
认识原型链
感觉原型就说了一大堆,其实除了有点绕,感觉真心不复杂!!!原型链字面意识上来讲就是原型组成的链吗,那么我们就通过类的继承先来搭建一条链(声明父类,多创建几个具有层级关系的子类)
// 构建父类 People,所有子类继承 name 属性和 eat 方法
class People {
constructor(name) {
this.name = name
}
eat(food) {
console.log(`${this.name}爱吃:${food}`)
}
}
// 构建子类 Student
class Student extends People {
constructor(name, number) {
super(name)
this.number = number
}
sayHi() {
console.log( `姓名:${this.name},学号:${this.number}`)
}
}
// 构建子类 Teacher
class Teacher extends People {
constructor(name, major) {
super(name)
this.major = major
}
teach() {
console.log(`${this.name}教${this.major}`)
}
}
// 根据 Student 类构建实例对象 zhangsan
const zhangsan = new Student('张三', 100)
console.log(zhangsan.name) // 张三
console.log(zhangsan.number) // 100
zhangsan.sayHi() // 姓名:张三,学号:100
zhangsan.eat('土豆') // 张三爱吃土豆
// 根据 Teacher 类构建实例对象 wanglaoshi
const wanglaoshi = new Teacher('王老师', '语文')
console.log(wanglaoshi.name) // 王老师
console.log(wanglaoshi.major) // 语文
wanglaoshi.teach() // 王老师教语文
wanglaoshi.eat('大蒜') // 王老师爱吃大蒜
又出现了两个奇怪的单词
extends
和super
,其实都和继承相关,extends
很容易就能看出是构建继承关系,super
在这里表示父类的构造函数,用来新建父类的this
对象。子类必须在constructor
方法中调用super
方法,否则新建实例就会报错。这是因为子类自己的this
对象,必须先通过 父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super
方法,子类就得不到this
对象。这里就不展开代码罗列了,简单了解概念就行。毕竟class
写法属于ES6
语法部分,其实它的用法也比较多,可能需要单独来记录一篇。
因为 Student
类继承于 People
类,前面说原型的时候也提到了,每一个类创建就会根据一组特定的规则为该函数创建一个 prototype
属性,这个属性指向函数的原型对象,而每一个函数对象都有 __proto__
属性,那么我们先基于这些理论来构建子类 Student
和父类 People
的初步关系:
console.log(Student.prototype.__proto__ === People.prototype) // true
算了,看这段代码估计小伙伴也懵逼,还是继续画流程图,用图说话:
04.png从图中其实我们可以将
Student
也理解成是通过 new People
生成的一个实例对象(仅供理解,可能JS底层解析是这样渲染的,当参考就行...)。zhangsan
这个实例对象拥有 name
和 number
两个自身属性。而 sayHi()
其实来自于 Student
类,eat()
其实来自于 People
类,怎么去验证这件事情了?基础篇(一)中我们写深拷贝函数的时候用到过 hasOwnProperty
这个方法,当时说了这个方法主要是验证该对象的属性是自身属性还是通过原型 prototype
添加的属性,那我们在这里在来尝试一下:
console.log(zhangsan.hasOwnProperty('name')) // true
console.log(zhangsan.hasOwnProperty('number')) // true
console.log(zhangsan.hasOwnProperty('sayHi')) // false
console.log(zhangsan.hasOwnProperty('eat')) // false
image.png奇怪的问题又来了,
hasOwnProperty
又从哪里来的呢,为啥zhangsan
可以对它进行直接调用呢?我们要不要验证一下zhangsan.hasOwnProperty('hasOwnProperty')
来看看,不用说肯定是false
,那么说明zhangsan
肯定是在哪里继承了这个方法才能直接使用这个方法,很明显People
和Student
两个类中都没有这个方法,那它到底来自哪里呢?看图解答:
原来 People
类的上一级还有一个大Boss,那就是 Object
,也就是说 People
的显示原型对象的隐式原型指向了 Object
的显示原型,而 Object
的显示原型中包含了一系列底层帮我们定义好的方法:toString
、hasOwnProperty
等,所以 zhangsan
才可以直接调用 hasOwnProperty
这个方法。在这里我们要知道 Object.prototype
已经是所有原型的最顶层了,它的隐式原型永远指向 null
,也就是不会再往上找了。这些原型所形成的的闭环其实就是原型链,
总结原型链的概念
简单理解原型链就是原型组成的链,实例的 _ proto _ 是一个原型对象,既然它是一个原型对象也有 _ proto _ 属性,原型 _ proto _ 又是原型的原型,我们就可以一直通过_ proto _向上找,这就是原型链。当向上找到Object的原型的时候,这条原型链就算到头了。通过原型链,实例对象可以一层一层往上找不属于自身定义的属性或者方法并直接调用它们。
基于原型链再看 instanceof
instanceof 的原理其实就是通过隐式原型
__proto__
去原型链中向上查找,如果该实例的隐式原型能在原型链上找到对应类的显示原型,那么就说明它们有继承关系即返回true
,如果找不到即会返回false
。
扩展思考
学而不用基本等于没用,总结了原型和原型链这一大堆内容,我们总要用起来才能加深我们对知识的理解!题目来了,手写一个简易的
jQuery
并且要考虑插件和扩展性?
<body>
<p>111</p>
<p>222</p>
<p>333</p>
</body>
<script>
class jQuery {
// 获取元素节点
constructor(selector) {
const result = document.querySelectorAll(selector)
const length = result.length
for (let i = 0; i < length; i++) {
this[i] = result[i]
}
this.length = length
this.selector = selector
}
// 根据索引获取对应元素
get(index) {
return this[index]
}
// 遍历
each(fn) {
for (let i = 0; i < this.length; i++) {
const elem = this[i]
const index = i
fn(elem, index)
}
}
// 绑定事件
on(type, fn) {
return this.each(elem => {
elem.addEventListener(type, fn, false)
})
}
}
// 验证我们写的函数
let $p = new jQuery('p')
console.log($p) // jQuery {0: p, 1: p, 2: p, 3: p, length: 4, selector: "p"}
$p.get(0) // <p>111</p>
$p.each((item, index) => {
console.log(item, index) // <p>111</p> 0 <p>222</p> 1 <p>333</p> 2
})
$p.on('click', function () {
console.log(111)
})
</script>
初步验证,我们上面写的四个简单的方法都可以用,接下来我们要考虑如何给这个简单的 jQuery 来装上插件,这里我们假设写一个弹框插件:
jQuery.prototype.dialog = function (info) {
alert(info)
}
let $p = new jQuery('p')
$p.on('click', function () {
$p.dialog('我是弹窗')
})
好吧,一般写插件其实就是在它的原型上增加一些方法而已,那如果我们想基于这个 jQuery
自己扩展写一下方法呢?比如我这里想增加一个添加类名的方法
class myJquery extends jQuery {
constructor(selector) {
super(selector)
}
// 假设接收两个参数,第一个是类名,第二个是索引(不传默认所有的都加)
addClass(className, index) {
const result = document.querySelectorAll(this.selector)
// 判断第二个参数是否存在
if (index && typeof index === 'number') {
result[index].className = className
} else {
// 不存在所有元素都加上
for (let i = 0; i < this.length; i++) {
result[i].className = className
}
}
}
}
let $p = new myJquery('p')
$p.addClass('aaa', 1)
$p.addClass('aaa')
好吧,写的很简单,实现其实也不难,更多的是思维发散。原型和原型链就整理到这里了,看起来简单实际整理确也用了好长时间,不过好记性不如烂笔头,多写相信终归是有用的。说实话自己只是个小菜鸟,如果文中有不对的地方或者理解有误的地方欢迎大家提出并指正。每一天都要相对前一天进步一点,加油!!!
网友评论