说来惭愧,做前端也有好几年了,从最开始的 JQuery 搭配 js原型, 通过面向对象的方式开发项目,到后来用 React Vue ES6/7/8等技术做项目,工作中也经常会通过原型在内置js 对象上去扩展封装一些方法等,但是好像从来没有总结过 javascript 的原型和原型链。正好最近在啃面试题看到这些知识,决定整理总结一下。留着以后复习。写的不好的地方还请个位大神指正。
1、prototype
原型 prototype 其实是 function 对象的一个属性,
打印出来看了一下,结果它也是对象
function Handphone (color, brand) {
this.color = color
this.brand = brand
this.screen = '18:9'
this.system = 'Android'
}
console.log(Handphone.prototype)
打印出来的结果是一个对象,对象里包含两个属性, constructor
和 __proto__
;这里的 constructor
指向的是构造函数本身, Handphone
, __proto__
属性下边我们再说, 展开 constructor
属性以后,里边这些浅粉色的属性都是 javascript
内置的属性。 下边的 name
属性就是对应的构造函数的名字。我们还发现Handphone
构造函数中的 color、brand、 screen、 system
属性都没有出现在 Handphone.prototype
对象中,是因为没有经过实例化的构造函数,其内部定义的属性不会创建。
让我们继续,给这个构造函数的原型上添加一些其他属性,在看一下
function Handphone (color, brand) {
this.color = color
this.brand = brand
this.screen = '18:9'
this.system = 'Android'
}
Handphone.prototype.rom = '64g'
Handphone.prototype.ram = '6g'
Handphone.prototype.screen = '16:9'
console.log(Handphone.prototype)
这时候 Handphone.prototype
上的属性就打印出来了,然后我们在来实例化这个构造函数看下
function Handphone (color, brand) {
this.color = color
this.brand = brand
this.screen = '18:9'
this.system = 'Android'
}
Handphone.prototype.rom = '64g'
Handphone.prototype.ram = '6g'
Handphone.prototype.screen = '16:9'
var hp1 = new Handphone('red', '小米')
var hp2 = new Handphone('black', '华为')
console.log(hp1)
console.log(hp2)
这时候再来打印下 构造函数原型(Handphone.prototype)
上的属性看下
function Handphone (color, brand) {
this.color = color
this.brand = brand
this.screen = '18:9'
this.system = 'Android'
}
Handphone.prototype.rom = '64g'
Handphone.prototype.ram = '6g'
Handphone.prototype.screen = '16:9'
var hp1 = new Handphone('red', '小米')
var hp2 = new Handphone('black', '华为')
console.log(hp1)
console.log(hp2)
console.log(hp1.rom) // 64g
console.log(hp2.ram) // 6g
console.log(hp1.screen) // 18:9
console.log(hp2.screen) // 18:9
这时候发现,通过 Handphone
构造函数构造出来的实例 hp1 hp2
对象去访问,Hnadphone.prototype
上的属性 也是可以访问到的,通过这里,我们可以总结出来prototype
到底是什么:
其实这个 prototype 是定义构造函数构造出的每个实例对象的公共祖先,所有被该构造函数构造出的实例对象都可以继承原型上的属性和方法
2、constructor
上边我们说过,构造函数的原型上都有constructor
属性,这个属性指向的是构造函数本身,我们还可以给当前构造函数通过 constructor
属性指定其他的构造函数
function Telephone () {
this.name = '123'
}
Telephone.prototype.name = '张三'
Handphone.prototype = {
constructor: Telephone
}
console.log(Handphone.prototype)
打印结果如下
3、__proto__
function Car () {}
Car.prototype.name = 'Benz'
Car.prototype.age = 123
console.log(Car.prototype)
var car = new Car()
console.log(car)
打印结果如下
通过上打印结果我们可以看出来 Car.prototype
打印的结果是 红框 ① 中的内容, 实例化对象car
打印 的结果是 红框② 里的内容,只有一个 __proto__
属性,这个属性对应的是一个对象,展开后的内容也就是红框 ③里的内容,对比之后发现, 红框③里的内容和 红框①里的内容是一样的。由此可见:
__protot__
属性就是构造函数实例化以后对象的一个属性,这个属性指向的是构造函数的 prototype (原型) 对象
然后我们通过 car.name 和 car.__proto__.name
来访问下 car 实例的 name 属性
function Car () {}
Car.prototype.name = 'Benz'
Car.prototype.age = 123
console.log(Car.prototype)
var car = new Car()
console.log(car)
console.log(car.name)
console.log(car.__proto__.name)
结果如下:
发现都读取到了 Car.prototype
上的 name
属性了
根据上边的两个例子和打印内容我们可以总结出来 __proto__
属性到底是什么了:
其实__protot__
属性就是构造函数实例化以后对象的一个属性,这个属性指向的是构造函数的 prototype (原型) 对象,通过实例化对象调用 __proto__
可以访问到构造函数的原型。
同样 __proto__属性也是可以更改的: 如下
function Person () {}
Person.prototype.name = '张三'
var p1 = {
name: '李四'
}
var person = new Person()
console.log(person.name) // 张三
person.__proto__ = p1
console.log(person.name) // 李四
还有一个有意思的面试题:
Car.prototype.name = 'Benz'
function Car () {}
var car = new Car()
// Car.prototype.name = 'Mazda'
Car.prototype = {
name: 'Mazda'
}
console.log(car.name) // Benz
console.log(car)
打印结果如下
通过上边的图片我们可以看出来, car.name
打印的结果是 Benz
, 打印car
实例 我们发现
name: 'Mazda'
跑到了 __proto__ 下的 constructor 构造函数下的 prototype 里
,也就是 Car
构造函数的原型里了,这是因为什么呢?
我们在看上边的代码, 通过 Car.prototype = {} 定义 name: 'Mazda'
的上边已经实例化了 Car 构造函数,
并且 Car.prototype = {}
这种 原型属性直接等于一个花括号的这种方式是重写, 也就是重写了Car 构造函数的原型属性,而且是 Car 构造函数被实例化以后,重写了 Car 构造函数的原型。
所以, name: 'Mazda'
属性才会跑到了 实例对象 __proto__ 下的 cosntructor (也就是 Car 构造函数)下的 prototype (原型) 里
如果 我们把代码改成这样:打印出来的结果就是 Mazda 了
Car.prototype.name = 'Benz'
function Car () {}
var car = new Car()
Car.prototype.name = 'Mazda'
// Car.prototype = {
// name: 'Mazda'
// }
console.log(car.name) // Mazda
console.log(car)
然后展开打印的实例 car 的 __proto__
对象,发现 name 属性已经被修改了。
这是因为 Car.prototype.name = 'Mazda'
这种方式是重新赋值,覆盖掉原来原型上的name 属性的值,即使上边实例化过这个构造函数了。只要是在console.log() 前重新赋值的,那打印出来的值,都是最后赋值的值。
4、原型链
Professor.prototype.tSkill = 'JAVA'
function Professor () {}
var professor = new Professor()
// Teacher原型 继承 professor 实例
Teacher.prototype = professor
function Teacher () {
this.mSkill = 'JS/JQ'
}
var teacher = new Teacher()
// Student 原型继承 teacher 实例
Student.prototype = teacher
function Student () {
this.pSkill = 'HTML/CSS'
}
var student = new Student()
console.log(student)
console.log(student.tSkill)
上边的例子 是 ES5 通过原型继承的方式写法,这里如果有不明白的同学可以自行百度,网上讲 javascript 继承的按理很多,这里不再过多概述。
通过上边里小例子 访问 student.tSkill
打印出来的是 JAVA
然后我们打印 student
实例 对象,展开里边的 __proto__
属性后发现,通过沿着__proto__
属性一直向上找,可以找到 Professor
构造函数的原型,其实当我们访问 stucent.tSkill
属性,沿着 __proto__
去找原型里的属性,一层一层的继承的属性的这条链条就是原型链。
通俗点说: 就是每个实例对象下都有 __protot__
属性 (这个属性对应的是,当前构造函数的原型),当我们要访问实例对象上的某个属性时,js 先在实例对象本身的属性中寻找,如果没有的话,再去当前构造函数的原型上去找,如果还没有,并且如果这个构造函数继承了其他构造函数的实例,就会继续向上找,去被继承的对象的 实例属性中找,如果被继承的实例对象属性中没有,继续去被继承的实例对应的构造函数的原型上去找。一直通过这个过程向上找。通过 __proto__
属性一层等继承的这个链条,就是原型链
原型链一直向上,肯定会有一个最顶层的对象,这里我们来看下,最顶层的对象对应的是什么
上图红框里的内容是我们展开最后一个 __proto__
属性对应的对象里的内容
发现里边的 cosntructor 对应的是 Object() 构造函数
里边包含了 Object 原型上的一些属性和方法
通过上边的内容我们可以总结出: 原型链的最顶端 __proto__
属性对应的其实就是 Object.protptype
,然而Object.prototype 的原型为 null ,并且 null 没有原型
, 所以原型链的最顶端就是 null