在理解手写继承的原理之前必须要先学会三个API的实现原理
手写原型链相关API
这三个API和继承的实现以及原型链的指向息息相关,务必要先理解这几个API再去学习继承。
手写new操作符
function myNew(fn, ...args) {
// 1.创建一个新对象
const obj = {}
// 2.新对象原型指向构造函数原型对象
obj.__proto__ = Func.prototype
// 3.将构建函数的this指向新对象
let result = Func.apply(obj, args)
// 4.根据返回值判断
return result instanceof Object ? result : obj
}
还需要知道原型链的查找流程,如果不清楚可以去了解一下instanceof实现原理
手写instanceof
function myInstanceOf(obj, constructor) {
let proto = Object.getPrototypeOf(obj);
while (proto) {
if (proto === constructor.prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}
手写Object.create
function myCreate(obj, props) {
function F() {}
F.prototype = obj;
const newObj = new F();
if (props) {
Object.defineProperties(newObj, props);
}
return newObj;
}
原型链继承
使用原型链实现简单的继承。
function Parent() {
this.name = 'parent1';
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child1';
}
// new的过程Child.prototype.__proto__ = Parent.prototype; (1)
Child.prototype = new Parent();
console.log(new Child()); // {"type":"child2"}
// new的过程s1.__proto__ = Child.prototype
// 通过(1)又可得 s1.__proto__.__proto__ == Parent.prototype
var s1 = new Child();
var s2 = new Child();
s1.play.push(4);
console.log(s1.play, s2.play); // [1,2,3,4] [1,2,3,4]
缺点:由以上输出可知,new的实例共享的同一个原型对象,内存空间是共享的。
构造函数继承
通过改变this指向将父类的属性和方法定义在自己身上。
function Parent() {
this.name = 'parent2';
}
Parent.prototype.getName = function () {
return this.name;
}
function Child() {
// 修改this指向,
Parent.call(this);
this.type = 'child2'
}
let s1 = new Child();
let s2 = new Child();
console.log(s1.name); // parent2
s1.name = "change"
console.log(s1.name); // change
console.log(s2.name); // parent2
console.log(s1.getName()); // 会报错
缺点:与父类失去了关联,没有继承原型属性和方法。
组合继承
将原型链继承和组合继承组合在一起。
function Parent() {
this.name = 'parent3';
this.play = [1, 2, 3];
}
Parent.prototype.getName = function () {
return this.name;
}
function Child() {
// 第二次调用 Parent()
Parent.call(this);
this.type = 'child3';
}
// 第一次调用 Parent()
Child.prototype = new Parent();
console.dir(Child3);
// 手动挂上构造器,指向自己的构造函数,在此之前自建的prototype没有constructor
Child.prototype.constructor = Child;
var s3 = new Child();
var s4 = new Child();
s3.play.push(4);
console.log(s3.play, s4.play); // 不互相影响
s3.name = "change";
console.log(s3.getName()); // 输出'change'
console.log(s4.getName()); // 正常输出'parent3'
缺点:每次new Child都会执行两次Parent,造成了多构造一次的性能开销。
原型式继承
借助Object.create方法实现普通对象的继承。这里我们需要了解create的过程发生了什么
使用Object.create指向父类
let Parent = {
name: "parent4",
friends: ["p1", "p2", "p3"],
getName: function () {
return this.name;
}
};
//通过create的流程使person1.__proto__指向了Parent本身(注意是本身而不是prototype)
// F.prototype = Parent4
// person1.__proto__ = F.prototype
let person1 = Object.create(Parent);
person1.name = "tom";
person1.friends.push("jerry");
let person2 = Object.create(Parent);
person2.friends.push("lucy");
console.log(person1.name); // tom
console.log(person1.name === person2.getName()); // true
console.log(person2.name); // parent4
console.log(person1.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person2.friends); // ["p1", "p2", "p3","jerry","lucy"]
缺点:Object.create方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存。
寄生式继承
和原型式继承相同,只是把create部分放进函数中。
let parent = {
name: "parent5",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
function clone(original) {
let clone = Object.create(original);
clone.getFriends = function() {
return this.friends;
};
return clone;
}
let person5 = clone(parent);
console.log(person5.getName()); // parent5
console.log(person5.getFriends()); // ["p1", "p2", "p3"]
寄生组合式继承
function clone(parent, child) {
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
// 与上述代码不同的是此处是child.prototype.__proto__指向parent.prototype(1)
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}
function Parent() {
this.name = 'parent6';
this.play = [1, 2, 3];
}
Parent.prototype.getName = function () {
return this.name;
}
function Child() {
Parent.call(this);
this.friends = 'child5';
}
clone(Parent, Child);
Child.prototype.getFriends = function () {
return this.friends;
}
//new后person6.__proto__ == Child.prototype
//又因为(1)所以可以通过原型链,Child.prototype.__proto__查找parent.prototype
let person6 = new Child();
console.log(person6); //{friends:"child5",name:"child5",play:[1,2,3],__proto__:Parent}
console.log(person6.getName()); // parent
console.log(person6.getFriends()); // child5
感兴趣的也可以使用babel转换ES6代码,思路与寄生组合式继承基本相同。
总结
要真正学会继承得要学会
- new操作符的实现原理
- 原型链的思想(可以通过instanceof实现来理解)
- Object.create的实现原理
在以上的基础上理清原型的流动即可理解原型、原型链、继承这几个知识点。