原型和原型链
比我们通过一个构造函数new了一个新对象,构造函数的原型prototype指向一个对象,所有通过该构造函数new的新对象可以共享它所包含的属性和方法。
构造函数的原型prototype是一个对象,那么它也可以有自己的构造函数原型prototype,通过这样,形成一个原型链。原型链最终都可以上溯到Object.prototype。Object.prototype的__proto__是null。null没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null。
来看一个例子:
function Test() {}
var test = new Test();
test.__proto__===Test.prototype
Test.prototype.__proto__===Object.protype
Object.protype.__proto__===null
test.__proto__.__proto__.__proto__ === null
Test.__proto__===Function.prototype
获取对象原型API: Object.getPrototypeOf(a) 即a.proto (tip: 方法在Object上而不是Object.prototype上)
为了更清晰地理解,看以下练习:
var obj= {}
obj.__proto__ === Object.prototype // 为 true
var fn = function(){}
fn.__proto__ === Function.prototype // 为 true
fn.__proto__.__proto__ === Object.prototype // 为 true
var array = []
array.__proto__ === Array.prototype // 为 true
array.__proto__.__proto__ === Object.prototype // 为 true
Array.__proto__ === Function.prototype // 为 true, Array的本质为一个构造函数
实际上,看a.__proto__是什么,就看a的本质是什么:
1.new出来的对象,则指向其构造函数的prototype;
2.构造函数,则Function.prototype
new
new的过程:
初始化一个新对象
该对象的__proto__属性指向构造函数的原型prototype
将构造函数的this指向新对象,并执行函数
将新对象返回
[如果构造函数有返回一个对象(null除外),则将构造函数内的对象返回]
手写new:
function myNew(func, ...params) {
const obj = {};
obj.__proto__ = func.prototype
const temp = func.apply(obj, params)
if(temp && typeof temp === 'object') {
return temp
}else{
return obj
}
}
在这里插播一个经常面试的知识点:
手写apply, call,bind
既然知道这三个方法是用来改变this指向,那么可以利用this在方法中谁调用指向谁的特性,将函数绑定到对象上,执行后再删除
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>my call apply bind</title>
</head>
<body>
<script>
Function.prototype.myApply = function (context, params) {
context.fn = this;
context.fn(...params)
delete context.fn
}
Function.prototype.myCall = function (context, ...params) {
context.fn = this;
context.fn(...params)
delete context.fn
}
Function.prototype.myBind = function (context, params) {
const self = this
return function () {
self.apply(context, params)
}
}
function Person(name, age) {
this.name = name
this.age = age
}
let obj = {}
let obj1 = {}
let obj2 = {}
Person.myApply(obj, ['lf', 18])
Person.myCall(obj1, 'lf', 18)
Person.myBind(obj2, ['lf', 18])()
</script>
</body>
</html>
继承
既然通过原型链实例能访问到上层的一些方法和属性,那么,自然而然继承可以由他来实现。
原型链继承
最容易想到的是,让构造函数的原型指向另一个你想要继承的构造函数的实例,如下:
// 原型链继承
// 缺点1:任何一个实例改变原型的属性,所有实例的原型属性都会变,因为指向的是同一个
// 缺点2:没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数
function Father(firstName) {
this.firstName = firstName
//属性为引用类型会被共享
this.ver = ['1.0', '2.0']
}
function Son(lastName) {
this.lastName = lastName
}
Son.prototype = new Father('lee')
const a = new Son('mei')
const b = new Son('lei')
a.ver[0] = '3.0'
console.log(a)
console.log(b)
<script>
// 借用构造函数,解决了上面的两个问题
// 缺点1:方法都在构造函数中定义,函数无法复用
// 缺点2:在超类型的原型中定义的方法,对子类型而言也是不可见的
function Father(firstName) {
this.firstName = firstName
//属性为引用类型会被共享
this.ver = ['1.0', '2.0']
}
function Son(firstName, lastName) {
Father.call(this, firstName)
this.lastName = lastName
}
const a = new Son('lee', 'mei')
const b = new Son('han', 'lei')
a.ver[0] = '3.0'
console.log(a)
console.log(b)
</script>
<script>
// 组合原型链和构造函数,解决了上面的四个问题
// 缺点1:会执行构造函数两遍,父类的属性会创建两遍
function Father(firstName) {
this.firstName = firstName
//属性为引用类型会被共享
this.ver = ['1.0', '2.0']
}
function Son(firstName, lastName) {
//第二次调用
Father.call(this, firstName)
this.lastName = lastName
}
//第一次调用
Son.prototype = new Father('tan')
const a = new Son('lee', 'mei')
const b = new Son('han', 'lei')
a.ver[0] = '3.0'
console.log(a)
console.log(b)
</script>
<script>
// 原型式继承
// 如果目的是让一个对象与另一个对象保持类似的情况.
// 在传入一个参数的情况下,Object.create()与 object()方法的行为相同。
// Object.create(null)创建一个空对象,不继承object的任何方法
function object(o) {
function F() {}
F.prototype = o
return new F()
}
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
}
const a = object(person)
console.log(a)
</script>
<script>
// 终极版,利用原型式继承,让子构造函数原型能够调用父构造函数原型的方法
function Father(firstName) {
this.firstName = firstName
this.ver = ['1.0', '2.0']
}
function Son(firstName, lastName) {
Father.call(this, firstName)
this.lastName = lastName
}
Father.prototype.say = function () {
console.log(this.firstName)
}
// 不直接指向父类的原型,因为这样两者指向了同一个对象,实现了继承,但改变任何一个,另一个也会受影响
Son.prototype = Object.create(Father.prototype)
const a = new Son('lee', 'mei')
const b = new Son('han', 'lei')
a.ver[0] = '3.0'
console.log(a)
console.log(b)
</script>
通过上面的流程也可以很好地去理解ES6中的extends。
es6 extends super
class Father {
constructor(firstName) {
this.firstName = firstName
this.ver = ['1.0', '2.0']
}
update(firstName) {
this.firstName = firstName
}
}
class Son extends Father{
constructor(firstName, lastName) {
// 继承父类属性,必须有这句之后才能调用父类方法
super(firstName)
this.lastName = lastName
}
update(firstName, lastName) {
// 调用父类方法
super.update(firstName)
this.lastName = lastName
}
}
// 通过观察xiao的结构可以看出,原理大概是终极版
let xiao = new Son('lee', 'mei')
看看下面的两个例子:
function F1(){
this.a1='aaa';
this.b1='bbb';
this.f1=function(){
console.log('f1f1')
}
}
F1.prototype.f2=function() {console.log('f2f2')}
class S1 extends F1{
k1(){
this.f1();
}
k2() {
super.f2() ;
}
}
new S1().f1();
new S1().k1();
new S1().k2() ;
function F1(){
this.a1='aaa';
this.b1='bbb';
this.f1=function(){
console.log('f1f1')
}
}
class S1 extends F1{
k1(){
super.f1();
}
}
new S1().f1();
new S1().k1();
这些输出你做对了吗?
TIPS:当你调用super()时,解析为调用父类的constructor。当你调用super. xxx(),解析为调用父类的原型对象上的方法。